Skip to content

Commit ecb5252

Browse files
Adding a custom struct to use for call end hooks (#396)
* adding a custom struct to use for call end hooks The struct has project id, the scl call id generated by livekit/livekit and the protocol level sip call id * Assigning the right values to the identifiers * Improving the tests a bit * Correctly passing the ProjectID at the end of the call * Using Session End instead of CallEnd to be more accurate. Also handling call drop use cases before the connection was established * Ensuring project id is available prior to call connection * Changes from PR feedback * Pulling in latest version of protocol to add SipCallId to SipCall
1 parent 1e66bb9 commit ecb5252

8 files changed

Lines changed: 67 additions & 22 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
1313
github.com/livekit/media-sdk v0.0.0-20250620175140-c86da91c22a2
1414
github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded
15-
github.com/livekit/protocol v1.39.0
15+
github.com/livekit/protocol v1.39.4-0.20250626001605-256d96948009
1616
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c
1717
github.com/livekit/server-sdk-go/v2 v2.9.2-0.20250606164215-22b67ed30bd9
1818
github.com/livekit/sipgo v0.13.2-0.20250601220430-a77cc3f220fb

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ github.com/livekit/media-sdk v0.0.0-20250620175140-c86da91c22a2 h1:uHDhc7gk5Wcxd
127127
github.com/livekit/media-sdk v0.0.0-20250620175140-c86da91c22a2/go.mod h1:7ssWiG+U4xnbvLih9WiZbhQP6zIKMjgXdUtIE1bm/E8=
128128
github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded h1:ylZPdnlX1RW9Z15SD4mp87vT2D2shsk0hpLJwSPcq3g=
129129
github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded/go.mod h1:mSNtYzSf6iY9xM3UX42VEI+STHvMgHmrYzEHPcdhB8A=
130-
github.com/livekit/protocol v1.39.0 h1:xmpkEr0+29xiAu+Z/m7CmsGuLKHf5ba5+5rkGdMdZEY=
131-
github.com/livekit/protocol v1.39.0/go.mod h1:6HPISM0bkTXTk9RIaQTCe0IDbomBPz7Jwp+N3w5sqL0=
130+
github.com/livekit/protocol v1.39.4-0.20250626001605-256d96948009 h1:2q0bh85UfRSD4eZqB+EboPH1V7ikeX9MiLKRVpIz1yA=
131+
github.com/livekit/protocol v1.39.4-0.20250626001605-256d96948009/go.mod h1:6HPISM0bkTXTk9RIaQTCe0IDbomBPz7Jwp+N3w5sqL0=
132132
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c h1:WwEr0YBejYbKzk8LSaO9h8h0G9MnE7shyDu8yXQWmEc=
133133
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c/go.mod h1:kmD+AZPkWu0MaXIMv57jhNlbiSZZ/Jx4bzlxBDVmJes=
134134
github.com/livekit/server-sdk-go/v2 v2.9.2-0.20250606164215-22b67ed30bd9 h1:PffMsBrfW7KPOHy/P6I939+xqTbJBWQtmbx89SWeLkU=

pkg/service/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,6 @@ func (s *Service) DeregisterTransferSIPParticipantTopic(sipCallId string) {
254254
}
255255
}
256256

257-
func (s *Service) OnCallEnd(ctx context.Context, callInfo *livekit.SIPCallInfo, reason string) {
257+
func (s *Service) OnSessionEnd(ctx context.Context, callIdentifier *sip.CallIdentifier, callInfo *livekit.SIPCallInfo, reason string) {
258258
s.log.Infow("SIP call ended", "callID", callInfo.CallId, "reason", reason)
259259
}

pkg/sip/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
223223
mediaEncryption: enc,
224224
}
225225
log.Infow("Creating SIP participant")
226-
call, err := c.newCall(ctx, c.conf, log, LocalTag(req.SipCallId), roomConf, sipConf, state)
226+
call, err := c.newCall(ctx, c.conf, log, LocalTag(req.SipCallId), roomConf, sipConf, state, req.ProjectId)
227227
if err != nil {
228228
return nil, err
229229
}

pkg/sip/inbound.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ type inboundCall struct {
376376
started core.Fuse
377377
stats Stats
378378
jitterBuf bool
379+
projectID string
379380
}
380381

381382
func (s *Server) newInboundCall(
@@ -398,6 +399,7 @@ func (s *Server) newInboundCall(
398399
extraAttrs: extra,
399400
dtmf: make(chan dtmf.Event, 10),
400401
jitterBuf: SelectValueBool(s.conf.EnableJitterBuffer, s.conf.EnableJitterBufferProb),
402+
projectID: "", // Will be set in handleInvite when available
401403
}
402404
// we need it created earlier so that the audio mixer is available for pin prompts
403405
c.lkRoom = NewRoom(log, &c.stats.Room)
@@ -416,6 +418,11 @@ func (c *inboundCall) handleInvite(ctx context.Context, req *sip.Request, trunkI
416418
defer c.mon.CallEnd()
417419
defer c.close(true, callDropped, "other")
418420

421+
// Extract and store the SIP call ID from the request
422+
if h := req.CallID(); h != nil {
423+
c.call.SipCallId = h.Value()
424+
}
425+
419426
c.cc.StartRinging()
420427
// Send initial request. In the best case scenario, we will immediately get a room name to join.
421428
// Otherwise, we could even learn that this number is not allowed and reject the call, or ask for pin if required.
@@ -427,6 +434,7 @@ func (c *inboundCall) handleInvite(ctx context.Context, req *sip.Request, trunkI
427434
})
428435
if disp.ProjectID != "" {
429436
c.log = c.log.WithValues("projectID", disp.ProjectID)
437+
c.projectID = disp.ProjectID
430438
}
431439
if disp.TrunkID != "" {
432440
c.log = c.log.WithValues("sipTrunk", disp.TrunkID)
@@ -765,6 +773,7 @@ func (c *inboundCall) pinPrompt(ctx context.Context, trunkID string) (disp CallD
765773
})
766774
if disp.ProjectID != "" {
767775
c.log = c.log.WithValues("projectID", disp.ProjectID)
776+
c.projectID = disp.ProjectID
768777
}
769778
if disp.TrunkID != "" {
770779
c.log = c.log.WithValues("sipTrunk", disp.TrunkID)
@@ -827,7 +836,11 @@ func (c *inboundCall) close(error bool, status CallStatus, reason string) {
827836

828837
// Call the handler asynchronously to avoid blocking
829838
if c.s.handler != nil {
830-
go c.s.handler.OnCallEnd(context.Background(), c.state.callInfo, reason)
839+
go c.s.handler.OnSessionEnd(context.Background(), &CallIdentifier{
840+
ProjectID: c.projectID,
841+
CallID: c.call.LkCallId,
842+
SipCallID: c.call.SipCallId,
843+
}, c.state.callInfo, reason)
831844
}
832845

833846
c.cancel()

pkg/sip/outbound.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type outboundCall struct {
7575
closing core.Fuse
7676
stats Stats
7777
jitterBuf bool
78+
projectID string
7879

7980
mu sync.RWMutex
8081
mon *stats.CallMonitor
@@ -83,7 +84,7 @@ type outboundCall struct {
8384
sipConf sipOutboundConfig
8485
}
8586

86-
func (c *Client) newCall(ctx context.Context, conf *config.Config, log logger.Logger, id LocalTag, room RoomConfig, sipConf sipOutboundConfig, state *CallState) (*outboundCall, error) {
87+
func (c *Client) newCall(ctx context.Context, conf *config.Config, log logger.Logger, id LocalTag, room RoomConfig, sipConf sipOutboundConfig, state *CallState, projectID string) (*outboundCall, error) {
8788
if sipConf.maxCallDuration <= 0 || sipConf.maxCallDuration > maxCallDuration {
8889
sipConf.maxCallDuration = maxCallDuration
8990
}
@@ -104,6 +105,7 @@ func (c *Client) newCall(ctx context.Context, conf *config.Config, log logger.Lo
104105
sipConf: sipConf,
105106
state: state,
106107
jitterBuf: jitterBuf,
108+
projectID: projectID,
107109
}
108110
call.log = call.log.WithValues("jitterBuf", call.jitterBuf)
109111
call.cc = c.newOutbound(log, id, URI{
@@ -302,7 +304,11 @@ func (c *outboundCall) close(err error, status CallStatus, description string, r
302304

303305
// Call the handler asynchronously to avoid blocking
304306
if c.c.handler != nil {
305-
go c.c.handler.OnCallEnd(context.Background(), c.state.callInfo, description)
307+
go c.c.handler.OnSessionEnd(context.Background(), &CallIdentifier{
308+
ProjectID: c.projectID,
309+
CallID: c.state.callInfo.CallId,
310+
SipCallID: c.cc.CallID(),
311+
}, c.state.callInfo, description)
306312
}
307313
})
308314
}

pkg/sip/server.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ type CallDispatch struct {
9999
MediaEncryption livekit.SIPMediaEncryption
100100
}
101101

102+
type CallIdentifier struct {
103+
ProjectID string
104+
CallID string
105+
SipCallID string
106+
}
107+
102108
type Handler interface {
103109
GetAuthCredentials(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error)
104110
DispatchCall(ctx context.Context, info *CallInfo) CallDispatch
@@ -107,7 +113,7 @@ type Handler interface {
107113
RegisterTransferSIPParticipantTopic(sipCallId string) error
108114
DeregisterTransferSIPParticipantTopic(sipCallId string)
109115

110-
OnCallEnd(ctx context.Context, callInfo *livekit.SIPCallInfo, reason string)
116+
OnSessionEnd(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string)
111117
}
112118

113119
type Server struct {

pkg/sip/service_test.go

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func expectNoResponse(t *testing.T, tx sip.ClientTransaction) {
5757
type TestHandler struct {
5858
GetAuthCredentialsFunc func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error)
5959
DispatchCallFunc func(ctx context.Context, info *CallInfo) CallDispatch
60-
OnCallEndFunc func(ctx context.Context, callInfo *livekit.SIPCallInfo, reason string)
60+
OnSessionEndFunc func(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string)
6161
}
6262

6363
func (h TestHandler) GetAuthCredentials(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error) {
@@ -81,9 +81,9 @@ func (h TestHandler) DeregisterTransferSIPParticipantTopic(sipCallId string) {
8181
// no-op
8282
}
8383

84-
func (h TestHandler) OnCallEnd(ctx context.Context, callInfo *livekit.SIPCallInfo, reason string) {
85-
if h.OnCallEndFunc != nil {
86-
h.OnCallEndFunc(ctx, callInfo, reason)
84+
func (h TestHandler) OnSessionEnd(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string) {
85+
if h.OnSessionEndFunc != nil {
86+
h.OnSessionEndFunc(ctx, callIdentifier, callInfo, reason)
8787
}
8888
}
8989

@@ -177,13 +177,16 @@ func TestService_AuthDrop(t *testing.T) {
177177
})
178178
}
179179

180-
func TestService_OnCallEnd(t *testing.T) {
180+
func TestService_OnSessionEnd(t *testing.T) {
181181
const (
182-
expectedCallID = "test-call-id"
183-
expectedReason = "test-reason"
182+
expectedCallID = "test-call-id"
183+
expectedSipCallID = "test-sip-call-id"
184+
expectedProjectID = "test-project"
185+
expectedReason = "test-reason"
184186
)
185187

186188
callEnded := make(chan struct{})
189+
var receivedCallIdentifier *CallIdentifier
187190
var receivedCallInfo *livekit.SIPCallInfo
188191
var receivedReason string
189192

@@ -202,7 +205,8 @@ func TestService_OnCallEnd(t *testing.T) {
202205
},
203206
}
204207
},
205-
OnCallEndFunc: func(ctx context.Context, callInfo *livekit.SIPCallInfo, reason string) {
208+
OnSessionEndFunc: func(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string) {
209+
receivedCallIdentifier = callIdentifier
206210
receivedCallInfo = callInfo
207211
receivedReason = reason
208212
close(callEnded)
@@ -228,21 +232,37 @@ func TestService_OnCallEnd(t *testing.T) {
228232
s.SetHandler(h)
229233
require.NoError(t, s.Start())
230234

231-
// Call OnCallEnd directly
232-
h.OnCallEnd(context.Background(), &livekit.SIPCallInfo{
235+
// Call OnSessionEnd directly with test data
236+
h.OnSessionEnd(context.Background(), &CallIdentifier{
237+
ProjectID: expectedProjectID,
238+
CallID: expectedCallID,
239+
SipCallID: expectedSipCallID,
240+
}, &livekit.SIPCallInfo{
233241
CallId: expectedCallID,
242+
ParticipantAttributes: map[string]string{
243+
"projectID": expectedProjectID,
244+
AttrSIPCallIDFull: expectedSipCallID,
245+
},
234246
}, expectedReason)
235247

236-
// Wait for OnCallEnd to be called
248+
// Wait for OnSessionEnd to be called
237249
select {
238250
case <-callEnded:
239251
// Success
240252
case <-time.After(time.Second):
241-
t.Fatal("OnCallEnd was not called")
253+
t.Fatal("OnSessionEnd was not called")
242254
}
243255

244-
// Verify the parameters passed to OnCallEnd
256+
// Verify the CallIdentifier fields are correctly populated
257+
require.NotNil(t, receivedCallIdentifier, "CallIdentifier should not be nil")
258+
require.Equal(t, expectedProjectID, receivedCallIdentifier.ProjectID, "CallIdentifier.ProjectID should match")
259+
require.Equal(t, expectedCallID, receivedCallIdentifier.CallID, "CallIdentifier.CallID should match")
260+
require.Equal(t, expectedSipCallID, receivedCallIdentifier.SipCallID, "CallIdentifier.SipCallID should match")
261+
262+
// Verify the CallInfo fields
245263
require.NotNil(t, receivedCallInfo, "CallInfo should not be nil")
264+
require.Equal(t, expectedProjectID, receivedCallInfo.ParticipantAttributes["projectID"], "CallInfo.ParticipantAttributes[projectID] should match")
246265
require.Equal(t, expectedCallID, receivedCallInfo.CallId, "CallInfo.CallId should match")
266+
require.Equal(t, expectedSipCallID, receivedCallInfo.ParticipantAttributes[AttrSIPCallIDFull], "CallInfo.ParticipantAttributes[sip.callIDFull] should match")
247267
require.Equal(t, expectedReason, receivedReason, "Reason should match")
248268
}

0 commit comments

Comments
 (0)