Compare commits

..

No commits in common. "44a35aa5c2116bf0660946e42ef59474396243dc" and "88679cda2f477ce3034cd9bdad1d9d4e19f7cbc7" have entirely different histories.

66 changed files with 810 additions and 4941 deletions

View File

@ -26,7 +26,7 @@ Welcome to JetKVM development! This guide will help you get started quickly, whe
- **[Git](https://git-scm.com/downloads)** for version control
- **[SSH access](https://jetkvm.com/docs/advanced-usage/developing#developer-mode)** to your JetKVM device
- **Audio build dependencies:**
- **New:** The audio system uses a dual-subprocess architecture with CGO, ALSA, and Opus integration. You must run the provided scripts in `tools/` to set up the cross-compiler and build static ALSA/Opus libraries for ARM. See below.
- **New in this release:** The audio pipeline is now fully in-process using CGO, ALSA, and Opus. You must run the provided scripts in `tools/` to set up the cross-compiler and build static ALSA/Opus libraries for ARM. See below.
### Development Environment
@ -71,7 +71,7 @@ This ensures compatibility with shell scripts and build tools used in the projec
# This will run tools/setup_rv1106_toolchain.sh and tools/build_audio_deps.sh
# It will clone the cross-compiler and build ALSA/Opus static libs in $HOME/.jetkvm
#
# **Note:** This is required for the audio subprocess architecture. If you skip this step, builds will not succeed.
# **Note:** This is required for the new in-process audio pipeline. If you skip this step, audio will not work.
```
4. **Find your JetKVM IP address** (check your router or device screen)
@ -83,7 +83,7 @@ This ensures compatibility with shell scripts and build tools used in the projec
6. **Open in browser:** `http://192.168.1.100`
That's it! You're now running your own development version of JetKVM, **with bidirectional audio streaming using the dual-subprocess architecture.**
That's it! You're now running your own development version of JetKVM, **with in-process audio streaming for the first time.**
---
@ -135,14 +135,14 @@ tail -f /var/log/jetkvm.log
│ ├── src/routes/ # Pages (login, settings, etc.)
│ └── src/components/ # UI components
├── internal/ # Internal Go packages
│ └── audio/ # Dual-subprocess audio architecture (CGO, ALSA, Opus) [NEW]
│ └── audio/ # In-process audio pipeline (CGO, ALSA, Opus) [NEW]
├── tools/ # Toolchain and audio dependency setup scripts
└── Makefile # Build and dev automation (see audio targets)
```
**Key files for beginners:**
- `internal/audio/` - [NEW] Dual-subprocess audio architecture (CGO, ALSA, Opus)
- `internal/audio/` - [NEW] In-process audio pipeline (CGO, ALSA, Opus)
- `web.go` - Add new API endpoints here
- `config.go` - Add new settings here
- `ui/src/routes/` - Add new pages here
@ -174,7 +174,7 @@ npm install
### Quick Backend Changes
*Best for: API, backend, or audio logic changes (including audio subprocess architecture)*
*Best for: API, backend, or audio logic changes (including audio pipeline)*
```bash
# Skip frontend build for faster deployment
@ -353,7 +353,7 @@ go clean -modcache
go mod tidy
make build_dev
# If you see errors about missing ALSA/Opus or toolchain, run:
make dev_env # Required for audio subprocess architecture
make dev_env # Required for new audio support
```
### "Can't connect to device"

View File

@ -34,7 +34,7 @@ OPTIM_CFLAGS := -O3 -mcpu=cortex-a7 -mfpu=neon -mfloat-abi=hard -ftree-vectorize
PROMETHEUS_TAG := github.com/prometheus/common/version
KVM_PKG_NAME := github.com/jetkvm/kvm
GO_BUILD_ARGS := -tags netgo -tags timetzdata
GO_BUILD_ARGS := -tags netgo
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
GO_LDFLAGS := \
-s -w \

View File

@ -22,7 +22,7 @@ JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse, *
## Features
- **Ultra-low Latency** - 1080p@60FPS video with 30-60ms latency using H.264 encoding. Smooth mouse, keyboard, and audio for responsive remote control.
- **First-Class Audio Support** - JetKVM now supports bidirectional, low-latency audio streaming using a dual-subprocess architecture with ALSA and Opus integration via CGO. Features both audio output (PC→Browser) and audio input (Browser→PC) with dedicated subprocesses for optimal performance and isolation.
- **First-Class Audio Support** - JetKVM now supports in-process, low-latency audio streaming using ALSA and Opus, fully integrated via CGO. No external audio binaries or IPC required—audio is delivered directly from the device to your browser.
- **Free & Optional Remote Access** - Remote management via JetKVM Cloud using WebRTC.
- **Open-source software** - Written in Golang (with CGO for audio) on Linux. Easily customizable through SSH access to the JetKVM device.
@ -42,7 +42,7 @@ If you've found an issue and want to report it, please check our [Issues](https:
# Development
JetKVM is written in Go & TypeScript, with some C for low-level integration. **Audio support uses a sophisticated dual-subprocess architecture with CGO, ALSA, and Opus integration for bidirectional streaming with complete process isolation.**
JetKVM is written in Go & TypeScript, with some C for low-level integration. **Audio support is now fully in-process using CGO, ALSA, and Opus—no external audio binaries required.**
The project contains two main parts: the backend software (Go, CGO) that runs on the KVM device, and the frontend software (React/TypeScript) that is served by the KVM device and the cloud.
@ -53,7 +53,7 @@ For quick device development, use the `./dev_deploy.sh` script. It will build th
## Backend
The backend is written in Go and is responsible for KVM device management, audio/video streaming, the cloud API, and the cloud web. **Audio uses dedicated subprocesses for both output and input streams, with CGO-based ALSA and Opus processing, IPC communication via Unix sockets, and comprehensive process supervision for reliability.**
The backend is written in Go and is responsible for KVM device management, audio/video streaming, the cloud API, and the cloud web. **Audio is now captured and encoded in-process using ALSA and Opus via CGO, with no external processes or IPC.**
## Frontend

View File

@ -82,7 +82,6 @@ type Config struct {
CloudToken string `json:"cloud_token"`
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
JigglerConfig *JigglerConfig `json:"jiggler_config"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
@ -118,14 +117,7 @@ var defaultConfig = &Config{
DisplayMaxBrightness: 64,
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes
// This is the "Standard" jiggler option in the UI
JigglerConfig: &JigglerConfig{
InactivityLimitSeconds: 60,
JitterPercentage: 25,
ScheduleCronTab: "0 * * * * *",
Timezone: "UTC",
},
TLSMode: "",
TLSMode: "",
UsbConfig: &usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget

9
go.mod
View File

@ -11,7 +11,6 @@ require (
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/logger v1.2.6
github.com/gin-gonic/gin v1.10.1
github.com/go-co-op/gocron/v2 v2.16.3
github.com/google/uuid v1.6.0
github.com/guregu/null/v6 v6.0.0
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341
@ -29,9 +28,9 @@ require (
github.com/stretchr/testify v1.10.0
github.com/vishvananda/netlink v1.3.1
go.bug.st/serial v1.6.4
golang.org/x/crypto v0.40.0
golang.org/x/crypto v0.39.0
golang.org/x/net v0.41.0
golang.org/x/sys v0.34.0
golang.org/x/sys v0.33.0
)
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
@ -51,7 +50,6 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@ -77,7 +75,6 @@ require (
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
@ -85,7 +82,7 @@ require (
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

20
go.sum
View File

@ -38,8 +38,6 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-co-op/gocron/v2 v2.16.3 h1:kYqukZqBa8RC2+AFAHnunmKcs9GRTjwBo8WRF3I6cbI=
github.com/go-co-op/gocron/v2 v2.16.3/go.mod h1:aTf7/+5Jo2E+cyAqq625UQ6DzpkV96b22VHIUAt6l3c=
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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -64,8 +62,6 @@ github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoN
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@ -150,8 +146,6 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
@ -181,12 +175,10 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
@ -196,10 +188,10 @@ 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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -1,338 +0,0 @@
package audio
import (
"context"
"math"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// AdaptiveBufferConfig holds configuration for adaptive buffer sizing
type AdaptiveBufferConfig struct {
// Buffer size limits (in frames)
MinBufferSize int
MaxBufferSize int
DefaultBufferSize int
// System load thresholds
LowCPUThreshold float64 // Below this, increase buffer size
HighCPUThreshold float64 // Above this, decrease buffer size
LowMemoryThreshold float64 // Below this, increase buffer size
HighMemoryThreshold float64 // Above this, decrease buffer size
// Latency thresholds (in milliseconds)
TargetLatency time.Duration
MaxLatency time.Duration
// Adaptation parameters
AdaptationInterval time.Duration
SmoothingFactor float64 // 0.0-1.0, higher = more responsive
}
// DefaultAdaptiveBufferConfig returns optimized config for JetKVM hardware
func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
return AdaptiveBufferConfig{
// Conservative buffer sizes for 256MB RAM constraint
MinBufferSize: 3, // Minimum 3 frames (slightly higher for stability)
MaxBufferSize: 20, // Maximum 20 frames (increased for high load scenarios)
DefaultBufferSize: 6, // Default 6 frames (increased for better stability)
// CPU thresholds optimized for single-core ARM Cortex A7 under load
LowCPUThreshold: 20.0, // Below 20% CPU
HighCPUThreshold: 60.0, // Above 60% CPU (lowered to be more responsive)
// Memory thresholds for 256MB total RAM
LowMemoryThreshold: 35.0, // Below 35% memory usage
HighMemoryThreshold: 75.0, // Above 75% memory usage (lowered for earlier response)
// Latency targets
TargetLatency: 20 * time.Millisecond, // Target 20ms latency
MaxLatency: 50 * time.Millisecond, // Max acceptable 50ms
// Adaptation settings
AdaptationInterval: 500 * time.Millisecond, // Check every 500ms
SmoothingFactor: 0.3, // Moderate responsiveness
}
}
// AdaptiveBufferManager manages dynamic buffer sizing based on system conditions
type AdaptiveBufferManager struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentInputBufferSize int64 // Current input buffer size (atomic)
currentOutputBufferSize int64 // Current output buffer size (atomic)
averageLatency int64 // Average latency in nanoseconds (atomic)
systemCPUPercent int64 // System CPU percentage * 100 (atomic)
systemMemoryPercent int64 // System memory percentage * 100 (atomic)
adaptationCount int64 // Metrics tracking (atomic)
config AdaptiveBufferConfig
logger zerolog.Logger
processMonitor *ProcessMonitor
// Control channels
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// Metrics tracking
lastAdaptation time.Time
mutex sync.RWMutex
}
// NewAdaptiveBufferManager creates a new adaptive buffer manager
func NewAdaptiveBufferManager(config AdaptiveBufferConfig) *AdaptiveBufferManager {
ctx, cancel := context.WithCancel(context.Background())
return &AdaptiveBufferManager{
currentInputBufferSize: int64(config.DefaultBufferSize),
currentOutputBufferSize: int64(config.DefaultBufferSize),
config: config,
logger: logging.GetDefaultLogger().With().Str("component", "adaptive-buffer").Logger(),
processMonitor: GetProcessMonitor(),
ctx: ctx,
cancel: cancel,
lastAdaptation: time.Now(),
}
}
// Start begins the adaptive buffer management
func (abm *AdaptiveBufferManager) Start() {
abm.wg.Add(1)
go abm.adaptationLoop()
abm.logger.Info().Msg("Adaptive buffer manager started")
}
// Stop stops the adaptive buffer management
func (abm *AdaptiveBufferManager) Stop() {
abm.cancel()
abm.wg.Wait()
abm.logger.Info().Msg("Adaptive buffer manager stopped")
}
// GetInputBufferSize returns the current recommended input buffer size
func (abm *AdaptiveBufferManager) GetInputBufferSize() int {
return int(atomic.LoadInt64(&abm.currentInputBufferSize))
}
// GetOutputBufferSize returns the current recommended output buffer size
func (abm *AdaptiveBufferManager) GetOutputBufferSize() int {
return int(atomic.LoadInt64(&abm.currentOutputBufferSize))
}
// UpdateLatency updates the current latency measurement
func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) {
// Use exponential moving average for latency
currentAvg := atomic.LoadInt64(&abm.averageLatency)
newLatency := latency.Nanoseconds()
if currentAvg == 0 {
atomic.StoreInt64(&abm.averageLatency, newLatency)
} else {
// Exponential moving average: 70% historical, 30% current
newAvg := int64(float64(currentAvg)*0.7 + float64(newLatency)*0.3)
atomic.StoreInt64(&abm.averageLatency, newAvg)
}
}
// adaptationLoop is the main loop that adjusts buffer sizes
func (abm *AdaptiveBufferManager) adaptationLoop() {
defer abm.wg.Done()
ticker := time.NewTicker(abm.config.AdaptationInterval)
defer ticker.Stop()
for {
select {
case <-abm.ctx.Done():
return
case <-ticker.C:
abm.adaptBufferSizes()
}
}
}
// adaptBufferSizes analyzes system conditions and adjusts buffer sizes
func (abm *AdaptiveBufferManager) adaptBufferSizes() {
// Collect current system metrics
metrics := abm.processMonitor.GetCurrentMetrics()
if len(metrics) == 0 {
return // No metrics available
}
// Calculate system-wide CPU and memory usage
totalCPU := 0.0
totalMemory := 0.0
processCount := 0
for _, metric := range metrics {
totalCPU += metric.CPUPercent
totalMemory += metric.MemoryPercent
processCount++
}
if processCount == 0 {
return
}
// Store system metrics atomically
systemCPU := totalCPU // Total CPU across all monitored processes
systemMemory := totalMemory / float64(processCount) // Average memory usage
atomic.StoreInt64(&abm.systemCPUPercent, int64(systemCPU*100))
atomic.StoreInt64(&abm.systemMemoryPercent, int64(systemMemory*100))
// Get current latency
currentLatencyNs := atomic.LoadInt64(&abm.averageLatency)
currentLatency := time.Duration(currentLatencyNs)
// Calculate adaptation factors
cpuFactor := abm.calculateCPUFactor(systemCPU)
memoryFactor := abm.calculateMemoryFactor(systemMemory)
latencyFactor := abm.calculateLatencyFactor(currentLatency)
// Combine factors with weights (CPU has highest priority for KVM coexistence)
combinedFactor := 0.5*cpuFactor + 0.3*memoryFactor + 0.2*latencyFactor
// Apply adaptation with smoothing
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
currentOutput := float64(atomic.LoadInt64(&abm.currentOutputBufferSize))
// Calculate new buffer sizes
newInputSize := abm.applyAdaptation(currentInput, combinedFactor)
newOutputSize := abm.applyAdaptation(currentOutput, combinedFactor)
// Update buffer sizes if they changed significantly
adjustmentMade := false
if math.Abs(newInputSize-currentInput) >= 0.5 || math.Abs(newOutputSize-currentOutput) >= 0.5 {
atomic.StoreInt64(&abm.currentInputBufferSize, int64(math.Round(newInputSize)))
atomic.StoreInt64(&abm.currentOutputBufferSize, int64(math.Round(newOutputSize)))
atomic.AddInt64(&abm.adaptationCount, 1)
abm.mutex.Lock()
abm.lastAdaptation = time.Now()
abm.mutex.Unlock()
adjustmentMade = true
abm.logger.Debug().
Float64("cpu_percent", systemCPU).
Float64("memory_percent", systemMemory).
Dur("latency", currentLatency).
Float64("combined_factor", combinedFactor).
Int("new_input_size", int(newInputSize)).
Int("new_output_size", int(newOutputSize)).
Msg("Adapted buffer sizes")
}
// Update metrics with current state
currentInputSize := int(atomic.LoadInt64(&abm.currentInputBufferSize))
currentOutputSize := int(atomic.LoadInt64(&abm.currentOutputBufferSize))
UpdateAdaptiveBufferMetrics(currentInputSize, currentOutputSize, systemCPU, systemMemory, adjustmentMade)
}
// calculateCPUFactor returns adaptation factor based on CPU usage
// Returns: -1.0 (decrease buffers) to +1.0 (increase buffers)
func (abm *AdaptiveBufferManager) calculateCPUFactor(cpuPercent float64) float64 {
if cpuPercent > abm.config.HighCPUThreshold {
// High CPU: decrease buffers to reduce latency and give CPU to KVM
return -1.0
} else if cpuPercent < abm.config.LowCPUThreshold {
// Low CPU: increase buffers for better quality
return 1.0
}
// Medium CPU: linear interpolation
midpoint := (abm.config.HighCPUThreshold + abm.config.LowCPUThreshold) / 2
return (midpoint - cpuPercent) / (midpoint - abm.config.LowCPUThreshold)
}
// calculateMemoryFactor returns adaptation factor based on memory usage
func (abm *AdaptiveBufferManager) calculateMemoryFactor(memoryPercent float64) float64 {
if memoryPercent > abm.config.HighMemoryThreshold {
// High memory: decrease buffers to free memory
return -1.0
} else if memoryPercent < abm.config.LowMemoryThreshold {
// Low memory: increase buffers for better performance
return 1.0
}
// Medium memory: linear interpolation
midpoint := (abm.config.HighMemoryThreshold + abm.config.LowMemoryThreshold) / 2
return (midpoint - memoryPercent) / (midpoint - abm.config.LowMemoryThreshold)
}
// calculateLatencyFactor returns adaptation factor based on latency
func (abm *AdaptiveBufferManager) calculateLatencyFactor(latency time.Duration) float64 {
if latency > abm.config.MaxLatency {
// High latency: decrease buffers
return -1.0
} else if latency < abm.config.TargetLatency {
// Low latency: can increase buffers
return 1.0
}
// Medium latency: linear interpolation
midLatency := (abm.config.MaxLatency + abm.config.TargetLatency) / 2
return float64(midLatency-latency) / float64(midLatency-abm.config.TargetLatency)
}
// applyAdaptation applies the adaptation factor to current buffer size
func (abm *AdaptiveBufferManager) applyAdaptation(currentSize, factor float64) float64 {
// Calculate target size based on factor
var targetSize float64
if factor > 0 {
// Increase towards max
targetSize = currentSize + factor*(float64(abm.config.MaxBufferSize)-currentSize)
} else {
// Decrease towards min
targetSize = currentSize + factor*(currentSize-float64(abm.config.MinBufferSize))
}
// Apply smoothing
newSize := currentSize + abm.config.SmoothingFactor*(targetSize-currentSize)
// Clamp to valid range
return math.Max(float64(abm.config.MinBufferSize),
math.Min(float64(abm.config.MaxBufferSize), newSize))
}
// GetStats returns current adaptation statistics
func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} {
abm.mutex.RLock()
lastAdaptation := abm.lastAdaptation
abm.mutex.RUnlock()
return map[string]interface{}{
"input_buffer_size": abm.GetInputBufferSize(),
"output_buffer_size": abm.GetOutputBufferSize(),
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / 100,
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / 100,
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
"last_adaptation": lastAdaptation,
}
}
// Global adaptive buffer manager instance
var globalAdaptiveBufferManager *AdaptiveBufferManager
var adaptiveBufferOnce sync.Once
// GetAdaptiveBufferManager returns the global adaptive buffer manager instance
func GetAdaptiveBufferManager() *AdaptiveBufferManager {
adaptiveBufferOnce.Do(func() {
globalAdaptiveBufferManager = NewAdaptiveBufferManager(DefaultAdaptiveBufferConfig())
})
return globalAdaptiveBufferManager
}
// StartAdaptiveBuffering starts the global adaptive buffer manager
func StartAdaptiveBuffering() {
GetAdaptiveBufferManager().Start()
}
// StopAdaptiveBuffering stops the global adaptive buffer manager
func StopAdaptiveBuffering() {
if globalAdaptiveBufferManager != nil {
globalAdaptiveBufferManager.Stop()
}
}

View File

@ -1,198 +0,0 @@
package audio
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
)
// AdaptiveOptimizer automatically adjusts audio parameters based on latency metrics
type AdaptiveOptimizer struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
optimizationCount int64 // Number of optimizations performed (atomic)
lastOptimization int64 // Timestamp of last optimization (atomic)
optimizationLevel int64 // Current optimization level (0-10) (atomic)
latencyMonitor *LatencyMonitor
bufferManager *AdaptiveBufferManager
logger zerolog.Logger
// Control channels
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// Configuration
config OptimizerConfig
}
// OptimizerConfig holds configuration for the adaptive optimizer
type OptimizerConfig struct {
MaxOptimizationLevel int // Maximum optimization level (0-10)
CooldownPeriod time.Duration // Minimum time between optimizations
Aggressiveness float64 // How aggressively to optimize (0.0-1.0)
RollbackThreshold time.Duration // Latency threshold to rollback optimizations
StabilityPeriod time.Duration // Time to wait for stability after optimization
}
// DefaultOptimizerConfig returns a sensible default configuration
func DefaultOptimizerConfig() OptimizerConfig {
return OptimizerConfig{
MaxOptimizationLevel: 8,
CooldownPeriod: 30 * time.Second,
Aggressiveness: 0.7,
RollbackThreshold: 300 * time.Millisecond,
StabilityPeriod: 10 * time.Second,
}
}
// NewAdaptiveOptimizer creates a new adaptive optimizer
func NewAdaptiveOptimizer(latencyMonitor *LatencyMonitor, bufferManager *AdaptiveBufferManager, config OptimizerConfig, logger zerolog.Logger) *AdaptiveOptimizer {
ctx, cancel := context.WithCancel(context.Background())
optimizer := &AdaptiveOptimizer{
latencyMonitor: latencyMonitor,
bufferManager: bufferManager,
config: config,
logger: logger.With().Str("component", "adaptive-optimizer").Logger(),
ctx: ctx,
cancel: cancel,
}
// Register as latency monitor callback
latencyMonitor.AddOptimizationCallback(optimizer.handleLatencyOptimization)
return optimizer
}
// Start begins the adaptive optimization process
func (ao *AdaptiveOptimizer) Start() {
ao.wg.Add(1)
go ao.optimizationLoop()
ao.logger.Info().Msg("Adaptive optimizer started")
}
// Stop stops the adaptive optimizer
func (ao *AdaptiveOptimizer) Stop() {
ao.cancel()
ao.wg.Wait()
ao.logger.Info().Msg("Adaptive optimizer stopped")
}
// initializeStrategies sets up the available optimization strategies
// handleLatencyOptimization is called when latency optimization is needed
func (ao *AdaptiveOptimizer) handleLatencyOptimization(metrics LatencyMetrics) error {
currentLevel := atomic.LoadInt64(&ao.optimizationLevel)
lastOpt := atomic.LoadInt64(&ao.lastOptimization)
// Check cooldown period
if time.Since(time.Unix(0, lastOpt)) < ao.config.CooldownPeriod {
return nil
}
// Determine if we need to increase or decrease optimization level
targetLevel := ao.calculateTargetOptimizationLevel(metrics)
if targetLevel > currentLevel {
return ao.increaseOptimization(int(targetLevel))
} else if targetLevel < currentLevel {
return ao.decreaseOptimization(int(targetLevel))
}
return nil
}
// calculateTargetOptimizationLevel determines the appropriate optimization level
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
// Base calculation on current latency vs target
latencyRatio := float64(metrics.Current) / float64(50*time.Millisecond) // 50ms target
// Adjust based on trend
switch metrics.Trend {
case LatencyTrendIncreasing:
latencyRatio *= 1.2 // Be more aggressive
case LatencyTrendDecreasing:
latencyRatio *= 0.8 // Be less aggressive
case LatencyTrendVolatile:
latencyRatio *= 1.1 // Slightly more aggressive
}
// Apply aggressiveness factor
latencyRatio *= ao.config.Aggressiveness
// Convert to optimization level
targetLevel := int64(latencyRatio * 2) // Scale to 0-10 range
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
targetLevel = int64(ao.config.MaxOptimizationLevel)
}
if targetLevel < 0 {
targetLevel = 0
}
return targetLevel
}
// increaseOptimization applies optimization strategies up to the target level
func (ao *AdaptiveOptimizer) increaseOptimization(targetLevel int) error {
atomic.StoreInt64(&ao.optimizationLevel, int64(targetLevel))
atomic.StoreInt64(&ao.lastOptimization, time.Now().UnixNano())
atomic.AddInt64(&ao.optimizationCount, 1)
return nil
}
// decreaseOptimization rolls back optimization strategies to the target level
func (ao *AdaptiveOptimizer) decreaseOptimization(targetLevel int) error {
atomic.StoreInt64(&ao.optimizationLevel, int64(targetLevel))
atomic.StoreInt64(&ao.lastOptimization, time.Now().UnixNano())
return nil
}
// optimizationLoop runs the main optimization monitoring loop
func (ao *AdaptiveOptimizer) optimizationLoop() {
defer ao.wg.Done()
ticker := time.NewTicker(ao.config.StabilityPeriod)
defer ticker.Stop()
for {
select {
case <-ao.ctx.Done():
return
case <-ticker.C:
ao.checkStability()
}
}
}
// checkStability monitors system stability and rolls back if needed
func (ao *AdaptiveOptimizer) checkStability() {
metrics := ao.latencyMonitor.GetMetrics()
// Check if we need to rollback due to excessive latency
if metrics.Current > ao.config.RollbackThreshold {
currentLevel := int(atomic.LoadInt64(&ao.optimizationLevel))
if currentLevel > 0 {
ao.logger.Warn().Dur("current_latency", metrics.Current).Dur("threshold", ao.config.RollbackThreshold).Msg("Rolling back optimizations due to excessive latency")
if err := ao.decreaseOptimization(currentLevel - 1); err != nil {
ao.logger.Error().Err(err).Msg("Failed to decrease optimization level")
}
}
}
}
// GetOptimizationStats returns current optimization statistics
func (ao *AdaptiveOptimizer) GetOptimizationStats() map[string]interface{} {
return map[string]interface{}{
"optimization_level": atomic.LoadInt64(&ao.optimizationLevel),
"optimization_count": atomic.LoadInt64(&ao.optimizationCount),
"last_optimization": time.Unix(0, atomic.LoadInt64(&ao.lastOptimization)),
}
}
// Strategy implementation methods (stubs for now)

View File

@ -199,16 +199,7 @@ func (bap *BatchAudioProcessor) processBatchRead(batch []batchReadRequest) {
start := time.Now()
if atomic.CompareAndSwapInt32(&bap.threadPinned, 0, 1) {
runtime.LockOSThread()
// Set high priority for batch audio processing
if err := SetAudioThreadPriority(); err != nil {
bap.logger.Warn().Err(err).Msg("Failed to set batch audio processing priority")
}
defer func() {
if err := ResetThreadPriority(); err != nil {
bap.logger.Warn().Err(err).Msg("Failed to reset thread priority")
}
runtime.UnlockOSThread()
atomic.StoreInt32(&bap.threadPinned, 0)
bap.stats.OSThreadPinTime += time.Since(start)

View File

@ -2,41 +2,16 @@ package audio
import (
"sync"
"sync/atomic"
)
type AudioBufferPool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentSize int64 // Current pool size (atomic)
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
// Other fields
pool sync.Pool
bufferSize int
maxPoolSize int
mutex sync.RWMutex
// Memory optimization fields
preallocated []*[]byte // Pre-allocated buffers for immediate use
preallocSize int // Number of pre-allocated buffers
pool sync.Pool
bufferSize int
}
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Pre-allocate 20% of max pool size for immediate availability
preallocSize := 20
preallocated := make([]*[]byte, 0, preallocSize)
// Pre-allocate buffers to reduce initial allocation overhead
for i := 0; i < preallocSize; i++ {
buf := make([]byte, 0, bufferSize)
preallocated = append(preallocated, &buf)
}
return &AudioBufferPool{
bufferSize: bufferSize,
maxPoolSize: 100, // Limit pool size to prevent excessive memory usage
preallocated: preallocated,
preallocSize: preallocSize,
bufferSize: bufferSize,
pool: sync.Pool{
New: func() interface{} {
return make([]byte, 0, bufferSize)
@ -46,68 +21,17 @@ func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
}
func (p *AudioBufferPool) Get() []byte {
// First try pre-allocated buffers for fastest access
p.mutex.Lock()
if len(p.preallocated) > 0 {
buf := p.preallocated[len(p.preallocated)-1]
p.preallocated = p.preallocated[:len(p.preallocated)-1]
p.mutex.Unlock()
atomic.AddInt64(&p.hitCount, 1)
return (*buf)[:0] // Reset length but keep capacity
}
p.mutex.Unlock()
// Try sync.Pool next
if buf := p.pool.Get(); buf != nil {
bufPtr := buf.(*[]byte)
// Update pool size counter when retrieving from pool
p.mutex.Lock()
if p.currentSize > 0 {
p.currentSize--
}
p.mutex.Unlock()
atomic.AddInt64(&p.hitCount, 1)
return (*bufPtr)[:0] // Reset length but keep capacity
return *buf.(*[]byte)
}
// Last resort: allocate new buffer
atomic.AddInt64(&p.missCount, 1)
return make([]byte, 0, p.bufferSize)
}
func (p *AudioBufferPool) Put(buf []byte) {
if cap(buf) < p.bufferSize {
return // Buffer too small, don't pool it
if cap(buf) >= p.bufferSize {
resetBuf := buf[:0]
p.pool.Put(&resetBuf)
}
// Reset buffer for reuse
resetBuf := buf[:0]
// First try to return to pre-allocated pool for fastest reuse
p.mutex.Lock()
if len(p.preallocated) < p.preallocSize {
p.preallocated = append(p.preallocated, &resetBuf)
p.mutex.Unlock()
return
}
p.mutex.Unlock()
// Check sync.Pool size limit to prevent excessive memory usage
p.mutex.RLock()
currentSize := p.currentSize
p.mutex.RUnlock()
if currentSize >= int64(p.maxPoolSize) {
return // Pool is full, let GC handle this buffer
}
// Return to sync.Pool
p.pool.Put(&resetBuf)
// Update pool size counter
p.mutex.Lock()
p.currentSize++
p.mutex.Unlock()
}
var (
@ -130,83 +54,3 @@ func GetAudioControlBuffer() []byte {
func PutAudioControlBuffer(buf []byte) {
audioControlPool.Put(buf)
}
// GetPoolStats returns detailed statistics about this buffer pool
func (p *AudioBufferPool) GetPoolStats() AudioBufferPoolDetailedStats {
p.mutex.RLock()
preallocatedCount := len(p.preallocated)
currentSize := p.currentSize
p.mutex.RUnlock()
hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount)
totalRequests := hitCount + missCount
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * 100
}
return AudioBufferPoolDetailedStats{
BufferSize: p.bufferSize,
MaxPoolSize: p.maxPoolSize,
CurrentPoolSize: currentSize,
PreallocatedCount: int64(preallocatedCount),
PreallocatedMax: int64(p.preallocSize),
HitCount: hitCount,
MissCount: missCount,
HitRate: hitRate,
}
}
// AudioBufferPoolDetailedStats provides detailed pool statistics
type AudioBufferPoolDetailedStats struct {
BufferSize int
MaxPoolSize int
CurrentPoolSize int64
PreallocatedCount int64
PreallocatedMax int64
HitCount int64
MissCount int64
HitRate float64 // Percentage
}
// GetAudioBufferPoolStats returns statistics about the audio buffer pools
type AudioBufferPoolStats struct {
FramePoolSize int64
FramePoolMax int
ControlPoolSize int64
ControlPoolMax int
// Enhanced statistics
FramePoolHitRate float64
ControlPoolHitRate float64
FramePoolDetails AudioBufferPoolDetailedStats
ControlPoolDetails AudioBufferPoolDetailedStats
}
func GetAudioBufferPoolStats() AudioBufferPoolStats {
audioFramePool.mutex.RLock()
frameSize := audioFramePool.currentSize
frameMax := audioFramePool.maxPoolSize
audioFramePool.mutex.RUnlock()
audioControlPool.mutex.RLock()
controlSize := audioControlPool.currentSize
controlMax := audioControlPool.maxPoolSize
audioControlPool.mutex.RUnlock()
// Get detailed statistics
frameDetails := audioFramePool.GetPoolStats()
controlDetails := audioControlPool.GetPoolStats()
return AudioBufferPoolStats{
FramePoolSize: frameSize,
FramePoolMax: frameMax,
ControlPoolSize: controlSize,
ControlPoolMax: controlMax,
FramePoolHitRate: frameDetails.HitRate,
ControlPoolHitRate: controlDetails.HitRate,
FramePoolDetails: frameDetails,
ControlPoolDetails: controlDetails,
}
}

View File

@ -22,14 +22,8 @@ static snd_pcm_t *pcm_handle = NULL;
static snd_pcm_t *pcm_playback_handle = NULL;
static OpusEncoder *encoder = NULL;
static OpusDecoder *decoder = NULL;
// Optimized Opus encoder settings for ARM Cortex-A7
static int opus_bitrate = 96000; // Increased for better quality
static int opus_complexity = 3; // Reduced for ARM performance
static int opus_vbr = 1; // Variable bitrate enabled
static int opus_vbr_constraint = 1; // Constrained VBR for consistent latency
static int opus_signal_type = OPUS_SIGNAL_MUSIC; // Optimized for general audio
static int opus_bandwidth = OPUS_BANDWIDTH_FULLBAND; // Full bandwidth
static int opus_dtx = 0; // Disable DTX for real-time audio
static int opus_bitrate = 64000;
static int opus_complexity = 5;
static int sample_rate = 48000;
static int channels = 2;
static int frame_size = 960; // 20ms for 48kHz
@ -170,7 +164,7 @@ int jetkvm_audio_init() {
return -1;
}
// Initialize Opus encoder with optimized settings
// Initialize Opus encoder
int opus_err = 0;
encoder = opus_encoder_create(sample_rate, channels, OPUS_APPLICATION_AUDIO, &opus_err);
if (!encoder || opus_err != OPUS_OK) {
@ -179,18 +173,8 @@ int jetkvm_audio_init() {
return -2;
}
// Apply optimized Opus encoder settings
opus_encoder_ctl(encoder, OPUS_SET_BITRATE(opus_bitrate));
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(opus_complexity));
opus_encoder_ctl(encoder, OPUS_SET_VBR(opus_vbr));
opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(opus_vbr_constraint));
opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(opus_signal_type));
opus_encoder_ctl(encoder, OPUS_SET_BANDWIDTH(opus_bandwidth));
opus_encoder_ctl(encoder, OPUS_SET_DTX(opus_dtx));
// Enable packet loss concealment for better resilience
opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(5));
// Set prediction disabled for lower latency
opus_encoder_ctl(encoder, OPUS_SET_PREDICTION_DISABLED(1));
capture_initialized = 1;
capture_initializing = 0;

View File

@ -99,42 +99,6 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
return nil
}
// WriteOpusFrameZeroCopy writes an Opus frame using zero-copy optimization
func (aim *AudioInputManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
if !aim.IsRunning() {
return nil // Not running, silently drop
}
if frame == nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
return nil
}
// Track end-to-end latency from WebRTC to IPC
startTime := time.Now()
err := aim.ipcManager.WriteOpusFrameZeroCopy(frame)
processingTime := time.Since(startTime)
// Log high latency warnings
if processingTime > 10*time.Millisecond {
aim.logger.Warn().
Dur("latency_ms", processingTime).
Msg("High audio processing latency detected")
}
if err != nil {
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
return err
}
// Update metrics
atomic.AddInt64(&aim.metrics.FramesSent, 1)
atomic.AddInt64(&aim.metrics.BytesProcessed, int64(frame.Length()))
aim.metrics.LastFrameTime = time.Now()
aim.metrics.AverageLatency = processingTime
return nil
}
// GetMetrics returns current audio input metrics
func (aim *AudioInputManager) GetMetrics() AudioInputMetrics {
return AudioInputMetrics{

View File

@ -8,22 +8,17 @@ import (
"net"
"os"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
const (
inputMagicNumber uint32 = 0x4A4B4D49 // "JKMI" (JetKVM Microphone Input)
inputSocketName = "audio_input.sock"
maxFrameSize = 4096 // Maximum Opus frame size
writeTimeout = 15 * time.Millisecond // Non-blocking write timeout (increased for high load)
maxDroppedFrames = 100 // Maximum consecutive dropped frames before reconnect
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
messagePoolSize = 256 // Pre-allocated message pool size
maxFrameSize = 4096 // Maximum Opus frame size
writeTimeout = 5 * time.Millisecond // Non-blocking write timeout
maxDroppedFrames = 100 // Maximum consecutive dropped frames before reconnect
)
// InputMessageType represents the type of IPC message
@ -46,113 +41,6 @@ type InputIPCMessage struct {
Data []byte
}
// OptimizedIPCMessage represents an optimized message with pre-allocated buffers
type OptimizedIPCMessage struct {
header [headerSize]byte // Pre-allocated header buffer
data []byte // Reusable data buffer
msg InputIPCMessage // Embedded message
}
// MessagePool manages a pool of reusable messages to reduce allocations
type MessagePool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
// Other fields
pool chan *OptimizedIPCMessage
// Memory optimization fields
preallocated []*OptimizedIPCMessage // Pre-allocated messages for immediate use
preallocSize int // Number of pre-allocated messages
maxPoolSize int // Maximum pool size to prevent memory bloat
mutex sync.RWMutex // Protects preallocated slice
}
// Global message pool instance
var globalMessagePool = &MessagePool{
pool: make(chan *OptimizedIPCMessage, messagePoolSize),
}
var messagePoolInitOnce sync.Once
// initializeMessagePool initializes the message pool with pre-allocated messages
func initializeMessagePool() {
messagePoolInitOnce.Do(func() {
// Pre-allocate 30% of pool size for immediate availability
preallocSize := messagePoolSize * 30 / 100
globalMessagePool.preallocSize = preallocSize
globalMessagePool.maxPoolSize = messagePoolSize * 2 // Allow growth up to 2x
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
// Pre-allocate messages to reduce initial allocation overhead
for i := 0; i < preallocSize; i++ {
msg := &OptimizedIPCMessage{
data: make([]byte, 0, maxFrameSize),
}
globalMessagePool.preallocated = append(globalMessagePool.preallocated, msg)
}
// Fill the channel pool with remaining messages
for i := preallocSize; i < messagePoolSize; i++ {
globalMessagePool.pool <- &OptimizedIPCMessage{
data: make([]byte, 0, maxFrameSize),
}
}
})
}
// Get retrieves a message from the pool
func (mp *MessagePool) Get() *OptimizedIPCMessage {
initializeMessagePool()
// First try pre-allocated messages for fastest access
mp.mutex.Lock()
if len(mp.preallocated) > 0 {
msg := mp.preallocated[len(mp.preallocated)-1]
mp.preallocated = mp.preallocated[:len(mp.preallocated)-1]
mp.mutex.Unlock()
atomic.AddInt64(&mp.hitCount, 1)
return msg
}
mp.mutex.Unlock()
// Try channel pool next
select {
case msg := <-mp.pool:
atomic.AddInt64(&mp.hitCount, 1)
return msg
default:
// Pool exhausted, create new message
atomic.AddInt64(&mp.missCount, 1)
return &OptimizedIPCMessage{
data: make([]byte, 0, maxFrameSize),
}
}
}
// Put returns a message to the pool
func (mp *MessagePool) Put(msg *OptimizedIPCMessage) {
// Reset the message for reuse
msg.data = msg.data[:0]
msg.msg = InputIPCMessage{}
// First try to return to pre-allocated pool for fastest reuse
mp.mutex.Lock()
if len(mp.preallocated) < mp.preallocSize {
mp.preallocated = append(mp.preallocated, msg)
mp.mutex.Unlock()
return
}
mp.mutex.Unlock()
// Try channel pool next
select {
case mp.pool <- msg:
// Successfully returned to pool
default:
// Pool full, let GC handle it
}
}
// InputIPCConfig represents configuration for audio input
type InputIPCConfig struct {
SampleRate int
@ -178,9 +66,6 @@ type AudioInputServer struct {
processChan chan *InputIPCMessage // Buffered channel for processing queue
stopChan chan struct{} // Stop signal for all goroutines
wg sync.WaitGroup // Wait group for goroutine coordination
// Socket buffer configuration
socketBufferConfig SocketBufferConfig
}
// NewAudioInputServer creates a new audio input server
@ -194,20 +79,15 @@ func NewAudioInputServer() (*AudioInputServer, error) {
return nil, fmt.Errorf("failed to create unix socket: %w", err)
}
// Get initial buffer size from adaptive buffer manager
adaptiveManager := GetAdaptiveBufferManager()
initialBufferSize := int64(adaptiveManager.GetInputBufferSize())
// Initialize socket buffer configuration
socketBufferConfig := DefaultSocketBufferConfig()
// Initialize with adaptive buffer size (start with 1000 frames)
initialBufferSize := int64(1000)
return &AudioInputServer{
listener: listener,
messageChan: make(chan *InputIPCMessage, initialBufferSize),
processChan: make(chan *InputIPCMessage, initialBufferSize),
stopChan: make(chan struct{}),
bufferSize: initialBufferSize,
socketBufferConfig: socketBufferConfig,
listener: listener,
messageChan: make(chan *InputIPCMessage, initialBufferSize),
processChan: make(chan *InputIPCMessage, initialBufferSize),
stopChan: make(chan struct{}),
bufferSize: initialBufferSize,
}, nil
}
@ -277,16 +157,6 @@ func (ais *AudioInputServer) acceptConnections() {
return
}
// Configure socket buffers for optimal performance
if err := ConfigureSocketBuffers(conn, ais.socketBufferConfig); err != nil {
// Log warning but don't fail - socket buffer optimization is not critical
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Warn().Err(err).Msg("Failed to configure socket buffers, continuing with defaults")
} else {
// Record socket buffer metrics for monitoring
RecordSocketBufferMetrics(conn, "audio-input")
}
ais.mtx.Lock()
// Close existing connection if any
if ais.conn != nil {
@ -322,22 +192,21 @@ func (ais *AudioInputServer) handleConnection(conn net.Conn) {
// readMessage reads a complete message from the connection
func (ais *AudioInputServer) readMessage(conn net.Conn) (*InputIPCMessage, error) {
// Get optimized message from pool
optMsg := globalMessagePool.Get()
defer globalMessagePool.Put(optMsg)
// Read header (magic + type + length + timestamp)
headerSize := 4 + 1 + 4 + 8 // uint32 + uint8 + uint32 + int64
header := make([]byte, headerSize)
// Read header directly into pre-allocated buffer
_, err := io.ReadFull(conn, optMsg.header[:])
_, err := io.ReadFull(conn, header)
if err != nil {
return nil, err
}
// Parse header using optimized access
msg := &optMsg.msg
msg.Magic = binary.LittleEndian.Uint32(optMsg.header[0:4])
msg.Type = InputMessageType(optMsg.header[4])
msg.Length = binary.LittleEndian.Uint32(optMsg.header[5:9])
msg.Timestamp = int64(binary.LittleEndian.Uint64(optMsg.header[9:17]))
// Parse header
msg := &InputIPCMessage{}
msg.Magic = binary.LittleEndian.Uint32(header[0:4])
msg.Type = InputMessageType(header[4])
msg.Length = binary.LittleEndian.Uint32(header[5:9])
msg.Timestamp = int64(binary.LittleEndian.Uint64(header[9:17]))
// Validate magic number
if msg.Magic != inputMagicNumber {
@ -349,37 +218,16 @@ func (ais *AudioInputServer) readMessage(conn net.Conn) (*InputIPCMessage, error
return nil, fmt.Errorf("message too large: %d bytes", msg.Length)
}
// Read data if present using pooled buffer
// Read data if present
if msg.Length > 0 {
// Ensure buffer capacity
if cap(optMsg.data) < int(msg.Length) {
optMsg.data = make([]byte, msg.Length)
} else {
optMsg.data = optMsg.data[:msg.Length]
}
_, err = io.ReadFull(conn, optMsg.data)
msg.Data = make([]byte, msg.Length)
_, err = io.ReadFull(conn, msg.Data)
if err != nil {
return nil, err
}
msg.Data = optMsg.data
}
// Return a copy of the message (data will be copied by caller if needed)
result := &InputIPCMessage{
Magic: msg.Magic,
Type: msg.Type,
Length: msg.Length,
Timestamp: msg.Timestamp,
}
if msg.Length > 0 {
// Copy data to ensure it's not affected by buffer reuse
result.Data = make([]byte, msg.Length)
copy(result.Data, msg.Data)
}
return result, nil
return msg, nil
}
// processMessage processes a received message
@ -434,20 +282,19 @@ func (ais *AudioInputServer) sendAck() error {
return ais.writeMessage(ais.conn, msg)
}
// writeMessage writes a message to the connection using optimized buffers
// writeMessage writes a message to the connection
func (ais *AudioInputServer) writeMessage(conn net.Conn, msg *InputIPCMessage) error {
// Get optimized message from pool for header preparation
optMsg := globalMessagePool.Get()
defer globalMessagePool.Put(optMsg)
// Prepare header
headerSize := 4 + 1 + 4 + 8
header := make([]byte, headerSize)
// Prepare header in pre-allocated buffer
binary.LittleEndian.PutUint32(optMsg.header[0:4], msg.Magic)
optMsg.header[4] = byte(msg.Type)
binary.LittleEndian.PutUint32(optMsg.header[5:9], msg.Length)
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(msg.Timestamp))
binary.LittleEndian.PutUint32(header[0:4], msg.Magic)
header[4] = byte(msg.Type)
binary.LittleEndian.PutUint32(header[5:9], msg.Length)
binary.LittleEndian.PutUint64(header[9:17], uint64(msg.Timestamp))
// Write header
_, err := conn.Write(optMsg.header[:])
_, err := conn.Write(header)
if err != nil {
return err
}
@ -465,7 +312,7 @@ func (ais *AudioInputServer) writeMessage(conn net.Conn, msg *InputIPCMessage) e
// AudioInputClient handles IPC communication from the main process
type AudioInputClient struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
// Atomic fields must be first for proper alignment on ARM
droppedFrames int64 // Atomic counter for dropped frames
totalFrames int64 // Atomic counter for total frames
@ -563,35 +410,6 @@ func (aic *AudioInputClient) SendFrame(frame []byte) error {
return aic.writeMessage(msg)
}
// SendFrameZeroCopy sends a zero-copy Opus frame to the audio input server
func (aic *AudioInputClient) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
aic.mtx.Lock()
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil {
return fmt.Errorf("not connected")
}
if frame == nil || frame.Length() == 0 {
return nil // Empty frame, ignore
}
if frame.Length() > maxFrameSize {
return fmt.Errorf("frame too large: %d bytes", frame.Length())
}
// Use zero-copy data directly
msg := &InputIPCMessage{
Magic: inputMagicNumber,
Type: InputMessageTypeOpusFrame,
Length: uint32(frame.Length()),
Timestamp: time.Now().UnixNano(),
Data: frame.Data(), // Zero-copy data access
}
return aic.writeMessage(msg)
}
// SendConfig sends a configuration update to the audio input server
func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
aic.mtx.Lock()
@ -642,15 +460,14 @@ func (aic *AudioInputClient) writeMessage(msg *InputIPCMessage) error {
// Increment total frames counter
atomic.AddInt64(&aic.totalFrames, 1)
// Get optimized message from pool for header preparation
optMsg := globalMessagePool.Get()
defer globalMessagePool.Put(optMsg)
// Prepare header
headerSize := 4 + 1 + 4 + 8
header := make([]byte, headerSize)
// Prepare header in pre-allocated buffer
binary.LittleEndian.PutUint32(optMsg.header[0:4], msg.Magic)
optMsg.header[4] = byte(msg.Type)
binary.LittleEndian.PutUint32(optMsg.header[5:9], msg.Length)
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(msg.Timestamp))
binary.LittleEndian.PutUint32(header[0:4], msg.Magic)
header[4] = byte(msg.Type)
binary.LittleEndian.PutUint32(header[5:9], msg.Length)
binary.LittleEndian.PutUint64(header[9:17], uint64(msg.Timestamp))
// Use non-blocking write with timeout
ctx, cancel := context.WithTimeout(context.Background(), writeTimeout)
@ -659,8 +476,8 @@ func (aic *AudioInputClient) writeMessage(msg *InputIPCMessage) error {
// Create a channel to signal write completion
done := make(chan error, 1)
go func() {
// Write header using pre-allocated buffer
_, err := aic.conn.Write(optMsg.header[:])
// Write header
_, err := aic.conn.Write(header)
if err != nil {
done <- err
return
@ -753,20 +570,6 @@ func (ais *AudioInputServer) startReaderGoroutine() {
func (ais *AudioInputServer) startProcessorGoroutine() {
ais.wg.Add(1)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Set high priority for audio processing
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-processor").Logger()
if err := SetAudioThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to set audio processing priority")
}
defer func() {
if err := ResetThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to reset thread priority")
}
}()
defer ais.wg.Done()
for {
select {
@ -805,28 +608,10 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
func (ais *AudioInputServer) startMonitorGoroutine() {
ais.wg.Add(1)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Set I/O priority for monitoring
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-monitor").Logger()
if err := SetAudioIOThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to set audio I/O priority")
}
defer func() {
if err := ResetThreadPriority(); err != nil {
logger.Warn().Err(err).Msg("Failed to reset thread priority")
}
}()
defer ais.wg.Done()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
// Buffer size update ticker (less frequent)
bufferUpdateTicker := time.NewTicker(500 * time.Millisecond)
defer bufferUpdateTicker.Stop()
for {
select {
case <-ais.stopChan:
@ -838,46 +623,52 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
case msg := <-ais.processChan:
start := time.Now()
err := ais.processMessage(msg)
processingTime := time.Since(start)
processingTime := time.Since(start).Nanoseconds()
// Calculate end-to-end latency using message timestamp
var latency time.Duration
if msg.Type == InputMessageTypeOpusFrame && msg.Timestamp > 0 {
msgTime := time.Unix(0, msg.Timestamp)
latency = time.Since(msgTime)
endToEndLatency := time.Since(msgTime).Nanoseconds()
// Use exponential moving average for end-to-end latency tracking
currentAvg := atomic.LoadInt64(&ais.processingTime)
// Weight: 90% historical, 10% current (for smoother averaging)
newAvg := (currentAvg*9 + latency.Nanoseconds()) / 10
newAvg := (currentAvg*9 + endToEndLatency) / 10
atomic.StoreInt64(&ais.processingTime, newAvg)
} else {
// Fallback to processing time only
latency = processingTime
currentAvg := atomic.LoadInt64(&ais.processingTime)
newAvg := (currentAvg + processingTime.Nanoseconds()) / 2
newAvg := (currentAvg + processingTime) / 2
atomic.StoreInt64(&ais.processingTime, newAvg)
}
// Report latency to adaptive buffer manager
ais.ReportLatency(latency)
if err != nil {
atomic.AddInt64(&ais.droppedFrames, 1)
}
default:
// No more messages to process
goto checkBufferUpdate
goto adaptiveBuffering
}
}
checkBufferUpdate:
// Check if we need to update buffer size
select {
case <-bufferUpdateTicker.C:
// Update buffer size from adaptive buffer manager
ais.UpdateBufferSize()
default:
// No buffer update needed
adaptiveBuffering:
// Adaptive buffer sizing based on processing time
avgTime := atomic.LoadInt64(&ais.processingTime)
currentSize := atomic.LoadInt64(&ais.bufferSize)
if avgTime > 10*1000*1000 { // > 10ms processing time
// Increase buffer size
newSize := currentSize * 2
if newSize > 1000 {
newSize = 1000
}
atomic.StoreInt64(&ais.bufferSize, newSize)
} else if avgTime < 1*1000*1000 { // < 1ms processing time
// Decrease buffer size
newSize := currentSize / 2
if newSize < 50 {
newSize = 50
}
atomic.StoreInt64(&ais.bufferSize, newSize)
}
}
}
@ -892,64 +683,6 @@ func (ais *AudioInputServer) GetServerStats() (total, dropped int64, avgProcessi
atomic.LoadInt64(&ais.bufferSize)
}
// UpdateBufferSize updates the buffer size from adaptive buffer manager
func (ais *AudioInputServer) UpdateBufferSize() {
adaptiveManager := GetAdaptiveBufferManager()
newSize := int64(adaptiveManager.GetInputBufferSize())
atomic.StoreInt64(&ais.bufferSize, newSize)
}
// ReportLatency reports processing latency to adaptive buffer manager
func (ais *AudioInputServer) ReportLatency(latency time.Duration) {
adaptiveManager := GetAdaptiveBufferManager()
adaptiveManager.UpdateLatency(latency)
}
// GetMessagePoolStats returns detailed statistics about the message pool
func (mp *MessagePool) GetMessagePoolStats() MessagePoolStats {
mp.mutex.RLock()
preallocatedCount := len(mp.preallocated)
mp.mutex.RUnlock()
hitCount := atomic.LoadInt64(&mp.hitCount)
missCount := atomic.LoadInt64(&mp.missCount)
totalRequests := hitCount + missCount
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * 100
}
// Calculate channel pool size
channelPoolSize := len(mp.pool)
return MessagePoolStats{
MaxPoolSize: mp.maxPoolSize,
ChannelPoolSize: channelPoolSize,
PreallocatedCount: int64(preallocatedCount),
PreallocatedMax: int64(mp.preallocSize),
HitCount: hitCount,
MissCount: missCount,
HitRate: hitRate,
}
}
// MessagePoolStats provides detailed message pool statistics
type MessagePoolStats struct {
MaxPoolSize int
ChannelPoolSize int
PreallocatedCount int64
PreallocatedMax int64
HitCount int64
MissCount int64
HitRate float64 // Percentage
}
// GetGlobalMessagePoolStats returns statistics for the global message pool
func GetGlobalMessagePoolStats() MessagePoolStats {
return globalMessagePool.GetMessagePoolStats()
}
// Helper functions
// getInputSocketPath returns the path to the input socket

View File

@ -116,40 +116,6 @@ func (aim *AudioInputIPCManager) WriteOpusFrame(frame []byte) error {
return nil
}
// WriteOpusFrameZeroCopy sends an Opus frame via IPC using zero-copy optimization
func (aim *AudioInputIPCManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
if atomic.LoadInt32(&aim.running) == 0 {
return nil // Not running, silently ignore
}
if frame == nil || frame.Length() == 0 {
return nil // Empty frame, ignore
}
// Start latency measurement
startTime := time.Now()
// Update metrics
atomic.AddInt64(&aim.metrics.FramesSent, 1)
atomic.AddInt64(&aim.metrics.BytesProcessed, int64(frame.Length()))
aim.metrics.LastFrameTime = startTime
// Send frame via IPC using zero-copy data
err := aim.supervisor.SendFrameZeroCopy(frame)
if err != nil {
// Count as dropped frame
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
aim.logger.Debug().Err(err).Msg("Failed to send zero-copy frame via IPC")
return err
}
// Calculate and update latency (end-to-end IPC transmission time)
latency := time.Since(startTime)
aim.updateLatencyMetrics(latency)
return nil
}
// IsRunning returns whether the IPC manager is running
func (aim *AudioInputIPCManager) IsRunning() bool {
return atomic.LoadInt32(&aim.running) == 1

View File

@ -16,10 +16,6 @@ func RunAudioInputServer() error {
logger := logging.GetDefaultLogger().With().Str("component", "audio-input-server").Logger()
logger.Info().Msg("Starting audio input server subprocess")
// Start adaptive buffer management for optimal performance
StartAdaptiveBuffering()
defer StopAdaptiveBuffering()
// Initialize CGO audio system
err := CGOAudioPlaybackInit()
if err != nil {

View File

@ -244,19 +244,6 @@ func (ais *AudioInputSupervisor) SendFrame(frame []byte) error {
return ais.client.SendFrame(frame)
}
// SendFrameZeroCopy sends a zero-copy frame to the subprocess
func (ais *AudioInputSupervisor) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
if ais.client == nil {
return fmt.Errorf("client not initialized")
}
if !ais.client.IsConnected() {
return fmt.Errorf("client not connected")
}
return ais.client.SendFrameZeroCopy(frame)
}
// SendConfig sends a configuration update to the subprocess (convenience method)
func (ais *AudioInputSupervisor) SendConfig(config InputIPCConfig) error {
if ais.client == nil {

View File

@ -1,7 +1,6 @@
package audio
import (
"context"
"encoding/binary"
"fmt"
"io"
@ -9,123 +8,22 @@ import (
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
const (
outputMagicNumber uint32 = 0x4A4B4F55 // "JKOU" (JetKVM Output)
outputSocketName = "audio_output.sock"
outputMaxFrameSize = 4096 // Maximum Opus frame size
outputWriteTimeout = 10 * time.Millisecond // Non-blocking write timeout (increased for high load)
outputMaxDroppedFrames = 50 // Maximum consecutive dropped frames
outputHeaderSize = 17 // Fixed header size: 4+1+4+8 bytes
outputMessagePoolSize = 128 // Pre-allocated message pool size
magicNumber uint32 = 0x4A4B564D // "JKVM"
socketName = "audio_output.sock"
)
// OutputMessageType represents the type of IPC message
type OutputMessageType uint8
const (
OutputMessageTypeOpusFrame OutputMessageType = iota
OutputMessageTypeConfig
OutputMessageTypeStop
OutputMessageTypeHeartbeat
OutputMessageTypeAck
)
// OutputIPCMessage represents an IPC message for audio output
type OutputIPCMessage struct {
Magic uint32
Type OutputMessageType
Length uint32
Timestamp int64
Data []byte
}
// OutputOptimizedMessage represents a pre-allocated message for zero-allocation operations
type OutputOptimizedMessage struct {
header [outputHeaderSize]byte // Pre-allocated header buffer
data []byte // Reusable data buffer
}
// OutputMessagePool manages pre-allocated messages for zero-allocation IPC
type OutputMessagePool struct {
pool chan *OutputOptimizedMessage
}
// NewOutputMessagePool creates a new message pool
func NewOutputMessagePool(size int) *OutputMessagePool {
pool := &OutputMessagePool{
pool: make(chan *OutputOptimizedMessage, size),
}
// Pre-allocate messages
for i := 0; i < size; i++ {
msg := &OutputOptimizedMessage{
data: make([]byte, outputMaxFrameSize),
}
pool.pool <- msg
}
return pool
}
// Get retrieves a message from the pool
func (p *OutputMessagePool) Get() *OutputOptimizedMessage {
select {
case msg := <-p.pool:
return msg
default:
// Pool exhausted, create new message
return &OutputOptimizedMessage{
data: make([]byte, outputMaxFrameSize),
}
}
}
// Put returns a message to the pool
func (p *OutputMessagePool) Put(msg *OutputOptimizedMessage) {
select {
case p.pool <- msg:
// Successfully returned to pool
default:
// Pool full, let GC handle it
}
}
// Global message pool for output IPC
var globalOutputMessagePool = NewOutputMessagePool(outputMessagePoolSize)
type AudioServer struct {
// Atomic fields must be first for proper alignment on ARM
bufferSize int64 // Current buffer size (atomic)
droppedFrames int64 // Dropped frames counter (atomic)
totalFrames int64 // Total frames counter (atomic)
listener net.Listener
conn net.Conn
mtx sync.Mutex
running bool
// Advanced message handling
messageChan chan *OutputIPCMessage // Buffered channel for incoming messages
stopChan chan struct{} // Stop signal
wg sync.WaitGroup // Wait group for goroutine coordination
// Latency monitoring
latencyMonitor *LatencyMonitor
adaptiveOptimizer *AdaptiveOptimizer
// Socket buffer configuration
socketBufferConfig SocketBufferConfig
}
func NewAudioServer() (*AudioServer, error) {
socketPath := getOutputSocketPath()
socketPath := filepath.Join("/var/run", socketName)
// Remove existing socket if any
os.Remove(socketPath)
@ -134,192 +32,26 @@ func NewAudioServer() (*AudioServer, error) {
return nil, fmt.Errorf("failed to create unix socket: %w", err)
}
// Initialize with adaptive buffer size (start with 500 frames)
initialBufferSize := int64(500)
// Initialize latency monitoring
latencyConfig := DefaultLatencyConfig()
logger := zerolog.New(os.Stderr).With().Timestamp().Str("component", "audio-server").Logger()
latencyMonitor := NewLatencyMonitor(latencyConfig, logger)
// Initialize adaptive buffer manager with default config
bufferConfig := DefaultAdaptiveBufferConfig()
bufferManager := NewAdaptiveBufferManager(bufferConfig)
// Initialize adaptive optimizer
optimizerConfig := DefaultOptimizerConfig()
adaptiveOptimizer := NewAdaptiveOptimizer(latencyMonitor, bufferManager, optimizerConfig, logger)
// Initialize socket buffer configuration
socketBufferConfig := DefaultSocketBufferConfig()
return &AudioServer{
listener: listener,
messageChan: make(chan *OutputIPCMessage, initialBufferSize),
stopChan: make(chan struct{}),
bufferSize: initialBufferSize,
latencyMonitor: latencyMonitor,
adaptiveOptimizer: adaptiveOptimizer,
socketBufferConfig: socketBufferConfig,
}, nil
return &AudioServer{listener: listener}, nil
}
func (s *AudioServer) Start() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.running {
return fmt.Errorf("server already running")
conn, err := s.listener.Accept()
if err != nil {
return fmt.Errorf("failed to accept connection: %w", err)
}
s.running = true
// Start latency monitoring and adaptive optimization
if s.latencyMonitor != nil {
s.latencyMonitor.Start()
}
if s.adaptiveOptimizer != nil {
s.adaptiveOptimizer.Start()
}
// Start message processor goroutine
s.startProcessorGoroutine()
// Accept connections in a goroutine
go s.acceptConnections()
s.conn = conn
return nil
}
// acceptConnections accepts incoming connections
func (s *AudioServer) acceptConnections() {
for s.running {
conn, err := s.listener.Accept()
if err != nil {
if s.running {
// Only log error if we're still supposed to be running
continue
}
return
}
// Configure socket buffers for optimal performance
if err := ConfigureSocketBuffers(conn, s.socketBufferConfig); err != nil {
// Log warning but don't fail - socket buffer optimization is not critical
logger := logging.GetDefaultLogger().With().Str("component", "audio-server").Logger()
logger.Warn().Err(err).Msg("Failed to configure socket buffers, continuing with defaults")
} else {
// Record socket buffer metrics for monitoring
RecordSocketBufferMetrics(conn, "audio-output")
}
s.mtx.Lock()
// Close existing connection if any
if s.conn != nil {
s.conn.Close()
}
s.conn = conn
s.mtx.Unlock()
}
}
// startProcessorGoroutine starts the message processor
func (s *AudioServer) startProcessorGoroutine() {
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
select {
case msg := <-s.messageChan:
// Process message (currently just frame sending)
if msg.Type == OutputMessageTypeOpusFrame {
if err := s.sendFrameToClient(msg.Data); err != nil {
// Log error but continue processing
atomic.AddInt64(&s.droppedFrames, 1)
}
}
case <-s.stopChan:
return
}
}
}()
}
func (s *AudioServer) Stop() {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.running {
return
}
s.running = false
// Stop latency monitoring and adaptive optimization
if s.adaptiveOptimizer != nil {
s.adaptiveOptimizer.Stop()
}
if s.latencyMonitor != nil {
s.latencyMonitor.Stop()
}
// Signal processor to stop
close(s.stopChan)
s.wg.Wait()
if s.conn != nil {
s.conn.Close()
s.conn = nil
}
}
func (s *AudioServer) Close() error {
s.Stop()
if s.listener != nil {
s.listener.Close()
if s.conn != nil {
s.conn.Close()
}
// Remove socket file
os.Remove(getOutputSocketPath())
return nil
return s.listener.Close()
}
func (s *AudioServer) SendFrame(frame []byte) error {
if len(frame) > outputMaxFrameSize {
return fmt.Errorf("frame size %d exceeds maximum %d", len(frame), outputMaxFrameSize)
}
start := time.Now()
// Create IPC message
msg := &OutputIPCMessage{
Magic: outputMagicNumber,
Type: OutputMessageTypeOpusFrame,
Length: uint32(len(frame)),
Timestamp: start.UnixNano(),
Data: frame,
}
// Try to send via message channel (non-blocking)
select {
case s.messageChan <- msg:
atomic.AddInt64(&s.totalFrames, 1)
// Record latency for monitoring
if s.latencyMonitor != nil {
processingTime := time.Since(start)
s.latencyMonitor.RecordLatency(processingTime, "ipc_send")
}
return nil
default:
// Channel full, drop frame to prevent blocking
atomic.AddInt64(&s.droppedFrames, 1)
return fmt.Errorf("message channel full - frame dropped")
}
}
// sendFrameToClient sends frame data directly to the connected client
func (s *AudioServer) sendFrameToClient(frame []byte) error {
s.mtx.Lock()
defer s.mtx.Unlock()
@ -327,199 +59,70 @@ func (s *AudioServer) sendFrameToClient(frame []byte) error {
return fmt.Errorf("no client connected")
}
start := time.Now()
// Get optimized message from pool
optMsg := globalOutputMessagePool.Get()
defer globalOutputMessagePool.Put(optMsg)
// Prepare header in pre-allocated buffer
binary.LittleEndian.PutUint32(optMsg.header[0:4], outputMagicNumber)
optMsg.header[4] = byte(OutputMessageTypeOpusFrame)
binary.LittleEndian.PutUint32(optMsg.header[5:9], uint32(len(frame)))
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(start.UnixNano()))
// Use non-blocking write with timeout
ctx, cancel := context.WithTimeout(context.Background(), outputWriteTimeout)
defer cancel()
// Create a channel to signal write completion
done := make(chan error, 1)
go func() {
// Write header using pre-allocated buffer
_, err := s.conn.Write(optMsg.header[:])
if err != nil {
done <- err
return
}
// Write frame data
if len(frame) > 0 {
_, err = s.conn.Write(frame)
if err != nil {
done <- err
return
}
}
done <- nil
}()
// Wait for completion or timeout
select {
case err := <-done:
if err != nil {
atomic.AddInt64(&s.droppedFrames, 1)
return err
}
// Record latency for monitoring
if s.latencyMonitor != nil {
writeLatency := time.Since(start)
s.latencyMonitor.RecordLatency(writeLatency, "ipc_write")
}
return nil
case <-ctx.Done():
// Timeout occurred - drop frame to prevent blocking
atomic.AddInt64(&s.droppedFrames, 1)
return fmt.Errorf("write timeout - frame dropped")
// Write magic number
if err := binary.Write(s.conn, binary.BigEndian, magicNumber); err != nil {
return fmt.Errorf("failed to write magic number: %w", err)
}
}
// GetServerStats returns server performance statistics
func (s *AudioServer) GetServerStats() (total, dropped int64, bufferSize int64) {
return atomic.LoadInt64(&s.totalFrames),
atomic.LoadInt64(&s.droppedFrames),
atomic.LoadInt64(&s.bufferSize)
// Write frame size
if err := binary.Write(s.conn, binary.BigEndian, uint32(len(frame))); err != nil {
return fmt.Errorf("failed to write frame size: %w", err)
}
// Write frame data
if _, err := s.conn.Write(frame); err != nil {
return fmt.Errorf("failed to write frame data: %w", err)
}
return nil
}
type AudioClient struct {
// Atomic fields must be first for proper alignment on ARM
droppedFrames int64 // Atomic counter for dropped frames
totalFrames int64 // Atomic counter for total frames
conn net.Conn
mtx sync.Mutex
running bool
conn net.Conn
mtx sync.Mutex
}
func NewAudioClient() *AudioClient {
return &AudioClient{}
}
// Connect connects to the audio output server
func (c *AudioClient) Connect() error {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.running {
return nil // Already connected
}
socketPath := getOutputSocketPath()
func NewAudioClient() (*AudioClient, error) {
socketPath := filepath.Join("/var/run", socketName)
// Try connecting multiple times as the server might not be ready
// Reduced retry count and delay for faster startup
for i := 0; i < 8; i++ {
for i := 0; i < 5; i++ {
conn, err := net.Dial("unix", socketPath)
if err == nil {
c.conn = conn
c.running = true
return nil
return &AudioClient{conn: conn}, nil
}
// Exponential backoff starting at 50ms
delay := time.Duration(50*(1<<uint(i/3))) * time.Millisecond
if delay > 400*time.Millisecond {
delay = 400 * time.Millisecond
}
time.Sleep(delay)
time.Sleep(time.Second)
}
return fmt.Errorf("failed to connect to audio output server")
}
// Disconnect disconnects from the audio output server
func (c *AudioClient) Disconnect() {
c.mtx.Lock()
defer c.mtx.Unlock()
if !c.running {
return
}
c.running = false
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
}
// IsConnected returns whether the client is connected
func (c *AudioClient) IsConnected() bool {
c.mtx.Lock()
defer c.mtx.Unlock()
return c.running && c.conn != nil
return nil, fmt.Errorf("failed to connect to audio server")
}
func (c *AudioClient) Close() error {
c.Disconnect()
return nil
return c.conn.Close()
}
func (c *AudioClient) ReceiveFrame() ([]byte, error) {
c.mtx.Lock()
defer c.mtx.Unlock()
if !c.running || c.conn == nil {
return nil, fmt.Errorf("not connected")
// Read magic number
var magic uint32
if err := binary.Read(c.conn, binary.BigEndian, &magic); err != nil {
return nil, fmt.Errorf("failed to read magic number: %w", err)
}
// Get optimized message from pool for header reading
optMsg := globalOutputMessagePool.Get()
defer globalOutputMessagePool.Put(optMsg)
// Read header
if _, err := io.ReadFull(c.conn, optMsg.header[:]); err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
// Parse header
magic := binary.LittleEndian.Uint32(optMsg.header[0:4])
if magic != outputMagicNumber {
if magic != magicNumber {
return nil, fmt.Errorf("invalid magic number: %x", magic)
}
msgType := OutputMessageType(optMsg.header[4])
if msgType != OutputMessageTypeOpusFrame {
return nil, fmt.Errorf("unexpected message type: %d", msgType)
}
size := binary.LittleEndian.Uint32(optMsg.header[5:9])
if size > outputMaxFrameSize {
return nil, fmt.Errorf("frame size %d exceeds maximum %d", size, outputMaxFrameSize)
// Read frame size
var size uint32
if err := binary.Read(c.conn, binary.BigEndian, &size); err != nil {
return nil, fmt.Errorf("failed to read frame size: %w", err)
}
// Read frame data
frame := make([]byte, size)
if size > 0 {
if _, err := io.ReadFull(c.conn, frame); err != nil {
return nil, fmt.Errorf("failed to read frame data: %w", err)
}
if _, err := io.ReadFull(c.conn, frame); err != nil {
return nil, fmt.Errorf("failed to read frame data: %w", err)
}
atomic.AddInt64(&c.totalFrames, 1)
return frame, nil
}
// GetClientStats returns client performance statistics
func (c *AudioClient) GetClientStats() (total, dropped int64) {
return atomic.LoadInt64(&c.totalFrames),
atomic.LoadInt64(&c.droppedFrames)
}
// Helper functions
// getOutputSocketPath returns the path to the output socket
func getOutputSocketPath() string {
if path := os.Getenv("JETKVM_AUDIO_OUTPUT_SOCKET"); path != "" {
return path
}
return filepath.Join("/var/run", outputSocketName)
}

View File

@ -1,312 +0,0 @@
package audio
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
)
// LatencyMonitor tracks and optimizes audio latency in real-time
type LatencyMonitor struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentLatency int64 // Current latency in nanoseconds (atomic)
averageLatency int64 // Rolling average latency in nanoseconds (atomic)
minLatency int64 // Minimum observed latency in nanoseconds (atomic)
maxLatency int64 // Maximum observed latency in nanoseconds (atomic)
latencySamples int64 // Number of latency samples collected (atomic)
jitterAccumulator int64 // Accumulated jitter for variance calculation (atomic)
lastOptimization int64 // Timestamp of last optimization in nanoseconds (atomic)
config LatencyConfig
logger zerolog.Logger
// Control channels
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// Optimization callbacks
optimizationCallbacks []OptimizationCallback
mutex sync.RWMutex
// Performance tracking
latencyHistory []LatencyMeasurement
historyMutex sync.RWMutex
}
// LatencyConfig holds configuration for latency monitoring
type LatencyConfig struct {
TargetLatency time.Duration // Target latency to maintain
MaxLatency time.Duration // Maximum acceptable latency
OptimizationInterval time.Duration // How often to run optimization
HistorySize int // Number of latency measurements to keep
JitterThreshold time.Duration // Jitter threshold for optimization
AdaptiveThreshold float64 // Threshold for adaptive adjustments (0.0-1.0)
}
// LatencyMeasurement represents a single latency measurement
type LatencyMeasurement struct {
Timestamp time.Time
Latency time.Duration
Jitter time.Duration
Source string // Source of the measurement (e.g., "input", "output", "processing")
}
// OptimizationCallback is called when latency optimization is triggered
type OptimizationCallback func(metrics LatencyMetrics) error
// LatencyMetrics provides comprehensive latency statistics
type LatencyMetrics struct {
Current time.Duration
Average time.Duration
Min time.Duration
Max time.Duration
Jitter time.Duration
SampleCount int64
Trend LatencyTrend
}
// LatencyTrend indicates the direction of latency changes
type LatencyTrend int
const (
LatencyTrendStable LatencyTrend = iota
LatencyTrendIncreasing
LatencyTrendDecreasing
LatencyTrendVolatile
)
// DefaultLatencyConfig returns a sensible default configuration
func DefaultLatencyConfig() LatencyConfig {
return LatencyConfig{
TargetLatency: 50 * time.Millisecond,
MaxLatency: 200 * time.Millisecond,
OptimizationInterval: 5 * time.Second,
HistorySize: 100,
JitterThreshold: 20 * time.Millisecond,
AdaptiveThreshold: 0.8, // Trigger optimization when 80% above target
}
}
// NewLatencyMonitor creates a new latency monitoring system
func NewLatencyMonitor(config LatencyConfig, logger zerolog.Logger) *LatencyMonitor {
ctx, cancel := context.WithCancel(context.Background())
return &LatencyMonitor{
config: config,
logger: logger.With().Str("component", "latency-monitor").Logger(),
ctx: ctx,
cancel: cancel,
latencyHistory: make([]LatencyMeasurement, 0, config.HistorySize),
minLatency: int64(time.Hour), // Initialize to high value
}
}
// Start begins latency monitoring and optimization
func (lm *LatencyMonitor) Start() {
lm.wg.Add(1)
go lm.monitoringLoop()
lm.logger.Info().Msg("Latency monitor started")
}
// Stop stops the latency monitor
func (lm *LatencyMonitor) Stop() {
lm.cancel()
lm.wg.Wait()
lm.logger.Info().Msg("Latency monitor stopped")
}
// RecordLatency records a new latency measurement
func (lm *LatencyMonitor) RecordLatency(latency time.Duration, source string) {
now := time.Now()
latencyNanos := latency.Nanoseconds()
// Update atomic counters
atomic.StoreInt64(&lm.currentLatency, latencyNanos)
atomic.AddInt64(&lm.latencySamples, 1)
// Update min/max
for {
oldMin := atomic.LoadInt64(&lm.minLatency)
if latencyNanos >= oldMin || atomic.CompareAndSwapInt64(&lm.minLatency, oldMin, latencyNanos) {
break
}
}
for {
oldMax := atomic.LoadInt64(&lm.maxLatency)
if latencyNanos <= oldMax || atomic.CompareAndSwapInt64(&lm.maxLatency, oldMax, latencyNanos) {
break
}
}
// Update rolling average using exponential moving average
oldAvg := atomic.LoadInt64(&lm.averageLatency)
newAvg := oldAvg + (latencyNanos-oldAvg)/10 // Alpha = 0.1
atomic.StoreInt64(&lm.averageLatency, newAvg)
// Calculate jitter (difference from average)
jitter := latencyNanos - newAvg
if jitter < 0 {
jitter = -jitter
}
atomic.AddInt64(&lm.jitterAccumulator, jitter)
// Store in history
lm.historyMutex.Lock()
measurement := LatencyMeasurement{
Timestamp: now,
Latency: latency,
Jitter: time.Duration(jitter),
Source: source,
}
if len(lm.latencyHistory) >= lm.config.HistorySize {
// Remove oldest measurement
copy(lm.latencyHistory, lm.latencyHistory[1:])
lm.latencyHistory[len(lm.latencyHistory)-1] = measurement
} else {
lm.latencyHistory = append(lm.latencyHistory, measurement)
}
lm.historyMutex.Unlock()
}
// GetMetrics returns current latency metrics
func (lm *LatencyMonitor) GetMetrics() LatencyMetrics {
current := atomic.LoadInt64(&lm.currentLatency)
average := atomic.LoadInt64(&lm.averageLatency)
min := atomic.LoadInt64(&lm.minLatency)
max := atomic.LoadInt64(&lm.maxLatency)
samples := atomic.LoadInt64(&lm.latencySamples)
jitterSum := atomic.LoadInt64(&lm.jitterAccumulator)
var jitter time.Duration
if samples > 0 {
jitter = time.Duration(jitterSum / samples)
}
return LatencyMetrics{
Current: time.Duration(current),
Average: time.Duration(average),
Min: time.Duration(min),
Max: time.Duration(max),
Jitter: jitter,
SampleCount: samples,
Trend: lm.calculateTrend(),
}
}
// AddOptimizationCallback adds a callback for latency optimization
func (lm *LatencyMonitor) AddOptimizationCallback(callback OptimizationCallback) {
lm.mutex.Lock()
lm.optimizationCallbacks = append(lm.optimizationCallbacks, callback)
lm.mutex.Unlock()
}
// monitoringLoop runs the main monitoring and optimization loop
func (lm *LatencyMonitor) monitoringLoop() {
defer lm.wg.Done()
ticker := time.NewTicker(lm.config.OptimizationInterval)
defer ticker.Stop()
for {
select {
case <-lm.ctx.Done():
return
case <-ticker.C:
lm.runOptimization()
}
}
}
// runOptimization checks if optimization is needed and triggers callbacks
func (lm *LatencyMonitor) runOptimization() {
metrics := lm.GetMetrics()
// Check if optimization is needed
needsOptimization := false
// Check if current latency exceeds threshold
if metrics.Current > lm.config.MaxLatency {
needsOptimization = true
lm.logger.Warn().Dur("current_latency", metrics.Current).Dur("max_latency", lm.config.MaxLatency).Msg("Latency exceeds maximum threshold")
}
// Check if average latency is above adaptive threshold
adaptiveThreshold := time.Duration(float64(lm.config.TargetLatency.Nanoseconds()) * (1.0 + lm.config.AdaptiveThreshold))
if metrics.Average > adaptiveThreshold {
needsOptimization = true
lm.logger.Info().Dur("average_latency", metrics.Average).Dur("threshold", adaptiveThreshold).Msg("Average latency above adaptive threshold")
}
// Check if jitter is too high
if metrics.Jitter > lm.config.JitterThreshold {
needsOptimization = true
lm.logger.Info().Dur("jitter", metrics.Jitter).Dur("threshold", lm.config.JitterThreshold).Msg("Jitter above threshold")
}
if needsOptimization {
atomic.StoreInt64(&lm.lastOptimization, time.Now().UnixNano())
// Run optimization callbacks
lm.mutex.RLock()
callbacks := make([]OptimizationCallback, len(lm.optimizationCallbacks))
copy(callbacks, lm.optimizationCallbacks)
lm.mutex.RUnlock()
for _, callback := range callbacks {
if err := callback(metrics); err != nil {
lm.logger.Error().Err(err).Msg("Optimization callback failed")
}
}
lm.logger.Info().Interface("metrics", metrics).Msg("Latency optimization triggered")
}
}
// calculateTrend analyzes recent latency measurements to determine trend
func (lm *LatencyMonitor) calculateTrend() LatencyTrend {
lm.historyMutex.RLock()
defer lm.historyMutex.RUnlock()
if len(lm.latencyHistory) < 10 {
return LatencyTrendStable
}
// Analyze last 10 measurements
recentMeasurements := lm.latencyHistory[len(lm.latencyHistory)-10:]
var increasing, decreasing int
for i := 1; i < len(recentMeasurements); i++ {
if recentMeasurements[i].Latency > recentMeasurements[i-1].Latency {
increasing++
} else if recentMeasurements[i].Latency < recentMeasurements[i-1].Latency {
decreasing++
}
}
// Determine trend based on direction changes
if increasing > 6 {
return LatencyTrendIncreasing
} else if decreasing > 6 {
return LatencyTrendDecreasing
} else if increasing+decreasing > 7 {
return LatencyTrendVolatile
}
return LatencyTrendStable
}
// GetLatencyHistory returns a copy of recent latency measurements
func (lm *LatencyMonitor) GetLatencyHistory() []LatencyMeasurement {
lm.historyMutex.RLock()
defer lm.historyMutex.RUnlock()
history := make([]LatencyMeasurement, len(lm.latencyHistory))
copy(history, lm.latencyHistory)
return history
}

View File

@ -1,198 +0,0 @@
package audio
import (
"encoding/json"
"net/http"
"runtime"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// MemoryMetrics provides comprehensive memory allocation statistics
type MemoryMetrics struct {
// Runtime memory statistics
RuntimeStats RuntimeMemoryStats `json:"runtime_stats"`
// Audio buffer pool statistics
BufferPools AudioBufferPoolStats `json:"buffer_pools"`
// Zero-copy frame pool statistics
ZeroCopyPool ZeroCopyFramePoolStats `json:"zero_copy_pool"`
// Message pool statistics
MessagePool MessagePoolStats `json:"message_pool"`
// Batch processor statistics
BatchProcessor BatchProcessorMemoryStats `json:"batch_processor,omitempty"`
// Collection timestamp
Timestamp time.Time `json:"timestamp"`
}
// RuntimeMemoryStats provides Go runtime memory statistics
type RuntimeMemoryStats struct {
Alloc uint64 `json:"alloc"` // Bytes allocated and not yet freed
TotalAlloc uint64 `json:"total_alloc"` // Total bytes allocated (cumulative)
Sys uint64 `json:"sys"` // Total bytes obtained from OS
Lookups uint64 `json:"lookups"` // Number of pointer lookups
Mallocs uint64 `json:"mallocs"` // Number of mallocs
Frees uint64 `json:"frees"` // Number of frees
HeapAlloc uint64 `json:"heap_alloc"` // Bytes allocated and not yet freed (heap)
HeapSys uint64 `json:"heap_sys"` // Bytes obtained from OS for heap
HeapIdle uint64 `json:"heap_idle"` // Bytes in idle spans
HeapInuse uint64 `json:"heap_inuse"` // Bytes in non-idle spans
HeapReleased uint64 `json:"heap_released"` // Bytes released to OS
HeapObjects uint64 `json:"heap_objects"` // Total number of allocated objects
StackInuse uint64 `json:"stack_inuse"` // Bytes used by stack spans
StackSys uint64 `json:"stack_sys"` // Bytes obtained from OS for stack
MSpanInuse uint64 `json:"mspan_inuse"` // Bytes used by mspan structures
MSpanSys uint64 `json:"mspan_sys"` // Bytes obtained from OS for mspan
MCacheInuse uint64 `json:"mcache_inuse"` // Bytes used by mcache structures
MCacheSys uint64 `json:"mcache_sys"` // Bytes obtained from OS for mcache
BuckHashSys uint64 `json:"buck_hash_sys"` // Bytes used by profiling bucket hash table
GCSys uint64 `json:"gc_sys"` // Bytes used for garbage collection metadata
OtherSys uint64 `json:"other_sys"` // Bytes used for other system allocations
NextGC uint64 `json:"next_gc"` // Target heap size for next GC
LastGC uint64 `json:"last_gc"` // Time of last GC (nanoseconds since epoch)
PauseTotalNs uint64 `json:"pause_total_ns"` // Total GC pause time
NumGC uint32 `json:"num_gc"` // Number of completed GC cycles
NumForcedGC uint32 `json:"num_forced_gc"` // Number of forced GC cycles
GCCPUFraction float64 `json:"gc_cpu_fraction"` // Fraction of CPU time used by GC
}
// BatchProcessorMemoryStats provides batch processor memory statistics
type BatchProcessorMemoryStats struct {
Initialized bool `json:"initialized"`
Running bool `json:"running"`
Stats BatchAudioStats `json:"stats"`
BufferPool AudioBufferPoolDetailedStats `json:"buffer_pool,omitempty"`
}
// GetBatchAudioProcessor is defined in batch_audio.go
// BatchAudioStats is defined in batch_audio.go
var memoryMetricsLogger *zerolog.Logger
func getMemoryMetricsLogger() *zerolog.Logger {
if memoryMetricsLogger == nil {
logger := logging.GetDefaultLogger().With().Str("component", "memory-metrics").Logger()
memoryMetricsLogger = &logger
}
return memoryMetricsLogger
}
// CollectMemoryMetrics gathers comprehensive memory allocation statistics
func CollectMemoryMetrics() MemoryMetrics {
// Collect runtime memory statistics
var m runtime.MemStats
runtime.ReadMemStats(&m)
runtimeStats := RuntimeMemoryStats{
Alloc: m.Alloc,
TotalAlloc: m.TotalAlloc,
Sys: m.Sys,
Lookups: m.Lookups,
Mallocs: m.Mallocs,
Frees: m.Frees,
HeapAlloc: m.HeapAlloc,
HeapSys: m.HeapSys,
HeapIdle: m.HeapIdle,
HeapInuse: m.HeapInuse,
HeapReleased: m.HeapReleased,
HeapObjects: m.HeapObjects,
StackInuse: m.StackInuse,
StackSys: m.StackSys,
MSpanInuse: m.MSpanInuse,
MSpanSys: m.MSpanSys,
MCacheInuse: m.MCacheInuse,
MCacheSys: m.MCacheSys,
BuckHashSys: m.BuckHashSys,
GCSys: m.GCSys,
OtherSys: m.OtherSys,
NextGC: m.NextGC,
LastGC: m.LastGC,
PauseTotalNs: m.PauseTotalNs,
NumGC: m.NumGC,
NumForcedGC: m.NumForcedGC,
GCCPUFraction: m.GCCPUFraction,
}
// Collect audio buffer pool statistics
bufferPoolStats := GetAudioBufferPoolStats()
// Collect zero-copy frame pool statistics
zeroCopyStats := GetGlobalZeroCopyPoolStats()
// Collect message pool statistics
messagePoolStats := GetGlobalMessagePoolStats()
// Collect batch processor statistics if available
var batchStats BatchProcessorMemoryStats
if processor := GetBatchAudioProcessor(); processor != nil {
batchStats.Initialized = true
batchStats.Running = processor.IsRunning()
batchStats.Stats = processor.GetStats()
// Note: BatchAudioProcessor uses sync.Pool, detailed stats not available
}
return MemoryMetrics{
RuntimeStats: runtimeStats,
BufferPools: bufferPoolStats,
ZeroCopyPool: zeroCopyStats,
MessagePool: messagePoolStats,
BatchProcessor: batchStats,
Timestamp: time.Now(),
}
}
// HandleMemoryMetrics provides an HTTP handler for memory metrics
func HandleMemoryMetrics(w http.ResponseWriter, r *http.Request) {
logger := getMemoryMetricsLogger()
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
metrics := CollectMemoryMetrics()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
if err := json.NewEncoder(w).Encode(metrics); err != nil {
logger.Error().Err(err).Msg("failed to encode memory metrics")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
logger.Debug().Msg("memory metrics served")
}
// LogMemoryMetrics logs current memory metrics for debugging
func LogMemoryMetrics() {
logger := getMemoryMetricsLogger()
metrics := CollectMemoryMetrics()
logger.Info().
Uint64("heap_alloc_mb", metrics.RuntimeStats.HeapAlloc/1024/1024).
Uint64("heap_sys_mb", metrics.RuntimeStats.HeapSys/1024/1024).
Uint64("heap_objects", metrics.RuntimeStats.HeapObjects).
Uint32("num_gc", metrics.RuntimeStats.NumGC).
Float64("gc_cpu_fraction", metrics.RuntimeStats.GCCPUFraction).
Float64("buffer_pool_hit_rate", metrics.BufferPools.FramePoolHitRate).
Float64("zero_copy_hit_rate", metrics.ZeroCopyPool.HitRate).
Float64("message_pool_hit_rate", metrics.MessagePool.HitRate).
Msg("memory metrics snapshot")
}
// StartMemoryMetricsLogging starts periodic memory metrics logging
func StartMemoryMetricsLogging(interval time.Duration) {
logger := getMemoryMetricsLogger()
logger.Info().Dur("interval", interval).Msg("starting memory metrics logging")
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
LogMemoryMetrics()
}
}()
}

View File

@ -10,67 +10,6 @@ import (
)
var (
// Adaptive buffer metrics
adaptiveInputBufferSize = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_adaptive_input_buffer_size_bytes",
Help: "Current adaptive input buffer size in bytes",
},
)
adaptiveOutputBufferSize = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_adaptive_output_buffer_size_bytes",
Help: "Current adaptive output buffer size in bytes",
},
)
adaptiveBufferAdjustmentsTotal = promauto.NewCounter(
prometheus.CounterOpts{
Name: "jetkvm_adaptive_buffer_adjustments_total",
Help: "Total number of adaptive buffer size adjustments",
},
)
adaptiveSystemCpuPercent = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_adaptive_system_cpu_percent",
Help: "System CPU usage percentage used by adaptive buffer manager",
},
)
adaptiveSystemMemoryPercent = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_adaptive_system_memory_percent",
Help: "System memory usage percentage used by adaptive buffer manager",
},
)
// Socket buffer metrics
socketBufferSizeGauge = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_socket_buffer_size_bytes",
Help: "Current socket buffer size in bytes",
},
[]string{"component", "buffer_type"}, // buffer_type: send, receive
)
socketBufferUtilizationGauge = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "jetkvm_audio_socket_buffer_utilization_percent",
Help: "Socket buffer utilization percentage",
},
[]string{"component", "buffer_type"}, // buffer_type: send, receive
)
socketBufferOverflowCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "jetkvm_audio_socket_buffer_overflow_total",
Help: "Total number of socket buffer overflows",
},
[]string{"component", "buffer_type"}, // buffer_type: send, receive
)
// Audio output metrics
audioFramesReceivedTotal = promauto.NewCounter(
prometheus.CounterOpts{
@ -425,23 +364,6 @@ func UpdateMicrophoneConfigMetrics(config AudioConfig) {
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateAdaptiveBufferMetrics updates Prometheus metrics with adaptive buffer information
func UpdateAdaptiveBufferMetrics(inputBufferSize, outputBufferSize int, cpuPercent, memoryPercent float64, adjustmentMade bool) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
adaptiveInputBufferSize.Set(float64(inputBufferSize))
adaptiveOutputBufferSize.Set(float64(outputBufferSize))
adaptiveSystemCpuPercent.Set(cpuPercent)
adaptiveSystemMemoryPercent.Set(memoryPercent)
if adjustmentMade {
adaptiveBufferAdjustmentsTotal.Inc()
}
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// GetLastMetricsUpdate returns the timestamp of the last metrics update
func GetLastMetricsUpdate() time.Time {
timestamp := atomic.LoadInt64(&lastMetricsUpdate)

View File

@ -8,12 +8,10 @@ import (
// 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
lockPtr unsafe.Pointer
}
func NewMicrophoneContentionManager(cooldown time.Duration) *MicrophoneContentionManager {

View File

@ -2,9 +2,6 @@ package audio
import (
"context"
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
@ -12,28 +9,6 @@ import (
"github.com/rs/zerolog"
)
// OutputStreamer manages high-performance audio output streaming
type OutputStreamer struct {
// Atomic fields must be first for proper alignment on ARM
processedFrames int64 // Total processed frames counter (atomic)
droppedFrames int64 // Dropped frames counter (atomic)
processingTime int64 // Average processing time in nanoseconds (atomic)
lastStatsTime int64 // Last statistics update time (atomic)
client *AudioClient
bufferPool *AudioBufferPool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
running bool
mtx sync.Mutex
// Performance optimization fields
batchSize int // Adaptive batch size for frame processing
processingChan chan []byte // Buffered channel for frame processing
statsInterval time.Duration // Statistics reporting interval
}
var (
outputStreamingRunning int32
outputStreamingCancel context.CancelFunc
@ -48,253 +23,6 @@ func getOutputStreamingLogger() *zerolog.Logger {
return outputStreamingLogger
}
func NewOutputStreamer() (*OutputStreamer, error) {
client := NewAudioClient()
// Get initial batch size from adaptive buffer manager
adaptiveManager := GetAdaptiveBufferManager()
initialBatchSize := adaptiveManager.GetOutputBufferSize()
ctx, cancel := context.WithCancel(context.Background())
return &OutputStreamer{
client: client,
bufferPool: NewAudioBufferPool(MaxAudioFrameSize), // Use existing buffer pool
ctx: ctx,
cancel: cancel,
batchSize: initialBatchSize, // Use adaptive batch size
processingChan: make(chan []byte, 500), // Large buffer for smooth processing
statsInterval: 5 * time.Second, // Statistics every 5 seconds
lastStatsTime: time.Now().UnixNano(),
}, nil
}
func (s *OutputStreamer) Start() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.running {
return fmt.Errorf("output streamer already running")
}
// Connect to audio output server
if err := s.client.Connect(); err != nil {
return fmt.Errorf("failed to connect to audio output server: %w", err)
}
s.running = true
// Start multiple goroutines for optimal performance
s.wg.Add(3)
go s.streamLoop() // Main streaming loop
go s.processingLoop() // Frame processing loop
go s.statisticsLoop() // Performance monitoring loop
return nil
}
func (s *OutputStreamer) Stop() {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.running {
return
}
s.running = false
s.cancel()
// Close processing channel to signal goroutines
close(s.processingChan)
// Wait for all goroutines to finish
s.wg.Wait()
if s.client != nil {
s.client.Close()
}
}
func (s *OutputStreamer) streamLoop() {
defer s.wg.Done()
// Pin goroutine to OS thread for consistent performance
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Adaptive timing for frame reading
frameInterval := time.Duration(20) * time.Millisecond // 50 FPS base rate
ticker := time.NewTicker(frameInterval)
defer ticker.Stop()
// Batch size update ticker
batchUpdateTicker := time.NewTicker(500 * time.Millisecond)
defer batchUpdateTicker.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-batchUpdateTicker.C:
// Update batch size from adaptive buffer manager
s.UpdateBatchSize()
case <-ticker.C:
// Read audio data from CGO with timing measurement
startTime := time.Now()
frameBuf := s.bufferPool.Get()
n, err := CGOAudioReadEncode(frameBuf)
processingDuration := time.Since(startTime)
if err != nil {
getOutputStreamingLogger().Warn().Err(err).Msg("Failed to read audio data")
s.bufferPool.Put(frameBuf)
atomic.AddInt64(&s.droppedFrames, 1)
continue
}
if n > 0 {
// Send frame for processing (non-blocking)
frameData := make([]byte, n)
copy(frameData, frameBuf[:n])
select {
case s.processingChan <- frameData:
atomic.AddInt64(&s.processedFrames, 1)
// Update processing time statistics
atomic.StoreInt64(&s.processingTime, int64(processingDuration))
// Report latency to adaptive buffer manager
s.ReportLatency(processingDuration)
default:
// Processing channel full, drop frame
atomic.AddInt64(&s.droppedFrames, 1)
}
}
s.bufferPool.Put(frameBuf)
}
}
}
// processingLoop handles frame processing in a separate goroutine
func (s *OutputStreamer) processingLoop() {
defer s.wg.Done()
// Pin goroutine to OS thread for consistent performance
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Set high priority for audio output processing
if err := SetAudioThreadPriority(); err != nil {
getOutputStreamingLogger().Warn().Err(err).Msg("Failed to set audio output processing priority")
}
defer func() {
if err := ResetThreadPriority(); err != nil {
getOutputStreamingLogger().Warn().Err(err).Msg("Failed to reset thread priority")
}
}()
for range s.processingChan {
// Process frame (currently just receiving, but can be extended)
if _, err := s.client.ReceiveFrame(); err != nil {
if s.client.IsConnected() {
getOutputStreamingLogger().Warn().Err(err).Msg("Failed to receive frame")
atomic.AddInt64(&s.droppedFrames, 1)
}
// Try to reconnect if disconnected
if !s.client.IsConnected() {
if err := s.client.Connect(); err != nil {
getOutputStreamingLogger().Warn().Err(err).Msg("Failed to reconnect")
}
}
}
}
}
// statisticsLoop monitors and reports performance statistics
func (s *OutputStreamer) statisticsLoop() {
defer s.wg.Done()
ticker := time.NewTicker(s.statsInterval)
defer ticker.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-ticker.C:
s.reportStatistics()
}
}
}
// reportStatistics logs current performance statistics
func (s *OutputStreamer) reportStatistics() {
processed := atomic.LoadInt64(&s.processedFrames)
dropped := atomic.LoadInt64(&s.droppedFrames)
processingTime := atomic.LoadInt64(&s.processingTime)
if processed > 0 {
dropRate := float64(dropped) / float64(processed+dropped) * 100
avgProcessingTime := time.Duration(processingTime)
getOutputStreamingLogger().Info().Int64("processed", processed).Int64("dropped", dropped).Float64("drop_rate", dropRate).Dur("avg_processing", avgProcessingTime).Msg("Output Audio Stats")
// Get client statistics
clientTotal, clientDropped := s.client.GetClientStats()
getOutputStreamingLogger().Info().Int64("total", clientTotal).Int64("dropped", clientDropped).Msg("Client Stats")
}
}
// GetStats returns streaming statistics
func (s *OutputStreamer) GetStats() (processed, dropped int64, avgProcessingTime time.Duration) {
processed = atomic.LoadInt64(&s.processedFrames)
dropped = atomic.LoadInt64(&s.droppedFrames)
processingTimeNs := atomic.LoadInt64(&s.processingTime)
avgProcessingTime = time.Duration(processingTimeNs)
return
}
// GetDetailedStats returns comprehensive streaming statistics
func (s *OutputStreamer) GetDetailedStats() map[string]interface{} {
processed := atomic.LoadInt64(&s.processedFrames)
dropped := atomic.LoadInt64(&s.droppedFrames)
processingTime := atomic.LoadInt64(&s.processingTime)
stats := map[string]interface{}{
"processed_frames": processed,
"dropped_frames": dropped,
"avg_processing_time_ns": processingTime,
"batch_size": s.batchSize,
"channel_buffer_size": cap(s.processingChan),
"channel_current_size": len(s.processingChan),
"connected": s.client.IsConnected(),
}
if processed+dropped > 0 {
stats["drop_rate_percent"] = float64(dropped) / float64(processed+dropped) * 100
}
// Add client statistics
clientTotal, clientDropped := s.client.GetClientStats()
stats["client_total_frames"] = clientTotal
stats["client_dropped_frames"] = clientDropped
return stats
}
// UpdateBatchSize updates the batch size from adaptive buffer manager
func (s *OutputStreamer) UpdateBatchSize() {
s.mtx.Lock()
adaptiveManager := GetAdaptiveBufferManager()
s.batchSize = adaptiveManager.GetOutputBufferSize()
s.mtx.Unlock()
}
// ReportLatency reports processing latency to adaptive buffer manager
func (s *OutputStreamer) ReportLatency(latency time.Duration) {
adaptiveManager := GetAdaptiveBufferManager()
adaptiveManager.UpdateLatency(latency)
}
// StartAudioOutputStreaming starts audio output streaming (capturing system audio)
func StartAudioOutputStreaming(send func([]byte)) error {
if !atomic.CompareAndSwapInt32(&outputStreamingRunning, 0, 1) {
@ -333,13 +61,10 @@ func StartAudioOutputStreaming(send func([]byte)) error {
continue
}
if n > 0 {
// Get frame buffer from pool to reduce allocations
frame := GetAudioFrameBuffer()
frame = frame[:n] // Resize to actual frame size
// Send frame to callback
frame := make([]byte, n)
copy(frame, buffer[:n])
send(frame)
// Return buffer to pool after sending
PutAudioFrameBuffer(frame)
RecordFrameReceived(n)
}
// Small delay to prevent busy waiting

View File

@ -1,165 +0,0 @@
//go:build linux
package audio
import (
"runtime"
"syscall"
"unsafe"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// SchedParam represents scheduling parameters for Linux
type SchedParam struct {
Priority int32
}
// Priority levels for audio processing
const (
// SCHED_FIFO priorities (1-99, higher = more priority)
AudioHighPriority = 80 // High priority for critical audio processing
AudioMediumPriority = 60 // Medium priority for regular audio processing
AudioLowPriority = 40 // Low priority for background audio tasks
// SCHED_NORMAL is the default (priority 0)
NormalPriority = 0
)
// Scheduling policies
const (
SCHED_NORMAL = 0
SCHED_FIFO = 1
SCHED_RR = 2
)
// PriorityScheduler manages thread priorities for audio processing
type PriorityScheduler struct {
logger zerolog.Logger
enabled bool
}
// NewPriorityScheduler creates a new priority scheduler
func NewPriorityScheduler() *PriorityScheduler {
return &PriorityScheduler{
logger: logging.GetDefaultLogger().With().Str("component", "priority-scheduler").Logger(),
enabled: true,
}
}
// SetThreadPriority sets the priority of the current thread
func (ps *PriorityScheduler) SetThreadPriority(priority int, policy int) error {
if !ps.enabled {
return nil
}
// Lock to OS thread to ensure we're setting priority for the right thread
runtime.LockOSThread()
// Get current thread ID
tid := syscall.Gettid()
// Set scheduling parameters
param := &SchedParam{
Priority: int32(priority),
}
// Use syscall to set scheduler
_, _, errno := syscall.Syscall(syscall.SYS_SCHED_SETSCHEDULER,
uintptr(tid),
uintptr(policy),
uintptr(unsafe.Pointer(param)))
if errno != 0 {
// If we can't set real-time priority, try nice value instead
if policy != SCHED_NORMAL {
ps.logger.Warn().Int("errno", int(errno)).Msg("Failed to set real-time priority, falling back to nice")
return ps.setNicePriority(priority)
}
return errno
}
ps.logger.Debug().Int("tid", tid).Int("priority", priority).Int("policy", policy).Msg("Thread priority set")
return nil
}
// setNicePriority sets nice value as fallback when real-time scheduling is not available
func (ps *PriorityScheduler) setNicePriority(rtPriority int) error {
// Convert real-time priority to nice value (inverse relationship)
// RT priority 80 -> nice -10, RT priority 40 -> nice 0
niceValue := (40 - rtPriority) / 4
if niceValue < -20 {
niceValue = -20
}
if niceValue > 19 {
niceValue = 19
}
err := syscall.Setpriority(syscall.PRIO_PROCESS, 0, niceValue)
if err != nil {
ps.logger.Warn().Err(err).Int("nice", niceValue).Msg("Failed to set nice priority")
return err
}
ps.logger.Debug().Int("nice", niceValue).Msg("Nice priority set as fallback")
return nil
}
// SetAudioProcessingPriority sets high priority for audio processing threads
func (ps *PriorityScheduler) SetAudioProcessingPriority() error {
return ps.SetThreadPriority(AudioHighPriority, SCHED_FIFO)
}
// SetAudioIOPriority sets medium priority for audio I/O threads
func (ps *PriorityScheduler) SetAudioIOPriority() error {
return ps.SetThreadPriority(AudioMediumPriority, SCHED_FIFO)
}
// SetAudioBackgroundPriority sets low priority for background audio tasks
func (ps *PriorityScheduler) SetAudioBackgroundPriority() error {
return ps.SetThreadPriority(AudioLowPriority, SCHED_FIFO)
}
// ResetPriority resets thread to normal scheduling
func (ps *PriorityScheduler) ResetPriority() error {
return ps.SetThreadPriority(NormalPriority, SCHED_NORMAL)
}
// Disable disables priority scheduling (useful for testing or fallback)
func (ps *PriorityScheduler) Disable() {
ps.enabled = false
ps.logger.Info().Msg("Priority scheduling disabled")
}
// Enable enables priority scheduling
func (ps *PriorityScheduler) Enable() {
ps.enabled = true
ps.logger.Info().Msg("Priority scheduling enabled")
}
// Global priority scheduler instance
var globalPriorityScheduler *PriorityScheduler
// GetPriorityScheduler returns the global priority scheduler instance
func GetPriorityScheduler() *PriorityScheduler {
if globalPriorityScheduler == nil {
globalPriorityScheduler = NewPriorityScheduler()
}
return globalPriorityScheduler
}
// SetAudioThreadPriority is a convenience function to set audio processing priority
func SetAudioThreadPriority() error {
return GetPriorityScheduler().SetAudioProcessingPriority()
}
// SetAudioIOThreadPriority is a convenience function to set audio I/O priority
func SetAudioIOThreadPriority() error {
return GetPriorityScheduler().SetAudioIOPriority()
}
// ResetThreadPriority is a convenience function to reset thread priority
func ResetThreadPriority() error {
return GetPriorityScheduler().ResetPriority()
}

View File

@ -2,7 +2,6 @@ package audio
import (
"context"
"fmt"
"sync"
"time"
@ -14,10 +13,6 @@ import (
// AudioRelay handles forwarding audio frames from the audio server subprocess
// to WebRTC without any CGO audio processing. This runs in the main process.
type AudioRelay struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
framesRelayed int64
framesDropped int64
client *AudioClient
ctx context.Context
cancel context.CancelFunc
@ -30,6 +25,10 @@ type AudioRelay struct {
audioTrack AudioTrackWriter
config AudioConfig
muted bool
// Statistics
framesRelayed int64
framesDropped int64
}
// AudioTrackWriter interface for WebRTC audio track
@ -59,16 +58,14 @@ func (r *AudioRelay) Start(audioTrack AudioTrackWriter, config AudioConfig) erro
}
// Create audio client to connect to subprocess
client := NewAudioClient()
client, err := NewAudioClient()
if err != nil {
return err
}
r.client = client
r.audioTrack = audioTrack
r.config = config
// Connect to the audio output server
if err := client.Connect(); err != nil {
return fmt.Errorf("failed to connect to audio output server: %w", err)
}
// Start relay goroutine
r.wg.Add(1)
go r.relayLoop()
@ -91,7 +88,7 @@ func (r *AudioRelay) Stop() {
r.wg.Wait()
if r.client != nil {
r.client.Disconnect()
r.client.Close()
r.client = nil
}

View File

@ -1,160 +0,0 @@
package audio
import (
"fmt"
"net"
"syscall"
)
const (
// Socket buffer sizes optimized for JetKVM's audio workload
OptimalSocketBuffer = 128 * 1024 // 128KB (32 frames @ 4KB each)
MaxSocketBuffer = 256 * 1024 // 256KB for high-load scenarios
MinSocketBuffer = 32 * 1024 // 32KB minimum for basic functionality
)
// 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: OptimalSocketBuffer,
RecvBufferSize: OptimalSocketBuffer,
Enabled: true,
}
}
// HighLoadSocketBufferConfig returns configuration for high-load scenarios
func HighLoadSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{
SendBufferSize: MaxSocketBuffer,
RecvBufferSize: MaxSocketBuffer,
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
func ValidateSocketBufferConfig(config SocketBufferConfig) error {
if !config.Enabled {
return nil
}
if config.SendBufferSize < MinSocketBuffer {
return fmt.Errorf("send buffer size %d is below minimum %d", config.SendBufferSize, MinSocketBuffer)
}
if config.RecvBufferSize < MinSocketBuffer {
return fmt.Errorf("receive buffer size %d is below minimum %d", config.RecvBufferSize, MinSocketBuffer)
}
if config.SendBufferSize > MaxSocketBuffer {
return fmt.Errorf("send buffer size %d exceeds maximum %d", config.SendBufferSize, MaxSocketBuffer)
}
if config.RecvBufferSize > MaxSocketBuffer {
return fmt.Errorf("receive buffer size %d exceeds maximum %d", config.RecvBufferSize, MaxSocketBuffer)
}
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
sendSize, recvSize, err := GetSocketBufferSizes(conn)
if err != nil {
// Log error but don't fail
return
}
// Record buffer sizes
socketBufferSizeGauge.WithLabelValues(component, "send").Set(float64(sendSize))
socketBufferSizeGauge.WithLabelValues(component, "receive").Set(float64(recvSize))
}
// RecordSocketBufferOverflow records a socket buffer overflow event
func RecordSocketBufferOverflow(component, bufferType string) {
socketBufferOverflowCounter.WithLabelValues(component, bufferType).Inc()
}
// UpdateSocketBufferUtilization updates socket buffer utilization metrics
func UpdateSocketBufferUtilization(component, bufferType string, utilizationPercent float64) {
socketBufferUtilizationGauge.WithLabelValues(component, bufferType).Set(utilizationPercent)
}

View File

@ -1,314 +0,0 @@
package audio
import (
"sync"
"sync/atomic"
"unsafe"
)
// ZeroCopyAudioFrame represents an audio frame that can be passed between
// components without copying the underlying data
type ZeroCopyAudioFrame struct {
data []byte
length int
capacity int
refCount int32
mutex sync.RWMutex
pooled bool
}
// ZeroCopyFramePool manages reusable zero-copy audio frames
type ZeroCopyFramePool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
counter int64 // Frame counter (atomic)
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
// Other fields
pool sync.Pool
maxSize int
mutex sync.RWMutex
// Memory optimization fields
preallocated []*ZeroCopyAudioFrame // Pre-allocated frames for immediate use
preallocSize int // Number of pre-allocated frames
maxPoolSize int // Maximum pool size to prevent memory bloat
}
// NewZeroCopyFramePool creates a new zero-copy frame pool
func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
// Pre-allocate 15 frames for immediate availability
preallocSize := 15
maxPoolSize := 50 // Limit total pool size
preallocated := make([]*ZeroCopyAudioFrame, 0, preallocSize)
// Pre-allocate frames to reduce initial allocation overhead
for i := 0; i < preallocSize; i++ {
frame := &ZeroCopyAudioFrame{
data: make([]byte, 0, maxFrameSize),
capacity: maxFrameSize,
pooled: true,
}
preallocated = append(preallocated, frame)
}
return &ZeroCopyFramePool{
maxSize: maxFrameSize,
preallocated: preallocated,
preallocSize: preallocSize,
maxPoolSize: maxPoolSize,
pool: sync.Pool{
New: func() interface{} {
return &ZeroCopyAudioFrame{
data: make([]byte, 0, maxFrameSize),
capacity: maxFrameSize,
pooled: true,
}
},
},
}
}
// Get retrieves a zero-copy frame from the pool
func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
// First try pre-allocated frames for fastest access
p.mutex.Lock()
if len(p.preallocated) > 0 {
frame := p.preallocated[len(p.preallocated)-1]
p.preallocated = p.preallocated[:len(p.preallocated)-1]
p.mutex.Unlock()
frame.mutex.Lock()
frame.refCount = 1
frame.length = 0
frame.data = frame.data[:0]
frame.mutex.Unlock()
atomic.AddInt64(&p.hitCount, 1)
return frame
}
p.mutex.Unlock()
// Try sync.Pool next
frame := p.pool.Get().(*ZeroCopyAudioFrame)
frame.mutex.Lock()
frame.refCount = 1
frame.length = 0
frame.data = frame.data[:0]
frame.mutex.Unlock()
atomic.AddInt64(&p.hitCount, 1)
return frame
}
// Put returns a zero-copy frame to the pool
func (p *ZeroCopyFramePool) Put(frame *ZeroCopyAudioFrame) {
if frame == nil || !frame.pooled {
return
}
frame.mutex.Lock()
frame.refCount--
if frame.refCount <= 0 {
frame.refCount = 0
frame.length = 0
frame.data = frame.data[:0]
frame.mutex.Unlock()
// First try to return to pre-allocated pool for fastest reuse
p.mutex.Lock()
if len(p.preallocated) < p.preallocSize {
p.preallocated = append(p.preallocated, frame)
p.mutex.Unlock()
return
}
p.mutex.Unlock()
// Check pool size limit to prevent excessive memory usage
p.mutex.RLock()
currentCount := atomic.LoadInt64(&p.counter)
p.mutex.RUnlock()
if currentCount >= int64(p.maxPoolSize) {
return // Pool is full, let GC handle this frame
}
// Return to sync.Pool
p.pool.Put(frame)
atomic.AddInt64(&p.counter, 1)
} else {
frame.mutex.Unlock()
}
}
// Data returns the frame data as a slice (zero-copy view)
func (f *ZeroCopyAudioFrame) Data() []byte {
f.mutex.RLock()
defer f.mutex.RUnlock()
return f.data[:f.length]
}
// SetData sets the frame data (zero-copy if possible)
func (f *ZeroCopyAudioFrame) SetData(data []byte) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(data) > f.capacity {
// Need to reallocate - not zero-copy but necessary
f.data = make([]byte, len(data))
f.capacity = len(data)
f.pooled = false // Can't return to pool anymore
}
// Zero-copy assignment when data fits in existing buffer
if cap(f.data) >= len(data) {
f.data = f.data[:len(data)]
copy(f.data, data)
} else {
f.data = append(f.data[:0], data...)
}
f.length = len(data)
return nil
}
// SetDataDirect sets frame data using direct buffer assignment (true zero-copy)
// WARNING: The caller must ensure the buffer remains valid for the frame's lifetime
func (f *ZeroCopyAudioFrame) SetDataDirect(data []byte) {
f.mutex.Lock()
defer f.mutex.Unlock()
f.data = data
f.length = len(data)
f.capacity = cap(data)
f.pooled = false // Direct assignment means we can't pool this frame
}
// AddRef increments the reference count for shared access
func (f *ZeroCopyAudioFrame) AddRef() {
f.mutex.Lock()
f.refCount++
f.mutex.Unlock()
}
// Release decrements the reference count
func (f *ZeroCopyAudioFrame) Release() {
f.mutex.Lock()
f.refCount--
f.mutex.Unlock()
}
// Length returns the current data length
func (f *ZeroCopyAudioFrame) Length() int {
f.mutex.RLock()
defer f.mutex.RUnlock()
return f.length
}
// Capacity returns the buffer capacity
func (f *ZeroCopyAudioFrame) Capacity() int {
f.mutex.RLock()
defer f.mutex.RUnlock()
return f.capacity
}
// UnsafePointer returns an unsafe pointer to the data for CGO calls
// WARNING: Only use this for CGO interop, ensure frame lifetime
func (f *ZeroCopyAudioFrame) UnsafePointer() unsafe.Pointer {
f.mutex.RLock()
defer f.mutex.RUnlock()
if len(f.data) == 0 {
return nil
}
return unsafe.Pointer(&f.data[0])
}
// Global zero-copy frame pool
// GetZeroCopyPoolStats returns detailed statistics about the zero-copy frame pool
func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
p.mutex.RLock()
preallocatedCount := len(p.preallocated)
currentCount := atomic.LoadInt64(&p.counter)
p.mutex.RUnlock()
hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount)
totalRequests := hitCount + missCount
var hitRate float64
if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * 100
}
return ZeroCopyFramePoolStats{
MaxFrameSize: p.maxSize,
MaxPoolSize: p.maxPoolSize,
CurrentPoolSize: currentCount,
PreallocatedCount: int64(preallocatedCount),
PreallocatedMax: int64(p.preallocSize),
HitCount: hitCount,
MissCount: missCount,
HitRate: hitRate,
}
}
// ZeroCopyFramePoolStats provides detailed zero-copy pool statistics
type ZeroCopyFramePoolStats struct {
MaxFrameSize int
MaxPoolSize int
CurrentPoolSize int64
PreallocatedCount int64
PreallocatedMax int64
HitCount int64
MissCount int64
HitRate float64 // Percentage
}
var (
globalZeroCopyPool = NewZeroCopyFramePool(MaxAudioFrameSize)
)
// GetZeroCopyFrame gets a frame from the global pool
func GetZeroCopyFrame() *ZeroCopyAudioFrame {
return globalZeroCopyPool.Get()
}
// GetGlobalZeroCopyPoolStats returns statistics for the global zero-copy pool
func GetGlobalZeroCopyPoolStats() ZeroCopyFramePoolStats {
return globalZeroCopyPool.GetZeroCopyPoolStats()
}
// PutZeroCopyFrame returns a frame to the global pool
func PutZeroCopyFrame(frame *ZeroCopyAudioFrame) {
globalZeroCopyPool.Put(frame)
}
// ZeroCopyAudioReadEncode performs audio read and encode with zero-copy optimization
func ZeroCopyAudioReadEncode() (*ZeroCopyAudioFrame, error) {
frame := GetZeroCopyFrame()
// Ensure frame has enough capacity
if frame.Capacity() < MaxAudioFrameSize {
// Reallocate if needed
frame.data = make([]byte, MaxAudioFrameSize)
frame.capacity = MaxAudioFrameSize
frame.pooled = false
}
// Use unsafe pointer for direct CGO call
n, err := CGOAudioReadEncode(frame.data[:MaxAudioFrameSize])
if err != nil {
PutZeroCopyFrame(frame)
return nil, err
}
if n == 0 {
PutZeroCopyFrame(frame)
return nil, nil
}
// Set the actual data length
frame.mutex.Lock()
frame.length = n
frame.data = frame.data[:n]
frame.mutex.Unlock()
return frame, nil
}

View File

@ -1,72 +0,0 @@
//go:build ignore
package main
import (
"archive/zip"
"bytes"
"flag"
"fmt"
"os"
"text/template"
)
var tmpl = `// Code generated by "go run gen.go". DO NOT EDIT.
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go
package tzdata
var TimeZones = []string{
{{- range . }}
"{{.}}",
{{- end }}
}
`
var filename = flag.String("output", "tzdata.go", "output file name")
func main() {
flag.Parse()
path := os.Getenv("ZONEINFO")
if path == "" {
fmt.Println("ZONEINFO is not set")
os.Exit(1)
}
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("ZONEINFO %s does not exist\n", path)
os.Exit(1)
}
zipfile, err := zip.OpenReader(path)
if err != nil {
fmt.Printf("Error opening ZONEINFO %s: %v\n", path, err)
os.Exit(1)
}
defer zipfile.Close()
timezones := []string{}
for _, file := range zipfile.File {
timezones = append(timezones, file.Name)
}
var buf bytes.Buffer
tmpl, err := template.New("tzdata").Parse(tmpl)
if err != nil {
fmt.Printf("Error parsing template: %v\n", err)
os.Exit(1)
}
err = tmpl.Execute(&buf, timezones)
if err != nil {
fmt.Printf("Error executing template: %v\n", err)
os.Exit(1)
}
err = os.WriteFile(*filename, buf.Bytes(), 0644)
if err != nil {
fmt.Printf("Error writing file %s: %v\n", *filename, err)
os.Exit(1)
}
}

View File

@ -1,602 +0,0 @@
// Code generated by "go run gen.go". DO NOT EDIT.
//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run gen.go -output tzdata.go
package tzdata
var TimeZones = []string{
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmara",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Timbuktu",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/ComodRivadavia",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Atikokan",
"America/Atka",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Ensenada",
"America/Fort_Nelson",
"America/Fort_Wayne",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Knox_IN",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montreal",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Acre",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Rosario",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Shiprock",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Virgin",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/South_Pole",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Ashkhabad",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Chongqing",
"Asia/Chungking",
"Asia/Colombo",
"Asia/Dacca",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Harbin",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Istanbul",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kashgar",
"Asia/Kathmandu",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macao",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Tel_Aviv",
"Asia/Thimbu",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ujung_Pandang",
"Asia/Ulaanbaatar",
"Asia/Ulan_Bator",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Faroe",
"Atlantic/Jan_Mayen",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/ACT",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Canberra",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/LHI",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/NSW",
"Australia/North",
"Australia/Perth",
"Australia/Queensland",
"Australia/South",
"Australia/Sydney",
"Australia/Tasmania",
"Australia/Victoria",
"Australia/West",
"Australia/Yancowinna",
"Brazil/Acre",
"Brazil/DeNoronha",
"Brazil/East",
"Brazil/West",
"CET",
"CST6CDT",
"Canada/Atlantic",
"Canada/Central",
"Canada/Eastern",
"Canada/Mountain",
"Canada/Newfoundland",
"Canada/Pacific",
"Canada/Saskatchewan",
"Canada/Yukon",
"Chile/Continental",
"Chile/EasterIsland",
"Cuba",
"EET",
"EST",
"EST5EDT",
"Egypt",
"Eire",
"Etc/GMT",
"Etc/GMT+0",
"Etc/GMT+1",
"Etc/GMT+10",
"Etc/GMT+11",
"Etc/GMT+12",
"Etc/GMT+2",
"Etc/GMT+3",
"Etc/GMT+4",
"Etc/GMT+5",
"Etc/GMT+6",
"Etc/GMT+7",
"Etc/GMT+8",
"Etc/GMT+9",
"Etc/GMT-0",
"Etc/GMT-1",
"Etc/GMT-10",
"Etc/GMT-11",
"Etc/GMT-12",
"Etc/GMT-13",
"Etc/GMT-14",
"Etc/GMT-2",
"Etc/GMT-3",
"Etc/GMT-4",
"Etc/GMT-5",
"Etc/GMT-6",
"Etc/GMT-7",
"Etc/GMT-8",
"Etc/GMT-9",
"Etc/GMT0",
"Etc/Greenwich",
"Etc/UCT",
"Etc/UTC",
"Etc/Universal",
"Etc/Zulu",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belfast",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Kyiv",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Nicosia",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Tiraspol",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Factory",
"GB",
"GB-Eire",
"GMT",
"GMT+0",
"GMT-0",
"GMT0",
"Greenwich",
"HST",
"Hongkong",
"Iceland",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Iran",
"Israel",
"Jamaica",
"Japan",
"Kwajalein",
"Libya",
"MET",
"MST",
"MST7MDT",
"Mexico/BajaNorte",
"Mexico/BajaSur",
"Mexico/General",
"NZ",
"NZ-CHAT",
"Navajo",
"PRC",
"PST8PDT",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kanton",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Pohnpei",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Samoa",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
"Pacific/Yap",
"Poland",
"Portugal",
"ROC",
"ROK",
"Singapore",
"Turkey",
"UCT",
"US/Alaska",
"US/Aleutian",
"US/Arizona",
"US/Central",
"US/East-Indiana",
"US/Eastern",
"US/Hawaii",
"US/Indiana-Starke",
"US/Michigan",
"US/Mountain",
"US/Pacific",
"US/Samoa",
"UTC",
"Universal",
"W-SU",
"WET",
"Zulu",
}

View File

@ -1,156 +1,39 @@
package kvm
import (
"fmt"
"math/rand"
"time"
_ "time/tzdata"
"github.com/go-co-op/gocron/v2"
"github.com/jetkvm/kvm/internal/tzdata"
)
type JigglerConfig struct {
InactivityLimitSeconds int `json:"inactivity_limit_seconds"`
JitterPercentage int `json:"jitter_percentage"`
ScheduleCronTab string `json:"schedule_cron_tab"`
Timezone string `json:"timezone,omitempty"`
}
var lastUserInput = time.Now()
var jigglerEnabled = false
var jobDelta time.Duration = 0
var scheduler gocron.Scheduler = nil
func rpcSetJigglerState(enabled bool) {
jigglerEnabled = enabled
}
func rpcGetJigglerState() bool {
return jigglerEnabled
}
func rpcGetTimezones() []string {
return tzdata.TimeZones
}
func rpcGetJigglerConfig() (JigglerConfig, error) {
return *config.JigglerConfig, nil
}
func rpcSetJigglerConfig(jigglerConfig JigglerConfig) error {
logger.Info().Msgf("jigglerConfig: %v, %v, %v, %v", jigglerConfig.InactivityLimitSeconds, jigglerConfig.JitterPercentage, jigglerConfig.ScheduleCronTab, jigglerConfig.Timezone)
config.JigglerConfig = &jigglerConfig
err := removeExistingCrobJobs(scheduler)
if err != nil {
return fmt.Errorf("error removing cron jobs from scheduler %v", err)
}
err = runJigglerCronTab()
if err != nil {
return fmt.Errorf("error scheduling jiggler crontab: %v", err)
}
err = SaveConfig()
if err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}
func removeExistingCrobJobs(s gocron.Scheduler) error {
for _, j := range s.Jobs() {
err := s.RemoveJob(j.ID())
if err != nil {
return err
}
}
return nil
}
func initJiggler() {
ensureConfigLoaded()
err := runJigglerCronTab()
if err != nil {
logger.Error().Msgf("Error scheduling jiggler crontab: %v", err)
return
}
}
func runJigglerCronTab() error {
cronTab := config.JigglerConfig.ScheduleCronTab
// Apply timezone if specified and valid
if config.JigglerConfig.Timezone != "" && config.JigglerConfig.Timezone != "UTC" {
// Validate timezone before applying
if _, err := time.LoadLocation(config.JigglerConfig.Timezone); err != nil {
logger.Warn().Msgf("Invalid timezone '%s', falling back to UTC: %v", config.JigglerConfig.Timezone, err)
// Don't add TZ prefix, let it run in UTC
} else {
cronTab = fmt.Sprintf("TZ=%s %s", config.JigglerConfig.Timezone, cronTab)
}
}
s, err := gocron.NewScheduler()
if err != nil {
return err
}
scheduler = s
_, err = s.NewJob(
gocron.CronJob(
cronTab,
true,
),
gocron.NewTask(
func() {
runJiggler()
},
),
)
if err != nil {
return err
}
s.Start()
delta, err := calculateJobDelta(s)
jobDelta = delta
logger.Info().Msgf("Time between jiggler runs: %v", jobDelta)
if err != nil {
return err
}
return nil
go runJiggler()
}
func runJiggler() {
if jigglerEnabled {
if config.JigglerConfig.JitterPercentage != 0 {
jitter := calculateJitterDuration(jobDelta)
time.Sleep(jitter)
}
inactivitySeconds := config.JigglerConfig.InactivityLimitSeconds
timeSinceLastInput := time.Since(gadget.GetLastUserInputTime())
logger.Debug().Msgf("Time since last user input %v", timeSinceLastInput)
if timeSinceLastInput > time.Duration(inactivitySeconds)*time.Second {
logger.Debug().Msg("Jiggling mouse...")
//TODO: change to rel mouse
err := rpcAbsMouseReport(1, 1, 0)
if err != nil {
logger.Warn().Msgf("Failed to jiggle mouse: %v", err)
}
err = rpcAbsMouseReport(0, 0, 0)
if err != nil {
logger.Warn().Msgf("Failed to reset mouse position: %v", err)
for {
if jigglerEnabled {
if time.Since(lastUserInput) > 20*time.Second {
//TODO: change to rel mouse
err := rpcAbsMouseReport(1, 1, 0)
if err != nil {
logger.Warn().Err(err).Msg("Failed to jiggle mouse")
}
err = rpcAbsMouseReport(0, 0, 0)
if err != nil {
logger.Warn().Err(err).Msg("Failed to reset mouse position")
}
}
}
time.Sleep(20 * time.Second)
}
}
func calculateJobDelta(s gocron.Scheduler) (time.Duration, error) {
j := s.Jobs()[0]
runs, err := j.NextRuns(2)
if err != nil {
return 0.0, err
}
return runs[1].Sub(runs[0]), nil
}
func calculateJitterDuration(delta time.Duration) time.Duration {
jitter := rand.Float64() * float64(config.JigglerConfig.JitterPercentage) / 100 * delta.Seconds()
return time.Duration(jitter * float64(time.Second))
}

View File

@ -1091,9 +1091,6 @@ var rpcHandlers = map[string]RPCHandler{
"rpcMountBuiltInImage": {Func: rpcMountBuiltInImage, Params: []string{"filename"}},
"setJigglerState": {Func: rpcSetJigglerState, Params: []string{"enabled"}},
"getJigglerState": {Func: rpcGetJigglerState},
"setJigglerConfig": {Func: rpcSetJigglerConfig, Params: []string{"jigglerConfig"}},
"getJigglerConfig": {Func: rpcGetJigglerConfig},
"getTimezones": {Func: rpcGetTimezones},
"sendWOLMagicPacket": {Func: rpcSendWOLMagicPacket, Params: []string{"macAddress"}},
"getStreamQualityFactor": {Func: rpcGetStreamQualityFactor},
"setStreamQualityFactor": {Func: rpcSetStreamQualityFactor, Params: []string{"factor"}},

View File

@ -31,9 +31,6 @@ func runAudioServer() {
}
func startAudioSubprocess() error {
// Start adaptive buffer management for optimal performance
audio.StartAdaptiveBuffering()
// Create audio server supervisor
audioSupervisor = audio.NewAudioServerSupervisor()
@ -62,8 +59,6 @@ func startAudioSubprocess() error {
// Stop audio relay when process exits
audio.StopAudioRelay()
// Stop adaptive buffering
audio.StopAdaptiveBuffering()
},
// onRestart
func(attempt int, delay time.Duration) {

877
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "kvm-ui",
"private": true,
"version": "2025.08.07.001",
"version": "0.0.0",
"type": "module",
"engines": {
"node": "22.15.0"
@ -19,7 +19,7 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^2.2.7",
"@headlessui/react": "^2.2.4",
"@headlessui/tailwindcss": "^0.2.2",
"@heroicons/react": "^2.2.0",
"@vitejs/plugin-basic-ssl": "^2.1.0",
@ -33,16 +33,16 @@
"dayjs": "^1.11.13",
"eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^11.0.4",
"framer-motion": "^12.23.12",
"framer-motion": "^12.23.3",
"lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4",
"react": "^19.1.1",
"react": "^19.1.0",
"react-animate-height": "^3.2.3",
"react-dom": "^19.1.1",
"react-dom": "^19.1.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0",
"react-router-dom": "^6.22.3",
"react-simple-keyboard": "^3.8.106",
"react-simple-keyboard": "^3.8.93",
"react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10",
"recharts": "^2.15.3",
@ -54,21 +54,21 @@
"devDependencies": {
"@eslint/compat": "^1.3.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.32.0",
"@eslint/js": "^9.30.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.11",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/semver": "^7.7.0",
"@types/validator": "^13.15.2",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@typescript-eslint/eslint-plugin": "^8.36.0",
"@typescript-eslint/parser": "^8.36.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"autoprefixer": "^10.4.21",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint": "^9.30.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
@ -78,7 +78,7 @@
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.11",
"typescript": "^5.9.2",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-tsconfig-paths": "^5.1.4"
}

View File

@ -27,7 +27,7 @@ export default function FieldLabel({
>
{label}
{description && (
<span className="mb-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
<span className="my-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
{description}
</span>
)}
@ -36,11 +36,11 @@ export default function FieldLabel({
} else if (as === "span") {
return (
<div className="flex select-none flex-col">
<span className="font-display text-[13px] font-semibold leading-snug text-black dark:text-white">
<span className="font-display text-[13px] font-medium leading-snug text-black dark:text-white">
{label}
</span>
{description && (
<span className="mb-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
<span className="my-0.5 text-[13px] font-normal text-slate-600 dark:text-slate-400">
{description}
</span>
)}
@ -49,4 +49,4 @@ export default function FieldLabel({
} else {
return <></>;
}
}
}

View File

@ -26,7 +26,7 @@ type InputFieldProps = {
type InputFieldWithLabelProps = InputFieldProps & {
label: React.ReactNode;
description?: React.ReactNode | string | null;
description?: string | null;
};
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(

View File

@ -1,176 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import { LuExternalLink } from "react-icons/lu";
import { Button, LinkButton } from "@components/Button";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { InputFieldWithLabel } from "./InputField";
import { SelectMenuBasic } from "./SelectMenuBasic";
export interface JigglerConfig {
inactivity_limit_seconds: number;
jitter_percentage: number;
schedule_cron_tab: string;
timezone?: string;
}
export function JigglerSetting({
onSave,
defaultJigglerState,
}: {
onSave: (jigglerConfig: JigglerConfig) => void;
defaultJigglerState?: JigglerConfig;
}) {
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>(
defaultJigglerState || {
inactivity_limit_seconds: 20,
jitter_percentage: 0,
schedule_cron_tab: "*/20 * * * * *",
timezone: "UTC",
},
);
const { send } = useJsonRpc();
const [timezones, setTimezones] = useState<string[]>([]);
useEffect(() => {
send("getTimezones", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
setTimezones(resp.result as string[]);
});
}, [send]);
const timezoneOptions = useMemo(
() =>
timezones.map((timezone: string) => ({
value: timezone,
label: timezone,
})),
[timezones],
);
const exampleConfigs = [
{
name: "Business Hours 9-17",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 9-17 * * 1-5",
timezone: jigglerConfigState.timezone || "UTC",
},
},
{
name: "Business Hours 8-17",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 8-17 * * 1-5",
timezone: jigglerConfigState.timezone || "UTC",
},
},
];
return (
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
Examples
</h4>
<div className="flex flex-wrap gap-2">
{exampleConfigs.map((example, index) => (
<Button
key={index}
size="XS"
theme="light"
text={example.name}
onClick={() => setJigglerConfigState(example.config)}
/>
))}
<LinkButton
to="https://crontab.guru/examples.html"
size="XS"
theme="light"
text="More examples"
LeadingIcon={LuExternalLink}
/>
</div>
</div>
<div className="grid grid-cols-1 items-end gap-4 md:grid-cols-2">
<InputFieldWithLabel
required
size="SM"
label="Cron Schedule"
description="Cron expression for scheduling"
placeholder="*/20 * * * * *"
value={jigglerConfigState.schedule_cron_tab}
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
schedule_cron_tab: e.target.value,
})
}
/>
<InputFieldWithLabel
size="SM"
label="Inactivity Limit Seconds"
description="Inactivity time before jiggle"
value={jigglerConfigState.inactivity_limit_seconds}
type="number"
min="1"
max="100"
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
inactivity_limit_seconds: Number(e.target.value),
})
}
/>
<InputFieldWithLabel
required
size="SM"
label="Random delay"
description="To avoid recognizable patterns"
placeholder="25"
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
value={jigglerConfigState.jitter_percentage}
type="number"
min="0"
max="100"
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
jitter_percentage: Number(e.target.value),
})
}
/>
<SelectMenuBasic
size="SM"
label="Timezone"
description="Timezone for cron schedule"
value={jigglerConfigState.timezone || "UTC"}
disabled={timezones.length === 0}
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
timezone: e.target.value,
})
}
options={timezoneOptions}
/>
</div>
<div className="flex gap-x-2">
<Button
size="SM"
theme="primary"
text="Save Jiggler Config"
onClick={() => onSave(jigglerConfigState)}
/>
</div>
</div>
);
}

View File

@ -10,7 +10,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
export default function MacroBar() {
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();
const { executeMacro } = useKeyboard();
const { send } = useJsonRpc();
const [send] = useJsonRpc();
useEffect(() => {
setSendFn(send);

View File

@ -26,7 +26,7 @@ type SelectMenuProps = Pick<
const sizes = {
XS: "h-[24.5px] pl-3 pr-8 text-xs",
SM: "h-[36px] pl-3 pr-8 text-[13px]",
SM: "h-[32px] pl-3 pr-8 text-[13px]",
MD: "h-[40px] pl-4 pr-10 text-sm",
LG: "h-[48px] pl-4 pr-10 px-5 text-base",
};
@ -62,7 +62,7 @@ export const SelectMenuBasic = React.forwardRef<HTMLSelectElement, SelectMenuPro
"text-sm",
)}
>
{label && <FieldLabel label={label} id={id} />}
{label && <FieldLabel label={label} id={id} as="span" />}
<Card className="w-auto border! border-solid border-slate-800/30! shadow-xs outline-0 dark:border-slate-300/30!">
<select
ref={ref}

View File

@ -1,11 +0,0 @@
export default function SettingsNestedSection({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="ml-2 border-l border-slate-800/30 pl-4 dark:border-slate-300/30">
{children}
</div>
);
}

View File

@ -1,6 +1,6 @@
import { useCallback , useEffect, useState } from "react";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import { useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { SettingsItem } from "../routes/devices.$id.settings";
@ -59,7 +59,7 @@ const usbPresets = [
];
export function UsbDeviceSetting() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [loading, setLoading] = useState(false);
const [usbDeviceConfig, setUsbDeviceConfig] =
@ -67,7 +67,7 @@ export function UsbDeviceSetting() {
const [selectedPreset, setSelectedPreset] = useState<string>("default");
const syncUsbDeviceConfig = useCallback(() => {
send("getUsbDevices", {}, (resp: JsonRpcResponse) => {
send("getUsbDevices", {}, resp => {
if ("error" in resp) {
console.error("Failed to load USB devices:", resp.error);
notifications.error(
@ -97,7 +97,7 @@ export function UsbDeviceSetting() {
const handleUsbConfigChange = useCallback(
(devices: UsbDeviceConfig) => {
setLoading(true);
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
send("setUsbDevices", { devices }, async resp => {
if ("error" in resp) {
notifications.error(
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`,

View File

@ -4,7 +4,7 @@ import { Button } from "@components/Button";
import { UsbConfigState } from "../hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import { useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { SettingsItem } from "../routes/devices.$id.settings";
@ -54,7 +54,7 @@ const usbConfigs = [
type UsbConfigMap = Record<string, USBConfig>;
export function UsbInfoSetting() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [loading, setLoading] = useState(false);
const [usbConfigProduct, setUsbConfigProduct] = useState("");
@ -94,7 +94,7 @@ export function UsbInfoSetting() {
);
const syncUsbConfigProduct = useCallback(() => {
send("getUsbConfig", {}, (resp: JsonRpcResponse) => {
send("getUsbConfig", {}, resp => {
if ("error" in resp) {
console.error("Failed to load USB Config:", resp.error);
notifications.error(
@ -114,7 +114,7 @@ export function UsbInfoSetting() {
const handleUsbConfigChange = useCallback(
(usbConfig: USBConfig) => {
setLoading(true);
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
send("setUsbConfig", { usbConfig }, async resp => {
if ("error" in resp) {
notifications.error(
`Failed to set usb config: ${resp.error.data || "Unknown error"}`,
@ -137,7 +137,7 @@ export function UsbInfoSetting() {
);
useEffect(() => {
send("getDeviceID", {}, async (resp: JsonRpcResponse) => {
send("getDeviceID", {}, async resp => {
if ("error" in resp) {
return notifications.error(
`Failed to get device ID: ${resp.error.data || "Unknown error"}`,
@ -205,10 +205,10 @@ function USBConfigDialog({
product: "",
});
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const syncUsbConfig = useCallback(() => {
send("getUsbConfig", {}, (resp: JsonRpcResponse) => {
send("getUsbConfig", {}, resp => {
if ("error" in resp) {
console.error("Failed to load USB Config:", resp.error);
} else {

View File

@ -99,7 +99,7 @@ export default function WebRTCVideo({ microphone }: WebRTCVideoProps) {
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
// Misc states and hooks
const { send } = useJsonRpc();
const [send] = useJsonRpc();
// Video-related
useResizeObserver({

View File

@ -7,7 +7,7 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";
import LoadingSpinner from "@/components/LoadingSpinner";
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
import { useJsonRpc } from "../../hooks/useJsonRpc";
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
@ -23,7 +23,7 @@ export function ATXPowerControl() {
> | null>(null);
const [atxState, setAtxState] = useState<ATXState | null>(null);
const { send } = useJsonRpc(function onRequest(resp) {
const [send] = useJsonRpc(function onRequest(resp) {
if (resp.method === "atxState") {
setAtxState(resp.params as ATXState);
}
@ -31,7 +31,7 @@ export function ATXPowerControl() {
// Request initial state
useEffect(() => {
send("getATXState", {}, (resp: JsonRpcResponse) => {
send("getATXState", {}, resp => {
if ("error" in resp) {
notifications.error(
`Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
@ -54,7 +54,7 @@ export function ATXPowerControl() {
const timer = setTimeout(() => {
// Send long press action
console.log("Sending long press ATX power action");
send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => {
send("setATXPowerAction", { action: "power-long" }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
@ -75,7 +75,7 @@ export function ATXPowerControl() {
// Send short press action
console.log("Sending short press ATX power action");
send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => {
send("setATXPowerAction", { action: "power-short" }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
@ -127,7 +127,7 @@ export function ATXPowerControl() {
LeadingIcon={LuRotateCcw}
text="Reset"
onClick={() => {
send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => {
send("setATXPowerAction", { action: "reset" }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,

View File

@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { Button } from "@components/Button";
import Card from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import FieldLabel from "@components/FieldLabel";
import LoadingSpinner from "@components/LoadingSpinner";
@ -19,11 +19,11 @@ interface DCPowerState {
}
export function DCPowerControl() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [powerState, setPowerState] = useState<DCPowerState | null>(null);
const getDCPowerState = useCallback(() => {
send("getDCPowerState", {}, (resp: JsonRpcResponse) => {
send("getDCPowerState", {}, resp => {
if ("error" in resp) {
notifications.error(
`Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
@ -35,7 +35,7 @@ export function DCPowerControl() {
}, [send]);
const handlePowerToggle = (enabled: boolean) => {
send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => {
send("setDCPowerState", { enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
@ -47,7 +47,7 @@ export function DCPowerControl() {
};
const handleRestoreChange = (state: number) => {
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => {
send("setDCRestoreState", { state }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { Button } from "@components/Button";
import Card from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { useUiStore } from "@/hooks/stores";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
@ -17,7 +17,7 @@ interface SerialSettings {
}
export function SerialConsole() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [settings, setSettings] = useState<SerialSettings>({
baudRate: "9600",
dataBits: "8",
@ -26,7 +26,7 @@ export function SerialConsole() {
});
useEffect(() => {
send("getSerialSettings", {}, (resp: JsonRpcResponse) => {
send("getSerialSettings", {}, resp => {
if ("error" in resp) {
notifications.error(
`Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
@ -39,7 +39,7 @@ export function SerialConsole() {
const handleSettingChange = (setting: keyof SerialSettings, value: string) => {
const newSettings = { ...settings, [setting]: value };
send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => {
send("setSerialSettings", { settings: newSettings }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to update serial settings: ${resp.error.data || "Unknown error"}`,

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import Card, { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
@ -39,12 +39,12 @@ const AVAILABLE_EXTENSIONS: Extension[] = [
];
export default function ExtensionPopover() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [activeExtension, setActiveExtension] = useState<Extension | null>(null);
// Load active extension on component mount
useEffect(() => {
send("getActiveExtension", {}, (resp: JsonRpcResponse) => {
send("getActiveExtension", {}, resp => {
if ("error" in resp) return;
const extensionId = resp.result as string;
if (extensionId) {
@ -57,7 +57,7 @@ export default function ExtensionPopover() {
}, [send]);
const handleSetActiveExtension = (extension: Extension | null) => {
send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => {
send("setActiveExtension", { extensionId: extension?.id || "" }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set active extension: ${resp.error.data || "Unknown error"}`,

View File

@ -16,13 +16,13 @@ import Card, { GridCard } from "@components/Card";
import { formatters } from "@/utils";
import { RemoteVirtualMediaState, useMountMediaStore, useRTCStore } from "@/hooks/stores";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import notifications from "@/notifications";
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const diskDataChannelStats = useRTCStore(state => state.diskDataChannelStats);
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const { remoteVirtualMediaState, setModalView, setRemoteVirtualMediaState } =
useMountMediaStore();
@ -47,7 +47,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
}, [diskDataChannelStats]);
const syncRemoteVirtualMediaState = useCallback(() => {
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
send("getVirtualMediaState", {}, response => {
if ("error" in response) {
notifications.error(
`Failed to get virtual media state: ${response.error.message}`,
@ -59,7 +59,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
}, [send, setRemoteVirtualMediaState]);
const handleUnmount = () => {
send("unmountImage", {}, (response: JsonRpcResponse) => {
send("unmountImage", {}, response => {
if ("error" in response) {
notifications.error(`Failed to unmount image: ${response.error.message}`);
} else {

View File

@ -7,7 +7,7 @@ import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import { TextAreaWithLabel } from "@components/TextArea";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
import { keys, modifiers } from "@/keyboardMappings";
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
@ -28,7 +28,7 @@ export default function PasteModal() {
const setPasteMode = useHidStore(state => state.setPasteModeEnabled);
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const [invalidChars, setInvalidChars] = useState<string[]>([]);
@ -47,7 +47,7 @@ export default function PasteModal() {
}, [keyboardLayout]);
useEffect(() => {
send("getKeyboardLayout", {}, (resp: JsonRpcResponse) => {
send("getKeyboardLayout", {}, resp => {
if ("error" in resp) return;
setKeyboardLayout(resp.result as string);
});

View File

@ -3,7 +3,7 @@ import { useClose } from "@headlessui/react";
import { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@/hooks/stores";
import notifications from "@/notifications";
@ -18,7 +18,7 @@ export default function WakeOnLanModal() {
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const close = useClose();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
@ -33,7 +33,7 @@ export default function WakeOnLanModal() {
setErrorMessage(null);
if (rpcDataChannel?.readyState !== "open") return;
send("sendWOLMagicPacket", { macAddress }, (resp: JsonRpcResponse) => {
send("sendWOLMagicPacket", { macAddress }, resp => {
if ("error" in resp) {
const isInvalid = resp.error.data?.includes("invalid MAC address");
if (isInvalid) {
@ -52,7 +52,7 @@ export default function WakeOnLanModal() {
);
const syncStoredDevices = useCallback(() => {
send("getWakeOnLanDevices", {}, (resp: JsonRpcResponse) => {
send("getWakeOnLanDevices", {}, resp => {
if ("result" in resp) {
setStoredDevices(resp.result as StoredDevice[]);
} else {
@ -70,7 +70,7 @@ export default function WakeOnLanModal() {
(index: number) => {
const updatedDevices = storedDevices.filter((_, i) => i !== index);
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, resp => {
if ("error" in resp) {
console.error("Failed to update Wake-on-LAN devices:", resp.error);
} else {
@ -86,7 +86,7 @@ export default function WakeOnLanModal() {
if (!name || !macAddress) return;
const updatedDevices = [...storedDevices, { name, macAddress }];
console.log("updatedDevices", updatedDevices);
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, resp => {
if ("error" in resp) {
console.error("Failed to add Wake-on-LAN device:", resp.error);
setAddDeviceErrorMessage("Failed to add device");

View File

@ -853,7 +853,7 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
try {
await new Promise<void>((resolve, reject) => {
sendFn("getKeyboardMacros", {}, (response: JsonRpcResponse) => {
sendFn("getKeyboardMacros", {}, response => {
if (response.error) {
console.error("Error loading macros:", response.error);
reject(new Error(response.error.message));

View File

@ -78,5 +78,5 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
};
}, [rpcDataChannel, onRequest]);
return { send };
return [send];
}

View File

@ -5,7 +5,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
import { keys, modifiers } from "@/keyboardMappings";
export default function useKeyboard() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const updateActiveKeysAndModifiers = useHidStore(

View File

@ -27,7 +27,7 @@ import NetBootIcon from "@/assets/netboot-icon.svg";
import Fieldset from "@/components/Fieldset";
import { DEVICE_API } from "@/ui.config";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import { useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { isOnDevice } from "../main";
import { cx } from "../cva.config";
@ -64,10 +64,10 @@ export function Dialog({ onClose }: { onClose: () => void }) {
setRemoteVirtualMediaState(null);
}
const { send } = useJsonRpc();
const [send] = useJsonRpc();
async function syncRemoteVirtualMediaState() {
return new Promise((resolve, reject) => {
send("getVirtualMediaState", {}, (resp: JsonRpcResponse) => {
send("getVirtualMediaState", {}, resp => {
if ("error" in resp) {
reject(new Error(resp.error.message));
} else {
@ -89,7 +89,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
console.log(`Mounting ${url} as ${mode}`);
setMountInProgress(true);
send("mountWithHTTP", { url, mode }, async (resp: JsonRpcResponse) => {
send("mountWithHTTP", { url, mode }, async resp => {
if ("error" in resp) triggerError(resp.error.message);
clearMountMediaState();
@ -108,7 +108,7 @@ export function Dialog({ onClose }: { onClose: () => void }) {
console.log(`Mounting ${fileName} as ${mode}`);
setMountInProgress(true);
send("mountWithStorage", { filename: fileName, mode }, async (resp: JsonRpcResponse) => {
send("mountWithStorage", { filename: fileName, mode }, async resp => {
if ("error" in resp) triggerError(resp.error.message);
clearMountMediaState();
@ -526,13 +526,8 @@ function UrlView({
icon: UbuntuIcon,
},
{
name: "Debian 13 Trixie",
url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.0.0-amd64-netinst.iso",
icon: DebianIcon,
},
{
name: "Debian 12 Bookworm (old-stable)",
url: "https://cdimage.debian.org/mirror/cdimage/archive/12.11.0/amd64/iso-cd/debian-12.11.0-amd64-netinst.iso",
name: "Debian 12",
url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.9.0-amd64-netinst.iso",
icon: DebianIcon,
},
{
@ -689,7 +684,7 @@ function DeviceFileView({
const [currentPage, setCurrentPage] = useState(1);
const filesPerPage = 5;
const { send } = useJsonRpc();
const [send] = useJsonRpc();
interface StorageSpace {
bytesUsed: number;
@ -718,12 +713,12 @@ function DeviceFileView({
}, [storageSpace]);
const syncStorage = useCallback(() => {
send("listStorageFiles", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Error listing storage files: ${resp.error}`);
send("listStorageFiles", {}, res => {
if ("error" in res) {
notifications.error(`Error listing storage files: ${res.error}`);
return;
}
const { files } = resp.result as StorageFiles;
const { files } = res.result as StorageFiles;
const formattedFiles = files.map(file => ({
name: file.filename,
size: formatters.bytes(file.size),
@ -733,13 +728,13 @@ function DeviceFileView({
setOnStorageFiles(formattedFiles);
});
send("getStorageSpace", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Error getting storage space: ${resp.error}`);
send("getStorageSpace", {}, res => {
if ("error" in res) {
notifications.error(`Error getting storage space: ${res.error}`);
return;
}
const space = resp.result as StorageSpace;
const space = res.result as StorageSpace;
setStorageSpace(space);
});
}, [send, setOnStorageFiles, setStorageSpace]);
@ -762,9 +757,9 @@ function DeviceFileView({
function handleDeleteFile(file: { name: string; size: string; createdAt: string }) {
console.log("Deleting file:", file);
send("deleteStorageFile", { filename: file.name }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(`Error deleting file: ${resp.error}`);
send("deleteStorageFile", { filename: file.name }, res => {
if ("error" in res) {
notifications.error(`Error deleting file: ${res.error}`);
return;
}
@ -1001,7 +996,7 @@ function UploadFileView({
const [fileError, setFileError] = useState<string | null>(null);
const [uploadError, setUploadError] = useState<string | null>(null);
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const rtcDataChannelRef = useRef<RTCDataChannel | null>(null);
useEffect(() => {
@ -1216,7 +1211,7 @@ function UploadFileView({
setUploadState("uploading");
console.log("Upload state set to 'uploading'");
send("startStorageFileUpload", { filename: file.name, size: file.size }, (resp: JsonRpcResponse) => {
send("startStorageFileUpload", { filename: file.name, size: file.size }, resp => {
console.log("startStorageFileUpload response:", resp);
if ("error" in resp) {
console.error("Upload error:", resp.error.message);

View File

@ -12,7 +12,7 @@ import { SettingsSectionHeader } from "@/components/SettingsSectionHeader";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import notifications from "@/notifications";
import { DEVICE_API } from "@/ui.config";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { isOnDevice } from "@/main";
import { TextAreaWithLabel } from "@components/TextArea";
@ -42,7 +42,7 @@ export default function SettingsAccessIndexRoute() {
const { navigateTo } = useDeviceUiNavigation();
const navigate = useNavigate();
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [isAdopted, setAdopted] = useState(false);
const [deviceId, setDeviceId] = useState<string | null>(null);
@ -56,7 +56,7 @@ export default function SettingsAccessIndexRoute() {
const [tlsKey, setTlsKey] = useState<string>("");
const getCloudState = useCallback(() => {
send("getCloudState", {}, (resp: JsonRpcResponse) => {
send("getCloudState", {}, resp => {
if ("error" in resp) return console.error(resp.error);
const cloudState = resp.result as CloudState;
setAdopted(cloudState.connected);
@ -77,7 +77,7 @@ export default function SettingsAccessIndexRoute() {
}, [send]);
const getTLSState = useCallback(() => {
send("getTLSState", {}, (resp: JsonRpcResponse) => {
send("getTLSState", {}, resp => {
if ("error" in resp) return console.error(resp.error);
const tlsState = resp.result as TLSState;
@ -88,7 +88,7 @@ export default function SettingsAccessIndexRoute() {
}, [send]);
const deregisterDevice = async () => {
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
send("deregisterDevice", {}, resp => {
if ("error" in resp) {
notifications.error(
`Failed to de-register device: ${resp.error.data || "Unknown error"}`,
@ -110,7 +110,7 @@ export default function SettingsAccessIndexRoute() {
return;
}
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
@ -156,7 +156,7 @@ export default function SettingsAccessIndexRoute() {
state.privateKey = key;
}
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
send("setTLSState", { state }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`,
@ -198,7 +198,7 @@ export default function SettingsAccessIndexRoute() {
getCloudState();
getTLSState();
send("getDeviceID", {}, async (resp: JsonRpcResponse) => {
send("getDeviceID", {}, async resp => {
if ("error" in resp) return console.error(resp.error);
setDeviceId(resp.result as string);
});

View File

@ -8,14 +8,14 @@ import { ConfirmDialog } from "../components/ConfirmDialog";
import { SettingsPageHeader } from "../components/SettingsPageheader";
import { TextAreaWithLabel } from "../components/TextArea";
import { useSettingsStore } from "../hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import { useJsonRpc } from "../hooks/useJsonRpc";
import { isOnDevice } from "../main";
import notifications from "../notifications";
import { SettingsItem } from "./devices.$id.settings";
export default function SettingsAdvancedRoute() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [sshKey, setSSHKey] = useState<string>("");
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
@ -27,35 +27,35 @@ export default function SettingsAdvancedRoute() {
const settings = useSettingsStore();
useEffect(() => {
send("getDevModeState", {}, (resp: JsonRpcResponse) => {
send("getDevModeState", {}, resp => {
if ("error" in resp) return;
const result = resp.result as { enabled: boolean };
setDeveloperMode(result.enabled);
});
send("getSSHKeyState", {}, (resp: JsonRpcResponse) => {
send("getSSHKeyState", {}, resp => {
if ("error" in resp) return;
setSSHKey(resp.result as string);
});
send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
send("getUsbEmulationState", {}, resp => {
if ("error" in resp) return;
setUsbEmulationEnabled(resp.result as boolean);
});
send("getDevChannelState", {}, (resp: JsonRpcResponse) => {
send("getDevChannelState", {}, resp => {
if ("error" in resp) return;
setDevChannel(resp.result as boolean);
});
send("getLocalLoopbackOnly", {}, (resp: JsonRpcResponse) => {
send("getLocalLoopbackOnly", {}, resp => {
if ("error" in resp) return;
setLocalLoopbackOnly(resp.result as boolean);
});
}, [send, setDeveloperMode]);
const getUsbEmulationState = useCallback(() => {
send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
send("getUsbEmulationState", {}, resp => {
if ("error" in resp) return;
setUsbEmulationEnabled(resp.result as boolean);
});
@ -63,7 +63,7 @@ export default function SettingsAdvancedRoute() {
const handleUsbEmulationToggle = useCallback(
(enabled: boolean) => {
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
send("setUsbEmulationState", { enabled: enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
@ -78,7 +78,7 @@ export default function SettingsAdvancedRoute() {
);
const handleResetConfig = useCallback(() => {
send("resetConfig", {}, (resp: JsonRpcResponse) => {
send("resetConfig", {}, resp => {
if ("error" in resp) {
notifications.error(
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
@ -90,7 +90,7 @@ export default function SettingsAdvancedRoute() {
}, [send]);
const handleUpdateSSHKey = useCallback(() => {
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
send("setSSHKeyState", { sshKey }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
@ -103,7 +103,7 @@ export default function SettingsAdvancedRoute() {
const handleDevModeChange = useCallback(
(developerMode: boolean) => {
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
send("setDevModeState", { enabled: developerMode }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
@ -118,7 +118,7 @@ export default function SettingsAdvancedRoute() {
const handleDevChannelChange = useCallback(
(enabled: boolean) => {
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
send("setDevChannelState", { enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
@ -133,7 +133,7 @@ export default function SettingsAdvancedRoute() {
const applyLoopbackOnlyMode = useCallback(
(enabled: boolean) => {
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
send("setLocalLoopbackOnly", { enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,

View File

@ -1,7 +1,7 @@
import { useState , useEffect } from "react";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { SettingsPageHeader } from "../components/SettingsPageheader";
import { Button } from "../components/Button";
@ -13,7 +13,7 @@ import { useDeviceStore } from "../hooks/stores";
import { SettingsItem } from "./devices.$id.settings";
export default function SettingsGeneralRoute() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const { navigateTo } = useDeviceUiNavigation();
const [autoUpdate, setAutoUpdate] = useState(true);
@ -24,14 +24,14 @@ export default function SettingsGeneralRoute() {
});
useEffect(() => {
send("getAutoUpdateState", {}, (resp: JsonRpcResponse) => {
send("getAutoUpdateState", {}, resp => {
if ("error" in resp) return;
setAutoUpdate(resp.result as boolean);
});
}, [send]);
const handleAutoUpdateChange = (enabled: boolean) => {
send("setAutoUpdateState", { enabled }, (resp: JsonRpcResponse) => {
send("setAutoUpdateState", { enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set auto-update: ${resp.error.data || "Unknown error"}`,

View File

@ -6,7 +6,7 @@ import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate();
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary

View File

@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import Card from "@/components/Card";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
import { UpdateState, useDeviceStore, useUpdateStore } from "@/hooks/stores";
import notifications from "@/notifications";
@ -16,7 +16,7 @@ export default function SettingsGeneralUpdateRoute() {
const { updateSuccess } = location.state || {};
const { setModalView, otaState } = useUpdateStore();
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const onConfirmUpdate = useCallback(() => {
send("tryUpdate", {});
@ -134,14 +134,14 @@ function LoadingState({
}) {
const [progressWidth, setProgressWidth] = useState("0%");
const abortControllerRef = useRef<AbortController | null>(null);
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const setAppVersion = useDeviceStore(state => state.setAppVersion);
const setSystemVersion = useDeviceStore(state => state.setSystemVersion);
const getVersionInfo = useCallback(() => {
return new Promise<SystemVersionInfo>((resolve, reject) => {
send("getUpdateStatus", {}, async (resp: JsonRpcResponse) => {
send("getUpdateStatus", {}, async resp => {
if ("error" in resp) {
notifications.error(`Failed to check for updates: ${resp.error}`);
reject(new Error("Failed to check for updates"));

View File

@ -3,7 +3,7 @@ import { useEffect } from "react";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsItem } from "@routes/devices.$id.settings";
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
@ -12,7 +12,7 @@ import { UsbInfoSetting } from "../components/UsbInfoSetting";
import { FeatureFlag } from "../components/FeatureFlag";
export default function SettingsHardwareRoute() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const settings = useSettingsStore();
const setDisplayRotation = useSettingsStore(state => state.setDisplayRotation);
@ -23,7 +23,7 @@ export default function SettingsHardwareRoute() {
};
const handleDisplayRotationSave = () => {
send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, (resp: JsonRpcResponse) => {
send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
@ -48,7 +48,7 @@ export default function SettingsHardwareRoute() {
};
const handleBacklightSettingsSave = () => {
send("setBacklightSettings", { params: settings.backlightSettings }, (resp: JsonRpcResponse) => {
send("setBacklightSettings", { params: settings.backlightSettings }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
@ -60,7 +60,7 @@ export default function SettingsHardwareRoute() {
};
useEffect(() => {
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
send("getBacklightSettings", {}, resp => {
if ("error" in resp) {
return notifications.error(
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo } from "react";
import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { keyboardOptions } from "@/keyboardLayouts";
@ -39,10 +39,10 @@ export default function SettingsKeyboardRoute() {
{ value: "host", label: "Host Only" },
];
const { send } = useJsonRpc();
const [send] = useJsonRpc();
useEffect(() => {
send("getKeyboardLayout", {}, (resp: JsonRpcResponse) => {
send("getKeyboardLayout", {}, resp => {
if ("error" in resp) return;
setKeyboardLayout(resp.result as string);
});
@ -51,7 +51,7 @@ export default function SettingsKeyboardRoute() {
const onKeyboardLayoutChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const layout = e.target.value;
send("setKeyboardLayout", { layout }, (resp: JsonRpcResponse) => {
send("setKeyboardLayout", { layout }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`,

View File

@ -1,68 +1,21 @@
import { CheckCircleIcon } from "@heroicons/react/16/solid";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import MouseIcon from "@/assets/mouse-icon.svg";
import PointingFinger from "@/assets/pointing-finger.svg";
import { GridCard } from "@/components/Card";
import { Checkbox } from "@/components/Checkbox";
import { useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { JigglerSetting } from "@components/JigglerSetting";
import { useFeatureFlag } from "../hooks/useFeatureFlag";
import { cx } from "../cva.config";
import notifications from "../notifications";
import SettingsNestedSection from "../components/SettingsNestedSection";
import { SettingsItem } from "./devices.$id.settings";
export interface JigglerConfig {
inactivity_limit_seconds: number;
jitter_percentage: number;
schedule_cron_tab: string;
timezone?: string;
}
const jigglerOptions = [
{ value: "disabled", label: "Disabled", config: null },
{
value: "frequent",
label: "Frequent - 30s",
config: {
inactivity_limit_seconds: 30,
jitter_percentage: 25,
schedule_cron_tab: "*/30 * * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
},
},
{
value: "standard",
label: "Standard - 1m",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
},
},
{
value: "light",
label: "Light - 5m",
config: {
inactivity_limit_seconds: 300,
jitter_percentage: 25,
schedule_cron_tab: "0 */5 * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
},
},
] as const;
type JigglerValues = (typeof jigglerOptions)[number]["value"] | "custom";
export default function SettingsMouseRoute() {
const hideCursor = useSettingsStore(state => state.isCursorHidden);
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
@ -70,13 +23,13 @@ export default function SettingsMouseRoute() {
const mouseMode = useSettingsStore(state => state.mouseMode);
const setMouseMode = useSettingsStore(state => state.setMouseMode);
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
const setScrollThrottling = useSettingsStore(state => state.setScrollThrottling);
const { isEnabled: isScrollSensitivityEnabled } = useFeatureFlag("0.3.8");
const [selectedJigglerOption, setSelectedJigglerOption] =
useState<JigglerValues | null>(null);
const [currentJigglerConfig, setCurrentJigglerConfig] = useState<JigglerConfig | null>(
null,
const [jiggler, setJiggler] = useState(false);
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
const setScrollThrottling = useSettingsStore(
state => state.setScrollThrottling,
);
const scrollThrottlingOptions = [
@ -87,100 +40,25 @@ export default function SettingsMouseRoute() {
{ value: "100", label: "Very High" },
];
const { send } = useJsonRpc();
const syncJigglerSettings = useCallback(() => {
send("getJigglerState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
const isEnabled = resp.result as boolean;
// If the jiggler is disabled, set the selected option to "disabled" and nothing else
if (!isEnabled) return setSelectedJigglerOption("disabled");
send("getJigglerConfig", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return;
const result = resp.result as JigglerConfig;
setCurrentJigglerConfig(result);
const value = jigglerOptions.find(
o =>
o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds &&
o?.config?.jitter_percentage === result.jitter_percentage &&
o?.config?.schedule_cron_tab === result.schedule_cron_tab,
)?.value;
setSelectedJigglerOption(value || "custom");
});
});
}, [send]);
const [send] = useJsonRpc();
useEffect(() => {
syncJigglerSettings();
}, [syncJigglerSettings]);
send("getJigglerState", {}, resp => {
if ("error" in resp) return;
setJiggler(resp.result as boolean);
});
}, [isScrollSensitivityEnabled, send]);
const saveJigglerConfig = useCallback(
(jigglerConfig: JigglerConfig) => {
// We assume the jiggler should be set to enabled if the config is being updated
send("setJigglerState", { enabled: true }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
return notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
);
}
});
send("setJigglerConfig", { jigglerConfig }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
const errorMsg = resp.error.data || "Unknown error";
// Check for cron syntax errors and provide user-friendly message
if (
errorMsg.includes("invalid syntax") ||
errorMsg.includes("parse failure") ||
errorMsg.includes("invalid cron")
) {
return notifications.error(
"Invalid cron expression. Please check your schedule format (e.g., '0 * * * * *' for every minute).",
);
}
return notifications.error(`Failed to set jiggler config: ${errorMsg}`);
}
notifications.success(`Jiggler Config successfully updated`);
syncJigglerSettings();
});
},
[send, syncJigglerSettings],
);
const handleJigglerChange = (option: JigglerValues) => {
if (option === "custom") {
setSelectedJigglerOption("custom");
// We don't need to sync the jiggler settings when the option is "custom". The user will press "Save" to save the custom settings.
return;
}
// We don't need to update the device jiggler state when the option is "disabled"
if (option === "disabled") {
send("setJigglerState", { enabled: false }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
return notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
);
}
});
notifications.success(`Jiggler Config successfully updated`);
return setSelectedJigglerOption("disabled");
}
const jigglerConfig = jigglerOptions.find(o => o.value === option)?.config;
if (!jigglerConfig) {
return notifications.error("There was an error setting the jiggler config");
}
saveJigglerConfig(jigglerConfig);
const handleJigglerChange = (enabled: boolean) => {
send("setJigglerState", { enabled }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set jiggler state: ${resp.error.data || "Unknown error"}`,
);
return;
}
setJiggler(enabled);
});
};
return (
@ -201,49 +79,30 @@ export default function SettingsMouseRoute() {
/>
</SettingsItem>
<SettingsItem
title="Scroll Throttling"
description="Reduce the frequency of scroll events"
>
<SelectMenuBasic
size="SM"
label=""
className="max-w-[292px]"
value={scrollThrottling}
fullWidth
onChange={e => setScrollThrottling(parseInt(e.target.value))}
options={scrollThrottlingOptions}
/>
</SettingsItem>
<SettingsItem
title="Scroll Throttling"
description="Reduce the frequency of scroll events"
title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
>
<SelectMenuBasic
size="SM"
label=""
className="max-w-[292px]"
value={scrollThrottling}
fullWidth
onChange={e => setScrollThrottling(parseInt(e.target.value))}
options={scrollThrottlingOptions}
<Checkbox
checked={jiggler}
onChange={e => handleJigglerChange(e.target.checked)}
/>
</SettingsItem>
<SettingsItem title="Jiggler" description="Simulate movement of a computer mouse">
<SelectMenuBasic
size="SM"
label=""
value={selectedJigglerOption || "disabled"}
options={[
...jigglerOptions.map(option => ({
value: option.value,
label: option.label,
})),
{ value: "custom", label: "Custom" },
]}
onChange={e => {
handleJigglerChange(
e.target.value as (typeof jigglerOptions)[number]["value"],
);
}}
/>
</SettingsItem>
{selectedJigglerOption === "custom" && (
<SettingsNestedSection>
<JigglerSetting
onSave={saveJigglerConfig}
defaultJigglerState={currentJigglerConfig || undefined}
/>
</SettingsNestedSection>
)}
<div className="space-y-4">
<SettingsItem title="Modes" description="Choose the mouse input mode" />
<div className="flex items-center gap-4">

View File

@ -13,7 +13,7 @@ import {
TimeSyncMode,
useNetworkStateStore,
} from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import InputField, { InputFieldWithLabel } from "@components/InputField";
@ -72,7 +72,7 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
}
export default function SettingsNetworkRoute() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [networkState, setNetworkState] = useNetworkStateStore(state => [
state,
state.setNetworkState,
@ -104,7 +104,7 @@ export default function SettingsNetworkRoute() {
const getNetworkSettings = useCallback(() => {
setNetworkSettingsLoaded(false);
send("getNetworkSettings", {}, (resp: JsonRpcResponse) => {
send("getNetworkSettings", {}, resp => {
if ("error" in resp) return;
console.log(resp.result);
setNetworkSettings(resp.result as NetworkSettings);
@ -117,7 +117,7 @@ export default function SettingsNetworkRoute() {
}, [send]);
const getNetworkState = useCallback(() => {
send("getNetworkState", {}, (resp: JsonRpcResponse) => {
send("getNetworkState", {}, resp => {
if ("error" in resp) return;
console.log(resp.result);
setNetworkState(resp.result as NetworkState);
@ -127,7 +127,7 @@ export default function SettingsNetworkRoute() {
const setNetworkSettingsRemote = useCallback(
(settings: NetworkSettings) => {
setNetworkSettingsLoaded(false);
send("setNetworkSettings", { settings }, (resp: JsonRpcResponse) => {
send("setNetworkSettings", { settings }, resp => {
if ("error" in resp) {
notifications.error(
"Failed to save network settings: " +
@ -148,7 +148,7 @@ export default function SettingsNetworkRoute() {
);
const handleRenewLease = useCallback(() => {
send("renewDHCPLease", {}, (resp: JsonRpcResponse) => {
send("renewDHCPLease", {}, resp => {
if ("error" in resp) {
notifications.error("Failed to renew lease: " + resp.error.message);
} else {

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { Button } from "@/components/Button";
import { TextAreaWithLabel } from "@/components/TextArea";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores";
@ -41,7 +41,7 @@ const streamQualityOptions = [
];
export default function SettingsVideoRoute() {
const { send } = useJsonRpc();
const [send] = useJsonRpc();
const [streamQuality, setStreamQuality] = useState("1");
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
const [edid, setEdid] = useState<string | null>(null);
@ -55,12 +55,12 @@ export default function SettingsVideoRoute() {
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
useEffect(() => {
send("getStreamQualityFactor", {}, (resp: JsonRpcResponse) => {
send("getStreamQualityFactor", {}, resp => {
if ("error" in resp) return;
setStreamQuality(String(resp.result));
});
send("getEDID", {}, (resp: JsonRpcResponse) => {
send("getEDID", {}, resp => {
if ("error" in resp) {
notifications.error(`Failed to get EDID: ${resp.error.data || "Unknown error"}`);
return;
@ -85,7 +85,7 @@ export default function SettingsVideoRoute() {
}, [send]);
const handleStreamQualityChange = (factor: string) => {
send("setStreamQualityFactor", { factor: Number(factor) }, (resp: JsonRpcResponse) => {
send("setStreamQualityFactor", { factor: Number(factor) }, resp => {
if ("error" in resp) {
notifications.error(
`Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
@ -99,7 +99,7 @@ export default function SettingsVideoRoute() {
};
const handleEDIDChange = (newEdid: string) => {
send("setEDID", { edid: newEdid }, (resp: JsonRpcResponse) => {
send("setEDID", { edid: newEdid }, resp => {
if ("error" in resp) {
notifications.error(`Failed to set EDID: ${resp.error.data || "Unknown error"}`);
return;

View File

@ -39,7 +39,7 @@ import { checkAuth, isInCloud, isOnDevice } from "@/main";
import DashboardNavbar from "@components/Header";
import ConnectionStatsSidebar from "@/components/sidebar/connectionStats";
import AudioMetricsSidebar from "@/components/sidebar/AudioMetricsSidebar";
import { JsonRpcRequest, JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { JsonRpcRequest, useJsonRpc } from "@/hooks/useJsonRpc";
import Terminal from "@components/Terminal";
import { CLOUD_API, DEVICE_API } from "@/ui.config";
@ -653,11 +653,11 @@ export default function KvmIdRoute() {
}
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const { send } = useJsonRpc(onJsonRpcRequest);
const [send] = useJsonRpc(onJsonRpcRequest);
useEffect(() => {
if (rpcDataChannel?.readyState !== "open") return;
send("getVideoState", {}, (resp: JsonRpcResponse) => {
send("getVideoState", {}, resp => {
if ("error" in resp) return;
setHdmiState(resp.result as Parameters<VideoState["setHdmiState"]>[0]);
});
@ -669,7 +669,7 @@ export default function KvmIdRoute() {
if (keyboardLedState !== undefined) return;
console.log("Requesting keyboard led state");
send("getKeyboardLedState", {}, (resp: JsonRpcResponse) => {
send("getKeyboardLedState", {}, resp => {
if ("error" in resp) {
// -32601 means the method is not supported
if (resp.error.code === -32601) {
@ -742,7 +742,7 @@ export default function KvmIdRoute() {
useEffect(() => {
if (appVersion) return;
send("getUpdateStatus", {}, async (resp: JsonRpcResponse) => {
send("getUpdateStatus", {}, async resp => {
if ("error" in resp) {
notifications.error(`Failed to get device version: ${resp.error}`);
return

3
web.go
View File

@ -457,9 +457,6 @@ func setupRouter() *gin.Engine {
})
})
// Audio memory allocation metrics endpoint
protected.GET("/audio/memory-metrics", gin.WrapF(audio.HandleMemoryMetrics))
protected.GET("/microphone/process-metrics", func(c *gin.Context) {
if currentSession == nil || currentSession.AudioInputManager == nil {
c.JSON(200, gin.H{

View File

@ -29,13 +29,14 @@ type Session struct {
DiskChannel *webrtc.DataChannel
AudioInputManager *audio.AudioInputManager
shouldUmountVirtualMedia bool
// Microphone operation throttling
micCooldown time.Duration
// Audio frame processing
audioFrameChan chan []byte
audioStopChan chan struct{}
audioWg sync.WaitGroup
rpcQueue chan webrtc.DataChannelMessage
}
type SessionConfig struct {
@ -117,7 +118,6 @@ func newSession(config SessionConfig) (*Session, error) {
if err != nil {
return nil, err
}
session := &Session{
peerConnection: peerConnection,
AudioInputManager: audio.NewAudioInputManager(),
@ -129,21 +129,13 @@ func newSession(config SessionConfig) (*Session, error) {
// Start audio processing goroutine
session.startAudioProcessor(*logger)
session.rpcQueue = make(chan webrtc.DataChannelMessage, 256)
go func() {
for msg := range session.rpcQueue {
onRPCMessage(msg, session)
}
}()
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
scopedLogger.Info().Str("label", d.Label()).Uint16("id", *d.ID()).Msg("New DataChannel")
switch d.Label() {
case "rpc":
session.RPCChannel = d
d.OnMessage(func(msg webrtc.DataChannelMessage) {
// Enqueue to ensure ordered processing
session.rpcQueue <- msg
go onRPCMessage(msg, session)
})
triggerOTAStateUpdate()
triggerVideoStateUpdate()
@ -267,11 +259,6 @@ func newSession(config SessionConfig) (*Session, error) {
if session == currentSession {
currentSession = nil
}
// Stop RPC processor
if session.rpcQueue != nil {
close(session.rpcQueue)
session.rpcQueue = nil
}
if session.shouldUmountVirtualMedia {
err := rpcUnmountImage()
scopedLogger.Warn().Err(err).Msg("unmount image failed on connection close")