Skip to content

Commit dc659bb

Browse files
committed
Add end call participant RPC.
1 parent 4c00317 commit dc659bb

7 files changed

Lines changed: 286 additions & 124 deletions

File tree

pkg/sip/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,11 @@ func (c *Client) onBye(req *sip.Request, tx sip.ServerTransaction) bool {
350350
call.log.Infow("BYE from remote")
351351
go func(call *outboundCall) {
352352
call.cc.AcceptBye(req, tx)
353-
call.CloseWithReason(ctx, CallHangup, stats.Success("bye"), livekit.DisconnectReason_CLIENT_INITIATED)
353+
call.CloseWith(ctx, EndCall{
354+
Status: CallHangup,
355+
Term: stats.Success("bye"),
356+
Room: livekit.DisconnectReason_CLIENT_INITIATED,
357+
})
354358
}(call)
355359
return true
356360
}

pkg/sip/errors.go

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ import (
2121
// inviteFailure is the verdict for a failed outbound INVITE: how to record
2222
// the call, how to bucket the SLI, and which error to surface back.
2323
type inviteFailure struct {
24-
status CallStatus
25-
term stats.Termination
26-
reason livekit.DisconnectReason
27-
reportErr error // nil skips writing SIPCallInfo.Error
24+
EndCall
2825
returnErr error
2926
}
3027

@@ -54,18 +51,20 @@ func (e SDPError) GRPCStatus() *status.Status {
5451

5552
func (e SDPError) ClassifyInvite() inviteFailure {
5653
res := inviteFailure{
57-
status: callRejected,
58-
reason: livekit.DisconnectReason_MEDIA_FAILURE,
59-
reportErr: e.Err,
54+
EndCall: EndCall{
55+
Status: callRejected,
56+
Room: livekit.DisconnectReason_MEDIA_FAILURE,
57+
Report: e.Err,
58+
},
6059
returnErr: e,
6160
}
6261
switch {
6362
case errors.Is(e.Err, sdp.ErrNoCommonMedia):
64-
res.term = stats.ClientError("no-common-codec")
63+
res.Term = stats.ClientError("no-common-codec")
6564
case errors.Is(e.Err, sdp.ErrNoCommonCrypto):
66-
res.term = stats.ClientError("encryption-required")
65+
res.Term = stats.ClientError("encryption-required")
6766
default:
68-
res.term = stats.ClientError("sdp-error")
67+
res.Term = stats.ClientError("sdp-error")
6968
}
7069
return res
7170
}
@@ -89,10 +88,12 @@ func (e transactionTimeoutError) ClassifyInvite() inviteFailure {
8988
reason = "no-final-response"
9089
}
9190
return inviteFailure{
92-
status: callUnavailable,
93-
term: stats.ClientError(reason),
94-
reason: livekit.DisconnectReason_SIP_TRUNK_FAILURE,
95-
reportErr: e, // keep so the customer sees their destination didn't complete
91+
EndCall: EndCall{
92+
Status: callUnavailable,
93+
Term: stats.ClientError(reason),
94+
Room: livekit.DisconnectReason_SIP_TRUNK_FAILURE,
95+
Report: e, // keep so the customer sees their destination didn't complete
96+
},
9697
returnErr: psrpc.NewError(psrpc.Canceled, e),
9798
}
9899
}
@@ -107,106 +108,108 @@ func classifyInviteError(err error) inviteFailure {
107108
}
108109

109110
res := inviteFailure{
110-
status: callDropped,
111-
term: stats.ServerError("invite-failed"),
112-
reason: livekit.DisconnectReason_UNKNOWN_REASON,
113-
reportErr: err,
111+
EndCall: EndCall{
112+
Status: callDropped,
113+
Term: stats.ServerError("invite-failed"),
114+
Room: livekit.DisconnectReason_UNKNOWN_REASON,
115+
Report: err,
116+
},
114117
returnErr: err,
115118
}
116119

117120
if sipStatus, ok := errors.AsType[*livekit.SIPStatus](err); ok {
118121
code := int(sipStatus.Code)
119122
switch code {
120123
case int(sip.StatusUnauthorized), int(sip.StatusProxyAuthRequired):
121-
res.status, res.term, res.reason = callRejected, stats.ClientError("auth-required"), livekit.DisconnectReason_USER_REJECTED
122-
res.reportErr = nil
124+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("auth-required"), livekit.DisconnectReason_USER_REJECTED
125+
res.Report = nil
123126
case int(sip.StatusForbidden):
124-
res.status, res.term, res.reason = callRejected, stats.ClientError("forbidden"), livekit.DisconnectReason_USER_REJECTED
125-
res.reportErr = nil
127+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("forbidden"), livekit.DisconnectReason_USER_REJECTED
128+
res.Report = nil
126129
case int(sip.StatusNotFound):
127-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("not-found"), livekit.DisconnectReason_USER_UNAVAILABLE
128-
res.reportErr = nil
130+
res.Status, res.Term, res.Room = callUnavailable, stats.ClientError("not-found"), livekit.DisconnectReason_USER_UNAVAILABLE
131+
res.Report = nil
129132
case int(sip.StatusRequestTimeout):
130-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("request-timeout"), livekit.DisconnectReason_USER_UNAVAILABLE
131-
res.reportErr = nil
133+
res.Status, res.Term, res.Room = callUnavailable, stats.ClientError("request-timeout"), livekit.DisconnectReason_USER_UNAVAILABLE
134+
res.Report = nil
132135
case int(sip.StatusTemporarilyUnavailable):
133-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("unavailable"), livekit.DisconnectReason_USER_UNAVAILABLE
134-
res.reportErr = nil
136+
res.Status, res.Term, res.Room = callUnavailable, stats.ClientError("unavailable"), livekit.DisconnectReason_USER_UNAVAILABLE
137+
res.Report = nil
135138
case int(sip.StatusBusyHere):
136-
res.status, res.term, res.reason = callRejected, stats.ClientError("busy"), livekit.DisconnectReason_USER_REJECTED
137-
res.reportErr = nil
139+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("busy"), livekit.DisconnectReason_USER_REJECTED
140+
res.Report = nil
138141
case int(sip.StatusNotAcceptableHere):
139-
res.status, res.term, res.reason = callRejected, stats.ClientError("not-acceptable"), livekit.DisconnectReason_USER_REJECTED
140-
res.reportErr = nil
142+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("not-acceptable"), livekit.DisconnectReason_USER_REJECTED
143+
res.Report = nil
141144
default:
142145
switch {
143146
case code >= 400 && code < 500:
144-
res.status, res.term, res.reason = callRejected, stats.ClientError(fmt.Sprintf("client-error-%d", code)), livekit.DisconnectReason_USER_UNAVAILABLE
145-
res.reportErr = nil
147+
res.Status, res.Term, res.Room = callRejected, stats.ClientError(fmt.Sprintf("client-error-%d", code)), livekit.DisconnectReason_USER_UNAVAILABLE
148+
res.Report = nil
146149
case code >= 500 && code < 600:
147150
// Some upstreams (notably Twilio) return a 5xx when the customer's own trunk exceeds its configured CPS or
148151
// concurrent-call cap. That's a customer-side rate limit, not upstream infrastructure breakage, so it must not count
149152
// against the server-error SLI. Match on the response body — brittle, so kept narrow to the known phrases.
150153
body := strings.ToLower(sipStatus.GetStatus())
151154
switch {
152155
case strings.Contains(body, "cps limit exceeded"):
153-
res.status, res.term, res.reason = callRejected, stats.ClientError("cps-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
156+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("cps-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
154157
// keep reportErr so the customer can see they hit their cap
155158
case strings.Contains(body, "concurrent call limit exceeded"):
156-
res.status, res.term, res.reason = callRejected, stats.ClientError("concurrent-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
159+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("concurrent-limit-exceeded"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
157160
// keep reportErr so the customer can see they hit their cap
158161
default:
159162
// Carrier-side 5xx; keep reportErr for the detail.
160-
res.status, res.term, res.reason = callDropped, stats.UpstreamError(fmt.Sprintf("upstream-server-error-%d", code)), livekit.DisconnectReason_SIP_TRUNK_FAILURE
163+
res.Status, res.Term, res.Room = callDropped, stats.UpstreamError(fmt.Sprintf("upstream-server-error-%d", code)), livekit.DisconnectReason_SIP_TRUNK_FAILURE
161164
}
162165
case code >= 600 && code < 700:
163-
res.status, res.term, res.reason = callRejected, stats.ClientError(fmt.Sprintf("global-decline-%d", code)), livekit.DisconnectReason_USER_REJECTED
164-
res.reportErr = nil
166+
res.Status, res.Term, res.Room = callRejected, stats.ClientError(fmt.Sprintf("global-decline-%d", code)), livekit.DisconnectReason_USER_REJECTED
167+
res.Report = nil
165168
}
166169
}
167170
return res
168171
}
169172

170173
if errors.Is(err, ErrSIPRequestTimeout) {
171-
res.status, res.term, res.reason = callUnavailable, stats.ClientError("no-answer"), livekit.DisconnectReason_USER_UNAVAILABLE
172-
res.reportErr = nil
174+
res.Status, res.Term, res.Room = callUnavailable, stats.ClientError("no-answer"), livekit.DisconnectReason_USER_UNAVAILABLE
175+
res.Report = nil
173176
return res
174177
}
175178

176179
// Context cancellation / deadline. Check before *net.OpError because an
177180
// op error can wrap a context error, and the context cause is more
178181
// informative.
179182
if errors.Is(err, context.DeadlineExceeded) {
180-
res.status, res.term, res.reason = callDropped, stats.ServerError("deadline-exceeded"), livekit.DisconnectReason_UNKNOWN_REASON
183+
res.Status, res.Term, res.Room = callDropped, stats.ServerError("deadline-exceeded"), livekit.DisconnectReason_UNKNOWN_REASON
181184
res.returnErr = psrpc.NewError(psrpc.DeadlineExceeded, err)
182185
return res
183186
}
184187
if errors.Is(err, context.Canceled) {
185-
res.status, res.term, res.reason = callRejected, stats.ClientError("canceled"), livekit.DisconnectReason_USER_UNAVAILABLE
186-
res.reportErr = nil
188+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("canceled"), livekit.DisconnectReason_USER_UNAVAILABLE
189+
res.Report = nil
187190
res.returnErr = psrpc.NewError(psrpc.Canceled, err)
188191
return res
189192
}
190193

191194
// Specific net error types before *net.OpError (which wraps them).
192195
if _, ok := errors.AsType[*net.AddrError](err); ok {
193-
res.status, res.term, res.reason = callDropped, stats.ClientError("address-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
196+
res.Status, res.Term, res.Room = callDropped, stats.ClientError("address-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
194197
res.returnErr = psrpc.NewError(psrpc.InvalidArgument, err)
195198
return res
196199
}
197200
if _, ok := errors.AsType[*net.DNSError](err); ok {
198-
res.status, res.term, res.reason = callDropped, stats.ClientError("dns-resolution"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
201+
res.Status, res.Term, res.Room = callDropped, stats.ClientError("dns-resolution"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
199202
res.returnErr = psrpc.NewError(psrpc.InvalidArgument, err)
200203
return res
201204
}
202205
if _, ok := errors.AsType[*net.OpError](err); ok {
203-
res.status, res.term, res.reason = callDropped, stats.ServerError("network-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
206+
res.Status, res.Term, res.Room = callDropped, stats.ServerError("network-error"), livekit.DisconnectReason_SIP_TRUNK_FAILURE
204207
res.returnErr = psrpc.NewError(psrpc.Unavailable, err)
205208
return res
206209
}
207210

208211
if errors.Is(err, ErrAuthMaxRetry) || errors.Is(err, ErrAuthMissingCreds) || errors.Is(err, ErrAuthNoHeader) {
209-
res.status, res.term, res.reason = callRejected, stats.ClientError("auth-failed"), livekit.DisconnectReason_USER_REJECTED
212+
res.Status, res.Term, res.Room = callRejected, stats.ClientError("auth-failed"), livekit.DisconnectReason_USER_REJECTED
210213
// keep reportErr so the auth detail is recorded
211214
return res
212215
}

pkg/sip/errors_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,13 @@ func TestClassifyInviteError(t *testing.T) {
9595
for _, tc := range cases {
9696
t.Run(tc.name, func(t *testing.T) {
9797
res := classifyInviteError(tc.err)
98-
require.Equal(t, tc.wantStatus, res.status, "status")
99-
require.Equal(t, tc.wantTerm, res.term, "termination")
100-
require.Equal(t, tc.wantReason, res.reason, "disconnect reason")
98+
require.Equal(t, tc.wantStatus, res.Status, "status")
99+
require.Equal(t, tc.wantTerm, res.Term, "termination")
100+
require.Equal(t, tc.wantReason, res.Room, "disconnect reason")
101101
if tc.wantReport {
102-
require.NotNil(t, res.reportErr, "reportErr expected non-nil")
102+
require.NotNil(t, res.Report, "reportErr expected non-nil")
103103
} else {
104-
require.Nil(t, res.reportErr, "reportErr expected nil")
104+
require.Nil(t, res.Report, "reportErr expected nil")
105105
}
106106
})
107107
}

0 commit comments

Comments
 (0)