Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions pkg/sip/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,6 @@ type inboundCall struct {
sigTs SignalingTimestamps
jitterBuf bool
projectID string
audioOut *msdk.WriteCloserSwitch[msdk.PCM16Sample] // inner writer owned by MediaPort
}

func (s *Server) newInboundCall(
Expand Down Expand Up @@ -767,7 +766,6 @@ func (s *Server) newInboundCall(
endCall: make(chan EndCall, 1),
jitterBuf: SelectValueBool(s.conf.EnableJitterBuffer, s.conf.EnableJitterBufferProb),
projectID: "", // Will be set in handleInvite when available
audioOut: msdk.NewWriteCloserSwitch[msdk.PCM16Sample](RoomSampleRate),
}
c.stats.Update()
c.setLog(log.WithValues("jitterBuf", c.jitterBuf))
Expand Down Expand Up @@ -955,6 +953,8 @@ func (c *inboundCall) handleInvite(ctx context.Context, tid traceid.ID, req *sip
ackTimeout <-chan time.Time
)

roomAudioOut := msdk.NewWriteCloserSwitch[msdk.PCM16Sample](RoomSampleRate)

acceptCall := func(answerData []byte) (bool, error) {
defer c.mon.StageDurTimer("call-accept")()
headers := disp.Headers
Expand Down Expand Up @@ -985,7 +985,7 @@ func (c *inboundCall) handleInvite(ctx context.Context, tid traceid.ID, req *sip
// Start this timer right after the Accept.
ackTimeout = time.After(inviteOkAckLateTimeout)
}
if old := c.audioOut.Swap(c.media.GetOutboundAudioWriter()); old != nil {
if old := roomAudioOut.Swap(c.media.GetOutboundAudioWriter()); old != nil {
c.log().Warnw("unexpected audio out writer", nil)
old.Close()
}
Expand All @@ -996,7 +996,7 @@ func (c *inboundCall) handleInvite(ctx context.Context, tid traceid.ID, req *sip
return true, nil
}

if err := c.createMediaPort(mconf, conf, disp.FeatureFlags); err != nil {
if err := c.createMediaPort(mconf, conf, roomAudioOut, disp.FeatureFlags); err != nil {
return rejectMedia(err)
}

Expand Down Expand Up @@ -1160,7 +1160,7 @@ func (w *pinDTMFWriter) WriteSample(msg *livekit.SipDTMF) error {
return nil
}

func (c *inboundCall) createMediaPort(mconf *sipMediaConfig, conf *config.Config, featureFlags map[string]string) error {
func (c *inboundCall) createMediaPort(mconf *sipMediaConfig, conf *config.Config, roomAudioOut *msdk.WriteCloserSwitch[msdk.PCM16Sample], featureFlags map[string]string) error {
c.mmu.Lock()
defer c.mmu.Unlock()
if c.media != nil {
Expand Down Expand Up @@ -1191,7 +1191,7 @@ func (c *inboundCall) createMediaPort(mconf *sipMediaConfig, conf *config.Config
c.mediaCodecs = mconf.Codecs

// Mixer is created with the room; attach it now so pin prompts can play.
if old := c.lkRoom.WriteOutboundAudioTo(c.audioOut); old != nil {
if old := c.lkRoom.WriteOutboundAudioTo(roomAudioOut); old != nil {
c.log().Warnw("room has unexpected outbound audio writer", nil)
old.Close()
}
Expand Down Expand Up @@ -1741,16 +1741,20 @@ func (c *inboundCall) transferCall(ctx context.Context, transferTo string, heade

// Mute the room audio to the SIP participant.
// Skip closing the existing writer, which is c.audioOut.
_ = c.lkRoom.WriteOutboundAudioTo(nil)
oldRoomAudioOut := c.lkRoom.WriteOutboundAudioTo(nil)

defer func() {
if retErr != nil && !c.done.Load() {
c.lkRoom.WriteOutboundAudioTo(c.audioOut)
c.lkRoom.WriteOutboundAudioTo(oldRoomAudioOut)
} else {
if err := oldRoomAudioOut.Close(); err != nil {
c.log().Warnw("failed to close old audio output", err)
}
}
}()
Comment on lines 1746 to 1754

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Dial-tone sink closed before its writer goroutine stops

On a successful transfer, deferred cleanup closes oldRoomAudioOut before the deferred rcancel() runs, because defers execute LIFO and rcancel is registered first. The tones.Play goroutine keeps writing samples to that writer until rctx is cancelled, so it writes to the writer during and after its close.

Prompt for agents
In inboundCall.transferCall (pkg/sip/inbound.go around lines 1737-1762), the dial-tone goroutine started with tones.Play(rctx, oldRoomAudioOut, ...) keeps writing audio samples to oldRoomAudioOut until rctx is cancelled. However, the deferred cleanup that calls oldRoomAudioOut.Close() is registered after 'defer rcancel()', so due to LIFO defer ordering the Close runs before rcancel() cancels the context. This means on the success path the writer is closed while the tones.Play goroutine may still be writing to it (write-after-close race). Ensure the tone goroutine is stopped (rctx cancelled) and ideally has finished before closing oldRoomAudioOut, e.g. by cancelling and waiting for the goroutine (a done channel or WaitGroup) prior to closing, or by registering the close defer so it runs strictly after the goroutine has exited.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


go func() {
err := tones.Play(rctx, c.audioOut, ringVolume, tones.ETSIRinging)
err := tones.Play(rctx, oldRoomAudioOut, ringVolume, tones.ETSIRinging)
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
c.log().Infow("cannot play dial tone", "error", err)
}
Expand Down