mirror of
https://github.com/jetkvm/kvm.git
synced 2025-09-16 08:38:14 +00:00
This change replaces all instances of GetConfig() function calls with direct access to the Config variable throughout the audio package. The modification improves performance by eliminating function call overhead and simplifies the codebase by removing unnecessary indirection. The commit also includes minor optimizations in validation logic and connection handling, while maintaining all existing functionality. Error handling remains robust with appropriate fallbacks when config values are not available. Additional improvements include: - Enhanced connection health monitoring in UnifiedAudioClient - Optimized validation functions using cached config values - Reduced memory allocations in hot paths - Improved error recovery during quality changes
319 lines
8.8 KiB
Go
319 lines
8.8 KiB
Go
package kvm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gwatts/rootcerts"
|
|
"github.com/jetkvm/kvm/internal/audio"
|
|
"github.com/pion/webrtc/v4"
|
|
)
|
|
|
|
var (
|
|
appCtx context.Context
|
|
isAudioServer bool
|
|
audioProcessDone chan struct{}
|
|
audioSupervisor *audio.AudioOutputSupervisor
|
|
)
|
|
|
|
// runAudioServer is now handled by audio.RunAudioOutputServer
|
|
// This function is kept for backward compatibility but delegates to the audio package
|
|
func runAudioServer() {
|
|
err := audio.RunAudioOutputServer()
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("audio output server failed")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func startAudioSubprocess() error {
|
|
// Initialize validation cache for optimal performance
|
|
audio.InitValidationCache()
|
|
|
|
// Start adaptive buffer management for optimal performance
|
|
audio.StartAdaptiveBuffering()
|
|
|
|
// Start goroutine monitoring to detect and prevent leaks
|
|
audio.StartGoroutineMonitoring()
|
|
|
|
// Enable batch audio processing to reduce CGO call overhead
|
|
if err := audio.EnableBatchAudioProcessing(); err != nil {
|
|
logger.Warn().Err(err).Msg("failed to enable batch audio processing")
|
|
}
|
|
|
|
// Create audio server supervisor
|
|
audioSupervisor = audio.NewAudioOutputSupervisor()
|
|
|
|
// Set the global supervisor for access from audio package
|
|
audio.SetAudioOutputSupervisor(audioSupervisor)
|
|
|
|
// Create and register audio input supervisor (but don't start it)
|
|
// Audio input will be started on-demand through the UI
|
|
audioInputSupervisor := audio.NewAudioInputSupervisor()
|
|
audio.SetAudioInputSupervisor(audioInputSupervisor)
|
|
|
|
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
|
|
config := audio.Config
|
|
audioInputSupervisor.SetOpusConfig(
|
|
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
|
|
config.AudioQualityLowOpusComplexity,
|
|
config.AudioQualityLowOpusVBR,
|
|
config.AudioQualityLowOpusSignalType,
|
|
config.AudioQualityLowOpusBandwidth,
|
|
config.AudioQualityLowOpusDTX,
|
|
)
|
|
|
|
// Note: Audio input supervisor is NOT started here - it will be started on-demand
|
|
// when the user activates microphone input through the UI
|
|
|
|
// Set up callbacks for process lifecycle events
|
|
audioSupervisor.SetCallbacks(
|
|
// onProcessStart
|
|
func(pid int) {
|
|
logger.Info().Int("pid", pid).Msg("audio server process started")
|
|
|
|
// Wait for audio output server to be fully ready before starting relay
|
|
// This prevents "no client connected" errors during quality changes
|
|
go func() {
|
|
// Give the audio output server time to initialize and start listening
|
|
// Increased delay to reduce frame drops during connection establishment
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Start audio relay system for main process
|
|
// If there's an active WebRTC session, use its audio track
|
|
var audioTrack *webrtc.TrackLocalStaticSample
|
|
if currentSession != nil && currentSession.AudioTrack != nil {
|
|
audioTrack = currentSession.AudioTrack
|
|
logger.Info().Msg("restarting audio relay with existing WebRTC audio track")
|
|
} else {
|
|
logger.Info().Msg("starting audio relay without WebRTC track (will be updated when session is created)")
|
|
}
|
|
|
|
if err := audio.StartAudioRelay(audioTrack); err != nil {
|
|
logger.Error().Err(err).Msg("failed to start audio relay")
|
|
// Retry once after additional delay if initial attempt fails
|
|
time.Sleep(1 * time.Second)
|
|
if err := audio.StartAudioRelay(audioTrack); err != nil {
|
|
logger.Error().Err(err).Msg("failed to start audio relay after retry")
|
|
}
|
|
}
|
|
}()
|
|
},
|
|
// onProcessExit
|
|
func(pid int, exitCode int, crashed bool) {
|
|
if crashed {
|
|
logger.Error().Int("pid", pid).Int("exit_code", exitCode).Msg("audio server process crashed")
|
|
} else {
|
|
logger.Info().Int("pid", pid).Msg("audio server process exited gracefully")
|
|
}
|
|
|
|
// Stop audio relay when process exits
|
|
audio.StopAudioRelay()
|
|
// Stop adaptive buffering
|
|
audio.StopAdaptiveBuffering()
|
|
// Stop goroutine monitoring
|
|
audio.StopGoroutineMonitoring()
|
|
// Disable batch audio processing
|
|
audio.DisableBatchAudioProcessing()
|
|
},
|
|
// onRestart
|
|
func(attempt int, delay time.Duration) {
|
|
logger.Warn().Int("attempt", attempt).Dur("delay", delay).Msg("restarting audio server process")
|
|
},
|
|
)
|
|
|
|
// Start the supervisor
|
|
if err := audioSupervisor.Start(); err != nil {
|
|
return fmt.Errorf("failed to start audio supervisor: %w", err)
|
|
}
|
|
|
|
// Monitor supervisor and handle cleanup
|
|
go func() {
|
|
defer close(audioProcessDone)
|
|
|
|
// Wait for supervisor to stop
|
|
for audioSupervisor.IsRunning() {
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
logger.Info().Msg("audio supervisor stopped")
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func Main(audioServer bool, audioInputServer bool) {
|
|
// Initialize channel and set audio server flag
|
|
isAudioServer = audioServer
|
|
audioProcessDone = make(chan struct{})
|
|
|
|
// If running as audio server, only initialize audio processing
|
|
if isAudioServer {
|
|
runAudioServer()
|
|
return
|
|
}
|
|
|
|
// If running as audio input server, only initialize audio input processing
|
|
if audioInputServer {
|
|
err := audio.RunAudioInputServer()
|
|
if err != nil {
|
|
logger.Error().Err(err).Msg("audio input server failed")
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
LoadConfig()
|
|
|
|
var cancel context.CancelFunc
|
|
appCtx, cancel = context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to get local version")
|
|
}
|
|
|
|
logger.Info().
|
|
Interface("system_version", systemVersionLocal).
|
|
Interface("app_version", appVersionLocal).
|
|
Msg("starting JetKVM")
|
|
|
|
go runWatchdog()
|
|
go confirmCurrentSystem()
|
|
|
|
http.DefaultClient.Timeout = 1 * time.Minute
|
|
|
|
err = rootcerts.UpdateDefaultTransport()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to load Root CA certificates")
|
|
}
|
|
logger.Info().
|
|
Int("ca_certs_loaded", len(rootcerts.Certs())).
|
|
Msg("loaded Root CA certificates")
|
|
|
|
// Initialize network
|
|
if err := initNetwork(); err != nil {
|
|
logger.Error().Err(err).Msg("failed to initialize network")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize time sync
|
|
initTimeSync()
|
|
timeSync.Start()
|
|
|
|
// Initialize mDNS
|
|
if err := initMdns(); err != nil {
|
|
logger.Error().Err(err).Msg("failed to initialize mDNS")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize native ctrl socket server
|
|
StartNativeCtrlSocketServer()
|
|
|
|
// Initialize native video socket server
|
|
StartNativeVideoSocketServer()
|
|
|
|
initPrometheus()
|
|
|
|
go func() {
|
|
err = ExtractAndRunNativeBin()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to extract and run native bin")
|
|
// (future) prepare an error message screen buffer to show on kvm screen
|
|
}
|
|
}()
|
|
|
|
// initialize usb gadget
|
|
initUsbGadget()
|
|
|
|
// Start audio subprocess
|
|
err = startAudioSubprocess()
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to start audio subprocess")
|
|
}
|
|
|
|
// Initialize session provider for audio events
|
|
initializeAudioSessionProvider()
|
|
|
|
// Initialize audio event broadcaster for WebSocket-based real-time updates
|
|
audio.InitializeAudioEventBroadcaster()
|
|
logger.Info().Msg("audio event broadcaster initialized")
|
|
|
|
if err := setInitialVirtualMediaState(); err != nil {
|
|
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
|
}
|
|
|
|
if err := initImagesFolder(); err != nil {
|
|
logger.Warn().Err(err).Msg("failed to init images folder")
|
|
}
|
|
initJiggler()
|
|
|
|
// initialize display
|
|
initDisplay()
|
|
|
|
go func() {
|
|
time.Sleep(15 * time.Minute)
|
|
for {
|
|
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
|
|
if !config.AutoUpdateEnabled {
|
|
return
|
|
}
|
|
if currentSession != nil {
|
|
logger.Debug().Msg("skipping update since a session is active")
|
|
time.Sleep(1 * time.Minute)
|
|
continue
|
|
}
|
|
includePreRelease := config.IncludePreRelease
|
|
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
|
if err != nil {
|
|
logger.Warn().Err(err).Msg("failed to auto update")
|
|
}
|
|
time.Sleep(1 * time.Hour)
|
|
}
|
|
}()
|
|
//go RunFuseServer()
|
|
go RunWebServer()
|
|
|
|
go RunWebSecureServer()
|
|
// Web secure server is started only if TLS mode is enabled
|
|
if config.TLSMode != "" {
|
|
startWebSecureServer()
|
|
}
|
|
|
|
// As websocket client already checks if the cloud token is set, we can start it here.
|
|
go RunWebsocketClient()
|
|
|
|
initSerialPort()
|
|
sigs := make(chan os.Signal, 1)
|
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
|
<-sigs
|
|
logger.Info().Msg("JetKVM Shutting Down")
|
|
|
|
// Stop audio subprocess and wait for cleanup
|
|
if !isAudioServer {
|
|
if audioSupervisor != nil {
|
|
logger.Info().Msg("stopping audio supervisor")
|
|
audioSupervisor.Stop()
|
|
}
|
|
<-audioProcessDone
|
|
} else {
|
|
audio.StopNonBlockingAudioStreaming()
|
|
}
|
|
//if fuseServer != nil {
|
|
// err := setMassStorageImage(" ")
|
|
// if err != nil {
|
|
// logger.Infof("Failed to unmount mass storage image: %v", err)
|
|
// }
|
|
// err = fuseServer.Unmount()
|
|
// if err != nil {
|
|
// logger.Infof("Failed to unmount fuse: %v", err)
|
|
// }
|
|
|
|
// os.Exit(0)
|
|
}
|