fix: close audio stream channels on disconnect and add Opus PLC
Close audio stream channels in handleUserRemove to prevent goroutine leaks. Each remote user that sends audio spawns a goroutine in OnAudioStream that blocks on an unbuffered channel; without explicit closure, these goroutines leaked on every user disconnect. Add Opus Packet Loss Concealment for detected sequence gaps. When a sequence number discontinuity indicates lost packets, feed empty data to the Opus decoder to produce PLC frames that bridge the gap. This replaces the previous approach of resetting the decoder, which caused audible glitches/silence on packet loss. Extracted dispatchAudio helper to avoid code duplication between real and PLC frame delivery.
This commit is contained in:
committed by
Brandon McGinty
parent
181b325c2d
commit
80580d2a3c
+50
-13
@@ -103,7 +103,6 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
|
|||||||
decoder := user.decoder
|
decoder := user.decoder
|
||||||
if decoder == nil {
|
if decoder == nil {
|
||||||
// TODO: decoder pool
|
// TODO: decoder pool
|
||||||
// TODO: de-reference after stream is done
|
|
||||||
codec := c.audioCodec
|
codec := c.audioCodec
|
||||||
if codec == nil {
|
if codec == nil {
|
||||||
return errNoCodec
|
return errNoCodec
|
||||||
@@ -119,18 +118,19 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
|
|||||||
}
|
}
|
||||||
buffer = buffer[n:]
|
buffer = buffer[n:]
|
||||||
|
|
||||||
// Detect sequence gaps (packet loss) and reset the decoder to prevent
|
// Detect sequence gaps (packet loss). Use Opus Packet Loss
|
||||||
// permanent audio corruption from state desync.
|
// Concealment to fill gaps rather than resetting the decoder,
|
||||||
|
// which would cause audible glitches.
|
||||||
// Mumble uses a monotonically increasing sequence that wraps at MaxInt32.
|
// Mumble uses a monotonically increasing sequence that wraps at MaxInt32.
|
||||||
if user.audioSequenceValid {
|
if user.audioSequenceValid {
|
||||||
// Only treat as discontinuity if the gap is small enough to be loss
|
|
||||||
// rather than a legitimate wrap-around or restart.
|
|
||||||
gap := seq - user.audioSequence
|
gap := seq - user.audioSequence
|
||||||
if gap > 1 && gap < 100 {
|
if gap > 1 && gap < 100 {
|
||||||
decoder.Reset()
|
// Lost packets detected; generate PLC frames for each.
|
||||||
|
for i := int64(1); i < gap; i++ {
|
||||||
|
c.dispatchPLC(user, audioTarget, decoder)
|
||||||
|
}
|
||||||
} else if gap < 0 && gap > -100 {
|
} else if gap < 0 && gap > -100 {
|
||||||
// Reordered packet — reset to be safe, since the decoder
|
// Reordered packet — reset decoder to prevent corruption.
|
||||||
// state depends on correct frame ordering.
|
|
||||||
decoder.Reset()
|
decoder.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +175,33 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
|
|||||||
event.HasPosition = true
|
event.HasPosition = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.dispatchAudio(user, &event)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchPLC generates a Packet Loss Concealment frame from the decoder
|
||||||
|
// and dispatches it to all audio listeners for the given user.
|
||||||
|
func (c *Client) dispatchPLC(user *User, audioTarget byte, decoder AudioDecoder) {
|
||||||
|
// Feed empty data to the decoder to trigger Opus PLC, which
|
||||||
|
// produces a concealed frame bridging the gap.
|
||||||
|
pcm, err := decoder.Decode(nil, AudioMaximumFrameSize)
|
||||||
|
if err != nil {
|
||||||
|
// If PLC fails, reset the decoder so the next real packet
|
||||||
|
// starts from a clean state.
|
||||||
|
decoder.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event := AudioPacket{
|
||||||
|
Client: c,
|
||||||
|
Sender: user,
|
||||||
|
Target: &VoiceTarget{ID: uint32(audioTarget)},
|
||||||
|
AudioBuffer: AudioBuffer(pcm),
|
||||||
|
}
|
||||||
|
c.dispatchAudio(user, &event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchAudio sends an audio packet to all registered audio listeners.
|
||||||
|
func (c *Client) dispatchAudio(user *User, packet *AudioPacket) {
|
||||||
c.volatile.Lock()
|
c.volatile.Lock()
|
||||||
for item := c.Config.AudioListeners.head; item != nil; item = item.next {
|
for item := c.Config.AudioListeners.head; item != nil; item = item.next {
|
||||||
c.volatile.Unlock()
|
c.volatile.Unlock()
|
||||||
@@ -182,19 +209,17 @@ func (c *Client) handleUDPTunnel(buffer []byte) error {
|
|||||||
if ch == nil {
|
if ch == nil {
|
||||||
ch = make(chan *AudioPacket)
|
ch = make(chan *AudioPacket)
|
||||||
item.streams[user] = ch
|
item.streams[user] = ch
|
||||||
event := AudioStreamEvent{
|
streamEvent := AudioStreamEvent{
|
||||||
Client: c,
|
Client: c,
|
||||||
User: user,
|
User: user,
|
||||||
C: ch,
|
C: ch,
|
||||||
}
|
}
|
||||||
item.listener.OnAudioStream(&event)
|
item.listener.OnAudioStream(&streamEvent)
|
||||||
}
|
}
|
||||||
ch <- &event
|
ch <- packet
|
||||||
c.volatile.Lock()
|
c.volatile.Lock()
|
||||||
}
|
}
|
||||||
c.volatile.Unlock()
|
c.volatile.Unlock()
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) handleAuthenticate(buffer []byte) error {
|
func (c *Client) handleAuthenticate(buffer []byte) error {
|
||||||
@@ -479,6 +504,18 @@ func (c *Client) handleUserRemove(buffer []byte) error {
|
|||||||
delete(event.User.Channel.Users, session)
|
delete(event.User.Channel.Users, session)
|
||||||
}
|
}
|
||||||
delete(c.Users, session)
|
delete(c.Users, session)
|
||||||
|
|
||||||
|
// Close audio stream channels for the disconnected user.
|
||||||
|
// This is safe because handleUserRemove and handleUDPTunnel
|
||||||
|
// both run in the serialized readRoutine; no concurrent send
|
||||||
|
// on these channels is possible once the user is removed.
|
||||||
|
for item := c.Config.AudioListeners.head; item != nil; item = item.next {
|
||||||
|
if ch, ok := item.streams[event.User]; ok {
|
||||||
|
close(ch)
|
||||||
|
delete(item.streams, event.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if packet.Reason != nil {
|
if packet.Reason != nil {
|
||||||
event.String = *packet.Reason
|
event.String = *packet.Reason
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user