mirror of https://github.com/jetkvm/kvm.git
220 lines
5.9 KiB
Go
220 lines
5.9 KiB
Go
package audio
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Global relay instance for the main process
|
|
var (
|
|
globalRelay *AudioRelay
|
|
relayMutex sync.RWMutex
|
|
)
|
|
|
|
// StartAudioRelay starts the audio relay system for the main process
|
|
// This replaces the CGO-based audio system when running in main process mode
|
|
// audioTrack can be nil initially and updated later via UpdateAudioRelayTrack
|
|
func StartAudioRelay(audioTrack AudioTrackWriter) error {
|
|
relayMutex.Lock()
|
|
defer relayMutex.Unlock()
|
|
|
|
if globalRelay != nil {
|
|
return nil // Already running
|
|
}
|
|
|
|
// Create new relay
|
|
relay := NewAudioRelay()
|
|
|
|
// Retry starting the relay with exponential backoff
|
|
// This handles cases where the subprocess hasn't created its socket yet
|
|
maxAttempts := 5
|
|
baseDelay := 200 * time.Millisecond
|
|
maxDelay := 2 * time.Second
|
|
|
|
var lastErr error
|
|
for i := 0; i < maxAttempts; i++ {
|
|
if err := relay.Start(audioTrack); err != nil {
|
|
lastErr = err
|
|
if i < maxAttempts-1 {
|
|
// Calculate exponential backoff delay
|
|
delay := time.Duration(float64(baseDelay) * (1.5 * float64(i+1)))
|
|
if delay > maxDelay {
|
|
delay = maxDelay
|
|
}
|
|
time.Sleep(delay)
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed to start audio relay after %d attempts: %w", maxAttempts, lastErr)
|
|
}
|
|
|
|
// Success
|
|
globalRelay = relay
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("failed to start audio relay after %d attempts: %w", maxAttempts, lastErr)
|
|
}
|
|
|
|
// StopAudioRelay stops the audio relay system
|
|
func StopAudioRelay() {
|
|
relayMutex.Lock()
|
|
defer relayMutex.Unlock()
|
|
|
|
if globalRelay != nil {
|
|
globalRelay.Stop()
|
|
globalRelay = nil
|
|
}
|
|
}
|
|
|
|
// SetAudioRelayMuted sets the mute state for the audio relay
|
|
func SetAudioRelayMuted(muted bool) {
|
|
relayMutex.RLock()
|
|
defer relayMutex.RUnlock()
|
|
|
|
if globalRelay != nil {
|
|
globalRelay.SetMuted(muted)
|
|
}
|
|
}
|
|
|
|
// IsAudioRelayMuted returns the current mute state of the audio relay
|
|
func IsAudioRelayMuted() bool {
|
|
relayMutex.RLock()
|
|
defer relayMutex.RUnlock()
|
|
|
|
if globalRelay != nil {
|
|
return globalRelay.IsMuted()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetAudioRelayStats returns statistics from the audio relay
|
|
func GetAudioRelayStats() (framesRelayed, framesDropped int64) {
|
|
relayMutex.RLock()
|
|
defer relayMutex.RUnlock()
|
|
|
|
if globalRelay != nil {
|
|
return globalRelay.GetStats()
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
// IsAudioRelayRunning returns whether the audio relay is currently running
|
|
func IsAudioRelayRunning() bool {
|
|
relayMutex.RLock()
|
|
defer relayMutex.RUnlock()
|
|
|
|
return globalRelay != nil
|
|
}
|
|
|
|
// UpdateAudioRelayTrack updates the WebRTC audio track for the relay
|
|
// This function is refactored to prevent mutex deadlocks during quality changes
|
|
func UpdateAudioRelayTrack(audioTrack AudioTrackWriter) error {
|
|
var needsCallback bool
|
|
var callbackFunc TrackReplacementCallback
|
|
|
|
// Critical section: minimize time holding the mutex
|
|
relayMutex.Lock()
|
|
if globalRelay == nil {
|
|
// No relay running, start one with the provided track
|
|
relay := NewAudioRelay()
|
|
if err := relay.Start(audioTrack); err != nil {
|
|
relayMutex.Unlock()
|
|
return err
|
|
}
|
|
globalRelay = relay
|
|
} else {
|
|
// Update the track in the existing relay
|
|
globalRelay.UpdateTrack(audioTrack)
|
|
}
|
|
|
|
// Capture callback state while holding mutex
|
|
needsCallback = trackReplacementCallback != nil
|
|
if needsCallback {
|
|
callbackFunc = trackReplacementCallback
|
|
}
|
|
relayMutex.Unlock()
|
|
|
|
// Execute callback outside of mutex to prevent deadlock
|
|
if needsCallback && callbackFunc != nil {
|
|
// Use goroutine with timeout to prevent blocking
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- callbackFunc(audioTrack)
|
|
}()
|
|
|
|
// Wait for callback with timeout
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
// Log error but don't fail the relay operation
|
|
// The relay can still work even if WebRTC track replacement fails
|
|
_ = err // Suppress linter warning
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
// Timeout: log warning but continue
|
|
// This prevents indefinite blocking during quality changes
|
|
_ = fmt.Errorf("track replacement callback timed out")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CurrentSessionCallback is a function type for getting the current session's audio track
|
|
type CurrentSessionCallback func() AudioTrackWriter
|
|
|
|
// TrackReplacementCallback is a function type for replacing the WebRTC audio track
|
|
type TrackReplacementCallback func(AudioTrackWriter) error
|
|
|
|
// currentSessionCallback holds the callback function to get the current session's audio track
|
|
var currentSessionCallback CurrentSessionCallback
|
|
|
|
// trackReplacementCallback holds the callback function to replace the WebRTC audio track
|
|
var trackReplacementCallback TrackReplacementCallback
|
|
|
|
// SetCurrentSessionCallback sets the callback function to get the current session's audio track
|
|
func SetCurrentSessionCallback(callback CurrentSessionCallback) {
|
|
currentSessionCallback = callback
|
|
}
|
|
|
|
// SetTrackReplacementCallback sets the callback function to replace the WebRTC audio track
|
|
func SetTrackReplacementCallback(callback TrackReplacementCallback) {
|
|
trackReplacementCallback = callback
|
|
}
|
|
|
|
// UpdateAudioRelayTrackAsync performs async track update to prevent blocking
|
|
// This is used during WebRTC session creation to avoid deadlocks
|
|
func UpdateAudioRelayTrackAsync(audioTrack AudioTrackWriter) {
|
|
go func() {
|
|
if err := UpdateAudioRelayTrack(audioTrack); err != nil {
|
|
// Log error but don't block session creation
|
|
_ = err // Suppress linter warning
|
|
}
|
|
}()
|
|
}
|
|
|
|
// connectRelayToCurrentSession connects the audio relay to the current WebRTC session's audio track
|
|
// This is used when restarting the relay during unmute operations
|
|
func connectRelayToCurrentSession() error {
|
|
if currentSessionCallback == nil {
|
|
return errors.New("no current session callback set")
|
|
}
|
|
|
|
track := currentSessionCallback()
|
|
if track == nil {
|
|
return errors.New("no current session audio track available")
|
|
}
|
|
|
|
relayMutex.Lock()
|
|
defer relayMutex.Unlock()
|
|
|
|
if globalRelay != nil {
|
|
globalRelay.UpdateTrack(track)
|
|
return nil
|
|
}
|
|
|
|
return errors.New("no global relay running")
|
|
}
|