mirror of https://github.com/jetkvm/kvm.git
Compare commits
3 Commits
48e3caf690
...
2f42a24ee8
| Author | SHA1 | Date |
|---|---|---|
|
|
2f42a24ee8 | |
|
|
432303e228 | |
|
|
1dbc6c9d06 |
2
Makefile
2
Makefile
|
|
@ -1,5 +1,3 @@
|
|||
.PHONY: setup_toolchain build_audio_deps dev_env lint lint-go lint-ui lint-fix lint-go-fix lint-ui-fix ui-lint
|
||||
|
||||
# Clone the rv1106-system toolchain to $HOME/.jetkvm/rv1106-system
|
||||
setup_toolchain:
|
||||
bash tools/setup_rv1106_toolchain.sh
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
|
||||
|
||||
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse, **Audio**) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
||||
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse, Audio) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
||||
|
||||
|
||||
|
||||
|
|
@ -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 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.
|
||||
- **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.
|
||||
|
||||
|
|
@ -38,11 +38,9 @@ The best place to search for answers is our [Documentation](https://jetkvm.com/d
|
|||
|
||||
If you've found an issue and want to report it, please check our [Issues](https://github.com/jetkvm/kvm/issues) page. Make sure the description contains information about the firmware version you're using, your platform, and a clear explanation of the steps to reproduce the issue.
|
||||
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
var audioControlService *audio.AudioControlService
|
||||
|
||||
func initAudioControlService() {
|
||||
func ensureAudioControlService() *audio.AudioControlService {
|
||||
if audioControlService == nil {
|
||||
sessionProvider := &SessionProviderImpl{}
|
||||
audioControlService = audio.NewAudioControlService(sessionProvider, logger)
|
||||
|
|
@ -31,50 +31,44 @@ func initAudioControlService() {
|
|||
return nil
|
||||
})
|
||||
}
|
||||
return audioControlService
|
||||
}
|
||||
|
||||
// --- Global Convenience Functions for Audio Control ---
|
||||
|
||||
// StopAudioOutputAndRemoveTracks is a global helper to stop audio output subprocess and remove WebRTC tracks
|
||||
func StopAudioOutputAndRemoveTracks() error {
|
||||
initAudioControlService()
|
||||
return audioControlService.MuteAudio(true)
|
||||
// MuteAudioOutput is a global helper to mute audio output
|
||||
func MuteAudioOutput() error {
|
||||
return ensureAudioControlService().MuteAudio(true)
|
||||
}
|
||||
|
||||
// StartAudioOutputAndAddTracks is a global helper to start audio output subprocess and add WebRTC tracks
|
||||
func StartAudioOutputAndAddTracks() error {
|
||||
initAudioControlService()
|
||||
return audioControlService.MuteAudio(false)
|
||||
// UnmuteAudioOutput is a global helper to unmute audio output
|
||||
func UnmuteAudioOutput() error {
|
||||
return ensureAudioControlService().MuteAudio(false)
|
||||
}
|
||||
|
||||
// StopMicrophoneAndRemoveTracks is a global helper to stop microphone subprocess and remove WebRTC tracks
|
||||
func StopMicrophoneAndRemoveTracks() error {
|
||||
initAudioControlService()
|
||||
return audioControlService.StopMicrophone()
|
||||
// StopMicrophone is a global helper to stop microphone subprocess
|
||||
func StopMicrophone() error {
|
||||
return ensureAudioControlService().StopMicrophone()
|
||||
}
|
||||
|
||||
// StartMicrophoneAndAddTracks is a global helper to start microphone subprocess and add WebRTC tracks
|
||||
func StartMicrophoneAndAddTracks() error {
|
||||
initAudioControlService()
|
||||
return audioControlService.StartMicrophone()
|
||||
// StartMicrophone is a global helper to start microphone subprocess
|
||||
func StartMicrophone() error {
|
||||
return ensureAudioControlService().StartMicrophone()
|
||||
}
|
||||
|
||||
// IsAudioOutputActive is a global helper to check if audio output subprocess is running
|
||||
func IsAudioOutputActive() bool {
|
||||
initAudioControlService()
|
||||
return audioControlService.IsAudioOutputActive()
|
||||
return ensureAudioControlService().IsAudioOutputActive()
|
||||
}
|
||||
|
||||
// IsMicrophoneActive is a global helper to check if microphone subprocess is running
|
||||
func IsMicrophoneActive() bool {
|
||||
initAudioControlService()
|
||||
return audioControlService.IsMicrophoneActive()
|
||||
return ensureAudioControlService().IsMicrophoneActive()
|
||||
}
|
||||
|
||||
// ResetMicrophone is a global helper to reset the microphone
|
||||
func ResetMicrophone() error {
|
||||
initAudioControlService()
|
||||
return audioControlService.ResetMicrophone()
|
||||
return ensureAudioControlService().ResetMicrophone()
|
||||
}
|
||||
|
||||
// GetCurrentSessionAudioTrack returns the current session's audio track for audio relay
|
||||
|
|
@ -118,20 +112,20 @@ func ReplaceCurrentSessionAudioTrack(newTrack *webrtc.TrackLocalStaticSample) er
|
|||
|
||||
// SetAudioQuality is a global helper to set audio output quality
|
||||
func SetAudioQuality(quality audio.AudioQuality) error {
|
||||
initAudioControlService()
|
||||
ensureAudioControlService()
|
||||
audioControlService.SetAudioQuality(quality)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAudioQualityPresets is a global helper to get available audio quality presets
|
||||
func GetAudioQualityPresets() map[audio.AudioQuality]audio.AudioConfig {
|
||||
initAudioControlService()
|
||||
ensureAudioControlService()
|
||||
return audioControlService.GetAudioQualityPresets()
|
||||
}
|
||||
|
||||
// GetCurrentAudioQuality is a global helper to get current audio quality configuration
|
||||
func GetCurrentAudioQuality() audio.AudioConfig {
|
||||
initAudioControlService()
|
||||
ensureAudioControlService()
|
||||
return audioControlService.GetCurrentAudioQuality()
|
||||
}
|
||||
|
||||
|
|
@ -148,9 +142,9 @@ func handleAudioMute(c *gin.Context) {
|
|||
|
||||
var err error
|
||||
if req.Muted {
|
||||
err = StopAudioOutputAndRemoveTracks()
|
||||
err = MuteAudioOutput()
|
||||
} else {
|
||||
err = StartAudioOutputAndAddTracks()
|
||||
err = UnmuteAudioOutput()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -166,7 +160,7 @@ func handleAudioMute(c *gin.Context) {
|
|||
|
||||
// handleMicrophoneStart handles POST /microphone/start requests
|
||||
func handleMicrophoneStart(c *gin.Context) {
|
||||
err := StartMicrophoneAndAddTracks()
|
||||
err := StartMicrophone()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
|
@ -177,7 +171,7 @@ func handleMicrophoneStart(c *gin.Context) {
|
|||
|
||||
// handleMicrophoneStop handles POST /microphone/stop requests
|
||||
func handleMicrophoneStop(c *gin.Context) {
|
||||
err := StopMicrophoneAndRemoveTracks()
|
||||
err := StopMicrophone()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
|
@ -199,9 +193,9 @@ func handleMicrophoneMute(c *gin.Context) {
|
|||
|
||||
var err error
|
||||
if req.Muted {
|
||||
err = StopMicrophoneAndRemoveTracks()
|
||||
err = StopMicrophone()
|
||||
} else {
|
||||
err = StartMicrophoneAndAddTracks()
|
||||
err = StartMicrophone()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -225,19 +219,19 @@ func handleMicrophoneReset(c *gin.Context) {
|
|||
|
||||
// handleSubscribeAudioEvents handles WebSocket audio event subscription
|
||||
func handleSubscribeAudioEvents(connectionID string, wsCon *websocket.Conn, runCtx context.Context, l *zerolog.Logger) {
|
||||
initAudioControlService()
|
||||
ensureAudioControlService()
|
||||
audioControlService.SubscribeToAudioEvents(connectionID, wsCon, runCtx, l)
|
||||
}
|
||||
|
||||
// handleUnsubscribeAudioEvents handles WebSocket audio event unsubscription
|
||||
func handleUnsubscribeAudioEvents(connectionID string, l *zerolog.Logger) {
|
||||
initAudioControlService()
|
||||
ensureAudioControlService()
|
||||
audioControlService.UnsubscribeFromAudioEvents(connectionID, l)
|
||||
}
|
||||
|
||||
// handleAudioStatus handles GET requests for audio status
|
||||
func handleAudioStatus(c *gin.Context) {
|
||||
initAudioControlService()
|
||||
ensureAudioControlService()
|
||||
|
||||
status := audioControlService.GetAudioStatus()
|
||||
c.JSON(200, status)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
func main() {
|
||||
versionPtr := flag.Bool("version", false, "print version and exit")
|
||||
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||
audioServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
|
||||
audioOutputServerPtr := flag.Bool("audio-output-server", false, "Run as audio server subprocess")
|
||||
audioInputServerPtr := flag.Bool("audio-input-server", false, "Run as audio input server subprocess")
|
||||
|
||||
flag.Parse()
|
||||
|
|
@ -26,5 +26,5 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
kvm.Main(*audioServerPtr, *audioInputServerPtr)
|
||||
kvm.Main(*audioOutputServerPtr, *audioInputServerPtr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,36 +4,35 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
var audioMuteState struct {
|
||||
muted bool
|
||||
mu sync.RWMutex
|
||||
// AudioState holds all audio-related state with a single mutex
|
||||
type AudioState struct {
|
||||
mu sync.RWMutex
|
||||
audioMuted bool
|
||||
microphoneMuted bool
|
||||
}
|
||||
|
||||
var microphoneMuteState struct {
|
||||
muted bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
var globalAudioState = &AudioState{}
|
||||
|
||||
func SetAudioMuted(muted bool) {
|
||||
audioMuteState.mu.Lock()
|
||||
defer audioMuteState.mu.Unlock()
|
||||
audioMuteState.muted = muted
|
||||
globalAudioState.mu.Lock()
|
||||
defer globalAudioState.mu.Unlock()
|
||||
globalAudioState.audioMuted = muted
|
||||
}
|
||||
|
||||
func IsAudioMuted() bool {
|
||||
audioMuteState.mu.RLock()
|
||||
defer audioMuteState.mu.RUnlock()
|
||||
return audioMuteState.muted
|
||||
globalAudioState.mu.RLock()
|
||||
defer globalAudioState.mu.RUnlock()
|
||||
return globalAudioState.audioMuted
|
||||
}
|
||||
|
||||
func SetMicrophoneMuted(muted bool) {
|
||||
microphoneMuteState.mu.Lock()
|
||||
defer microphoneMuteState.mu.Unlock()
|
||||
microphoneMuteState.muted = muted
|
||||
globalAudioState.mu.Lock()
|
||||
defer globalAudioState.mu.Unlock()
|
||||
globalAudioState.microphoneMuted = muted
|
||||
}
|
||||
|
||||
func IsMicrophoneMuted() bool {
|
||||
microphoneMuteState.mu.RLock()
|
||||
defer microphoneMuteState.mu.RUnlock()
|
||||
return microphoneMuteState.muted
|
||||
globalAudioState.mu.RLock()
|
||||
defer globalAudioState.mu.RUnlock()
|
||||
return globalAudioState.microphoneMuted
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue