Skip to content

Commit 55e76da

Browse files
TEL-912 Response on transfer requests (#807)
* Add a status and reason to a participant transfer * Tests * Adding a TODO for future iterations
1 parent 3564ecf commit 55e76da

9 files changed

Lines changed: 1078 additions & 200 deletions

File tree

go.mod

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/livekit/sip
22

3-
go 1.26
3+
go 1.26.3
44

55
require (
66
github.com/at-wat/ebml-go v0.18.0
@@ -10,10 +10,10 @@ require (
1010
github.com/jfreymuth/oggvorbis v1.0.5
1111
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
1212
github.com/livekit/media-sdk v0.0.0-20260812193843-5a5218b19550
13-
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0
14-
github.com/livekit/protocol v1.50.5-0.20260818023635-24e126c61d3e
13+
github.com/livekit/mediatransportutil v0.0.0-20260727210231-81a5287a7109
14+
github.com/livekit/protocol v1.50.5-0.20260821144838-17c16cf496fd
1515
github.com/livekit/psrpc v0.7.3
16-
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260625120102-b6d1838861e7
16+
github.com/livekit/server-sdk-go/v2 v2.18.2-0.20260821165736-9b5928e4fcb8
1717
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb
1818
github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12
1919
github.com/ory/dockertest/v3 v3.12.0
@@ -26,7 +26,7 @@ require (
2626
go.opentelemetry.io/otel v1.44.0
2727
go.opentelemetry.io/otel/trace v1.44.0
2828
golang.org/x/exp v0.0.0-20260603202125-055de637280b
29-
google.golang.org/protobuf v1.36.11
29+
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
3030
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
3131
gopkg.in/yaml.v3 v3.0.1
3232
)
@@ -40,7 +40,7 @@ require (
4040
cel.dev/expr v0.25.2 // indirect
4141
dario.cat/mergo v1.0.0 // indirect
4242
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
43-
github.com/Microsoft/go-winio v0.6.2 // indirect
43+
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect
4444
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
4545
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
4646
github.com/benbjohnson/clock v1.3.5 // indirect
@@ -49,7 +49,7 @@ require (
4949
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
5050
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
5151
github.com/cespare/xxhash/v2 v2.3.0 // indirect
52-
github.com/containerd/continuity v0.4.5 // indirect
52+
github.com/containerd/continuity v0.5.0 // indirect
5353
github.com/containerd/errdefs v1.0.0 // indirect
5454
github.com/containerd/errdefs/pkg v0.3.0 // indirect
5555
github.com/dennwc/iters v1.2.2 // indirect

go.sum

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
1212
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
1313
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
1414
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
15-
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
16-
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
15+
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY=
16+
github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
1717
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
1818
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
1919
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
@@ -38,8 +38,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
3838
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
3939
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4040
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
41-
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
42-
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
41+
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
42+
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
4343
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
4444
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
4545
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -130,14 +130,14 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
130130
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
131131
github.com/livekit/media-sdk v0.0.0-20260812193843-5a5218b19550 h1:aqaMkSNcx2GqCPsmGpkBN6MT5mlVyPZTECw5CxTAxOc=
132132
github.com/livekit/media-sdk v0.0.0-20260812193843-5a5218b19550/go.mod h1:TuYRjSepaakL6ATsM9V2VMuksewW1PlhA32BG7Pxty0=
133-
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0 h1:XHNNzebIKZRkLimla/hFGrAIX5EMWHctrgt3hLw7s+I=
134-
github.com/livekit/mediatransportutil v0.0.0-20260608063931-a3417d38cda0/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
135-
github.com/livekit/protocol v1.50.5-0.20260818023635-24e126c61d3e h1:DHSagaJXDSrnqfOuwNLby2r+VIknFJxvJzAazm9Tdh8=
136-
github.com/livekit/protocol v1.50.5-0.20260818023635-24e126c61d3e/go.mod h1:h4wP4IBPkT5aew7SZSGViHSmanKAiKWsHij/5YfL4BE=
133+
github.com/livekit/mediatransportutil v0.0.0-20260727210231-81a5287a7109 h1:jLE+M9fTj4HeTZSy4T7wD1M42uOeBnbzmoCoWDsTBQI=
134+
github.com/livekit/mediatransportutil v0.0.0-20260727210231-81a5287a7109/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
135+
github.com/livekit/protocol v1.50.5-0.20260821144838-17c16cf496fd h1:pGWIR9fw1eC699v2njRX/7Y+baEXB7p6k9CvDg2uVrQ=
136+
github.com/livekit/protocol v1.50.5-0.20260821144838-17c16cf496fd/go.mod h1:h4wP4IBPkT5aew7SZSGViHSmanKAiKWsHij/5YfL4BE=
137137
github.com/livekit/psrpc v0.7.3 h1:bekuZt/ZQzg8+/M8G6G5jq7bvV9fAKdPHSOZeTwrIIc=
138138
github.com/livekit/psrpc v0.7.3/go.mod h1:rAI+m2+/cb4x9RXhLRtUx5ZwdfjjXOl4zi46IjEetaw=
139-
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260625120102-b6d1838861e7 h1:7I1SzZrgyE2PTPzPuWfR+6HO6C7sO0VJb61fUT5KJRw=
140-
github.com/livekit/server-sdk-go/v2 v2.16.7-0.20260625120102-b6d1838861e7/go.mod h1:B3qlhVBZ4olBWRN/KxokLZE2d4LujNk4n2yYDL3+u2s=
139+
github.com/livekit/server-sdk-go/v2 v2.18.2-0.20260821165736-9b5928e4fcb8 h1:ztpx/fsWaP+IZ2PcuvRO6MGiMDNxwcYptx9bxcK2TSo=
140+
github.com/livekit/server-sdk-go/v2 v2.18.2-0.20260821165736-9b5928e4fcb8/go.mod h1:qkHEZCUjVLpBs2zIhDo7zbX5f1mehGAROkRkPNSyfpc=
141141
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb h1:HmgaJMGs0Nco/Z+XMc9f+xFgrbood9yJsIBtl1OY76M=
142142
github.com/livekit/sipgo v0.13.2-0.20260519205735-a5b4a38b6ceb/go.mod h1:aDa6mbFktNzA1D917RhFlIB5IOfNBTmrwt+/lX960j0=
143143
github.com/mackerelio/go-osstat v0.2.8 h1:I2duicTaCGWoM53XwAwA9OIe1inu0xnVs8/pqOWWVr4=
@@ -314,8 +314,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:
314314
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
315315
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
316316
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
317-
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
318-
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
317+
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
318+
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
319319
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
320320
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
321321
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

pkg/sip/inbound.go

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,12 +1637,12 @@ func (c *inboundCall) handleDTMF(tone dtmf.Event) {
16371637
}
16381638
}
16391639

1640-
func (c *inboundCall) transferCall(ctx context.Context, transferTo string, headers map[string]string, dialtone bool) (retErr error) {
1640+
func (c *inboundCall) transferCall(ctx context.Context, transferTo string, headers map[string]string, dialtone bool) (transferID string, retErr error) {
16411641
var err error
16421642

1643-
tID := c.state.StartTransfer(transferTo)
1643+
transferID = c.state.StartTransfer(transferTo)
16441644
defer func() {
1645-
c.state.EndTransfer(tID, retErr)
1645+
c.state.EndTransfer(transferID, retErr)
16461646
}()
16471647

16481648
if dialtone && c.started.IsBroken() && !c.done.Load() {
@@ -1674,16 +1674,15 @@ func (c *inboundCall) transferCall(ctx context.Context, transferTo string, heade
16741674
err = c.cc.TransferCall(ctx, transferTo, headers, c.ctx.Done())
16751675
if err != nil {
16761676
c.log().Infow("inbound call failed to transfer", "error", err, "transferTo", transferTo)
1677-
return err
1677+
return transferID, err
16781678
}
16791679

16801680
c.log().Infow("inbound call transferred", "transferTo", transferTo)
16811681

16821682
// Give time for the peer to hang up first, but hang up ourselves if this doesn't happen within 1 second
16831683
time.AfterFunc(referByeTimeout, func() { c.Close() })
16841684

1685-
return nil
1686-
1685+
return transferID, nil
16871686
}
16881687

16891688
func (s *Server) newInbound(invite *sip.Request, inviteTx sip.ServerTransaction, src netip.AddrPort) (*sipInbound, error) {
@@ -2231,37 +2230,30 @@ func (c *sipInbound) TransferCall(ctx context.Context, transferTo string, header
22312230
return err
22322231
}
22332232

2234-
select {
2235-
case <-ctx.Done():
2236-
return psrpc.NewErrorf(psrpc.Canceled, "refer canceled")
2237-
case <-callDone:
2238-
// REFER was accepted (2xx) but the call ended before it completed —
2239-
// remote BYE, room deletion, or local hangup.
2240-
c.log.Infow("refer canceled: call ended before transfer completed")
2241-
return nil
2242-
case err := <-c.referDone:
2243-
if err != nil {
2244-
return err
2245-
}
2246-
}
2247-
2248-
return nil
2233+
return waitReferResult(ctx, c.log, callDone, c.referDone)
22492234
}
22502235

22512236
func (c *sipInbound) handleNotify(req *sip.Request, tx sip.ServerTransaction) error {
2252-
method, cseq, status, reason, err := handleNotify(req)
2237+
info, err := handleNotify(req)
22532238
if err != nil {
22542239
return err
22552240
}
2256-
c.log.Infow("handling NOTIFY", "method", method, "status", status, "reason", reason, "cseq", cseq)
2241+
c.log.Infow("handling NOTIFY", "method", info.Method, "status", info.Status,
2242+
"reason", info.Reason, "cseq", info.CSeq, "subscription", info.Sub.String())
22572243

2258-
switch method {
2244+
switch info.Method {
22592245
default:
22602246
return nil
22612247
case sip.REFER:
2248+
// Read referCseq under the lock, then release it before handing the
2249+
// result over. That handoff can park on the unbuffered channel for
2250+
// notifyAckTimeout, and while we hold the read lock every caller of
2251+
// c.mu.Lock() waits: AcceptBye and CloseWithStatus among them, so an
2252+
// arriving BYE would be what we blocked.
22622253
c.mu.RLock()
2263-
defer c.mu.RUnlock()
2264-
handleReferNotify(cseq, status, reason, c.referCseq, c.referDone)
2254+
referCseq := c.referCseq
2255+
c.mu.RUnlock()
2256+
handleReferNotify(info, referCseq, c.referDone)
22652257
return nil
22662258
}
22672259
}

pkg/sip/outbound.go

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -789,14 +789,14 @@ func (c *outboundCall) handleDTMF(ev dtmf.Event) {
789789
}, lksdk.WithDataPublishReliable(true))
790790
}
791791

792-
func (c *outboundCall) transferCall(ctx context.Context, transferTo string, headers map[string]string, dialtone bool) (retErr error) {
792+
func (c *outboundCall) transferCall(ctx context.Context, transferTo string, headers map[string]string, dialtone bool) (transferID string, retErr error) {
793793
ctx, span := Tracer.Start(ctx, "sip.outbound.transferCall")
794794
defer span.End()
795795
var err error
796796

797-
tID := c.state.StartTransfer(transferTo)
797+
transferID = c.state.StartTransfer(transferTo)
798798
defer func() {
799-
c.state.EndTransfer(tID, retErr)
799+
c.state.EndTransfer(transferID, retErr)
800800
}()
801801

802802
if dialtone && c.started.IsBroken() && !c.stopped.IsBroken() {
@@ -828,7 +828,7 @@ func (c *outboundCall) transferCall(ctx context.Context, transferTo string, head
828828
err = c.cc.transferCall(ctx, transferTo, headers, c.closing.Watch())
829829
if err != nil {
830830
c.log.Infow("outbound call failed to transfer", "error", err, "transferTo", transferTo)
831-
return err
831+
return transferID, err
832832
}
833833

834834
c.log.Infow("outbound call transferred", "transferTo", transferTo)
@@ -842,7 +842,7 @@ func (c *outboundCall) transferCall(ctx context.Context, transferTo string, head
842842
})
843843
})
844844

845-
return nil
845+
return transferID, nil
846846
}
847847

848848
func (c *Client) newOutbound(log logger.Logger, id LocalTag, uri *sip.Uri, to *sip.ToHeader, from *sip.FromHeader, contact URI, getHeaders setHeadersFunc) *sipOutbound {
@@ -1318,40 +1318,33 @@ func (c *sipOutbound) transferCall(ctx context.Context, transferTo string, heade
13181318
return err
13191319
}
13201320

1321-
select {
1322-
case <-ctx.Done():
1323-
return psrpc.NewErrorf(psrpc.Canceled, "refer canceled")
1324-
case <-callDone:
1325-
// REFER was accepted (2xx) but the call ended before it completed —
1326-
// remote BYE, room deletion, or local hangup.
1327-
c.log.Infow("refer canceled: call ended before transfer completed")
1328-
return nil
1329-
case err := <-c.referDone:
1330-
if err != nil {
1331-
return err
1332-
}
1333-
}
1334-
1335-
return nil
1321+
return waitReferResult(ctx, c.log, callDone, c.referDone)
13361322
}
13371323

13381324
func (c *sipOutbound) handleNotify(req *sip.Request, tx sip.ServerTransaction) error {
1339-
method, cseq, status, reason, err := handleNotify(req)
1325+
info, err := handleNotify(req)
13401326
if err != nil {
13411327
c.log.Infow("error parsing NOTIFY request", "error", err)
13421328

13431329
return err
13441330
}
13451331

1346-
c.log.Infow("handling NOTIFY", "method", method, "status", status, "reason", reason, "cseq", cseq)
1332+
c.log.Infow("handling NOTIFY", "method", info.Method, "status", info.Status,
1333+
"reason", info.Reason, "cseq", info.CSeq, "subscription", info.Sub.String())
13471334

1348-
switch method {
1335+
switch info.Method {
13491336
default:
13501337
return nil
13511338
case sip.REFER:
1339+
// Read referCseq under the lock, then release it before handing the
1340+
// result over. That handoff can park on the unbuffered channel for
1341+
// notifyAckTimeout, and while we hold the read lock every caller of
1342+
// c.mu.Lock() waits: AcceptBye and Close among them, so an arriving BYE
1343+
// would be what we blocked.
13521344
c.mu.RLock()
1353-
defer c.mu.RUnlock()
1354-
handleReferNotify(cseq, status, reason, c.referCseq, c.referDone)
1345+
referCseq := c.referCseq
1346+
c.mu.RUnlock()
1347+
handleReferNotify(info, referCseq, c.referDone)
13551348
return nil
13561349
}
13571350
}

0 commit comments

Comments
 (0)