Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changeset/khaki-stamps-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"github.com/livekit/protocol": minor
---

Adding ability to specify media timeout per SIP trunk
311 changes: 183 additions & 128 deletions livekit/livekit_sip.pb.go

Large diffs are not rendered by default.

685 changes: 344 additions & 341 deletions livekit/livekit_sip.twirp.go

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions livekit/sip.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,34 @@ import (
"slices"
"strconv"
"strings"
"time"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/livekit/protocol/utils/xtwirp"
"golang.org/x/text/language"
)

// MaxSIPMediaTimeout is the maximum allowed trunk / API value for media_timeout
// (no incoming RTP before the RTP path is torn down)
const MaxSIPMediaTimeout = 10 * time.Minute

func validateDuration(name string, d *durationpb.Duration, min, max *time.Duration) error {
if d == nil {
return nil
}
dur := d.AsDuration()
if min != nil && dur < *min {
return fmt.Errorf("%s must not be less than %v", name, *min)
}
if max != nil && dur > *max {
return fmt.Errorf("%s must not be greater than %v", name, *max)
}
return nil
}

var (
_ xtwirp.ErrorMeta = (*SIPStatus)(nil)
_ error = (*SIPStatus)(nil)
Expand Down Expand Up @@ -451,6 +471,10 @@ func (p *SIPInboundTrunkInfo) Validate() error {
if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil {
return err
}
timeout := MaxSIPMediaTimeout
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After #1530 you could use new(MaxSIPMediaTimeout) as it updates Go to 1.26.

if err := validateDuration("media_timeout", p.MediaTimeout, nil, &timeout); err != nil {
return err
}
return nil
}

Expand All @@ -464,6 +488,10 @@ func (p *SIPInboundTrunkUpdate) Validate() error {
if err := p.AllowedNumbers.Validate(); err != nil {
return err
}
timeout := MaxSIPMediaTimeout
if err := validateDuration("media_timeout", p.MediaTimeout, nil, &timeout); err != nil {
return err
}
return nil
}

Expand All @@ -479,6 +507,7 @@ func (p *SIPInboundTrunkUpdate) Apply(info *SIPInboundTrunkInfo) error {
applyUpdate(&info.Name, p.Name)
applyUpdate(&info.Metadata, p.Metadata)
applyUpdate(&info.MediaEncryption, p.MediaEncryption)
applyUpdatePtr(&info.MediaTimeout, p.MediaTimeout)
return info.Validate()
}

Expand Down Expand Up @@ -540,6 +569,10 @@ func (p *SIPOutboundTrunkInfo) Validate() error {
if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil {
return err
}
timeout := MaxSIPMediaTimeout
if err := validateDuration("media_timeout", p.MediaTimeout, nil, &timeout); err != nil {
return err
}
return nil
}

Expand All @@ -559,13 +592,21 @@ func (p *SIPOutboundConfig) Validate() error {
if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil {
return err
}
timeout := MaxSIPMediaTimeout
if err := validateDuration("media_timeout", p.MediaTimeout, nil, &timeout); err != nil {
return err
}
return nil
}

func (p *SIPOutboundTrunkUpdate) Validate() error {
if err := p.Numbers.Validate(); err != nil {
return err
}
timeout := MaxSIPMediaTimeout
if err := validateDuration("media_timeout", p.MediaTimeout, nil, &timeout); err != nil {
return err
}
return nil
}

Expand All @@ -583,6 +624,7 @@ func (p *SIPOutboundTrunkUpdate) Apply(info *SIPOutboundTrunkInfo) error {
applyUpdate(&info.Metadata, p.Metadata)
applyUpdate(&info.MediaEncryption, p.MediaEncryption)
applyUpdate(&info.FromHost, p.FromHost)
applyUpdatePtr(&info.MediaTimeout, p.MediaTimeout)
return info.Validate()
}

Expand Down
60 changes: 60 additions & 0 deletions livekit/sip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package livekit
import (
"slices"
"testing"
"time"

"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
)

func TestSIPTrunkAs(t *testing.T) {
Expand Down Expand Up @@ -191,6 +193,64 @@ func TestSIPValidate(t *testing.T) {
},
exp: false,
},
{
name: "inbound media_timeout over max",
req: &SIPInboundTrunkInfo{
Numbers: []string{"+1111"},
MediaTimeout: durationpb.New(20 * time.Minute),
},
exp: false,
},
{
name: "inbound media_timeout ok",
req: &SIPInboundTrunkInfo{
Numbers: []string{"+1111"},
MediaTimeout: durationpb.New(5 * time.Minute),
},
exp: true,
},
{
name: "outbound media_timeout over max",
req: &SIPOutboundTrunkInfo{
Address: "sip.example.com",
Numbers: []string{"+2222"},
MediaTimeout: durationpb.New(20 * time.Minute),
},
exp: false,
},
{
name: "outbound media_timeout ok",
req: &SIPOutboundTrunkInfo{
Address: "sip.example.com",
Numbers: []string{"+2222"},
MediaTimeout: durationpb.New(5 * time.Minute),
},
exp: true,
},
{
name: "CreateSIPParticipantRequest media_timeout ok",
req: &CreateSIPParticipantRequest{
SipCallTo: "+3333",
RoomName: "room",
Trunk: &SIPOutboundConfig{
MediaTimeout: durationpb.New(5 * time.Minute),
Hostname: "sip.example.com",
},
},
exp: true,
},
{
name: "CreateSIPParticipantRequest media_timeout invalid",
req: &CreateSIPParticipantRequest{
SipCallTo: "+3333",
RoomName: "room",
Trunk: &SIPOutboundConfig{
MediaTimeout: durationpb.New(20 * time.Minute),
Hostname: "sip.example.com",
},
},
exp: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
Expand Down
19 changes: 15 additions & 4 deletions protobufs/livekit_sip.proto
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,10 @@ message SIPInboundTrunkInfo {
google.protobuf.Timestamp created_at = 17;
google.protobuf.Timestamp updated_at = 18;

// NEXT ID: 19
// Max time a call can last without incoming RTP data received. If unset, defaults are used.
google.protobuf.Duration media_timeout = 19;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lets move it to the dispatch rule level, similar to #1530. If you move this field to the SIPMediaConfig it will already be on the right level.

In general we should avoid adding new settings to trunks, as they might be gone after we implement the webhooks.


// NEXT ID: 20
}

message SIPInboundTrunkUpdate {
Expand All @@ -370,6 +373,7 @@ message SIPInboundTrunkUpdate {
(logger.redact_format) = "<redacted ({{ .Size }} bytes)>"
];
optional SIPMediaEncryption media_encryption = 8;
optional google.protobuf.Duration media_timeout = 9;
}

message CreateSIPOutboundTrunkRequest {
Expand Down Expand Up @@ -443,7 +447,10 @@ message SIPOutboundTrunkInfo {
google.protobuf.Timestamp created_at = 16;
google.protobuf.Timestamp updated_at = 17;

// NEXT ID: 18
// Max time a call can last without incoming RTP data received. If unset, defaults are used.
google.protobuf.Duration media_timeout = 18;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, we could just leave it on the CreateSIPParticipant level and avoid using trunks.


// NEXT ID: 19
}

message SIPOutboundTrunkUpdate {
Expand All @@ -460,8 +467,9 @@ message SIPOutboundTrunkUpdate {
];
optional SIPMediaEncryption media_encryption = 8;
optional string from_host = 10;
optional google.protobuf.Duration media_timeout = 11;

// NEXT ID: 11
// NEXT ID: 12
}

message GetSIPInboundTrunkRequest {
Expand Down Expand Up @@ -716,7 +724,10 @@ message SIPOutboundConfig {
// Optional custom hostname for the 'From' SIP header. When set, outbound calls use this host instead of the default project SIP domain.
string from_host = 8;

// NEXT ID: 9
// Max time a call can last without incoming RTP data received. If unset, defaults are used.
google.protobuf.Duration media_timeout = 9;

// NEXT ID: 10
}

// A SIP Participant is a singular SIP session connected to a LiveKit room via
Expand Down
5 changes: 4 additions & 1 deletion protobufs/rpc/io.proto
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,10 @@ message EvaluateSIPDispatchRulesResponse {

map<string, string> feature_flags = 23;

// NEXT ID: 24
// Per-call RTP media timeout; if unset, SIP service defaults apply.
google.protobuf.Duration media_timeout = 24;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want to allow overriding the initial media timeout as well?


// NEXT ID: 25
}

message UpdateSIPCallStateRequest {
Expand Down
5 changes: 4 additions & 1 deletion protobufs/rpc/sip.proto
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ message InternalCreateSIPParticipantRequest {
// Project-level feature flags from ProjectSettings.FeatureFlags
map<string, string> feature_flags = 33;

// NEXT ID: 34
// Per-call RTP media timeout; if unset, SIP service defaults apply.
google.protobuf.Duration media_timeout = 34;

// NEXT ID: 35
}

message InternalCreateSIPParticipantResponse {
Expand Down
91 changes: 51 additions & 40 deletions rpc/io.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading