mirror of https://github.com/jetkvm/kvm.git
Refactor audio subsystem for improved maintainability
Changes: - Consolidate duplicate stop logic into helper functions - Fix RPC getAudioConfig to return actual runtime values instead of inconsistent defaults (bitrate was returning 128 vs actual 192) - Improve setAudioTrack mutex handling to eliminate nested locking - Simplify ALSA error retry logic by reorganizing conditional branches - Split CGO Connect() into separate input/output methods for clarity - Use map lookup for sample rate validation instead of long if-chain - Add inline comments documenting validation steps All changes preserve existing functionality while reducing code duplication and improving readability. Tested with both HDMI and USB audio sources.
This commit is contained in:
parent
0dbf2dfda9
commit
ee23e3bf22
138
audio.go
138
audio.go
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
var (
|
||||
audioMutex sync.Mutex
|
||||
setAudioTrackMutex sync.Mutex // Prevents concurrent setAudioTrack() calls
|
||||
inputSourceMutex sync.Mutex // Serializes Connect() and WriteMessage() calls to input source
|
||||
outputSource atomic.Pointer[audio.AudioSource]
|
||||
inputSource atomic.Pointer[audio.AudioSource]
|
||||
|
|
@ -31,9 +30,8 @@ var (
|
|||
func getAlsaDevice(source string) string {
|
||||
if source == "hdmi" {
|
||||
return "hw:0,0"
|
||||
} else {
|
||||
return "hw:1,0"
|
||||
}
|
||||
return "hw:1,0"
|
||||
}
|
||||
|
||||
func initAudio() {
|
||||
|
|
@ -49,33 +47,47 @@ func initAudio() {
|
|||
|
||||
func getAudioConfig() audio.AudioConfig {
|
||||
cfg := audio.DefaultAudioConfig()
|
||||
|
||||
// Validate and apply bitrate
|
||||
if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 {
|
||||
cfg.Bitrate = uint16(config.AudioBitrate)
|
||||
} else if config.AudioBitrate != 0 {
|
||||
audioLogger.Warn().Int("bitrate", config.AudioBitrate).Uint16("default", cfg.Bitrate).Msg("Invalid audio bitrate, using default")
|
||||
}
|
||||
|
||||
// Validate and apply complexity
|
||||
if config.AudioComplexity >= 0 && config.AudioComplexity <= 10 {
|
||||
cfg.Complexity = uint8(config.AudioComplexity)
|
||||
} else {
|
||||
audioLogger.Warn().Int("complexity", config.AudioComplexity).Uint8("default", cfg.Complexity).Msg("Invalid audio complexity, using default")
|
||||
}
|
||||
|
||||
// Apply boolean flags directly
|
||||
cfg.DTXEnabled = config.AudioDTXEnabled
|
||||
cfg.FECEnabled = config.AudioFECEnabled
|
||||
|
||||
// Validate and apply buffer periods
|
||||
if config.AudioBufferPeriods >= 2 && config.AudioBufferPeriods <= 24 {
|
||||
cfg.BufferPeriods = uint8(config.AudioBufferPeriods)
|
||||
} else if config.AudioBufferPeriods != 0 {
|
||||
audioLogger.Warn().Int("buffer_periods", config.AudioBufferPeriods).Uint8("default", cfg.BufferPeriods).Msg("Invalid buffer periods, using default")
|
||||
}
|
||||
if config.AudioSampleRate == 32000 || config.AudioSampleRate == 44100 || config.AudioSampleRate == 48000 || config.AudioSampleRate == 96000 {
|
||||
|
||||
// Validate and apply sample rate using a map for valid rates
|
||||
validRates := map[int]bool{32000: true, 44100: true, 48000: true, 96000: true}
|
||||
if validRates[config.AudioSampleRate] {
|
||||
cfg.SampleRate = uint32(config.AudioSampleRate)
|
||||
} else if config.AudioSampleRate != 0 {
|
||||
audioLogger.Warn().Int("sample_rate", config.AudioSampleRate).Uint32("default", cfg.SampleRate).Msg("Invalid sample rate, using default")
|
||||
}
|
||||
|
||||
// Validate and apply packet loss percentage
|
||||
if config.AudioPacketLossPerc >= 0 && config.AudioPacketLossPerc <= 100 {
|
||||
cfg.PacketLossPerc = uint8(config.AudioPacketLossPerc)
|
||||
} else {
|
||||
audioLogger.Warn().Int("packet_loss_perc", config.AudioPacketLossPerc).Uint8("default", cfg.PacketLossPerc).Msg("Invalid packet loss percentage, using default")
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
|
|
@ -144,32 +156,42 @@ func startInputAudioUnderMutex(alsaPlaybackDevice string) {
|
|||
}
|
||||
}
|
||||
|
||||
func stopOutputAudio() {
|
||||
// stopAudioComponents safely stops and cleans up audio components
|
||||
func stopAudioComponents(relay *atomic.Pointer[audio.OutputRelay], source *atomic.Pointer[audio.AudioSource]) {
|
||||
audioMutex.Lock()
|
||||
outRelay := outputRelay.Swap(nil)
|
||||
outSource := outputSource.Swap(nil)
|
||||
oldRelay := relay.Swap(nil)
|
||||
oldSource := source.Swap(nil)
|
||||
audioMutex.Unlock()
|
||||
|
||||
if outRelay != nil {
|
||||
outRelay.Stop()
|
||||
if oldRelay != nil {
|
||||
oldRelay.Stop()
|
||||
}
|
||||
if outSource != nil {
|
||||
(*outSource).Disconnect()
|
||||
if oldSource != nil {
|
||||
(*oldSource).Disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// stopAudioComponentsInput safely stops and cleans up input audio components
|
||||
func stopAudioComponentsInput(relay *atomic.Pointer[audio.InputRelay], source *atomic.Pointer[audio.AudioSource]) {
|
||||
audioMutex.Lock()
|
||||
oldRelay := relay.Swap(nil)
|
||||
oldSource := source.Swap(nil)
|
||||
audioMutex.Unlock()
|
||||
|
||||
if oldRelay != nil {
|
||||
oldRelay.Stop()
|
||||
}
|
||||
if oldSource != nil {
|
||||
(*oldSource).Disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
func stopOutputAudio() {
|
||||
stopAudioComponents(&outputRelay, &outputSource)
|
||||
}
|
||||
|
||||
func stopInputAudio() {
|
||||
audioMutex.Lock()
|
||||
inRelay := inputRelay.Swap(nil)
|
||||
inSource := inputSource.Swap(nil)
|
||||
audioMutex.Unlock()
|
||||
|
||||
if inRelay != nil {
|
||||
inRelay.Stop()
|
||||
}
|
||||
if inSource != nil {
|
||||
(*inSource).Disconnect()
|
||||
}
|
||||
stopAudioComponentsInput(&inputRelay, &inputSource)
|
||||
}
|
||||
|
||||
func stopAudio() {
|
||||
|
|
@ -195,15 +217,24 @@ func onWebRTCDisconnect() {
|
|||
}
|
||||
|
||||
func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) {
|
||||
setAudioTrackMutex.Lock()
|
||||
defer setAudioTrackMutex.Unlock()
|
||||
audioMutex.Lock()
|
||||
defer audioMutex.Unlock()
|
||||
|
||||
stopOutputAudio()
|
||||
// Stop output without mutex (already holding audioMutex)
|
||||
outRelay := outputRelay.Swap(nil)
|
||||
outSource := outputSource.Swap(nil)
|
||||
if outRelay != nil {
|
||||
outRelay.Stop()
|
||||
}
|
||||
if outSource != nil {
|
||||
(*outSource).Disconnect()
|
||||
}
|
||||
|
||||
currentAudioTrack = audioTrack
|
||||
|
||||
if err := startAudio(); err != nil {
|
||||
audioLogger.Error().Err(err).Msg("Failed to start with new audio track")
|
||||
// Start audio without taking mutex again (already holding audioMutex)
|
||||
if audioInitialized && activeConnections.Load() > 0 && audioOutputEnabled.Load() && currentAudioTrack != nil {
|
||||
startOutputAudioUnderMutex(getAlsaDevice(config.AudioOutputSource))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,14 +249,10 @@ func SetAudioOutputEnabled(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if enabled {
|
||||
if activeConnections.Load() > 0 {
|
||||
if enabled && activeConnections.Load() > 0 {
|
||||
return startAudio()
|
||||
}
|
||||
} else {
|
||||
stopOutputAudio()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -234,14 +261,10 @@ func SetAudioInputEnabled(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if enabled {
|
||||
if activeConnections.Load() > 0 {
|
||||
if enabled && activeConnections.Load() > 0 {
|
||||
return startAudio()
|
||||
}
|
||||
} else {
|
||||
stopInputAudio()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -290,6 +313,7 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
|
|||
trackLogger.Debug().Msg("starting input track handler")
|
||||
|
||||
for {
|
||||
// Check if we've been superseded by another track
|
||||
currentTrackID := currentInputTrack.Load()
|
||||
if currentTrackID != nil && *currentTrackID != myTrackID {
|
||||
trackLogger.Debug().
|
||||
|
|
@ -298,6 +322,7 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
|
|||
return
|
||||
}
|
||||
|
||||
// Read RTP packet
|
||||
rtpPacket, _, err := track.ReadRTP()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
|
|
@ -308,42 +333,51 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
|
|||
continue
|
||||
}
|
||||
|
||||
opusData := rtpPacket.Payload
|
||||
if len(opusData) == 0 {
|
||||
// Skip empty payloads
|
||||
if len(rtpPacket.Payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if input is disabled
|
||||
if !audioInputEnabled.Load() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Early check to avoid mutex acquisition if source is nil (optimization)
|
||||
// Process the audio packet
|
||||
if err := processInputPacket(rtpPacket.Payload); err != nil {
|
||||
trackLogger.Warn().Err(err).Msg("failed to process audio packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processInputPacket handles writing audio data to the input source
|
||||
func processInputPacket(opusData []byte) error {
|
||||
// Early check to avoid mutex acquisition if source is nil
|
||||
if inputSource.Load() == nil {
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
inputSourceMutex.Lock()
|
||||
defer inputSourceMutex.Unlock()
|
||||
|
||||
// Reload source inside mutex to ensure we have the currently active source
|
||||
// This prevents races with startInputAudioUnderMutex swapping the source
|
||||
source := inputSource.Load()
|
||||
if source == nil {
|
||||
inputSourceMutex.Unlock()
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure source is connected
|
||||
if !(*source).IsConnected() {
|
||||
if err := (*source).Connect(); err != nil {
|
||||
inputSourceMutex.Unlock()
|
||||
continue
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = (*source).WriteMessage(0, opusData)
|
||||
inputSourceMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
audioLogger.Warn().Err(err).Msg("failed to write audio message")
|
||||
// Write the message
|
||||
if err := (*source).WriteMessage(0, opusData); err != nil {
|
||||
(*source).Disconnect()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,19 +209,17 @@ static int safe_alsa_open(snd_pcm_t **handle, const char *device, snd_pcm_stream
|
|||
|
||||
attempt++;
|
||||
|
||||
if (err == -EBUSY || err == -EAGAIN) {
|
||||
precise_sleep_us(backoff_us);
|
||||
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
|
||||
} else if (err == -ENODEV || err == -ENOENT) {
|
||||
precise_sleep_us(backoff_us);
|
||||
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
|
||||
} else if (err == -EPERM || err == -EACCES) {
|
||||
precise_sleep_us(backoff_us >> 1);
|
||||
// Apply different sleep strategies based on error type
|
||||
if (err == -EPERM || err == -EACCES) {
|
||||
precise_sleep_us(backoff_us >> 1); // Shorter wait for permission errors
|
||||
} else {
|
||||
precise_sleep_us(backoff_us);
|
||||
// Exponential backoff for all retry-worthy errors
|
||||
if (err == -EBUSY || err == -EAGAIN || err == -ENODEV || err == -ENOENT) {
|
||||
backoff_us = (backoff_us < 50000) ? (backoff_us << 1) : 50000;
|
||||
}
|
||||
}
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,16 +75,13 @@ func (c *CgoSource) Connect() error {
|
|||
}
|
||||
|
||||
if c.outputDevice {
|
||||
os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice)
|
||||
return c.connectOutput()
|
||||
}
|
||||
return c.connectInput()
|
||||
}
|
||||
|
||||
dtx := C.uchar(0)
|
||||
if c.config.DTXEnabled {
|
||||
dtx = C.uchar(1)
|
||||
}
|
||||
fec := C.uchar(0)
|
||||
if c.config.FECEnabled {
|
||||
fec = C.uchar(1)
|
||||
}
|
||||
func (c *CgoSource) connectOutput() error {
|
||||
os.Setenv("ALSA_CAPTURE_DEVICE", c.alsaDevice)
|
||||
|
||||
c.logger.Debug().
|
||||
Uint16("bitrate_kbps", c.config.Bitrate).
|
||||
|
|
@ -100,14 +97,14 @@ func (c *CgoSource) Connect() error {
|
|||
C.uint(uint32(c.config.Bitrate)*1000),
|
||||
C.uchar(c.config.Complexity),
|
||||
C.uint(c.config.SampleRate),
|
||||
C.uchar(2),
|
||||
C.ushort(960),
|
||||
C.ushort(1500),
|
||||
C.uint(1000),
|
||||
C.uchar(5),
|
||||
C.uint(500000),
|
||||
dtx,
|
||||
fec,
|
||||
C.uchar(2), // capture_channels
|
||||
C.ushort(960), // frame_size
|
||||
C.ushort(1500), // max_packet_size
|
||||
C.uint(1000), // sleep_us
|
||||
C.uchar(5), // max_attempts
|
||||
C.uint(500000), // max_backoff
|
||||
boolToUchar(c.config.DTXEnabled),
|
||||
boolToUchar(c.config.FECEnabled),
|
||||
C.uchar(c.config.BufferPeriods),
|
||||
C.uchar(c.config.PacketLossPerc),
|
||||
)
|
||||
|
|
@ -117,17 +114,22 @@ func (c *CgoSource) Connect() error {
|
|||
c.logger.Error().Int("rc", int(rc)).Msg("Failed to initialize audio capture")
|
||||
return fmt.Errorf("jetkvm_audio_capture_init failed: %d", rc)
|
||||
}
|
||||
} else {
|
||||
|
||||
c.connected = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CgoSource) connectInput() error {
|
||||
os.Setenv("ALSA_PLAYBACK_DEVICE", c.alsaDevice)
|
||||
|
||||
C.update_audio_decoder_constants(
|
||||
C.uint(c.config.SampleRate),
|
||||
C.uchar(1),
|
||||
C.ushort(960),
|
||||
C.ushort(1500),
|
||||
C.uint(1000),
|
||||
C.uchar(5),
|
||||
C.uint(500000),
|
||||
C.uchar(1), // playback_channels
|
||||
C.ushort(960), // frame_size
|
||||
C.ushort(1500), // max_packet_size
|
||||
C.uint(1000), // sleep_us
|
||||
C.uchar(5), // max_attempts
|
||||
C.uint(500000), // max_backoff
|
||||
C.uchar(c.config.BufferPeriods),
|
||||
)
|
||||
|
||||
|
|
@ -136,12 +138,18 @@ func (c *CgoSource) Connect() error {
|
|||
c.logger.Error().Int("rc", int(rc)).Msg("Failed to initialize audio playback")
|
||||
return fmt.Errorf("jetkvm_audio_playback_init failed: %d", rc)
|
||||
}
|
||||
}
|
||||
|
||||
c.connected = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolToUchar(b bool) C.uchar {
|
||||
if b {
|
||||
return C.uchar(1)
|
||||
}
|
||||
return C.uchar(0)
|
||||
}
|
||||
|
||||
func (c *CgoSource) Disconnect() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// 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",
|
||||
|
|
|
|||
31
jsonrpc.go
31
jsonrpc.go
|
|
@ -1030,30 +1030,15 @@ type AudioConfigResponse struct {
|
|||
|
||||
func rpcGetAudioConfig() (AudioConfigResponse, error) {
|
||||
ensureConfigLoaded()
|
||||
bitrate := config.AudioBitrate
|
||||
if bitrate < 64 || bitrate > 256 {
|
||||
bitrate = 128
|
||||
}
|
||||
bufferPeriods := config.AudioBufferPeriods
|
||||
if bufferPeriods < 2 || bufferPeriods > 24 {
|
||||
bufferPeriods = 12
|
||||
}
|
||||
sampleRate := config.AudioSampleRate
|
||||
if sampleRate != 32000 && sampleRate != 44100 && sampleRate != 48000 && sampleRate != 96000 {
|
||||
sampleRate = 48000
|
||||
}
|
||||
packetLossPerc := config.AudioPacketLossPerc
|
||||
if packetLossPerc < 0 || packetLossPerc > 100 {
|
||||
packetLossPerc = 20
|
||||
}
|
||||
cfg := getAudioConfig()
|
||||
return AudioConfigResponse{
|
||||
Bitrate: bitrate,
|
||||
Complexity: config.AudioComplexity,
|
||||
DTXEnabled: config.AudioDTXEnabled,
|
||||
FECEnabled: config.AudioFECEnabled,
|
||||
BufferPeriods: bufferPeriods,
|
||||
SampleRate: sampleRate,
|
||||
PacketLossPerc: packetLossPerc,
|
||||
Bitrate: int(cfg.Bitrate),
|
||||
Complexity: int(cfg.Complexity),
|
||||
DTXEnabled: cfg.DTXEnabled,
|
||||
FECEnabled: cfg.FECEnabled,
|
||||
BufferPeriods: int(cfg.BufferPeriods),
|
||||
SampleRate: int(cfg.SampleRate),
|
||||
PacketLossPerc: int(cfg.PacketLossPerc),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export default function SettingsAudioRoute() {
|
|||
} = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Load boolean settings
|
||||
send("getAudioOutputEnabled", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setAudioOutputEnabled(resp.result as boolean);
|
||||
|
|
@ -68,6 +69,7 @@ export default function SettingsAudioRoute() {
|
|||
setAudioOutputSource(resp.result as string);
|
||||
});
|
||||
|
||||
// Load complex audio configuration
|
||||
send("getAudioConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
const config = resp.result as AudioConfigResult;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function SettingsVideoRoute() {
|
|||
const [edid, setEdid] = useState<string | null>(null);
|
||||
const [edidLoading, setEdidLoading] = useState(true);
|
||||
const [defaultEdid, setDefaultEdid] = useState<string>("");
|
||||
const [edids, setEdids] = useState<Array<{value: string, label: string}>>([]);
|
||||
const [edids, setEdids] = useState<{value: string, label: string}[]>([]);
|
||||
const { debugMode } = useSettingsStore();
|
||||
// Video enhancement settings from store
|
||||
const {
|
||||
|
|
|
|||
Loading…
Reference in New Issue