Fix critical deadlock when switching audio sources

Problem:
When switching audio sources (USB to HDMI or vice versa), the application
would hang indefinitely. This was caused by a deadlock between Go and C
layers:

1. Main thread calls SetAudioOutputSource() → stopOutputAudio()
2. stopOutputAudio() calls outputRelay.Stop() which waits for goroutine
3. Goroutine is blocked in ReadMessage() holding Go mutex
4. ReadMessage() calls blocking C function jetkvm_audio_read_encode()
5. C function is blocked reading from ALSA device
6. Disconnect() can't acquire Go mutex to clean up
7. Deadlock: Main thread waiting for goroutine, goroutine waiting for ALSA

Solution:
Release the Go mutex BEFORE calling blocking C functions in ReadMessage()
and WriteMessage(). The C layer has its own pthread mutex protection and
handles stop requests via atomic flags. This allows:
- Disconnect() to acquire the mutex immediately
- C layer to detect stop request and return quickly
- Goroutines to exit cleanly
- Audio source switching to work flawlessly

Fixes:
- internal/audio/cgo_source.go:ReadMessage() - Release mutex before C call
- internal/audio/cgo_source.go:WriteMessage() - Release mutex before C call

This fix eliminates the hang when switching between USB and HDMI audio
sources.
This commit is contained in:
Alex P 2025-11-18 01:41:39 +02:00
parent a305217e04
commit 051950f220
1 changed files with 12 additions and 4 deletions

View File

@ -167,17 +167,21 @@ func (c *CgoSource) IsConnected() bool {
}
func (c *CgoSource) ReadMessage() (uint8, []byte, error) {
// Check connection status with mutex
c.mu.Lock()
defer c.mu.Unlock()
if !c.connected {
c.mu.Unlock()
return 0, nil, fmt.Errorf("not connected")
}
if c.direction != "output" {
c.mu.Unlock()
return 0, nil, fmt.Errorf("ReadMessage only supported for output direction")
}
c.mu.Unlock()
// Call C function without holding mutex to avoid deadlock
// The C layer has its own locking and handles stop requests
opusSize := C.jetkvm_audio_read_encode(unsafe.Pointer(&c.opusBuf[0]))
if opusSize < 0 {
return 0, nil, fmt.Errorf("jetkvm_audio_read_encode failed: %d", opusSize)
@ -195,16 +199,18 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) {
}
func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
// Check connection status and validate parameters with mutex
c.mu.Lock()
defer c.mu.Unlock()
if !c.connected {
c.mu.Unlock()
return fmt.Errorf("not connected")
}
if c.direction != "input" {
c.mu.Unlock()
return fmt.Errorf("WriteMessage only supported for input direction")
}
c.mu.Unlock()
if msgType != ipcMsgTypeOpus {
return nil
@ -218,6 +224,8 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error {
return fmt.Errorf("opus packet too large: %d bytes (max 1500)", len(payload))
}
// Call C function without holding mutex to avoid deadlock
// The C layer has its own locking and handles stop requests
rc := C.jetkvm_audio_decode_write(unsafe.Pointer(&payload[0]), C.int(len(payload)))
if rc < 0 {
return fmt.Errorf("jetkvm_audio_decode_write failed: %d", rc)