From 908baeb9424391f7d5ca3707621e1f75246ee32c Mon Sep 17 00:00:00 2001 From: cnderrauber Date: Thu, 6 Jun 2024 14:31:20 +0800 Subject: [PATCH 01/30] initialize bucket size by publish bitrates (#2763) --- pkg/rtc/mediatrack.go | 7 ++++++- pkg/sfu/buffer/buffer.go | 10 +++++++++- pkg/sfu/buffer/buffer_test.go | 10 +++++----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pkg/rtc/mediatrack.go b/pkg/rtc/mediatrack.go index 21191a924..47113a07a 100644 --- a/pkg/rtc/mediatrack.go +++ b/pkg/rtc/mediatrack.go @@ -353,11 +353,16 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track *webrtc.Tra t.SetSimulcast(true) } + var bitrates int + if len(ti.Layers) > int(layer) { + bitrates = int(ti.Layers[layer].GetBitrate()) + } + if t.IsSimulcast() { t.MediaTrackReceiver.SetLayerSsrc(mime, track.RID(), uint32(track.SSRC())) } - buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability) + buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, bitrates) // if subscriber request fps before fps calculated, update them after fps updated. buff.OnFpsChanged(func() { diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index e48692ba2..06492966a 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -194,7 +194,7 @@ func (b *Buffer) SetAudioLossProxying(enable bool) { b.enableAudioLossProxying = enable } -func (b *Buffer) Bind(params webrtc.RTPParameters, codec webrtc.RTPCodecCapability) { +func (b *Buffer) Bind(params webrtc.RTPParameters, codec webrtc.RTPCodecCapability, bitrates int) { b.Lock() defer b.Unlock() if b.bound { @@ -264,6 +264,14 @@ func (b *Buffer) Bind(params webrtc.RTPParameters, codec webrtc.RTPCodecCapabili } } } + if bitrates > 0 { + pps := bitrates / 8 / 1200 + for pps > b.bucket.Capacity() { + if b.bucket.Grow() >= b.maxVideoPkts { + break + } + } + } default: b.codecType = webrtc.RTPCodecType(0) diff --git a/pkg/sfu/buffer/buffer_test.go b/pkg/sfu/buffer/buffer_test.go index d27ef3fe2..e97ad1db4 100644 --- a/pkg/sfu/buffer/buffer_test.go +++ b/pkg/sfu/buffer/buffer_test.go @@ -68,7 +68,7 @@ func TestNack(t *testing.T) { buff.Bind(webrtc.RTPParameters{ HeaderExtensions: nil, Codecs: []webrtc.RTPCodecParameters{vp8Codec}, - }, vp8Codec.RTPCodecCapability) + }, vp8Codec.RTPCodecCapability, 0) rtt := uint32(20) buff.nacker.SetRTT(rtt) for i := 0; i < 15; i++ { @@ -127,7 +127,7 @@ func TestNack(t *testing.T) { buff.Bind(webrtc.RTPParameters{ HeaderExtensions: nil, Codecs: []webrtc.RTPCodecParameters{vp8Codec}, - }, vp8Codec.RTPCodecCapability) + }, vp8Codec.RTPCodecCapability, 0) rtt := uint32(30) buff.nacker.SetRTT(rtt) for i := 0; i < 15; i++ { @@ -193,7 +193,7 @@ func TestNewBuffer(t *testing.T) { buff.Bind(webrtc.RTPParameters{ HeaderExtensions: nil, Codecs: []webrtc.RTPCodecParameters{vp8Codec}, - }, vp8Codec.RTPCodecCapability) + }, vp8Codec.RTPCodecCapability, 0) for _, p := range TestPackets { buf, _ := p.Marshal() @@ -229,7 +229,7 @@ func TestFractionLostReport(t *testing.T) { buff.Bind(webrtc.RTPParameters{ HeaderExtensions: nil, Codecs: []webrtc.RTPCodecParameters{opusCodec}, - }, opusCodec.RTPCodecCapability) + }, opusCodec.RTPCodecCapability, 0) for i := 0; i < 15; i++ { pkt := rtp.Packet{ Header: rtp.Header{SequenceNumber: uint16(i), Timestamp: uint32(i)}, @@ -261,7 +261,7 @@ func TestFractionLostReport(t *testing.T) { buff.Bind(webrtc.RTPParameters{ HeaderExtensions: nil, Codecs: []webrtc.RTPCodecParameters{opusCodec}, - }, opusCodec.RTPCodecCapability) + }, opusCodec.RTPCodecCapability, 0) for i := 0; i < 15; i++ { pkt := rtp.Packet{ Header: rtp.Header{SequenceNumber: uint16(i), Timestamp: uint32(i)}, From d95b59de588a74cdb509b9917983f073917181ea Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Wed, 5 Jun 2024 23:50:54 -0700 Subject: [PATCH 02/30] update protocol (#2764) * update protocol * deps --- go.mod | 2 +- go.sum | 4 ++-- pkg/telemetry/analyticsservice.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 78874e7ed..91fb35dff 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a - github.com/livekit/protocol v1.17.1-0.20240606023900-429fec77a69b + github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 github.com/mackerelio/go-osstat v0.2.4 github.com/magefile/mage v1.15.0 diff --git a/go.sum b/go.sum index 9beab0153..bd103e127 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkD github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a h1:ATbv0x7G5tW2HgiouQ57csFE/G4gekl2oV1cxb2Dy24= github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= -github.com/livekit/protocol v1.17.1-0.20240606023900-429fec77a69b h1:VZMvqc23x/dXRpJQLc6CIkCuLUjev0HDLFO9NCEqfOk= -github.com/livekit/protocol v1.17.1-0.20240606023900-429fec77a69b/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= +github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 h1:r6e2oEjmIR7PmeWpIzxImHJQU6gJ4gLwYlLH7NwVwVY= +github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs= diff --git a/pkg/telemetry/analyticsservice.go b/pkg/telemetry/analyticsservice.go index 7e2707550..92166d1ca 100644 --- a/pkg/telemetry/analyticsservice.go +++ b/pkg/telemetry/analyticsservice.go @@ -21,8 +21,8 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/livekit/protocol/livekit" - "github.com/livekit/protocol/livekit/rpc" "github.com/livekit/protocol/logger" + "github.com/livekit/protocol/rpc" "github.com/livekit/livekit-server/pkg/config" "github.com/livekit/livekit-server/pkg/routing" From 8887a43a867d3d664ccabe3ee8ada7755ee9908c Mon Sep 17 00:00:00 2001 From: lukasIO Date: Thu, 6 Jun 2024 17:57:10 +0200 Subject: [PATCH 03/30] update readme (#2768) --- .github/banner_dark.png | Bin 54220 -> 127518 bytes .github/banner_light.png | Bin 23647 -> 49035 bytes README.md | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/banner_dark.png b/.github/banner_dark.png index 20c986b9740f8bf2349c0f6b5075c546fa03b4f0..61a14b0b57cbc4bb39321af7090b908f27b18c0c 100644 GIT binary patch literal 127518 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 AZ2$lO literal 54220 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�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 A!2kdN diff --git a/.github/banner_light.png b/.github/banner_light.png index 7cd686bb53184e09aa070b6c5d9914ac6dcaee72..aa04dc5763ccdc258163476bc1ba693d92909f2b 100644 GIT binary patch literal 49035 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 P$uJtfoq?~! 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 literal 23647 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 T{&PzE4-|b}6CI4!{gD3xy<0P~ diff --git a/README.md b/README.md index e79fb3d07..a653de3ba 100644 --- a/README.md +++ b/README.md @@ -305,8 +305,8 @@ LiveKit server is licensed under Apache License v2.0.
- - + + From 7d035deef8928a3c755ba2fc39e9f93c0c265fbb Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Thu, 6 Jun 2024 23:03:21 +0530 Subject: [PATCH 04/30] Clean up logging fields a bit (#2767) --- pkg/sfu/buffer/rtpstats_receiver.go | 2 ++ pkg/sfu/buffer/rtpstats_sender.go | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/sfu/buffer/rtpstats_receiver.go b/pkg/sfu/buffer/rtpstats_receiver.go index d09b07cb7..c21e64bf3 100644 --- a/pkg/sfu/buffer/rtpstats_receiver.go +++ b/pkg/sfu/buffer/rtpstats_receiver.go @@ -214,6 +214,8 @@ func (r *RTPStatsReceiver) Update( "hdrSize", hdrSize, "payloadSize", payloadSize, "paddingSize", paddingSize, + "first", r.srFirst, + "last", r.srNewest, } } if gapSN <= 0 { // duplicate OR out-of-order diff --git a/pkg/sfu/buffer/rtpstats_sender.go b/pkg/sfu/buffer/rtpstats_sender.go index a1b209a80..4796f6910 100644 --- a/pkg/sfu/buffer/rtpstats_sender.go +++ b/pkg/sfu/buffer/rtpstats_sender.go @@ -296,15 +296,13 @@ func (r *RTPStatsSender) Update( "startTime", r.startTime.String(), "firstTime", r.firstTime.String(), "highestTime", r.highestTime.String(), - "prevSN", r.extHighestSN, + "highestSN", r.extHighestSN, "currSN", extSequenceNumber, "gapSN", gapSN, - "prevTS", r.extHighestTS, + "highestTS", r.extHighestTS, "currTS", extTimestamp, - "gapTS", extTimestamp - r.extHighestTS, + "gapTS", int64(extTimestamp - r.extHighestTS), "packetTime", packetTime.String(), - "sequenceNumber", extSequenceNumber, - "timestamp", extTimestamp, "marker", marker, "hdrSize", hdrSize, "payloadSize", payloadSize, From c265ab7104026ed09d9c625bfec3dddb862a3c93 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Fri, 7 Jun 2024 08:21:06 +0530 Subject: [PATCH 05/30] Log invalid spatial layer (#2769) --- pkg/sfu/receiver.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/sfu/receiver.go b/pkg/sfu/receiver.go index 337bb3021..f530e92a5 100644 --- a/pkg/sfu/receiver.go +++ b/pkg/sfu/receiver.go @@ -714,6 +714,14 @@ func (w *WebRTCReceiver) forwardRTP(layer int32) { spatialTracker = w.streamTrackerManager.AddTracker(pkt.Spatial) } } + if spatialLayer > buffer.DefaultMaxLayerSpatial { // TODO-REMOVE-AFTER-DEBUG + w.logger.Warnw( + "invalid spatial layer", nil, + "mime", w.codec.MimeType, + "layer", layer, + "spatialLayer", spatialLayer, + ) + } writeCount := w.downTrackSpreader.Broadcast(func(dt TrackSender) { _ = dt.WriteRTP(pkt, spatialLayer) From 73852d0a1373f746f402265e7f93de8d437bff06 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Fri, 7 Jun 2024 12:36:02 +0530 Subject: [PATCH 06/30] Reduce large sequence number jump threshold for logging. (#2770) Seeing some unexplained large jumps on remotes across relay. Unclear if there was a jump on origin side at some point. Reducing threshold for large jump so that we can catch unexpected jumps more. --- pkg/rtc/uptrackmanager.go | 7 ++----- pkg/sfu/buffer/rtpstats_base.go | 2 ++ pkg/sfu/buffer/rtpstats_receiver.go | 6 +++--- pkg/sfu/buffer/rtpstats_sender.go | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pkg/rtc/uptrackmanager.go b/pkg/rtc/uptrackmanager.go index a0674324e..b0fabdefa 100644 --- a/pkg/rtc/uptrackmanager.go +++ b/pkg/rtc/uptrackmanager.go @@ -20,6 +20,7 @@ import ( "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" + "golang.org/x/exp/maps" "github.com/livekit/livekit-server/pkg/rtc/types" "github.com/livekit/protocol/utils" @@ -147,11 +148,7 @@ func (u *UpTrackManager) GetPublishedTracks() []types.MediaTrack { u.lock.RLock() defer u.lock.RUnlock() - tracks := make([]types.MediaTrack, 0, len(u.publishedTracks)) - for _, t := range u.publishedTracks { - tracks = append(tracks, t) - } - return tracks + return maps.Values(u.publishedTracks) } func (u *UpTrackManager) UpdateSubscriptionPermission( diff --git a/pkg/sfu/buffer/rtpstats_base.go b/pkg/sfu/buffer/rtpstats_base.go index 1fcef9a23..b7ca0a163 100644 --- a/pkg/sfu/buffer/rtpstats_base.go +++ b/pkg/sfu/buffer/rtpstats_base.go @@ -37,6 +37,8 @@ const ( cFirstPacketTimeAdjustThreshold = 15 * time.Second cPassthroughNTPTimestamp = true + + cSequenceNumberLargeJumpThreshold = 1000 ) // ------------------------------------------------------- diff --git a/pkg/sfu/buffer/rtpstats_receiver.go b/pkg/sfu/buffer/rtpstats_receiver.go index c21e64bf3..c15dcecfe 100644 --- a/pkg/sfu/buffer/rtpstats_receiver.go +++ b/pkg/sfu/buffer/rtpstats_receiver.go @@ -219,7 +219,7 @@ func (r *RTPStatsReceiver) Update( } } if gapSN <= 0 { // duplicate OR out-of-order - if -gapSN >= cNumSequenceNumbers/2 { + if -gapSN >= cSequenceNumberLargeJumpThreshold { if r.largeJumpNegativeCount%100 == 0 { r.logger.Warnw( "large sequence number gap negative", nil, @@ -249,7 +249,7 @@ func (r *RTPStatsReceiver) Update( flowState.ExtSequenceNumber = resSN.ExtendedVal flowState.ExtTimestamp = resTS.ExtendedVal } else { // in-order - if gapSN >= cNumSequenceNumbers/2 || resTS.ExtendedVal < resTS.PreExtendedHighest { + if gapSN >= cSequenceNumberLargeJumpThreshold || resTS.ExtendedVal < resTS.PreExtendedHighest { if r.largeJumpCount%100 == 0 { r.logger.Warnw( "large sequence number gap OR time reversed", nil, @@ -596,7 +596,7 @@ func (r *RTPStatsReceiver) MarshalLogObject(e zapcore.ObjectEncoder) error { defer r.lock.RUnlock() e.AddObject("base", r.rtpStatsBase) - e.AddUint64("extendedStartSN", r.sequenceNumber.GetExtendedStart()) + e.AddUint64("extStartSN", r.sequenceNumber.GetExtendedStart()) e.AddUint64("extHighestSN", r.sequenceNumber.GetExtendedHighest()) e.AddUint64("extStartTS", r.timestamp.GetExtendedStart()) e.AddUint64("extHighestTS", r.timestamp.GetExtendedHighest()) diff --git a/pkg/sfu/buffer/rtpstats_sender.go b/pkg/sfu/buffer/rtpstats_sender.go index 4796f6910..215520f45 100644 --- a/pkg/sfu/buffer/rtpstats_sender.go +++ b/pkg/sfu/buffer/rtpstats_sender.go @@ -316,7 +316,7 @@ func (r *RTPStatsSender) Update( // do not start on a padding only packet return } - if -gapSN >= cNumSequenceNumbers/2 { + if -gapSN >= cSequenceNumberLargeJumpThreshold { if r.largeJumpNegativeCount%100 == 0 { r.logger.Warnw( "large sequence number gap negative", nil, @@ -372,7 +372,7 @@ func (r *RTPStatsSender) Update( r.setSnInfo(extSequenceNumber, r.extHighestSN, uint16(pktSize), uint8(hdrSize), uint16(payloadSize), marker, true) } } else { // in-order - if gapSN >= cNumSequenceNumbers/2 || extTimestamp < r.extHighestTS { + if gapSN >= cSequenceNumberLargeJumpThreshold || extTimestamp < r.extHighestTS { if r.largeJumpCount%100 == 0 { r.logger.Warnw( "large sequence number gap OR time reversed", nil, From cee3fdb25e7614f8b8df658cc569ad7c61782465 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Fri, 7 Jun 2024 23:56:10 +0530 Subject: [PATCH 07/30] Better lock for sender report TS offset. (#2771) * Better lock for sender report TS offset. It is possible that a resume has happened and new time stamp offset calculated. But, a sender report from publisher comes with a time stamp prior to the time stamp which was used for offset calculation. Using that sender report in the forwarding path causes jumps. Example - Track forwarding, let us tsOffset = `a` - Unmute/layer switch - one of those events happens, a new tsOffset will be calculated, let us say that offset is `b` and it is based on incoming time stmap of `c`. - A sender report from publisher could arrive with timestamp = `d`. o If `d` >= `c`, the offset `b` is correct and can be applied. o But, it is possible that `d` < `c`, in that case, offset `a` should be used and not `b`. To address this, keep track of incoming extended timestamp at switch point and accept incoming sender reports which have a timestamp >= switch point timestamp. * clean up * log more details on invalid layer --- pkg/sfu/forwarder.go | 30 ++++++++++++++++-------------- pkg/sfu/receiver.go | 8 ++++++++ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/pkg/sfu/forwarder.go b/pkg/sfu/forwarder.go index 87c6cc345..68e6df310 100644 --- a/pkg/sfu/forwarder.go +++ b/pkg/sfu/forwarder.go @@ -233,14 +233,15 @@ type Forwarder struct { pubMuted bool resumeBehindThreshold float64 - started bool - preStartTime time.Time - extFirstTS uint64 - lastSSRC uint32 - referenceLayerSpatial int32 - dummyStartTSOffset uint64 - refInfos [buffer.DefaultMaxLayerSpatial + 1]refInfo - refIsSVC bool + started bool + preStartTime time.Time + extFirstTS uint64 + lastSSRC uint32 + lastSwitchExtIncomingTS uint64 + referenceLayerSpatial int32 + dummyStartTSOffset uint64 + refInfos [buffer.DefaultMaxLayerSpatial + 1]refInfo + refIsSVC bool provisional *VideoAllocationProvisional @@ -568,7 +569,7 @@ func (f *Forwarder) GetMaxSubscribedSpatial() int32 { return layer } -func (f *Forwarder) getReferenceLayer() (int32, int32) { +func (f *Forwarder) getRefLayer() (int32, int32) { if f.lastSSRC == 0 { return buffer.InvalidLayerSpatial, buffer.InvalidLayerSpatial } @@ -594,10 +595,10 @@ func (f *Forwarder) SetRefSenderReport(isSVC bool, layer int32, srData *buffer.R defer f.lock.Unlock() f.refIsSVC = isSVC - refLayer, _ := f.getReferenceLayer() + refLayer, _ := f.getRefLayer() if layer >= 0 && int(layer) < len(f.refInfos) { f.refInfos[layer] = refInfo{srData, 0, false} - if layer == refLayer { + if layer == refLayer && srData.RTPTimestampExt >= f.lastSwitchExtIncomingTS { f.refInfos[layer].tsOffset = f.rtpMunger.GetTSOffset() f.refInfos[layer].isTSOffsetValid = true } @@ -643,7 +644,7 @@ func (f *Forwarder) GetSenderReportParams() (int32, uint64, *buffer.RTCPSenderRe f.lock.RLock() defer f.lock.RUnlock() - refLayer, currentLayerSpatial := f.getReferenceLayer() + refLayer, currentLayerSpatial := f.getRefLayer() if refLayer == buffer.InvalidLayerSpatial || !f.refInfos[refLayer].isTSOffsetValid { return buffer.InvalidLayerSpatial, 0, nil } @@ -1566,7 +1567,7 @@ func (f *Forwarder) GetTranslationParams(extPkt *buffer.ExtPacket, layer int32) }, ErrUnknownKind } -func (f *Forwarder) getReferenceLayerRTPTimestamp(ts uint32, refLayer, targetLayer int32) (uint32, error) { +func (f *Forwarder) getRefLayerRTPTimestamp(ts uint32, refLayer, targetLayer int32) (uint32, error) { if refLayer < 0 || int(refLayer) > len(f.refInfos) || targetLayer < 0 || int(targetLayer) > len(f.refInfos) { return 0, fmt.Errorf("invalid layer(s), refLayer: %d, targetLayer: %d", refLayer, targetLayer) } @@ -1670,7 +1671,7 @@ func (f *Forwarder) processSourceSwitch(extPkt *buffer.ExtPacket, layer int32) e switchingAt := time.Now() if !f.skipReferenceTS { var err error - refTS, err = f.getReferenceLayerRTPTimestamp(extPkt.Packet.Timestamp, f.referenceLayerSpatial, layer) + refTS, err = f.getRefLayerRTPTimestamp(extPkt.Packet.Timestamp, f.referenceLayerSpatial, layer) if err != nil { // error out if refTS is not available. It can happen when there is no sender report // for the layer being switched to. Can especially happen at the start of the track when layer switches are @@ -1853,6 +1854,7 @@ func (f *Forwarder) getTranslationParamsCommon(extPkt *buffer.ExtPacket, layer i } f.logger.Debugw("switching feed", "from", f.lastSSRC, "to", extPkt.Packet.SSRC) f.lastSSRC = extPkt.Packet.SSRC + f.lastSwitchExtIncomingTS = extPkt.ExtTimestamp } tpRTP, err := f.rtpMunger.UpdateAndGetSnTs(extPkt, tp.marker) diff --git a/pkg/sfu/receiver.go b/pkg/sfu/receiver.go index f530e92a5..1fd0db1c6 100644 --- a/pkg/sfu/receiver.go +++ b/pkg/sfu/receiver.go @@ -720,6 +720,14 @@ func (w *WebRTCReceiver) forwardRTP(layer int32) { "mime", w.codec.MimeType, "layer", layer, "spatialLayer", spatialLayer, + "sn", pkt.Packet.SequenceNumber, + "esn", pkt.ExtSequenceNumber, + "timestamp", pkt.Packet.Timestamp, + "ets", pkt.ExtTimestamp, + "payloadSize", len(pkt.Packet.Payload), + "rtpVersion", pkt.Packet.Version, + "payloadType", pkt.Packet.PayloadType, + "ssrc", pkt.Packet.SSRC, ) } From b58db8225445396f5331990499bc4a37532c5ab7 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Sat, 8 Jun 2024 10:36:05 +0530 Subject: [PATCH 08/30] Log invalid RTP packet (#2774) --- pkg/sfu/buffer/buffer.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index 06492966a..b050bea49 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -316,6 +316,21 @@ func (b *Buffer) Write(pkt []byte) (n int, err error) { return } + if rtpPacket.Version != 2 || rtpPacket.PayloadType != b.payloadType { + b.logger.Warnw( + "invalid RTP packet", nil, + "version", rtpPacket.Version, + "sn", rtpPacket.SequenceNumber, + "timestamp", rtpPacket.Timestamp, + "payloadSize", len(rtpPacket.Payload), + "payloadType", rtpPacket.PayloadType, + "ssrc", rtpPacket.SSRC, + "rtpStats", b.rtpStats, + "snRangeMap", b.snRangeMap, + ) + // TODO-REMOVE-AFTER-DEBUG + } + now := time.Now() if b.twcc != nil && b.twccExtID != 0 && !b.closed.Load() { if ext := rtpPacket.GetExtension(b.twccExtID); ext != nil { From 38d213ed10834339691f2ed07e979b2c243eb149 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Sun, 9 Jun 2024 01:03:38 +0530 Subject: [PATCH 09/30] Do not compare payload type before bind (#2775) --- pkg/sfu/buffer/buffer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index b050bea49..82e5920c1 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -316,7 +316,7 @@ func (b *Buffer) Write(pkt []byte) (n int, err error) { return } - if rtpPacket.Version != 2 || rtpPacket.PayloadType != b.payloadType { + if rtpPacket.Version != 2 || (b.payloadType != 0 && rtpPacket.PayloadType != b.payloadType) { b.logger.Warnw( "invalid RTP packet", nil, "version", rtpPacket.Version, From a31f59b6899f15b89ca7d5a1a1b9a0e03f7c6a9d Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Sun, 9 Jun 2024 23:07:01 +0530 Subject: [PATCH 10/30] Log first time adjustment total. (#2776) * Log first time adjustment total. Seeing cases where the first time is 400ms+ before start time. Possible it is getting that much adjustment, but would be good to see how much total adjustment happens. * log propagation delay --- pkg/sfu/buffer/rtpstats_base.go | 7 +++++-- pkg/sfu/buffer/rtpstats_receiver.go | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/sfu/buffer/rtpstats_base.go b/pkg/sfu/buffer/rtpstats_base.go index b7ca0a163..d26907179 100644 --- a/pkg/sfu/buffer/rtpstats_base.go +++ b/pkg/sfu/buffer/rtpstats_base.go @@ -174,8 +174,9 @@ type rtpStatsBase struct { startTime time.Time endTime time.Time - firstTime time.Time - highestTime time.Time + firstTime time.Time + firstTimeAdjustment time.Duration + highestTime time.Time lastTransit uint64 lastJitterExtTimestamp uint64 @@ -551,6 +552,7 @@ func (r *rtpStatsBase) maybeAdjustFirstPacketTime(srData *RTCPSenderReportData, r.logger.Infow("adjusting first packet time, too big, ignoring", getFields()...) } else { r.logger.Debugw("adjusting first packet time", getFields()...) + r.firstTimeAdjustment += r.firstTime.Sub(firstTime) r.firstTime = firstTime } } @@ -645,6 +647,7 @@ func (r *rtpStatsBase) MarshalLogObject(e zapcore.ObjectEncoder) error { e.AddTime("startTime", r.startTime) e.AddTime("endTime", r.endTime) e.AddTime("firstTime", r.firstTime) + e.AddDuration("firstTimeAdjustment", r.firstTimeAdjustment) e.AddTime("highestTime", r.highestTime) e.AddUint64("bytes", r.bytes) diff --git a/pkg/sfu/buffer/rtpstats_receiver.go b/pkg/sfu/buffer/rtpstats_receiver.go index c15dcecfe..94952ef1c 100644 --- a/pkg/sfu/buffer/rtpstats_receiver.go +++ b/pkg/sfu/buffer/rtpstats_receiver.go @@ -596,10 +596,14 @@ func (r *RTPStatsReceiver) MarshalLogObject(e zapcore.ObjectEncoder) error { defer r.lock.RUnlock() e.AddObject("base", r.rtpStatsBase) + e.AddUint64("extStartSN", r.sequenceNumber.GetExtendedStart()) e.AddUint64("extHighestSN", r.sequenceNumber.GetExtendedHighest()) e.AddUint64("extStartTS", r.timestamp.GetExtendedStart()) e.AddUint64("extHighestTS", r.timestamp.GetExtendedHighest()) + + e.AddDuration("propagationDelay", r.propagationDelay) + e.AddDuration("longTermDeltaPropagationDelay", r.longTermDeltaPropagationDelay) return nil } From 129ba62d61c5b7c8195b7b54dad13601ec21c06b Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Mon, 10 Jun 2024 15:43:59 +0530 Subject: [PATCH 11/30] Validate RTP packets. (#2778) * Validate RTP packets. Check version, payload type (if available) and SSRC (if available) and drop bad packets. And let repair mechanisms take effect for those packets. * address data race reported by test * fix an unlock and test packets --- pkg/rtc/mediatrack.go | 4 +-- pkg/sfu/buffer/buffer.go | 18 ++++++++----- pkg/sfu/buffer/buffer_test.go | 46 +++++++++++++++++++++++++++++---- pkg/sfu/streamtrackermanager.go | 9 ++++++- pkg/sfu/utils/helpers.go | 19 ++++++++++++++ 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/pkg/rtc/mediatrack.go b/pkg/rtc/mediatrack.go index 47113a07a..4531f0dcd 100644 --- a/pkg/rtc/mediatrack.go +++ b/pkg/rtc/mediatrack.go @@ -358,9 +358,7 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track *webrtc.Tra bitrates = int(ti.Layers[layer].GetBitrate()) } - if t.IsSimulcast() { - t.MediaTrackReceiver.SetLayerSsrc(mime, track.RID(), uint32(track.SSRC())) - } + t.MediaTrackReceiver.SetLayerSsrc(mime, track.RID(), uint32(track.SSRC())) buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, bitrates) diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index 82e5920c1..f8dc63e06 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -316,19 +316,25 @@ func (b *Buffer) Write(pkt []byte) (n int, err error) { return } - if rtpPacket.Version != 2 || (b.payloadType != 0 && rtpPacket.PayloadType != b.payloadType) { + if err = utils.ValidateRTPPacket(&rtpPacket, b.payloadType, b.mediaSSRC); err != nil { b.logger.Warnw( - "invalid RTP packet", nil, + "validating RTP packet failed", err, "version", rtpPacket.Version, - "sn", rtpPacket.SequenceNumber, - "timestamp", rtpPacket.Timestamp, - "payloadSize", len(rtpPacket.Payload), + "padding", rtpPacket.Padding, + "marker", rtpPacket.Marker, + "expectedPayloadType", b.payloadType, "payloadType", rtpPacket.PayloadType, + "sequenceNumber", rtpPacket.SequenceNumber, + "timestamp", rtpPacket.Timestamp, + "expectedSSRC", b.mediaSSRC, "ssrc", rtpPacket.SSRC, + "numExtensions", len(rtpPacket.Extensions), + "payloadSize", len(rtpPacket.Payload), "rtpStats", b.rtpStats, "snRangeMap", b.snRangeMap, ) - // TODO-REMOVE-AFTER-DEBUG + b.Unlock() + return } now := time.Now() diff --git a/pkg/sfu/buffer/buffer_test.go b/pkg/sfu/buffer/buffer_test.go index e97ad1db4..e5ce7fdc4 100644 --- a/pkg/sfu/buffer/buffer_test.go +++ b/pkg/sfu/buffer/buffer_test.go @@ -44,7 +44,7 @@ var opusCodec = webrtc.RTPCodecParameters{ MimeType: "audio/opus", ClockRate: 48000, }, - PayloadType: 96, + PayloadType: 111, } func TestNack(t *testing.T) { @@ -81,7 +81,13 @@ func TestNack(t *testing.T) { time.Sleep(500 * time.Millisecond) // even a long wait should not exceed max retries } pkt := rtp.Packet{ - Header: rtp.Header{SequenceNumber: uint16(i), Timestamp: uint32(i)}, + Header: rtp.Header{ + Version: 2, + PayloadType: 96, + SequenceNumber: uint16(i), + Timestamp: uint32(i), + SSRC: 123, + }, Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}, } b, err := pkt.Marshal() @@ -140,7 +146,13 @@ func TestNack(t *testing.T) { time.Sleep(500 * time.Millisecond) // even a long wait should not exceed max retries } pkt := rtp.Packet{ - Header: rtp.Header{SequenceNumber: uint16(i + 65533), Timestamp: uint32(i)}, + Header: rtp.Header{ + Version: 2, + PayloadType: 96, + SequenceNumber: uint16(i + 65533), + Timestamp: uint32(i), + SSRC: 123, + }, Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}, } b, err := pkt.Marshal() @@ -166,23 +178,35 @@ func TestNewBuffer(t *testing.T) { var TestPackets = []*rtp.Packet{ { Header: rtp.Header{ + Version: 2, + PayloadType: 96, SequenceNumber: 65533, + SSRC: 123, }, }, { Header: rtp.Header{ + Version: 2, + PayloadType: 96, SequenceNumber: 65534, + SSRC: 123, }, Payload: []byte{1}, }, { Header: rtp.Header{ + Version: 2, + PayloadType: 96, SequenceNumber: 2, + SSRC: 123, }, }, { Header: rtp.Header{ + Version: 2, + PayloadType: 96, SequenceNumber: 65535, + SSRC: 123, }, }, } @@ -232,7 +256,13 @@ func TestFractionLostReport(t *testing.T) { }, opusCodec.RTPCodecCapability, 0) for i := 0; i < 15; i++ { pkt := rtp.Packet{ - Header: rtp.Header{SequenceNumber: uint16(i), Timestamp: uint32(i)}, + Header: rtp.Header{ + Version: 2, + PayloadType: 111, + SequenceNumber: uint16(i), + Timestamp: uint32(i), + SSRC: 123, + }, Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}, } b, err := pkt.Marshal() @@ -264,7 +294,13 @@ func TestFractionLostReport(t *testing.T) { }, opusCodec.RTPCodecCapability, 0) for i := 0; i < 15; i++ { pkt := rtp.Packet{ - Header: rtp.Header{SequenceNumber: uint16(i), Timestamp: uint32(i)}, + Header: rtp.Header{ + Version: 2, + PayloadType: 111, + SequenceNumber: uint16(i), + Timestamp: uint32(i), + SSRC: 123, + }, Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}, } b, err := pkt.Marshal() diff --git a/pkg/sfu/streamtrackermanager.go b/pkg/sfu/streamtrackermanager.go index 890b70257..d106dd9e5 100644 --- a/pkg/sfu/streamtrackermanager.go +++ b/pkg/sfu/streamtrackermanager.go @@ -264,7 +264,7 @@ func (s *StreamTrackerManager) RemoveAllTrackers() { s.trackers[layer] = nil } s.availableLayers = make([]int32, 0) - s.maxExpectedLayerFromTrackInfo() + s.maxExpectedLayerFromTrackInfoLocked() s.paused = false ddTracker := s.ddTracker s.ddTracker = nil @@ -530,6 +530,13 @@ func (s *StreamTrackerManager) removeAvailableLayer(layer int32) { } func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfo() { + s.lock.Lock() + defer s.lock.Unlock() + + s.maxExpectedLayerFromTrackInfoLocked() +} + +func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfoLocked() { s.maxExpectedLayer = buffer.InvalidLayerSpatial ti := s.trackInfo.Load() if ti != nil { diff --git a/pkg/sfu/utils/helpers.go b/pkg/sfu/utils/helpers.go index 476050416..f3f12161e 100644 --- a/pkg/sfu/utils/helpers.go +++ b/pkg/sfu/utils/helpers.go @@ -15,9 +15,11 @@ package utils import ( + "errors" "strings" "github.com/pion/interceptor" + "github.com/pion/rtp" "github.com/pion/webrtc/v3" ) @@ -51,3 +53,20 @@ func GetHeaderExtensionID(extensions []interceptor.RTPHeaderExtension, extension } return 0 } + +// ValidateRTPPacket checks for a valid RTP packet and returns an error if fields are incorrect +func ValidateRTPPacket(pkt *rtp.Packet, expectedPayloadType uint8, expectedSSRC uint32) error { + if pkt.Version != 2 { + return errors.New("invalid RTP version") + } + + if expectedPayloadType != 0 && pkt.PayloadType != expectedPayloadType { + return errors.New("invalid RTP payload type") + } + + if expectedSSRC != 0 && pkt.SSRC != expectedSSRC { + return errors.New("invalid RTP SSRC") + } + + return nil +} From 29614cd4a189306177f5b034225723cd3c2b7ed0 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Mon, 10 Jun 2024 16:11:11 -0700 Subject: [PATCH 12/30] clean up egress launcher (#2779) --- pkg/rtc/egress.go | 1 - pkg/service/clients.go | 24 ++++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/pkg/rtc/egress.go b/pkg/rtc/egress.go index db6d12813..b3fc017a3 100644 --- a/pkg/rtc/egress.go +++ b/pkg/rtc/egress.go @@ -29,7 +29,6 @@ import ( type EgressLauncher interface { StartEgress(context.Context, *rpc.StartEgressRequest) (*livekit.EgressInfo, error) - StartEgressWithClusterId(ctx context.Context, clusterId string, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) } func StartParticipantEgress( diff --git a/pkg/service/clients.go b/pkg/service/clients.go index 05c05ca7a..9fd887878 100644 --- a/pkg/service/clients.go +++ b/pkg/service/clients.go @@ -54,7 +54,16 @@ func NewEgressLauncher(client rpc.EgressClient, io IOClient) rtc.EgressLauncher } func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) { - info, err := s.StartEgressWithClusterId(ctx, "", req) + if s.client == nil { + return nil, ErrEgressNotConnected + } + + // Ensure we have an Egress ID + if req.EgressId == "" { + req.EgressId = guid.New(utils.EgressPrefix) + } + + info, err := s.client.StartEgress(ctx, "", req) if err != nil { return nil, err } @@ -66,16 +75,3 @@ func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRe return info, nil } - -func (s *egressLauncher) StartEgressWithClusterId(ctx context.Context, clusterId string, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) { - if s.client == nil { - return nil, ErrEgressNotConnected - } - - // Ensure we have an Egress ID - if req.EgressId == "" { - req.EgressId = guid.New(utils.EgressPrefix) - } - - return s.client.StartEgress(ctx, clusterId, req) -} From 8064e1673c17fbc1d476c849503bdc5505182844 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 13:12:10 -0700 Subject: [PATCH 13/30] fix(deps): update go deps (#2747) Generated by renovateBot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 26 +++++++++++++------------- go.sum | 52 ++++++++++++++++++++++++++-------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 91fb35dff..46b16997e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gammazero/deque v0.2.1 github.com/gammazero/workerpool v1.1.3 github.com/google/wire v0.6.0 - github.com/gorilla/websocket v1.5.1 + github.com/gorilla/websocket v1.5.2 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jellydator/ttlcache/v3 v3.2.0 @@ -22,7 +22,7 @@ require ( github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 - github.com/mackerelio/go-osstat v0.2.4 + github.com/mackerelio/go-osstat v0.2.5 github.com/magefile/mage v1.15.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 github.com/mitchellh/go-homedir v1.1.0 @@ -39,19 +39,19 @@ require ( github.com/pion/webrtc/v3 v3.2.40 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.5.3 github.com/rs/cors v1.11.0 github.com/stretchr/testify v1.9.0 github.com/thoas/go-funk v0.9.3 github.com/twitchtv/twirp v8.1.3+incompatible - github.com/ua-parser/uap-go v0.0.0-20240113215029-33f8e6d47f38 + github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 github.com/urfave/cli/v2 v2.27.2 - github.com/urfave/negroni/v3 v3.1.0 + github.com/urfave/negroni/v3 v3.1.1 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 golang.org/x/sync v0.7.0 - google.golang.org/protobuf v1.34.1 + google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -99,12 +99,12 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap/exp v0.2.0 // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect google.golang.org/grpc v1.64.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index bd103e127..d94143090 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= +github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -124,8 +124,8 @@ github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 h1:r6e2oEjmIR7 github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= -github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs= -github.com/mackerelio/go-osstat v0.2.4/go.mod h1:Zy+qzGdZs3A9cuIqmgbJvwbmLQH9dJvtio5ZjJTbdlQ= +github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= +github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= @@ -223,8 +223,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= -github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= -github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= @@ -252,12 +252,12 @@ github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= -github.com/ua-parser/uap-go v0.0.0-20240113215029-33f8e6d47f38 h1:F04Na0QJP9GJrwmK3vQDuDrCuGllrrfngW8CIeF1aag= -github.com/ua-parser/uap-go v0.0.0-20240113215029-33f8e6d47f38/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E= +github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 h1:SIKIoA4e/5Y9ZOl0DCe3eVMLPOQzJxgZpfdHHeauNTM= +github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= -github.com/urfave/negroni/v3 v3.1.0 h1:lzmuxGSpnJCT/ujgIAjkU3+LW3NX8alCglO/L6KjIGQ= -github.com/urfave/negroni/v3 v3.1.0/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= +github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= +github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -284,16 +284,16 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck= -golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -318,8 +318,8 @@ golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -365,8 +365,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -389,16 +389,16 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -406,8 +406,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 5def48bad93de005aac49944ffbafe77debf272a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Monnom?= Date: Thu, 13 Jun 2024 00:00:02 +0200 Subject: [PATCH 14/30] fix agent jobs not launching when using the CreateRoom API (#2784) --- pkg/service/roomservice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/roomservice.go b/pkg/service/roomservice.go index 285c025d9..36e36c04c 100644 --- a/pkg/service/roomservice.go +++ b/pkg/service/roomservice.go @@ -104,7 +104,7 @@ func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomReq defer done() if created { - go s.agentClient.LaunchJob(ctx, &agent.JobDescription{ + go s.agentClient.LaunchJob(context.Background(), &agent.JobDescription{ JobType: livekit.JobType_JT_ROOM, Room: rm, }) From 6e4b0c20d15bdef751c51295be00f280d8cacf90 Mon Sep 17 00:00:00 2001 From: cnderrauber Date: Thu, 13 Jun 2024 10:13:18 +0800 Subject: [PATCH 15/30] update dep for fixing bucket grow (#2785) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 46b16997e..d575592ed 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/jellydator/ttlcache/v3 v3.2.0 github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 - github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a + github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 github.com/mackerelio/go-osstat v0.2.5 diff --git a/go.sum b/go.sum index d94143090..2c677afa0 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58= github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= -github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a h1:ATbv0x7G5tW2HgiouQ57csFE/G4gekl2oV1cxb2Dy24= -github.com/livekit/mediatransportutil v0.0.0-20240501132628-6105557bbb9a/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= +github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 h1:p60OjeixzXnhGFQL8wmdUwWPxijEDe9ZJFMosq+byec= +github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 h1:r6e2oEjmIR7PmeWpIzxImHJQU6gJ4gLwYlLH7NwVwVY= github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= From ea6036810050001e19fd9ac7f74cd477c12a039a Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Fri, 14 Jun 2024 11:10:57 +0530 Subject: [PATCH 16/30] Do not error out on invalid packet. (#2789) Remove the return when encountering invalid packet. Also, log more sparesely. Proper error returns from util so that we can selectively drop packets based on error type, for example SSRC mismatches are okay type of thing. --- pkg/sfu/buffer/buffer.go | 38 ++++++++++++++++++++------------------ pkg/sfu/utils/helpers.go | 13 ++++++++++--- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index f8dc63e06..89f776608 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -132,6 +132,7 @@ type Buffer struct { packetNotFoundCount atomic.Uint32 packetTooOldCount atomic.Uint32 extPacketTooMuchCount atomic.Uint32 + invalidPacketCount atomic.Uint32 primaryBufferForRTX *Buffer rtxPktBuf []byte @@ -317,24 +318,25 @@ func (b *Buffer) Write(pkt []byte) (n int, err error) { } if err = utils.ValidateRTPPacket(&rtpPacket, b.payloadType, b.mediaSSRC); err != nil { - b.logger.Warnw( - "validating RTP packet failed", err, - "version", rtpPacket.Version, - "padding", rtpPacket.Padding, - "marker", rtpPacket.Marker, - "expectedPayloadType", b.payloadType, - "payloadType", rtpPacket.PayloadType, - "sequenceNumber", rtpPacket.SequenceNumber, - "timestamp", rtpPacket.Timestamp, - "expectedSSRC", b.mediaSSRC, - "ssrc", rtpPacket.SSRC, - "numExtensions", len(rtpPacket.Extensions), - "payloadSize", len(rtpPacket.Payload), - "rtpStats", b.rtpStats, - "snRangeMap", b.snRangeMap, - ) - b.Unlock() - return + invalidPacketCount := b.invalidPacketCount.Inc() + if (invalidPacketCount-1)%100 == 0 { + b.logger.Warnw( + "validating RTP packet failed", err, + "version", rtpPacket.Version, + "padding", rtpPacket.Padding, + "marker", rtpPacket.Marker, + "expectedPayloadType", b.payloadType, + "payloadType", rtpPacket.PayloadType, + "sequenceNumber", rtpPacket.SequenceNumber, + "timestamp", rtpPacket.Timestamp, + "expectedSSRC", b.mediaSSRC, + "ssrc", rtpPacket.SSRC, + "numExtensions", len(rtpPacket.Extensions), + "payloadSize", len(rtpPacket.Payload), + "rtpStats", b.rtpStats, + "snRangeMap", b.snRangeMap, + ) + } } now := time.Now() diff --git a/pkg/sfu/utils/helpers.go b/pkg/sfu/utils/helpers.go index f3f12161e..074842777 100644 --- a/pkg/sfu/utils/helpers.go +++ b/pkg/sfu/utils/helpers.go @@ -16,6 +16,7 @@ package utils import ( "errors" + "fmt" "strings" "github.com/pion/interceptor" @@ -54,18 +55,24 @@ func GetHeaderExtensionID(extensions []interceptor.RTPHeaderExtension, extension return 0 } +var ( + ErrInvalidRTPVersion = errors.New("invalid RTP version") + ErrRTPPayloadTypeMismatch = errors.New("RTP payload type mismatch") + ErrRTPSSRCMismatch = errors.New("RTP SSRC mismatch") +) + // ValidateRTPPacket checks for a valid RTP packet and returns an error if fields are incorrect func ValidateRTPPacket(pkt *rtp.Packet, expectedPayloadType uint8, expectedSSRC uint32) error { if pkt.Version != 2 { - return errors.New("invalid RTP version") + return fmt.Errorf("%w, expected: 2, actual: %d", ErrInvalidRTPVersion, pkt.Version) } if expectedPayloadType != 0 && pkt.PayloadType != expectedPayloadType { - return errors.New("invalid RTP payload type") + return fmt.Errorf("%w, expected: %d, actual: %d", ErrRTPPayloadTypeMismatch, expectedPayloadType, pkt.PayloadType) } if expectedSSRC != 0 && pkt.SSRC != expectedSSRC { - return errors.New("invalid RTP SSRC") + return fmt.Errorf("%w, expected: %d, actual: %d", ErrRTPSSRCMismatch, expectedSSRC, pkt.SSRC) } return nil From ecf1175832d2038786427e849a22d3df1c93d953 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Thu, 13 Jun 2024 23:00:50 -0700 Subject: [PATCH 17/30] Generate and send uuid with analytics (#2790) * Generate and send uuid with analytics * go mod --- go.mod | 2 +- go.sum | 4 ++-- pkg/telemetry/analyticsservice.go | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d575592ed..b6574795e 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 - github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 + github.com/livekit/protocol v1.17.1-0.20240614054716-725bc923f98b github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 github.com/mackerelio/go-osstat v0.2.5 github.com/magefile/mage v1.15.0 diff --git a/go.sum b/go.sum index 2c677afa0..eb5cbd0f2 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkD github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 h1:p60OjeixzXnhGFQL8wmdUwWPxijEDe9ZJFMosq+byec= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= -github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2 h1:r6e2oEjmIR7PmeWpIzxImHJQU6gJ4gLwYlLH7NwVwVY= -github.com/livekit/protocol v1.17.1-0.20240606064424-fcde125058b2/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= +github.com/livekit/protocol v1.17.1-0.20240614054716-725bc923f98b h1:wRADQpZkCv2ml1W3PZCmFxJKdyAS4T1nleaAdv8rRpY= +github.com/livekit/protocol v1.17.1-0.20240614054716-725bc923f98b/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= diff --git a/pkg/telemetry/analyticsservice.go b/pkg/telemetry/analyticsservice.go index 92166d1ca..f6f44e5e7 100644 --- a/pkg/telemetry/analyticsservice.go +++ b/pkg/telemetry/analyticsservice.go @@ -23,6 +23,7 @@ import ( "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" "github.com/livekit/protocol/rpc" + "github.com/livekit/protocol/utils/guid" "github.com/livekit/livekit-server/pkg/config" "github.com/livekit/livekit-server/pkg/routing" @@ -58,6 +59,7 @@ func (a *analyticsService) SendStats(_ context.Context, stats []*livekit.Analyti } for _, stat := range stats { + stat.Id = guid.New("AS_") stat.AnalyticsKey = a.analyticsKey stat.Node = a.nodeID } @@ -71,6 +73,7 @@ func (a *analyticsService) SendEvent(_ context.Context, event *livekit.Analytics return } + event.Id = guid.New("AE_") event.NodeId = a.nodeID event.AnalyticsKey = a.analyticsKey if err := a.events.Send(&livekit.AnalyticsEvents{ From 58e365847b6d36963fc708af2c0e4eb4ca791c6e Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Thu, 13 Jun 2024 23:22:39 -0700 Subject: [PATCH 18/30] add test helper for config yaml tags (#2791) * add test helper for config yaml tags * deps * cleanup * cleanup --- go.mod | 4 +- go.sum | 4 +- pkg/config/config_test.go | 6 +++ pkg/config/configtest/checkyamltag.go | 64 +++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 pkg/config/configtest/checkyamltag.go diff --git a/go.mod b/go.mod index b6574795e..d094e728c 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 - github.com/livekit/protocol v1.17.1-0.20240614054716-725bc923f98b + github.com/livekit/protocol v1.17.1-0.20240614060801-425cb974f7a4 github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 github.com/mackerelio/go-osstat v0.2.5 github.com/magefile/mage v1.15.0 @@ -48,6 +48,7 @@ require ( github.com/urfave/cli/v2 v2.27.2 github.com/urfave/negroni/v3 v3.1.1 go.uber.org/atomic v1.11.0 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 golang.org/x/sync v0.7.0 @@ -97,7 +98,6 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap/exp v0.2.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/mod v0.18.0 // indirect diff --git a/go.sum b/go.sum index eb5cbd0f2..a25f10515 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkD github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 h1:p60OjeixzXnhGFQL8wmdUwWPxijEDe9ZJFMosq+byec= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= -github.com/livekit/protocol v1.17.1-0.20240614054716-725bc923f98b h1:wRADQpZkCv2ml1W3PZCmFxJKdyAS4T1nleaAdv8rRpY= -github.com/livekit/protocol v1.17.1-0.20240614054716-725bc923f98b/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= +github.com/livekit/protocol v1.17.1-0.20240614060801-425cb974f7a4 h1:5O3/wahQIMnw+PO3O1kgxPSrpJf7PkPcD3GvWmb1TJQ= +github.com/livekit/protocol v1.17.1-0.20240614060801-425cb974f7a4/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 0e4719fff..833266de6 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -20,6 +20,8 @@ import ( "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" + + "github.com/livekit/livekit-server/pkg/config/configtest" ) func TestConfig_UnmarshalKeys(t *testing.T) { @@ -80,3 +82,7 @@ func TestGeneratedFlags(t *testing.T) { require.NotNil(t, conf.RTC.ReconnectOnSubscriptionError) require.False(t, *conf.RTC.ReconnectOnSubscriptionError) } + +func TestYAMLTag(t *testing.T) { + require.NoError(t, configtest.CheckYAMLTags(Config{})) +} diff --git a/pkg/config/configtest/checkyamltag.go b/pkg/config/configtest/checkyamltag.go new file mode 100644 index 000000000..a8ee58371 --- /dev/null +++ b/pkg/config/configtest/checkyamltag.go @@ -0,0 +1,64 @@ +package configtest + +import ( + "fmt" + "reflect" + "slices" + "strings" + + "go.uber.org/multierr" + "google.golang.org/protobuf/proto" +) + +var protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem() + +func checkYAMLTags(t reflect.Type, seen map[reflect.Type]struct{}) error { + if _, ok := seen[t]; ok { + return nil + } + seen[t] = struct{}{} + + switch t.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.Pointer: + return checkYAMLTags(t.Elem(), seen) + case reflect.Struct: + if reflect.PointerTo(t).Implements(protoMessageType) { + // ignore protobuf messages + return nil + } + + var errs error + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + if field.Type.Kind() == reflect.Bool { + // ignore boolean fields + continue + } + + if field.Tag.Get("config") == "allowempty" { + // ignore configured exceptions + continue + } + + parts := strings.Split(field.Tag.Get("yaml"), ",") + if parts[0] == "-" { + // ignore unparsed fields + continue + } + + if !slices.Contains(parts, "omitempty") && !slices.Contains(parts, "inline") { + errs = multierr.Append(errs, fmt.Errorf("%s/%s.%s missing omitempty tag", t.PkgPath(), t.Name(), field.Name)) + } + + errs = multierr.Append(errs, checkYAMLTags(field.Type, seen)) + } + return errs + default: + return nil + } +} + +func CheckYAMLTags(config any) error { + return checkYAMLTags(reflect.TypeOf(config), map[reflect.Type]struct{}{}) +} From f92e7e3db8e787200d3c422704574dc37f87169f Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Sat, 15 Jun 2024 20:44:50 +0530 Subject: [PATCH 19/30] use pending lock, no need for participant lock (#2793) --- pkg/rtc/participant.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/rtc/participant.go b/pkg/rtc/participant.go index 1238fcca1..504b7bc24 100644 --- a/pkg/rtc/participant.go +++ b/pkg/rtc/participant.go @@ -779,8 +779,9 @@ func (p *ParticipantImpl) AddTrack(req *livekit.AddTrackRequest) { return } - p.lock.Lock() - defer p.lock.Unlock() + p.pendingTracksLock.Lock() + defer p.pendingTracksLock.Unlock() + ti := p.addPendingTrackLocked(req) if ti == nil { return @@ -1767,9 +1768,6 @@ func (p *ParticipantImpl) onSubscribedMaxQualityChange( } func (p *ParticipantImpl) addPendingTrackLocked(req *livekit.AddTrackRequest) *livekit.TrackInfo { - p.pendingTracksLock.Lock() - defer p.pendingTracksLock.Unlock() - if req.Sid != "" { track := p.GetPublishedTrack(livekit.TrackID(req.Sid)) if track == nil { From ecfc42c3f9918919af0f14a998598068f983c062 Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sat, 15 Jun 2024 18:59:03 -0700 Subject: [PATCH 20/30] Version 1.6.2 (#2794) --- CHANGELOG | 76 ++++++++++++++++++++++++++++++++++++++++++++++ version/version.go | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 362fb85ce..3a9278aaa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,82 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.2] - 2024-06-15 + +### Added + +- Support for optional publisher datachannel (#2693) +- add room/participant name limit (#2704) +- Pass through timestamp in abs capture time (#2715) +- Support SIP transports. (#2724) + +### Fixed + +- add missing strings.EqualFold for some mimeType comparisons (#2701) +- connection reset without any closing handshake on clientside (#2709) +- Do not propagate RTCP if report is not processed. (#2739) +- Fix DD tracker addition. (#2751) +- Reset tracker on expected layer increase. (#2753) +- Do not add tracker for invalid layers. (#2759) +- Do not compare payload type before bind (#2775) +- fix agent jobs not launching when using the CreateRoom API (#2784) + +### Changed + +- Performance improvements to forwarding by using condition var. (#2691 #2699) +- Simplify time stamp calculation on switches. (#2688) +- Simplify layer roll back. (#2702) +- ensure room is running before attempting to delete (#2705) +- Redact egress object in CreateRoom request (#2710) +- reduce participant lock scope (#2732) +- Demote some less useful/noisy logs. (#2743) +- Stop probe on probe controller reset (#2744) +- initialize bucket size by publish bitrates (#2763) +- Validate RTP packets. (#2778) + +## [1.6.1] - 2024-04-26 + +This release changes the default behavior when creating or updating WHIP +ingress. WHIP ingress will now default to disabling transcoding and +forwarding media unchanged to the LiveKit subscribers. This behavior can +be changed by using the new `enable_transcoding` available in updated +SDKs. The behavior of existing ingresses is unchanged. + +### Added + +- Add support for "abs-capture-time" extension. (#2640) +- Add PropagationDelay API to sender report data (#2646) +- Add support for EnableTranscoding ingress option (#2681) +- Pass new SIP metadata. Update protocol. (#2683) +- Handle UpdateLocalAudioTrack and UpdateLocalVideoTrack. (#2684) +- Forward transcription data packets to the room (#2687) + +### Fixed + +- backwards compatability for IsRecorder (#2647) +- Reduce RED weight in half. (#2648) +- add disconnected chan to participant (#2650) +- add typed ops queue (#2655) +- ICE config cache module. (#2654) +- use typed ops queue in pctransport (#2656) +- Use the ingress state updated_at field to ensure that out of order RPC do not overwrite state (#2657) +- Log ICE candidates to debug TCP connection issues. (#2658) +- Debug logging addition of ICE candidate (#2659) +- fix participant, ensure room name matches (#2660) +- replace keyframe ticker with timer (#2661) +- fix key frame timer (#2662) +- Disable dynamic playout delay for screenshare track (#2663) +- Don't log dd invalid template index (#2664) +- Do codec munging when munging RTP header. (#2665) +- Connection quality LOST only if RTCP is also not available. (#2670) +- Handle large jumps in RTCP sender report timestamp. (#2674) +- Bump golang.org/x/net from 0.22.0 to 0.23.0 (#2673) +- do not capture pointers in ops queue closures (#2675) +- Fix SubParticipant twice when paticipant left (#2672) +- use ttlcache (#2677) +- Detach subscriber datachannel to save memory (#2680) +- Clean up UpdateVideoLayers (#2685) + ## [1.6.0] - 2024-04-10 ### Added diff --git a/version/version.go b/version/version.go index b6dc66aa5..35b5cfe1f 100644 --- a/version/version.go +++ b/version/version.go @@ -14,4 +14,4 @@ package version -const Version = "1.6.1" +const Version = "1.6.2" From 88a340202a43caeab7d82f0fb357fc9d7a24a4db Mon Sep 17 00:00:00 2001 From: David Zhao Date: Sat, 15 Jun 2024 19:55:16 -0700 Subject: [PATCH 21/30] Update release workflow (#2795) * Update release workflow * rename --- .github/workflows/docker.yaml | 2 +- .github/workflows/release.yaml | 6 +++--- .goreleaser.yaml | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 38258deb8..e4e161faa 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -43,7 +43,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '>=1.21' + go-version-file: "go.mod" - name: Download Go modules run: go mod download diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 84f9ef1fb..ce7e9a93a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -35,13 +35,13 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '>=1.21' + go-version-file: "go.mod" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser - version: latest - args: release --rm-dist + version: '~> v2' + args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f8e54c4e1..44b98294b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +version: 2 + before: hooks: - go mod tidy @@ -31,6 +33,7 @@ builds: goos: - linux - windows + archives: - format_overrides: - goos: windows @@ -56,4 +59,3 @@ checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ incpatch .Version }}-next" - From 2bc101d3231774dfc9f5c1a71440c732aba6d189 Mon Sep 17 00:00:00 2001 From: Paul Wells Date: Sun, 16 Jun 2024 21:16:29 -0700 Subject: [PATCH 22/30] use request context for LaunchJob api request (#2796) * use request context for LaunchJob api request * one more --- pkg/service/roomservice.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/service/roomservice.go b/pkg/service/roomservice.go index 36e36c04c..0bae86fab 100644 --- a/pkg/service/roomservice.go +++ b/pkg/service/roomservice.go @@ -104,7 +104,7 @@ func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomReq defer done() if created { - go s.agentClient.LaunchJob(context.Background(), &agent.JobDescription{ + go s.agentClient.LaunchJob(context.WithoutCancel(ctx), &agent.JobDescription{ JobType: livekit.JobType_JT_ROOM, Room: rm, }) @@ -314,7 +314,7 @@ func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat } if created { - go s.agentClient.LaunchJob(ctx, &agent.JobDescription{ + go s.agentClient.LaunchJob(context.WithoutCancel(ctx), &agent.JobDescription{ JobType: livekit.JobType_JT_ROOM, Room: room, }) From 5d969ba35bcd94e44621a7bfe42cd86776955b61 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Mon, 17 Jun 2024 12:57:04 +0530 Subject: [PATCH 23/30] remove some debug (#2797) --- pkg/sfu/buffer/rtpstats_base.go | 8 +++++++- pkg/sfu/rtpmunger.go | 15 --------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/pkg/sfu/buffer/rtpstats_base.go b/pkg/sfu/buffer/rtpstats_base.go index d26907179..e588f1dcb 100644 --- a/pkg/sfu/buffer/rtpstats_base.go +++ b/pkg/sfu/buffer/rtpstats_base.go @@ -610,7 +610,13 @@ func (r *rtpStatsBase) deltaInfo(snapshotID uint32, extStartSN uint64, extHighes // padding packets delta could be higher than expected due to out-of-order padding packets packetsPadding := now.packetsPadding - then.packetsPadding if packetsExpected < packetsPadding { - r.logger.Infow("padding packets more than expected", "packetsExpected", packetsExpected, "packetsPadding", packetsPadding) + r.logger.Infow( + "padding packets more than expected", + "packetsExpected", packetsExpected, + "packetsPadding", packetsPadding, + "startSequenceNumber", then.extStartSN, + "endSequenceNumber", now.extStartSN-1, + ) packetsExpected = 0 } else { packetsExpected -= packetsPadding diff --git a/pkg/sfu/rtpmunger.go b/pkg/sfu/rtpmunger.go index c1fff7cd3..0e8fa78ab 100644 --- a/pkg/sfu/rtpmunger.go +++ b/pkg/sfu/rtpmunger.go @@ -75,7 +75,6 @@ type RTPMunger struct { extHighestIncomingSN uint64 snRangeMap *utils.RangeMap[uint64, uint64] - extHighestIncomingTS uint64 // TODO-REMOVE-AFTER-DATA-COLLECTION extLastSN uint64 extSecondLastSN uint64 @@ -139,7 +138,6 @@ func (r *RTPMunger) SeedLast(state RTPMungerState) { func (r *RTPMunger) SetLastSnTs(extPkt *buffer.ExtPacket) { r.extHighestIncomingSN = extPkt.ExtSequenceNumber - 1 - r.extHighestIncomingTS = extPkt.ExtTimestamp - 1 r.extLastSN = extPkt.ExtSequenceNumber r.extSecondLastSN = r.extLastSN - 1 @@ -153,7 +151,6 @@ func (r *RTPMunger) SetLastSnTs(extPkt *buffer.ExtPacket) { func (r *RTPMunger) UpdateSnTsOffsets(extPkt *buffer.ExtPacket, snAdjust uint64, tsAdjust uint64) { r.extHighestIncomingSN = extPkt.ExtSequenceNumber - 1 - r.extHighestIncomingTS = extPkt.ExtTimestamp - 1 r.snRangeMap.ClearAndResetValue(extPkt.ExtSequenceNumber, extPkt.ExtSequenceNumber-r.extLastSN-snAdjust) r.updateSnOffset() @@ -194,18 +191,6 @@ func (r *RTPMunger) UpdateAndGetSnTs(extPkt *buffer.ExtPacket, marker bool) (Tra // in-order - either contiguous packet with payload OR packet following a gap, may or may not have payload r.extHighestIncomingSN = extPkt.ExtSequenceNumber - // TODO-REMOVE-AFTER-DATA-COLLECTION - tsDiff := int64(extPkt.ExtTimestamp - r.extHighestIncomingTS) - if tsDiff > 24000 { // 1/2 second at audio clock rate - r.logger.Infow( - "big jump in incoming timestamp", - "last", r.extHighestIncomingTS, - "current", extPkt.ExtTimestamp, - "diff", tsDiff, - ) - } - r.extHighestIncomingTS = extPkt.ExtTimestamp - ordering := SequenceNumberOrderingContiguous if diff > 1 { ordering = SequenceNumberOrderingGap From 6a328364592b02ccafdfd33ea19bc0788639710a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:07:22 -0700 Subject: [PATCH 24/30] fix(deps): update go deps (#2788) Generated by renovateBot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d094e728c..b85a91cda 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gammazero/deque v0.2.1 github.com/gammazero/workerpool v1.1.3 github.com/google/wire v0.6.0 - github.com/gorilla/websocket v1.5.2 + github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jellydator/ttlcache/v3 v3.2.0 @@ -50,7 +50,7 @@ require ( go.uber.org/atomic v1.11.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 golang.org/x/sync v0.7.0 google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index a25f10515..f42fa94c8 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= -github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -286,8 +286,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= From aa72466ac8546061e0e23934dbc9b0a05a99db87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:07:40 -0700 Subject: [PATCH 25/30] chore(deps): update docker/build-push-action action to v6 (#2798) Generated by renovateBot Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index e4e161faa..8da0f1700 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -66,7 +66,7 @@ jobs: - name: Build and push id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . push: ${{ github.event_name != 'pull_request' }} From ef838e4fa2d9d8a81a54a32a14b3d138e61339c8 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Mon, 17 Jun 2024 23:51:00 +0530 Subject: [PATCH 26/30] Indicate if track is expectd to be resumed in `onClose` callback. (#2800) That is the main change. Changed variable name to `isExpectedToResume` everywhere to be consistent. Planning to use the callback value in relays to determine if the down track should be closed or switched to a different up track. --- pkg/rtc/mediatrack.go | 6 +-- pkg/rtc/mediatrackreceiver.go | 48 ++++++++++--------- pkg/rtc/mediatracksubscriptions.go | 24 +++++----- pkg/rtc/participant.go | 2 +- pkg/rtc/subscribedtrack.go | 4 +- pkg/rtc/subscriptionmanager.go | 16 +++---- pkg/rtc/subscriptionmanager_test.go | 28 +++++------ pkg/rtc/types/interfaces.go | 14 +++--- .../typesfakes/fake_local_media_track.go | 12 ++--- pkg/rtc/types/typesfakes/fake_media_track.go | 12 ++--- .../types/typesfakes/fake_subscribed_track.go | 12 ++--- pkg/rtc/uptrackmanager.go | 12 ++--- pkg/sfu/downtrack.go | 6 +-- 13 files changed, 99 insertions(+), 97 deletions(-) diff --git a/pkg/rtc/mediatrack.go b/pkg/rtc/mediatrack.go index 4531f0dcd..85368df95 100644 --- a/pkg/rtc/mediatrack.go +++ b/pkg/rtc/mediatrack.go @@ -411,13 +411,13 @@ func (t *MediaTrack) Restart() { } } -func (t *MediaTrack) Close(willBeResumed bool) { +func (t *MediaTrack) Close(isExpectedToResume bool) { t.MediaTrackReceiver.SetClosing() if t.dynacastManager != nil { t.dynacastManager.Close() } - t.MediaTrackReceiver.ClearAllReceivers(willBeResumed) - t.MediaTrackReceiver.Close() + t.MediaTrackReceiver.ClearAllReceivers(isExpectedToResume) + t.MediaTrackReceiver.Close(isExpectedToResume) } func (t *MediaTrack) SetMuted(muted bool) { diff --git a/pkg/rtc/mediatrackreceiver.go b/pkg/rtc/mediatrackreceiver.go index b346eb37f..e4c5b4448 100644 --- a/pkg/rtc/mediatrackreceiver.go +++ b/pkg/rtc/mediatrackreceiver.go @@ -97,16 +97,16 @@ type MediaTrackReceiverParams struct { type MediaTrackReceiver struct { params MediaTrackReceiverParams - lock sync.RWMutex - receivers []*simulcastReceiver - trackInfo *livekit.TrackInfo - potentialCodecs []webrtc.RTPCodecParameters - state mediaTrackReceiverState - willBeResumed bool + lock sync.RWMutex + receivers []*simulcastReceiver + trackInfo *livekit.TrackInfo + potentialCodecs []webrtc.RTPCodecParameters + state mediaTrackReceiverState + isExpectedToResume bool onSetupReceiver func(mime string) onMediaLossFeedback func(dt *sfu.DownTrack, report *rtcp.ReceiverReport) - onClose []func() + onClose []func(isExpectedToResume bool) *MediaTrackSubscriptions } @@ -258,7 +258,7 @@ func (t *MediaTrackReceiver) SetPotentialCodecs(codecs []webrtc.RTPCodecParamete t.lock.Unlock() } -func (t *MediaTrackReceiver) ClearReceiver(mime string, willBeResumed bool) { +func (t *MediaTrackReceiver) ClearReceiver(mime string, isExpectedToResume bool) { t.lock.Lock() receivers := slices.Clone(t.receivers) for idx, receiver := range receivers { @@ -272,20 +272,20 @@ func (t *MediaTrackReceiver) ClearReceiver(mime string, willBeResumed bool) { t.receivers = receivers t.lock.Unlock() - t.removeAllSubscribersForMime(mime, willBeResumed) + t.removeAllSubscribersForMime(mime, isExpectedToResume) } -func (t *MediaTrackReceiver) ClearAllReceivers(willBeResumed bool) { +func (t *MediaTrackReceiver) ClearAllReceivers(isExpectedToResume bool) { t.params.Logger.Debugw("clearing all receivers") t.lock.Lock() receivers := t.receivers t.receivers = nil - t.willBeResumed = willBeResumed + t.isExpectedToResume = isExpectedToResume t.lock.Unlock() for _, r := range receivers { - t.removeAllSubscribersForMime(r.Codec().MimeType, willBeResumed) + t.removeAllSubscribersForMime(r.Codec().MimeType, isExpectedToResume) } } @@ -332,16 +332,18 @@ func (t *MediaTrackReceiver) TryClose() bool { numActiveReceivers++ } } + + isExpectedToResume := t.isExpectedToResume t.lock.RUnlock() if numActiveReceivers != 0 { return false } - t.Close() + t.Close(isExpectedToResume) return true } -func (t *MediaTrackReceiver) Close() { +func (t *MediaTrackReceiver) Close(isExpectedToResume bool) { t.lock.Lock() if t.state == mediaTrackReceiverStateClosed { t.lock.Unlock() @@ -353,7 +355,7 @@ func (t *MediaTrackReceiver) Close() { t.lock.Unlock() for _, f := range onclose { - f() + f(isExpectedToResume) } } @@ -437,7 +439,7 @@ func (t *MediaTrackReceiver) SetMuted(muted bool) { t.MediaTrackSubscriptions.SetMuted(muted) } -func (t *MediaTrackReceiver) AddOnClose(f func()) { +func (t *MediaTrackReceiver) AddOnClose(f func(isExpectedToResume bool)) { if f == nil { return } @@ -499,16 +501,16 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su // media track could have been closed while adding subscription remove := false - willBeResumed := false + isExpectedToResume := false t.lock.RLock() if t.state != mediaTrackReceiverStateOpen { - willBeResumed = t.willBeResumed + isExpectedToResume = t.isExpectedToResume remove = true } t.lock.RUnlock() if remove { - _ = t.MediaTrackSubscriptions.RemoveSubscriber(sub.ID(), willBeResumed) + _ = t.MediaTrackSubscriptions.RemoveSubscriber(sub.ID(), isExpectedToResume) return nil, ErrNotOpen } @@ -517,14 +519,14 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su // RemoveSubscriber removes participant from subscription // stop all forwarders to the client -func (t *MediaTrackReceiver) RemoveSubscriber(subscriberID livekit.ParticipantID, willBeResumed bool) { - _ = t.MediaTrackSubscriptions.RemoveSubscriber(subscriberID, willBeResumed) +func (t *MediaTrackReceiver) RemoveSubscriber(subscriberID livekit.ParticipantID, isExpectedToResume bool) { + _ = t.MediaTrackSubscriptions.RemoveSubscriber(subscriberID, isExpectedToResume) } -func (t *MediaTrackReceiver) removeAllSubscribersForMime(mime string, willBeResumed bool) { +func (t *MediaTrackReceiver) removeAllSubscribersForMime(mime string, isExpectedToResume bool) { t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime) for _, subscriberID := range t.MediaTrackSubscriptions.GetAllSubscribersForMime(mime) { - t.RemoveSubscriber(subscriberID, willBeResumed) + t.RemoveSubscriber(subscriberID, isExpectedToResume) } } diff --git a/pkg/rtc/mediatracksubscriptions.go b/pkg/rtc/mediatracksubscriptions.go index e2f4d5b00..f49faeb68 100644 --- a/pkg/rtc/mediatracksubscriptions.go +++ b/pkg/rtc/mediatracksubscriptions.go @@ -296,8 +296,8 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr * // But, the subscription could be removed early if the published track is closed // while adding subscription. In those cases, subscription manager would not have set // the `OnClose` callback. So, set it here to handle cases of early close. - subTrack.OnClose(func(willBeResumed bool) { - if !willBeResumed { + subTrack.OnClose(func(isExpectedToResume bool) { + if !isExpectedToResume { if err := sub.RemoveTrackFromSubscriber(sender); err != nil { t.params.Logger.Warnw("could not remove track from peer connection", err) } @@ -306,8 +306,8 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr * downTrack.SetTransceiver(transceiver) - downTrack.OnCloseHandler(func(willBeResumed bool) { - go t.downTrackClosed(sub, willBeResumed) + downTrack.OnCloseHandler(func(isExpectedToResume bool) { + go t.downTrackClosed(sub, isExpectedToResume) }) t.subscribedTracksMu.Lock() @@ -319,24 +319,24 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr * // RemoveSubscriber removes participant from subscription // stop all forwarders to the client -func (t *MediaTrackSubscriptions) RemoveSubscriber(subscriberID livekit.ParticipantID, willBeResumed bool) error { +func (t *MediaTrackSubscriptions) RemoveSubscriber(subscriberID livekit.ParticipantID, isExpectedToResume bool) error { subTrack := t.getSubscribedTrack(subscriberID) if subTrack == nil { return errNotFound } - t.params.Logger.Debugw("removing subscriber", "subscriberID", subscriberID, "willBeResumed", willBeResumed) - t.closeSubscribedTrack(subTrack, willBeResumed) + t.params.Logger.Debugw("removing subscriber", "subscriberID", subscriberID, "isExpectedToResume", isExpectedToResume) + t.closeSubscribedTrack(subTrack, isExpectedToResume) return nil } -func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.SubscribedTrack, willBeResumed bool) { +func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.SubscribedTrack, isExpectedToResume bool) { dt := subTrack.DownTrack() if dt == nil { return } - if willBeResumed { + if isExpectedToResume { dt.CloseWithFlush(false) } else { // flushing blocks, avoid blocking when publisher removes all its subscribers @@ -418,7 +418,7 @@ func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} { func (t *MediaTrackSubscriptions) downTrackClosed( sub types.LocalParticipant, - willBeResumed bool, + isExpectedToResume bool, ) { subscriberID := sub.ID() t.subscribedTracksMu.RLock() @@ -429,7 +429,7 @@ func (t *MediaTrackSubscriptions) downTrackClosed( // Cache transceiver for potential re-use on resume. // To ensure subscription manager does not re-subscribe before caching, // delete the subscribed track only after caching. - if willBeResumed { + if isExpectedToResume { dt := subTrack.DownTrack() tr := dt.GetTransceiver() if tr != nil { @@ -442,6 +442,6 @@ func (t *MediaTrackSubscriptions) downTrackClosed( delete(t.subscribedTracks, subscriberID) t.subscribedTracksMu.Unlock() - subTrack.Close(willBeResumed) + subTrack.Close(isExpectedToResume) } } diff --git a/pkg/rtc/participant.go b/pkg/rtc/participant.go index 504b7bc24..e7e847291 100644 --- a/pkg/rtc/participant.go +++ b/pkg/rtc/participant.go @@ -2125,7 +2125,7 @@ func (p *ParticipantImpl) addMediaTrack(signalCid string, sdpCid string, ti *liv } trackID := livekit.TrackID(ti.Sid) - mt.AddOnClose(func() { + mt.AddOnClose(func(_isExpectedToRsume bool) { if p.supervisor != nil { p.supervisor.ClearPublishedTrack(trackID, mt) } diff --git a/pkg/rtc/subscribedtrack.go b/pkg/rtc/subscribedtrack.go index eff9b8358..f5dd9e995 100644 --- a/pkg/rtc/subscribedtrack.go +++ b/pkg/rtc/subscribedtrack.go @@ -139,9 +139,9 @@ func (t *SubscribedTrack) Bound(err error) { } // for DownTrack callback to notify us that it's closed -func (t *SubscribedTrack) Close(willBeResumed bool) { +func (t *SubscribedTrack) Close(isExpectedToResume bool) { if onClose := t.onClose.Load(); onClose != nil { - go onClose.(func(bool))(willBeResumed) + go onClose.(func(bool))(isExpectedToResume) } } diff --git a/pkg/rtc/subscriptionmanager.go b/pkg/rtc/subscriptionmanager.go index a7dfa6608..4a49b11db 100644 --- a/pkg/rtc/subscriptionmanager.go +++ b/pkg/rtc/subscriptionmanager.go @@ -91,7 +91,7 @@ func NewSubscriptionManager(params SubscriptionManagerParams) *SubscriptionManag return m } -func (m *SubscriptionManager) Close(willBeResumed bool) { +func (m *SubscriptionManager) Close(isExpectedToResume bool) { m.lock.Lock() if m.isClosed() { m.lock.Unlock() @@ -113,7 +113,7 @@ func (m *SubscriptionManager) Close(willBeResumed bool) { } } - if willBeResumed { + if isExpectedToResume { for _, dt := range downTracksToClose { dt.CloseWithFlush(false) } @@ -523,8 +523,8 @@ func (m *SubscriptionManager) subscribe(s *trackSubscription) error { ) } if err == nil && subTrack != nil { // subTrack could be nil if already subscribed - subTrack.OnClose(func(willBeResumed bool) { - m.handleSubscribedTrackClose(s, willBeResumed) + subTrack.OnClose(func(isExpectedToResume bool) { + m.handleSubscribedTrackClose(s, isExpectedToResume) }) subTrack.AddOnBind(func(err error) { if err != nil { @@ -615,10 +615,10 @@ func (m *SubscriptionManager) handleSourceTrackRemoved(trackID livekit.TrackID) // - subscriber-initiated unsubscribe // - UpTrack was closed // - publisher revoked permissions for the participant -func (m *SubscriptionManager) handleSubscribedTrackClose(s *trackSubscription, willBeResumed bool) { +func (m *SubscriptionManager) handleSubscribedTrackClose(s *trackSubscription, isExpectedToResume bool) { s.logger.Debugw( "subscribed track closed", - "willBeResumed", willBeResumed, + "isExpectedToResume", isExpectedToResume, ) wasBound := s.isBound() subTrack := s.getSubscribedTrack() @@ -666,7 +666,7 @@ func (m *SubscriptionManager) handleSubscribedTrackClose(s *trackSubscription, w context.Background(), m.params.Participant.ID(), &livekit.TrackInfo{Sid: string(s.trackID), Type: subTrack.MediaTrack().Kind()}, - !willBeResumed, + !isExpectedToResume, ) dt := subTrack.DownTrack() @@ -684,7 +684,7 @@ func (m *SubscriptionManager) handleSubscribedTrackClose(s *trackSubscription, w } } - if !willBeResumed { + if !isExpectedToResume { sender := subTrack.RTPSender() if sender != nil { s.logger.Debugw("removing PeerConnection track", diff --git a/pkg/rtc/subscriptionmanager_test.go b/pkg/rtc/subscriptionmanager_test.go index a666f47f6..c6410391c 100644 --- a/pkg/rtc/subscriptionmanager_test.go +++ b/pkg/rtc/subscriptionmanager_test.go @@ -214,11 +214,11 @@ func TestUnsubscribe(t *testing.T) { st, err := res.Track.AddSubscriber(sm.params.Participant) require.NoError(t, err) s.subscribedTrack = st - st.OnClose(func(willBeResumed bool) { - sm.handleSubscribedTrackClose(s, willBeResumed) + st.OnClose(func(isExpectedToResume bool) { + sm.handleSubscribedTrackClose(s, isExpectedToResume) }) - res.Track.(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, willBeResumed bool) { - setTestSubscribedTrackClosed(t, st, willBeResumed) + res.Track.(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) { + setTestSubscribedTrackClosed(t, st, isExpectedToResume) }) sm.lock.Lock() @@ -279,18 +279,18 @@ func TestSubscribeStatusChanged(t *testing.T) { return !s1.needsSubscribe() && !s2.needsSubscribe() }, subSettleTimeout, subCheckInterval, "track1 and track2 should be subscribed") st1 := s1.getSubscribedTrack() - st1.OnClose(func(willBeResumed bool) { - sm.handleSubscribedTrackClose(s1, willBeResumed) + st1.OnClose(func(isExpectedToResume bool) { + sm.handleSubscribedTrackClose(s1, isExpectedToResume) }) st2 := s2.getSubscribedTrack() - st2.OnClose(func(willBeResumed bool) { - sm.handleSubscribedTrackClose(s2, willBeResumed) + st2.OnClose(func(isExpectedToResume bool) { + sm.handleSubscribedTrackClose(s2, isExpectedToResume) }) - st1.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, willBeResumed bool) { - setTestSubscribedTrackClosed(t, st1, willBeResumed) + st1.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) { + setTestSubscribedTrackClosed(t, st1, isExpectedToResume) }) - st2.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, willBeResumed bool) { - setTestSubscribedTrackClosed(t, st2, willBeResumed) + st2.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) { + setTestSubscribedTrackClosed(t, st2, isExpectedToResume) }) require.Eventually(t, func() bool { @@ -533,9 +533,9 @@ func setTestSubscribedTrackBound(t *testing.T, st types.SubscribedTrack) { } } -func setTestSubscribedTrackClosed(t *testing.T, st types.SubscribedTrack, willBeResumed bool) { +func setTestSubscribedTrackClosed(t *testing.T, st types.SubscribedTrack, isExpectedToResume bool) { fst, ok := st.(*typesfakes.FakeSubscribedTrack) require.True(t, ok) - fst.OnCloseArgsForCall(0)(willBeResumed) + fst.OnCloseArgsForCall(0)(isExpectedToResume) } diff --git a/pkg/rtc/types/interfaces.go b/pkg/rtc/types/interfaces.go index b51aaa784..93bb32206 100644 --- a/pkg/rtc/types/interfaces.go +++ b/pkg/rtc/types/interfaces.go @@ -261,7 +261,7 @@ type Participant interface { IsPublisher() bool GetPublishedTrack(trackID livekit.TrackID) MediaTrack GetPublishedTracks() []MediaTrack - RemovePublishedTrack(track MediaTrack, willBeResumed bool, shouldClose bool) + RemovePublishedTrack(track MediaTrack, isExpectedToResume bool, shouldClose bool) GetAudioLevel() (smoothedLevel float64, active bool) @@ -466,15 +466,15 @@ type MediaTrack interface { GetAudioLevel() (level float64, active bool) - Close(willBeResumed bool) + Close(isExpectedToResume bool) IsOpen() bool // callbacks - AddOnClose(func()) + AddOnClose(func(isExpectedToResume bool)) // subscribers AddSubscriber(participant LocalParticipant) (SubscribedTrack, error) - RemoveSubscriber(participantID livekit.ParticipantID, willBeResumed bool) + RemoveSubscriber(participantID livekit.ParticipantID, isExpectedToResume bool) IsSubscriber(subID livekit.ParticipantID) bool RevokeDisallowedSubscribers(allowedSubscriberIdentities []livekit.ParticipantIdentity) []livekit.ParticipantIdentity GetAllSubscribers() []livekit.ParticipantID @@ -487,7 +487,7 @@ type MediaTrack interface { GetTemporalLayerForSpatialFps(spatial int32, fps uint32, mime string) int32 Receivers() []sfu.TrackReceiver - ClearAllReceivers(willBeResumed bool) + ClearAllReceivers(isExpectedToResume bool) IsEncrypted() bool } @@ -514,8 +514,8 @@ type LocalMediaTrack interface { type SubscribedTrack interface { AddOnBind(f func(error)) IsBound() bool - Close(willBeResumed bool) - OnClose(f func(willBeResumed bool)) + Close(isExpectedToResume bool) + OnClose(f func(isExpectedToResume bool)) ID() livekit.TrackID PublisherID() livekit.ParticipantID PublisherIdentity() livekit.ParticipantIdentity diff --git a/pkg/rtc/types/typesfakes/fake_local_media_track.go b/pkg/rtc/types/typesfakes/fake_local_media_track.go index 3545f9187..1bf6297b4 100644 --- a/pkg/rtc/types/typesfakes/fake_local_media_track.go +++ b/pkg/rtc/types/typesfakes/fake_local_media_track.go @@ -10,10 +10,10 @@ import ( ) type FakeLocalMediaTrack struct { - AddOnCloseStub func(func()) + AddOnCloseStub func(func(isExpectedToResume bool)) addOnCloseMutex sync.RWMutex addOnCloseArgsForCall []struct { - arg1 func() + arg1 func(isExpectedToResume bool) } AddSubscriberStub func(types.LocalParticipant) (types.SubscribedTrack, error) addSubscriberMutex sync.RWMutex @@ -351,10 +351,10 @@ type FakeLocalMediaTrack struct { invocationsMutex sync.RWMutex } -func (fake *FakeLocalMediaTrack) AddOnClose(arg1 func()) { +func (fake *FakeLocalMediaTrack) AddOnClose(arg1 func(isExpectedToResume bool)) { fake.addOnCloseMutex.Lock() fake.addOnCloseArgsForCall = append(fake.addOnCloseArgsForCall, struct { - arg1 func() + arg1 func(isExpectedToResume bool) }{arg1}) stub := fake.AddOnCloseStub fake.recordInvocation("AddOnClose", []interface{}{arg1}) @@ -370,13 +370,13 @@ func (fake *FakeLocalMediaTrack) AddOnCloseCallCount() int { return len(fake.addOnCloseArgsForCall) } -func (fake *FakeLocalMediaTrack) AddOnCloseCalls(stub func(func())) { +func (fake *FakeLocalMediaTrack) AddOnCloseCalls(stub func(func(isExpectedToResume bool))) { fake.addOnCloseMutex.Lock() defer fake.addOnCloseMutex.Unlock() fake.AddOnCloseStub = stub } -func (fake *FakeLocalMediaTrack) AddOnCloseArgsForCall(i int) func() { +func (fake *FakeLocalMediaTrack) AddOnCloseArgsForCall(i int) func(isExpectedToResume bool) { fake.addOnCloseMutex.RLock() defer fake.addOnCloseMutex.RUnlock() argsForCall := fake.addOnCloseArgsForCall[i] diff --git a/pkg/rtc/types/typesfakes/fake_media_track.go b/pkg/rtc/types/typesfakes/fake_media_track.go index 174d49b82..887646a18 100644 --- a/pkg/rtc/types/typesfakes/fake_media_track.go +++ b/pkg/rtc/types/typesfakes/fake_media_track.go @@ -10,10 +10,10 @@ import ( ) type FakeMediaTrack struct { - AddOnCloseStub func(func()) + AddOnCloseStub func(func(isExpectedToResume bool)) addOnCloseMutex sync.RWMutex addOnCloseArgsForCall []struct { - arg1 func() + arg1 func(isExpectedToResume bool) } AddSubscriberStub func(types.LocalParticipant) (types.SubscribedTrack, error) addSubscriberMutex sync.RWMutex @@ -287,10 +287,10 @@ type FakeMediaTrack struct { invocationsMutex sync.RWMutex } -func (fake *FakeMediaTrack) AddOnClose(arg1 func()) { +func (fake *FakeMediaTrack) AddOnClose(arg1 func(isExpectedToResume bool)) { fake.addOnCloseMutex.Lock() fake.addOnCloseArgsForCall = append(fake.addOnCloseArgsForCall, struct { - arg1 func() + arg1 func(isExpectedToResume bool) }{arg1}) stub := fake.AddOnCloseStub fake.recordInvocation("AddOnClose", []interface{}{arg1}) @@ -306,13 +306,13 @@ func (fake *FakeMediaTrack) AddOnCloseCallCount() int { return len(fake.addOnCloseArgsForCall) } -func (fake *FakeMediaTrack) AddOnCloseCalls(stub func(func())) { +func (fake *FakeMediaTrack) AddOnCloseCalls(stub func(func(isExpectedToResume bool))) { fake.addOnCloseMutex.Lock() defer fake.addOnCloseMutex.Unlock() fake.AddOnCloseStub = stub } -func (fake *FakeMediaTrack) AddOnCloseArgsForCall(i int) func() { +func (fake *FakeMediaTrack) AddOnCloseArgsForCall(i int) func(isExpectedToResume bool) { fake.addOnCloseMutex.RLock() defer fake.addOnCloseMutex.RUnlock() argsForCall := fake.addOnCloseArgsForCall[i] diff --git a/pkg/rtc/types/typesfakes/fake_subscribed_track.go b/pkg/rtc/types/typesfakes/fake_subscribed_track.go index 375e2cb44..42c9a011c 100644 --- a/pkg/rtc/types/typesfakes/fake_subscribed_track.go +++ b/pkg/rtc/types/typesfakes/fake_subscribed_track.go @@ -81,10 +81,10 @@ type FakeSubscribedTrack struct { needsNegotiationReturnsOnCall map[int]struct { result1 bool } - OnCloseStub func(func(willBeResumed bool)) + OnCloseStub func(func(isExpectedToResume bool)) onCloseMutex sync.RWMutex onCloseArgsForCall []struct { - arg1 func(willBeResumed bool) + arg1 func(isExpectedToResume bool) } PublisherIDStub func() livekit.ParticipantID publisherIDMutex sync.RWMutex @@ -557,10 +557,10 @@ func (fake *FakeSubscribedTrack) NeedsNegotiationReturnsOnCall(i int, result1 bo }{result1} } -func (fake *FakeSubscribedTrack) OnClose(arg1 func(willBeResumed bool)) { +func (fake *FakeSubscribedTrack) OnClose(arg1 func(isExpectedToResume bool)) { fake.onCloseMutex.Lock() fake.onCloseArgsForCall = append(fake.onCloseArgsForCall, struct { - arg1 func(willBeResumed bool) + arg1 func(isExpectedToResume bool) }{arg1}) stub := fake.OnCloseStub fake.recordInvocation("OnClose", []interface{}{arg1}) @@ -576,13 +576,13 @@ func (fake *FakeSubscribedTrack) OnCloseCallCount() int { return len(fake.onCloseArgsForCall) } -func (fake *FakeSubscribedTrack) OnCloseCalls(stub func(func(willBeResumed bool))) { +func (fake *FakeSubscribedTrack) OnCloseCalls(stub func(func(isExpectedToResume bool))) { fake.onCloseMutex.Lock() defer fake.onCloseMutex.Unlock() fake.OnCloseStub = stub } -func (fake *FakeSubscribedTrack) OnCloseArgsForCall(i int) func(willBeResumed bool) { +func (fake *FakeSubscribedTrack) OnCloseArgsForCall(i int) func(isExpectedToResume bool) { fake.onCloseMutex.RLock() defer fake.onCloseMutex.RUnlock() argsForCall := fake.onCloseArgsForCall[i] diff --git a/pkg/rtc/uptrackmanager.go b/pkg/rtc/uptrackmanager.go index b0fabdefa..22ea6b1ec 100644 --- a/pkg/rtc/uptrackmanager.go +++ b/pkg/rtc/uptrackmanager.go @@ -66,7 +66,7 @@ func NewUpTrackManager(params UpTrackManagerParams) *UpTrackManager { } } -func (u *UpTrackManager) Close(willBeResumed bool) { +func (u *UpTrackManager) Close(isExpectedToResume bool) { u.lock.Lock() if u.closed { u.lock.Unlock() @@ -80,7 +80,7 @@ func (u *UpTrackManager) Close(willBeResumed bool) { u.lock.Unlock() for _, t := range publishedTracks { - t.Close(willBeResumed) + t.Close(isExpectedToResume) } if onClose := u.getOnUpTrackManagerClose(); onClose != nil { @@ -274,7 +274,7 @@ func (u *UpTrackManager) AddPublishedTrack(track types.MediaTrack) { u.lock.Unlock() u.params.Logger.Debugw("added published track", "trackID", track.ID(), "trackInfo", logger.Proto(track.ToProto())) - track.AddOnClose(func() { + track.AddOnClose(func(_isExpectedToResume bool) { u.lock.Lock() delete(u.publishedTracks, track.ID()) // not modifying subscription permissions, will get reset on next update from participant @@ -282,11 +282,11 @@ func (u *UpTrackManager) AddPublishedTrack(track types.MediaTrack) { }) } -func (u *UpTrackManager) RemovePublishedTrack(track types.MediaTrack, willBeResumed bool, shouldClose bool) { +func (u *UpTrackManager) RemovePublishedTrack(track types.MediaTrack, isExpectedToResume bool, shouldClose bool) { if shouldClose { - track.Close(willBeResumed) + track.Close(isExpectedToResume) } else { - track.ClearAllReceivers(willBeResumed) + track.ClearAllReceivers(isExpectedToResume) } u.lock.Lock() delete(u.publishedTracks, track.ID()) diff --git a/pkg/sfu/downtrack.go b/pkg/sfu/downtrack.go index f9d1f1358..170abadad 100644 --- a/pkg/sfu/downtrack.go +++ b/pkg/sfu/downtrack.go @@ -296,7 +296,7 @@ type DownTrack struct { onStatsUpdate func(dt *DownTrack, stat *livekit.AnalyticsStat) onMaxSubscribedLayerChanged func(dt *DownTrack, layer int32) onRttUpdate func(dt *DownTrack, rtt uint32) - onCloseHandler func(willBeResumed bool) + onCloseHandler func(isExpectedToResume bool) createdAt int64 } @@ -1169,14 +1169,14 @@ func (d *DownTrack) UpTrackBitrateReport(availableLayers []int32, bitrates Bitra } // OnCloseHandler method to be called on remote tracked removed -func (d *DownTrack) OnCloseHandler(fn func(willBeResumed bool)) { +func (d *DownTrack) OnCloseHandler(fn func(isExpectedToResume bool)) { d.cbMu.Lock() defer d.cbMu.Unlock() d.onCloseHandler = fn } -func (d *DownTrack) getOnCloseHandler() func(willBeResumed bool) { +func (d *DownTrack) getOnCloseHandler() func(isExpectedToResume bool) { d.cbMu.RLock() defer d.cbMu.RUnlock() From 74c7b9317086782d5678b077e686dbd0cef6f72f Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Mon, 17 Jun 2024 21:49:51 +0300 Subject: [PATCH 27/30] Support new SIP Trunk API. Improve Redis tests. (#2799) --- go.mod | 25 +- go.sum | 76 +++- pkg/service/docker_test.go | 79 ++++ pkg/service/interfaces.go | 6 + pkg/service/ioservice_sip.go | 10 +- pkg/service/redisstore.go | 121 ++---- pkg/service/redisstore_sip.go | 199 +++++++++ pkg/service/redisstore_sip_test.go | 263 ++++++++++++ pkg/service/redisstore_test.go | 15 +- pkg/service/servicefakes/fake_sipstore.go | 472 ++++++++++++++++++++++ pkg/service/sip.go | 91 ++++- pkg/service/utils_test.go | 34 +- 12 files changed, 1288 insertions(+), 103 deletions(-) create mode 100644 pkg/service/docker_test.go create mode 100644 pkg/service/redisstore_sip.go create mode 100644 pkg/service/redisstore_sip_test.go diff --git a/go.mod b/go.mod index b85a91cda..d80c3a640 100644 --- a/go.mod +++ b/go.mod @@ -20,13 +20,14 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 - github.com/livekit/protocol v1.17.1-0.20240614060801-425cb974f7a4 + github.com/livekit/protocol v1.17.1-0.20240617184219-32c577d805ed github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 github.com/mackerelio/go-osstat v0.2.5 github.com/magefile/mage v1.15.0 github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 github.com/mitchellh/go-homedir v1.1.0 github.com/olekukonko/tablewriter v0.0.5 + github.com/ory/dockertest/v3 v3.10.0 github.com/pion/dtls/v2 v2.2.11 github.com/pion/ice/v2 v2.3.24 github.com/pion/interceptor v0.1.29 @@ -57,18 +58,30 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/continuity v0.4.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/docker/cli v26.1.4+incompatible // indirect + github.com/docker/docker v27.0.0+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/eapache/channels v1.1.0 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/subcommands v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -81,9 +94,15 @@ require ( github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mdlayher/netlink v1.7.1 // indirect github.com/mdlayher/socket v0.4.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect github.com/nats-io/nats.go v1.35.0 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runc v1.1.13 // indirect github.com/pion/datachannel v1.5.5 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.12 // indirect @@ -96,6 +115,10 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.uber.org/zap/exp v0.2.0 // indirect diff --git a/go.sum b/go.sum index f42fa94c8..061c3305a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -10,15 +18,21 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao= github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= +github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= +github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM= github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,6 +40,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= +github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.0.0+incompatible h1:JRugTYuelmWlW0M3jakcIadDx2HUoUO6+Tf2C5jVfwA= +github.com/docker/docker v27.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k= @@ -50,6 +72,10 @@ github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -61,6 +87,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -101,6 +129,8 @@ github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786 h1:N527AHMa79 github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= @@ -114,14 +144,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58= github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 h1:p60OjeixzXnhGFQL8wmdUwWPxijEDe9ZJFMosq+byec= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= -github.com/livekit/protocol v1.17.1-0.20240614060801-425cb974f7a4 h1:5O3/wahQIMnw+PO3O1kgxPSrpJf7PkPcD3GvWmb1TJQ= -github.com/livekit/protocol v1.17.1-0.20240614060801-425cb974f7a4/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= +github.com/livekit/protocol v1.17.1-0.20240617184219-32c577d805ed h1:S4avs1NKG6bBgHYuBOrQWnNxJSOdunGOB84BQfGzKmQ= +github.com/livekit/protocol v1.17.1-0.20240617184219-32c577d805ed/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= @@ -153,6 +185,12 @@ github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/nats-io/nats.go v1.35.0 h1:XFNqNM7v5B+MQMKqVGAyHwYhyKb48jrenXNxIU20ULk= github.com/nats-io/nats.go v1.35.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -163,6 +201,14 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= +github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= @@ -234,11 +280,14 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -258,8 +307,17 @@ github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= @@ -276,6 +334,7 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= @@ -288,6 +347,8 @@ golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -300,7 +361,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -321,6 +384,8 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -348,11 +413,13 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -393,6 +460,8 @@ golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -400,6 +469,7 @@ golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8= @@ -421,3 +491,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/pkg/service/docker_test.go b/pkg/service/docker_test.go new file mode 100644 index 000000000..48937f3d4 --- /dev/null +++ b/pkg/service/docker_test.go @@ -0,0 +1,79 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service_test + +import ( + "fmt" + "log" + "net" + "os" + "sync/atomic" + "testing" + + "github.com/ory/dockertest/v3" +) + +var Docker *dockertest.Pool + +func TestMain(m *testing.M) { + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not construct pool: %s", err) + } + + // uses pool to try to connect to Docker + err = pool.Client.Ping() + if err != nil { + log.Fatalf("Could not connect to Docker: %s", err) + } + Docker = pool + + code := m.Run() + os.Exit(code) +} + +func waitTCPPort(t testing.TB, addr string) { + if err := Docker.Retry(func() error { + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Log(err) + return err + } + _ = conn.Close() + return nil + }); err != nil { + t.Fatal(err) + } +} + +var redisLast uint32 + +func runRedis(t testing.TB) string { + c, err := Docker.RunWithOptions(&dockertest.RunOptions{ + Name: fmt.Sprintf("lktest-redis-%d", atomic.AddUint32(&redisLast, 1)), + Repository: "redis", Tag: "latest", + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = Docker.Purge(c) + }) + addr := c.GetHostPort("6379/tcp") + waitTCPPort(t, addr) + + t.Log("Redis running on", addr) + return addr +} diff --git a/pkg/service/interfaces.go b/pkg/service/interfaces.go index 73c7d4a67..9ebce8271 100644 --- a/pkg/service/interfaces.go +++ b/pkg/service/interfaces.go @@ -80,8 +80,14 @@ type RoomAllocator interface { //counterfeiter:generate . SIPStore type SIPStore interface { StoreSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error + StoreSIPInboundTrunk(ctx context.Context, info *livekit.SIPInboundTrunkInfo) error + StoreSIPOutboundTrunk(ctx context.Context, info *livekit.SIPOutboundTrunkInfo) error LoadSIPTrunk(ctx context.Context, sipTrunkID string) (*livekit.SIPTrunkInfo, error) + LoadSIPInboundTrunk(ctx context.Context, sipTrunkID string) (*livekit.SIPInboundTrunkInfo, error) + LoadSIPOutboundTrunk(ctx context.Context, sipTrunkID string) (*livekit.SIPOutboundTrunkInfo, error) ListSIPTrunk(ctx context.Context) ([]*livekit.SIPTrunkInfo, error) + ListSIPInboundTrunk(ctx context.Context) ([]*livekit.SIPInboundTrunkInfo, error) + ListSIPOutboundTrunk(ctx context.Context) ([]*livekit.SIPOutboundTrunkInfo, error) DeleteSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error StoreSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error diff --git a/pkg/service/ioservice_sip.go b/pkg/service/ioservice_sip.go index f3c686bfb..e974833ac 100644 --- a/pkg/service/ioservice_sip.go +++ b/pkg/service/ioservice_sip.go @@ -26,8 +26,8 @@ import ( // matchSIPTrunk finds a SIP Trunk definition matching the request. // Returns nil if no rules matched or an error if there are conflicting definitions. -func (s *IOInfoService) matchSIPTrunk(ctx context.Context, calling, called string) (*livekit.SIPTrunkInfo, error) { - trunks, err := s.ss.ListSIPTrunk(ctx) +func (s *IOInfoService) matchSIPTrunk(ctx context.Context, calling, called string) (*livekit.SIPInboundTrunkInfo, error) { + trunks, err := s.ss.ListSIPInboundTrunk(ctx) if err != nil { return nil, err } @@ -36,7 +36,7 @@ func (s *IOInfoService) matchSIPTrunk(ctx context.Context, calling, called strin // matchSIPDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched. // Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs). -func (s *IOInfoService) matchSIPDispatchRule(ctx context.Context, trunk *livekit.SIPTrunkInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) { +func (s *IOInfoService) matchSIPDispatchRule(ctx context.Context, trunk *livekit.SIPInboundTrunkInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) { // Trunk can still be nil here in case none matched or were defined. // This is still fine, but only in case we'll match exactly one wildcard dispatch rule. rules, err := s.ss.ListSIPDispatchRule(ctx) @@ -96,7 +96,7 @@ func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc. log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId) return &rpc.GetSIPTrunkAuthenticationResponse{ SipTrunkId: trunk.SipTrunkId, - Username: trunk.InboundUsername, - Password: trunk.InboundPassword, + Username: trunk.AuthUsername, + Password: trunk.AuthPassword, }, nil } diff --git a/pkg/service/redisstore.go b/pkg/service/redisstore.go index 52dfcaa62..4da0ab360 100644 --- a/pkg/service/redisstore.go +++ b/pkg/service/redisstore.go @@ -52,9 +52,6 @@ const ( IngressStatePrefix = "{ingress}_state:" RoomIngressPrefix = "room_{ingress}:" - SIPTrunkKey = "sip_trunk" - SIPDispatchRuleKey = "sip_dispatch_rule" - // RoomParticipantsPrefix is hash of participant_name => ParticipantInfo RoomParticipantsPrefix = "room_participants:" @@ -825,94 +822,54 @@ func (s *RedisStore) DeleteIngress(_ context.Context, info *livekit.IngressInfo) return nil } -func (s *RedisStore) loadOne(ctx context.Context, key, id string, info proto.Message, notFoundErr error) error { +func redisStoreOne(ctx context.Context, s *RedisStore, key, id string, p proto.Message) error { + if id == "" { + return errors.New("id is not set") + } + data, err := proto.Marshal(p) + if err != nil { + return err + } + return s.rc.HSet(s.ctx, key, id, data).Err() +} + +func redisLoadOne[T any, P interface { + *T + proto.Message +}](ctx context.Context, s *RedisStore, key, id string, notFoundErr error) (P, error) { data, err := s.rc.HGet(s.ctx, key, id).Result() - switch err { - case nil: - return proto.Unmarshal([]byte(data), info) - case redis.Nil: - return notFoundErr - default: - return err + if err == redis.Nil { + return nil, notFoundErr + } else if err != nil { + return nil, err } + var p P = new(T) + err = proto.Unmarshal([]byte(data), p) + if err != nil { + return nil, err + } + return p, err } -func (s *RedisStore) loadMany(ctx context.Context, key string, onResult func() proto.Message) error { +func redisLoadMany[T any, P interface { + *T + proto.Message +}](ctx context.Context, s *RedisStore, key string) ([]P, error) { data, err := s.rc.HGetAll(s.ctx, key).Result() - if err != nil { - if err == redis.Nil { - return nil - } - return err + if err == redis.Nil { + return nil, nil + } else if err != nil { + return nil, err } + list := make([]P, 0, len(data)) for _, d := range data { - if err = proto.Unmarshal([]byte(d), onResult()); err != nil { - return err + var p P = new(T) + if err = proto.Unmarshal([]byte(d), p); err != nil { + return list, err } + list = append(list, p) } - return nil -} - -func (s *RedisStore) StoreSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error { - data, err := proto.Marshal(info) - if err != nil { - return err - } - - return s.rc.HSet(s.ctx, SIPTrunkKey, info.SipTrunkId, data).Err() -} - -func (s *RedisStore) LoadSIPTrunk(ctx context.Context, sipTrunkId string) (*livekit.SIPTrunkInfo, error) { - info := &livekit.SIPTrunkInfo{} - if err := s.loadOne(ctx, SIPTrunkKey, sipTrunkId, info, ErrSIPTrunkNotFound); err != nil { - return nil, err - } - - return info, nil -} - -func (s *RedisStore) DeleteSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error { - return s.rc.HDel(s.ctx, SIPTrunkKey, info.SipTrunkId).Err() -} - -func (s *RedisStore) ListSIPTrunk(ctx context.Context) (infos []*livekit.SIPTrunkInfo, err error) { - err = s.loadMany(ctx, SIPTrunkKey, func() proto.Message { - infos = append(infos, &livekit.SIPTrunkInfo{}) - return infos[len(infos)-1] - }) - - return infos, err -} - -func (s *RedisStore) StoreSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error { - data, err := proto.Marshal(info) - if err != nil { - return err - } - - return s.rc.HSet(s.ctx, SIPDispatchRuleKey, info.SipDispatchRuleId, data).Err() -} - -func (s *RedisStore) LoadSIPDispatchRule(ctx context.Context, sipDispatchRuleId string) (*livekit.SIPDispatchRuleInfo, error) { - info := &livekit.SIPDispatchRuleInfo{} - if err := s.loadOne(ctx, SIPDispatchRuleKey, sipDispatchRuleId, info, ErrSIPDispatchRuleNotFound); err != nil { - return nil, err - } - - return info, nil -} - -func (s *RedisStore) DeleteSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error { - return s.rc.HDel(s.ctx, SIPDispatchRuleKey, info.SipDispatchRuleId).Err() -} - -func (s *RedisStore) ListSIPDispatchRule(ctx context.Context) (infos []*livekit.SIPDispatchRuleInfo, err error) { - err = s.loadMany(ctx, SIPDispatchRuleKey, func() proto.Message { - infos = append(infos, &livekit.SIPDispatchRuleInfo{}) - return infos[len(infos)-1] - }) - - return infos, err + return list, nil } diff --git a/pkg/service/redisstore_sip.go b/pkg/service/redisstore_sip.go new file mode 100644 index 000000000..d4cfbb09e --- /dev/null +++ b/pkg/service/redisstore_sip.go @@ -0,0 +1,199 @@ +// Copyright 2023 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "context" + + "github.com/livekit/protocol/livekit" +) + +const ( + SIPTrunkKey = "sip_trunk" + SIPInboundTrunkKey = "sip_inbound_trunk" + SIPOutboundTrunkKey = "sip_outbound_trunk" + SIPDispatchRuleKey = "sip_dispatch_rule" +) + +func (s *RedisStore) StoreSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error { + return redisStoreOne(s.ctx, s, SIPTrunkKey, info.SipTrunkId, info) +} + +func (s *RedisStore) StoreSIPInboundTrunk(ctx context.Context, info *livekit.SIPInboundTrunkInfo) error { + return redisStoreOne(s.ctx, s, SIPInboundTrunkKey, info.SipTrunkId, info) +} + +func (s *RedisStore) StoreSIPOutboundTrunk(ctx context.Context, info *livekit.SIPOutboundTrunkInfo) error { + return redisStoreOne(s.ctx, s, SIPOutboundTrunkKey, info.SipTrunkId, info) +} + +func (s *RedisStore) loadSIPLegacyTrunk(ctx context.Context, id string) (*livekit.SIPTrunkInfo, error) { + return redisLoadOne[livekit.SIPTrunkInfo](ctx, s, SIPTrunkKey, id, ErrSIPTrunkNotFound) +} + +func (s *RedisStore) loadSIPInboundTrunk(ctx context.Context, id string) (*livekit.SIPInboundTrunkInfo, error) { + return redisLoadOne[livekit.SIPInboundTrunkInfo](ctx, s, SIPInboundTrunkKey, id, ErrSIPTrunkNotFound) +} + +func (s *RedisStore) loadSIPOutboundTrunk(ctx context.Context, id string) (*livekit.SIPOutboundTrunkInfo, error) { + return redisLoadOne[livekit.SIPOutboundTrunkInfo](ctx, s, SIPOutboundTrunkKey, id, ErrSIPTrunkNotFound) +} + +func (s *RedisStore) LoadSIPTrunk(ctx context.Context, id string) (*livekit.SIPTrunkInfo, error) { + tr, err := s.loadSIPLegacyTrunk(ctx, id) + if err == nil { + return tr, nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + in, err := s.loadSIPInboundTrunk(ctx, id) + if err == nil { + return in.AsTrunkInfo(), nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + out, err := s.loadSIPOutboundTrunk(ctx, id) + if err == nil { + return out.AsTrunkInfo(), nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + return nil, ErrSIPTrunkNotFound +} + +func (s *RedisStore) LoadSIPInboundTrunk(ctx context.Context, id string) (*livekit.SIPInboundTrunkInfo, error) { + in, err := s.loadSIPInboundTrunk(ctx, id) + if err == nil { + return in, nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + tr, err := s.loadSIPLegacyTrunk(ctx, id) + if err == nil { + return tr.AsInbound(), nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + return nil, ErrSIPTrunkNotFound +} + +func (s *RedisStore) LoadSIPOutboundTrunk(ctx context.Context, id string) (*livekit.SIPOutboundTrunkInfo, error) { + in, err := s.loadSIPOutboundTrunk(ctx, id) + if err == nil { + return in, nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + tr, err := s.loadSIPLegacyTrunk(ctx, id) + if err == nil { + return tr.AsOutbound(), nil + } else if err != ErrSIPTrunkNotFound { + return nil, err + } + return nil, ErrSIPTrunkNotFound +} + +func (s *RedisStore) deleteSIPTrunk(ctx context.Context, id string) error { + tx := s.rc.TxPipeline() + tx.HDel(s.ctx, SIPTrunkKey, id) + tx.HDel(s.ctx, SIPInboundTrunkKey, id) + tx.HDel(s.ctx, SIPOutboundTrunkKey, id) + _, err := tx.Exec(ctx) + return err +} + +func (s *RedisStore) DeleteSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error { + return s.deleteSIPTrunk(ctx, info.SipTrunkId) +} + +func (s *RedisStore) listSIPLegacyTrunk(ctx context.Context) ([]*livekit.SIPTrunkInfo, error) { + return redisLoadMany[livekit.SIPTrunkInfo](ctx, s, SIPTrunkKey) +} + +func (s *RedisStore) listSIPInboundTrunk(ctx context.Context) ([]*livekit.SIPInboundTrunkInfo, error) { + return redisLoadMany[livekit.SIPInboundTrunkInfo](ctx, s, SIPInboundTrunkKey) +} + +func (s *RedisStore) listSIPOutboundTrunk(ctx context.Context) ([]*livekit.SIPOutboundTrunkInfo, error) { + return redisLoadMany[livekit.SIPOutboundTrunkInfo](ctx, s, SIPOutboundTrunkKey) +} + +func (s *RedisStore) ListSIPTrunk(ctx context.Context) ([]*livekit.SIPTrunkInfo, error) { + infos, err := s.listSIPLegacyTrunk(ctx) + if err != nil { + return nil, err + } + in, err := s.listSIPInboundTrunk(ctx) + if err != nil { + return infos, err + } + for _, t := range in { + infos = append(infos, t.AsTrunkInfo()) + } + out, err := s.listSIPOutboundTrunk(ctx) + if err != nil { + return infos, err + } + for _, t := range out { + infos = append(infos, t.AsTrunkInfo()) + } + return infos, nil +} + +func (s *RedisStore) ListSIPInboundTrunk(ctx context.Context) (infos []*livekit.SIPInboundTrunkInfo, err error) { + in, err := s.listSIPInboundTrunk(ctx) + if err != nil { + return in, err + } + old, err := s.listSIPLegacyTrunk(ctx) + if err != nil { + return nil, err + } + for _, t := range old { + in = append(in, t.AsInbound()) + } + return in, nil +} + +func (s *RedisStore) ListSIPOutboundTrunk(ctx context.Context) (infos []*livekit.SIPOutboundTrunkInfo, err error) { + out, err := s.listSIPOutboundTrunk(ctx) + if err != nil { + return out, err + } + old, err := s.listSIPLegacyTrunk(ctx) + if err != nil { + return nil, err + } + for _, t := range old { + out = append(out, t.AsOutbound()) + } + return out, nil +} + +func (s *RedisStore) StoreSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error { + return redisStoreOne(ctx, s, SIPDispatchRuleKey, info.SipDispatchRuleId, info) +} + +func (s *RedisStore) LoadSIPDispatchRule(ctx context.Context, sipDispatchRuleId string) (*livekit.SIPDispatchRuleInfo, error) { + return redisLoadOne[livekit.SIPDispatchRuleInfo](ctx, s, SIPDispatchRuleKey, sipDispatchRuleId, ErrSIPDispatchRuleNotFound) +} + +func (s *RedisStore) DeleteSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error { + return s.rc.HDel(s.ctx, SIPDispatchRuleKey, info.SipDispatchRuleId).Err() +} + +func (s *RedisStore) ListSIPDispatchRule(ctx context.Context) (infos []*livekit.SIPDispatchRuleInfo, err error) { + return redisLoadMany[livekit.SIPDispatchRuleInfo](ctx, s, SIPDispatchRuleKey) +} diff --git a/pkg/service/redisstore_sip_test.go b/pkg/service/redisstore_sip_test.go new file mode 100644 index 000000000..7e8e569f7 --- /dev/null +++ b/pkg/service/redisstore_sip_test.go @@ -0,0 +1,263 @@ +// Copyright 2024 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service_test + +import ( + "context" + "slices" + "strings" + "testing" + + "github.com/livekit/protocol/livekit" + "github.com/livekit/protocol/utils" + "github.com/livekit/protocol/utils/guid" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + "github.com/livekit/livekit-server/pkg/service" +) + +func TestSIPStoreDispatch(t *testing.T) { + ctx := context.Background() + rs := redisStore(t) + + id := guid.New(utils.SIPDispatchRulePrefix) + + // No dispatch rules initially. + list, err := rs.ListSIPDispatchRule(ctx) + require.NoError(t, err) + require.Empty(t, list) + + // Loading non-existent dispatch should return proper not found error. + got, err := rs.LoadSIPDispatchRule(ctx, id) + require.Equal(t, service.ErrSIPDispatchRuleNotFound, err) + require.Nil(t, got) + + // Creation without ID should fail. + rule := &livekit.SIPDispatchRuleInfo{ + TrunkIds: []string{"trunk"}, + Rule: &livekit.SIPDispatchRule{Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{ + DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{ + RoomName: "room", + Pin: "1234", + }, + }}, + } + err = rs.StoreSIPDispatchRule(ctx, rule) + require.Error(t, err) + + // Creation + rule.SipDispatchRuleId = id + err = rs.StoreSIPDispatchRule(ctx, rule) + require.NoError(t, err) + + // Loading + got, err = rs.LoadSIPDispatchRule(ctx, id) + require.NoError(t, err) + require.True(t, proto.Equal(rule, got)) + + // Listing + list, err = rs.ListSIPDispatchRule(ctx) + require.NoError(t, err) + require.Len(t, list, 1) + require.True(t, proto.Equal(rule, list[0])) + + // Deletion. Should not return error if not exists. + err = rs.DeleteSIPDispatchRule(ctx, &livekit.SIPDispatchRuleInfo{SipDispatchRuleId: id}) + require.NoError(t, err) + err = rs.DeleteSIPDispatchRule(ctx, &livekit.SIPDispatchRuleInfo{SipDispatchRuleId: id}) + require.NoError(t, err) + + // Check that it's deleted. + list, err = rs.ListSIPDispatchRule(ctx) + require.NoError(t, err) + require.Empty(t, list) + + got, err = rs.LoadSIPDispatchRule(ctx, id) + require.Equal(t, service.ErrSIPDispatchRuleNotFound, err) + require.Nil(t, got) +} + +func TestSIPStoreTrunk(t *testing.T) { + ctx := context.Background() + rs := redisStore(t) + + oldID := guid.New(utils.SIPTrunkPrefix) + inID := guid.New(utils.SIPTrunkPrefix) + outID := guid.New(utils.SIPTrunkPrefix) + + // No trunks initially. Check legacy, inbound, outbound. + // Loading non-existent trunk should return proper not found error. + oldList, err := rs.ListSIPTrunk(ctx) + require.NoError(t, err) + require.Empty(t, oldList) + + old, err := rs.LoadSIPTrunk(ctx, oldID) + require.Equal(t, service.ErrSIPTrunkNotFound, err) + require.Nil(t, old) + + inList, err := rs.ListSIPInboundTrunk(ctx) + require.NoError(t, err) + require.Empty(t, inList) + + in, err := rs.LoadSIPInboundTrunk(ctx, oldID) + require.Equal(t, service.ErrSIPTrunkNotFound, err) + require.Nil(t, in) + + outList, err := rs.ListSIPOutboundTrunk(ctx) + require.NoError(t, err) + require.Empty(t, outList) + + out, err := rs.LoadSIPOutboundTrunk(ctx, oldID) + require.Equal(t, service.ErrSIPTrunkNotFound, err) + require.Nil(t, out) + + // Creation without ID should fail. + oldT := &livekit.SIPTrunkInfo{ + Name: "Legacy", + } + err = rs.StoreSIPTrunk(ctx, oldT) + require.Error(t, err) + + inT := &livekit.SIPInboundTrunkInfo{ + Name: "Inbound", + } + err = rs.StoreSIPInboundTrunk(ctx, inT) + require.Error(t, err) + + outT := &livekit.SIPOutboundTrunkInfo{ + Name: "Outbound", + } + err = rs.StoreSIPOutboundTrunk(ctx, outT) + require.Error(t, err) + + // Creation + oldT.SipTrunkId = oldID + err = rs.StoreSIPTrunk(ctx, oldT) + require.NoError(t, err) + + inT.SipTrunkId = inID + err = rs.StoreSIPInboundTrunk(ctx, inT) + require.NoError(t, err) + + outT.SipTrunkId = outID + err = rs.StoreSIPOutboundTrunk(ctx, outT) + require.NoError(t, err) + + // Loading (with matching kind) + oldT2, err := rs.LoadSIPTrunk(ctx, oldID) + require.NoError(t, err) + require.True(t, proto.Equal(oldT, oldT2)) + + inT2, err := rs.LoadSIPInboundTrunk(ctx, inID) + require.NoError(t, err) + require.True(t, proto.Equal(inT, inT2)) + + outT2, err := rs.LoadSIPOutboundTrunk(ctx, outID) + require.NoError(t, err) + require.True(t, proto.Equal(outT, outT2)) + + // Loading (compat) + oldT2, err = rs.LoadSIPTrunk(ctx, inID) + require.NoError(t, err) + require.True(t, proto.Equal(inT.AsTrunkInfo(), oldT2)) + + oldT2, err = rs.LoadSIPTrunk(ctx, outID) + require.NoError(t, err) + require.True(t, proto.Equal(outT.AsTrunkInfo(), oldT2)) + + inT2, err = rs.LoadSIPInboundTrunk(ctx, oldID) + require.NoError(t, err) + require.True(t, proto.Equal(oldT.AsInbound(), inT2)) + + outT2, err = rs.LoadSIPOutboundTrunk(ctx, oldID) + require.NoError(t, err) + require.True(t, proto.Equal(oldT.AsOutbound(), outT2)) + + // Listing (always shows legacy + new) + listOld, err := rs.ListSIPTrunk(ctx) + require.NoError(t, err) + require.Len(t, listOld, 3) + slices.SortFunc(listOld, func(a, b *livekit.SIPTrunkInfo) int { + return strings.Compare(a.Name, b.Name) + }) + require.True(t, proto.Equal(inT.AsTrunkInfo(), listOld[0])) + require.True(t, proto.Equal(oldT, listOld[1])) + require.True(t, proto.Equal(outT.AsTrunkInfo(), listOld[2])) + + listIn, err := rs.ListSIPInboundTrunk(ctx) + require.NoError(t, err) + require.Len(t, listIn, 2) + slices.SortFunc(listIn, func(a, b *livekit.SIPInboundTrunkInfo) int { + return strings.Compare(a.Name, b.Name) + }) + require.True(t, proto.Equal(inT, listIn[0])) + require.True(t, proto.Equal(oldT.AsInbound(), listIn[1])) + + listOut, err := rs.ListSIPOutboundTrunk(ctx) + require.NoError(t, err) + require.Len(t, listOut, 2) + slices.SortFunc(listOut, func(a, b *livekit.SIPOutboundTrunkInfo) int { + return strings.Compare(a.Name, b.Name) + }) + require.True(t, proto.Equal(oldT.AsOutbound(), listOut[0])) + require.True(t, proto.Equal(outT, listOut[1])) + + // Deletion. Should not return error if not exists. + err = rs.DeleteSIPTrunk(ctx, &livekit.SIPTrunkInfo{SipTrunkId: oldID}) + require.NoError(t, err) + err = rs.DeleteSIPTrunk(ctx, &livekit.SIPTrunkInfo{SipTrunkId: oldID}) + require.NoError(t, err) + + // Other objects are still there. + inT2, err = rs.LoadSIPInboundTrunk(ctx, inID) + require.NoError(t, err) + require.True(t, proto.Equal(inT, inT2)) + + outT2, err = rs.LoadSIPOutboundTrunk(ctx, outID) + require.NoError(t, err) + require.True(t, proto.Equal(outT, outT2)) + + // Delete the rest + err = rs.DeleteSIPTrunk(ctx, &livekit.SIPTrunkInfo{SipTrunkId: inID}) + require.NoError(t, err) + err = rs.DeleteSIPTrunk(ctx, &livekit.SIPTrunkInfo{SipTrunkId: outID}) + require.NoError(t, err) + + // Check everything is deleted. + oldList, err = rs.ListSIPTrunk(ctx) + require.NoError(t, err) + require.Empty(t, oldList) + + inList, err = rs.ListSIPInboundTrunk(ctx) + require.NoError(t, err) + require.Empty(t, inList) + + outList, err = rs.ListSIPOutboundTrunk(ctx) + require.NoError(t, err) + require.Empty(t, outList) + + old, err = rs.LoadSIPTrunk(ctx, oldID) + require.Equal(t, service.ErrSIPTrunkNotFound, err) + require.Nil(t, old) + + in, err = rs.LoadSIPInboundTrunk(ctx, oldID) + require.Equal(t, service.ErrSIPTrunkNotFound, err) + require.Nil(t, in) + + out, err = rs.LoadSIPOutboundTrunk(ctx, oldID) + require.Equal(t, service.ErrSIPTrunkNotFound, err) + require.Nil(t, out) +} diff --git a/pkg/service/redisstore_test.go b/pkg/service/redisstore_test.go index 5f62a71f1..d62db1025 100644 --- a/pkg/service/redisstore_test.go +++ b/pkg/service/redisstore_test.go @@ -31,9 +31,13 @@ import ( "github.com/livekit/livekit-server/pkg/service" ) +func redisStore(t testing.TB) *service.RedisStore { + return service.NewRedisStore(redisClient(t)) +} + func TestRoomInternal(t *testing.T) { ctx := context.Background() - rs := service.NewRedisStore(redisClient()) + rs := redisStore(t) room := &livekit.Room{ Sid: "123", @@ -61,7 +65,7 @@ func TestRoomInternal(t *testing.T) { func TestParticipantPersistence(t *testing.T) { ctx := context.Background() - rs := service.NewRedisStore(redisClient()) + rs := redisStore(t) roomName := livekit.RoomName("room1") _ = rs.DeleteRoom(ctx, roomName) @@ -108,7 +112,7 @@ func TestParticipantPersistence(t *testing.T) { func TestRoomLock(t *testing.T) { ctx := context.Background() - rs := service.NewRedisStore(redisClient()) + rs := redisStore(t) lockInterval := 5 * time.Millisecond roomName := livekit.RoomName("myroom") @@ -158,8 +162,7 @@ func TestRoomLock(t *testing.T) { func TestEgressStore(t *testing.T) { ctx := context.Background() - rc := redisClient() - rs := service.NewRedisStore(rc) + rs := redisStore(t) roomName := "egress-test" @@ -229,7 +232,7 @@ func TestEgressStore(t *testing.T) { func TestIngressStore(t *testing.T) { ctx := context.Background() - rs := service.NewRedisStore(redisClient()) + rs := redisStore(t) info := &livekit.IngressInfo{ IngressId: "ingressId", diff --git a/pkg/service/servicefakes/fake_sipstore.go b/pkg/service/servicefakes/fake_sipstore.go index fe88a5359..d5ccde202 100644 --- a/pkg/service/servicefakes/fake_sipstore.go +++ b/pkg/service/servicefakes/fake_sipstore.go @@ -47,6 +47,32 @@ type FakeSIPStore struct { result1 []*livekit.SIPDispatchRuleInfo result2 error } + ListSIPInboundTrunkStub func(context.Context) ([]*livekit.SIPInboundTrunkInfo, error) + listSIPInboundTrunkMutex sync.RWMutex + listSIPInboundTrunkArgsForCall []struct { + arg1 context.Context + } + listSIPInboundTrunkReturns struct { + result1 []*livekit.SIPInboundTrunkInfo + result2 error + } + listSIPInboundTrunkReturnsOnCall map[int]struct { + result1 []*livekit.SIPInboundTrunkInfo + result2 error + } + ListSIPOutboundTrunkStub func(context.Context) ([]*livekit.SIPOutboundTrunkInfo, error) + listSIPOutboundTrunkMutex sync.RWMutex + listSIPOutboundTrunkArgsForCall []struct { + arg1 context.Context + } + listSIPOutboundTrunkReturns struct { + result1 []*livekit.SIPOutboundTrunkInfo + result2 error + } + listSIPOutboundTrunkReturnsOnCall map[int]struct { + result1 []*livekit.SIPOutboundTrunkInfo + result2 error + } ListSIPTrunkStub func(context.Context) ([]*livekit.SIPTrunkInfo, error) listSIPTrunkMutex sync.RWMutex listSIPTrunkArgsForCall []struct { @@ -74,6 +100,34 @@ type FakeSIPStore struct { result1 *livekit.SIPDispatchRuleInfo result2 error } + LoadSIPInboundTrunkStub func(context.Context, string) (*livekit.SIPInboundTrunkInfo, error) + loadSIPInboundTrunkMutex sync.RWMutex + loadSIPInboundTrunkArgsForCall []struct { + arg1 context.Context + arg2 string + } + loadSIPInboundTrunkReturns struct { + result1 *livekit.SIPInboundTrunkInfo + result2 error + } + loadSIPInboundTrunkReturnsOnCall map[int]struct { + result1 *livekit.SIPInboundTrunkInfo + result2 error + } + LoadSIPOutboundTrunkStub func(context.Context, string) (*livekit.SIPOutboundTrunkInfo, error) + loadSIPOutboundTrunkMutex sync.RWMutex + loadSIPOutboundTrunkArgsForCall []struct { + arg1 context.Context + arg2 string + } + loadSIPOutboundTrunkReturns struct { + result1 *livekit.SIPOutboundTrunkInfo + result2 error + } + loadSIPOutboundTrunkReturnsOnCall map[int]struct { + result1 *livekit.SIPOutboundTrunkInfo + result2 error + } LoadSIPTrunkStub func(context.Context, string) (*livekit.SIPTrunkInfo, error) loadSIPTrunkMutex sync.RWMutex loadSIPTrunkArgsForCall []struct { @@ -100,6 +154,30 @@ type FakeSIPStore struct { storeSIPDispatchRuleReturnsOnCall map[int]struct { result1 error } + StoreSIPInboundTrunkStub func(context.Context, *livekit.SIPInboundTrunkInfo) error + storeSIPInboundTrunkMutex sync.RWMutex + storeSIPInboundTrunkArgsForCall []struct { + arg1 context.Context + arg2 *livekit.SIPInboundTrunkInfo + } + storeSIPInboundTrunkReturns struct { + result1 error + } + storeSIPInboundTrunkReturnsOnCall map[int]struct { + result1 error + } + StoreSIPOutboundTrunkStub func(context.Context, *livekit.SIPOutboundTrunkInfo) error + storeSIPOutboundTrunkMutex sync.RWMutex + storeSIPOutboundTrunkArgsForCall []struct { + arg1 context.Context + arg2 *livekit.SIPOutboundTrunkInfo + } + storeSIPOutboundTrunkReturns struct { + result1 error + } + storeSIPOutboundTrunkReturnsOnCall map[int]struct { + result1 error + } StoreSIPTrunkStub func(context.Context, *livekit.SIPTrunkInfo) error storeSIPTrunkMutex sync.RWMutex storeSIPTrunkArgsForCall []struct { @@ -304,6 +382,134 @@ func (fake *FakeSIPStore) ListSIPDispatchRuleReturnsOnCall(i int, result1 []*liv }{result1, result2} } +func (fake *FakeSIPStore) ListSIPInboundTrunk(arg1 context.Context) ([]*livekit.SIPInboundTrunkInfo, error) { + fake.listSIPInboundTrunkMutex.Lock() + ret, specificReturn := fake.listSIPInboundTrunkReturnsOnCall[len(fake.listSIPInboundTrunkArgsForCall)] + fake.listSIPInboundTrunkArgsForCall = append(fake.listSIPInboundTrunkArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.ListSIPInboundTrunkStub + fakeReturns := fake.listSIPInboundTrunkReturns + fake.recordInvocation("ListSIPInboundTrunk", []interface{}{arg1}) + fake.listSIPInboundTrunkMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSIPStore) ListSIPInboundTrunkCallCount() int { + fake.listSIPInboundTrunkMutex.RLock() + defer fake.listSIPInboundTrunkMutex.RUnlock() + return len(fake.listSIPInboundTrunkArgsForCall) +} + +func (fake *FakeSIPStore) ListSIPInboundTrunkCalls(stub func(context.Context) ([]*livekit.SIPInboundTrunkInfo, error)) { + fake.listSIPInboundTrunkMutex.Lock() + defer fake.listSIPInboundTrunkMutex.Unlock() + fake.ListSIPInboundTrunkStub = stub +} + +func (fake *FakeSIPStore) ListSIPInboundTrunkArgsForCall(i int) context.Context { + fake.listSIPInboundTrunkMutex.RLock() + defer fake.listSIPInboundTrunkMutex.RUnlock() + argsForCall := fake.listSIPInboundTrunkArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSIPStore) ListSIPInboundTrunkReturns(result1 []*livekit.SIPInboundTrunkInfo, result2 error) { + fake.listSIPInboundTrunkMutex.Lock() + defer fake.listSIPInboundTrunkMutex.Unlock() + fake.ListSIPInboundTrunkStub = nil + fake.listSIPInboundTrunkReturns = struct { + result1 []*livekit.SIPInboundTrunkInfo + result2 error + }{result1, result2} +} + +func (fake *FakeSIPStore) ListSIPInboundTrunkReturnsOnCall(i int, result1 []*livekit.SIPInboundTrunkInfo, result2 error) { + fake.listSIPInboundTrunkMutex.Lock() + defer fake.listSIPInboundTrunkMutex.Unlock() + fake.ListSIPInboundTrunkStub = nil + if fake.listSIPInboundTrunkReturnsOnCall == nil { + fake.listSIPInboundTrunkReturnsOnCall = make(map[int]struct { + result1 []*livekit.SIPInboundTrunkInfo + result2 error + }) + } + fake.listSIPInboundTrunkReturnsOnCall[i] = struct { + result1 []*livekit.SIPInboundTrunkInfo + result2 error + }{result1, result2} +} + +func (fake *FakeSIPStore) ListSIPOutboundTrunk(arg1 context.Context) ([]*livekit.SIPOutboundTrunkInfo, error) { + fake.listSIPOutboundTrunkMutex.Lock() + ret, specificReturn := fake.listSIPOutboundTrunkReturnsOnCall[len(fake.listSIPOutboundTrunkArgsForCall)] + fake.listSIPOutboundTrunkArgsForCall = append(fake.listSIPOutboundTrunkArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.ListSIPOutboundTrunkStub + fakeReturns := fake.listSIPOutboundTrunkReturns + fake.recordInvocation("ListSIPOutboundTrunk", []interface{}{arg1}) + fake.listSIPOutboundTrunkMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSIPStore) ListSIPOutboundTrunkCallCount() int { + fake.listSIPOutboundTrunkMutex.RLock() + defer fake.listSIPOutboundTrunkMutex.RUnlock() + return len(fake.listSIPOutboundTrunkArgsForCall) +} + +func (fake *FakeSIPStore) ListSIPOutboundTrunkCalls(stub func(context.Context) ([]*livekit.SIPOutboundTrunkInfo, error)) { + fake.listSIPOutboundTrunkMutex.Lock() + defer fake.listSIPOutboundTrunkMutex.Unlock() + fake.ListSIPOutboundTrunkStub = stub +} + +func (fake *FakeSIPStore) ListSIPOutboundTrunkArgsForCall(i int) context.Context { + fake.listSIPOutboundTrunkMutex.RLock() + defer fake.listSIPOutboundTrunkMutex.RUnlock() + argsForCall := fake.listSIPOutboundTrunkArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSIPStore) ListSIPOutboundTrunkReturns(result1 []*livekit.SIPOutboundTrunkInfo, result2 error) { + fake.listSIPOutboundTrunkMutex.Lock() + defer fake.listSIPOutboundTrunkMutex.Unlock() + fake.ListSIPOutboundTrunkStub = nil + fake.listSIPOutboundTrunkReturns = struct { + result1 []*livekit.SIPOutboundTrunkInfo + result2 error + }{result1, result2} +} + +func (fake *FakeSIPStore) ListSIPOutboundTrunkReturnsOnCall(i int, result1 []*livekit.SIPOutboundTrunkInfo, result2 error) { + fake.listSIPOutboundTrunkMutex.Lock() + defer fake.listSIPOutboundTrunkMutex.Unlock() + fake.ListSIPOutboundTrunkStub = nil + if fake.listSIPOutboundTrunkReturnsOnCall == nil { + fake.listSIPOutboundTrunkReturnsOnCall = make(map[int]struct { + result1 []*livekit.SIPOutboundTrunkInfo + result2 error + }) + } + fake.listSIPOutboundTrunkReturnsOnCall[i] = struct { + result1 []*livekit.SIPOutboundTrunkInfo + result2 error + }{result1, result2} +} + func (fake *FakeSIPStore) ListSIPTrunk(arg1 context.Context) ([]*livekit.SIPTrunkInfo, error) { fake.listSIPTrunkMutex.Lock() ret, specificReturn := fake.listSIPTrunkReturnsOnCall[len(fake.listSIPTrunkArgsForCall)] @@ -433,6 +639,136 @@ func (fake *FakeSIPStore) LoadSIPDispatchRuleReturnsOnCall(i int, result1 *livek }{result1, result2} } +func (fake *FakeSIPStore) LoadSIPInboundTrunk(arg1 context.Context, arg2 string) (*livekit.SIPInboundTrunkInfo, error) { + fake.loadSIPInboundTrunkMutex.Lock() + ret, specificReturn := fake.loadSIPInboundTrunkReturnsOnCall[len(fake.loadSIPInboundTrunkArgsForCall)] + fake.loadSIPInboundTrunkArgsForCall = append(fake.loadSIPInboundTrunkArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.LoadSIPInboundTrunkStub + fakeReturns := fake.loadSIPInboundTrunkReturns + fake.recordInvocation("LoadSIPInboundTrunk", []interface{}{arg1, arg2}) + fake.loadSIPInboundTrunkMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSIPStore) LoadSIPInboundTrunkCallCount() int { + fake.loadSIPInboundTrunkMutex.RLock() + defer fake.loadSIPInboundTrunkMutex.RUnlock() + return len(fake.loadSIPInboundTrunkArgsForCall) +} + +func (fake *FakeSIPStore) LoadSIPInboundTrunkCalls(stub func(context.Context, string) (*livekit.SIPInboundTrunkInfo, error)) { + fake.loadSIPInboundTrunkMutex.Lock() + defer fake.loadSIPInboundTrunkMutex.Unlock() + fake.LoadSIPInboundTrunkStub = stub +} + +func (fake *FakeSIPStore) LoadSIPInboundTrunkArgsForCall(i int) (context.Context, string) { + fake.loadSIPInboundTrunkMutex.RLock() + defer fake.loadSIPInboundTrunkMutex.RUnlock() + argsForCall := fake.loadSIPInboundTrunkArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSIPStore) LoadSIPInboundTrunkReturns(result1 *livekit.SIPInboundTrunkInfo, result2 error) { + fake.loadSIPInboundTrunkMutex.Lock() + defer fake.loadSIPInboundTrunkMutex.Unlock() + fake.LoadSIPInboundTrunkStub = nil + fake.loadSIPInboundTrunkReturns = struct { + result1 *livekit.SIPInboundTrunkInfo + result2 error + }{result1, result2} +} + +func (fake *FakeSIPStore) LoadSIPInboundTrunkReturnsOnCall(i int, result1 *livekit.SIPInboundTrunkInfo, result2 error) { + fake.loadSIPInboundTrunkMutex.Lock() + defer fake.loadSIPInboundTrunkMutex.Unlock() + fake.LoadSIPInboundTrunkStub = nil + if fake.loadSIPInboundTrunkReturnsOnCall == nil { + fake.loadSIPInboundTrunkReturnsOnCall = make(map[int]struct { + result1 *livekit.SIPInboundTrunkInfo + result2 error + }) + } + fake.loadSIPInboundTrunkReturnsOnCall[i] = struct { + result1 *livekit.SIPInboundTrunkInfo + result2 error + }{result1, result2} +} + +func (fake *FakeSIPStore) LoadSIPOutboundTrunk(arg1 context.Context, arg2 string) (*livekit.SIPOutboundTrunkInfo, error) { + fake.loadSIPOutboundTrunkMutex.Lock() + ret, specificReturn := fake.loadSIPOutboundTrunkReturnsOnCall[len(fake.loadSIPOutboundTrunkArgsForCall)] + fake.loadSIPOutboundTrunkArgsForCall = append(fake.loadSIPOutboundTrunkArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.LoadSIPOutboundTrunkStub + fakeReturns := fake.loadSIPOutboundTrunkReturns + fake.recordInvocation("LoadSIPOutboundTrunk", []interface{}{arg1, arg2}) + fake.loadSIPOutboundTrunkMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSIPStore) LoadSIPOutboundTrunkCallCount() int { + fake.loadSIPOutboundTrunkMutex.RLock() + defer fake.loadSIPOutboundTrunkMutex.RUnlock() + return len(fake.loadSIPOutboundTrunkArgsForCall) +} + +func (fake *FakeSIPStore) LoadSIPOutboundTrunkCalls(stub func(context.Context, string) (*livekit.SIPOutboundTrunkInfo, error)) { + fake.loadSIPOutboundTrunkMutex.Lock() + defer fake.loadSIPOutboundTrunkMutex.Unlock() + fake.LoadSIPOutboundTrunkStub = stub +} + +func (fake *FakeSIPStore) LoadSIPOutboundTrunkArgsForCall(i int) (context.Context, string) { + fake.loadSIPOutboundTrunkMutex.RLock() + defer fake.loadSIPOutboundTrunkMutex.RUnlock() + argsForCall := fake.loadSIPOutboundTrunkArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSIPStore) LoadSIPOutboundTrunkReturns(result1 *livekit.SIPOutboundTrunkInfo, result2 error) { + fake.loadSIPOutboundTrunkMutex.Lock() + defer fake.loadSIPOutboundTrunkMutex.Unlock() + fake.LoadSIPOutboundTrunkStub = nil + fake.loadSIPOutboundTrunkReturns = struct { + result1 *livekit.SIPOutboundTrunkInfo + result2 error + }{result1, result2} +} + +func (fake *FakeSIPStore) LoadSIPOutboundTrunkReturnsOnCall(i int, result1 *livekit.SIPOutboundTrunkInfo, result2 error) { + fake.loadSIPOutboundTrunkMutex.Lock() + defer fake.loadSIPOutboundTrunkMutex.Unlock() + fake.LoadSIPOutboundTrunkStub = nil + if fake.loadSIPOutboundTrunkReturnsOnCall == nil { + fake.loadSIPOutboundTrunkReturnsOnCall = make(map[int]struct { + result1 *livekit.SIPOutboundTrunkInfo + result2 error + }) + } + fake.loadSIPOutboundTrunkReturnsOnCall[i] = struct { + result1 *livekit.SIPOutboundTrunkInfo + result2 error + }{result1, result2} +} + func (fake *FakeSIPStore) LoadSIPTrunk(arg1 context.Context, arg2 string) (*livekit.SIPTrunkInfo, error) { fake.loadSIPTrunkMutex.Lock() ret, specificReturn := fake.loadSIPTrunkReturnsOnCall[len(fake.loadSIPTrunkArgsForCall)] @@ -560,6 +896,130 @@ func (fake *FakeSIPStore) StoreSIPDispatchRuleReturnsOnCall(i int, result1 error }{result1} } +func (fake *FakeSIPStore) StoreSIPInboundTrunk(arg1 context.Context, arg2 *livekit.SIPInboundTrunkInfo) error { + fake.storeSIPInboundTrunkMutex.Lock() + ret, specificReturn := fake.storeSIPInboundTrunkReturnsOnCall[len(fake.storeSIPInboundTrunkArgsForCall)] + fake.storeSIPInboundTrunkArgsForCall = append(fake.storeSIPInboundTrunkArgsForCall, struct { + arg1 context.Context + arg2 *livekit.SIPInboundTrunkInfo + }{arg1, arg2}) + stub := fake.StoreSIPInboundTrunkStub + fakeReturns := fake.storeSIPInboundTrunkReturns + fake.recordInvocation("StoreSIPInboundTrunk", []interface{}{arg1, arg2}) + fake.storeSIPInboundTrunkMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSIPStore) StoreSIPInboundTrunkCallCount() int { + fake.storeSIPInboundTrunkMutex.RLock() + defer fake.storeSIPInboundTrunkMutex.RUnlock() + return len(fake.storeSIPInboundTrunkArgsForCall) +} + +func (fake *FakeSIPStore) StoreSIPInboundTrunkCalls(stub func(context.Context, *livekit.SIPInboundTrunkInfo) error) { + fake.storeSIPInboundTrunkMutex.Lock() + defer fake.storeSIPInboundTrunkMutex.Unlock() + fake.StoreSIPInboundTrunkStub = stub +} + +func (fake *FakeSIPStore) StoreSIPInboundTrunkArgsForCall(i int) (context.Context, *livekit.SIPInboundTrunkInfo) { + fake.storeSIPInboundTrunkMutex.RLock() + defer fake.storeSIPInboundTrunkMutex.RUnlock() + argsForCall := fake.storeSIPInboundTrunkArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSIPStore) StoreSIPInboundTrunkReturns(result1 error) { + fake.storeSIPInboundTrunkMutex.Lock() + defer fake.storeSIPInboundTrunkMutex.Unlock() + fake.StoreSIPInboundTrunkStub = nil + fake.storeSIPInboundTrunkReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSIPStore) StoreSIPInboundTrunkReturnsOnCall(i int, result1 error) { + fake.storeSIPInboundTrunkMutex.Lock() + defer fake.storeSIPInboundTrunkMutex.Unlock() + fake.StoreSIPInboundTrunkStub = nil + if fake.storeSIPInboundTrunkReturnsOnCall == nil { + fake.storeSIPInboundTrunkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.storeSIPInboundTrunkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeSIPStore) StoreSIPOutboundTrunk(arg1 context.Context, arg2 *livekit.SIPOutboundTrunkInfo) error { + fake.storeSIPOutboundTrunkMutex.Lock() + ret, specificReturn := fake.storeSIPOutboundTrunkReturnsOnCall[len(fake.storeSIPOutboundTrunkArgsForCall)] + fake.storeSIPOutboundTrunkArgsForCall = append(fake.storeSIPOutboundTrunkArgsForCall, struct { + arg1 context.Context + arg2 *livekit.SIPOutboundTrunkInfo + }{arg1, arg2}) + stub := fake.StoreSIPOutboundTrunkStub + fakeReturns := fake.storeSIPOutboundTrunkReturns + fake.recordInvocation("StoreSIPOutboundTrunk", []interface{}{arg1, arg2}) + fake.storeSIPOutboundTrunkMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSIPStore) StoreSIPOutboundTrunkCallCount() int { + fake.storeSIPOutboundTrunkMutex.RLock() + defer fake.storeSIPOutboundTrunkMutex.RUnlock() + return len(fake.storeSIPOutboundTrunkArgsForCall) +} + +func (fake *FakeSIPStore) StoreSIPOutboundTrunkCalls(stub func(context.Context, *livekit.SIPOutboundTrunkInfo) error) { + fake.storeSIPOutboundTrunkMutex.Lock() + defer fake.storeSIPOutboundTrunkMutex.Unlock() + fake.StoreSIPOutboundTrunkStub = stub +} + +func (fake *FakeSIPStore) StoreSIPOutboundTrunkArgsForCall(i int) (context.Context, *livekit.SIPOutboundTrunkInfo) { + fake.storeSIPOutboundTrunkMutex.RLock() + defer fake.storeSIPOutboundTrunkMutex.RUnlock() + argsForCall := fake.storeSIPOutboundTrunkArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSIPStore) StoreSIPOutboundTrunkReturns(result1 error) { + fake.storeSIPOutboundTrunkMutex.Lock() + defer fake.storeSIPOutboundTrunkMutex.Unlock() + fake.StoreSIPOutboundTrunkStub = nil + fake.storeSIPOutboundTrunkReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSIPStore) StoreSIPOutboundTrunkReturnsOnCall(i int, result1 error) { + fake.storeSIPOutboundTrunkMutex.Lock() + defer fake.storeSIPOutboundTrunkMutex.Unlock() + fake.StoreSIPOutboundTrunkStub = nil + if fake.storeSIPOutboundTrunkReturnsOnCall == nil { + fake.storeSIPOutboundTrunkReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.storeSIPOutboundTrunkReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeSIPStore) StoreSIPTrunk(arg1 context.Context, arg2 *livekit.SIPTrunkInfo) error { fake.storeSIPTrunkMutex.Lock() ret, specificReturn := fake.storeSIPTrunkReturnsOnCall[len(fake.storeSIPTrunkArgsForCall)] @@ -631,14 +1091,26 @@ func (fake *FakeSIPStore) Invocations() map[string][][]interface{} { defer fake.deleteSIPTrunkMutex.RUnlock() fake.listSIPDispatchRuleMutex.RLock() defer fake.listSIPDispatchRuleMutex.RUnlock() + fake.listSIPInboundTrunkMutex.RLock() + defer fake.listSIPInboundTrunkMutex.RUnlock() + fake.listSIPOutboundTrunkMutex.RLock() + defer fake.listSIPOutboundTrunkMutex.RUnlock() fake.listSIPTrunkMutex.RLock() defer fake.listSIPTrunkMutex.RUnlock() fake.loadSIPDispatchRuleMutex.RLock() defer fake.loadSIPDispatchRuleMutex.RUnlock() + fake.loadSIPInboundTrunkMutex.RLock() + defer fake.loadSIPInboundTrunkMutex.RUnlock() + fake.loadSIPOutboundTrunkMutex.RLock() + defer fake.loadSIPOutboundTrunkMutex.RUnlock() fake.loadSIPTrunkMutex.RLock() defer fake.loadSIPTrunkMutex.RUnlock() fake.storeSIPDispatchRuleMutex.RLock() defer fake.storeSIPDispatchRuleMutex.RUnlock() + fake.storeSIPInboundTrunkMutex.RLock() + defer fake.storeSIPInboundTrunkMutex.RUnlock() + fake.storeSIPOutboundTrunkMutex.RLock() + defer fake.storeSIPOutboundTrunkMutex.RUnlock() fake.storeSIPTrunkMutex.RLock() defer fake.storeSIPTrunkMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/pkg/service/sip.go b/pkg/service/sip.go index 7421a5bd8..c70522560 100644 --- a/pkg/service/sip.go +++ b/pkg/service/sip.go @@ -16,6 +16,7 @@ package service import ( "context" + "errors" "fmt" "time" @@ -82,7 +83,38 @@ func (s *SIPService) CreateSIPTrunk(ctx context.Context, req *livekit.CreateSIPT } // Validate all trunks including the new one first. - list, err := s.store.ListSIPTrunk(ctx) + list, err := s.store.ListSIPInboundTrunk(ctx) + if err != nil { + return nil, err + } + list = append(list, info.AsInbound()) + if err = sip.ValidateTrunks(list); err != nil { + return nil, err + } + + // Now we can generate ID and store. + info.SipTrunkId = guid.New(utils.SIPTrunkPrefix) + if err := s.store.StoreSIPTrunk(ctx, info); err != nil { + return nil, err + } + return info, nil +} + +func (s *SIPService) CreateSIPInboundTrunk(ctx context.Context, req *livekit.CreateSIPInboundTrunkRequest) (*livekit.SIPInboundTrunkInfo, error) { + if s.store == nil { + return nil, ErrSIPNotConnected + } + info := req.Trunk + if info == nil { + return nil, errors.New("trunk info is required") + } else if info.SipTrunkId != "" { + return nil, errors.New("trunk ID must be empty") + } + + // Keep ID empty still, so that validation can print "" instead of a non-existent ID in the error. + + // Validate all trunks including the new one first. + list, err := s.store.ListSIPInboundTrunk(ctx) if err != nil { return nil, err } @@ -93,7 +125,26 @@ func (s *SIPService) CreateSIPTrunk(ctx context.Context, req *livekit.CreateSIPT // Now we can generate ID and store. info.SipTrunkId = guid.New(utils.SIPTrunkPrefix) - if err := s.store.StoreSIPTrunk(ctx, info); err != nil { + if err := s.store.StoreSIPInboundTrunk(ctx, info); err != nil { + return nil, err + } + return info, nil +} + +func (s *SIPService) CreateSIPOutboundTrunk(ctx context.Context, req *livekit.CreateSIPOutboundTrunkRequest) (*livekit.SIPOutboundTrunkInfo, error) { + if s.store == nil { + return nil, ErrSIPNotConnected + } + info := req.Trunk + if info == nil { + return nil, errors.New("trunk info is required") + } else if info.SipTrunkId != "" { + return nil, errors.New("trunk ID must be empty") + } + + // No additional validation needed for outbound. + info.SipTrunkId = guid.New(utils.SIPTrunkPrefix) + if err := s.store.StoreSIPOutboundTrunk(ctx, info); err != nil { return nil, err } return info, nil @@ -112,6 +163,32 @@ func (s *SIPService) ListSIPTrunk(ctx context.Context, req *livekit.ListSIPTrunk return &livekit.ListSIPTrunkResponse{Items: trunks}, nil } +func (s *SIPService) ListSIPInboundTrunk(ctx context.Context, req *livekit.ListSIPInboundTrunkRequest) (*livekit.ListSIPInboundTrunkResponse, error) { + if s.store == nil { + return nil, ErrSIPNotConnected + } + + trunks, err := s.store.ListSIPInboundTrunk(ctx) + if err != nil { + return nil, err + } + + return &livekit.ListSIPInboundTrunkResponse{Items: trunks}, nil +} + +func (s *SIPService) ListSIPOutboundTrunk(ctx context.Context, req *livekit.ListSIPOutboundTrunkRequest) (*livekit.ListSIPOutboundTrunkResponse, error) { + if s.store == nil { + return nil, ErrSIPNotConnected + } + + trunks, err := s.store.ListSIPOutboundTrunk(ctx) + if err != nil { + return nil, err + } + + return &livekit.ListSIPOutboundTrunkResponse{Items: trunks}, nil +} + func (s *SIPService) DeleteSIPTrunk(ctx context.Context, req *livekit.DeleteSIPTrunkRequest) (*livekit.SIPTrunkInfo, error) { if s.store == nil { return nil, ErrSIPNotConnected @@ -199,13 +276,17 @@ func (s *SIPService) CreateSIPParticipantWithToken(ctx context.Context, req *liv log := logger.GetLogger() log = log.WithValues("callId", callID, "roomName", req.RoomName, "sipTrunk", req.SipTrunkId, "toUser", req.SipCallTo) - trunk, err := s.store.LoadSIPTrunk(ctx, req.SipTrunkId) + trunk, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId) if err != nil { log.Errorw("cannot get trunk to update sip participant", err) return nil, err } - log = log.WithValues("fromUser", trunk.OutboundNumber, "toHost", trunk.OutboundAddress) - ireq := rpc.NewCreateSIPParticipantRequest(callID, wsUrl, token, req, trunk) + ireq, err := rpc.NewCreateSIPParticipantRequest(callID, wsUrl, token, req, trunk) + if err != nil { + log.Errorw("cannot create sip participant request", err) + return nil, err + } + log = log.WithValues("fromUser", ireq.Number, "toHost", trunk.Address) // CreateSIPParticipant will wait for LiveKit Participant to be created and that can take some time. // Thus, we must set a higher deadline for it, if it's not set already. diff --git a/pkg/service/utils_test.go b/pkg/service/utils_test.go index 99c19ac35..62d2975b8 100644 --- a/pkg/service/utils_test.go +++ b/pkg/service/utils_test.go @@ -15,7 +15,9 @@ package service_test import ( + "context" "testing" + "time" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" @@ -23,10 +25,38 @@ import ( "github.com/livekit/livekit-server/pkg/service" ) -func redisClient() *redis.Client { - return redis.NewClient(&redis.Options{ +func redisClient(t testing.TB) *redis.Client { + cli := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := cli.Ping(ctx).Err() + if err == nil { + t.Cleanup(func() { + _ = cli.Close() + }) + return cli + } + _ = cli.Close() + t.Logf("local redis not available: %v", err) + + t.Logf("starting redis in docker") + addr := runRedis(t) + cli = redis.NewClient(&redis.Options{ + Addr: addr, + }) + ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err = cli.Ping(ctx).Err(); err != nil { + _ = cli.Close() + t.Fatal(err) + } + t.Cleanup(func() { + _ = cli.Close() + }) + return cli } func TestIsValidDomain(t *testing.T) { From 8a229fda9da589ab9525745d7b82b71571a6f282 Mon Sep 17 00:00:00 2001 From: Lukas Herman Date: Mon, 17 Jun 2024 17:52:08 -0400 Subject: [PATCH 28/30] add participant session duration metric (#2801) --- pkg/rtc/participant.go | 6 ++++++ pkg/telemetry/prometheus/rooms.go | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/pkg/rtc/participant.go b/pkg/rtc/participant.go index e7e847291..ed64cda3b 100644 --- a/pkg/rtc/participant.go +++ b/pkg/rtc/participant.go @@ -164,6 +164,7 @@ type ParticipantImpl struct { isPublisher atomic.Bool sessionStartRecorded atomic.Bool + lastActiveAt time.Time // when first connected connectedAt time.Time // timer that's set when disconnect is detected on primary PC @@ -1375,6 +1376,11 @@ func (p *ParticipantImpl) updateState(state livekit.ParticipantInfo_State) { return } + if state == livekit.ParticipantInfo_DISCONNECTED && oldState == livekit.ParticipantInfo_ACTIVE { + prometheus.RecordSessionDuration(int(p.ProtocolVersion()), time.Since(p.lastActiveAt)) + } else if state == livekit.ParticipantInfo_ACTIVE { + p.lastActiveAt = time.Now() + } p.params.Logger.Debugw("updating participant state", "state", state.String()) p.dirty.Store(true) diff --git a/pkg/telemetry/prometheus/rooms.go b/pkg/telemetry/prometheus/rooms.go index 6c7540d78..7e1102026 100644 --- a/pkg/telemetry/prometheus/rooms.go +++ b/pkg/telemetry/prometheus/rooms.go @@ -45,6 +45,7 @@ var ( promTrackPublishCounter *prometheus.CounterVec promTrackSubscribeCounter *prometheus.CounterVec promSessionStartTime *prometheus.HistogramVec + promSessionDuration *prometheus.HistogramVec ) func initRoomStats(nodeID string, nodeType livekit.NodeType) { @@ -100,6 +101,13 @@ func initRoomStats(nodeID string, nodeType livekit.NodeType) { ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()}, Buckets: prometheus.ExponentialBucketsRange(100, 10000, 15), }, []string{"protocol_version"}) + promSessionDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: livekitNamespace, + Subsystem: "session", + Name: "duration_ms", + ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()}, + Buckets: prometheus.ExponentialBucketsRange(100, 4*60*60*1000, 15), + }, []string{"protocol_version"}) prometheus.MustRegister(promRoomCurrent) prometheus.MustRegister(promRoomDuration) @@ -109,6 +117,7 @@ func initRoomStats(nodeID string, nodeType livekit.NodeType) { prometheus.MustRegister(promTrackPublishCounter) prometheus.MustRegister(promTrackSubscribeCounter) prometheus.MustRegister(promSessionStartTime) + prometheus.MustRegister(promSessionDuration) } func RoomStarted() { @@ -186,3 +195,7 @@ func RecordTrackSubscribeFailure(err error, isUserError bool) { func RecordSessionStartTime(protocolVersion int, d time.Duration) { promSessionStartTime.WithLabelValues(strconv.Itoa(protocolVersion)).Observe(float64(d.Milliseconds())) } + +func RecordSessionDuration(protocolVersion int, d time.Duration) { + promSessionDuration.WithLabelValues(strconv.Itoa(protocolVersion)).Observe(float64(d.Milliseconds())) +} From d4e50b633f750ecdbca2b556f78692d4bcbe314f Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Thu, 20 Jun 2024 10:52:12 +0530 Subject: [PATCH 29/30] Do not log warns on duplicate. (#2807) With RTX, some clients use very old packets for probing. Check for duplicate before logging warning about old packet/negative sequence number jump. Also, double the history so that duplicate tracking is better. Adds about 1/2 KB per RTP stream. --- pkg/sfu/buffer/buffer.go | 28 +++++++++++++++------------- pkg/sfu/buffer/rtpstats_receiver.go | 22 +++++++++++----------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index 89f776608..829ee1794 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -611,12 +611,23 @@ func (b *Buffer) calc(rawPkt []byte, rtpPacket *rtp.Packet, arrivalTime time.Tim rtpPacket.Header.SequenceNumber = uint16(flowState.ExtSequenceNumber) _, err = b.bucket.AddPacketWithSequenceNumber(rawPkt, rtpPacket.Header.SequenceNumber) if err != nil { - if errors.Is(err, bucket.ErrPacketTooOld) { - packetTooOldCount := b.packetTooOldCount.Inc() - if (packetTooOldCount-1)%100 == 0 { + if !flowState.IsDuplicate { + if errors.Is(err, bucket.ErrPacketTooOld) { + packetTooOldCount := b.packetTooOldCount.Inc() + if (packetTooOldCount-1)%100 == 0 { + b.logger.Warnw( + "could not add packet to bucket", err, + "count", packetTooOldCount, + "flowState", &flowState, + "snAdjustment", snAdjustment, + "incomingSequenceNumber", flowState.ExtSequenceNumber+snAdjustment, + "rtpStats", b.rtpStats, + "snRangeMap", b.snRangeMap, + ) + } + } else if err != bucket.ErrRTXPacket { b.logger.Warnw( "could not add packet to bucket", err, - "count", packetTooOldCount, "flowState", &flowState, "snAdjustment", snAdjustment, "incomingSequenceNumber", flowState.ExtSequenceNumber+snAdjustment, @@ -624,15 +635,6 @@ func (b *Buffer) calc(rawPkt []byte, rtpPacket *rtp.Packet, arrivalTime time.Tim "snRangeMap", b.snRangeMap, ) } - } else if err != bucket.ErrRTXPacket { - b.logger.Warnw( - "could not add packet to bucket", err, - "flowState", &flowState, - "snAdjustment", snAdjustment, - "incomingSequenceNumber", flowState.ExtSequenceNumber+snAdjustment, - "rtpStats", b.rtpStats, - "snRangeMap", b.snRangeMap, - ) } return } diff --git a/pkg/sfu/buffer/rtpstats_receiver.go b/pkg/sfu/buffer/rtpstats_receiver.go index 94952ef1c..6cbc9fd4c 100644 --- a/pkg/sfu/buffer/rtpstats_receiver.go +++ b/pkg/sfu/buffer/rtpstats_receiver.go @@ -28,7 +28,7 @@ import ( ) const ( - cHistorySize = 4096 + cHistorySize = 8192 // RTCP Sender Reports are re-based to SFU time base so that all subscriber side // can have the same time base (i. e. SFU time base). To convert publisher side @@ -219,16 +219,6 @@ func (r *RTPStatsReceiver) Update( } } if gapSN <= 0 { // duplicate OR out-of-order - if -gapSN >= cSequenceNumberLargeJumpThreshold { - if r.largeJumpNegativeCount%100 == 0 { - r.logger.Warnw( - "large sequence number gap negative", nil, - append(getLoggingFields(), "count", r.largeJumpNegativeCount)..., - ) - } - r.largeJumpNegativeCount++ - } - if gapSN != 0 { r.packetsOutOfOrder++ } @@ -248,6 +238,16 @@ func (r *RTPStatsReceiver) Update( flowState.IsOutOfOrder = true flowState.ExtSequenceNumber = resSN.ExtendedVal flowState.ExtTimestamp = resTS.ExtendedVal + + if !flowState.IsDuplicate && -gapSN >= cSequenceNumberLargeJumpThreshold { + if r.largeJumpNegativeCount%100 == 0 { + r.logger.Warnw( + "large sequence number gap negative", nil, + append(getLoggingFields(), "count", r.largeJumpNegativeCount)..., + ) + } + r.largeJumpNegativeCount++ + } } else { // in-order if gapSN >= cSequenceNumberLargeJumpThreshold || resTS.ExtendedVal < resTS.PreExtendedHighest { if r.largeJumpCount%100 == 0 { From 7a774cc82a665617e21e067dfade5c385b1e700d Mon Sep 17 00:00:00 2001 From: David Zhao Date: Thu, 20 Jun 2024 02:14:19 -0400 Subject: [PATCH 30/30] Support for participant attributes (#2806) * Support for participant attributes * move metadata setters to LocalParticipant * address feedback * forward error * update go mod * update attributes first --- config-sample.yaml | 10 ++- go.mod | 2 +- go.sum | 4 +- pkg/config/config.go | 49 ++++++++---- pkg/rtc/errors.go | 5 +- pkg/rtc/participant.go | 48 ++++++++++++ pkg/rtc/room.go | 13 +++- pkg/rtc/signalhandler.go | 10 ++- pkg/rtc/types/interfaces.go | 10 ++- .../typesfakes/fake_local_participant.go | 74 ++++++++++++++++++ pkg/rtc/types/typesfakes/fake_participant.go | 78 ------------------- pkg/rtc/types/typesfakes/fake_room.go | 53 +++++++++++-- pkg/service/errors.go | 1 + pkg/service/ioservice_sip.go | 2 +- pkg/service/roommanager.go | 11 ++- pkg/service/roomservice.go | 23 ++++-- pkg/service/roomservice_test.go | 12 +-- pkg/service/rtcservice.go | 6 +- pkg/service/wire.go | 6 +- pkg/service/wire_gen.go | 8 +- test/client/client.go | 12 +++ test/integration_helpers.go | 14 +++- test/multinode_test.go | 75 ++++++++++++++++++ test/singlenode_test.go | 4 +- 24 files changed, 388 insertions(+), 142 deletions(-) diff --git a/config-sample.yaml b/config-sample.yaml index 9370819a4..6a647823a 100644 --- a/config-sample.yaml +++ b/config-sample.yaml @@ -183,8 +183,6 @@ keys: # # allow tracks to be unmuted remotely, defaults to false # # tracks can always be muted from the Room Service APIs # enable_remote_unmute: true -# # limit size of room and participant's metadata, 0 for no limit -# max_metadata_size: 0 # # control playout delay in ms of video track (and associated audio track) # playout_delay: # enabled: true @@ -311,3 +309,11 @@ keys: # # value less or equal than 0 means no limit. # subscription_limit_video: 0 # subscription_limit_audio: 0 +# # limit size of room and participant's metadata, 0 for no limit +# max_metadata_size: 0 +# # limit size of participant attributes, 0 for no limit +# max_attributes_size: 0 +# # limit length of room names +# max_room_name_length: 0 +# # limit length of participant identity +# max_participant_identity_length: 0 diff --git a/go.mod b/go.mod index d80c3a640..ef8dc0491 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 - github.com/livekit/protocol v1.17.1-0.20240617184219-32c577d805ed + github.com/livekit/protocol v1.18.0 github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 github.com/mackerelio/go-osstat v0.2.5 github.com/magefile/mage v1.15.0 diff --git a/go.sum b/go.sum index 061c3305a..f7d3af548 100644 --- a/go.sum +++ b/go.sum @@ -152,8 +152,8 @@ github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkD github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75 h1:p60OjeixzXnhGFQL8wmdUwWPxijEDe9ZJFMosq+byec= github.com/livekit/mediatransportutil v0.0.0-20240613015318-84b69facfb75/go.mod h1:jwKUCmObuiEDH0iiuJHaGMXwRs3RjrB4G6qqgkr/5oE= -github.com/livekit/protocol v1.17.1-0.20240617184219-32c577d805ed h1:S4avs1NKG6bBgHYuBOrQWnNxJSOdunGOB84BQfGzKmQ= -github.com/livekit/protocol v1.17.1-0.20240617184219-32c577d805ed/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= +github.com/livekit/protocol v1.18.0 h1:LLOjKBA8rtnGpVGjAmKUROy7bv/l9q1wyn9hNmj8Sdg= +github.com/livekit/protocol v1.18.0/go.mod h1:cN8WmGQR+kWz1+UWcAQdFFUcbW76PnfZDdkLAbYIqd4= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 h1:mTZyrjk5WEWMsvaYtJ42pG7DuxysKj21DKPINpGSIto= github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0= github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= diff --git a/pkg/config/config.go b/pkg/config/config.go index c1b40cc12..306580414 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -231,17 +231,20 @@ type VideoConfig struct { type RoomConfig struct { // enable rooms to be automatically created - AutoCreate bool `yaml:"auto_create,omitempty"` - EnabledCodecs []CodecSpec `yaml:"enabled_codecs,omitempty"` - MaxParticipants uint32 `yaml:"max_participants,omitempty"` - EmptyTimeout uint32 `yaml:"empty_timeout,omitempty"` - DepartureTimeout uint32 `yaml:"departure_timeout,omitempty"` - EnableRemoteUnmute bool `yaml:"enable_remote_unmute,omitempty"` - MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"` - PlayoutDelay PlayoutDelayConfig `yaml:"playout_delay,omitempty"` - SyncStreams bool `yaml:"sync_streams,omitempty"` - MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"` - MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"` + AutoCreate bool `yaml:"auto_create,omitempty"` + EnabledCodecs []CodecSpec `yaml:"enabled_codecs,omitempty"` + MaxParticipants uint32 `yaml:"max_participants,omitempty"` + EmptyTimeout uint32 `yaml:"empty_timeout,omitempty"` + DepartureTimeout uint32 `yaml:"departure_timeout,omitempty"` + EnableRemoteUnmute bool `yaml:"enable_remote_unmute,omitempty"` + PlayoutDelay PlayoutDelayConfig `yaml:"playout_delay,omitempty"` + SyncStreams bool `yaml:"sync_streams,omitempty"` + // deprecated, moved to limits + MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"` + // deprecated, moved to limits + MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"` + // deprecated, moved to limits + MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"` } type CodecSpec struct { @@ -300,6 +303,11 @@ type LimitConfig struct { BytesPerSec float32 `yaml:"bytes_per_sec,omitempty"` SubscriptionLimitVideo int32 `yaml:"subscription_limit_video,omitempty"` SubscriptionLimitAudio int32 `yaml:"subscription_limit_audio,omitempty"` + MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"` + // total size of all attributes on a participant + MaxAttributesSize uint32 `yaml:"max_attributes_size,omitempty"` + MaxRoomNameLength int `yaml:"max_room_name_length,omitempty"` + MaxParticipantIdentityLength int `yaml:"max_participant_identity_length,omitempty"` } type IngressConfig struct { @@ -494,8 +502,12 @@ var DefaultConfig = Config{ {Mime: webrtc.MimeTypeVP9}, {Mime: webrtc.MimeTypeAV1}, }, - EmptyTimeout: 5 * 60, - DepartureTimeout: 20, + EmptyTimeout: 5 * 60, + DepartureTimeout: 20, + }, + Limit: LimitConfig{ + MaxMetadataSize: 64000, + MaxAttributesSize: 64000, MaxRoomNameLength: 256, MaxParticipantIdentityLength: 256, }, @@ -585,6 +597,17 @@ func NewConfig(confString string, strictMode bool, c *cli.Context, baseFlags []c conf.Logging.ComponentLevels["pion"] = conf.Logging.PionLevel } + // copy over legacy limits + if conf.Room.MaxMetadataSize != 0 { + conf.Limit.MaxMetadataSize = conf.Room.MaxMetadataSize + } + if conf.Room.MaxParticipantIdentityLength != 0 { + conf.Limit.MaxParticipantIdentityLength = conf.Room.MaxParticipantIdentityLength + } + if conf.Room.MaxRoomNameLength != 0 { + conf.Limit.MaxRoomNameLength = conf.Room.MaxRoomNameLength + } + return &conf, nil } diff --git a/pkg/rtc/errors.go b/pkg/rtc/errors.go index 32df7d91f..08e8ef33e 100644 --- a/pkg/rtc/errors.go +++ b/pkg/rtc/errors.go @@ -14,7 +14,9 @@ package rtc -import "errors" +import ( + "errors" +) var ( ErrRoomClosed = errors.New("room has already closed") @@ -29,6 +31,7 @@ var ( ErrEmptyParticipantID = errors.New("participant ID cannot be empty") ErrMissingGrants = errors.New("VideoGrant is missing") ErrInternalError = errors.New("internal error") + ErrAttributeExceedsLimits = errors.New("attribute size exceeds limits") // Track subscription related ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track") diff --git a/pkg/rtc/participant.go b/pkg/rtc/participant.go index ed64cda3b..95636b1d9 100644 --- a/pkg/rtc/participant.go +++ b/pkg/rtc/participant.go @@ -136,6 +136,7 @@ type ParticipantParams struct { VersionGenerator utils.TimedVersionGenerator TrackResolver types.MediaTrackResolver DisableDynacast bool + MaxAttributesSize uint32 SubscriberAllowPause bool SubscriptionLimitAudio int32 SubscriptionLimitVideo int32 @@ -459,6 +460,52 @@ func (p *ParticipantImpl) SetMetadata(metadata string) { } } +func (p *ParticipantImpl) SetAttributes(attrs map[string]string) error { + p.lock.Lock() + grants := p.grants.Load().Clone() + if grants.Attributes == nil { + grants.Attributes = make(map[string]string) + } + var keysToDelete []string + for k, v := range attrs { + if v == "" { + keysToDelete = append(keysToDelete, k) + } else { + grants.Attributes[k] = v + } + } + for _, k := range keysToDelete { + delete(grants.Attributes, k) + } + + maxAttributesSize := p.params.MaxAttributesSize + if maxAttributesSize > 0 { + total := 0 + for k, v := range grants.Attributes { + total += len(k) + len(v) + } + if uint32(total) > maxAttributesSize { + p.lock.Unlock() + return ErrAttributeExceedsLimits + } + } + + p.grants.Store(grants) + p.dirty.Store(true) + + onParticipantUpdate := p.onParticipantUpdate + onClaimsChanged := p.onClaimsChanged + p.lock.Unlock() + + if onParticipantUpdate != nil { + onParticipantUpdate(p) + } + if onClaimsChanged != nil { + onClaimsChanged(p) + } + return nil +} + func (p *ParticipantImpl) ClaimGrants() *auth.ClaimGrants { return p.grants.Load() } @@ -551,6 +598,7 @@ func (p *ParticipantImpl) ToProtoWithVersion() (*livekit.ParticipantInfo, utils. Version: v, Permission: grants.Video.ToPermission(), Metadata: grants.Metadata, + Attributes: grants.Attributes, Region: p.params.Region, IsPublisher: p.IsPublisher(), Kind: grants.GetParticipantKind(), diff --git a/pkg/rtc/room.go b/pkg/rtc/room.go index 560dc06a1..9a5ecfc7b 100644 --- a/pkg/rtc/room.go +++ b/pkg/rtc/room.go @@ -819,13 +819,24 @@ func (r *Room) SetMetadata(metadata string) <-chan struct{} { return r.protoProxy.MarkDirty(true) } -func (r *Room) UpdateParticipantMetadata(participant types.LocalParticipant, name string, metadata string) { +func (r *Room) UpdateParticipantMetadata( + participant types.LocalParticipant, + name string, + metadata string, + attributes map[string]string, +) error { + if attributes != nil && len(attributes) > 0 { + if err := participant.SetAttributes(attributes); err != nil { + return err + } + } if metadata != "" { participant.SetMetadata(metadata) } if name != "" { participant.SetName(name) } + return nil } func (r *Room) sendRoomUpdate() { diff --git a/pkg/rtc/signalhandler.go b/pkg/rtc/signalhandler.go index d7d9c75ad..f9202e039 100644 --- a/pkg/rtc/signalhandler.go +++ b/pkg/rtc/signalhandler.go @@ -93,7 +93,15 @@ func HandleParticipantSignal(room types.Room, participant types.LocalParticipant case *livekit.SignalRequest_UpdateMetadata: if participant.ClaimGrants().Video.GetCanUpdateOwnMetadata() { - room.UpdateParticipantMetadata(participant, msg.UpdateMetadata.Name, msg.UpdateMetadata.Metadata) + err := room.UpdateParticipantMetadata( + participant, + msg.UpdateMetadata.Name, + msg.UpdateMetadata.Metadata, + msg.UpdateMetadata.Attributes, + ) + if err != nil { + pLogger.Warnw("could not update metadata", err) + } } case *livekit.SignalRequest_UpdateAudioTrack: diff --git a/pkg/rtc/types/interfaces.go b/pkg/rtc/types/interfaces.go index 93bb32206..e548900e8 100644 --- a/pkg/rtc/types/interfaces.go +++ b/pkg/rtc/types/interfaces.go @@ -255,9 +255,6 @@ type Participant interface { CanSkipBroadcast() bool ToProto() *livekit.ParticipantInfo - SetName(name string) - SetMetadata(metadata string) - IsPublisher() bool GetPublishedTrack(trackID livekit.TrackID) MediaTrack GetPublishedTracks() []MediaTrack @@ -329,6 +326,11 @@ type LocalParticipant interface { SetSignalSourceValid(valid bool) HandleSignalSourceClose() + // updates + SetName(name string) + SetMetadata(metadata string) + SetAttributes(attributes map[string]string) error + // permissions ClaimGrants() *auth.ClaimGrants SetPermission(permission *livekit.ParticipantPermission) bool @@ -437,7 +439,7 @@ type Room interface { SimulateScenario(participant LocalParticipant, scenario *livekit.SimulateScenario) error ResolveMediaTrackForSubscriber(subIdentity livekit.ParticipantIdentity, trackID livekit.TrackID) MediaResolverResult GetLocalParticipants() []LocalParticipant - UpdateParticipantMetadata(participant LocalParticipant, name string, metadata string) + UpdateParticipantMetadata(participant LocalParticipant, name string, metadata string, attributes map[string]string) error } // MediaTrack represents a media track diff --git a/pkg/rtc/types/typesfakes/fake_local_participant.go b/pkg/rtc/types/typesfakes/fake_local_participant.go index 8a4bf1b86..7c239d81e 100644 --- a/pkg/rtc/types/typesfakes/fake_local_participant.go +++ b/pkg/rtc/types/typesfakes/fake_local_participant.go @@ -733,6 +733,17 @@ type FakeLocalParticipant struct { sendSpeakerUpdateReturnsOnCall map[int]struct { result1 error } + SetAttributesStub func(map[string]string) error + setAttributesMutex sync.RWMutex + setAttributesArgsForCall []struct { + arg1 map[string]string + } + setAttributesReturns struct { + result1 error + } + setAttributesReturnsOnCall map[int]struct { + result1 error + } SetICEConfigStub func(*livekit.ICEConfig) setICEConfigMutex sync.RWMutex setICEConfigArgsForCall []struct { @@ -4881,6 +4892,67 @@ func (fake *FakeLocalParticipant) SendSpeakerUpdateReturnsOnCall(i int, result1 }{result1} } +func (fake *FakeLocalParticipant) SetAttributes(arg1 map[string]string) error { + fake.setAttributesMutex.Lock() + ret, specificReturn := fake.setAttributesReturnsOnCall[len(fake.setAttributesArgsForCall)] + fake.setAttributesArgsForCall = append(fake.setAttributesArgsForCall, struct { + arg1 map[string]string + }{arg1}) + stub := fake.SetAttributesStub + fakeReturns := fake.setAttributesReturns + fake.recordInvocation("SetAttributes", []interface{}{arg1}) + fake.setAttributesMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeLocalParticipant) SetAttributesCallCount() int { + fake.setAttributesMutex.RLock() + defer fake.setAttributesMutex.RUnlock() + return len(fake.setAttributesArgsForCall) +} + +func (fake *FakeLocalParticipant) SetAttributesCalls(stub func(map[string]string) error) { + fake.setAttributesMutex.Lock() + defer fake.setAttributesMutex.Unlock() + fake.SetAttributesStub = stub +} + +func (fake *FakeLocalParticipant) SetAttributesArgsForCall(i int) map[string]string { + fake.setAttributesMutex.RLock() + defer fake.setAttributesMutex.RUnlock() + argsForCall := fake.setAttributesArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeLocalParticipant) SetAttributesReturns(result1 error) { + fake.setAttributesMutex.Lock() + defer fake.setAttributesMutex.Unlock() + fake.SetAttributesStub = nil + fake.setAttributesReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeLocalParticipant) SetAttributesReturnsOnCall(i int, result1 error) { + fake.setAttributesMutex.Lock() + defer fake.setAttributesMutex.Unlock() + fake.SetAttributesStub = nil + if fake.setAttributesReturnsOnCall == nil { + fake.setAttributesReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setAttributesReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeLocalParticipant) SetICEConfig(arg1 *livekit.ICEConfig) { fake.setICEConfigMutex.Lock() fake.setICEConfigArgsForCall = append(fake.setICEConfigArgsForCall, struct { @@ -6571,6 +6643,8 @@ func (fake *FakeLocalParticipant) Invocations() map[string][][]interface{} { defer fake.sendRoomUpdateMutex.RUnlock() fake.sendSpeakerUpdateMutex.RLock() defer fake.sendSpeakerUpdateMutex.RUnlock() + fake.setAttributesMutex.RLock() + defer fake.setAttributesMutex.RUnlock() fake.setICEConfigMutex.RLock() defer fake.setICEConfigMutex.RUnlock() fake.setMetadataMutex.RLock() diff --git a/pkg/rtc/types/typesfakes/fake_participant.go b/pkg/rtc/types/typesfakes/fake_participant.go index 9494b87e6..9c74e6f63 100644 --- a/pkg/rtc/types/typesfakes/fake_participant.go +++ b/pkg/rtc/types/typesfakes/fake_participant.go @@ -175,16 +175,6 @@ type FakeParticipant struct { arg2 bool arg3 bool } - SetMetadataStub func(string) - setMetadataMutex sync.RWMutex - setMetadataArgsForCall []struct { - arg1 string - } - SetNameStub func(string) - setNameMutex sync.RWMutex - setNameArgsForCall []struct { - arg1 string - } StateStub func() livekit.ParticipantInfo_State stateMutex sync.RWMutex stateArgsForCall []struct { @@ -1115,70 +1105,6 @@ func (fake *FakeParticipant) RemovePublishedTrackArgsForCall(i int) (types.Media return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } -func (fake *FakeParticipant) SetMetadata(arg1 string) { - fake.setMetadataMutex.Lock() - fake.setMetadataArgsForCall = append(fake.setMetadataArgsForCall, struct { - arg1 string - }{arg1}) - stub := fake.SetMetadataStub - fake.recordInvocation("SetMetadata", []interface{}{arg1}) - fake.setMetadataMutex.Unlock() - if stub != nil { - fake.SetMetadataStub(arg1) - } -} - -func (fake *FakeParticipant) SetMetadataCallCount() int { - fake.setMetadataMutex.RLock() - defer fake.setMetadataMutex.RUnlock() - return len(fake.setMetadataArgsForCall) -} - -func (fake *FakeParticipant) SetMetadataCalls(stub func(string)) { - fake.setMetadataMutex.Lock() - defer fake.setMetadataMutex.Unlock() - fake.SetMetadataStub = stub -} - -func (fake *FakeParticipant) SetMetadataArgsForCall(i int) string { - fake.setMetadataMutex.RLock() - defer fake.setMetadataMutex.RUnlock() - argsForCall := fake.setMetadataArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeParticipant) SetName(arg1 string) { - fake.setNameMutex.Lock() - fake.setNameArgsForCall = append(fake.setNameArgsForCall, struct { - arg1 string - }{arg1}) - stub := fake.SetNameStub - fake.recordInvocation("SetName", []interface{}{arg1}) - fake.setNameMutex.Unlock() - if stub != nil { - fake.SetNameStub(arg1) - } -} - -func (fake *FakeParticipant) SetNameCallCount() int { - fake.setNameMutex.RLock() - defer fake.setNameMutex.RUnlock() - return len(fake.setNameArgsForCall) -} - -func (fake *FakeParticipant) SetNameCalls(stub func(string)) { - fake.setNameMutex.Lock() - defer fake.setNameMutex.Unlock() - fake.SetNameStub = stub -} - -func (fake *FakeParticipant) SetNameArgsForCall(i int) string { - fake.setNameMutex.RLock() - defer fake.setNameMutex.RUnlock() - argsForCall := fake.setNameArgsForCall[i] - return argsForCall.arg1 -} - func (fake *FakeParticipant) State() livekit.ParticipantInfo_State { fake.stateMutex.Lock() ret, specificReturn := fake.stateReturnsOnCall[len(fake.stateArgsForCall)] @@ -1561,10 +1487,6 @@ func (fake *FakeParticipant) Invocations() map[string][][]interface{} { defer fake.kindMutex.RUnlock() fake.removePublishedTrackMutex.RLock() defer fake.removePublishedTrackMutex.RUnlock() - fake.setMetadataMutex.RLock() - defer fake.setMetadataMutex.RUnlock() - fake.setNameMutex.RLock() - defer fake.setNameMutex.RUnlock() fake.stateMutex.RLock() defer fake.stateMutex.RUnlock() fake.subscriptionPermissionMutex.RLock() diff --git a/pkg/rtc/types/typesfakes/fake_room.go b/pkg/rtc/types/typesfakes/fake_room.go index 0abe91d69..503bf73ad 100644 --- a/pkg/rtc/types/typesfakes/fake_room.go +++ b/pkg/rtc/types/typesfakes/fake_room.go @@ -82,12 +82,19 @@ type FakeRoom struct { syncStateReturnsOnCall map[int]struct { result1 error } - UpdateParticipantMetadataStub func(types.LocalParticipant, string, string) + UpdateParticipantMetadataStub func(types.LocalParticipant, string, string, map[string]string) error updateParticipantMetadataMutex sync.RWMutex updateParticipantMetadataArgsForCall []struct { arg1 types.LocalParticipant arg2 string arg3 string + arg4 map[string]string + } + updateParticipantMetadataReturns struct { + result1 error + } + updateParticipantMetadataReturnsOnCall map[int]struct { + result1 error } UpdateSubscriptionPermissionStub func(types.LocalParticipant, *livekit.SubscriptionPermission) error updateSubscriptionPermissionMutex sync.RWMutex @@ -492,19 +499,26 @@ func (fake *FakeRoom) SyncStateReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeRoom) UpdateParticipantMetadata(arg1 types.LocalParticipant, arg2 string, arg3 string) { +func (fake *FakeRoom) UpdateParticipantMetadata(arg1 types.LocalParticipant, arg2 string, arg3 string, arg4 map[string]string) error { fake.updateParticipantMetadataMutex.Lock() + ret, specificReturn := fake.updateParticipantMetadataReturnsOnCall[len(fake.updateParticipantMetadataArgsForCall)] fake.updateParticipantMetadataArgsForCall = append(fake.updateParticipantMetadataArgsForCall, struct { arg1 types.LocalParticipant arg2 string arg3 string - }{arg1, arg2, arg3}) + arg4 map[string]string + }{arg1, arg2, arg3, arg4}) stub := fake.UpdateParticipantMetadataStub - fake.recordInvocation("UpdateParticipantMetadata", []interface{}{arg1, arg2, arg3}) + fakeReturns := fake.updateParticipantMetadataReturns + fake.recordInvocation("UpdateParticipantMetadata", []interface{}{arg1, arg2, arg3, arg4}) fake.updateParticipantMetadataMutex.Unlock() if stub != nil { - fake.UpdateParticipantMetadataStub(arg1, arg2, arg3) + return stub(arg1, arg2, arg3, arg4) } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 } func (fake *FakeRoom) UpdateParticipantMetadataCallCount() int { @@ -513,17 +527,40 @@ func (fake *FakeRoom) UpdateParticipantMetadataCallCount() int { return len(fake.updateParticipantMetadataArgsForCall) } -func (fake *FakeRoom) UpdateParticipantMetadataCalls(stub func(types.LocalParticipant, string, string)) { +func (fake *FakeRoom) UpdateParticipantMetadataCalls(stub func(types.LocalParticipant, string, string, map[string]string) error) { fake.updateParticipantMetadataMutex.Lock() defer fake.updateParticipantMetadataMutex.Unlock() fake.UpdateParticipantMetadataStub = stub } -func (fake *FakeRoom) UpdateParticipantMetadataArgsForCall(i int) (types.LocalParticipant, string, string) { +func (fake *FakeRoom) UpdateParticipantMetadataArgsForCall(i int) (types.LocalParticipant, string, string, map[string]string) { fake.updateParticipantMetadataMutex.RLock() defer fake.updateParticipantMetadataMutex.RUnlock() argsForCall := fake.updateParticipantMetadataArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeRoom) UpdateParticipantMetadataReturns(result1 error) { + fake.updateParticipantMetadataMutex.Lock() + defer fake.updateParticipantMetadataMutex.Unlock() + fake.UpdateParticipantMetadataStub = nil + fake.updateParticipantMetadataReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeRoom) UpdateParticipantMetadataReturnsOnCall(i int, result1 error) { + fake.updateParticipantMetadataMutex.Lock() + defer fake.updateParticipantMetadataMutex.Unlock() + fake.UpdateParticipantMetadataStub = nil + if fake.updateParticipantMetadataReturnsOnCall == nil { + fake.updateParticipantMetadataReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.updateParticipantMetadataReturnsOnCall[i] = struct { + result1 error + }{result1} } func (fake *FakeRoom) UpdateSubscriptionPermission(arg1 types.LocalParticipant, arg2 *livekit.SubscriptionPermission) error { diff --git a/pkg/service/errors.go b/pkg/service/errors.go index 26f24389d..4a5f4d37d 100644 --- a/pkg/service/errors.go +++ b/pkg/service/errors.go @@ -26,6 +26,7 @@ var ( ErrIngressNotFound = psrpc.NewErrorf(psrpc.NotFound, "ingress does not exist") ErrIngressNonReusable = psrpc.NewErrorf(psrpc.InvalidArgument, "ingress is not reusable and cannot be modified") ErrMetadataExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "metadata size exceeds limits") + ErrAttributeExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "attribute size exceeds limits") ErrRoomNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "room name length exceeds limits") ErrParticipantIdentityExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "participant identity length exceeds limits") ErrOperationFailed = psrpc.NewErrorf(psrpc.Internal, "operation cannot be completed") diff --git a/pkg/service/ioservice_sip.go b/pkg/service/ioservice_sip.go index e974833ac..f3996d9a8 100644 --- a/pkg/service/ioservice_sip.go +++ b/pkg/service/ioservice_sip.go @@ -74,7 +74,7 @@ func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.E return nil, err } log.Debugw("SIP dispatch rule matched", "sipRule", best.SipDispatchRuleId) - resp, err := sip.EvaluateDispatchRule(best, req) + resp, err := sip.EvaluateDispatchRule(trunkID, best, req) if err != nil { return nil, err } diff --git a/pkg/service/roommanager.go b/pkg/service/roommanager.go index b4d180788..3ac7520de 100644 --- a/pkg/service/roommanager.go +++ b/pkg/service/roommanager.go @@ -433,6 +433,7 @@ func (r *RoomManager) StartSession( AdaptiveStream: pi.AdaptiveStream, AllowTCPFallback: allowFallback, TURNSEnabled: r.config.IsTURNSEnabled(), + MaxAttributesSize: r.config.Limit.MaxAttributesSize, GetParticipantInfo: func(pID livekit.ParticipantID) *livekit.ParticipantInfo { if p := room.GetParticipantByID(pID); p != nil { return p.ToProto() @@ -708,8 +709,14 @@ func (r *RoomManager) UpdateParticipant(ctx context.Context, req *livekit.Update } participant.GetLogger().Debugw("updating participant", - "metadata", req.Metadata, "permission", req.Permission) - room.UpdateParticipantMetadata(participant, req.Name, req.Metadata) + "metadata", req.Metadata, + "permission", req.Permission, + "attributes", req.Attributes, + ) + err = room.UpdateParticipantMetadata(participant, req.Name, req.Metadata, req.Attributes) + if err != nil { + return nil, err + } if req.Permission != nil { participant.SetPermission(req.Permission) } diff --git a/pkg/service/roomservice.go b/pkg/service/roomservice.go index 0bae86fab..19657b09a 100644 --- a/pkg/service/roomservice.go +++ b/pkg/service/roomservice.go @@ -33,9 +33,8 @@ import ( "github.com/livekit/protocol/rpc" ) -// A rooms service that supports a single node type RoomService struct { - roomConf config.RoomConfig + limitConf config.LimitConfig apiConf config.APIConfig psrpcConf rpc.PSRPCConfig router routing.MessageRouter @@ -49,7 +48,7 @@ type RoomService struct { } func NewRoomService( - roomConf config.RoomConfig, + limitConf config.LimitConfig, apiConf config.APIConfig, psrpcConf rpc.PSRPCConfig, router routing.MessageRouter, @@ -62,7 +61,7 @@ func NewRoomService( participantClient rpc.TypedParticipantClient, ) (svc *RoomService, err error) { svc = &RoomService{ - roomConf: roomConf, + limitConf: limitConf, apiConf: apiConf, psrpcConf: psrpcConf, router: router, @@ -87,7 +86,7 @@ func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomReq return nil, ErrEgressNotConnected } - if limit := s.roomConf.MaxRoomNameLength; limit > 0 && len(req.Name) > limit { + if limit := s.limitConf.MaxRoomNameLength; limit > 0 && len(req.Name) > limit { return nil, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, limit) } @@ -232,10 +231,20 @@ func (s *RoomService) MutePublishedTrack(ctx context.Context, req *livekit.MuteR func (s *RoomService) UpdateParticipant(ctx context.Context, req *livekit.UpdateParticipantRequest) (*livekit.ParticipantInfo, error) { AppendLogFields(ctx, "room", req.Room, "participant", req.Identity) - maxMetadataSize := int(s.roomConf.MaxMetadataSize) + maxMetadataSize := int(s.limitConf.MaxMetadataSize) if maxMetadataSize > 0 && len(req.Metadata) > maxMetadataSize { return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(maxMetadataSize)) } + maxAttributeSize := int(s.limitConf.MaxAttributesSize) + if maxAttributeSize > 0 { + total := 0 + for key, val := range req.Attributes { + total += len(key) + len(val) + } + if total > maxAttributeSize { + return nil, twirp.InvalidArgumentError(ErrAttributeExceedsLimits.Error(), strconv.Itoa(maxAttributeSize)) + } + } if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil { return nil, twirpAuthError(err) @@ -270,7 +279,7 @@ func (s *RoomService) SendData(ctx context.Context, req *livekit.SendDataRequest func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.UpdateRoomMetadataRequest) (*livekit.Room, error) { AppendLogFields(ctx, "room", req.Room, "size", len(req.Metadata)) - maxMetadataSize := int(s.roomConf.MaxMetadataSize) + maxMetadataSize := int(s.limitConf.MaxMetadataSize) if maxMetadataSize > 0 && len(req.Metadata) > maxMetadataSize { return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(maxMetadataSize)) } diff --git a/pkg/service/roomservice_test.go b/pkg/service/roomservice_test.go index aa10a6399..71a6a4f73 100644 --- a/pkg/service/roomservice_test.go +++ b/pkg/service/roomservice_test.go @@ -34,7 +34,7 @@ import ( func TestDeleteRoom(t *testing.T) { t.Run("missing permissions", func(t *testing.T) { - svc := newTestRoomService(config.RoomConfig{}) + svc := newTestRoomService(config.LimitConfig{}) grant := &auth.ClaimGrants{ Video: &auth.VideoGrant{}, } @@ -48,7 +48,7 @@ func TestDeleteRoom(t *testing.T) { func TestMetaDataLimits(t *testing.T) { t.Run("metadata exceed limits", func(t *testing.T) { - svc := newTestRoomService(config.RoomConfig{MaxMetadataSize: 5}) + svc := newTestRoomService(config.LimitConfig{MaxMetadataSize: 5}) grant := &auth.ClaimGrants{ Video: &auth.VideoGrant{}, } @@ -72,8 +72,8 @@ func TestMetaDataLimits(t *testing.T) { }) notExceedsLimitsSvc := map[string]*TestRoomService{ - "metadata noe exceeds limits": newTestRoomService(config.RoomConfig{MaxMetadataSize: 5}), - "metadata no limits": newTestRoomService(config.RoomConfig{}), // no limits + "metadata noe exceeds limits": newTestRoomService(config.LimitConfig{MaxMetadataSize: 5}), + "metadata no limits": newTestRoomService(config.LimitConfig{}), // no limits } for n, s := range notExceedsLimitsSvc { @@ -104,12 +104,12 @@ func TestMetaDataLimits(t *testing.T) { } } -func newTestRoomService(conf config.RoomConfig) *TestRoomService { +func newTestRoomService(limitConf config.LimitConfig) *TestRoomService { router := &routingfakes.FakeRouter{} allocator := &servicefakes.FakeRoomAllocator{} store := &servicefakes.FakeServiceStore{} svc, err := service.NewRoomService( - conf, + limitConf, config.APIConfig{ExecutionTimeout: 2}, rpc.PSRPCConfig{}, router, diff --git a/pkg/service/rtcservice.go b/pkg/service/rtcservice.go index eaf0b2e87..5843231e2 100644 --- a/pkg/service/rtcservice.go +++ b/pkg/service/rtcservice.go @@ -120,7 +120,7 @@ func (s *RTCService) validate(r *http.Request) (livekit.RoomName, routing.Partic if claims.Identity == "" { return "", pi, http.StatusBadRequest, ErrIdentityEmpty } - if limit := s.config.Room.MaxParticipantIdentityLength; limit > 0 && len(claims.Identity) > limit { + if limit := s.config.Limit.MaxParticipantIdentityLength; limit > 0 && len(claims.Identity) > limit { return "", pi, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, limit) } @@ -136,7 +136,7 @@ func (s *RTCService) validate(r *http.Request) (livekit.RoomName, routing.Partic if onlyName != "" { roomName = onlyName } - if limit := s.config.Room.MaxRoomNameLength; limit > 0 && len(roomName) > limit { + if limit := s.config.Limit.MaxRoomNameLength; limit > 0 && len(roomName) > limit { return "", pi, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, limit) } @@ -508,7 +508,7 @@ func (s *RTCService) DrainConnections(interval time.Duration) { defer t.Stop() for c := range conns { - c.Close() + _ = c.Close() <-t.C } } diff --git a/pkg/service/wire.go b/pkg/service/wire.go index 91d6df7d1..c856c7384 100644 --- a/pkg/service/wire.go +++ b/pkg/service/wire.go @@ -54,7 +54,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live createClientConfiguration, createForwardStats, routing.CreateRouter, - getRoomConf, + getLimitConf, config.DefaultAPIConfig, wire.Bind(new(routing.MessageRouter), new(routing.Router)), wire.Bind(new(livekit.RoomService), new(*RoomService)), @@ -221,8 +221,8 @@ func createClientConfiguration() clientconfiguration.ClientConfigurationManager return clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations) } -func getRoomConf(config *config.Config) config.RoomConfig { - return config.Room +func getLimitConf(config *config.Config) config.LimitConfig { + return config.Limit } func getSignalRelayConfig(config *config.Config) config.SignalRelayConfig { diff --git a/pkg/service/wire_gen.go b/pkg/service/wire_gen.go index f731de75e..de2ecf664 100644 --- a/pkg/service/wire_gen.go +++ b/pkg/service/wire_gen.go @@ -36,7 +36,7 @@ import ( // Injectors from wire.go: func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*LivekitServer, error) { - roomConfig := getRoomConf(conf) + limitConfig := getLimitConf(conf) apiConfig := config.DefaultAPIConfig() psrpcConfig := getPSRPCConfig(conf) universalClient, err := createRedisClient(conf) @@ -96,7 +96,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live if err != nil { return nil, err } - roomService, err := NewRoomService(roomConfig, apiConfig, psrpcConfig, router, roomAllocator, objectStore, client, rtcEgressLauncher, topicFormatter, roomClient, participantClient) + roomService, err := NewRoomService(limitConfig, apiConfig, psrpcConfig, router, roomAllocator, objectStore, client, rtcEgressLauncher, topicFormatter, roomClient, participantClient) if err != nil { return nil, err } @@ -272,8 +272,8 @@ func createClientConfiguration() clientconfiguration.ClientConfigurationManager return clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations) } -func getRoomConf(config2 *config.Config) config.RoomConfig { - return config2.Room +func getLimitConf(config2 *config.Config) config.LimitConfig { + return config2.Limit } func getSignalRelayConfig(config2 *config.Config) config.SignalRelayConfig { diff --git a/test/client/client.go b/test/client/client.go index 5d461a74a..1564b4b97 100644 --- a/test/client/client.go +++ b/test/client/client.go @@ -35,6 +35,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/livekit/mediatransportutil/pkg/rtcconfig" + "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" @@ -110,6 +111,7 @@ type Options struct { Publish string ClientInfo *livekit.ClientInfo DisabledCodecs []webrtc.RTPCodecCapability + TokenCustomizer func(token *auth.AccessToken, grants *auth.VideoGrant) SignalRequestInterceptor SignalRequestInterceptor SignalResponseInterceptor SignalResponseInterceptor } @@ -563,6 +565,16 @@ func (c *RTCClient) SendIceCandidate(ic *webrtc.ICECandidate, target livekit.Sig }) } +func (c *RTCClient) SetAttributes(attrs map[string]string) error { + return c.SendRequest(&livekit.SignalRequest{ + Message: &livekit.SignalRequest_UpdateMetadata{ + UpdateMetadata: &livekit.UpdateParticipantMetadata{ + Attributes: attrs, + }, + }, + }) +} + func (c *RTCClient) hasPrimaryEverConnected() bool { if c.subscriberAsPrimary.Load() { return c.subscriber.HasEverConnected() diff --git a/test/integration_helpers.go b/test/integration_helpers.go index 8fffe6b81..69ea68291 100644 --- a/test/integration_helpers.go +++ b/test/integration_helpers.go @@ -202,7 +202,11 @@ func createMultiNodeServer(nodeID string, port uint32) *service.LivekitServer { // creates a client and runs against server func createRTCClient(name string, port int, opts *testclient.Options) *testclient.RTCClient { - token := joinToken(testRoom, name) + var customizer func(token *auth.AccessToken, grants *auth.VideoGrant) + if opts != nil { + customizer = opts.TokenCustomizer + } + token := joinToken(testRoom, name, customizer) ws, err := testclient.NewWebSocketConn(fmt.Sprintf("ws://localhost:%d", port), token, opts) if err != nil { panic(err) @@ -241,12 +245,16 @@ func redisClient() *redis.Client { }) } -func joinToken(room, name string) string { +func joinToken(room, name string, customFn func(token *auth.AccessToken, grants *auth.VideoGrant)) string { at := auth.NewAccessToken(testApiKey, testApiSecret). - AddGrant(&auth.VideoGrant{RoomJoin: true, Room: room}). SetIdentity(name). SetName(name). SetMetadata("metadata" + name) + grant := &auth.VideoGrant{RoomJoin: true, Room: room} + if customFn != nil { + customFn(at, grant) + } + at.AddGrant(grant) t, err := at.ToJWT() if err != nil { panic(err) diff --git a/test/multinode_test.go b/test/multinode_test.go index 8b1eb1d32..e964f3096 100644 --- a/test/multinode_test.go +++ b/test/multinode_test.go @@ -229,6 +229,81 @@ func TestMultiNodeRefreshToken(t *testing.T) { }) } +// ensure that token accurately reflects out of band updates +func TestMultiNodeUpdateAttributes(t *testing.T) { + if testing.Short() { + t.SkipNow() + return + } + + _, _, finish := setupMultiNodeTest("TestMultiNodeUpdateAttributes") + defer finish() + + c1 := createRTCClient("au1", defaultServerPort, &client.Options{ + TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) { + token.SetAttributes(map[string]string{ + "mykey": "au1", + }) + }, + }) + c2 := createRTCClient("au2", secondServerPort, &client.Options{ + TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) { + token.SetAttributes(map[string]string{ + "mykey": "au2", + }) + grants.SetCanUpdateOwnMetadata(true) + }, + }) + waitUntilConnected(t, c1, c2) + + testutils.WithTimeout(t, func() string { + rc2 := c1.GetRemoteParticipant(c2.ID()) + rc1 := c2.GetRemoteParticipant(c1.ID()) + if rc2 == nil || rc1 == nil { + return "participants could not see each other" + } + if rc1.Attributes == nil || rc1.Attributes["mykey"] != "au1" { + return "rc1's initial attributes are incorrect" + } + if rc2.Attributes == nil || rc2.Attributes["mykey"] != "au2" { + return "rc2's initial attributes are incorrect" + } + return "" + }) + + // this one should not go through + _ = c1.SetAttributes(map[string]string{"mykey": "shouldnotchange"}) + _ = c2.SetAttributes(map[string]string{"secondkey": "au2"}) + + // updates using room API should succeed + _, err := roomClient.UpdateParticipant(contextWithToken(adminRoomToken(testRoom)), &livekit.UpdateParticipantRequest{ + Room: testRoom, + Identity: "au1", + Attributes: map[string]string{ + "secondkey": "au1", + }, + }) + require.NoError(t, err) + + testutils.WithTimeout(t, func() string { + rc1 := c2.GetRemoteParticipant(c1.ID()) + rc2 := c1.GetRemoteParticipant(c2.ID()) + if rc1.Attributes["secondkey"] != "au1" { + return "au1's attribute update failed" + } + if rc2.Attributes["secondkey"] != "au2" { + return "au2's attribute update failed" + } + if rc1.Attributes["mykey"] != "au1" { + return "au1's mykey should not change" + } + if rc2.Attributes["mykey"] != "au2" { + return "au2's mykey should not change" + } + return "" + }) +} + func TestMultiNodeRevokePublishPermission(t *testing.T) { _, _, finish := setupMultiNodeTest("TestMultiNodeRevokePublishPermission") defer finish() diff --git a/test/singlenode_test.go b/test/singlenode_test.go index fe2e55b07..e136a8e68 100644 --- a/test/singlenode_test.go +++ b/test/singlenode_test.go @@ -408,12 +408,12 @@ func TestAutoCreate(t *testing.T) { waitForServerToStart(s) - token := joinToken(testRoom, "start-before-create") + token := joinToken(testRoom, "start-before-create", nil) _, err := testclient.NewWebSocketConn(fmt.Sprintf("ws://localhost:%d", defaultServerPort), token, nil) require.Error(t, err) // second join should also fail - token = joinToken(testRoom, "start-before-create-2") + token = joinToken(testRoom, "start-before-create-2", nil) _, err = testclient.NewWebSocketConn(fmt.Sprintf("ws://localhost:%d", defaultServerPort), token, nil) require.Error(t, err) })
LiveKit Ecosystem
Real-time SDKsReact Components · JavaScript · iOS/macOS · Android · Flutter · React Native · Rust · Python · Unity (web) · Unity (beta)
Server APIsNode.js · Golang · Ruby · Java/Kotlin · Python · Rust · PHP (community)
Real-time SDKsReact Components · Browser · iOS/macOS · Android · Flutter · React Native · Rust · Node.js · Python · Unity (web) · Unity (beta)
Server APIsNode.js · Golang · Ruby · Java/Kotlin · Python · Rust · PHP (community)
Agents FrameworksPython · Playground
ServicesLivekit server · Egress · Ingress · SIP
ResourcesDocs · Example apps · Cloud · Self-hosting · CLI