snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
Some checks failed
Release to Docker / docker (push) Failing after 2s
Release Test Server to Docker / docker (push) Failing after 3s
Test / test (push) Failing after 16s

This commit is contained in:
upstream-mirror
2026-09-15 16:14:22 +05:00
commit 78704103fc
422 changed files with 154805 additions and 0 deletions

158
pkg/rtc/clientinfo.go Normal file
View File

@@ -0,0 +1,158 @@
// 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 rtc
import (
"slices"
"strconv"
"strings"
"github.com/livekit/protocol/livekit"
)
type ClientInfo struct {
*livekit.ClientInfo
}
func (c ClientInfo) isFirefox() bool {
return c.ClientInfo != nil && (strings.EqualFold(c.ClientInfo.Browser, "firefox") || strings.EqualFold(c.ClientInfo.Browser, "firefox mobile"))
}
func (c ClientInfo) isSafari() bool {
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Browser, "safari")
}
func (c ClientInfo) isGo() bool {
return c.ClientInfo != nil && c.ClientInfo.Sdk == livekit.ClientInfo_GO
}
func (c ClientInfo) isLinux() bool {
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Os, "linux")
}
func (c ClientInfo) isAndroid() bool {
return c.ClientInfo != nil && strings.EqualFold(c.ClientInfo.Os, "android")
}
func (c ClientInfo) isOBS() bool {
return c.ClientInfo != nil && strings.Contains(c.ClientInfo.Browser, "OBS")
}
func (c ClientInfo) SupportsAudioRED() bool {
return !c.isFirefox() && !c.isSafari()
}
func (c ClientInfo) SupportsPrflxOverRelay() bool {
return !c.isFirefox()
}
// GoSDK(pion) relies on rtp packets to fire ontrack event, browsers and native (libwebrtc) rely on sdp
func (c ClientInfo) FireTrackByRTPPacket() bool {
return c.isGo()
}
func (c ClientInfo) SupportsCodecChange() bool {
return c.ClientInfo != nil && c.ClientInfo.Sdk != livekit.ClientInfo_GO && c.ClientInfo.Sdk != livekit.ClientInfo_UNKNOWN
}
func (c ClientInfo) CanHandleReconnectResponse() bool {
if c.Sdk == livekit.ClientInfo_JS {
// JS handles Reconnect explicitly in 1.6.3, prior to 1.6.4 it could not handle unknown responses
if c.compareVersion("1.6.3") < 0 {
return false
}
}
return true
}
func (c ClientInfo) SupportsICETCP() bool {
if c.ClientInfo == nil {
return false
}
if c.ClientInfo.Sdk == livekit.ClientInfo_GO {
// Go does not support active TCP
return false
}
if c.ClientInfo.Sdk == livekit.ClientInfo_SWIFT {
// ICE/TCP added in 1.0.5
return c.compareVersion("1.0.5") >= 0
}
// most SDKs support ICE/TCP
return true
}
func (c ClientInfo) SupportsChangeRTPSenderEncodingActive() bool {
return !c.isFirefox()
}
func (c ClientInfo) ComplyWithCodecOrderInSDPAnswer() bool {
return (!c.isLinux() && !c.isAndroid()) || !c.isFirefox()
}
// Rust SDK can't decode unknown signal message (TrackSubscribed and ErrorResponse)
func (c ClientInfo) SupportsTrackSubscribedEvent() bool {
return c.ClientInfo.GetSdk() != livekit.ClientInfo_RUST || c.ClientInfo.GetProtocol() >= 10
}
func (c ClientInfo) SupportsRequestResponse() bool {
return c.SupportsTrackSubscribedEvent()
}
func (c ClientInfo) SupportsSctpZeroChecksum() bool {
return c.ClientInfo.GetSdk() != livekit.ClientInfo_UNKNOWN &&
(!c.isGo() || c.compareVersion("2.4.0") >= 0)
}
func (c ClientInfo) SupportsTransceiverReuse() bool {
return !c.isSafari()
}
func (c ClientInfo) HasCapability(cap livekit.ClientInfo_Capability) bool {
if c.ClientInfo == nil {
return false
}
return slices.Contains(c.ClientInfo.Capabilities, cap)
}
func (c ClientInfo) SupportsPacketTrailer() bool {
return c.HasCapability(livekit.ClientInfo_CAP_PACKET_TRAILER)
}
// compareVersion compares a semver against the current client SDK version
// returning 1 if current version is greater than version
// 0 if they are the same, and -1 if it's an earlier version
func (c ClientInfo) compareVersion(version string) int {
if c.ClientInfo == nil {
return -1
}
parts0 := strings.Split(c.ClientInfo.Version, ".")
parts1 := strings.Split(version, ".")
ints0 := make([]int, 3)
ints1 := make([]int, 3)
for i := range 3 {
if len(parts0) > i {
ints0[i], _ = strconv.Atoi(parts0[i])
}
if len(parts1) > i {
ints1[i], _ = strconv.Atoi(parts1[i])
}
if ints0[i] > ints1[i] {
return 1
} else if ints0[i] < ints1[i] {
return -1
}
}
return 0
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2022 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 rtc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func TestClientInfo_CompareVersion(t *testing.T) {
c := ClientInfo{
ClientInfo: &livekit.ClientInfo{
Version: "1",
},
}
require.Equal(t, 1, c.compareVersion("0.1.0"))
require.Equal(t, 0, c.compareVersion("1.0.0"))
require.Equal(t, -1, c.compareVersion("1.0.5"))
}
func TestClientInfo_SupportsICETCP(t *testing.T) {
t.Run("GO SDK cannot support TCP", func(t *testing.T) {
c := ClientInfo{
ClientInfo: &livekit.ClientInfo{
Sdk: livekit.ClientInfo_GO,
},
}
require.False(t, c.SupportsICETCP())
})
t.Run("Swift SDK cannot support TCP before 1.0.5", func(t *testing.T) {
c := ClientInfo{
ClientInfo: &livekit.ClientInfo{
Sdk: livekit.ClientInfo_SWIFT,
Version: "1.0.4",
},
}
require.False(t, c.SupportsICETCP())
c.Version = "1.0.5"
require.True(t, c.SupportsICETCP())
})
}

207
pkg/rtc/config.go Normal file
View File

@@ -0,0 +1,207 @@
// 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 rtc
import (
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
act "github.com/livekit/livekit-server/pkg/sfu/rtpextension/abscapturetime"
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
)
const (
frameMarkingURI = "urn:ietf:params:rtp-hdrext:framemarking"
repairedRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
)
type WebRTCConfig struct {
rtcconfig.WebRTCConfig
BufferFactory *buffer.Factory
Receiver ReceiverConfig
Publisher DirectionConfig
Subscriber DirectionConfig
}
type ReceiverConfig struct {
PacketBufferSizeVideo int
PacketBufferSizeAudio int
}
type RTPHeaderExtensionConfig struct {
Audio []string
Video []string
}
type RTCPFeedbackConfig struct {
Audio []webrtc.RTCPFeedback
Video []webrtc.RTCPFeedback
}
type DirectionConfig struct {
RTPHeaderExtension RTPHeaderExtensionConfig
RTCPFeedback RTCPFeedbackConfig
}
func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
rtcConf := conf.RTC
webRTCConfig, err := rtcconfig.NewWebRTCConfig(&rtcConf.RTCConfig, conf.Development)
if err != nil {
return nil, err
}
// we don't want to use active TCP on a server, clients should be dialing
webRTCConfig.SettingEngine.DisableActiveTCP(true)
if rtcConf.PacketBufferSize == 0 {
rtcConf.PacketBufferSize = 500
}
if rtcConf.PacketBufferSizeVideo == 0 {
rtcConf.PacketBufferSizeVideo = rtcConf.PacketBufferSize
}
if rtcConf.PacketBufferSizeAudio == 0 {
rtcConf.PacketBufferSizeAudio = rtcConf.PacketBufferSize
}
return &WebRTCConfig{
WebRTCConfig: *webRTCConfig,
Receiver: ReceiverConfig{
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
},
Publisher: getPublisherConfig(false),
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
}, nil
}
func (c *WebRTCConfig) UpdatePublisherConfig(consolidated bool) {
c.Publisher = getPublisherConfig(consolidated)
}
func (c *WebRTCConfig) UpdateSubscriberConfig(ccConf config.CongestionControlConfig) {
c.Subscriber = getSubscriberConfig(ccConf.UseSendSideBWEInterceptor || ccConf.UseSendSideBWE)
}
func (c *WebRTCConfig) SetBufferFactory(factory *buffer.Factory) {
c.BufferFactory = factory
c.SettingEngine.BufferFactory = factory.GetOrNew
}
func getPublisherConfig(consolidated bool) DirectionConfig {
if consolidated {
return DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Audio: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.AudioLevelURI,
act.AbsCaptureTimeURI,
},
Video: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.TransportCCURI,
sdp.ABSSendTimeURI,
frameMarkingURI,
dd.ExtensionURI,
repairedRTPStreamIDURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
Audio: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBNACK},
},
Video: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBTransportCC},
{Type: webrtc.TypeRTCPFBGoogREMB},
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
},
},
}
}
return DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Audio: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.AudioLevelURI,
act.AbsCaptureTimeURI,
},
Video: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.TransportCCURI,
frameMarkingURI,
dd.ExtensionURI,
repairedRTPStreamIDURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
Audio: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBNACK},
},
Video: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBTransportCC},
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
},
},
}
}
func getSubscriberConfig(enableTWCC bool) DirectionConfig {
subscriberConfig := DirectionConfig{
RTPHeaderExtension: RTPHeaderExtensionConfig{
Video: []string{
dd.ExtensionURI,
act.AbsCaptureTimeURI,
},
Audio: []string{
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
Audio: []webrtc.RTCPFeedback{
// always enable NACK for audio but disable it later for red enabled transceiver. https://github.com/pion/webrtc/pull/2972
{Type: webrtc.TypeRTCPFBNACK},
},
Video: []webrtc.RTCPFeedback{
{Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"},
{Type: webrtc.TypeRTCPFBNACK},
{Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"},
},
},
}
if enableTWCC {
subscriberConfig.RTPHeaderExtension.Video = append(subscriberConfig.RTPHeaderExtension.Video, sdp.TransportCCURI)
subscriberConfig.RTCPFeedback.Video = append(subscriberConfig.RTCPFeedback.Video, webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBTransportCC})
} else {
subscriberConfig.RTPHeaderExtension.Video = append(subscriberConfig.RTPHeaderExtension.Video, sdp.ABSSendTimeURI)
subscriberConfig.RTCPFeedback.Video = append(subscriberConfig.RTCPFeedback.Video, webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBGoogREMB})
}
return subscriberConfig
}

137
pkg/rtc/datadowntrack.go Normal file
View File

@@ -0,0 +1,137 @@
// 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 rtc
import (
"fmt"
"sync"
"time"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
var _ types.DataDownTrack = (*DataDownTrack)(nil)
var _ types.DataTrackSender = (*DataDownTrack)(nil)
type DataDownTrackParams struct {
Logger logger.Logger
SubscriberID livekit.ParticipantID
PublishDataTrack types.DataTrack
Handle uint16
Transport types.DataTrackTransport
BytesTrackStats *BytesTrackStats
}
type DataDownTrack struct {
params DataDownTrackParams
logger logger.Logger
createdAt int64
lock sync.Mutex
closed bool
onClose func()
}
func NewDataDownTrack(params DataDownTrackParams) (*DataDownTrack, error) {
d := &DataDownTrack{
params: params,
createdAt: time.Now().UnixNano(),
}
d.logger = params.Logger.WithValues("name", d.Name(), "handle", d.Handle())
if err := d.params.PublishDataTrack.AddDataDownTrack(d); err != nil {
d.logger.Warnw("could not add data down track", err)
return nil, err
}
d.logger.Infow("created data down track")
return d, nil
}
func (d *DataDownTrack) Close() {
d.lock.Lock()
onClose := d.onClose
if d.closed {
d.lock.Unlock()
return
}
d.closed = true
d.lock.Unlock()
d.logger.Infow("closing data down track")
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.Stop()
}
d.params.PublishDataTrack.DeleteDataDownTrack(d.SubscriberID())
if onClose != nil {
onClose()
}
}
func (d *DataDownTrack) OnClose(fn func()) {
d.lock.Lock()
d.onClose = fn
closed := d.closed
d.lock.Unlock()
if closed && fn != nil {
fn()
}
}
func (d *DataDownTrack) Handle() uint16 {
return d.params.Handle
}
func (d *DataDownTrack) PublishDataTrack() types.DataTrack {
return d.params.PublishDataTrack
}
func (d *DataDownTrack) ID() livekit.TrackID {
return d.params.PublishDataTrack.ID()
}
func (d *DataDownTrack) Name() string {
return d.params.PublishDataTrack.Name()
}
func (d *DataDownTrack) SubscriberID() livekit.ParticipantID {
// add `createdAt` to ensure repeated subscriptions from same subscriber to same publisher does not collide
return livekit.ParticipantID(fmt.Sprintf("%s:%d", d.params.SubscriberID, d.createdAt))
}
func (d *DataDownTrack) WritePacket(data []byte, packet *datatrack.Packet, _arrivalTime int64) {
forwardedPacket := *packet
forwardedPacket.Handle = d.params.Handle
buf, err := forwardedPacket.Marshal()
if err != nil {
d.logger.Warnw("could not marshal data track message", err)
return
}
if err := d.params.Transport.SendDataTrackMessage(buf); err != nil {
d.logger.Warnw("could not send data track message", err)
return
}
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.AddBytes(uint64(len(buf)), true)
}
}
func (d *DataDownTrack) UpdateSubscriptionOptions(subscriptionOptions *livekit.DataTrackSubscriptionOptions) {
// DT-TODO
}

228
pkg/rtc/datatrack.go Normal file
View File

@@ -0,0 +1,228 @@
// 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 rtc
import (
"errors"
"slices"
"sync"
"github.com/frostbyte73/core"
"github.com/livekit/livekit-server/pkg/rtc/types"
sfuutils "github.com/livekit/livekit-server/pkg/sfu/utils"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
)
var (
errReceiverClosed = errors.New("datatrack is closed")
)
var _ types.DataTrack = (*DataTrack)(nil)
type DataTrackParams struct {
Logger logger.Logger
ParticipantID func() livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
BytesTrackStats *BytesTrackStats
}
type subscribedDataTrack struct {
subscriber types.LocalParticipant
dataDownTrack types.DataDownTrack
}
type DataTrack struct {
params DataTrackParams
logger logger.Logger
lock sync.Mutex
dti *livekit.DataTrackInfo
subscribedTracks map[livekit.ParticipantID]subscribedDataTrack
downTrackSpreader *sfuutils.DownTrackSpreader[types.DataTrackSender]
stats *dataTrackStats
closed core.Fuse
}
func NewDataTrack(params DataTrackParams, dti *livekit.DataTrackInfo) *DataTrack {
d := &DataTrack{
params: params,
dti: dti,
subscribedTracks: make(map[livekit.ParticipantID]subscribedDataTrack),
}
d.logger = params.Logger.WithValues("name", d.Name(), "handle", dti.PubHandle)
d.downTrackSpreader = sfuutils.NewDownTrackSpreader[types.DataTrackSender](sfuutils.DownTrackSpreaderParams{
Threshold: 20,
Logger: d.logger,
})
d.stats = newDataTrackStats(dataTrackStatsParams{Logger: d.logger})
d.logger.Infow("created data track", "dataTrackInfo", logger.Proto(d.dti))
return d
}
func (d *DataTrack) Close() {
d.logger.Infow("closing data track")
d.closed.Break()
d.stats.Close()
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.Stop()
}
}
func (d *DataTrack) PublisherID() livekit.ParticipantID {
return d.params.ParticipantID()
}
func (d *DataTrack) PublisherIdentity() livekit.ParticipantIdentity {
return d.params.ParticipantIdentity
}
func (d *DataTrack) ToProto() *livekit.DataTrackInfo {
return utils.CloneProto(d.dti)
}
func (d *DataTrack) PubHandle() uint16 {
return uint16(d.dti.PubHandle)
}
func (d *DataTrack) ID() livekit.TrackID {
return livekit.TrackID(d.dti.Sid)
}
func (d *DataTrack) Name() string {
return d.dti.Name
}
func (d *DataTrack) AddSubscriber(sub types.LocalParticipant) (types.DataDownTrack, error) {
d.lock.Lock()
defer d.lock.Unlock()
if _, ok := d.subscribedTracks[sub.ID()]; ok {
return nil, errAlreadySubscribed
}
bytesStats := NewBytesTrackStats(
sub.GetCountry(),
d.ID(),
sub.ID(),
sub.Kind(),
sub.KindDetails(),
sub.GetTelemetryListener,
sub.GetReporter(),
)
dataDownTrack, err := NewDataDownTrack(DataDownTrackParams{
Logger: sub.GetLogger().WithValues("trackID", d.ID()),
SubscriberID: sub.ID(),
PublishDataTrack: d,
Handle: sub.GetNextSubscribedDataTrackHandle(),
Transport: sub.GetDataTrackTransport(),
BytesTrackStats: bytesStats,
})
if err != nil {
bytesStats.Stop()
return nil, err
}
d.subscribedTracks[sub.ID()] = subscribedDataTrack{
subscriber: sub,
dataDownTrack: dataDownTrack,
}
return dataDownTrack, nil
}
func (d *DataTrack) RemoveSubscriber(subID livekit.ParticipantID) {
d.lock.Lock()
subscribedTrack, ok := d.subscribedTracks[subID]
delete(d.subscribedTracks, subID)
d.lock.Unlock()
if ok {
subscribedTrack.dataDownTrack.Close()
}
}
func (d *DataTrack) IsSubscriber(subID livekit.ParticipantID) bool {
d.lock.Lock()
defer d.lock.Unlock()
_, ok := d.subscribedTracks[subID]
return ok
}
func (d *DataTrack) RevokeDisallowedSubscribers(allowedSubscriberIdentities []livekit.ParticipantIdentity) []livekit.ParticipantIdentity {
var revokedSubscriberIdentities []livekit.ParticipantIdentity
d.lock.Lock()
disallowed := make(map[livekit.ParticipantID]livekit.ParticipantIdentity)
for subID, subscribedTrack := range d.subscribedTracks {
if IsParticipantExemptFromTrackPermissionsRestrictions(subscribedTrack.subscriber) {
continue
}
if !slices.Contains(allowedSubscriberIdentities, subscribedTrack.subscriber.Identity()) {
disallowed[subID] = subscribedTrack.subscriber.Identity()
}
}
d.lock.Unlock()
for subID, subIdentity := range disallowed {
d.logger.Infow("revoking data track subscription",
"subscriber", subIdentity,
"subscriberID", subID,
)
d.RemoveSubscriber(subID)
revokedSubscriberIdentities = append(revokedSubscriberIdentities, subIdentity)
}
return revokedSubscriberIdentities
}
func (d *DataTrack) AddDataDownTrack(dts types.DataTrackSender) error {
if d.closed.IsBroken() {
return errReceiverClosed
}
if d.downTrackSpreader.HasDownTrack(dts.SubscriberID()) {
d.logger.Infow("subscriberID already exists, replacing data downtrack", "subscriberID", dts.SubscriberID())
}
d.downTrackSpreader.Store(dts)
d.logger.Infow("data downtrack added", "subscriberID", dts.SubscriberID())
return nil
}
func (d *DataTrack) DeleteDataDownTrack(subscriberID livekit.ParticipantID) {
d.downTrackSpreader.Free(subscriberID)
d.logger.Infow("data downtrack deleted", "subscriberID", subscriberID)
}
func (d *DataTrack) HandlePacket(data []byte, packet *datatrack.Packet, arrivalTime int64) {
d.stats.Update(packet, arrivalTime, len(data))
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.AddBytes(uint64(len(data)), false)
}
d.downTrackSpreader.Broadcast(func(dts types.DataTrackSender) {
dts.WritePacket(data, packet, arrivalTime)
})
}

110
pkg/rtc/datatrack_stats.go Normal file
View File

@@ -0,0 +1,110 @@
// 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 rtc
import (
"sync"
"time"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
)
type dataTrackStatsParams struct {
Logger logger.Logger
}
type dataTrackStats struct {
params dataTrackStatsParams
lock sync.Mutex
startTime int64
endTime int64
highestSequenceNumber uint16
numPackets int
numPacketsLost int
numPacketsOutOfOrder int
numFrames int // count of `F` tagged packets, i. e. packets with final packet of frame marker
numBytes int
}
func newDataTrackStats(params dataTrackStatsParams) *dataTrackStats {
return &dataTrackStats{
params: params,
}
}
func (d *dataTrackStats) Update(packet *datatrack.Packet, arrivalTime int64, payloadLength int) {
d.lock.Lock()
defer d.lock.Unlock()
d.numBytes += payloadLength
if d.endTime != 0 {
return
}
if d.startTime == 0 {
d.startTime = arrivalTime
d.highestSequenceNumber = packet.SequenceNumber
d.numPackets = 1
} else {
diff := packet.SequenceNumber - d.highestSequenceNumber
switch {
case diff == 0: // duplicate
return
case diff > (1 << 15): // out of order
d.numPackets++
d.numPacketsOutOfOrder++
if d.numPacketsLost > 0 {
d.numPacketsLost--
}
default: // in order
d.numPackets++
d.numPacketsLost += int(diff) - 1
d.highestSequenceNumber = packet.SequenceNumber
}
}
if packet.IsFinalOfFrame {
d.numFrames++
}
}
func (d *dataTrackStats) Close() {
d.lock.Lock()
defer d.lock.Unlock()
d.endTime = mono.UnixNano()
if d.startTime != 0 {
duration := time.Duration(d.endTime - d.startTime).Seconds()
fps := float64(d.numFrames) / duration
d.params.Logger.Infow(
"data track stats",
"duration", duration,
"numPackets", d.numPackets,
"numPacketsLost", d.numPacketsLost,
"numPacketsOutOfOrder", d.numPacketsOutOfOrder,
"numFrames", d.numFrames,
"fps", fps,
"numBytes", d.numBytes,
)
}
}

71
pkg/rtc/datatrack_test.go Normal file
View File

@@ -0,0 +1,71 @@
// 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 rtc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
)
func newTestDataTrackSubscriber(id livekit.ParticipantID, identity livekit.ParticipantIdentity, isRecorder bool) *typesfakes.FakeLocalParticipant {
sub := &typesfakes.FakeLocalParticipant{}
sub.IDReturns(id)
sub.IdentityReturns(identity)
sub.IsRecorderReturns(isRecorder)
sub.GetLoggerReturns(logger.GetLogger())
sub.GetReporterReturns(roomobs.NewNoopParticipantSessionReporter())
sub.GetTelemetryListenerReturns(&typesfakes.FakeParticipantTelemetryListener{})
return sub
}
func TestDataTrackRevokeDisallowedSubscribers(t *testing.T) {
dt := NewDataTrack(
DataTrackParams{
Logger: logger.GetLogger(),
ParticipantID: func() livekit.ParticipantID { return "pubID" },
ParticipantIdentity: "pub",
},
&livekit.DataTrackInfo{
PubHandle: 1,
Sid: "DTR_test",
Name: "test",
},
)
defer dt.Close()
allowed := newTestDataTrackSubscriber("allowedID", "allowed", false)
disallowed := newTestDataTrackSubscriber("disallowedID", "disallowed", false)
recorder := newTestDataTrackSubscriber("recorderID", "recorder", true)
for _, sub := range []*typesfakes.FakeLocalParticipant{allowed, disallowed, recorder} {
_, err := dt.AddSubscriber(sub)
require.NoError(t, err)
require.True(t, dt.IsSubscriber(sub.ID()))
}
revoked := dt.RevokeDisallowedSubscribers([]livekit.ParticipantIdentity{"allowed"})
require.Equal(t, []livekit.ParticipantIdentity{"disallowed"}, revoked)
// disallowed subscriber is removed, allowed and permission exempt (recorder) subscribers are kept
require.True(t, dt.IsSubscriber(allowed.ID()))
require.False(t, dt.IsSubscriber(disallowed.ID()))
require.True(t, dt.IsSubscriber(recorder.ID()))
}

View File

@@ -0,0 +1,523 @@
// 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 dynacast
import (
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
type testDynacastManagerListener struct {
onSubscribedMaxQualityChange func(subscribedQualties []*livekit.SubscribedCodec)
onSubscribedAudioCodecChange func(subscribedCodecs []*livekit.SubscribedAudioCodec)
}
func (t *testDynacastManagerListener) OnDynacastSubscribedMaxQualityChange(
subscribedQualities []*livekit.SubscribedCodec,
_maxSubscribedQualities []types.SubscribedCodecQuality,
) {
t.onSubscribedMaxQualityChange(subscribedQualities)
}
func (t *testDynacastManagerListener) OnDynacastSubscribedAudioCodecChange(
codecs []*livekit.SubscribedAudioCodec,
) {
t.onSubscribedAudioCodecChange(codecs)
}
func TestSubscribedMaxQuality(t *testing.T) {
t.Run("subscribers muted", func(t *testing.T) {
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
// mute all subscribers of vp8
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
expectedSubscribedQualities := []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
})
t.Run("subscribers max quality", func(t *testing.T) {
lock := sync.RWMutex{}
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.(*dynacastManagerVideo).maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
mime.MimeTypeVP8: livekit.VideoQuality_LOW,
mime.MimeTypeAV1: livekit.VideoQuality_LOW,
}
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_MEDIUM)
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
expectedSubscribedQualities := []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// "s1" dropping to MEDIUM should disable HIGH layer
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_MEDIUM)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// "s1" , "s2" , "s3" dropping to LOW should disable HIGH & MEDIUM
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_LOW)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// muting "s2" only should not disable all qualities of vp8, no change of expected qualities
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
time.Sleep(100 * time.Millisecond)
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// muting "s1" and s3 also should disable all qualities
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_OFF)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// unmuting "s1" should enable vp8 previously set max quality
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// a higher quality from a different node should trigger that quality
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
{CodecMime: mime.MimeTypeVP8, Quality: livekit.VideoQuality_HIGH},
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_MEDIUM},
})
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
})
}
func TestCodecRegression(t *testing.T) {
t.Run("codec regression video", func(t *testing.T) {
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
expectedSubscribedQualities := []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
dm.HandleCodecRegression(mime.MimeTypeAV1, mime.MimeTypeVP8)
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
// av1 quality change should be forwarded to vp8
// av1 quality change of node should be ignored
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_HIGH},
})
expectedSubscribedQualities = []*livekit.SubscribedCodec{
{
Codec: mime.MimeTypeAV1.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
{
Codec: mime.MimeTypeVP8.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: true},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
})
t.Run("codec regression audio", func(t *testing.T) {
var lock sync.Mutex
actualSubscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0)
dm := NewDynacastManagerAudio(DynacastManagerAudioParams{
Listener: &testDynacastManagerListener{
onSubscribedAudioCodecChange: func(subscribedCodecs []*livekit.SubscribedAudioCodec) {
lock.Lock()
actualSubscribedCodecs = subscribedCodecs
lock.Unlock()
},
},
})
dm.NotifySubscription("s1", mime.MimeTypeRED, true)
expectedSubscribedCodecs := []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeRED.String(),
Enabled: true,
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
}, 10*time.Second, 100*time.Millisecond)
dm.HandleCodecRegression(mime.MimeTypeRED, mime.MimeTypeOpus)
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeRED.String(),
Enabled: false,
},
{
Codec: mime.MimeTypeOpus.String(),
Enabled: true,
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
}, 10*time.Second, 100*time.Millisecond)
// RED disable as subscriber or subscriber node should be ignored as it has been regressed
dm.NotifySubscription("s1", mime.MimeTypeRED, false)
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
{Codec: mime.MimeTypeRED.String(), Enabled: false},
})
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
}, 10*time.Second, 100*time.Millisecond)
// `s1` unsubscription should turn off `opus`
dm.NotifySubscription("s1", mime.MimeTypeOpus, false)
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeRED.String(),
Enabled: false,
},
{
Codec: mime.MimeTypeOpus.String(),
Enabled: false,
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
}, 10*time.Second, 100*time.Millisecond)
// a node subscription should turn `opus` back on
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeOpus.String(),
Enabled: true,
},
})
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeRED.String(),
Enabled: false,
},
{
Codec: mime.MimeTypeOpus.String(),
Enabled: true,
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
}, 10*time.Second, 100*time.Millisecond)
})
}
func subscribedCodecsAsString(c1 []*livekit.SubscribedCodec) string {
slices.SortFunc(c1, func(a, b *livekit.SubscribedCodec) int {
return strings.Compare(a.Codec, b.Codec)
})
var s1 strings.Builder
for _, c := range c1 {
s1.WriteString(c.String())
}
return s1.String()
}
func subscribedAudioCodecsAsString(c1 []*livekit.SubscribedAudioCodec) string {
slices.SortFunc(c1, func(a, b *livekit.SubscribedAudioCodec) int {
return strings.Compare(a.Codec, b.Codec)
})
var s1 strings.Builder
for _, c := range c1 {
s1.WriteString(c.String())
}
return s1.String()
}

View File

@@ -0,0 +1,198 @@
// 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 dynacast
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/codecs/mime"
)
var _ DynacastManager = (*dynacastManagerAudio)(nil)
var _ dynacastQualityListener = (*dynacastManagerAudio)(nil)
type DynacastManagerAudioParams struct {
Listener DynacastManagerListener
Logger logger.Logger
}
type dynacastManagerAudio struct {
params DynacastManagerAudioParams
subscribedCodecs map[mime.MimeType]bool
committedSubscribedCodecs map[mime.MimeType]bool
isClosed bool
*dynacastManagerBase
}
func NewDynacastManagerAudio(params DynacastManagerAudioParams) DynacastManager {
if params.Logger == nil {
params.Logger = logger.GetLogger()
}
d := &dynacastManagerAudio{
params: params,
subscribedCodecs: make(map[mime.MimeType]bool),
committedSubscribedCodecs: make(map[mime.MimeType]bool),
}
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
Logger: params.Logger,
OpsQueueDepth: 4,
OnRestart: func() {
d.committedSubscribedCodecs = make(map[mime.MimeType]bool)
},
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
dq := newDynacastQualityAudio(dynacastQualityAudioParams{
MimeType: mimeType,
Listener: d,
Logger: d.params.Logger,
})
return dq
},
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
d.subscribedCodecs[fromMime] = false
// if the new codec is not added, notify the publisher to start publishing
if _, ok := d.subscribedCodecs[toMime]; !ok {
d.subscribedCodecs[toMime] = true
}
},
OnUpdateNeeded: d.update,
})
return d
}
// It is possible for tracks to be in pending close state. When track
// is waiting to be closed, a node is not streaming a track. This can
// be used to force an update announcing that subscribed codec is disabled,
// i.e. indicating not pulling track any more.
func (d *dynacastManagerAudio) ForceEnable(enabled bool) {
d.lock.Lock()
defer d.lock.Unlock()
for mime := range d.committedSubscribedCodecs {
d.committedSubscribedCodecs[mime] = enabled
}
d.enqueueSubscribedChange()
}
func (d *dynacastManagerAudio) NotifySubscription(
subscriberID livekit.ParticipantID,
mime mime.MimeType,
enabled bool,
) {
dq := d.getOrCreateDynacastQuality(mime)
if dq != nil {
dq.NotifySubscription(subscriberID, enabled)
}
}
func (d *dynacastManagerAudio) NotifySubscriptionNode(
nodeID livekit.NodeID,
codecs []*livekit.SubscribedAudioCodec,
) {
for _, codec := range codecs {
dq := d.getOrCreateDynacastQuality(mime.NormalizeMimeType(codec.Codec))
if dq != nil {
dq.NotifySubscriptionNode(nodeID, codec.Enabled)
}
}
}
func (d *dynacastManagerAudio) OnUpdateAudioCodecForMime(mime mime.MimeType, enabled bool) {
d.lock.Lock()
if _, ok := d.regressedCodec[mime]; !ok {
d.subscribedCodecs[mime] = enabled
}
d.lock.Unlock()
d.update(false)
}
func (d *dynacastManagerAudio) update(force bool) {
d.lock.Lock()
d.params.Logger.Debugw(
"processing subscribed codec change",
"force", force,
"committedSubscribedCodecs", d.committedSubscribedCodecs,
"subscribedCodecs", d.subscribedCodecs,
)
if len(d.subscribedCodecs) == 0 {
// no mime has been added, nothing to update
d.lock.Unlock()
return
}
// add or remove of a mime triggers an update
changed := len(d.subscribedCodecs) != len(d.committedSubscribedCodecs)
if !changed {
for mime, enabled := range d.subscribedCodecs {
if ce, ok := d.committedSubscribedCodecs[mime]; ok {
if ce != enabled {
changed = true
break
}
}
}
}
if !force && !changed {
d.lock.Unlock()
return
}
d.params.Logger.Debugw(
"committing subscribed codec change",
"force", force,
"committedSubscribedCoecs", d.committedSubscribedCodecs,
"subscribedcodecs", d.subscribedCodecs,
)
// commit change
d.committedSubscribedCodecs = make(map[mime.MimeType]bool, len(d.subscribedCodecs))
for mime, enabled := range d.subscribedCodecs {
d.committedSubscribedCodecs[mime] = enabled
}
d.enqueueSubscribedChange()
d.lock.Unlock()
}
func (d *dynacastManagerAudio) enqueueSubscribedChange() {
if d.isClosed || d.params.Listener == nil {
return
}
subscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0, len(d.committedSubscribedCodecs))
for mime, enabled := range d.committedSubscribedCodecs {
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedAudioCodec{
Codec: mime.String(),
Enabled: enabled,
})
}
d.params.Logger.Debugw(
"subscribedAudioCodecChange",
"subscribedCodecs", logger.ProtoSlice(subscribedCodecs),
)
d.notifyOpsQueue.Enqueue(func() {
d.params.Listener.OnDynacastSubscribedAudioCodecChange(subscribedCodecs)
})
}

View File

@@ -0,0 +1,165 @@
// 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 dynacast
import (
"maps"
"slices"
"sync"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/utils"
)
type dynacastManagerBaseParams struct {
Logger logger.Logger
OpsQueueDepth uint
OnRestart func()
OnDynacastQualityCreate func(mimeType mime.MimeType) dynacastQuality
OnRegressCodec func(fromMime, toMime mime.MimeType)
OnUpdateNeeded func(force bool)
}
type dynacastManagerBase struct {
params dynacastManagerBaseParams
lock sync.RWMutex
regressedCodec map[mime.MimeType]struct{}
dynacastQuality map[mime.MimeType]dynacastQuality
notifyOpsQueue *utils.OpsQueue
isClosed bool
dynacastManagerNull
dynacastQualityListenerNull
}
func newDynacastManagerBase(params dynacastManagerBaseParams) *dynacastManagerBase {
if params.OpsQueueDepth == 0 {
params.OpsQueueDepth = 4
}
d := &dynacastManagerBase{
params: params,
regressedCodec: make(map[mime.MimeType]struct{}),
dynacastQuality: make(map[mime.MimeType]dynacastQuality),
notifyOpsQueue: utils.NewOpsQueue(utils.OpsQueueParams{
Name: "dynacast-notify",
MinSize: params.OpsQueueDepth,
FlushOnStop: true,
Logger: params.Logger,
}),
}
d.notifyOpsQueue.Start()
return d
}
func (d *dynacastManagerBase) AddCodec(mime mime.MimeType) {
d.getOrCreateDynacastQuality(mime)
}
func (d *dynacastManagerBase) HandleCodecRegression(fromMime, toMime mime.MimeType) {
fromDq := d.getOrCreateDynacastQuality(fromMime)
d.lock.Lock()
if d.isClosed {
d.lock.Unlock()
return
}
if fromDq == nil {
// should not happen as we have added the codec on setup receiver
d.params.Logger.Warnw("regression from codec not found", nil, "mime", fromMime, "toMime", toMime)
d.lock.Unlock()
return
}
d.regressedCodec[fromMime] = struct{}{}
d.params.OnRegressCodec(fromMime, toMime)
d.lock.Unlock()
d.params.OnUpdateNeeded(false)
fromDq.Stop()
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
}
func (d *dynacastManagerBase) Restart() {
d.lock.Lock()
d.params.OnRestart()
dqs := d.getDynacastQualitiesLocked()
d.lock.Unlock()
for _, dq := range dqs {
dq.Restart()
}
}
func (d *dynacastManagerBase) Close() {
d.notifyOpsQueue.Stop()
d.lock.Lock()
dqs := d.getDynacastQualitiesLocked()
d.dynacastQuality = make(map[mime.MimeType]dynacastQuality)
d.isClosed = true
d.lock.Unlock()
for _, dq := range dqs {
dq.Stop()
}
}
// There are situations like track unmute or streaming from a different node
// where subscription changes needs to sent to the provider immediately.
// This bypasses any debouncing and forces a subscription change update
// with immediate effect.
func (d *dynacastManagerBase) ForceUpdate() {
d.params.OnUpdateNeeded(true)
}
func (d *dynacastManagerBase) ClearSubscriberNodes() {
d.lock.Lock()
dqs := d.getDynacastQualitiesLocked()
d.lock.Unlock()
for _, dq := range dqs {
dq.ClearSubscriberNodes()
}
}
func (d *dynacastManagerBase) getOrCreateDynacastQuality(mimeType mime.MimeType) dynacastQuality {
d.lock.Lock()
defer d.lock.Unlock()
if d.isClosed || mimeType == mime.MimeTypeUnknown {
return nil
}
if dq := d.dynacastQuality[mimeType]; dq != nil {
return dq
}
dq := d.params.OnDynacastQualityCreate(mimeType)
dq.Start()
d.dynacastQuality[mimeType] = dq
return dq
}
func (d *dynacastManagerBase) getDynacastQualitiesLocked() []dynacastQuality {
return slices.Collect(maps.Values(d.dynacastQuality))
}

View File

@@ -0,0 +1,271 @@
// 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 dynacast
import (
"maps"
"time"
"github.com/bep/debounce"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
var _ DynacastManager = (*dynacastManagerVideo)(nil)
var _ dynacastQualityListener = (*dynacastManagerVideo)(nil)
type DynacastManagerVideoParams struct {
DynacastPauseDelay time.Duration
Listener DynacastManagerListener
Logger logger.Logger
}
type dynacastManagerVideo struct {
params DynacastManagerVideoParams
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
maxSubscribedQualityDebounce func(func())
maxSubscribedQualityDebouncePending bool
isClosed bool
*dynacastManagerBase
}
func NewDynacastManagerVideo(params DynacastManagerVideoParams) DynacastManager {
if params.Logger == nil {
params.Logger = logger.GetLogger()
}
d := &dynacastManagerVideo{
params: params,
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
}
if params.DynacastPauseDelay > 0 {
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
}
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
Logger: params.Logger,
OpsQueueDepth: 64,
OnRestart: func() {
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
},
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
dq := newDynacastQualityVideo(dynacastQualityVideoParams{
MimeType: mimeType,
Listener: d,
Logger: d.params.Logger,
})
return dq
},
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
d.maxSubscribedQuality[fromMime] = livekit.VideoQuality_OFF
// if the new codec is not added, notify the publisher to start publishing
if _, ok := d.maxSubscribedQuality[toMime]; !ok {
d.maxSubscribedQuality[toMime] = livekit.VideoQuality_HIGH
}
},
OnUpdateNeeded: d.update,
})
return d
}
// It is possible for tracks to be in pending close state. When track
// is waiting to be closed, a node is not streaming a track. This can
// be used to force an update announcing that subscribed quality is OFF,
// i.e. indicating not pulling track any more.
func (d *dynacastManagerVideo) ForceQuality(quality livekit.VideoQuality) {
d.lock.Lock()
defer d.lock.Unlock()
for mime := range d.committedMaxSubscribedQuality {
d.committedMaxSubscribedQuality[mime] = quality
}
d.enqueueSubscribedQualityChange()
}
func (d *dynacastManagerVideo) NotifySubscriberMaxQuality(
subscriberID livekit.ParticipantID,
mime mime.MimeType,
quality livekit.VideoQuality,
) {
dq := d.getOrCreateDynacastQuality(mime)
if dq != nil {
dq.NotifySubscriberMaxQuality(subscriberID, quality)
}
}
func (d *dynacastManagerVideo) NotifySubscriberNodeMaxQuality(
nodeID livekit.NodeID,
qualities []types.SubscribedCodecQuality,
) {
for _, quality := range qualities {
dq := d.getOrCreateDynacastQuality(quality.CodecMime)
if dq != nil {
dq.NotifySubscriberNodeMaxQuality(nodeID, quality.Quality)
}
}
}
func (d *dynacastManagerVideo) OnUpdateMaxQualityForMime(
mime mime.MimeType,
maxQuality livekit.VideoQuality,
) {
d.lock.Lock()
if _, ok := d.regressedCodec[mime]; !ok {
d.maxSubscribedQuality[mime] = maxQuality
}
d.lock.Unlock()
d.update(false)
}
func (d *dynacastManagerVideo) update(force bool) {
d.lock.Lock()
d.params.Logger.Debugw(
"processing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
if len(d.maxSubscribedQuality) == 0 {
// no mime has been added, nothing to update
d.lock.Unlock()
return
}
// add or remove of a mime triggers an update
changed := len(d.maxSubscribedQuality) != len(d.committedMaxSubscribedQuality)
downgradesOnly := !changed
if !changed {
for mime, quality := range d.maxSubscribedQuality {
if cq, ok := d.committedMaxSubscribedQuality[mime]; ok {
if cq != quality {
changed = true
}
if (cq == livekit.VideoQuality_OFF && quality != livekit.VideoQuality_OFF) || (cq != livekit.VideoQuality_OFF && quality != livekit.VideoQuality_OFF && cq < quality) {
downgradesOnly = false
}
}
}
}
if !force {
if !changed {
d.lock.Unlock()
return
}
if downgradesOnly && d.maxSubscribedQualityDebounce != nil {
if !d.maxSubscribedQualityDebouncePending {
d.params.Logger.Debugw(
"debouncing quality downgrade",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
d.maxSubscribedQualityDebounce(func() {
d.update(true)
})
d.maxSubscribedQualityDebouncePending = true
} else {
d.params.Logger.Debugw(
"quality downgrade waiting for debounce",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
}
d.lock.Unlock()
return
}
}
// clear debounce on send
if d.maxSubscribedQualityDebounce != nil {
d.maxSubscribedQualityDebounce(func() {})
d.maxSubscribedQualityDebouncePending = false
}
d.params.Logger.Debugw(
"committing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
// commit change
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality))
maps.Copy(d.committedMaxSubscribedQuality, d.maxSubscribedQuality)
d.enqueueSubscribedQualityChange()
d.lock.Unlock()
}
func (d *dynacastManagerVideo) enqueueSubscribedQualityChange() {
if d.isClosed || d.params.Listener == nil {
return
}
subscribedCodecs := make([]*livekit.SubscribedCodec, 0, len(d.committedMaxSubscribedQuality))
maxSubscribedQualities := make([]types.SubscribedCodecQuality, 0, len(d.committedMaxSubscribedQuality))
for mime, quality := range d.committedMaxSubscribedQuality {
maxSubscribedQualities = append(maxSubscribedQualities, types.SubscribedCodecQuality{
CodecMime: mime,
Quality: quality,
})
if quality == livekit.VideoQuality_OFF {
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedCodec{
Codec: mime.String(),
Qualities: []*livekit.SubscribedQuality{
{Quality: livekit.VideoQuality_LOW, Enabled: false},
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
},
})
} else {
var subscribedQualities []*livekit.SubscribedQuality
for q := livekit.VideoQuality_LOW; q <= livekit.VideoQuality_HIGH; q++ {
subscribedQualities = append(subscribedQualities, &livekit.SubscribedQuality{
Quality: q,
Enabled: q <= quality,
})
}
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedCodec{
Codec: mime.String(),
Qualities: subscribedQualities,
})
}
}
d.params.Logger.Debugw(
"subscribedMaxQualityChange",
"subscribedCodecs", subscribedCodecs,
"maxSubscribedQualities", maxSubscribedQualities,
)
d.notifyOpsQueue.Enqueue(func() {
d.params.Listener.OnDynacastSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
})
}

View File

@@ -0,0 +1,168 @@
// 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 dynacast
import (
"sync"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
var _ dynacastQuality = (*dynacastQualityAudio)(nil)
type dynacastQualityAudioParams struct {
MimeType mime.MimeType
Listener dynacastQualityListener
Logger logger.Logger
}
// dynacastQualityAudio manages enable a single receiver of a media track
type dynacastQualityAudio struct {
params dynacastQualityAudioParams
// quality level enable/disable
lock sync.RWMutex
initialized bool
subscriberEnables map[livekit.ParticipantID]bool
subscriberNodeEnables map[livekit.NodeID]bool
enabled bool
regressTo dynacastQuality
dynacastQualityNull
}
func newDynacastQualityAudio(params dynacastQualityAudioParams) dynacastQuality {
return &dynacastQualityAudio{
params: params,
subscriberEnables: make(map[livekit.ParticipantID]bool),
subscriberNodeEnables: make(map[livekit.NodeID]bool),
}
}
func (d *dynacastQualityAudio) Start() {
d.reset()
}
func (d *dynacastQualityAudio) Restart() {
d.reset()
}
func (d *dynacastQualityAudio) Stop() {
}
func (d *dynacastQualityAudio) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {
d.params.Logger.Debugw(
"setting subscriber codec enable",
"mime", d.params.MimeType,
"subscriberID", subscriberID,
"enabled", enabled,
)
d.lock.Lock()
if r := d.regressTo; r != nil {
d.lock.Unlock()
return
}
if !enabled {
delete(d.subscriberEnables, subscriberID)
} else {
d.subscriberEnables[subscriberID] = true
}
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *dynacastQualityAudio) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {
d.params.Logger.Debugw(
"setting subscriber node codec enabled",
"mime", d.params.MimeType,
"subscriberNodeID", nodeID,
"enabled", enabled,
)
d.lock.Lock()
if r := d.regressTo; r != nil {
// the downstream node will synthesize correct enable (its dynacast manager has codec regression), just ignore it
d.params.Logger.Debugw(
"ignoring node codec change, regressed to another dynacast quality",
"mime", d.params.MimeType,
"regressedMime", d.regressTo.Mime(),
)
d.lock.Unlock()
return
}
if !enabled {
delete(d.subscriberNodeEnables, nodeID)
} else {
d.subscriberNodeEnables[nodeID] = true
}
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *dynacastQualityAudio) ClearSubscriberNodes() {
d.lock.Lock()
d.subscriberNodeEnables = make(map[livekit.NodeID]bool)
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *dynacastQualityAudio) Mime() mime.MimeType {
return d.params.MimeType
}
func (d *dynacastQualityAudio) RegressTo(other dynacastQuality) {
d.lock.Lock()
d.regressTo = other
d.lock.Unlock()
other.Restart()
}
func (d *dynacastQualityAudio) reset() {
d.lock.Lock()
d.initialized = false
d.lock.Unlock()
}
func (d *dynacastQualityAudio) updateQualityChange(force bool) {
d.lock.Lock()
enabled := len(d.subscriberEnables) != 0 || len(d.subscriberNodeEnables) != 0
if enabled == d.enabled && d.initialized && !force {
d.lock.Unlock()
return
}
d.initialized = true
d.enabled = enabled
d.params.Logger.Debugw(
"notifying enabled change",
"mime", d.params.MimeType,
"enabled", d.enabled,
"subscriberNodeEnables", d.subscriberNodeEnables,
"subscribedEnables", d.subscriberEnables,
"force", force,
)
d.lock.Unlock()
d.params.Listener.OnUpdateAudioCodecForMime(d.params.MimeType, enabled)
}

View File

@@ -0,0 +1,249 @@
// 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 dynacast
import (
"sync"
"time"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
var _ dynacastQuality = (*dynacastQualityVideo)(nil)
const (
initialQualityUpdateWait = 10 * time.Second
)
type dynacastQualityVideoParams struct {
MimeType mime.MimeType
Listener dynacastQualityListener
Logger logger.Logger
}
// dynacastQualityVideo manages max subscribed quality of a single receiver of a media track
type dynacastQualityVideo struct {
params dynacastQualityVideoParams
// quality level enable/disable
lock sync.RWMutex
initialized bool
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality
maxSubscribedQuality livekit.VideoQuality
maxQualityTimer *time.Timer
regressTo dynacastQuality
dynacastQualityNull
}
func newDynacastQualityVideo(params dynacastQualityVideoParams) dynacastQuality {
return &dynacastQualityVideo{
params: params,
maxSubscriberQuality: make(map[livekit.ParticipantID]livekit.VideoQuality),
maxSubscriberNodeQuality: make(map[livekit.NodeID]livekit.VideoQuality),
}
}
func (d *dynacastQualityVideo) Start() {
d.reset()
}
func (d *dynacastQualityVideo) Restart() {
d.reset()
}
func (d *dynacastQualityVideo) Stop() {
d.stopMaxQualityTimer()
}
func (d *dynacastQualityVideo) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber max quality",
"mime", d.params.MimeType,
"subscriberID", subscriberID,
"quality", quality.String(),
)
d.lock.Lock()
if r := d.regressTo; r != nil {
d.lock.Unlock()
r.NotifySubscriberMaxQuality(subscriberID, quality)
return
}
if quality == livekit.VideoQuality_OFF {
delete(d.maxSubscriberQuality, subscriberID)
} else {
d.maxSubscriberQuality[subscriberID] = quality
}
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *dynacastQualityVideo) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber node max quality",
"mime", d.params.MimeType,
"subscriberNodeID", nodeID,
"quality", quality.String(),
)
d.lock.Lock()
if r := d.regressTo; r != nil {
// the downstream node will synthesize correct quality notify (its dynacast manager has codec regression), just ignore it
d.params.Logger.Debugw(
"ignoring node quality change, regressed to another dynacast quality",
"mime", d.params.MimeType,
"regressedMime", d.regressTo.Mime(),
)
d.lock.Unlock()
return
}
if quality == livekit.VideoQuality_OFF {
delete(d.maxSubscriberNodeQuality, nodeID)
} else {
d.maxSubscriberNodeQuality[nodeID] = quality
}
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *dynacastQualityVideo) ClearSubscriberNodes() {
d.lock.Lock()
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
d.lock.Unlock()
d.updateQualityChange(false)
}
func (d *dynacastQualityVideo) Mime() mime.MimeType {
return d.params.MimeType
}
func (d *dynacastQualityVideo) RegressTo(other dynacastQuality) {
d.lock.Lock()
d.regressTo = other
maxSubscriberQuality := d.maxSubscriberQuality
maxSubscriberNodeQuality := d.maxSubscriberNodeQuality
d.maxSubscriberQuality = make(map[livekit.ParticipantID]livekit.VideoQuality)
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
d.lock.Unlock()
other.Replace(maxSubscriberQuality, maxSubscriberNodeQuality)
}
func (d *dynacastQualityVideo) Replace(
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
) {
d.lock.Lock()
for subID, quality := range maxSubscriberQuality {
if oldQuality, ok := d.maxSubscriberQuality[subID]; ok {
// no QUALITY_OFF in the map
if quality > oldQuality {
d.maxSubscriberQuality[subID] = quality
}
} else {
d.maxSubscriberQuality[subID] = quality
}
}
for nodeID, quality := range maxSubscriberNodeQuality {
if oldQuality, ok := d.maxSubscriberNodeQuality[nodeID]; ok {
// no QUALITY_OFF in the map
if quality > oldQuality {
d.maxSubscriberNodeQuality[nodeID] = quality
}
} else {
d.maxSubscriberNodeQuality[nodeID] = quality
}
}
d.lock.Unlock()
d.Restart()
}
func (d *dynacastQualityVideo) reset() {
d.lock.Lock()
d.initialized = false
d.lock.Unlock()
d.startMaxQualityTimer()
}
func (d *dynacastQualityVideo) updateQualityChange(force bool) {
d.lock.Lock()
maxSubscribedQuality := livekit.VideoQuality_OFF
for _, subQuality := range d.maxSubscriberQuality {
if maxSubscribedQuality == livekit.VideoQuality_OFF || (subQuality != livekit.VideoQuality_OFF && subQuality > maxSubscribedQuality) {
maxSubscribedQuality = subQuality
}
}
for _, nodeQuality := range d.maxSubscriberNodeQuality {
if maxSubscribedQuality == livekit.VideoQuality_OFF || (nodeQuality != livekit.VideoQuality_OFF && nodeQuality > maxSubscribedQuality) {
maxSubscribedQuality = nodeQuality
}
}
if maxSubscribedQuality == d.maxSubscribedQuality && d.initialized && !force {
d.lock.Unlock()
return
}
d.initialized = true
d.maxSubscribedQuality = maxSubscribedQuality
d.params.Logger.Debugw(
"notifying quality change",
"mime", d.params.MimeType,
"maxSubscriberQuality", d.maxSubscriberQuality,
"maxSubscriberNodeQuality", d.maxSubscriberNodeQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
"force", force,
)
d.lock.Unlock()
d.params.Listener.OnUpdateMaxQualityForMime(d.params.MimeType, maxSubscribedQuality)
}
func (d *dynacastQualityVideo) startMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
if d.maxQualityTimer != nil {
d.maxQualityTimer.Stop()
d.maxQualityTimer = nil
}
d.maxQualityTimer = time.AfterFunc(initialQualityUpdateWait, func() {
d.stopMaxQualityTimer()
d.updateQualityChange(true)
})
}
func (d *dynacastQualityVideo) stopMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
if d.maxQualityTimer != nil {
d.maxQualityTimer.Stop()
d.maxQualityTimer = nil
}
}

View File

@@ -0,0 +1,185 @@
// 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 dynacast
import (
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
type DynacastManagerListener interface {
OnDynacastSubscribedMaxQualityChange(
subscribedQualities []*livekit.SubscribedCodec,
maxSubscribedQualities []types.SubscribedCodecQuality,
)
OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec)
}
var _ DynacastManagerListener = (*DynacastManagerListenerNull)(nil)
type DynacastManagerListenerNull struct {
}
func (d *DynacastManagerListenerNull) OnDynacastSubscribedMaxQualityChange(
subscribedQualities []*livekit.SubscribedCodec,
maxSubscribedQualities []types.SubscribedCodecQuality,
) {
}
func (d *DynacastManagerListenerNull) OnDynacastSubscribedAudioCodecChange(
codecs []*livekit.SubscribedAudioCodec,
) {
}
// -----------------------------------------
type DynacastManager interface {
AddCodec(mime mime.MimeType)
HandleCodecRegression(fromMime, toMime mime.MimeType)
Restart()
Close()
ForceUpdate()
ForceQuality(quality livekit.VideoQuality)
ForceEnable(enabled bool)
NotifySubscriberMaxQuality(
subscriberID livekit.ParticipantID,
mime mime.MimeType,
quality livekit.VideoQuality,
)
NotifySubscription(
subscriberID livekit.ParticipantID,
mime mime.MimeType,
enabled bool,
)
NotifySubscriberNodeMaxQuality(
nodeID livekit.NodeID,
qualities []types.SubscribedCodecQuality,
)
NotifySubscriptionNode(
nodeID livekit.NodeID,
codecs []*livekit.SubscribedAudioCodec,
)
ClearSubscriberNodes()
}
var _ DynacastManager = (*dynacastManagerNull)(nil)
type dynacastManagerNull struct {
}
func (d *dynacastManagerNull) AddCodec(mime mime.MimeType) {}
func (d *dynacastManagerNull) HandleCodecRegression(fromMime, toMime mime.MimeType) {}
func (d *dynacastManagerNull) Restart() {}
func (d *dynacastManagerNull) Close() {}
func (d *dynacastManagerNull) ForceUpdate() {}
func (d *dynacastManagerNull) ForceQuality(quality livekit.VideoQuality) {}
func (d *dynacastManagerNull) ForceEnable(enabled bool) {}
func (d *dynacastManagerNull) NotifySubscriberMaxQuality(
subscriberID livekit.ParticipantID,
mime mime.MimeType,
quality livekit.VideoQuality,
) {
}
func (d *dynacastManagerNull) NotifySubscription(
subscriberID livekit.ParticipantID,
mime mime.MimeType,
enabled bool,
) {
}
func (d *dynacastManagerNull) NotifySubscriberNodeMaxQuality(
nodeID livekit.NodeID,
qualities []types.SubscribedCodecQuality,
) {
}
func (d *dynacastManagerNull) NotifySubscriptionNode(
nodeID livekit.NodeID,
codecs []*livekit.SubscribedAudioCodec,
) {
}
func (d *dynacastManagerNull) ClearSubscriberNodes() {}
// ------------------------------------------------
type dynacastQualityListener interface {
OnUpdateMaxQualityForMime(mimeType mime.MimeType, maxQuality livekit.VideoQuality)
OnUpdateAudioCodecForMime(mimeType mime.MimeType, enabled bool)
}
var _ dynacastQualityListener = (*dynacastQualityListenerNull)(nil)
type dynacastQualityListenerNull struct {
}
func (d *dynacastQualityListenerNull) OnUpdateMaxQualityForMime(
mimeType mime.MimeType,
maxQuality livekit.VideoQuality,
) {
}
func (d *dynacastQualityListenerNull) OnUpdateAudioCodecForMime(
mimeType mime.MimeType,
enabled bool,
) {
}
// ------------------------------------------------
type dynacastQuality interface {
Start()
Restart()
Stop()
NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality)
NotifySubscription(subscriberID livekit.ParticipantID, enabled bool)
NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality)
NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool)
ClearSubscriberNodes()
Replace(
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
)
Mime() mime.MimeType
RegressTo(other dynacastQuality)
}
var _ dynacastQuality = (*dynacastQualityNull)(nil)
type dynacastQualityNull struct {
}
func (d *dynacastQualityNull) Start() {}
func (d *dynacastQualityNull) Restart() {}
func (d *dynacastQualityNull) Stop() {}
func (d *dynacastQualityNull) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
}
func (d *dynacastQualityNull) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {}
func (d *dynacastQualityNull) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
}
func (d *dynacastQualityNull) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {}
func (d *dynacastQualityNull) ClearSubscriberNodes() {}
func (d *dynacastQualityNull) Replace(
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
) {
}
func (d *dynacastQualityNull) Mime() mime.MimeType { return mime.MimeTypeUnknown }
func (d *dynacastQualityNull) RegressTo(other dynacastQuality) {}

176
pkg/rtc/egress.go Normal file
View File

@@ -0,0 +1,176 @@
// 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 rtc
import (
"context"
"errors"
"fmt"
"strings"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/webhook"
)
type EgressLauncher interface {
StartEgress(context.Context, *rpc.StartEgressRequest) (*livekit.EgressInfo, error)
StopEgress(context.Context, *livekit.StopEgressRequest) (*livekit.EgressInfo, error)
}
func StartParticipantEgress(
ctx context.Context,
launcher EgressLauncher,
ts telemetry.TelemetryService,
opts *livekit.AutoParticipantEgress,
identity livekit.ParticipantIdentity,
roomName livekit.RoomName,
roomID livekit.RoomID,
) error {
if req, err := startParticipantEgress(ctx, launcher, opts, identity, roomName, roomID); err != nil {
// send egress failed webhook
info := &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Participant{Participant: req},
}
ts.NotifyEgressEvent(ctx, webhook.EventEgressEnded, info)
return err
}
return nil
}
func startParticipantEgress(
ctx context.Context,
launcher EgressLauncher,
opts *livekit.AutoParticipantEgress,
identity livekit.ParticipantIdentity,
roomName livekit.RoomName,
roomID livekit.RoomID,
) (*livekit.ParticipantEgressRequest, error) {
req := &livekit.ParticipantEgressRequest{
RoomName: string(roomName),
Identity: string(identity),
FileOutputs: opts.FileOutputs,
SegmentOutputs: opts.SegmentOutputs,
}
switch o := opts.Options.(type) {
case *livekit.AutoParticipantEgress_Preset:
req.Options = &livekit.ParticipantEgressRequest_Preset{Preset: o.Preset}
case *livekit.AutoParticipantEgress_Advanced:
req.Options = &livekit.ParticipantEgressRequest_Advanced{Advanced: o.Advanced}
}
if launcher == nil {
return req, errors.New("egress launcher not found")
}
_, err := launcher.StartEgress(ctx, &rpc.StartEgressRequest{
Request: &rpc.StartEgressRequest_Participant{
Participant: req,
},
RoomId: string(roomID),
})
return req, err
}
func StartTrackEgress(
ctx context.Context,
launcher EgressLauncher,
ts telemetry.TelemetryService,
opts *livekit.AutoTrackEgress,
track types.MediaTrack,
roomName livekit.RoomName,
roomID livekit.RoomID,
) error {
if req, err := startTrackEgress(ctx, launcher, opts, track, roomName, roomID); err != nil {
// send egress failed webhook
info := &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Track{Track: req},
}
ts.NotifyEgressEvent(ctx, webhook.EventEgressEnded, info)
return err
}
return nil
}
func startTrackEgress(
ctx context.Context,
launcher EgressLauncher,
opts *livekit.AutoTrackEgress,
track types.MediaTrack,
roomName livekit.RoomName,
roomID livekit.RoomID,
) (*livekit.TrackEgressRequest, error) {
output := &livekit.DirectFileOutput{
Filepath: getFilePath(opts.Filepath),
}
switch out := opts.Output.(type) {
case *livekit.AutoTrackEgress_Azure:
output.Output = &livekit.DirectFileOutput_Azure{Azure: out.Azure}
case *livekit.AutoTrackEgress_Gcp:
output.Output = &livekit.DirectFileOutput_Gcp{Gcp: out.Gcp}
case *livekit.AutoTrackEgress_S3:
output.Output = &livekit.DirectFileOutput_S3{S3: out.S3}
}
req := &livekit.TrackEgressRequest{
RoomName: string(roomName),
TrackId: string(track.ID()),
Output: &livekit.TrackEgressRequest_File{
File: output,
},
}
if launcher == nil {
return req, errors.New("egress launcher not found")
}
_, err := launcher.StartEgress(ctx, &rpc.StartEgressRequest{
Request: &rpc.StartEgressRequest_Track{
Track: req,
},
RoomId: string(roomID),
})
return req, err
}
func getFilePath(filepath string) string {
if filepath == "" || strings.HasSuffix(filepath, "/") || strings.Contains(filepath, "{track_id}") {
return filepath
}
idx := strings.Index(filepath, ".")
if idx == -1 {
return fmt.Sprintf("%s-{track_id}", filepath)
} else {
return fmt.Sprintf("%s-%s%s", filepath[:idx], "{track_id}", filepath[idx:])
}
}

44
pkg/rtc/errors.go Normal file
View File

@@ -0,0 +1,44 @@
// 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 rtc
import (
"errors"
)
var (
ErrRoomClosed = errors.New("room has already closed")
ErrParticipantSessionClosed = errors.New("participant session is already closed")
ErrPermissionDenied = errors.New("no permissions to access the room")
ErrMaxParticipantsExceeded = errors.New("room has exceeded its max participants")
ErrLimitExceeded = errors.New("node has exceeded its configured limit")
ErrAlreadyJoined = errors.New("a participant with the same identity is already in the room")
ErrDataChannelUnavailable = errors.New("data channel is not available")
ErrDataChannelBufferFull = errors.New("data channel buffer is full")
ErrTransportFailure = errors.New("transport failure")
ErrEmptyIdentity = errors.New("participant identity cannot be empty")
ErrEmptyParticipantID = errors.New("participant ID cannot be empty")
ErrMissingGrants = errors.New("VideoGrant is missing")
ErrInternalError = errors.New("internal error")
// Track subscription related
ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track")
ErrNoSubscribePermission = errors.New("participant is not given permission to subscribe to tracks")
ErrTrackNotFound = errors.New("track cannot be found")
ErrTrackNotBound = errors.New("track not bound")
ErrSubscriptionLimitExceeded = errors.New("participant has exceeded its subscription limit")
ErrNoSubscribeMetricsPermission = errors.New("participant is not given permission to subscribe to metrics")
)

245
pkg/rtc/mediaengine.go Normal file
View File

@@ -0,0 +1,245 @@
// 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 rtc
import (
"fmt"
"strings"
"github.com/pion/webrtc/v4"
protoCodecs "github.com/livekit/protocol/codecs"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
type codecToRegister struct {
webrtc.RTPCodecParameters
strictFmtp bool
}
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
// audio codecs
if IsCodecEnabled(codecs, protoCodecs.OpusCodecParameters.RTPCodecCapability) {
cp := protoCodecs.OpusCodecParameters
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
if IsCodecEnabled(codecs, protoCodecs.RedCodecParameters.RTPCodecCapability) {
if err := me.RegisterCodec(protoCodecs.RedCodecParameters, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
}
for _, codec := range []webrtc.RTPCodecParameters{protoCodecs.PCMUCodecParameters, protoCodecs.PCMACodecParameters} {
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
continue
}
cp := codec
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Audio
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
// video codecs
rtxEnabled := IsCodecEnabled(codecs, protoCodecs.VideoRTXCodecParameters.RTPCodecCapability)
var codecsToRegister []codecToRegister
for _, codec := range protoCodecs.VideoCodecsParameters {
codecsToRegister = append(codecsToRegister, codecToRegister{RTPCodecParameters: codec, strictFmtp: false})
}
codecsToRegister = append(codecsToRegister,
codecToRegister{
RTPCodecParameters: protoCodecs.H264ProfileLevelId42001fPacketizationMode0CodecParameters,
strictFmtp: true,
},
codecToRegister{
RTPCodecParameters: protoCodecs.H264ProfileLevelId42001fPacketizationMode1CodecParameters,
strictFmtp: true,
},
codecToRegister{
RTPCodecParameters: protoCodecs.H264ProfileLevelId4d001fPacketizationMode0CodecParameters,
strictFmtp: true,
},
codecToRegister{
RTPCodecParameters: protoCodecs.H264ProfileLevelId4d001fPacketizationMode1CodecParameters,
strictFmtp: true,
},
)
for _, codec := range codecsToRegister {
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == protoCodecs.H264HighProfileFmtp {
continue
}
if mime.IsMimeTypeStringRTX(codec.MimeType) {
continue
}
if !isCodecEnabledWithFmtp(codecs, codec.RTPCodecCapability, codec.strictFmtp) {
continue
}
cp := codec
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Video
if err := me.RegisterCodec(cp.RTPCodecParameters, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
if !rtxEnabled {
continue
}
cpRtx := protoCodecs.VideoRTXCodecParameters
cpRtx.RTPCodecCapability.SDPFmtpLine = fmt.Sprintf("apt=%d", codec.PayloadType)
cpRtx.PayloadType = codec.PayloadType + 1
if err := me.RegisterCodec(cpRtx, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
return nil
}
func registerHeaderExtensions(me *webrtc.MediaEngine, rtpHeaderExtension RTPHeaderExtensionConfig) error {
for _, extension := range rtpHeaderExtension.Video {
if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
for _, extension := range rtpHeaderExtension.Audio {
if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
return nil
}
func createMediaEngine(codecs []*livekit.Codec, config DirectionConfig, filterOutH264HighProfile bool) (*webrtc.MediaEngine, error) {
me := &webrtc.MediaEngine{}
if err := registerCodecs(me, codecs, config.RTCPFeedback, filterOutH264HighProfile); err != nil {
return nil, err
}
if err := registerHeaderExtensions(me, config.RTPHeaderExtension); err != nil {
return nil, err
}
return me, nil
}
func IsCodecEnabled(codecs []*livekit.Codec, cap webrtc.RTPCodecCapability) bool {
return isCodecEnabledWithFmtp(codecs, cap, false)
}
func isCodecEnabledWithFmtp(codecs []*livekit.Codec, cap webrtc.RTPCodecCapability, strictFmtp bool) bool {
for _, codec := range codecs {
if !mime.IsMimeTypeStringEqual(codec.Mime, cap.MimeType) {
continue
}
if (!strictFmtp && codec.FmtpLine == "") || strings.EqualFold(codec.FmtpLine, cap.SDPFmtpLine) {
return true
}
}
return false
}
func selectAlternativeVideoCodec(enabledCodecs []*livekit.Codec) string {
for _, c := range enabledCodecs {
if mime.IsMimeTypeStringVideo(c.Mime) {
return c.Mime
}
}
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
return mime.MimeTypeVP8.String()
}
func selectAlternativeAudioCodec(enabledCodecs []*livekit.Codec) string {
for _, c := range enabledCodecs {
if mime.IsMimeTypeStringAudio(c.Mime) {
return c.Mime
}
}
// no viable codec in the list of enabled codecs, fall back to the most widely supported codec
return mime.MimeTypeOpus.String()
}
// mergeCodecsByMime returns a union b, deduplicated by mime type.
func mergeCodecsByMime(a, b []*livekit.Codec) []*livekit.Codec {
merged := make([]*livekit.Codec, 0, len(a)+len(b))
merged = append(merged, a...)
for _, c := range b {
seen := false
for _, existing := range a {
if mime.IsMimeTypeStringEqual(c.Mime, existing.Mime) {
seen = true
break
}
}
if !seen {
merged = append(merged, c)
}
}
return merged
}
func filterCodecs(
codecs []webrtc.RTPCodecParameters,
enabledCodecs []*livekit.Codec,
rtcpFeedbackConfig RTCPFeedbackConfig,
filterOutH264HighProfile bool,
) []webrtc.RTPCodecParameters {
filteredCodecs := make([]webrtc.RTPCodecParameters, 0, len(codecs))
for _, c := range codecs {
if filterOutH264HighProfile && isH264HighProfile(c.RTPCodecCapability.SDPFmtpLine) {
continue
}
for _, enabledCodec := range enabledCodecs {
if mime.NormalizeMimeType(enabledCodec.Mime) == mime.NormalizeMimeType(c.RTPCodecCapability.MimeType) {
if !mime.IsMimeTypeStringEqual(c.RTPCodecCapability.MimeType, mime.MimeTypeRTX.String()) {
if mime.IsMimeTypeStringVideo(c.RTPCodecCapability.MimeType) {
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Video
} else {
c.RTPCodecCapability.RTCPFeedback = rtcpFeedbackConfig.Audio
}
}
filteredCodecs = append(filteredCodecs, c)
break
}
}
}
return filteredCodecs
}
func isH264HighProfile(fmtp string) bool {
params := strings.Split(fmtp, ";")
for _, param := range params {
parts := strings.Split(param, "=")
if len(parts) == 2 {
if parts[0] == "profile-level-id" {
// https://datatracker.ietf.org/doc/html/rfc6184#section-8.1
// hex value 0x64 for profile_idc is high profile
return strings.HasPrefix(parts[1], "64")
}
}
}
return false
}

224
pkg/rtc/mediaengine_test.go Normal file
View File

@@ -0,0 +1,224 @@
// 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 rtc
import (
"strconv"
"strings"
"testing"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/config"
)
const (
h264MainProfilePacketizationMode0Fmtp = "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f"
h264MainProfilePacketizationMode1Fmtp = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f"
h264BaselineProfilePacketizationMode0Fmtp = "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f"
h264BaselineProfilePacketizationMode1Fmtp = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"
)
func TestIsCodecEnabled(t *testing.T) {
t.Run("empty fmtp requirement should match all", func(t *testing.T) {
enabledCodecs := []*livekit.Codec{{Mime: "video/h264"}}
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String(), SDPFmtpLine: "special"}))
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String()}))
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}))
})
t.Run("when fmtp is provided, require match", func(t *testing.T) {
enabledCodecs := []*livekit.Codec{{Mime: "video/h264", FmtpLine: "special"}}
require.True(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String(), SDPFmtpLine: "special"}))
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeH264.String()}))
require.False(t, IsCodecEnabled(enabledCodecs, webrtc.RTPCodecCapability{MimeType: mime.MimeTypeVP8.String()}))
})
t.Run("strict fmtp requires an exact fmtp, mime alone is not enough", func(t *testing.T) {
cap := webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
SDPFmtpLine: h264MainProfilePacketizationMode1Fmtp,
}
// mime-only config opts into every non-strict codec, but never a strict one
mimeOnly := []*livekit.Codec{{Mime: "video/h264"}}
require.True(t, isCodecEnabledWithFmtp(mimeOnly, cap, false))
require.False(t, isCodecEnabledWithFmtp(mimeOnly, cap, true))
// spelling the fmtp out enables it
explicit := []*livekit.Codec{{Mime: "video/h264", FmtpLine: h264MainProfilePacketizationMode1Fmtp}}
require.True(t, isCodecEnabledWithFmtp(explicit, cap, true))
// ...but only for the profile that was asked for
require.False(t, isCodecEnabledWithFmtp(explicit, webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
SDPFmtpLine: h264MainProfilePacketizationMode0Fmtp,
}, true))
})
}
type offeredCodec struct {
payloadType webrtc.PayloadType
name string
fmtp string
}
func (o offeredCodec) String() string {
return strconv.Itoa(int(o.payloadType)) + " " + o.name + " " + o.fmtp
}
// offeredVideoCodecs returns the video codecs an offer built from enabledCodecs carries.
// It goes through a real offer rather than inspecting the MediaEngine so that payload
// type assignment, which is what collides, is covered too.
func offeredVideoCodecs(t *testing.T, enabledCodecs []*livekit.Codec, filterOutH264HighProfile bool) []offeredCodec {
t.Helper()
me, err := createMediaEngine(enabledCodecs, DirectionConfig{}, filterOutH264HighProfile)
require.NoError(t, err)
pc, err := webrtc.NewAPI(webrtc.WithMediaEngine(me)).NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
_, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
var codecs []offeredCodec
indexByPayloadType := make(map[webrtc.PayloadType]int)
for _, line := range strings.Split(offer.SDP, "\r\n") {
attr, value, found := strings.Cut(line, ":")
if !found || (attr != "a=rtpmap" && attr != "a=fmtp") {
continue
}
rawPayloadType, rest, _ := strings.Cut(value, " ")
payloadType, err := strconv.Atoi(rawPayloadType)
require.NoError(t, err)
if attr == "a=rtpmap" {
indexByPayloadType[webrtc.PayloadType(payloadType)] = len(codecs)
codecs = append(codecs, offeredCodec{payloadType: webrtc.PayloadType(payloadType), name: rest})
continue
}
i, ok := indexByPayloadType[webrtc.PayloadType(payloadType)]
require.True(t, ok, "fmtp for payload type %d without an rtpmap", payloadType)
codecs[i].fmtp = rest
}
require.NotEmpty(t, codecs)
return codecs
}
func hasOfferedFmtp(codecs []offeredCodec, mimeType mime.MimeType, fmtp string) bool {
for _, c := range codecs {
name, _, _ := strings.Cut(c.name, "/")
if mime.NormalizeMimeType("video/"+name) == mimeType && c.fmtp == fmtp {
return true
}
}
return false
}
// defaultEnabledCodecs mirrors what roomallocator derives from config, so these tests
// track the shipped default codec set instead of a hand-maintained copy of it.
func defaultEnabledCodecs() []*livekit.Codec {
codecs := make([]*livekit.Codec, 0, len(config.DefaultConfig.Room.EnabledCodecs))
for _, c := range config.DefaultConfig.Room.EnabledCodecs {
codecs = append(codecs, &livekit.Codec{Mime: c.Mime, FmtpLine: c.FmtpLine})
}
return codecs
}
func TestRegisterCodecsH264Profiles(t *testing.T) {
t.Run("constrained baseline are offered by default", func(t *testing.T) {
codecs := offeredVideoCodecs(t, defaultEnabledCodecs(), false)
for _, fmtp := range []string{
"level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032",
} {
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, fmtp), "missing %q in %v", fmtp, codecs)
}
})
t.Run("main/baseline profile is not offered unless explicitly enabled", func(t *testing.T) {
codecs := offeredVideoCodecs(t, defaultEnabledCodecs(), false)
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264MainProfilePacketizationMode0Fmtp), "%v", codecs)
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264MainProfilePacketizationMode1Fmtp), "%v", codecs)
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264BaselineProfilePacketizationMode0Fmtp), "%v", codecs)
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, h264BaselineProfilePacketizationMode1Fmtp), "%v", codecs)
})
t.Run("main/baseline profile is offered when explicitly enabled", func(t *testing.T) {
for _, fmtp := range []string{
h264MainProfilePacketizationMode0Fmtp,
h264MainProfilePacketizationMode1Fmtp,
h264BaselineProfilePacketizationMode0Fmtp,
h264BaselineProfilePacketizationMode1Fmtp,
} {
t.Run(fmtp, func(t *testing.T) {
enabled := append(defaultEnabledCodecs(), &livekit.Codec{
Mime: mime.MimeTypeH264.String(),
FmtpLine: fmtp,
})
codecs := offeredVideoCodecs(t, enabled, false)
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, fmtp), "missing %q in %v", fmtp, codecs)
})
}
})
t.Run("high profile is filtered out for the answerer", func(t *testing.T) {
highProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
codecs := offeredVideoCodecs(t, defaultEnabledCodecs(), true)
require.False(t, hasOfferedFmtp(codecs, mime.MimeTypeH264, highProfileFmtp), "%v", codecs)
// the other profiles survive the filter
require.True(t, hasOfferedFmtp(codecs, mime.MimeTypeH264,
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f"), "%v", codecs)
})
}
// Every video codec claims its own payload type plus the next one for its RTX stream,
// so a codec whose payload type lands on another's RTX slot makes pion refuse to build
// the MediaEngine at all.
func TestRegisterCodecsPayloadTypesAreUnique(t *testing.T) {
enabled := append(defaultEnabledCodecs(),
&livekit.Codec{Mime: mime.MimeTypeH264.String(), FmtpLine: h264MainProfilePacketizationMode0Fmtp},
&livekit.Codec{Mime: mime.MimeTypeH264.String(), FmtpLine: h264MainProfilePacketizationMode1Fmtp},
)
for _, filterOutH264HighProfile := range []bool{false, true} {
codecs := offeredVideoCodecs(t, enabled, filterOutH264HighProfile)
seen := make(map[webrtc.PayloadType]offeredCodec, len(codecs))
for _, c := range codecs {
previous, duplicate := seen[c.payloadType]
require.False(t, duplicate, "payload type %d claimed by both %v and %v", c.payloadType, previous, c)
seen[c.payloadType] = c
}
}
}

105
pkg/rtc/medialossproxy.go Normal file
View File

@@ -0,0 +1,105 @@
// 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 rtc
import (
"sync"
"time"
"github.com/pion/rtcp"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/sfu"
)
const (
downLostUpdateDelta = time.Second
)
type MediaLossProxyParams struct {
Logger logger.Logger
}
type MediaLossProxy struct {
params MediaLossProxyParams
lock sync.Mutex
maxDownFracLost uint8
maxDownFracLostTs time.Time
maxDownFracLostValid bool
onMediaLossUpdate func(fractionalLoss uint8)
}
func NewMediaLossProxy(params MediaLossProxyParams) *MediaLossProxy {
return &MediaLossProxy{params: params}
}
func (m *MediaLossProxy) OnMediaLossUpdate(f func(fractionalLoss uint8)) {
m.lock.Lock()
m.onMediaLossUpdate = f
m.lock.Unlock()
}
func (m *MediaLossProxy) HandleMaxLossFeedback(_ *sfu.DownTrack, report *rtcp.ReceiverReport) {
m.lock.Lock()
for _, rr := range report.Reports {
m.maxDownFracLostValid = true
if m.maxDownFracLost < rr.FractionLost {
m.maxDownFracLost = rr.FractionLost
}
}
m.lock.Unlock()
m.maybeUpdateLoss()
}
func (m *MediaLossProxy) NotifySubscriberNodeMediaLoss(_nodeID livekit.NodeID, fractionalLoss uint8) {
m.lock.Lock()
m.maxDownFracLostValid = true
if m.maxDownFracLost < fractionalLoss {
m.maxDownFracLost = fractionalLoss
}
m.lock.Unlock()
m.maybeUpdateLoss()
}
func (m *MediaLossProxy) maybeUpdateLoss() {
var (
shouldUpdate bool
maxLost uint8
)
m.lock.Lock()
now := time.Now()
if now.Sub(m.maxDownFracLostTs) > downLostUpdateDelta && m.maxDownFracLostValid {
shouldUpdate = true
maxLost = m.maxDownFracLost
m.maxDownFracLost = 0
m.maxDownFracLostTs = now
m.maxDownFracLostValid = false
}
onMediaLossUpdate := m.onMediaLossUpdate
m.lock.Unlock()
if shouldUpdate {
if onMediaLossUpdate != nil {
onMediaLossUpdate(maxLost)
}
}
}

701
pkg/rtc/mediatrack.go Normal file
View File

@@ -0,0 +1,701 @@
// 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 rtc
import (
"math"
"sync"
"time"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/utils/mono"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/rtc/dynacast"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/connectionquality"
"github.com/livekit/livekit-server/pkg/sfu/interceptor"
"github.com/livekit/livekit-server/pkg/telemetry"
util "github.com/livekit/mediatransportutil"
)
var _ types.LocalMediaTrack = (*MediaTrack)(nil)
// MediaTrack represents a WebRTC track that needs to be forwarded
// Implements MediaTrack and PublishedTrack interface
type MediaTrack struct {
params MediaTrackParams
buffer atomic.Pointer[buffer.Buffer]
everSubscribed atomic.Bool
*MediaTrackReceiver
*MediaLossProxy
dynacastManager dynacast.DynacastManager
// serialises receiver creation in AddReceiver, i. e. the check-then-create
// of a receiver for a mime type. Held across receiver set up
addReceiverLock sync.Mutex
regressionTargetCodecReceived atomic.Bool
migrated atomic.Bool
published atomic.Bool
rttFromXR atomic.Bool
backupCodecPolicy livekit.BackupCodecPolicy
// immutable after construction
regressionTargetCodec mime.MimeType
}
type MediaTrackParams struct {
ParticipantID func() livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
ParticipantCountry string
ParticipantKind livekit.ParticipantInfo_Kind
ParticipantKindDetails []livekit.ParticipantInfo_KindDetail
BufferFactory *buffer.Factory
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
PLIThrottleConfig sfu.PLIThrottleConfig
AudioConfig sfu.AudioConfig
VideoConfig config.VideoConfig
TelemetryListener func() types.ParticipantTelemetryListener
Logger logger.Logger
Reporter roomobs.TrackReporter
SimTracks map[uint32]interceptor.SimulcastTrackInfo
OnRTCP func([]rtcp.Packet)
ForwardStats *sfu.ForwardStats
OnTrackEverSubscribed func(livekit.TrackID)
ShouldRegressCodec func() bool
PreferVideoSizeFromMedia bool
EnableRTPStreamRestartDetection bool
UpdateTrackInfoByVideoSizeChange bool
ForceBackupCodecPolicySimulcast bool
OnSubscribedMaxQualityChange func(
trackID livekit.TrackID,
trackInfo *livekit.TrackInfo,
subscribedQualities []*livekit.SubscribedCodec,
maxSubscribedQualities []types.SubscribedCodecQuality,
) error
OnSubscribedAudioCodecChange func(
trackID livekit.TrackID,
codecs []*livekit.SubscribedAudioCodec,
) error
}
func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
t := &MediaTrack{
params: params,
backupCodecPolicy: ti.BackupCodecPolicy,
}
if t.params.ForceBackupCodecPolicySimulcast {
t.backupCodecPolicy = livekit.BackupCodecPolicy_SIMULCAST
}
if t.backupCodecPolicy != livekit.BackupCodecPolicy_SIMULCAST && len(ti.Codecs) > 1 {
t.regressionTargetCodec = mime.NormalizeMimeType(ti.Codecs[1].MimeType)
t.params.Logger.Debugw("track enabled codec regression", "regressionCodec", t.regressionTargetCodec)
}
t.MediaTrackReceiver = NewMediaTrackReceiver(MediaTrackReceiverParams{
MediaTrack: t,
IsRelayed: false,
ParticipantID: params.ParticipantID,
ParticipantIdentity: params.ParticipantIdentity,
ParticipantVersion: params.ParticipantVersion,
ReceiverConfig: params.ReceiverConfig,
SubscriberConfig: params.SubscriberConfig,
AudioConfig: params.AudioConfig,
TelemetryListener: params.TelemetryListener,
Logger: params.Logger,
RegressionTargetCodec: t.regressionTargetCodec,
PreferVideoSizeFromMedia: params.PreferVideoSizeFromMedia,
}, ti)
if ti.Type == livekit.TrackType_AUDIO {
t.MediaLossProxy = NewMediaLossProxy(MediaLossProxyParams{
Logger: params.Logger,
})
t.MediaLossProxy.OnMediaLossUpdate(func(fractionalLoss uint8) {
if buff := t.buffer.Load(); buff != nil {
buff.SetLastFractionLostReport(fractionalLoss)
}
})
t.MediaTrackReceiver.OnMediaLossFeedback(t.MediaLossProxy.HandleMaxLossFeedback)
}
switch ti.Type {
case livekit.TrackType_VIDEO:
t.dynacastManager = dynacast.NewDynacastManagerVideo(dynacast.DynacastManagerVideoParams{
DynacastPauseDelay: params.VideoConfig.DynacastPauseDelay,
Listener: t,
Logger: params.Logger,
})
case livekit.TrackType_AUDIO:
if len(ti.Codecs) > 1 {
t.dynacastManager = dynacast.NewDynacastManagerAudio(dynacast.DynacastManagerAudioParams{
Listener: t,
Logger: params.Logger,
})
}
}
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
if t.dynacastManager != nil {
t.dynacastManager.AddCodec(mime)
}
})
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriberMaxQuality(
subscriberID,
mimeType,
buffer.GetVideoQualityForSpatialLayer(
mimeType,
layer,
t.MediaTrackReceiver.TrackInfo(),
),
)
}
},
)
t.MediaTrackReceiver.OnSubscriberAudioCodecChange(
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, enabled bool) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscription(subscriberID, mimeType, enabled)
}
},
)
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
if t.dynacastManager != nil {
t.dynacastManager.HandleCodecRegression(
mime.NormalizeMimeType(old.MimeType),
mime.NormalizeMimeType(new.MimeType),
)
}
})
return t
}
func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriberNodeMaxQuality(nodeID, qualities)
}
}
func (t *MediaTrack) NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriptionNode(nodeID, codecs)
}
}
func (t *MediaTrack) ClearSubscriberNodes() {
if t.dynacastManager != nil {
t.dynacastManager.ClearSubscriberNodes()
}
}
func (t *MediaTrack) HasSignalCid(cid string) bool {
if cid != "" {
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if c.Cid == cid {
return true
}
}
}
return false
}
func (t *MediaTrack) HasSdpCid(cid string) bool {
if cid != "" {
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if c.Cid == cid || c.SdpCid == cid {
return true
}
}
}
return false
}
func (t *MediaTrack) GetMimeTypeForSdpCid(cid string) mime.MimeType {
if cid != "" {
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if c.Cid == cid || c.SdpCid == cid {
return mime.NormalizeMimeType(c.MimeType)
}
}
}
return mime.MimeTypeUnknown
}
func (t *MediaTrack) GetCidsForMimeType(mimeType mime.MimeType) (string, string) {
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if mime.NormalizeMimeType(c.MimeType) == mimeType {
return c.Cid, c.SdpCid
}
}
return "", ""
}
func (t *MediaTrack) ToProto() *livekit.TrackInfo {
return t.MediaTrackReceiver.TrackInfoClone()
}
// AddReceiver adds a new RTP receiver to the track, returns true when receiver represents a new codec
// and if a receiver was added successfully
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) (bool, bool) {
var newCodec bool
ssrc := uint32(track.SSRC())
buff, rtcpReader := t.params.BufferFactory.GetBufferPair(ssrc)
if buff == nil || rtcpReader == nil {
t.params.Logger.Errorw("could not retrieve buffer pair", nil)
return newCodec, false
}
var lastRR uint32
rtcpReader.OnPacket(func(bytes []byte) {
pkts, err := rtcp.Unmarshal(bytes)
if err != nil {
t.params.Logger.Errorw("could not unmarshal RTCP", err, "size", len(bytes))
return
}
for _, pkt := range pkts {
switch pkt := pkt.(type) {
case *rtcp.SourceDescription:
case *rtcp.SenderReport:
if pkt.SSRC == uint32(track.SSRC()) {
buff.SetSenderReportData(&livekit.RTCPSenderReportState{
RtpTimestamp: pkt.RTPTime,
NtpTimestamp: pkt.NTPTime,
Packets: pkt.PacketCount,
Octets: uint64(pkt.OctetCount),
At: mono.UnixNano(),
})
}
case *rtcp.ExtendedReport:
rttFromXR:
for _, report := range pkt.Reports {
if rr, ok := report.(*rtcp.DLRRReportBlock); ok {
for _, dlrrReport := range rr.Reports {
if dlrrReport.LastRR <= lastRR {
continue
}
nowNTP := util.ToNtpTime(time.Now())
nowNTP32 := uint32(nowNTP >> 16)
ntpDiff := nowNTP32 - dlrrReport.LastRR - dlrrReport.DLRR
rtt := uint32(math.Ceil(float64(ntpDiff) * 1000.0 / 65536.0))
buff.SetRTT(rtt)
t.rttFromXR.Store(true)
lastRR = dlrrReport.LastRR
break rttFromXR
}
}
}
}
}
})
ti := t.MediaTrackReceiver.TrackInfoClone()
var regressCodec bool
enableRegression := t.enableRegression()
t.addReceiverLock.Lock()
mimeType := mime.NormalizeMimeType(track.Codec().MimeType)
layer := buffer.GetSpatialLayerForRid(mimeType, track.RID(), ti)
if layer < 0 {
t.params.Logger.Warnw(
"AddReceiver failed due to negative layer", nil,
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"codec", track.Codec(),
"trackInfo", logger.Proto(ti),
)
t.addReceiverLock.Unlock()
return newCodec, false
}
t.params.Logger.Debugw(
"AddReceiver",
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"codec", track.Codec(),
"trackInfo", logger.Proto(ti),
)
wr := t.MediaTrackReceiver.Receiver(mimeType)
if wr == nil {
priority := -1
for idx, c := range ti.Codecs {
if mime.IsMimeTypeStringEqual(track.Codec().MimeType, c.MimeType) {
priority = idx
break
}
}
if priority < 0 {
switch len(ti.Codecs) {
case 0:
// audio track
t.params.Logger.Warnw(
"unexpected 0 codecs in track info", nil,
"mime", mimeType,
"track", logger.Proto(ti),
)
priority = 0
case 1:
// older clients or non simulcast-codec, mime type only set later
if ti.Codecs[0].MimeType == "" {
priority = 0
}
}
}
if priority < 0 {
t.params.Logger.Warnw(
"could not find codec for webrtc receiver", nil,
"mime", mimeType,
"track", logger.Proto(ti),
)
t.addReceiverLock.Unlock()
return newCodec, false
}
newWR := sfu.NewWebRTCReceiver(
receiver,
track,
ti,
LoggerWithCodecMime(t.params.Logger, mimeType),
t.params.OnRTCP,
t.params.VideoConfig.StreamTrackerManager,
sfu.WithPliThrottleConfig(t.params.PLIThrottleConfig),
sfu.WithAudioConfig(t.params.AudioConfig),
sfu.WithLoadBalanceThreshold(20),
sfu.WithForwardStats(t.params.ForwardStats),
sfu.WithEnableRTPStreamRestartDetection(t.params.EnableRTPStreamRestartDetection),
)
newWR.OnCloseHandler(func() {
t.MediaTrackReceiver.SetClosing(false)
t.MediaTrackReceiver.ClearReceiver(mimeType, false)
if t.MediaTrackReceiver.TryClose() {
if t.dynacastManager != nil {
t.dynacastManager.Close()
}
}
})
// SIMULCAST-CODEC-TODO: these need to be receiver/mime aware, setting it up only for primary now
statsKey := telemetry.StatsKeyForTrack(
t.params.ParticipantCountry,
livekit.StreamType_UPSTREAM,
t.PublisherID(),
t.ID(),
ti.Source,
ti.Type,
)
for _, c := range ti.Codecs {
for _, l := range c.Layers {
t.params.Reporter.ReportLayer(roomobs.PackTrackLayer(l.Height, l.Width))
}
}
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
// send for only one codec, either primary (priority == 0) OR regressed codec
if priority == 0 || t.regressionTargetCodecReceived.Load() {
t.params.TelemetryListener().OnTrackStats(statsKey, stat)
if cs, ok := telemetry.CondenseStat(stat); ok {
t.params.Reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKind(t.params.ParticipantKind.String())
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(t.params.ParticipantKind))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(t.params.ParticipantKindDetails))
tx.ReportName(ti.Name)
tx.ReportKind(roomobs.TrackKindPub)
tx.ReportType(roomobs.TrackTypeFromProto(ti.Type))
tx.ReportSource(roomobs.TrackSourceFromProto(ti.Source))
tx.ReportMime(mime.NormalizeMimeType(ti.MimeType).ReporterType())
tx.ReportDuration(uint16(cs.EndTime.Sub(cs.StartTime).Milliseconds()))
tx.ReportFrames(uint16(cs.Frames))
tx.ReportRecvBytes(uint32(cs.Bytes))
tx.ReportRecvPackets(cs.Packets)
tx.ReportPacketsLost(cs.PacketsLost)
tx.ReportScore(stat.Score)
})
}
}
})
newWR.OnMaxLayerChange(func(mimeType mime.MimeType, maxLayer int32) {
// send for only one codec, either primary (priority == 0) OR regressed codec
if priority == 0 || t.regressionTargetCodecReceived.Load() {
t.MediaTrackReceiver.NotifyMaxLayerChange(mimeType, maxLayer)
}
})
// SIMULCAST-CODEC-TODO END: these need to be receiver/mime aware, setting it up only for primary now
if t.PrimaryReceiver() == nil {
// primary codec published, set potential codecs
potentialCodecs := make([]webrtc.RTPCodecParameters, 0, len(ti.Codecs))
parameters := receiver.GetParameters()
for _, c := range ti.Codecs {
for _, nc := range parameters.Codecs {
if mime.IsMimeTypeStringEqual(nc.MimeType, c.MimeType) {
potentialCodecs = append(potentialCodecs, nc)
break
}
}
}
if len(potentialCodecs) > 0 {
t.params.Logger.Debugw("primary codec published, set potential codecs", "potential", potentialCodecs)
t.MediaTrackReceiver.SetPotentialCodecs(potentialCodecs, parameters.HeaderExtensions)
}
}
t.buffer.Store(buff)
t.MediaTrackReceiver.SetupReceiver(newWR, priority, mid)
for ssrc, info := range t.params.SimTracks {
if info.Mid == mid && !info.IsRepairStream {
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, info.StreamID, ssrc, info.RepairSSRC)
}
}
wr = newWR
newCodec = true
newWR.AddOnCodecStateChange(func(codec webrtc.RTPCodecParameters, state sfu.ReceiverCodecState) {
t.MediaTrackReceiver.HandleReceiverCodecChange(newWR, codec, state)
})
// update subscriber video layers when video size changes
newWR.OnVideoSizeChanged(func() {
if t.params.UpdateTrackInfoByVideoSizeChange {
t.MediaTrackReceiver.UpdateVideoSize(mimeType, newWR.VideoSizes())
}
t.MediaTrackSubscriptions.UpdateVideoLayers()
})
}
if newCodec && enableRegression {
if mimeType == t.regressionTargetCodec {
t.params.Logger.Infow("regression target codec received", "codec", mimeType)
t.regressionTargetCodecReceived.Store(true)
regressCodec = true
} else if t.regressionTargetCodecReceived.Load() {
regressCodec = true
}
}
t.addReceiverLock.Unlock()
if err := wr.(*sfu.WebRTCReceiver).AddUpTrack(track, buff); err != nil {
t.params.Logger.Warnw(
"adding up track failed", err,
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"newCodec", newCodec,
)
buff.Close()
return newCodec, false
}
var expectedBitrate int
layers := buffer.GetVideoLayersForMimeType(mimeType, ti)
if layer >= 0 && len(layers) > int(layer) {
expectedBitrate = int(layers[layer].GetBitrate())
}
if err := buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, expectedBitrate); err != nil {
t.params.Logger.Warnw(
"binding buffer failed", err,
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"newCodec", newCodec,
)
buff.Close()
return newCodec, false
}
t.MediaTrackReceiver.MaybeSetSimulcast()
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, track.RID(), uint32(track.SSRC()), 0)
if regressCodec {
for _, c := range ti.Codecs {
if mime.NormalizeMimeType(c.MimeType) == t.regressionTargetCodec {
continue
}
t.params.Logger.Debugw("suspending codec for codec regression", "codec", c.MimeType)
if r := t.MediaTrackReceiver.Receiver(mime.NormalizeMimeType(c.MimeType)); r != nil {
if rtcreceiver, ok := r.(*sfu.WebRTCReceiver); ok {
rtcreceiver.SetCodecState(sfu.ReceiverCodecStateSuspended)
}
}
}
}
buff.OnNotifyRTX(t.MediaTrackReceiver.setLayerRtxInfo)
// if subscriber request fps before fps calculated, update them after fps updated.
buff.OnFpsChanged(func() {
t.MediaTrackSubscriptions.UpdateVideoLayers()
})
buff.OnFinalRtpStats(func(stats *livekit.RTPStats) {
t.params.TelemetryListener().OnTrackPublishRTPStats(
t.params.ParticipantID(),
t.ID(),
mimeType,
int(layer),
stats,
)
})
return newCodec, true
}
func (t *MediaTrack) GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality) {
receiver := t.ActiveReceiver()
if rtcReceiver, ok := receiver.(*sfu.WebRTCReceiver); ok {
return rtcReceiver.GetConnectionScoreAndQuality()
}
return connectionquality.MaxMOS, livekit.ConnectionQuality_EXCELLENT
}
func (t *MediaTrack) SetRTT(rtt uint32) {
if !t.rttFromXR.Load() {
t.MediaTrackReceiver.SetRTT(rtt)
}
}
func (t *MediaTrack) HasPendingCodec() bool {
return t.MediaTrackReceiver.PrimaryReceiver() == nil
}
func (t *MediaTrack) Restart() {
t.MediaTrackReceiver.Restart()
if t.dynacastManager != nil {
t.dynacastManager.Restart()
}
}
func (t *MediaTrack) Close(isExpectedToResume bool) {
t.MediaTrackReceiver.SetClosing(isExpectedToResume)
if t.dynacastManager != nil {
t.dynacastManager.Close()
}
t.MediaTrackReceiver.Close(isExpectedToResume)
}
func (t *MediaTrack) SetMuted(muted bool) {
// update quality based on subscription if unmuting.
// This will queue up the current state, but subscriber
// driven changes could update it.
if !muted && t.dynacastManager != nil {
t.dynacastManager.ForceUpdate()
}
t.MediaTrackReceiver.SetMuted(muted)
}
// OnTrackSubscribed is called when the track is subscribed by a non-hidden subscriber
// this allows the publisher to know when they should start sending data
func (t *MediaTrack) OnTrackSubscribed() {
if !t.everSubscribed.Swap(true) && t.params.OnTrackEverSubscribed != nil {
go t.params.OnTrackEverSubscribed(t.ID())
}
}
func (t *MediaTrack) enableRegression() bool {
return t.backupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION ||
(t.backupCodecPolicy == livekit.BackupCodecPolicy_PREFER_REGRESSION && t.params.ShouldRegressCodec())
}
func (t *MediaTrack) Logger() logger.Logger {
return t.params.Logger
}
// dynacast.DynacastManagerListtener implementation
var _ dynacast.DynacastManagerListener = (*MediaTrack)(nil)
func (t *MediaTrack) OnDynacastSubscribedMaxQualityChange(
subscribedQualities []*livekit.SubscribedCodec,
maxSubscribedQualities []types.SubscribedCodecQuality,
) {
if t.params.OnSubscribedMaxQualityChange != nil && !t.IsMuted() {
_ = t.params.OnSubscribedMaxQualityChange(
t.ID(),
t.ToProto(),
subscribedQualities,
maxSubscribedQualities,
)
}
for _, q := range maxSubscribedQualities {
receiver := t.Receiver(q.CodecMime)
if receiver != nil {
receiver.SetMaxExpectedSpatialLayer(
buffer.GetSpatialLayerForVideoQuality(
q.CodecMime,
q.Quality,
t.MediaTrackReceiver.TrackInfo(),
),
)
}
}
}
func (t *MediaTrack) OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec) {
if t.params.OnSubscribedAudioCodecChange != nil {
_ = t.params.OnSubscribedAudioCodecChange(t.ID(), codecs)
}
}
func (t *MediaTrack) SetMigrated(migrated bool) {
t.migrated.Store(migrated)
}
func (t *MediaTrack) Migrated() bool {
return t.migrated.Load()
}
func (t *MediaTrack) SetPublished(published bool) {
t.published.Store(published)
}
func (t *MediaTrack) Published() bool {
return t.published.Load()
}

197
pkg/rtc/mediatrack_test.go Normal file
View File

@@ -0,0 +1,197 @@
// 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 rtc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func TestTrackInfo(t *testing.T) {
// ensures that persisted trackinfo is being returned
ti := livekit.TrackInfo{
Sid: "testsid",
Name: "testtrack",
Source: livekit.TrackSource_SCREEN_SHARE,
Type: livekit.TrackType_VIDEO,
Simulcast: false,
Width: 100,
Height: 80,
Muted: true,
}
mt := NewMediaTrack(MediaTrackParams{}, &ti)
outInfo := mt.ToProto()
require.Equal(t, ti.Muted, outInfo.Muted)
require.Equal(t, ti.Name, outInfo.Name)
require.Equal(t, ti.Name, mt.Name())
require.Equal(t, livekit.TrackID(ti.Sid), mt.ID())
require.Equal(t, ti.Type, outInfo.Type)
require.Equal(t, ti.Type, mt.Kind())
require.Equal(t, ti.Source, outInfo.Source)
require.Equal(t, ti.Width, outInfo.Width)
require.Equal(t, ti.Height, outInfo.Height)
require.Equal(t, ti.Simulcast, outInfo.Simulcast)
}
func TestGetQualityForDimension(t *testing.T) {
t.Run("landscape source", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 300, 200))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 200, 250))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 700, 480))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 500, 1000))
})
t.Run("portrait source", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 540,
Height: 960,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeVP8, 200, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 400, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeVP8, 400, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeVP8, 600, 900))
})
t.Run("layers provided", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Codecs: []*livekit.SimulcastCodecInfo{
{
MimeType: mime.MimeTypeH264.String(),
Layers: []*livekit.VideoLayer{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 960,
Height: 540,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
},
},
},
},
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
})
t.Run("highest layer with smallest dimensions", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Codecs: []*livekit.SimulcastCodecInfo{
{
MimeType: mime.MimeTypeH264.String(),
Layers: []*livekit.VideoLayer{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 1080,
Height: 720,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
},
},
},
},
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800))
mt = NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Codecs: []*livekit.SimulcastCodecInfo{
{
MimeType: mime.MimeTypeH264.String(),
Layers: []*livekit.VideoLayer{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
},
},
},
},
})
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 120, 120))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(mime.MimeTypeH264, 300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(mime.MimeTypeH264, 1200, 800))
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
// 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 rtc
import (
"errors"
"slices"
"sync"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
)
var (
errAlreadySubscribed = errors.New("already subscribed")
errNotFound = errors.New("not found")
)
// MediaTrackSubscriptions manages subscriptions of a media track
type MediaTrackSubscriptions struct {
params MediaTrackSubscriptionsParams
subscribedTracksMu sync.RWMutex
subscribedTracks map[livekit.ParticipantID]types.SubscribedTrack
onDownTrackCreated func(downTrack *sfu.DownTrack)
onSubscriberMaxQualityChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)
onSubscriberAudioCodecChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)
}
type MediaTrackSubscriptionsParams struct {
MediaTrack types.MediaTrack
IsRelayed bool
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Logger logger.Logger
}
func NewMediaTrackSubscriptions(params MediaTrackSubscriptionsParams) *MediaTrackSubscriptions {
return &MediaTrackSubscriptions{
params: params,
subscribedTracks: make(map[livekit.ParticipantID]types.SubscribedTrack),
}
}
func (t *MediaTrackSubscriptions) OnDownTrackCreated(f func(downTrack *sfu.DownTrack)) {
t.onDownTrackCreated = f
}
func (t *MediaTrackSubscriptions) OnSubscriberMaxQualityChange(f func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)) {
t.onSubscriberMaxQualityChange = f
}
func (t *MediaTrackSubscriptions) OnSubscriberAudioCodecChange(f func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)) {
t.onSubscriberAudioCodecChange = f
}
func (t *MediaTrackSubscriptions) SetMuted(muted bool) {
// update mute of all subscribed tracks
for _, st := range t.getAllSubscribedTracks() {
st.SetPublisherMuted(muted)
}
}
func (t *MediaTrackSubscriptions) IsSubscriber(subID livekit.ParticipantID) bool {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
_, ok := t.subscribedTracks[subID]
return ok
}
// AddSubscriber subscribes sub to current mediaTrack
func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *WrappedReceiver) (types.SubscribedTrack, error) {
trackID := t.params.MediaTrack.ID()
subscriberID := sub.ID()
// don't subscribe to the same track multiple times
t.subscribedTracksMu.Lock()
if _, ok := t.subscribedTracks[subscriberID]; ok {
t.subscribedTracksMu.Unlock()
return nil, errAlreadySubscribed
}
t.subscribedTracksMu.Unlock()
subTrack, err := NewSubscribedTrack(SubscribedTrackParams{
ReceiverConfig: t.params.ReceiverConfig,
SubscriberConfig: t.params.SubscriberConfig,
Subscriber: sub,
MediaTrack: t.params.MediaTrack,
AdaptiveStream: sub.GetAdaptiveStream(),
EnableStartAtDesiredQuality: sub.GetEnableStartAtDesiredQuality(),
TelemetryListener: sub.GetTelemetryListener(),
WrappedReceiver: wr,
IsRelayed: t.params.IsRelayed,
OnDownTrackCreated: t.onDownTrackCreated,
OnDownTrackClosed: func(subscriberID livekit.ParticipantID) {
t.subscribedTracksMu.Lock()
delete(t.subscribedTracks, subscriberID)
t.subscribedTracksMu.Unlock()
},
OnSubscriberMaxQualityChange: t.onSubscriberMaxQualityChange,
OnSubscriberAudioCodecChange: t.onSubscriberAudioCodecChange,
})
if err != nil {
return nil, err
}
// Bind callback can happen from replaceTrack, so set it up early
var reusingTransceiver atomic.Bool
var dtState sfu.DownTrackState
downTrack := subTrack.DownTrack()
downTrack.OnBinding(func(err error) {
if err != nil {
go subTrack.Bound(err)
return
}
if reusingTransceiver.Load() {
sub.GetLogger().Debugw("seeding downtrack state", "trackID", trackID)
downTrack.SeedState(dtState)
}
if err = wr.AddDownTrack(downTrack); err != nil && err != sfu.ErrReceiverClosed {
sub.GetLogger().Errorw(
"could not add down track", err,
"publisher", subTrack.PublisherIdentity(),
"publisherID", subTrack.PublisherID(),
"trackID", trackID,
)
}
go subTrack.Bound(nil)
subTrack.SetPublisherMuted(t.params.MediaTrack.IsMuted())
})
var transceiver *webrtc.RTPTransceiver
var sender *webrtc.RTPSender
// try cached RTP senders for a chance to replace track
var existingTransceiver *webrtc.RTPTransceiver
replacedTrack := false
existingTransceiver, dtState = sub.GetCachedDownTrack(trackID)
if existingTransceiver != nil {
sub.GetLogger().Debugw(
"trying to use existing transceiver",
"publisher", subTrack.PublisherIdentity(),
"publisherID", subTrack.PublisherID(),
"trackID", trackID,
)
reusingTransceiver.Store(true)
rtpSender := existingTransceiver.Sender()
if rtpSender != nil {
// replaced track will bind immediately without negotiation, SetTransceiver first before bind
downTrack.SetTransceiver(existingTransceiver)
err := rtpSender.ReplaceTrack(downTrack)
if err == nil {
sender = rtpSender
transceiver = existingTransceiver
replacedTrack = true
sub.GetLogger().Debugw(
"track replaced",
"publisher", subTrack.PublisherIdentity(),
"publisherID", subTrack.PublisherID(),
"trackID", trackID,
)
}
}
if !replacedTrack {
// Could not re-use cached transceiver for this track.
// Stop the transceiver so that it is at least not active.
// It is not usable once stopped,
//
// Adding down track will create a new transceiver (or re-use
// an inactive existing one). In either case, a renegotiation
// will happen and that will notify remote of this stopped
// transceiver
existingTransceiver.Stop()
reusingTransceiver.Store(false)
}
}
// if cannot replace, find an unused transceiver or add new one
if transceiver == nil {
info := t.params.MediaTrack.ToProto()
addTrackParams := types.AddTrackParams{
Stereo: slices.Contains(info.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO),
Red: IsRedEnabled(info),
}
codecs := wr.Codecs()
if addTrackParams.Red && (len(codecs) == 1 && mime.IsMimeTypeStringOpus(codecs[0].MimeType)) {
addTrackParams.Red = false
}
sub.VerifySubscribeParticipantInfo(subTrack.PublisherID(), subTrack.PublisherVersion())
if sub.SupportsTransceiverReuse(t.params.MediaTrack) {
//
// AddTrack will create a new transceiver or re-use an unused one
// if the attributes match. This prevents SDP from bloating
// because of dormant transceivers building up.
//
sender, transceiver, err = sub.AddTrackLocal(downTrack, addTrackParams)
if err != nil {
return nil, err
}
} else {
sender, transceiver, err = sub.AddTransceiverFromTrackLocal(downTrack, addTrackParams)
if err != nil {
return nil, err
}
}
}
// whether re-using or stopping remove transceiver from cache
// NOTE: safety net, if somehow a cached transceiver is re-used by a different track
sub.UncacheDownTrack(transceiver)
// negotiation isn't required if we've replaced track
// ONE-SHOT-SIGNALLING-MODE: this should not be needed, but that mode information is not available here,
// but it is not detrimental to set this, needs clean up when participants modes are separated out better.
subTrack.SetNeedsNegotiation(!replacedTrack)
subTrack.SetRTPSender(sender)
// it is possible that subscribed track is closed before subscription manager sets
// the `OnClose` callback. That handler in subscription manager removes the track
// from the peer connection.
//
// 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.
// Subscription manager will reset this if this subscription proceeds till that point.
subTrack.OnClose(func(isExpectedToResume bool) {
if !isExpectedToResume {
if err := sub.RemoveTrackLocal(sender); err != nil {
t.params.Logger.Warnw("could not remove track from peer connection", err)
}
}
})
downTrack.SetTransceiver(transceiver)
t.subscribedTracksMu.Lock()
t.subscribedTracks[subscriberID] = subTrack
t.subscribedTracksMu.Unlock()
return subTrack, nil
}
// RemoveSubscriber removes participant from subscription
// stop all forwarders to the client
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, "isExpectedToResume", isExpectedToResume)
t.closeSubscribedTrack(subTrack, isExpectedToResume)
return nil
}
func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.SubscribedTrack, isExpectedToResume bool) {
dt := subTrack.DownTrack()
if dt == nil {
return
}
if isExpectedToResume {
dt.CloseWithFlush(false, false)
} else {
// flushing blocks, avoid blocking when publisher removes all its subscribers
go dt.CloseWithFlush(true, true)
}
}
func (t *MediaTrackSubscriptions) GetAllSubscribers() []livekit.ParticipantID {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
subs := make([]livekit.ParticipantID, 0, len(t.subscribedTracks))
for id := range t.subscribedTracks {
subs = append(subs, id)
}
return subs
}
func (t *MediaTrackSubscriptions) GetAllSubscribersForMime(mime mime.MimeType) []livekit.ParticipantID {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
subs := make([]livekit.ParticipantID, 0, len(t.subscribedTracks))
for id, subTrack := range t.subscribedTracks {
if subTrack.DownTrack().Mime() != mime {
continue
}
subs = append(subs, id)
}
return subs
}
func (t *MediaTrackSubscriptions) GetNumSubscribers() int {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
return len(t.subscribedTracks)
}
func (t *MediaTrackSubscriptions) UpdateVideoLayers() {
for _, st := range t.getAllSubscribedTracks() {
st.UpdateVideoLayer()
}
}
func (t *MediaTrackSubscriptions) getSubscribedTrack(subscriberID livekit.ParticipantID) types.SubscribedTrack {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
return t.subscribedTracks[subscriberID]
}
func (t *MediaTrackSubscriptions) getAllSubscribedTracks() []types.SubscribedTrack {
t.subscribedTracksMu.RLock()
defer t.subscribedTracksMu.RUnlock()
return t.getAllSubscribedTracksLocked()
}
func (t *MediaTrackSubscriptions) getAllSubscribedTracksLocked() []types.SubscribedTrack {
subTracks := make([]types.SubscribedTrack, 0, len(t.subscribedTracks))
for _, subTrack := range t.subscribedTracks {
subTracks = append(subTracks, subTrack)
}
return subTracks
}
func (t *MediaTrackSubscriptions) DebugInfo() []map[string]any {
subscribedTrackInfo := make([]map[string]any, 0)
for _, val := range t.getAllSubscribedTracks() {
if st, ok := val.(*SubscribedTrack); ok {
subscribedTrackInfo = append(subscribedTrackInfo, st.DownTrack().DebugInfo())
}
}
return subscribedTrackInfo
}

View File

@@ -0,0 +1,69 @@
package rtc
import (
"time"
"github.com/livekit/protocol/livekit"
)
type MigrationDataCacheState int
const (
MigrationDataCacheStateWaiting MigrationDataCacheState = iota
MigrationDataCacheStateTimeout
MigrationDataCacheStateDone
)
const (
migrationDataCacheMaxSize = 4 << 20 // 4 MiB
)
type MigrationDataCache struct {
lastSeq uint32
pkts []*livekit.DataPacket
size int
state MigrationDataCacheState
expiredAt time.Time
}
func NewMigrationDataCache(lastSeq uint32, expiredAt time.Time) *MigrationDataCache {
return &MigrationDataCache{
lastSeq: lastSeq,
expiredAt: expiredAt,
}
}
// Add adds a message to the cache if there is a gap between the last sequence number and cached messages then return the cache State:
// - MigrationDataCacheStateWaiting: waiting for the next packet (lastSeq + 1) of last sequence from old node
// - MigrationDataCacheStateTimeout: the next packet is not received before the expiredAt or the cache is full, participant
// will continue to process the reliable messages, subscribers will see the gap after the publisher migration
// - MigrationDataCacheStateDone: the next packet is received, participant can continue to process the reliable messages
func (c *MigrationDataCache) Add(pkt *livekit.DataPacket, size int) MigrationDataCacheState {
if c.state == MigrationDataCacheStateDone || c.state == MigrationDataCacheStateTimeout {
return c.state
}
if pkt.Sequence <= c.lastSeq {
return c.state
}
if pkt.Sequence == c.lastSeq+1 {
c.state = MigrationDataCacheStateDone
return c.state
}
c.pkts = append(c.pkts, pkt)
c.size += size
if c.size >= migrationDataCacheMaxSize || time.Now().After(c.expiredAt) {
c.state = MigrationDataCacheStateTimeout
}
return c.state
}
func (c *MigrationDataCache) Get() []*livekit.DataPacket {
return c.pkts
}
func (c *MigrationDataCache) Size() int {
return c.size
}

View File

@@ -0,0 +1,57 @@
package rtc
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func TestMigrationDataCache_Add(t *testing.T) {
expiredAt := time.Now().Add(100 * time.Millisecond)
cache := NewMigrationDataCache(10, expiredAt)
pkt1 := &livekit.DataPacket{Sequence: 9}
state := cache.Add(pkt1, 0)
require.Equal(t, MigrationDataCacheStateWaiting, state)
require.Empty(t, cache.Get())
pkt2 := &livekit.DataPacket{Sequence: 11}
state = cache.Add(pkt2, 0)
require.Equal(t, MigrationDataCacheStateDone, state)
require.Empty(t, cache.Get())
pkt3 := &livekit.DataPacket{Sequence: 12}
state = cache.Add(pkt3, 0)
require.Equal(t, MigrationDataCacheStateDone, state)
require.Empty(t, cache.Get())
cache2 := NewMigrationDataCache(20, time.Now().Add(10*time.Millisecond))
pkt4 := &livekit.DataPacket{Sequence: 22}
time.Sleep(20 * time.Millisecond)
state = cache2.Add(pkt4, 0)
require.Equal(t, MigrationDataCacheStateTimeout, state)
require.Len(t, cache2.Get(), 1)
require.Equal(t, uint32(22), cache2.Get()[0].Sequence)
}
func TestMigrationDataCache_MaxSize(t *testing.T) {
// the cache should not grow past the size budget even if the expiry is far in the future
cache := NewMigrationDataCache(10, time.Now().Add(time.Minute))
pktSize := 1000
seq := uint32(12)
state := MigrationDataCacheStateWaiting
for ; state == MigrationDataCacheStateWaiting; seq++ {
state = cache.Add(&livekit.DataPacket{Sequence: seq}, pktSize)
}
require.Equal(t, MigrationDataCacheStateTimeout, state)
require.LessOrEqual(t, cache.Size(), migrationDataCacheMaxSize+pktSize)
require.Len(t, cache.Get(), migrationDataCacheMaxSize/pktSize+1)
// once full, further packets are dropped, including the continuous one
require.Equal(t, MigrationDataCacheStateTimeout, cache.Add(&livekit.DataPacket{Sequence: 11}, pktSize))
}

4570
pkg/rtc/participant.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
// Copyright 2026 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 rtc
import (
"sync"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
type ParticipantDataBlobParams struct {
Logger logger.Logger
}
type ParticipantDataBlob struct {
params ParticipantDataBlobParams
lock sync.Mutex
blobs map[string]*livekit.DataBlob
}
func NewParticipantDataBlob(params ParticipantDataBlobParams) *ParticipantDataBlob {
return &ParticipantDataBlob{
params: params,
blobs: make(map[string]*livekit.DataBlob),
}
}
func (p *ParticipantDataBlob) Add(db *livekit.DataBlob) {
p.lock.Lock()
defer p.lock.Unlock()
if db.Key == nil {
return
}
p.blobs[db.Key.String()] = db
}
func (p *ParticipantDataBlob) Delete(dbKey *livekit.DataBlobKey) {
p.lock.Lock()
defer p.lock.Unlock()
if dbKey == nil {
return
}
delete(p.blobs, dbKey.String())
}
func (p *ParticipantDataBlob) Get(dbKey *livekit.DataBlobKey) *livekit.DataBlob {
p.lock.Lock()
defer p.lock.Unlock()
if dbKey == nil {
return nil
}
db, ok := p.blobs[dbKey.String()]
if !ok {
return nil
}
return db
}
func (p *ParticipantDataBlob) GetAll() []*livekit.DataBlob {
p.lock.Lock()
defer p.lock.Unlock()
all := make([]*livekit.DataBlob, 0, len(p.blobs))
for _, db := range p.blobs {
all = append(all, db)
}
return all
}
// -------------------------------

View File

@@ -0,0 +1,123 @@
// Copyright 2026 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 rtc
import (
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func (p *ParticipantImpl) HandleStoreDataBlobRequest(req *livekit.StoreDataBlobRequest) {
if !p.params.EnableParticipantDataBlob {
p.pubLogger.Warnw("data blob not enabled", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_NOT_ALLOWED,
Message: "data blob not enabled",
})
return
}
if req.Blob == nil || req.Blob.Key == nil || len(req.Blob.Key.String()) == 0 || !p.params.LimitConfig.CheckDataBlobKeyLength(req.Blob.Key.String()) {
p.pubLogger.Warnw("data blob is invalid", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_INVALID_REQUEST,
Message: "data blob is invalid",
})
return
}
if !p.params.LimitConfig.CheckDataTrackSchemaID(req.Blob.Key.GetSchemaId()) {
p.pubLogger.Warnw("data blob key schema id is invalid", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_INVALID_REQUEST,
Message: "encoding identifier is empty or exceeds the maximum length",
})
return
}
if len(req.Blob.Contents) == 0 {
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_INVALID_REQUEST,
Message: "data blob is empty",
})
return
}
if !p.params.LimitConfig.CanAddDataBlob(p.dataBlob.GetAll(), req.Blob) {
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_LIMIT_EXCEEDED,
Message: "async attribute definition exceeds limit",
})
return
}
p.AddDataBlob(req.Blob)
p.listener().OnStoreDataBlob(p, req.Blob)
p.sendStoreDataBlobResponse(req.RequestId, req.Blob.Key)
}
func (p *ParticipantImpl) HandleGetDataBlobRequest(req *livekit.GetDataBlobRequest) {
if req.Key == nil {
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_INVALID_REQUEST,
Message: "data blob key is required",
})
return
}
p.listener().OnGetDataBlob(p, req)
}
func (p *ParticipantImpl) AddDataBlob(dataBlob *livekit.DataBlob) {
p.dataBlob.Add(dataBlob)
}
func (p *ParticipantImpl) GetDataBlob(key *livekit.DataBlobKey) *livekit.DataBlob {
return p.dataBlob.Get(key)
}
func (p *ParticipantImpl) ProcessGetDataBlobRequest(req *livekit.GetDataBlobRequest, publisher types.Participant) {
if publisher == nil {
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_NOT_FOUND,
Message: "participant not found",
})
return
}
dataBlob := publisher.GetDataBlob(req.Key)
if dataBlob == nil {
p.sendRequestResponse(&livekit.RequestResponse{
RequestId: req.RequestId,
Reason: livekit.RequestResponse_NOT_FOUND,
Message: "data blob not found",
})
return
}
p.sendGetDataBlobResponse(req.RequestId, dataBlob)
}
func (p *ParticipantImpl) GetAllDataBlob() []*livekit.DataBlob {
return p.dataBlob.GetAll()
}

View File

@@ -0,0 +1,300 @@
// Copyright 2026 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 rtc
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing/routingfakes"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
)
func newParticipantWithDataBlob(t *testing.T, enabled bool, maxKeyLength int, maxSize uint32) *ParticipantImpl {
t.Helper()
p := newParticipantForTest("test")
p.params.EnableParticipantDataBlob = enabled
p.params.LimitConfig = config.LimitConfig{
MaxDataBlobKeyLength: maxKeyLength,
MaxDataBlobSize: maxSize,
}
return p
}
func lastRequestResponse(t *testing.T, sink *routingfakes.FakeMessageSink, idx int) *livekit.RequestResponse {
t.Helper()
msg := sink.WriteMessageArgsForCall(idx).(*livekit.SignalResponse)
rr, ok := msg.Message.(*livekit.SignalResponse_RequestResponse)
require.True(t, ok, "expected SignalResponse_RequestResponse, got %T", msg.Message)
return rr.RequestResponse
}
func TestHandleStoreDataBlobRequest(t *testing.T) {
t.Run("returns NOT_ALLOWED when feature not enabled", func(t *testing.T) {
p := newParticipantWithDataBlob(t, false, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
req := &livekit.StoreDataBlobRequest{
Blob: &livekit.DataBlob{
Key: genericKey("blob-1"),
Contents: []byte("def"),
},
}
p.HandleStoreDataBlobRequest(req)
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_NOT_ALLOWED, rr.Reason)
require.Empty(t, p.dataBlob.GetAll())
})
t.Run("returns INVALID_REQUEST when blob is nil", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
require.Empty(t, p.dataBlob.GetAll())
})
t.Run("returns INVALID_REQUEST when key is nil", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
Blob: &livekit.DataBlob{
Contents: []byte("def"),
},
})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
})
t.Run("returns INVALID_REQUEST when key has no oneof set", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
Blob: &livekit.DataBlob{
Key: &livekit.DataBlobKey{},
Contents: []byte("def"),
},
})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
})
t.Run("returns INVALID_REQUEST when key exceeds length limit", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 5, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
Blob: &livekit.DataBlob{
Key: genericKey(strings.Repeat("a", 64)),
Contents: []byte("def"),
},
})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
})
t.Run("returns INVALID_REQUEST when contents is empty", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
Blob: &livekit.DataBlob{
Key: genericKey("blob-1"),
},
})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
require.Empty(t, p.dataBlob.GetAll())
})
t.Run("returns LIMIT_EXCEEDED when adding would breach the limit", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 16)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
Blob: &livekit.DataBlob{
Key: genericKey("blob-1"),
Contents: []byte(strings.Repeat("x", 32)),
},
})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_LIMIT_EXCEEDED, rr.Reason)
require.Empty(t, p.dataBlob.GetAll())
})
t.Run("stores a valid blob, notifies listener, and sends response", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
listener := p.params.ParticipantListener.(*typesfakes.FakeLocalParticipantListener)
key := genericKey("blob-1")
contents := []byte("definition-bytes")
blob := &livekit.DataBlob{Key: key, Contents: contents}
p.HandleStoreDataBlobRequest(&livekit.StoreDataBlobRequest{
RequestId: 42,
Blob: blob,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
msg := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
response, ok := msg.Message.(*livekit.SignalResponse_StoreDataBlobResponse)
require.True(t, ok, "expected SignalResponse_StoreDataBlobResponse, got %T", msg.Message)
require.Equal(t, uint32(42), response.StoreDataBlobResponse.RequestId)
require.Equal(t, key, response.StoreDataBlobResponse.Key)
stored := p.dataBlob.Get(key)
require.NotNil(t, stored)
require.Equal(t, contents, stored.Contents)
require.Equal(t, 1, listener.OnStoreDataBlobCallCount())
gotParticipant, gotBlob := listener.OnStoreDataBlobArgsForCall(0)
require.Equal(t, p, gotParticipant)
require.Equal(t, blob, gotBlob)
})
}
func TestHandleGetDataBlobRequest(t *testing.T) {
t.Run("returns INVALID_REQUEST when key is missing", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.HandleGetDataBlobRequest(&livekit.GetDataBlobRequest{
ParticipantIdentity: "other",
})
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_INVALID_REQUEST, rr.Reason)
})
t.Run("forwards request to listener when key is provided", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
listener := p.params.ParticipantListener.(*typesfakes.FakeLocalParticipantListener)
req := &livekit.GetDataBlobRequest{
ParticipantIdentity: "other",
Key: genericKey("blob-1"),
}
p.HandleGetDataBlobRequest(req)
require.Equal(t, 1, listener.OnGetDataBlobCallCount())
gotParticipant, gotReq := listener.OnGetDataBlobArgsForCall(0)
require.Equal(t, p, gotParticipant)
require.Equal(t, req, gotReq)
})
}
func TestGetDataBlob(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
key := genericKey("blob-1")
require.Nil(t, p.GetDataBlob(key))
blob := &livekit.DataBlob{
Key: key,
Contents: []byte("definition"),
}
p.dataBlob.Add(blob)
got := p.GetDataBlob(key)
require.NotNil(t, got)
require.Equal(t, key.String(), got.Key.String())
require.Equal(t, []byte("definition"), got.Contents)
}
func TestProcessGetDataBlobRequest(t *testing.T) {
t.Run("returns NOT_FOUND when publisher is nil", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
p.ProcessGetDataBlobRequest(&livekit.GetDataBlobRequest{
Key: genericKey("blob-1"),
}, nil)
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_NOT_FOUND, rr.Reason)
require.Contains(t, rr.Message, "participant")
})
t.Run("returns NOT_FOUND when publisher has no matching blob", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
publisher := &typesfakes.FakeParticipant{}
publisher.GetDataBlobReturns(nil)
req := &livekit.GetDataBlobRequest{
Key: genericKey("blob-1"),
}
p.ProcessGetDataBlobRequest(req, publisher)
require.Equal(t, 1, publisher.GetDataBlobCallCount())
require.Equal(t, req.Key, publisher.GetDataBlobArgsForCall(0))
require.Equal(t, 1, sink.WriteMessageCallCount())
rr := lastRequestResponse(t, sink, 0)
require.Equal(t, livekit.RequestResponse_NOT_FOUND, rr.Reason)
})
t.Run("sends blob response when publisher has a matching blob", func(t *testing.T) {
p := newParticipantWithDataBlob(t, true, 0, 0)
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
key := genericKey("blob-1")
blob := &livekit.DataBlob{
Key: key,
Contents: []byte("definition-bytes"),
}
publisher := &typesfakes.FakeParticipant{}
publisher.GetDataBlobReturns(blob)
p.ProcessGetDataBlobRequest(&livekit.GetDataBlobRequest{
RequestId: 42,
Key: key,
}, publisher)
require.Equal(t, 1, sink.WriteMessageCallCount())
msg := sink.WriteMessageArgsForCall(0).(*livekit.SignalResponse)
response, ok := msg.Message.(*livekit.SignalResponse_GetDataBlobResponse)
require.True(t, ok, "expected SignalResponse_GetDataBlobResponse, got %T", msg.Message)
require.Equal(t, uint32(42), response.GetDataBlobResponse.RequestId)
require.Equal(t, blob, response.GetDataBlobResponse.Blob)
})
}

View File

@@ -0,0 +1,175 @@
// Copyright 2026 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 rtc
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func newTestDataBlob() *ParticipantDataBlob {
return NewParticipantDataBlob(ParticipantDataBlobParams{
Logger: logger.GetLogger(),
})
}
func genericKey(name string) *livekit.DataBlobKey {
return &livekit.DataBlobKey{
Key: &livekit.DataBlobKey_Generic{
Generic: name,
},
}
}
func TestParticipantDataBlob_AddAndGet(t *testing.T) {
a := newTestDataBlob()
key := genericKey("blob-1")
contents := []byte("definition-bytes")
a.Add(&livekit.DataBlob{Key: key, Contents: contents})
got := a.Get(key)
require.NotNil(t, got)
require.Equal(t, key.String(), got.Key.String())
require.Equal(t, contents, got.Contents)
}
func TestParticipantDataBlob_AddOverwrites(t *testing.T) {
a := newTestDataBlob()
key := genericKey("blob-1")
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("v1")})
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("v2")})
got := a.Get(key)
require.NotNil(t, got)
require.Equal(t, []byte("v2"), got.Contents)
require.Len(t, a.GetAll(), 1)
}
func TestParticipantDataBlob_DistinctKeys(t *testing.T) {
a := newTestDataBlob()
key1 := genericKey("blob-1")
key2 := genericKey("blob-2")
a.Add(&livekit.DataBlob{Key: key1, Contents: []byte("c1")})
a.Add(&livekit.DataBlob{Key: key2, Contents: []byte("c2")})
got1 := a.Get(key1)
require.NotNil(t, got1)
require.Equal(t, []byte("c1"), got1.Contents)
got2 := a.Get(key2)
require.NotNil(t, got2)
require.Equal(t, []byte("c2"), got2.Contents)
require.Len(t, a.GetAll(), 2)
}
func TestParticipantDataBlob_Delete(t *testing.T) {
a := newTestDataBlob()
key := genericKey("blob-1")
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("definition")})
a.Delete(key)
require.Nil(t, a.Get(key))
require.Empty(t, a.GetAll())
// deleting a non-existent key is a no-op
a.Delete(key)
require.Empty(t, a.GetAll())
}
func TestParticipantDataBlob_NilKey(t *testing.T) {
a := newTestDataBlob()
// nil key should be silently ignored, not panic
a.Add(&livekit.DataBlob{Contents: []byte("definition")})
require.Empty(t, a.GetAll())
require.Nil(t, a.Get(nil))
a.Delete(nil)
require.Empty(t, a.GetAll())
}
func TestParticipantDataBlob_GetMissing(t *testing.T) {
a := newTestDataBlob()
require.Nil(t, a.Get(genericKey("missing")))
}
func TestParticipantDataBlob_GetAllContents(t *testing.T) {
a := newTestDataBlob()
key1 := genericKey("blob-1")
key2 := genericKey("blob-2")
a.Add(&livekit.DataBlob{Key: key1, Contents: []byte("def-1")})
a.Add(&livekit.DataBlob{Key: key2, Contents: []byte("def-2")})
all := a.GetAll()
require.Len(t, all, 2)
for _, db := range all {
switch key := db.Key.Key.(type) {
case *livekit.DataBlobKey_Generic:
switch key.Generic {
case "blob-1":
require.Equal(t, []byte("def-1"), db.Contents)
case "blob-2":
require.Equal(t, []byte("def-2"), db.Contents)
default:
require.Fail(t, "unexpected key", key.Generic)
}
default:
require.Fail(t, "unexpected key type", "Generic")
}
}
}
func TestParticipantDataBlob_ConcurrentAccess(t *testing.T) {
a := newTestDataBlob()
const numGoroutines = 16
const opsPerGoroutine = 100
var wg sync.WaitGroup
wg.Add(numGoroutines)
for g := 0; g < numGoroutines; g++ {
go func(g int) {
defer wg.Done()
for i := 0; i < opsPerGoroutine; i++ {
key := genericKey(fmt.Sprintf("blob-%d", g%8))
a.Add(&livekit.DataBlob{Key: key, Contents: []byte("v")})
_ = a.Get(key)
_ = a.GetAll()
if i%3 == 0 {
a.Delete(key)
}
}
}(g)
}
wg.Wait()
}

View File

@@ -0,0 +1,212 @@
// 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 rtc
import (
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
)
func (p *ParticipantImpl) HandlePublishDataTrackRequest(req *livekit.PublishDataTrackRequest) {
if !p.CanPublishData() || !p.params.EnableDataTracks {
p.pubLogger.Warnw("no permission to publish data track", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
Reason: livekit.RequestResponse_NOT_ALLOWED,
Message: "does not have permission to publish data",
Request: &livekit.RequestResponse_PublishDataTrack{
PublishDataTrack: utils.CloneProto(req),
},
})
return
}
if req.PubHandle == 0 || req.PubHandle > 65535 {
p.pubLogger.Warnw("invalid data track handle", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
Reason: livekit.RequestResponse_INVALID_HANDLE,
Message: "handle should be > 0 AND < 65536",
Request: &livekit.RequestResponse_PublishDataTrack{
PublishDataTrack: utils.CloneProto(req),
},
})
return
}
if len(req.Name) == 0 || len(req.Name) > 256 {
p.pubLogger.Warnw("invalid data track name", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
Reason: livekit.RequestResponse_INVALID_NAME,
Message: "name should not be empty and should not exceed 256 characters",
Request: &livekit.RequestResponse_PublishDataTrack{
PublishDataTrack: utils.CloneProto(req),
},
})
return
}
if !p.params.LimitConfig.CheckDataTrackFrameEncoding(req.FrameEncoding) ||
!p.params.LimitConfig.CheckDataTrackSchemaID(req.Schema) {
p.pubLogger.Warnw("invalid encoding identifier", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
Reason: livekit.RequestResponse_INVALID_REQUEST,
Message: "encoding identifier is empty or exceeds the maximum length",
Request: &livekit.RequestResponse_PublishDataTrack{
PublishDataTrack: utils.CloneProto(req),
},
})
return
}
publishedDataTracks := p.UpDataTrackManager.GetPublishedDataTracks()
for _, dt := range publishedDataTracks {
message := ""
reason := livekit.RequestResponse_OK
switch {
case dt.PubHandle() == uint16(req.PubHandle):
message = "a data track with same handle already exists"
reason = livekit.RequestResponse_DUPLICATE_HANDLE
case dt.Name() == req.Name:
message = "a data track with same name already exists"
reason = livekit.RequestResponse_DUPLICATE_NAME
}
if message != "" {
p.pubLogger.Warnw(
"cannot publish duplicate data track", nil,
"req", logger.Proto(req),
"existing", logger.Proto(dt.ToProto()),
)
p.sendRequestResponse(&livekit.RequestResponse{
Reason: reason,
Message: message,
Request: &livekit.RequestResponse_PublishDataTrack{
PublishDataTrack: utils.CloneProto(req),
},
})
return
}
}
dti := &livekit.DataTrackInfo{
PubHandle: req.PubHandle,
Sid: guid.New(utils.DataTrackPrefix),
Name: req.Name,
Encryption: req.Encryption,
}
dti.FrameEncoding = utils.CloneProto(req.GetFrameEncoding())
dti.Schema = utils.CloneProto(req.GetSchema())
dt := NewDataTrack(
DataTrackParams{
Logger: p.params.Logger.WithValues("trackID", dti.Sid),
ParticipantID: p.ID,
ParticipantIdentity: p.params.Identity,
BytesTrackStats: NewBytesTrackStats(
p.params.Country,
livekit.TrackID(dti.Sid),
p.ID(),
p.Kind(),
p.KindDetails(),
p.GetTelemetryListener,
p.params.Reporter,
),
},
dti,
)
p.UpDataTrackManager.AddPublishedDataTrack(dt)
p.sendPublishDataTrackResponse(dti)
p.setIsPublisher(true)
p.dirty.Store(true)
}
func (p *ParticipantImpl) HandleUnpublishDataTrackRequest(req *livekit.UnpublishDataTrackRequest) {
dt := p.UpDataTrackManager.GetPublishedDataTrack(uint16(req.PubHandle))
if dt == nil {
p.pubLogger.Warnw("unpublish data track not found", nil, "req", logger.Proto(req))
p.sendRequestResponse(&livekit.RequestResponse{
Reason: livekit.RequestResponse_NOT_FOUND,
Request: &livekit.RequestResponse_UnpublishDataTrack{
UnpublishDataTrack: utils.CloneProto(req),
},
})
return
}
p.UpDataTrackManager.RemovePublishedDataTrack(dt)
p.sendUnpublishDataTrackResponse(dt.ToProto())
p.dirty.Store(true)
}
func (p *ParticipantImpl) HandleUpdateDataSubscription(req *livekit.UpdateDataSubscription) {
p.listener().OnUpdateDataSubscriptions(p, req)
}
func (p *ParticipantImpl) onReceivedDataTrackMessage(data []byte, arrivalTime int64) {
var packet datatrack.Packet
if err := packet.Unmarshal(data); err != nil {
p.params.Logger.Errorw("could not unmarshal data track message", err)
return
}
p.UpDataTrackManager.HandleReceivedDataTrackMessage(data, &packet, arrivalTime)
p.listener().OnDataTrackMessage(p, data, &packet)
}
// wraps the promoted UpTrackManager.UpdateSubscriptionPermission to also revoke
// data track subscriptions that are no longer permitted
func (p *ParticipantImpl) UpdateSubscriptionPermission(
subscriptionPermission *livekit.SubscriptionPermission,
timedVersion utils.TimedVersion,
resolverBySid func(participantID livekit.ParticipantID) types.LocalParticipant,
) error {
if err := p.UpTrackManager.UpdateSubscriptionPermission(subscriptionPermission, timedVersion, resolverBySid); err != nil {
return err
}
p.maybeRevokeDataTrackSubscriptions()
return nil
}
func (p *ParticipantImpl) maybeRevokeDataTrackSubscriptions() {
for _, dt := range p.UpDataTrackManager.GetPublishedDataTracks() {
allowed := p.UpTrackManager.GetAllowedSubscribers(dt.ID())
if allowed == nil {
// no restrictions
continue
}
dt.RevokeDisallowedSubscribers(allowed)
}
}
func (p *ParticipantImpl) GetNextSubscribedDataTrackHandle() uint16 {
p.lock.Lock()
defer p.lock.Unlock()
p.nextSubscribedDataTrackHandle++
if p.nextSubscribedDataTrackHandle == 0 {
p.nextSubscribedDataTrackHandle++
}
return p.nextSubscribedDataTrackHandle
}

File diff suppressed because it is too large Load Diff

408
pkg/rtc/participant_sdp.go Normal file
View File

@@ -0,0 +1,408 @@
// 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 rtc
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
lksdp "github.com/livekit/protocol/sdp"
"github.com/livekit/protocol/utils"
)
func (p *ParticipantImpl) populateSdpCid(parsedOffer *sdp.SessionDescription) ([]*sdp.MediaDescription, []*sdp.MediaDescription) {
processUnmatch := func(unmatches []*sdp.MediaDescription, trackType livekit.TrackType) {
for _, unmatch := range unmatches {
streamID, ok := lksdp.ExtractStreamID(unmatch)
if !ok {
continue
}
sdpCodecs, err := lksdp.CodecsFromMediaDescription(unmatch)
if err != nil || len(sdpCodecs) == 0 {
p.pubLogger.Errorw(
"extract codecs from media section failed", err,
"media", unmatch,
"parsedOffer", parsedOffer,
)
continue
}
p.pendingTracksLock.Lock()
signalCid, info, _, migrated, _ := p.getPendingTrack(streamID, trackType, false)
if migrated {
p.pendingTracksLock.Unlock()
continue
}
if info == nil {
p.pendingTracksLock.Unlock()
// could be already published track and the unmatch could be a back up codec publish
numUnmatchedTracks := 0
var unmatchedTrack types.MediaTrack
var unmatchedSdpMimeType mime.MimeType
found := false
for _, sdpCodec := range sdpCodecs {
sdpMimeType := mime.NormalizeMimeTypeCodec(sdpCodec.Name).ToMimeType()
for _, publishedTrack := range p.GetPublishedTracks() {
if sigCid, sdpCid := publishedTrack.(*MediaTrack).GetCidsForMimeType(sdpMimeType); sigCid != "" && sdpCid == "" {
// a back up codec has a SDP cid match
if sigCid == streamID {
found = true
break
} else {
numUnmatchedTracks++
unmatchedTrack = publishedTrack
unmatchedSdpMimeType = sdpMimeType
}
}
}
if found {
break
}
}
if !found && unmatchedTrack != nil {
if numUnmatchedTracks != 1 {
p.pubLogger.Warnw(
"too many unmatched tracks", nil,
"media", unmatch,
"parsedOffer", parsedOffer,
)
}
unmatchedTrack.(*MediaTrack).UpdateCodecSdpCid(unmatchedSdpMimeType, streamID)
p.pubLogger.Debugw(
"published track SDP cid updated",
"trackID", unmatchedTrack.ID(),
"track", logger.Proto(unmatchedTrack.ToProto()),
)
}
continue
}
if len(info.Codecs) == 0 {
p.pendingTracksLock.Unlock()
p.pubLogger.Warnw(
"track without codecs", nil,
"trackID", info.Sid,
"pendingTrack", p.pendingTracks[signalCid],
"media", unmatch,
"parsedOffer", parsedOffer,
)
continue
}
found := false
updated := false
for _, sdpCodec := range sdpCodecs {
if mime.NormalizeMimeTypeCodec(sdpCodec.Name) == mime.GetMimeTypeCodec(info.Codecs[0].MimeType) {
// set SdpCid only if different from SignalCid
if streamID != info.Codecs[0].Cid {
info.Codecs[0].SdpCid = streamID
updated = true
}
found = true
break
}
if found {
break
}
}
if !found {
// not using SimulcastCodec, i. e. mime type not available till track publish
if len(info.Codecs) == 1 {
// set SdpCid only if different from SignalCid
if streamID != info.Codecs[0].Cid {
info.Codecs[0].SdpCid = streamID
updated = true
}
}
}
if updated {
p.pendingTracks[signalCid].trackInfos[0] = utils.CloneProto(info)
p.pubLogger.Debugw(
"pending track SDP cid updated",
"signalCid", signalCid,
"trackID", info.Sid,
"pendingTrack", p.pendingTracks[signalCid],
)
}
p.pendingTracksLock.Unlock()
}
}
unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(parsedOffer, "audio")
if err != nil {
p.pubLogger.Warnw("could not get unmatch audios", err)
return nil, nil
}
unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(parsedOffer, "video")
if err != nil {
p.pubLogger.Warnw("could not get unmatch videos", err)
return nil, nil
}
processUnmatch(unmatchAudios, livekit.TrackType_AUDIO)
processUnmatch(unmatchVideos, livekit.TrackType_VIDEO)
return unmatchAudios, unmatchVideos
}
func (p *ParticipantImpl) setCodecPreferencesForPublisher(
parsedOffer *sdp.SessionDescription,
unmatchAudios []*sdp.MediaDescription,
unmatchVideos []*sdp.MediaDescription,
) {
unprocessedUnmatchAudios := p.setCodecPreferencesForPublisherMedia(
parsedOffer,
unmatchAudios,
livekit.TrackType_AUDIO,
)
p.setCodecPreferencesOpusRedForPublisher(parsedOffer, unprocessedUnmatchAudios)
_ = p.setCodecPreferencesForPublisherMedia(
parsedOffer,
unmatchVideos,
livekit.TrackType_VIDEO,
)
}
func (p *ParticipantImpl) setCodecPreferencesForPublisherMedia(
parsedOffer *sdp.SessionDescription,
unmatches []*sdp.MediaDescription,
trackType livekit.TrackType,
) []*sdp.MediaDescription {
unprocessed := make([]*sdp.MediaDescription, 0, len(unmatches))
for _, unmatch := range unmatches {
var ti *livekit.TrackInfo
var mimeType string
mid := lksdp.GetMidValue(unmatch)
if mid == "" {
unprocessed = append(unprocessed, unmatch)
continue
}
transceiver := p.TransportManager.GetPublisherRTPTransceiver(mid)
if transceiver == nil {
unprocessed = append(unprocessed, unmatch)
continue
}
streamID, ok := lksdp.ExtractStreamID(unmatch)
if !ok {
unprocessed = append(unprocessed, unmatch)
continue
}
p.pendingTracksLock.RLock()
mt := p.getPublishedTrackBySdpCid(streamID)
if mt != nil {
ti = mt.ToProto()
} else {
_, ti, _, _, _ = p.getPendingTrack(streamID, trackType, false)
}
p.pendingTracksLock.RUnlock()
if ti == nil {
unprocessed = append(unprocessed, unmatch)
continue
}
for _, c := range ti.Codecs {
if c.Cid == streamID || c.SdpCid == streamID {
mimeType = c.MimeType
break
}
}
if mimeType == "" && len(ti.Codecs) > 0 {
mimeType = ti.Codecs[0].MimeType
}
if mimeType == "" {
unprocessed = append(unprocessed, unmatch)
continue
}
configureReceiverCodecs(
transceiver,
mimeType,
p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer(),
)
}
return unprocessed
}
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(
parsedOffer *sdp.SessionDescription,
unmatchAudios []*sdp.MediaDescription,
) {
for _, unmatchAudio := range unmatchAudios {
mid := lksdp.GetMidValue(unmatchAudio)
if mid == "" {
continue
}
transceiver := p.TransportManager.GetPublisherRTPTransceiver(mid)
if transceiver == nil {
continue
}
streamID, ok := lksdp.ExtractStreamID(unmatchAudio)
if !ok {
continue
}
p.pendingTracksLock.RLock()
_, ti, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
if ti == nil {
continue
}
codecs, err := lksdp.CodecsFromMediaDescription(unmatchAudio)
if err != nil {
p.pubLogger.Errorw(
"extract codecs from media section failed", err,
"media", unmatchAudio,
"parsedOffer", parsedOffer,
)
continue
}
var opusPayload uint8
for _, codec := range codecs {
if mime.IsMimeTypeCodecStringOpus(codec.Name) {
opusPayload = codec.PayloadType
break
}
}
if opusPayload == 0 {
continue
}
preferRED := IsRedEnabled(ti)
// if RED is enabled for this track, prefer RED codec in offer
for _, codec := range codecs {
// codec contain opus/red
if preferRED &&
mime.IsMimeTypeCodecStringRED(codec.Name) &&
strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
configureReceiverCodecs(transceiver, "audio/red", true)
break
}
}
}
}
// configure publisher answer for audio track's dtx and stereo settings
func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescription) webrtc.SessionDescription {
offer := p.TransportManager.LastPublisherOffer()
parsedOffer, err := offer.Unmarshal()
if err != nil {
return answer
}
parsedAnswer, err := answer.Unmarshal()
if err != nil {
return answer
}
for _, m := range parsedAnswer.MediaDescriptions {
switch m.MediaName.Media {
case "audio":
_, ok := m.Attribute(sdp.AttrKeyInactive)
if ok {
continue
}
mid, ok := m.Attribute(sdp.AttrKeyMID)
if !ok {
continue
}
// find track info from offer's stream id
var ti *livekit.TrackInfo
for _, om := range parsedOffer.MediaDescriptions {
_, ok := om.Attribute(sdp.AttrKeyInactive)
if ok {
continue
}
omid, ok := om.Attribute(sdp.AttrKeyMID)
if ok && omid == mid {
streamID, ok := lksdp.ExtractStreamID(om)
if !ok {
continue
}
track, _ := p.getPublishedTrackBySdpCid(streamID).(*MediaTrack)
if track == nil {
p.pendingTracksLock.RLock()
_, ti, _, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
} else {
ti = track.ToProto()
}
break
}
}
if ti == nil {
// no need to configure
continue
}
opusPT, err := parsedAnswer.GetPayloadTypeForCodec(sdp.Codec{Name: mime.MimeTypeCodecOpus.String()})
if err != nil {
p.pubLogger.Infow("failed to get opus payload type", "error", err, "trackID", ti.Sid)
continue
}
for i, attr := range m.Attributes {
if strings.HasPrefix(attr.String(), fmt.Sprintf("fmtp:%d", opusPT)) {
if !slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_NO_DTX) {
attr.Value += ";usedtx=1"
} else {
attr.Value = strings.ReplaceAll(attr.Value, ";usedtx=1", "")
}
if slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_STEREO) {
attr.Value += ";stereo=1;maxaveragebitrate=510000"
} else {
attr.Value = strings.ReplaceAll(attr.Value, ";stereo=1", "")
}
m.Attributes[i] = attr
}
}
default:
continue
}
}
bytes, err := parsedAnswer.Marshal()
if err != nil {
p.pubLogger.Infow("failed to marshal answer", "error", err)
return answer
}
answer.SDP = string(bytes)
return answer
}

View File

@@ -0,0 +1,436 @@
// 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 rtc
import (
"time"
"github.com/pion/webrtc/v4"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
protosignalling "github.com/livekit/protocol/signalling"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func (p *ParticipantImpl) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
p.signaller.SwapResponseSink(sink, reason)
}
func (p *ParticipantImpl) GetResponseSink() routing.MessageSink {
return p.signaller.GetResponseSink()
}
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
p.signaller.CloseSignalConnection(reason)
}
func (p *ParticipantImpl) SendJoinResponse(joinResponse *livekit.JoinResponse) error {
// keep track of participant updates and versions
p.updateLock.Lock()
for _, op := range joinResponse.OtherParticipants {
p.updateCache.Add(livekit.ParticipantID(op.Sid), participantUpdateInfo{
identity: livekit.ParticipantIdentity(op.Identity),
version: op.Version,
state: op.State,
updatedAt: time.Now(),
})
}
p.updateLock.Unlock()
// send Join response
err := p.signaller.WriteMessage(p.signalling.SignalJoinResponse(joinResponse))
if err != nil {
return err
}
// update state after sending message, so that no participant updates could slip through before JoinResponse is sent
p.updateLock.Lock()
if p.State() == livekit.ParticipantInfo_JOINING {
p.updateState(livekit.ParticipantInfo_JOINED)
}
queuedUpdates := p.queuedUpdates
p.queuedUpdates = nil
p.updateLock.Unlock()
if len(queuedUpdates) > 0 {
return p.SendParticipantUpdate(queuedUpdates)
}
return nil
}
func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.ParticipantInfo) error {
p.updateLock.Lock()
if p.IsDisconnected() {
p.updateLock.Unlock()
return nil
}
// read under the lock the flush takes, so an update either queues before the flush
// or goes out after the connection has opened
if !p.IsReady() || p.signaller.HandshakePending() {
// queue up updates
p.queuedUpdates = append(p.queuedUpdates, participantsToUpdate...)
p.updateLock.Unlock()
return nil
}
validUpdates := make([]*livekit.ParticipantInfo, 0, len(participantsToUpdate))
for _, pi := range participantsToUpdate {
isValid := true
pID := livekit.ParticipantID(pi.Sid)
if lastVersion, ok := p.updateCache.Get(pID); ok {
// this is a message delivered out of order, a more recent version of the message had already been
// sent.
if pi.Version < lastVersion.version {
p.params.Logger.Debugw(
"skipping outdated participant update",
"otherParticipant", pi.Identity,
"otherPID", pi.Sid,
"version", pi.Version,
"lastVersion", lastVersion,
)
isValid = false
}
}
if pi.Permission != nil && pi.Permission.Hidden && pi.Sid != string(p.ID()) {
p.params.Logger.Debugw("skipping hidden participant update", "otherParticipant", pi.Identity)
isValid = false
}
if isValid {
p.updateCache.Add(pID, participantUpdateInfo{
identity: livekit.ParticipantIdentity(pi.Identity),
version: pi.Version,
state: pi.State,
updatedAt: time.Now(),
})
validUpdates = append(validUpdates, pi)
}
}
p.updateLock.Unlock()
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(validUpdates))
}
// SendSpeakerUpdate notifies participant changes to speakers. only send members that have changed since last update
func (p *ParticipantImpl) SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, force bool) error {
if !p.IsReady() {
return nil
}
var scopedSpeakers []*livekit.SpeakerInfo
if force {
scopedSpeakers = speakers
} else {
for _, s := range speakers {
participantID := livekit.ParticipantID(s.Sid)
if p.IsSubscribedTo(participantID) || participantID == p.ID() {
scopedSpeakers = append(scopedSpeakers, s)
}
}
}
return p.signaller.WriteMessage(p.signalling.SignalSpeakerUpdate(scopedSpeakers))
}
func (p *ParticipantImpl) SendRoomUpdate(room *livekit.Room) error {
return p.signaller.WriteMessage(p.signalling.SignalRoomUpdate(room))
}
func (p *ParticipantImpl) SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error {
return p.signaller.WriteMessage(p.signalling.SignalConnectionQualityUpdate(update))
}
func (p *ParticipantImpl) SendRefreshToken(token string) error {
return p.signaller.WriteMessage(p.signalling.SignalRefreshToken(token))
}
func (p *ParticipantImpl) sendRequestResponse(requestResponse *livekit.RequestResponse) error {
if !p.params.ClientInfo.SupportsRequestResponse() {
return nil
}
if requestResponse.Reason == livekit.RequestResponse_OK && !p.ProtocolVersion().SupportsNonErrorSignalResponse() {
return nil
}
return p.signaller.WriteMessage(p.signalling.SignalRequestResponse(requestResponse))
}
func (p *ParticipantImpl) SendRoomMovedResponse(roomMovedResponse *livekit.RoomMovedResponse) error {
return p.signaller.WriteMessage(p.signalling.SignalRoomMovedResponse(roomMovedResponse))
}
func (p *ParticipantImpl) HandleReconnectAndSendResponse(reconnectReason livekit.ReconnectReason, reconnectResponse *livekit.ReconnectResponse) error {
p.TransportManager.HandleClientReconnect(reconnectReason)
if !p.params.ClientInfo.CanHandleReconnectResponse() {
// no ReconnectResponse opens this connection, so nothing to hold back
return p.reconnectResponseSentAndFlush()
}
// send reconnect response
err := p.signaller.WriteMessage(p.signalling.SignalReconnectResponse(reconnectResponse))
// mark sent after sending the message, so that nothing could slip through before
// ReconnectResponse is sent. Marked on a failed write too: the sink is gone in that
// case, and leaving it unmarked would hold back every message after it.
flushErr := p.reconnectResponseSentAndFlush()
if err != nil {
return err
}
if flushErr != nil {
return flushErr
}
if p.params.ProtocolVersion.SupportsDisconnectedUpdate() {
return p.sendDisconnectUpdatesForReconnect()
}
return nil
}
// reconnectResponseSentAndFlush makes a migrating in participant ready and sends what was
// queued up while the connection was waiting for its ReconnectResponse.
func (p *ParticipantImpl) reconnectResponseSentAndFlush() error {
// a successful write opens the connection by itself, this covers the paths that do
// not write one, i. e. a client that cannot handle it and a failed write
p.signaller.OpenHandshake()
p.updateLock.Lock()
p.reconnectResponseSent.Store(true)
queuedUpdates := p.queuedUpdates
p.queuedUpdates = nil
p.updateLock.Unlock()
if len(queuedUpdates) > 0 {
return p.SendParticipantUpdate(queuedUpdates)
}
return nil
}
// flushQueuedUpdates sends the updates queued while the connection was closed for the
// handshake. Called when the connection opens, including when it opens without a
// ReconnectResponse having been written.
func (p *ParticipantImpl) flushQueuedUpdates() {
p.updateLock.Lock()
queuedUpdates := p.queuedUpdates
p.queuedUpdates = nil
p.updateLock.Unlock()
if len(queuedUpdates) == 0 {
return
}
if err := p.SendParticipantUpdate(queuedUpdates); err != nil {
p.params.Logger.Warnw("could not send queued participant updates", err)
}
}
func (p *ParticipantImpl) sendDisconnectUpdatesForReconnect() error {
lastSignalAt := p.TransportManager.LastSeenSignalAt()
var disconnectedParticipants []*livekit.ParticipantInfo
p.updateLock.Lock()
keys := p.updateCache.Keys()
for i := len(keys) - 1; i >= 0; i-- {
if info, ok := p.updateCache.Get(keys[i]); ok {
if info.updatedAt.Before(lastSignalAt) {
break
} else if info.state == livekit.ParticipantInfo_DISCONNECTED {
disconnectedParticipants = append(disconnectedParticipants, &livekit.ParticipantInfo{
Sid: string(keys[i]),
Identity: string(info.identity),
Version: info.version,
State: livekit.ParticipantInfo_DISCONNECTED,
})
}
}
}
p.updateLock.Unlock()
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(disconnectedParticipants))
}
func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
prevIC := p.icQueue[target].Swap(ic)
if prevIC == nil {
return nil
}
trickle := protosignalling.ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
p.params.Logger.Debugw("sending ICE candidate", "transport", target, "trickle", logger.Proto(trickle))
return p.signaller.WriteMessage(p.signalling.SignalICECandidate(trickle))
}
func (p *ParticipantImpl) sendTrackMuted(trackID livekit.TrackID, muted bool) {
_ = p.signaller.WriteMessage(p.signalling.SignalTrackMuted(&livekit.MuteTrackRequest{
Sid: string(trackID),
Muted: muted,
}))
}
func (p *ParticipantImpl) sendTrackPublished(cid string, ti *livekit.TrackInfo) error {
p.pubLogger.Debugw("sending track published", "cid", cid, "trackInfo", logger.Proto(ti))
return p.signaller.WriteMessage(p.signalling.SignalTrackPublished(&livekit.TrackPublishedResponse{
Cid: cid,
Track: ti,
}))
}
func (p *ParticipantImpl) sendTrackUnpublished(trackID livekit.TrackID) {
_ = p.signaller.WriteMessage(p.signalling.SignalTrackUnpublished(&livekit.TrackUnpublishedResponse{
TrackSid: string(trackID),
}))
}
func (p *ParticipantImpl) sendTrackHasBeenSubscribed(trackID livekit.TrackID) {
if !p.params.ClientInfo.SupportsTrackSubscribedEvent() {
return
}
_ = p.signaller.WriteMessage(p.signalling.SignalTrackSubscribed(&livekit.TrackSubscribed{
TrackSid: string(trackID),
}))
p.params.Logger.Debugw("track has been subscribed", "trackID", trackID)
}
func (p *ParticipantImpl) sendLeaveRequest(
reason types.ParticipantCloseReason,
isExpectedToResume bool,
isExpectedToReconnect bool,
sendOnlyIfSupportingLeaveRequestWithAction bool,
) error {
var leave *livekit.LeaveRequest
if p.ProtocolVersion().SupportsRegionsInLeaveRequest() {
leave = &livekit.LeaveRequest{
Reason: reason.ToDisconnectReason(),
}
switch {
case isExpectedToResume:
leave.Action = livekit.LeaveRequest_RESUME
case isExpectedToReconnect:
leave.Action = livekit.LeaveRequest_RECONNECT
default:
leave.Action = livekit.LeaveRequest_DISCONNECT
}
if leave.Action != livekit.LeaveRequest_DISCONNECT {
// sending region settings even for RESUME just in case client wants to a full reconnect despite server saying RESUME
leave.Regions = p.helper().GetRegionSettings(p.params.ClientInfo.Address)
}
} else {
if !sendOnlyIfSupportingLeaveRequestWithAction {
leave = &livekit.LeaveRequest{
CanReconnect: isExpectedToReconnect,
Reason: reason.ToDisconnectReason(),
}
}
}
if leave != nil {
return p.signaller.WriteMessage(p.signalling.SignalLeaveRequest(leave))
}
return nil
}
func (p *ParticipantImpl) sendSdpAnswer(answer webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error {
return p.signaller.WriteMessage(p.signalling.SignalSdpAnswer(protosignalling.ToProtoSessionDescription(answer, answerId, midToTrackID)))
}
func (p *ParticipantImpl) sendSdpOffer(offer webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
return p.signaller.WriteMessage(p.signalling.SignalSdpOffer(protosignalling.ToProtoSessionDescription(offer, offerId, midToTrackID)))
}
func (p *ParticipantImpl) sendStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) error {
return p.signaller.WriteMessage(p.signalling.SignalStreamStateUpdate(streamStateUpdate))
}
func (p *ParticipantImpl) sendSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) error {
return p.signaller.WriteMessage(p.signalling.SignalSubscribedQualityUpdate(subscribedQualityUpdate))
}
func (p *ParticipantImpl) sendSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) error {
return p.signaller.WriteMessage(p.signalling.SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate))
}
func (p *ParticipantImpl) sendSubscriptionResponse(trackID livekit.TrackID, subErr livekit.SubscriptionError) error {
return p.signaller.WriteMessage(p.signalling.SignalSubscriptionResponse(&livekit.SubscriptionResponse{
TrackSid: string(trackID),
Err: subErr,
}))
}
func (p *ParticipantImpl) SendSubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool) error {
p.subLogger.Debugw("sending subscription permission update", "publisherID", publisherID, "trackID", trackID, "allowed", allowed)
err := p.signaller.WriteMessage(p.signalling.SignalSubscriptionPermissionUpdate(&livekit.SubscriptionPermissionUpdate{
ParticipantSid: string(publisherID),
TrackSid: string(trackID),
Allowed: allowed,
}))
if err != nil {
p.subLogger.Errorw("could not send subscription permission update", err)
}
return err
}
func (p *ParticipantImpl) sendMediaSectionsRequirement(numAudios uint32, numVideos uint32) error {
p.pubLogger.Debugw(
"sending media sections requirement",
"numAudios", numAudios,
"numVideos", numVideos,
)
err := p.signaller.WriteMessage(p.signalling.SignalMediaSectionsRequirement(&livekit.MediaSectionsRequirement{
NumAudios: numAudios,
NumVideos: numVideos,
}))
if err != nil {
p.subLogger.Errorw("could not send media sections requirement", err)
}
return err
}
func (p *ParticipantImpl) sendPublishDataTrackResponse(dti *livekit.DataTrackInfo) error {
return p.signaller.WriteMessage(p.signalling.SignalPublishDataTrackResponse(&livekit.PublishDataTrackResponse{
Info: dti,
}))
}
func (p *ParticipantImpl) sendUnpublishDataTrackResponse(dti *livekit.DataTrackInfo) error {
return p.signaller.WriteMessage(p.signalling.SignalUnpublishDataTrackResponse(&livekit.UnpublishDataTrackResponse{
Info: dti,
}))
}
func (p *ParticipantImpl) SendDataTrackSubscriberHandles(handles map[uint32]*livekit.DataTrackSubscriberHandles_PublishedDataTrack) error {
return p.signaller.WriteMessage(p.signalling.SignalDataTrackSubscriberHandles(&livekit.DataTrackSubscriberHandles{
SubHandles: handles,
}))
}
func (p *ParticipantImpl) sendStoreDataBlobResponse(requestId uint32, key *livekit.DataBlobKey) error {
return p.signaller.WriteMessage(p.signalling.SignalStoreDataBlobResponse(&livekit.StoreDataBlobResponse{
RequestId: requestId,
Key: key,
}))
}
func (p *ParticipantImpl) sendGetDataBlobResponse(requestId uint32, dataBlob *livekit.DataBlob) error {
return p.signaller.WriteMessage(p.signalling.SignalGetDataBlobResponse(&livekit.GetDataBlobResponse{
RequestId: requestId,
Blob: dataBlob,
}))
}

2428
pkg/rtc/room.go Normal file

File diff suppressed because it is too large Load Diff

869
pkg/rtc/room_test.go Normal file
View File

@@ -0,0 +1,869 @@
// 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 rtc
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/auth/authfakes"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/webhook"
"github.com/livekit/livekit-server/version"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/audio"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
"github.com/livekit/livekit-server/pkg/testutils"
)
func init() {
prometheus.Init("test", livekit.NodeType_SERVER)
}
const (
numParticipants = 3
defaultDelay = 10 * time.Millisecond
audioUpdateInterval = 25
)
func init() {
config.InitLoggerFromConfig(&config.DefaultConfig.Logging)
roomUpdateInterval = defaultDelay
}
var iceServersForRoom = []*livekit.ICEServer{{Urls: []string{"stun:stun.l.google.com:19302"}}}
func TestJoinedState(t *testing.T) {
t.Run("new room should return joinedAt 0", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
require.Equal(t, int64(0), rm.FirstJoinedAt())
require.Equal(t, int64(0), rm.LastLeftAt())
})
t.Run("should be current time when a participant joins", func(t *testing.T) {
s := time.Now().Unix()
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
require.LessOrEqual(t, s, rm.FirstJoinedAt())
require.Equal(t, int64(0), rm.LastLeftAt())
})
t.Run("should be set when a participant leaves", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
p0 := rm.GetParticipants()[0]
s := time.Now().Unix()
rm.RemoveParticipant(p0.Identity(), p0.ID(), types.ParticipantCloseReasonClientRequestLeave)
require.LessOrEqual(t, s, rm.LastLeftAt())
})
t.Run("LastLeftAt should be set when there are still participants in the room", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
p0 := rm.GetParticipants()[0]
rm.RemoveParticipant(p0.Identity(), p0.ID(), types.ParticipantCloseReasonClientRequestLeave)
require.Greater(t, rm.LastLeftAt(), int64(0))
})
}
func TestRoomJoin(t *testing.T) {
t.Run("joining returns existing participant data", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
_ = rm.Join(pNew, nil, nil, iceServersForRoom)
// expect new participant to get a JoinReply
res := pNew.SendJoinResponseArgsForCall(0)
require.Equal(t, livekit.RoomID(res.Room.Sid), rm.ID())
require.Len(t, res.OtherParticipants, numParticipants)
require.Len(t, rm.GetParticipants(), numParticipants+1)
require.NotEmpty(t, res.IceServers)
})
t.Run("subscribe to existing channels upon join", func(t *testing.T) {
numExisting := 3
rm := newRoomWithParticipants(t, testRoomOpts{num: numExisting})
lpl := rm.LocalParticipantListener()
p := NewMockParticipant("new", types.CurrentProtocol, false, false, lpl)
err := rm.Join(p, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
lpl.OnStateChange(p)
// it should become a subscriber when connectivity changes
numTracks := 0
for _, op := range rm.GetParticipants() {
if p == op {
continue
}
numTracks += len(op.GetPublishedTracks())
}
require.Equal(t, numTracks, p.SubscribeToTrackCallCount())
})
t.Run("participant state change is broadcasted to others", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: numParticipants})
var changedParticipant types.Participant
rm.OnParticipantChanged(func(participant types.Participant) {
changedParticipant = participant
})
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
disconnectedParticipant := participants[1].(*typesfakes.FakeLocalParticipant)
disconnectedParticipant.StateReturns(livekit.ParticipantInfo_DISCONNECTED)
rm.RemoveParticipant(p.Identity(), p.ID(), types.ParticipantCloseReasonClientRequestLeave)
time.Sleep(defaultDelay)
require.Equal(t, p, changedParticipant)
numUpdates := 0
for _, op := range participants {
if op == p || op == disconnectedParticipant {
require.Zero(t, p.SendParticipantUpdateCallCount())
continue
}
fakeP := op.(*typesfakes.FakeLocalParticipant)
require.Equal(t, 1, fakeP.SendParticipantUpdateCallCount())
numUpdates += 1
}
require.Equal(t, numParticipants-2, numUpdates)
})
t.Run("cannot exceed max participants", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
rm.lock.Lock()
rm.protoRoom.MaxParticipants = 1
rm.lock.Unlock()
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false, rm.LocalParticipantListener())
err := rm.Join(p, nil, nil, iceServersForRoom)
require.Equal(t, ErrMaxParticipantsExceeded, err)
})
}
// various state changes to participant and that others are receiving update
func TestParticipantUpdate(t *testing.T) {
tests := []struct {
name string
sendToSender bool // should sender receive it
action func(p types.LocalParticipant)
}{
{
"track mutes are sent to everyone",
true,
func(p types.LocalParticipant) {
p.SetTrackMuted(&livekit.MuteTrackRequest{Muted: true}, false)
},
},
{
"track metadata updates are sent to everyone",
true,
func(p types.LocalParticipant) {
p.SetMetadata("")
},
},
{
"track publishes are sent to existing participants",
true,
func(p types.LocalParticipant) {
p.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
})
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
// remember how many times send has been called for each
callCounts := make(map[livekit.ParticipantID]int)
for _, p := range rm.GetParticipants() {
fp := p.(*typesfakes.FakeLocalParticipant)
callCounts[p.ID()] = fp.SendParticipantUpdateCallCount()
}
sender := rm.GetParticipants()[0]
test.action(sender)
// go through the other participants, make sure they've received update
for _, p := range rm.GetParticipants() {
expected := callCounts[p.ID()]
if p != sender || test.sendToSender {
expected += 1
}
fp := p.(*typesfakes.FakeLocalParticipant)
require.Equal(t, expected, fp.SendParticipantUpdateCallCount())
}
})
}
}
func TestPushAndDequeueUpdates(t *testing.T) {
identity := "test_user"
publisher1v1 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
IsPublisher: true,
Version: 1,
JoinedAt: 0,
}
publisher1v2 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
IsPublisher: true,
Version: 2,
JoinedAt: 1,
}
publisher2 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "2",
IsPublisher: true,
Version: 1,
JoinedAt: 2,
}
subscriber1v1 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 1,
JoinedAt: 0,
}
subscriber1v2 := &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 2,
JoinedAt: 1,
}
requirePIEquals := func(t *testing.T, a, b *livekit.ParticipantInfo) {
require.Equal(t, a.Sid, b.Sid)
require.Equal(t, a.Identity, b.Identity)
require.Equal(t, a.Version, b.Version)
}
testCases := []struct {
name string
pi *livekit.ParticipantInfo
closeReason types.ParticipantCloseReason
immediate bool
existing *ParticipantUpdate
expected []*ParticipantUpdate
validate func(t *testing.T, rm *Room, updates []*ParticipantUpdate)
}{
{
name: "publisher updates are immediate",
pi: publisher1v1,
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v1}},
},
{
name: "subscriber updates are queued",
pi: subscriber1v1,
},
{
name: "last version is enqueued",
pi: subscriber1v2,
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)}, // clone the existing value since it can be modified when setting to disconnected
validate: func(t *testing.T, rm *Room, _ []*ParticipantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
require.NotNil(t, queued)
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
},
},
{
name: "latest version when immediate",
pi: subscriber1v2,
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
immediate: true,
expected: []*ParticipantUpdate{{ParticipantInfo: subscriber1v2}},
validate: func(t *testing.T, rm *Room, _ []*ParticipantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
require.Nil(t, queued)
},
},
{
name: "out of order updates are rejected",
pi: subscriber1v1,
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v2)},
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
},
},
{
name: "sid change is broadcasted immediately with synthsized disconnect",
pi: publisher2,
closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant, // just to test if update contain the close reason
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v2), CloseReason: types.ParticipantCloseReasonStale},
expected: []*ParticipantUpdate{
{
ParticipantInfo: &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 2,
State: livekit.ParticipantInfo_DISCONNECTED,
},
IsSynthesizedDisconnect: true,
CloseReason: types.ParticipantCloseReasonStale,
},
{ParticipantInfo: publisher2, CloseReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
},
},
{
name: "when switching to publisher, queue is cleared",
pi: publisher1v2,
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v2}},
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
require.Empty(t, rm.batchedUpdates)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
if tc.existing != nil {
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.ParticipantInfo.Identity)] = tc.existing
}
rm.batchedUpdatesMu.Lock()
updates := PushAndDequeueUpdates(
tc.pi,
tc.closeReason,
tc.immediate,
rm.GetParticipant(livekit.ParticipantIdentity(tc.pi.Identity)),
rm.batchedUpdates,
)
rm.batchedUpdatesMu.Unlock()
require.Equal(t, len(tc.expected), len(updates))
for i, item := range tc.expected {
requirePIEquals(t, item.ParticipantInfo, updates[i].ParticipantInfo)
require.Equal(t, item.IsSynthesizedDisconnect, updates[i].IsSynthesizedDisconnect)
require.Equal(t, item.CloseReason, updates[i].CloseReason)
}
if tc.validate != nil {
tc.validate(t, rm, updates)
}
})
}
}
func TestRoomClosure(t *testing.T) {
t.Run("room closes after participant leaves", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
isClosed := false
var closeReason types.RoomCloseReason
rm.OnClose(func(reason types.RoomCloseReason) {
isClosed = true
closeReason = reason
})
p := rm.GetParticipants()[0]
rm.lock.Lock()
// allows immediate close after
rm.protoRoom.EmptyTimeout = 0
rm.lock.Unlock()
rm.RemoveParticipant(p.Identity(), p.ID(), types.ParticipantCloseReasonClientRequestLeave)
time.Sleep(time.Duration(rm.ToProto().DepartureTimeout)*time.Second + defaultDelay)
rm.CloseIfEmpty()
require.Len(t, rm.GetParticipants(), 0)
require.True(t, isClosed)
require.Equal(t, types.RoomCloseReasonIdleTimeout, closeReason)
require.Equal(t, ErrRoomClosed, rm.Join(p, nil, nil, iceServersForRoom))
})
t.Run("room does not close before empty timeout", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
isClosed := false
var closeReason types.RoomCloseReason
rm.OnClose(func(reason types.RoomCloseReason) {
isClosed = true
closeReason = reason
})
require.NotZero(t, rm.protoRoom.EmptyTimeout)
rm.CloseIfEmpty()
require.False(t, isClosed)
require.Equal(t, types.RoomCloseReasonUnknown, closeReason)
})
t.Run("room closes after empty timeout", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
isClosed := false
var closeReason types.RoomCloseReason
rm.OnClose(func(reason types.RoomCloseReason) {
isClosed = true
closeReason = reason
})
rm.lock.Lock()
rm.protoRoom.EmptyTimeout = 1
rm.lock.Unlock()
time.Sleep(1010 * time.Millisecond)
rm.CloseIfEmpty()
require.True(t, isClosed)
require.Equal(t, types.RoomCloseReasonIdleTimeout, closeReason)
})
}
func TestNewTrack(t *testing.T) {
t.Run("new track should be added to ready participants", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
lpl := rm.LocalParticipantListener()
participants := rm.GetParticipants()
p0 := participants[0].(*typesfakes.FakeLocalParticipant)
p0.StateReturns(livekit.ParticipantInfo_JOINED)
p1 := participants[1].(*typesfakes.FakeLocalParticipant)
p1.StateReturns(livekit.ParticipantInfo_ACTIVE)
pub := participants[2].(*typesfakes.FakeLocalParticipant)
// pub adds track
track := NewMockTrack(livekit.TrackType_VIDEO, "webcam")
lpl.OnTrackPublished(pub, track)
// only p1 should've been subscribed to
require.Equal(t, 0, p0.SubscribeToTrackCallCount())
require.Equal(t, 1, p1.SubscribeToTrackCallCount())
})
}
func TestActiveSpeakers(t *testing.T) {
t.Parallel()
getActiveSpeakerUpdates := func(p *typesfakes.FakeLocalParticipant) [][]*livekit.SpeakerInfo {
var updates [][]*livekit.SpeakerInfo
numCalls := p.SendSpeakerUpdateCallCount()
for i := range numCalls {
infos, _ := p.SendSpeakerUpdateArgsForCall(i)
updates = append(updates, infos)
}
return updates
}
audioUpdateDuration := (audioUpdateInterval + 10) * time.Millisecond
t.Run("participant should not be getting audio updates (protocol 2)", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1, protocol: 2})
defer rm.Close(types.RoomCloseReasonUnknown)
p := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
require.Empty(t, rm.GetActiveSpeakers())
time.Sleep(audioUpdateDuration)
updates := getActiveSpeakerUpdates(p)
require.Empty(t, updates)
})
t.Run("speakers should be sorted by loudness", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.RoomCloseReasonUnknown)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p2 := participants[1].(*typesfakes.FakeLocalParticipant)
p.GetAudioLevelReturns(20, true)
p2.GetAudioLevelReturns(10, true)
speakers := rm.GetActiveSpeakers()
require.Len(t, speakers, 2)
require.Equal(t, string(p.ID()), speakers[0].Sid)
require.Equal(t, string(p2.ID()), speakers[1].Sid)
})
t.Run("participants are getting audio updates (protocol 3+)", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: 3})
defer rm.Close(types.RoomCloseReasonUnknown)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
time.Sleep(time.Millisecond) // let the first update cycle run
p.GetAudioLevelReturns(30, true)
speakers := rm.GetActiveSpeakers()
require.NotEmpty(t, speakers)
require.Equal(t, string(p.ID()), speakers[0].Sid)
testutils.WithTimeout(t, func() string {
for _, op := range participants {
op := op.(*typesfakes.FakeLocalParticipant)
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return fmt.Sprintf("%s did not get any audio updates", op.Identity())
}
}
return ""
})
// no longer speaking, send update with empty items
p.GetAudioLevelReturns(127, false)
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(p)
lastUpdate := updates[len(updates)-1]
if len(lastUpdate) == 0 {
return "did not get updates of speaker going quiet"
}
if lastUpdate[0].Active {
return "speaker should not have been active"
}
return ""
})
})
t.Run("audio level is smoothed", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: 3, audioSmoothIntervals: 3})
defer rm.Close(types.RoomCloseReasonUnknown)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
op := participants[1].(*typesfakes.FakeLocalParticipant)
p.GetAudioLevelReturns(30, true)
convertedLevel := float32(audio.ConvertAudioLevel(30))
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return "no speaker updates received"
}
lastSpeakers := updates[len(updates)-1]
if len(lastSpeakers) == 0 {
return "no speakers in the update"
}
if lastSpeakers[0].Level > convertedLevel {
return ""
}
return "level mismatch"
})
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return "no updates received"
}
lastSpeakers := updates[len(updates)-1]
if len(lastSpeakers) == 0 {
return "no speakers found"
}
if lastSpeakers[0].Level > convertedLevel {
return ""
}
return "did not match expected levels"
})
p.GetAudioLevelReturns(127, false)
testutils.WithTimeout(t, func() string {
updates := getActiveSpeakerUpdates(op)
if len(updates) == 0 {
return "no speaker updates received"
}
lastSpeakers := updates[len(updates)-1]
if len(lastSpeakers) == 1 && !lastSpeakers[0].Active {
return ""
}
return "speakers didn't go back to zero"
})
})
}
func TestDataChannel(t *testing.T) {
t.Parallel()
const (
curAPI = iota
legacySID
legacyIdentity
)
modes := []int{
curAPI, legacySID, legacyIdentity,
}
modeNames := []string{
"cur", "legacy sid", "legacy identity",
}
setSource := func(mode int, dp *livekit.DataPacket, p types.LocalParticipant) {
switch mode {
case curAPI:
dp.ParticipantIdentity = string(p.Identity())
case legacySID:
dp.GetUser().ParticipantSid = string(p.ID())
case legacyIdentity:
dp.GetUser().ParticipantIdentity = string(p.Identity())
}
}
setDest := func(mode int, dp *livekit.DataPacket, p types.LocalParticipant) {
switch mode {
case curAPI:
dp.DestinationIdentities = []string{string(p.Identity())}
case legacySID:
dp.GetUser().DestinationSids = []string{string(p.ID())}
case legacyIdentity:
dp.GetUser().DestinationIdentities = []string{string(p.Identity())}
}
}
t.Run("participants should receive data", func(t *testing.T) {
for _, mode := range modes {
mode := mode
t.Run(modeNames[mode], func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
defer rm.Close(types.RoomCloseReasonUnknown)
lpl := rm.LocalParticipantListener()
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
packet := &livekit.DataPacket{
Kind: livekit.DataPacket_RELIABLE,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: []byte("message.."),
},
},
}
setSource(mode, packet, p)
packetExp := utils.CloneProto(packet)
if mode != legacySID {
packetExp.ParticipantIdentity = string(p.Identity())
packetExp.GetUser().ParticipantIdentity = string(p.Identity())
}
encoded, _ := proto.Marshal(packetExp)
lpl.OnDataMessage(p, packet.Kind, packet)
// ensure everyone has received the packet
for _, op := range participants {
fp := op.(*typesfakes.FakeLocalParticipant)
if fp == p {
require.Zero(t, fp.SendDataMessageCallCount())
continue
}
require.Equal(t, 1, fp.SendDataMessageCallCount())
_, got, _, _ := fp.SendDataMessageArgsForCall(0)
require.Equal(t, encoded, got)
}
})
}
})
t.Run("only one participant should receive the data", func(t *testing.T) {
for _, mode := range modes {
mode := mode
t.Run(modeNames[mode], func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 4})
defer rm.Close(types.RoomCloseReasonUnknown)
lpl := rm.LocalParticipantListener()
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p1 := participants[1].(*typesfakes.FakeLocalParticipant)
packet := &livekit.DataPacket{
Kind: livekit.DataPacket_RELIABLE,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: []byte("message to p1.."),
},
},
}
setSource(mode, packet, p)
setDest(mode, packet, p1)
packetExp := utils.CloneProto(packet)
if mode != legacySID {
packetExp.ParticipantIdentity = string(p.Identity())
packetExp.GetUser().ParticipantIdentity = string(p.Identity())
packetExp.DestinationIdentities = []string{string(p1.Identity())}
packetExp.GetUser().DestinationIdentities = []string{string(p1.Identity())}
}
encoded, _ := proto.Marshal(packetExp)
lpl.OnDataMessage(p, packet.Kind, packet)
// only p1 should receive the data
for _, op := range participants {
fp := op.(*typesfakes.FakeLocalParticipant)
if fp != p1 {
require.Zero(t, fp.SendDataMessageCallCount())
}
}
require.Equal(t, 1, p1.SendDataMessageCallCount())
_, got, _, _ := p1.SendDataMessageArgsForCall(0)
require.Equal(t, encoded, got)
})
}
})
t.Run("publishing disallowed", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.RoomCloseReasonUnknown)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p.CanPublishDataReturns(false)
packet := livekit.DataPacket{
Kind: livekit.DataPacket_RELIABLE,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: []byte{},
},
},
}
if p.CanPublishData() {
lpl := rm.LocalParticipantListener()
lpl.OnDataMessage(p, packet.Kind, &packet)
}
// no one should've been sent packet
for _, op := range participants {
fp := op.(*typesfakes.FakeLocalParticipant)
require.Zero(t, fp.SendDataMessageCallCount())
}
})
}
func TestHiddenParticipants(t *testing.T) {
t.Run("other participants don't receive hidden updates", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, numHidden: 1})
defer rm.Close(types.RoomCloseReasonUnknown)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
rm.Join(pNew, nil, nil, iceServersForRoom)
// expect new participant to get a JoinReply
res := pNew.SendJoinResponseArgsForCall(0)
require.Equal(t, livekit.RoomID(res.Room.Sid), rm.ID())
require.Len(t, res.OtherParticipants, 2)
require.Len(t, rm.GetParticipants(), 4)
require.NotEmpty(t, res.IceServers)
require.Equal(t, "testregion", res.ServerInfo.Region)
})
t.Run("hidden participant subscribes to tracks", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
lpl := rm.LocalParticipantListener()
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false, lpl)
err := rm.Join(hidden, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
hidden.StateReturns(livekit.ParticipantInfo_ACTIVE)
lpl.OnStateChange(hidden)
require.Eventually(t, func() bool { return hidden.SubscribeToTrackCallCount() == 2 }, 5*time.Second, 10*time.Millisecond)
})
}
func TestRoomUpdate(t *testing.T) {
t.Run("updates are sent when participant joined", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
defer rm.Close(types.RoomCloseReasonUnknown)
p1 := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
require.Equal(t, 0, p1.SendRoomUpdateCallCount())
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
require.NoError(t, rm.Join(p2, nil, nil, iceServersForRoom))
// p1 should have received an update
time.Sleep(2 * defaultDelay)
require.LessOrEqual(t, 1, p1.SendRoomUpdateCallCount())
require.EqualValues(t, 2, p1.SendRoomUpdateArgsForCall(p1.SendRoomUpdateCallCount()-1).NumParticipants)
})
t.Run("participants should receive metadata update", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.RoomCloseReasonUnknown)
rm.SetMetadata("test metadata...")
// callbacks are updated from goroutine
time.Sleep(2 * defaultDelay)
for _, op := range rm.GetParticipants() {
fp := op.(*typesfakes.FakeLocalParticipant)
// room updates are now sent for both participant joining and room metadata
require.GreaterOrEqual(t, fp.SendRoomUpdateCallCount(), 1)
}
})
}
type testRoomOpts struct {
num int
numHidden int
protocol types.ProtocolVersion
audioSmoothIntervals uint32
}
func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *Room {
kp := &authfakes.FakeKeyProvider{}
kp.GetSecretReturns("testkey")
n, err := webhook.NewDefaultNotifier(webhook.DefaultWebHookConfig, kp)
require.NoError(t, err)
rm := NewRoom(
&livekit.Room{Name: "room"},
nil,
WebRTCConfig{},
config.RoomConfig{
EmptyTimeout: 5 * 60,
DepartureTimeout: 1,
},
&sfu.AudioConfig{
AudioLevelConfig: audio.AudioLevelConfig{
UpdateInterval: audioUpdateInterval,
SmoothIntervals: opts.audioSmoothIntervals,
},
},
&livekit.ServerInfo{
Edition: livekit.ServerInfo_Standard,
Version: version.Version,
Protocol: types.CurrentProtocol,
NodeId: "testnode",
Region: "testregion",
},
telemetry.NewTelemetryService(n, &telemetryfakes.FakeAnalyticsService{}),
nil, nil, nil,
)
for i := 0; i < opts.num+opts.numHidden; i++ {
identity := livekit.ParticipantIdentity(fmt.Sprintf("p%d", i))
participant := NewMockParticipant(identity, opts.protocol, i >= opts.num, true, rm.LocalParticipantListener())
err := rm.Join(participant, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
participant.StateReturns(livekit.ParticipantInfo_ACTIVE)
participant.IsReadyReturns(true)
// each participant has a track
participant.GetPublishedTracksReturns([]types.MediaTrack{
&typesfakes.FakeMediaTrack{},
})
}
return rm
}

263
pkg/rtc/roomtrackmanager.go Normal file
View File

@@ -0,0 +1,263 @@
/*
* 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 rtc
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
// RoomTrackManager holds tracks that are published to the room
type RoomTrackManager struct {
logger logger.Logger
lock sync.RWMutex
changedNotifier *utils.ChangeNotifierManager
removedNotifier *utils.ChangeNotifierManager
tracks map[livekit.TrackID][]*TrackInfo
dataTracks map[livekit.TrackID][]*DataTrackInfo
}
type TrackInfo struct {
Track types.MediaTrack
PublisherIdentity livekit.ParticipantIdentity
PublisherID livekit.ParticipantID
}
type DataTrackInfo struct {
DataTrack types.DataTrack
PublisherIdentity livekit.ParticipantIdentity
PublisherID livekit.ParticipantID
}
func NewRoomTrackManager(logger logger.Logger) *RoomTrackManager {
return &RoomTrackManager{
logger: logger,
tracks: make(map[livekit.TrackID][]*TrackInfo),
dataTracks: make(map[livekit.TrackID][]*DataTrackInfo),
changedNotifier: utils.NewChangeNotifierManager(),
removedNotifier: utils.NewChangeNotifierManager(),
}
}
func (r *RoomTrackManager) AddTrack(track types.MediaTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
trackID := track.ID()
r.lock.Lock()
infos, ok := r.tracks[trackID]
if ok {
for _, info := range infos {
if info.Track == track {
r.lock.Unlock()
r.logger.Infow("not adding duplicate track", "trackID", trackID)
return
}
}
}
r.tracks[trackID] = append(r.tracks[trackID], &TrackInfo{
Track: track,
PublisherIdentity: publisherIdentity,
PublisherID: publisherID,
})
r.lock.Unlock()
r.NotifyTrackChanged(trackID)
}
func (r *RoomTrackManager) RemoveTrack(track types.MediaTrack) {
trackID := track.ID()
r.lock.Lock()
// ensure we are removing the same track as added
infos, ok := r.tracks[trackID]
if !ok {
r.lock.Unlock()
return
}
numRemoved := 0
idx := 0
for _, info := range infos {
if info.Track == track {
numRemoved++
} else {
r.tracks[trackID][idx] = info
idx++
}
}
for j := idx; j < len(infos); j++ {
r.tracks[trackID][j] = nil
}
r.tracks[trackID] = r.tracks[trackID][:idx]
if len(r.tracks[trackID]) == 0 {
delete(r.tracks, trackID)
}
r.lock.Unlock()
if numRemoved == 0 {
return
}
if numRemoved > 1 {
r.logger.Warnw("removed multiple tracks", nil, "trackID", trackID, "numRemoved", numRemoved)
}
n := r.removedNotifier.GetNotifier(string(trackID))
if n != nil {
n.NotifyChanged()
}
r.changedNotifier.RemoveNotifier(string(trackID), true)
r.removedNotifier.RemoveNotifier(string(trackID), true)
}
func (r *RoomTrackManager) GetTrackInfo(trackID livekit.TrackID) *TrackInfo {
r.lock.RLock()
defer r.lock.RUnlock()
infos := r.tracks[trackID]
if len(infos) == 0 {
return nil
}
// earliest added track is used till it is removed
info := infos[0]
// when track is about to close, do not resolve
if info.Track != nil && !info.Track.IsOpen() {
return nil
}
return info
}
func (r *RoomTrackManager) NotifyTrackChanged(trackID livekit.TrackID) {
n := r.changedNotifier.GetNotifier(string(trackID))
if n != nil {
n.NotifyChanged()
}
}
// HasObservers lets caller know if the current media track has any observers
// this is used to signal interest in the track. when another MediaTrack with the same
// trackID is being used, track is not considered to be observed.
func (r *RoomTrackManager) HasObservers(track types.MediaTrack) bool {
n := r.changedNotifier.GetNotifier(string(track.ID()))
if n == nil || !n.HasObservers() {
return false
}
info := r.GetTrackInfo(track.ID())
if info == nil || info.Track != track {
return false
}
return true
}
func (r *RoomTrackManager) GetOrCreateTrackChangeNotifier(trackID livekit.TrackID) *utils.ChangeNotifier {
return r.changedNotifier.GetOrCreateNotifier(string(trackID))
}
func (r *RoomTrackManager) GetOrCreateTrackRemoveNotifier(trackID livekit.TrackID) *utils.ChangeNotifier {
return r.removedNotifier.GetOrCreateNotifier(string(trackID))
}
func (r *RoomTrackManager) AddDataTrack(dataTrack types.DataTrack, publisherIdentity livekit.ParticipantIdentity, publisherID livekit.ParticipantID) {
trackID := dataTrack.ID()
r.lock.Lock()
infos, ok := r.dataTracks[trackID]
if ok {
for _, info := range infos {
if info.DataTrack == dataTrack {
r.lock.Unlock()
r.logger.Infow("not adding duplicate data track", "trackID", trackID)
return
}
}
}
r.dataTracks[trackID] = append(r.dataTracks[trackID], &DataTrackInfo{
DataTrack: dataTrack,
PublisherIdentity: publisherIdentity,
PublisherID: publisherID,
})
r.lock.Unlock()
r.NotifyTrackChanged(trackID)
}
func (r *RoomTrackManager) RemoveDataTrack(dataTrack types.DataTrack) {
trackID := dataTrack.ID()
r.lock.Lock()
// ensure we are removing the same track as added
infos, ok := r.dataTracks[trackID]
if !ok {
r.lock.Unlock()
return
}
numRemoved := 0
idx := 0
for _, info := range infos {
if info.DataTrack == dataTrack {
numRemoved++
} else {
r.dataTracks[trackID][idx] = info
idx++
}
}
for j := idx; j < len(infos); j++ {
r.dataTracks[trackID][j] = nil
}
r.dataTracks[trackID] = r.dataTracks[trackID][:idx]
if len(r.dataTracks[trackID]) == 0 {
delete(r.dataTracks, trackID)
}
r.lock.Unlock()
if numRemoved == 0 {
return
}
if numRemoved > 1 {
r.logger.Warnw("removed multiple data tracks", nil, "trackID", trackID, "numRemoved", numRemoved)
}
n := r.removedNotifier.GetNotifier(string(trackID))
if n != nil {
n.NotifyChanged()
}
r.changedNotifier.RemoveNotifier(string(trackID), true)
r.removedNotifier.RemoveNotifier(string(trackID), true)
}
func (r *RoomTrackManager) GetDataTrackInfo(trackID livekit.TrackID) *DataTrackInfo {
r.lock.RLock()
defer r.lock.RUnlock()
infos := r.dataTracks[trackID]
if len(infos) == 0 {
return nil
}
// earliest added data track is used till it is removed
return infos[0]
}
func (r *RoomTrackManager) Report() (int, int) {
r.lock.RLock()
defer r.lock.RUnlock()
return len(r.tracks), len(r.dataTracks)
}

View File

@@ -0,0 +1,311 @@
// 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 rtc
import (
"context"
"fmt"
"sync"
"time"
"github.com/frostbyte73/core"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/utils"
)
type BytesTrackType string
const (
BytesTrackTypeData BytesTrackType = "DT"
BytesTrackTypeSignal BytesTrackType = "SG"
)
// -------------------------------
type TrafficTotals struct {
At time.Time
SendBytes uint64
SendMessages uint32
RecvBytes uint64
RecvMessages uint32
}
// --------------------------------
// stats for signal and data channel
type BytesTrackStats struct {
country string
pID livekit.ParticipantID
kind livekit.ParticipantInfo_Kind
kindDetails []livekit.ParticipantInfo_KindDetail
trackID livekit.TrackID
send, recv atomic.Uint64
sendMessages, recvMessages atomic.Uint32
totalSendBytes, totalRecvBytes atomic.Uint64
totalSendMessages, totalRecvMessages atomic.Uint32
telemetryListener func() types.ParticipantTelemetryListener
reporter roomobs.TrackReporter
done core.Fuse
}
func NewBytesTrackStats(
country string,
trackID livekit.TrackID,
pID livekit.ParticipantID,
kind livekit.ParticipantInfo_Kind,
kindDetails []livekit.ParticipantInfo_KindDetail,
telemetryListener func() types.ParticipantTelemetryListener,
participantReporter roomobs.ParticipantSessionReporter,
) *BytesTrackStats {
s := &BytesTrackStats{
country: country,
pID: pID,
kind: kind,
kindDetails: kindDetails,
trackID: trackID,
telemetryListener: telemetryListener,
reporter: participantReporter.WithTrack(trackID.String()),
}
go s.worker()
return s
}
func (s *BytesTrackStats) AddBytes(bytes uint64, isSend bool) {
if isSend {
s.send.Add(bytes)
s.sendMessages.Inc()
s.totalSendBytes.Add(bytes)
s.totalSendMessages.Inc()
s.reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(s.kind))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(s.kindDetails))
tx.ReportType(roomobs.TrackTypeData)
tx.ReportSendBytes(uint32(bytes))
tx.ReportSendPackets(1)
})
} else {
s.recv.Add(bytes)
s.recvMessages.Inc()
s.totalRecvBytes.Add(bytes)
s.totalRecvMessages.Inc()
s.reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(s.kind))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(s.kindDetails))
tx.ReportType(roomobs.TrackTypeData)
tx.ReportRecvBytes(uint32(bytes))
tx.ReportRecvPackets(1)
})
}
}
func (s *BytesTrackStats) GetTrafficTotals() *TrafficTotals {
return &TrafficTotals{
At: time.Now(),
SendBytes: s.totalSendBytes.Load(),
SendMessages: s.totalSendMessages.Load(),
RecvBytes: s.totalRecvBytes.Load(),
RecvMessages: s.totalRecvMessages.Load(),
}
}
func (s *BytesTrackStats) Stop() {
s.done.Break()
}
func (s *BytesTrackStats) report() {
if recv := s.recv.Swap(0); recv > 0 {
packets := s.recvMessages.Swap(0)
s.telemetryListener().OnTrackStats(
telemetry.StatsKeyForData(s.country, livekit.StreamType_UPSTREAM, s.pID, s.trackID),
&livekit.AnalyticsStat{
Streams: []*livekit.AnalyticsStream{
{
PrimaryBytes: recv,
PrimaryPackets: packets,
},
},
},
)
}
if send := s.send.Swap(0); send > 0 {
packets := s.sendMessages.Swap(0)
s.telemetryListener().OnTrackStats(
telemetry.StatsKeyForData(s.country, livekit.StreamType_DOWNSTREAM, s.pID, s.trackID),
&livekit.AnalyticsStat{
Streams: []*livekit.AnalyticsStream{
{
PrimaryBytes: send,
PrimaryPackets: packets,
},
},
},
)
}
}
func (s *BytesTrackStats) worker() {
ticker := time.NewTicker(5 * time.Second)
defer func() {
ticker.Stop()
s.report()
}()
for {
select {
case <-s.done.Watch():
return
case <-ticker.C:
s.report()
}
}
}
// -----------------------------------------------------------------------
var _ types.ParticipantTelemetryListener = (*BytesSignalStats)(nil)
type BytesSignalStats struct {
BytesTrackStats
ctx context.Context
telemetry telemetry.TelemetryService
guard *telemetry.ReferenceGuard
participantResolver roomobs.ParticipantReporterResolver
trackResolver roomobs.KeyResolver
mu sync.Mutex
ri *livekit.Room
pi *livekit.ParticipantInfo
stopped chan struct{}
types.NullParticipantTelemetryListener
}
func NewBytesSignalStats(
ctx context.Context,
telemetryService telemetry.TelemetryService,
) *BytesSignalStats {
projectReporter := telemetryService.RoomProjectReporter(ctx)
participantReporter, participantReporterResolver := roomobs.DeferredParticipantReporter(projectReporter)
trackReporter, trackReporterResolver := participantReporter.WithDeferredTrack()
b := &BytesSignalStats{
ctx: ctx,
telemetry: telemetryService,
guard: &telemetry.ReferenceGuard{},
participantResolver: participantReporterResolver,
trackResolver: trackReporterResolver,
}
b.BytesTrackStats = BytesTrackStats{
telemetryListener: b.telemetryListener,
reporter: trackReporter,
}
return b
}
func (s *BytesSignalStats) ResolveRoom(ri *livekit.Room) {
s.mu.Lock()
defer s.mu.Unlock()
if s.ri == nil && ri.GetSid() != "" {
s.ri = &livekit.Room{
Sid: ri.Sid,
Name: ri.Name,
}
s.maybeStart()
}
}
func (s *BytesSignalStats) ResolveParticipant(pi *livekit.ParticipantInfo) {
s.mu.Lock()
defer s.mu.Unlock()
if s.pi == nil && pi != nil {
s.pi = &livekit.ParticipantInfo{
Sid: pi.Sid,
Identity: pi.Identity,
Kind: pi.Kind,
KindDetails: pi.KindDetails,
}
s.kind = pi.Kind
s.kindDetails = pi.KindDetails
s.maybeStart()
}
}
func (s *BytesSignalStats) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
if s.stopped != nil {
s.done.Break()
<-s.stopped
s.stopped = nil
s.done = core.Fuse{}
s.guard = &telemetry.ReferenceGuard{}
}
s.ri = nil
s.pi = nil
s.participantResolver.Reset()
s.trackResolver.Reset()
}
func (s *BytesSignalStats) maybeStart() {
if s.ri == nil || s.pi == nil {
return
}
s.pID = livekit.ParticipantID(s.pi.Sid)
s.trackID = BytesTrackIDForParticipantID(BytesTrackTypeSignal, s.pID)
s.participantResolver.Resolve(
livekit.RoomName(s.ri.Name),
livekit.RoomID(s.ri.Sid),
livekit.ParticipantIdentity(s.pi.Identity),
livekit.ParticipantID(s.pi.Sid),
)
s.trackResolver.Resolve(string(s.trackID))
s.telemetry.ParticipantJoined(s.ctx, s.ri, s.pi, nil, nil, false, s.guard)
s.stopped = make(chan struct{})
go s.worker()
}
func (s *BytesSignalStats) worker() {
s.BytesTrackStats.worker()
s.telemetry.ParticipantLeft(s.ctx, s.ri, s.pi, false, s.guard)
close(s.stopped)
}
func (s *BytesSignalStats) telemetryListener() types.ParticipantTelemetryListener {
return s
}
func (s *BytesSignalStats) OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat) {
stat.RoomId, stat.RoomName = s.ri.Sid, s.ri.Name
s.telemetry.TrackStats(livekit.RoomID(s.ri.Sid), livekit.RoomName(s.ri.Name), key, stat)
}
// -----------------------------------------------------------------------
func BytesTrackIDForParticipantID(typ BytesTrackType, participantID livekit.ParticipantID) livekit.TrackID {
return livekit.TrackID(fmt.Sprintf("%s%s%s", utils.TrackPrefix, typ, participantID))
}

View File

@@ -0,0 +1,27 @@
// 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 signalling
import (
"errors"
)
var (
ErrInvalidMessageType = errors.New("invalid message type")
ErrNameExceedsLimits = errors.New("name length exceeds limits")
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
ErrUpdateOwnMetadataNotAllowed = errors.New("update own metadata not allowed")
)

View File

@@ -0,0 +1,74 @@
// 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 signalling
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"google.golang.org/protobuf/proto"
)
type ParticipantSignalHandler interface {
HandleMessage(msg proto.Message) error
}
type ParticipantSignaller interface {
SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason)
GetResponseSink() routing.MessageSink
CloseSignalConnection(reason types.SignallingCloseReason)
// HandshakePending reports whether a resumed connection is still waiting for the
// ReconnectResponse it has to open with
HandshakePending() bool
// OpenHandshake lets messages flow on a resumed connection that does not open with
// a ReconnectResponse
OpenHandshake()
WriteMessage(msg proto.Message) error
}
type ParticipantSignalling interface {
SignalJoinResponse(join *livekit.JoinResponse) proto.Message
SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message
SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message
SignalRoomUpdate(room *livekit.Room) proto.Message
SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message
SignalRefreshToken(token string) proto.Message
SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message
SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message
SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message
SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message
SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message
SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message
SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message
SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message
SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message
SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message
SignalSdpOffer(offer *livekit.SessionDescription) proto.Message
SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message
SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message
SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message
SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message
SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message
SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message
SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message
SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message
SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message
SignalStoreDataBlobResponse(storeDataBlobResponse *livekit.StoreDataBlobResponse) proto.Message
SignalGetDataBlobResponse(getDataBlobResponse *livekit.GetDataBlobResponse) proto.Message
}

View File

@@ -0,0 +1,163 @@
// 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 signalling
import (
"fmt"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
var _ ParticipantSignalHandler = (*signalhandler)(nil)
type SignalHandlerParams struct {
Logger logger.Logger
Participant types.LocalParticipant
}
type signalhandler struct {
params SignalHandlerParams
}
func NewSignalHandler(params SignalHandlerParams) ParticipantSignalHandler {
return &signalhandler{
params: params,
}
}
func (s *signalhandler) HandleMessage(msg proto.Message) error {
req, ok := msg.(*livekit.SignalRequest)
if !ok {
s.params.Logger.Warnw(
"unknown message type", nil,
"messageType", fmt.Sprintf("%T", msg),
)
return ErrInvalidMessageType
}
s.params.Participant.UpdateLastSeenSignal()
s.params.Logger.Debugw("handling signal request", "request", logger.Proto(req))
switch msg := req.GetMessage().(type) {
case *livekit.SignalRequest_Offer:
s.params.Participant.HandleOffer(msg.Offer)
case *livekit.SignalRequest_Answer:
s.params.Participant.HandleAnswer(msg.Answer)
case *livekit.SignalRequest_Trickle:
s.params.Participant.HandleICETrickle(msg.Trickle)
case *livekit.SignalRequest_AddTrack:
s.params.Participant.AddTrack(msg.AddTrack)
case *livekit.SignalRequest_Mute:
s.params.Participant.SetTrackMuted(msg.Mute, false)
case *livekit.SignalRequest_Subscription:
// allow participant to indicate their interest in the subscription
// permission check happens later in SubscriptionManager
s.params.Participant.HandleUpdateSubscriptions(
livekit.StringsAsIDs[livekit.TrackID](msg.Subscription.TrackSids),
msg.Subscription.ParticipantTracks,
msg.Subscription.Subscribe,
)
case *livekit.SignalRequest_TrackSetting:
for _, sid := range livekit.StringsAsIDs[livekit.TrackID](msg.TrackSetting.TrackSids) {
s.params.Participant.UpdateSubscribedTrackSettings(sid, msg.TrackSetting)
}
case *livekit.SignalRequest_Leave:
reason := types.ParticipantCloseReasonClientRequestLeave
switch msg.Leave.Reason {
case livekit.DisconnectReason_CLIENT_INITIATED:
reason = types.ParticipantCloseReasonClientRequestLeave
case livekit.DisconnectReason_USER_UNAVAILABLE:
reason = types.ParticipantCloseReasonUserUnavailable
case livekit.DisconnectReason_USER_REJECTED:
reason = types.ParticipantCloseReasonUserRejected
case livekit.DisconnectReason_AGENT_ERROR:
reason = types.ParticipantCloseReasonAgentError
}
s.params.Logger.Debugw("client leaving room", "reason", reason)
s.params.Participant.HandleLeaveRequest(reason)
case *livekit.SignalRequest_SubscriptionPermission:
err := s.params.Participant.HandleUpdateSubscriptionPermission(msg.SubscriptionPermission)
if err != nil {
s.params.Logger.Warnw(
"could not update subscription permission", err,
"permissions", msg.SubscriptionPermission,
)
}
case *livekit.SignalRequest_SyncState:
err := s.params.Participant.HandleSyncState(msg.SyncState)
if err != nil {
s.params.Logger.Warnw(
"could not sync state", err,
"state", msg.SyncState,
)
}
case *livekit.SignalRequest_Simulate:
err := s.params.Participant.HandleSimulateScenario(msg.Simulate)
if err != nil {
s.params.Logger.Warnw(
"could not simulate scenario", err,
"simulate", msg.Simulate,
)
}
case *livekit.SignalRequest_PingReq:
if msg.PingReq.Rtt > 0 {
s.params.Participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
}
case *livekit.SignalRequest_UpdateMetadata:
s.params.Participant.UpdateMetadata(msg.UpdateMetadata, false)
case *livekit.SignalRequest_UpdateAudioTrack:
if err := s.params.Participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
s.params.Logger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
}
case *livekit.SignalRequest_UpdateVideoTrack:
if err := s.params.Participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
s.params.Logger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
}
case *livekit.SignalRequest_PublishDataTrackRequest:
s.params.Participant.HandlePublishDataTrackRequest(msg.PublishDataTrackRequest)
case *livekit.SignalRequest_UnpublishDataTrackRequest:
s.params.Participant.HandleUnpublishDataTrackRequest(msg.UnpublishDataTrackRequest)
case *livekit.SignalRequest_UpdateDataSubscription:
s.params.Participant.HandleUpdateDataSubscription(msg.UpdateDataSubscription)
case *livekit.SignalRequest_StoreDataBlobRequest:
s.params.Participant.HandleStoreDataBlobRequest(msg.StoreDataBlobRequest)
case *livekit.SignalRequest_GetDataBlobRequest:
s.params.Participant.HandleGetDataBlobRequest(msg.GetDataBlobRequest)
}
return nil
}

View File

@@ -0,0 +1,27 @@
// 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 signalling
import (
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignalHandler = (*signalhandlerUnimplemented)(nil)
type signalhandlerUnimplemented struct{}
func (u *signalhandlerUnimplemented) HandleMessage(msg proto.Message) error {
return nil
}

View File

@@ -0,0 +1,140 @@
// 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 signalling
import (
"fmt"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignaller = (*signallerAsync)(nil)
type SignallerAsyncParams struct {
Logger logger.Logger
Participant types.LocalParticipant
OnHandshakeOpened func()
}
type signallerAsync struct {
params SignallerAsyncParams
*signallerAsyncBase
}
func NewSignallerAsync(params SignallerAsyncParams) ParticipantSignaller {
return &signallerAsync{
params: params,
signallerAsyncBase: newSignallerAsyncBase(signallerAsyncBaseParams{
Logger: params.Logger,
OnHandshakeOpened: params.OnHandshakeOpened,
}),
}
}
func (s *signallerAsync) WriteMessage(msg proto.Message) error {
if msg == nil {
return nil
}
getMessageType := func(msg proto.Message) string {
messageType := "unknown"
if typed, ok := msg.(*livekit.SignalResponse); ok {
messageType = fmt.Sprintf("%T", typed.Message)
}
return messageType
}
if s.params.Participant.IsDisconnected() {
s.params.Logger.Debugw(
"could not send message to participant, participant disconnected",
"messageType", getMessageType(msg),
)
return nil
}
// a signal connection opens with a join response, or with a reconnect response when
// it is resumed or migrated in. The client reads that first message as the handshake,
// so nothing may go out ahead of it.
isHandshake := false
isSdp := false
if typed, ok := msg.(*livekit.SignalResponse); !ok {
s.params.Logger.Warnw(
"unknown message type", nil,
"messageType", fmt.Sprintf("%T", msg),
)
} else {
isHandshake = typed.GetJoin() != nil || typed.GetReconnect() != nil
isSdp = typed.GetOffer() != nil || typed.GetAnswer() != nil
}
if !isHandshake && (!s.params.Participant.IsReady() || s.HandshakePending()) {
logFunc := s.params.Logger.Debugw
if isSdp {
// a dropped SDP leaves the negotiation waiting for the peer until the
// negotiation state machine recovers it, so make it visible
logFunc = func(msg string, keysAndValues ...any) {
s.params.Logger.Infow(msg, keysAndValues...)
}
}
logFunc(
"dropping message, connection has not opened yet",
"messageType", getMessageType(msg),
)
return nil
}
sink := s.GetResponseSink()
if sink == nil {
s.params.Logger.Debugw(
"could not send message to participant, no sink",
"messageType", getMessageType(msg),
)
return nil
}
err := sink.WriteMessage(msg)
if err != nil {
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
s.params.Logger.Debugw(
"could not send message to participant",
"error", err,
"messageType", getMessageType(msg),
)
return nil
} else {
s.params.Logger.Warnw(
"could not send message to participant", err,
"messageType", getMessageType(msg),
)
return err
}
} else {
if isHandshake {
// the connection is open, hold nothing back any more
s.OpenHandshake()
}
s.params.Logger.Debugw("sent signal response", "response", logger.Proto(msg))
}
return nil
}

View File

@@ -0,0 +1,185 @@
// 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 signalling
import (
"sync"
"time"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
type signallerAsyncBaseParams struct {
Logger logger.Logger
// called when the connection opens, i. e. when messages held back for the
// handshake may be sent
OnHandshakeOpened func()
}
// how long a resumed connection holds messages back waiting for its ReconnectResponse.
// A path that resumes without sending one lets messages flow after this instead of
// holding them back for the rest of the session.
const handshakeWindow = 5 * time.Second
type signallerAsyncBase struct {
signallerUnimplemented
params signallerAsyncBaseParams
resSinkMu sync.Mutex
resSink routing.MessageSink
// set while a resumed connection holds messages back waiting for its
// ReconnectResponse, the timer opens the connection if none is written.
// handshakeGeneration tells a timeout whether it belongs to the current wait, a
// Stop cannot cancel a callback that has already started running.
handshakePending bool
handshakeGeneration uint32
handshakeTimer *time.Timer
}
func newSignallerAsyncBase(params signallerAsyncBaseParams) *signallerAsyncBase {
return &signallerAsyncBase{
params: params,
}
}
func (s *signallerAsyncBase) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
s.resSinkMu.Lock()
oldSink := s.resSink
s.resSink = sink
// a resumed connection has to open with the ReconnectResponse, the client takes it
// only as the first message it reads
opened := false
switch {
case sink == nil:
// the connection is gone, keep anything queued for the next one
s.disarmHandshakeLocked()
case reason == types.SignallingCloseReasonResume:
s.armHandshakeLocked()
default:
opened = s.disarmHandshakeLocked()
}
s.resSinkMu.Unlock()
if opened {
s.notifyHandshakeOpened()
}
if oldSink != nil {
if sink != nil {
s.params.Logger.Debugw(
"swapping signal connection",
"reason", reason,
"connID", oldSink.ConnectionID(),
"newConnID", sink.ConnectionID(),
)
} else {
s.params.Logger.Debugw(
"closing signal connection",
"reason", reason,
"connID", oldSink.ConnectionID(),
)
}
oldSink.Close()
}
}
// HandshakePending is a plain read, so a caller can decide to hold a message back
// while holding its own lock
func (s *signallerAsyncBase) HandshakePending() bool {
s.resSinkMu.Lock()
defer s.resSinkMu.Unlock()
return s.handshakePending
}
func (s *signallerAsyncBase) OpenHandshake() {
s.resSinkMu.Lock()
opened := s.disarmHandshakeLocked()
s.resSinkMu.Unlock()
if opened {
s.notifyHandshakeOpened()
}
}
func (s *signallerAsyncBase) armHandshakeLocked() {
s.stopHandshakeTimerLocked()
s.handshakePending = true
s.handshakeGeneration++
generation := s.handshakeGeneration
// the connection opens on its own if nothing writes a ReconnectResponse, a path
// that resumes without one should not hold messages back for the whole session
s.handshakeTimer = time.AfterFunc(handshakeWindow, func() { s.onHandshakeTimeout(generation) })
}
// disarmHandshakeLocked reports whether it opened a connection that was holding
// messages back
func (s *signallerAsyncBase) disarmHandshakeLocked() bool {
s.stopHandshakeTimerLocked()
wasPending := s.handshakePending
s.handshakePending = false
// a timeout of the wait that just ended is stale
s.handshakeGeneration++
return wasPending
}
func (s *signallerAsyncBase) stopHandshakeTimerLocked() {
if s.handshakeTimer != nil {
s.handshakeTimer.Stop()
s.handshakeTimer = nil
}
}
func (s *signallerAsyncBase) onHandshakeTimeout(generation uint32) {
s.resSinkMu.Lock()
if generation != s.handshakeGeneration {
// the wait this timeout was armed for has ended, a later one may be in progress
s.resSinkMu.Unlock()
return
}
opened := s.disarmHandshakeLocked()
s.resSinkMu.Unlock()
if !opened {
return
}
s.params.Logger.Warnw("resumed connection did not open with a ReconnectResponse", nil)
s.notifyHandshakeOpened()
}
// notifyHandshakeOpened runs without resSinkMu held, the callback sends messages
func (s *signallerAsyncBase) notifyHandshakeOpened() {
if s.params.OnHandshakeOpened != nil {
s.params.OnHandshakeOpened()
}
}
func (s *signallerAsyncBase) GetResponseSink() routing.MessageSink {
s.resSinkMu.Lock()
defer s.resSinkMu.Unlock()
return s.resSink
}
// closes signal connection to notify client to resume/reconnect
func (s *signallerAsyncBase) CloseSignalConnection(reason types.SignallingCloseReason) {
s.SwapResponseSink(nil, reason)
}

View File

@@ -0,0 +1,157 @@
package signalling
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/routing/routingfakes"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func newTestSignallerBase(onHandshakeOpened func()) *signallerAsyncBase {
return newSignallerAsyncBase(signallerAsyncBaseParams{
Logger: logger.GetLogger(),
OnHandshakeOpened: onHandshakeOpened,
})
}
// currentHandshakeGeneration is what a timeout armed right now would be tagged with
func currentHandshakeGeneration(s *signallerAsyncBase) uint32 {
s.resSinkMu.Lock()
defer s.resSinkMu.Unlock()
return s.handshakeGeneration
}
func TestHandshakeGate(t *testing.T) {
t.Run("a resumed connection holds messages back", func(t *testing.T) {
s := newTestSignallerBase(nil)
require.False(t, s.HandshakePending())
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
require.True(t, s.HandshakePending())
s.OpenHandshake()
require.False(t, s.HandshakePending())
})
t.Run("other sink swaps do not hold messages back", func(t *testing.T) {
s := newTestSignallerBase(nil)
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonUnknown)
require.False(t, s.HandshakePending())
})
t.Run("closing the connection clears the gate", func(t *testing.T) {
s := newTestSignallerBase(nil)
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
require.True(t, s.HandshakePending())
s.CloseSignalConnection(types.SignallingCloseReasonParticipantClose)
require.False(t, s.HandshakePending())
})
t.Run("the handshake window opens the gate", func(t *testing.T) {
s := newTestSignallerBase(nil)
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
require.True(t, s.HandshakePending())
s.onHandshakeTimeout(currentHandshakeGeneration(s))
require.False(t, s.HandshakePending())
})
t.Run("a swap to a fresh connection opens the gate", func(t *testing.T) {
s := newTestSignallerBase(nil)
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
require.True(t, s.HandshakePending())
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonUnknown)
require.False(t, s.HandshakePending())
})
t.Run("a timeout of a wait that has ended does nothing", func(t *testing.T) {
var opened atomic.Int32
s := newTestSignallerBase(func() { opened.Inc() })
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
stale := currentHandshakeGeneration(s)
// the client resumes again while the first timeout is running, Stop cannot
// cancel a callback that has already started
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
s.onHandshakeTimeout(stale)
require.True(t, s.HandshakePending())
require.Zero(t, opened.Load())
// and the wait in progress still opens on its own timeout
s.onHandshakeTimeout(currentHandshakeGeneration(s))
require.False(t, s.HandshakePending())
require.EqualValues(t, 1, opened.Load())
})
}
func TestHandshakeGateNotifiesOnOpen(t *testing.T) {
t.Run("on an explicit open", func(t *testing.T) {
var opened atomic.Int32
s := newTestSignallerBase(func() { opened.Inc() })
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
require.Zero(t, opened.Load())
s.OpenHandshake()
require.EqualValues(t, 1, opened.Load())
// only the transition notifies
s.OpenHandshake()
require.EqualValues(t, 1, opened.Load())
})
t.Run("on the handshake window expiring", func(t *testing.T) {
var opened atomic.Int32
s := newTestSignallerBase(func() { opened.Inc() })
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
s.onHandshakeTimeout(currentHandshakeGeneration(s))
require.False(t, s.HandshakePending())
require.EqualValues(t, 1, opened.Load())
})
t.Run("the window fires without anything else touching the gate", func(t *testing.T) {
var opened atomic.Int32
s := newTestSignallerBase(func() { opened.Inc() })
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
require.Eventually(t, func() bool {
return !s.HandshakePending() && opened.Load() == 1
}, 2*handshakeWindow, 100*time.Millisecond)
})
t.Run("not on a connection close", func(t *testing.T) {
var opened atomic.Int32
s := newTestSignallerBase(func() { opened.Inc() })
s.SwapResponseSink(&routingfakes.FakeMessageSink{}, types.SignallingCloseReasonResume)
s.CloseSignalConnection(types.SignallingCloseReasonParticipantClose)
require.False(t, s.HandshakePending())
require.Zero(t, opened.Load())
})
t.Run("not when the gate was never armed", func(t *testing.T) {
var opened atomic.Int32
s := newTestSignallerBase(func() { opened.Inc() })
s.OpenHandshake()
require.Zero(t, opened.Load())
})
}

View File

@@ -0,0 +1,45 @@
// 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 signalling
import (
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignaller = (*signallerUnimplemented)(nil)
type signallerUnimplemented struct{}
func (u *signallerUnimplemented) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
}
func (u *signallerUnimplemented) GetResponseSink() routing.MessageSink {
return nil
}
func (u *signallerUnimplemented) CloseSignalConnection(reason types.SignallingCloseReason) {}
func (u *signallerUnimplemented) HandshakePending() bool {
return false
}
func (u *signallerUnimplemented) OpenHandshake() {}
func (u *signallerUnimplemented) WriteMessage(msg proto.Message) error {
return nil
}

View File

@@ -0,0 +1,276 @@
// 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 signalling
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignalling = (*signalling)(nil)
type SignallingParams struct {
Logger logger.Logger
}
type signalling struct {
params SignallingParams
}
func NewSignalling(params SignallingParams) ParticipantSignalling {
return &signalling{
params: params,
}
}
func (s *signalling) SignalJoinResponse(join *livekit.JoinResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Join{
Join: join,
},
}
}
func (s *signalling) SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message {
if len(participants) == 0 {
return nil
}
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: participants,
},
},
}
}
func (s *signalling) SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message {
if len(speakers) == 0 {
return nil
}
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SpeakersChanged{
SpeakersChanged: &livekit.SpeakersChanged{
Speakers: speakers,
},
},
}
}
func (s *signalling) SignalRoomUpdate(room *livekit.Room) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomUpdate{
RoomUpdate: &livekit.RoomUpdate{
Room: room,
},
},
}
}
func (s *signalling) SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_ConnectionQuality{
ConnectionQuality: connectionQuality,
},
}
}
func (s *signalling) SignalRefreshToken(token string) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RefreshToken{
RefreshToken: token,
},
}
}
func (s *signalling) SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RequestResponse{
RequestResponse: requestResponse,
},
}
}
func (s *signalling) SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomMoved{
RoomMoved: roomMoved,
},
}
}
func (s *signalling) SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Reconnect{
Reconnect: reconnect,
},
}
}
func (s *signalling) SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Trickle{
Trickle: trickle,
},
}
}
func (s *signalling) SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Mute{
Mute: mute,
},
}
}
func (s *signalling) SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackPublished{
TrackPublished: trackPublished,
},
}
}
func (s *signalling) SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackUnpublished{
TrackUnpublished: trackUnpublished,
},
}
}
func (s *signalling) SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackSubscribed{
TrackSubscribed: trackSubscribed,
},
}
}
func (s *signalling) SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
}
}
func (s *signalling) SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Answer{
Answer: answer,
},
}
}
func (s *signalling) SignalSdpOffer(offer *livekit.SessionDescription) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_Offer{
Offer: offer,
},
}
}
func (s *signalling) SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_StreamStateUpdate{
StreamStateUpdate: streamStateUpdate,
},
}
}
func (s *signalling) SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscribedQualityUpdate{
SubscribedQualityUpdate: subscribedQualityUpdate,
},
}
}
func (s *signalling) SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscriptionResponse{
SubscriptionResponse: subscriptionResponse,
},
}
}
func (s *signalling) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscriptionPermissionUpdate{
SubscriptionPermissionUpdate: subscriptionPermissionUpdate,
},
}
}
func (s *signalling) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_MediaSectionsRequirement{
MediaSectionsRequirement: mediaSectionsRequirement,
},
}
}
func (s *signalling) SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_SubscribedAudioCodecUpdate{
SubscribedAudioCodecUpdate: subscribedAudioCodecUpdate,
},
}
}
func (s *signalling) SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_PublishDataTrackResponse{
PublishDataTrackResponse: publishDataTrackResponse,
},
}
}
func (s *signalling) SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_UnpublishDataTrackResponse{
UnpublishDataTrackResponse: unpublishDataTrackResponse,
},
}
}
func (s *signalling) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_DataTrackSubscriberHandles{
DataTrackSubscriberHandles: dataTrackSubscriberHandles,
},
}
}
func (s *signalling) SignalStoreDataBlobResponse(storeDataBlobResponse *livekit.StoreDataBlobResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_StoreDataBlobResponse{
StoreDataBlobResponse: storeDataBlobResponse,
},
}
}
func (s *signalling) SignalGetDataBlobResponse(getDataBlobResponse *livekit.GetDataBlobResponse) proto.Message {
return &livekit.SignalResponse{
Message: &livekit.SignalResponse_GetDataBlobResponse{
GetDataBlobResponse: getDataBlobResponse,
},
}
}

View File

@@ -0,0 +1,137 @@
// 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 signalling
import (
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
)
var _ ParticipantSignalling = (*signallingUnimplemented)(nil)
type signallingUnimplemented struct{}
func (u *signallingUnimplemented) SignalJoinResponse(join *livekit.JoinResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalParticipantUpdate(participants []*livekit.ParticipantInfo) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSpeakerUpdate(speakers []*livekit.SpeakerInfo) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRoomUpdate(room *livekit.Room) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalConnectionQualityUpdate(connectionQuality *livekit.ConnectionQualityUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRefreshToken(token string) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRequestResponse(requestResponse *livekit.RequestResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalRoomMovedResponse(roomMoved *livekit.RoomMovedResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalReconnectResponse(reconnect *livekit.ReconnectResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalICECandidate(trickle *livekit.TrickleRequest) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackMuted(mute *livekit.MuteTrackRequest) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackPublished(trackPublished *livekit.TrackPublishedResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackUnpublished(trackUnpublished *livekit.TrackUnpublishedResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalTrackSubscribed(trackSubscribed *livekit.TrackSubscribed) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalLeaveRequest(leave *livekit.LeaveRequest) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSdpAnswer(answer *livekit.SessionDescription) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSdpOffer(offer *livekit.SessionDescription) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalStreamStateUpdate(streamStateUpdate *livekit.StreamStateUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscribedQualityUpdate(subscribedQualityUpdate *livekit.SubscribedQualityUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscriptionResponse(subscriptionResponse *livekit.SubscriptionResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscriptionPermissionUpdate(subscriptionPermissionUpdate *livekit.SubscriptionPermissionUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalMediaSectionsRequirement(mediaSectionsRequirement *livekit.MediaSectionsRequirement) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalSubscribedAudioCodecUpdate(subscribedAudioCodecUpdate *livekit.SubscribedAudioCodecUpdate) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalPublishDataTrackResponse(publishDataTrackResponse *livekit.PublishDataTrackResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalUnpublishDataTrackResponse(unpublishDataTrackResponse *livekit.UnpublishDataTrackResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalDataTrackSubscriberHandles(dataTrackSubscriberHandles *livekit.DataTrackSubscriberHandles) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalStoreDataBlobResponse(storeDataBlobResponse *livekit.StoreDataBlobResponse) proto.Message {
return nil
}
func (u *signallingUnimplemented) SignalGetDataBlobResponse(getDataBlobResponse *livekit.GetDataBlobResponse) proto.Message {
return nil
}

505
pkg/rtc/subscribedtrack.go Normal file
View File

@@ -0,0 +1,505 @@
// 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 rtc
import (
"sync"
"time"
"github.com/bep/debounce"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/telemetry"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/utils"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
)
const (
subscriptionDebounceInterval = 100 * time.Millisecond
)
var _ types.SubscribedTrack = (*SubscribedTrack)(nil)
type SubscribedTrackParams struct {
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Subscriber types.LocalParticipant
MediaTrack types.MediaTrack
AdaptiveStream bool
EnableStartAtDesiredQuality bool
TelemetryListener types.ParticipantTelemetryListener
WrappedReceiver *WrappedReceiver
IsRelayed bool
OnDownTrackCreated func(downTrack *sfu.DownTrack)
OnDownTrackClosed func(subscriberID livekit.ParticipantID)
OnSubscriberMaxQualityChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, layer int32)
OnSubscriberAudioCodecChange func(subscriberID livekit.ParticipantID, mime mime.MimeType, enabled bool)
}
type SubscribedTrack struct {
params SubscribedTrackParams
logger logger.Logger
downTrack *sfu.DownTrack
sender atomic.Pointer[webrtc.RTPSender]
needsNegotiation atomic.Bool
versionGenerator utils.TimedVersionGenerator
settingsLock sync.Mutex
settings *livekit.UpdateTrackSettings
settingsVersion utils.TimedVersion
bindLock sync.Mutex
bound bool
onBindCallbacks []func(error)
onSubscribeStreamStarted atomic.Value //func(time.Duration)
onClose atomic.Value // func(bool)
debouncer func(func())
statsKey telemetry.StatsKey
reporter roomobs.TrackReporter
}
func NewSubscribedTrack(params SubscribedTrackParams) (*SubscribedTrack, error) {
s := &SubscribedTrack{
params: params,
logger: params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub).WithValues(
"trackID", params.MediaTrack.ID(),
"publisherID", params.MediaTrack.PublisherID(),
"publisher", params.MediaTrack.PublisherIdentity(),
),
versionGenerator: utils.NewDefaultTimedVersionGenerator(),
debouncer: debounce.New(subscriptionDebounceInterval),
statsKey: telemetry.StatsKeyForTrack(
params.Subscriber.GetCountry(),
livekit.StreamType_DOWNSTREAM,
params.Subscriber.ID(),
params.MediaTrack.ID(),
params.MediaTrack.Source(),
params.MediaTrack.Kind(),
),
reporter: params.Subscriber.GetReporter().WithTrack(params.MediaTrack.ID().String()),
}
var rtcpFeedback []webrtc.RTCPFeedback
var maxTrack int
switch params.MediaTrack.Kind() {
case livekit.TrackType_AUDIO:
rtcpFeedback = params.SubscriberConfig.RTCPFeedback.Audio
maxTrack = params.ReceiverConfig.PacketBufferSizeAudio
case livekit.TrackType_VIDEO:
rtcpFeedback = params.SubscriberConfig.RTCPFeedback.Video
maxTrack = params.ReceiverConfig.PacketBufferSizeVideo
default:
s.logger.Warnw("unexpected track type", nil, "kind", params.MediaTrack.Kind())
}
codecs := params.WrappedReceiver.Codecs()
for _, c := range codecs {
c.RTCPFeedback = rtcpFeedback
}
streamID := params.WrappedReceiver.StreamID()
if params.Subscriber.SupportsSyncStreamID() && params.MediaTrack.Stream() != "" {
streamID = PackSyncStreamID(params.MediaTrack.PublisherID(), params.MediaTrack.Stream())
}
isEncrypted := params.MediaTrack.IsEncrypted()
var trailer []byte
if isEncrypted {
trailer = params.Subscriber.GetTrailer()
}
subClientInfo := ClientInfo{ClientInfo: params.Subscriber.GetClientInfo()}
subSupportsPacketTrailer := subClientInfo.SupportsPacketTrailer()
// Strip packet trailer if track has packet trailer but subscriber does not have cap
stripPacketTrailer := params.MediaTrack.HasPacketTrailer() && !subSupportsPacketTrailer
downTrack, err := sfu.NewDownTrack(sfu.DownTrackParams{
Codecs: codecs,
IsEncrypted: isEncrypted,
Source: params.MediaTrack.Source(),
Receiver: params.WrappedReceiver,
BufferFactory: params.Subscriber.GetBufferFactory(),
SubID: params.Subscriber.ID(),
StreamID: streamID,
MaxTrack: maxTrack,
PlayoutDelayLimit: params.Subscriber.GetPlayoutDelayConfig(),
Pacer: params.Subscriber.GetPacer(),
Trailer: trailer,
StripPacketTrailer: stripPacketTrailer,
Logger: LoggerWithTrack(
params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub),
params.MediaTrack.ID(),
params.IsRelayed,
),
RTCPWriter: params.Subscriber.WriteSubscriberRTCP,
DisableSenderReportPassThrough: params.Subscriber.GetDisableSenderReportPassThrough(),
SupportsCodecChange: params.Subscriber.SupportsCodecChange(),
EnableStartAtDesiredQuality: params.EnableStartAtDesiredQuality,
Listener: s,
})
if err != nil {
return nil, err
}
if params.OnDownTrackCreated != nil {
params.OnDownTrackCreated(downTrack)
}
downTrack.AddReceiverReportListener(params.Subscriber.HandleReceiverReport)
s.downTrack = downTrack
return s, nil
}
func (t *SubscribedTrack) AddOnBind(f func(error)) {
t.bindLock.Lock()
bound := t.bound
if !bound {
t.onBindCallbacks = append(t.onBindCallbacks, f)
}
t.bindLock.Unlock()
if bound {
// fire immediately, do not need to persist since bind is a one time event
go f(nil)
}
}
// for DownTrack callback to notify us that it's bound
func (t *SubscribedTrack) Bound(err error) {
t.bindLock.Lock()
if err == nil {
t.bound = true
}
callbacks := t.onBindCallbacks
t.onBindCallbacks = nil
t.bindLock.Unlock()
if err == nil && t.MediaTrack().Kind() == livekit.TrackType_VIDEO {
// When AdaptiveStream is enabled, default the subscriber to LOW quality stream
// we would want LOW instead of OFF for a couple of reasons
// 1. when a subscriber unsubscribes from a track, we would forget their previously defined settings
// depending on client implementation, subscription on/off is kept separately from adaptive stream
// So when there are no changes to desired resolution, but the user re-subscribes, we may leave stream at OFF
// 2. when interacting with dynacast *and* adaptive stream. If the publisher was not publishing at the
// time of subscription, we might not be able to trigger adaptive stream updates on the client side
// (since there isn't any video frames coming through). this will leave the stream "stuck" on off, without
// a trigger to re-enable it
t.settingsLock.Lock()
if t.settings != nil {
if t.params.AdaptiveStream {
// remove `disabled` flag to force a visibility update
t.settings.Disabled = false
t.logger.Debugw("enabling subscriber track settings on bind", "settings", logger.Proto(t.settings))
}
} else {
if t.params.EnableStartAtDesiredQuality {
// default to HIGH quality so the subscriber acquires the top layer directly instead of
// ramping up from a lower layer. adaptive stream clients can still scale down afterwards
// based on viewport.
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_HIGH}
} else if t.params.AdaptiveStream {
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_LOW}
} else {
t.settings = &livekit.UpdateTrackSettings{Quality: livekit.VideoQuality_HIGH}
}
t.logger.Debugw("initializing subscriber track settings on bind", "settings", logger.Proto(t.settings))
}
t.settingsLock.Unlock()
t.applySettings()
}
for _, cb := range callbacks {
go cb(err)
}
}
// for DownTrack callback to notify us that it's closed
func (t *SubscribedTrack) Close(isExpectedToResume bool) {
if onClose := t.onClose.Load(); onClose != nil {
onClose.(func(bool))(isExpectedToResume)
}
}
func (t *SubscribedTrack) OnSubscribeStreamStarted(f func(time.Duration)) {
t.onSubscribeStreamStarted.Store(f)
}
func (t *SubscribedTrack) OnClose(f func(bool)) {
t.onClose.Store(f)
}
func (t *SubscribedTrack) IsBound() bool {
t.bindLock.Lock()
defer t.bindLock.Unlock()
return t.bound
}
func (t *SubscribedTrack) ID() livekit.TrackID {
return livekit.TrackID(t.downTrack.ID())
}
func (t *SubscribedTrack) PublisherID() livekit.ParticipantID {
return t.params.MediaTrack.PublisherID()
}
func (t *SubscribedTrack) PublisherIdentity() livekit.ParticipantIdentity {
return t.params.MediaTrack.PublisherIdentity()
}
func (t *SubscribedTrack) PublisherVersion() uint32 {
return t.params.MediaTrack.PublisherVersion()
}
func (t *SubscribedTrack) SubscriberID() livekit.ParticipantID {
return t.params.Subscriber.ID()
}
func (t *SubscribedTrack) SubscriberIdentity() livekit.ParticipantIdentity {
return t.params.Subscriber.Identity()
}
func (t *SubscribedTrack) Subscriber() types.LocalParticipant {
return t.params.Subscriber
}
func (t *SubscribedTrack) DownTrack() *sfu.DownTrack {
return t.downTrack
}
func (t *SubscribedTrack) MediaTrack() types.MediaTrack {
return t.params.MediaTrack
}
// has subscriber indicated it wants to mute this track
func (t *SubscribedTrack) IsMuted() bool {
t.settingsLock.Lock()
defer t.settingsLock.Unlock()
return t.isMutedLocked()
}
func (t *SubscribedTrack) isMutedLocked() bool {
if t.settings == nil {
return false
}
return t.settings.Disabled
}
func (t *SubscribedTrack) SetPublisherMuted(muted bool) {
t.downTrack.PubMute(muted)
}
func (t *SubscribedTrack) UpdateSubscriberSettings(settings *livekit.UpdateTrackSettings, isImmediate bool) {
t.settingsLock.Lock()
if proto.Equal(t.settings, settings) {
t.logger.Debugw("skipping subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsLock.Unlock()
return
}
isImmediate = isImmediate || (!settings.Disabled && settings.Disabled != t.isMutedLocked())
t.settings = utils.CloneProto(settings)
t.logger.Debugw("saving subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsLock.Unlock()
if isImmediate {
t.applySettings()
} else {
// avoid frequent changes to mute & video layers, unless it became visible
t.debouncer(t.applySettings)
}
}
func (t *SubscribedTrack) UpdateVideoLayer() {
t.applySettings()
}
func (t *SubscribedTrack) applySettings() {
t.settingsLock.Lock()
if t.settings == nil {
t.settingsLock.Unlock()
return
}
t.settingsVersion = t.versionGenerator.Next()
settingsVersion := t.settingsVersion
t.settingsLock.Unlock()
dt := t.DownTrack()
spatial := buffer.InvalidLayerSpatial
temporal := buffer.InvalidLayerTemporal
if dt.Kind() == webrtc.RTPCodecTypeVideo {
mt := t.MediaTrack()
quality := t.settings.Quality
mimeType := dt.Mime()
if t.settings.Width > 0 {
quality = mt.GetQualityForDimension(mimeType, t.settings.Width, t.settings.Height)
}
spatial = buffer.GetSpatialLayerForVideoQuality(mimeType, quality, mt.ToProto())
if t.settings.Fps > 0 {
temporal = mt.GetTemporalLayerForSpatialFps(mimeType, spatial, t.settings.Fps)
}
}
t.settingsLock.Lock()
if settingsVersion != t.settingsVersion {
// a newer settings has superseded this one
t.settingsLock.Unlock()
return
}
t.logger.Debugw("applying subscriber track settings", "settings", logger.Proto(t.settings))
if t.settings.Disabled {
dt.Mute(true)
t.settingsLock.Unlock()
return
} else {
dt.Mute(false)
}
if dt.Kind() == webrtc.RTPCodecTypeVideo {
dt.SetMaxSpatialLayer(spatial)
if temporal != buffer.InvalidLayerTemporal {
dt.SetMaxTemporalLayer(temporal)
}
}
t.settingsLock.Unlock()
}
func (t *SubscribedTrack) NeedsNegotiation() bool {
return t.needsNegotiation.Load()
}
func (t *SubscribedTrack) SetNeedsNegotiation(needs bool) {
t.needsNegotiation.Store(needs)
}
func (t *SubscribedTrack) RTPSender() *webrtc.RTPSender {
return t.sender.Load()
}
func (t *SubscribedTrack) SetRTPSender(sender *webrtc.RTPSender) {
t.sender.Store(sender)
}
// DownTrackListener implementation
var _ sfu.DownTrackListener = (*SubscribedTrack)(nil)
func (t *SubscribedTrack) OnBindAndConnected() {
if t.params.Subscriber.Hidden() {
return
}
t.params.MediaTrack.OnTrackSubscribed()
}
func (t *SubscribedTrack) OnStatsUpdate(stat *livekit.AnalyticsStat) {
t.params.TelemetryListener.OnTrackStats(t.statsKey, stat)
if cs, ok := telemetry.CondenseStat(stat); ok {
ti := t.params.WrappedReceiver.TrackInfo()
t.reporter.Tx(func(tx roomobs.TrackTx) {
tx.ParticipantSession().ReportKindCode(roomobs.ParticipantKindCode(t.params.Subscriber.Kind()))
tx.ParticipantSession().ReportKindDetailsCodes(roomobs.ParticipantKindDetailsCodes(t.params.Subscriber.KindDetails()))
tx.ReportName(ti.Name)
tx.ReportKind(roomobs.TrackKindSub)
tx.ReportType(roomobs.TrackTypeFromProto(ti.Type))
tx.ReportSource(roomobs.TrackSourceFromProto(ti.Source))
tx.ReportMime(mime.NormalizeMimeType(ti.MimeType).ReporterType())
tx.ReportLayer(roomobs.PackTrackLayer(ti.Height, ti.Width))
tx.ReportDuration(uint16(cs.EndTime.Sub(cs.StartTime).Milliseconds()))
tx.ReportFrames(uint16(cs.Frames))
tx.ReportSendBytes(uint32(cs.Bytes))
tx.ReportSendPackets(cs.Packets)
tx.ReportPacketsLost(cs.PacketsLost)
tx.ReportScore(stat.Score)
})
}
}
func (t *SubscribedTrack) OnMaxSubscribedLayerChanged(layer int32) {
if t.params.OnSubscriberMaxQualityChange != nil {
t.params.OnSubscriberMaxQualityChange(t.downTrack.SubscriberID(), t.downTrack.Mime(), layer)
}
}
func (t *SubscribedTrack) OnRttUpdate(rtt uint32) {
go t.params.Subscriber.UpdateMediaRTT(rtt)
}
func (t *SubscribedTrack) OnCodecNegotiated(codec webrtc.RTPCodecCapability) {
if isAvailable, needsPublish := t.params.WrappedReceiver.DetermineReceiver(codec); !isAvailable || !needsPublish {
return
}
if t.params.OnSubscriberMaxQualityChange != nil || t.params.OnSubscriberAudioCodecChange != nil {
go func() {
mimeType := mime.NormalizeMimeType(codec.MimeType)
switch t.params.MediaTrack.Kind() {
case livekit.TrackType_VIDEO:
spatial := buffer.GetSpatialLayerForVideoQuality(
mimeType,
livekit.VideoQuality_HIGH,
t.params.MediaTrack.ToProto(),
)
if t.params.OnSubscriberMaxQualityChange != nil {
t.params.OnSubscriberMaxQualityChange(t.downTrack.SubscriberID(), mimeType, spatial)
}
case livekit.TrackType_AUDIO:
if t.params.OnSubscriberAudioCodecChange != nil {
t.params.OnSubscriberAudioCodecChange(t.downTrack.SubscriberID(), mimeType, true)
}
}
}()
}
}
func (t *SubscribedTrack) OnDownTrackClose(isExpectedToResume bool) {
// 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 isExpectedToResume {
if tr := t.downTrack.GetTransceiver(); tr != nil {
t.params.Subscriber.CacheDownTrack(t.ID(), tr, t.downTrack.GetState())
}
}
if t.params.OnDownTrackClosed != nil {
t.params.OnDownTrackClosed(t.params.Subscriber.ID())
}
t.Close(isExpectedToResume)
}
func (t *SubscribedTrack) OnStreamStarted(elapsed time.Duration) {
t.params.TelemetryListener.OnTrackSubscribeStreamStarted(t.params.Subscriber.ID(), t.params.MediaTrack.ToProto(), elapsed)
if onSubscribeStreamStarted := t.onSubscribeStreamStarted.Load(); onSubscribeStreamStarted != nil {
onSubscribeStreamStarted.(func(time.Duration))(elapsed)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,714 @@
/*
* 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 rtc
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func init() {
reconcileInterval = 50 * time.Millisecond
notFoundTimeout = 200 * time.Millisecond
subscriptionTimeout = 200 * time.Millisecond
}
const (
subSettleTimeout = 600 * time.Millisecond
subCheckInterval = 10 * time.Millisecond
)
func TestSubscribe(t *testing.T) {
t.Run("happy path subscribe", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
subCount := atomic.Int32{}
failed := atomic.Bool{}
sm.params.OnTrackSubscribed = func(subTrack types.SubscribedTrack) {
subCount.Add(1)
}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
numParticipantSubscribed := atomic.Int32{}
numParticipantUnsubscribed := atomic.Int32{}
sm.OnSubscribeStatusChanged(func(pubID livekit.ParticipantID, subscribed bool) {
if subscribed {
numParticipantSubscribed.Add(1)
} else {
numParticipantUnsubscribed.Add(1)
}
})
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
return subCount.Load() == 1
}, subSettleTimeout, subCheckInterval, "track was not subscribed")
require.NotNil(t, s.getSubscribedTrack())
require.Len(t, sm.GetSubscribedTracks(), 1)
require.Eventually(t, func() bool {
return len(sm.GetSubscribedParticipants()) == 1
}, subSettleTimeout, subCheckInterval, "GetSubscribedParticipants should have returned one item")
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
// ensure telemetry events are sent
tl := sm.params.Participant.GetTelemetryListener().(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
// ensure bound
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
require.Eventually(t, func() bool {
return !s.needsBind()
}, subSettleTimeout, subCheckInterval, "track was not bound")
// telemetry event should have been sent
require.Equal(t, 1, tl.OnTrackSubscribedCallCount())
time.Sleep(notFoundTimeout)
require.False(t, failed.Load())
resolver.SetPause(true)
// ensure its resilience after being closed
setTestSubscribedTrackClosed(t, s.getSubscribedTrack(), false)
require.Eventually(t, func() bool {
return s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "needs subscribe did not persist across track close")
resolver.SetPause(false)
require.Eventually(t, func() bool {
return s.isDesired() && !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "track was not resubscribed")
// was subscribed twice, unsubscribed once (due to close)
require.Eventually(t, func() bool {
return numParticipantSubscribed.Load() == 2
}, subSettleTimeout, subCheckInterval, "participant subscribe status was not updated twice")
require.Eventually(t, func() bool {
return numParticipantUnsubscribed.Load() == 1
}, subSettleTimeout, subCheckInterval, "participant unsubscribe status was not updated")
})
t.Run("no track permission", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(false, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
failed := atomic.Bool{}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.getHasPermission()
}, subSettleTimeout, subCheckInterval, "should not have permission to subscribe")
time.Sleep(subscriptionTimeout)
// should not have called failed callbacks, isDesired remains unchanged
require.True(t, s.isDesired())
require.False(t, failed.Load())
require.True(t, s.needsSubscribe())
require.Len(t, sm.GetSubscribedTracks(), 0)
// trackSubscribed telemetry not sent
tl := sm.params.Participant.GetTelemetryListener().(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
require.Equal(t, 0, tl.OnTrackSubscribedCallCount())
// give permissions now
resolver.lock.Lock()
resolver.hasPermission = true
resolver.lock.Unlock()
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "should be subscribed")
require.Len(t, sm.GetSubscribedTracks(), 1)
})
t.Run("publisher left", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
failed := atomic.Bool{}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "should be subscribed")
resolver.lock.Lock()
resolver.hasTrack = false
resolver.lock.Unlock()
// publisher triggers close
setTestSubscribedTrackClosed(t, s.getSubscribedTrack(), false)
require.Eventually(t, func() bool {
return !s.isDesired()
}, subSettleTimeout, subCheckInterval, "isDesired not set to false")
})
}
func TestUnsubscribe(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
unsubCount := atomic.Int32{}
sm.params.OnTrackUnsubscribed = func(subTrack types.SubscribedTrack) {
unsubCount.Add(1)
}
resolver := newTestResolver(true, true, "pub", "pubID")
s := &mediaTrackSubscription{
trackSubscription: trackSubscription{
trackID: "track",
desired: true,
subscriberID: sm.params.Participant.ID(),
publisherID: "pubID",
publisherIdentity: "pub",
logger: logger.GetLogger(),
hasPermission: true,
},
bound: true,
}
// a bunch of unfortunate manual wiring
res := resolver.Resolve(nil, s.trackID)
res.TrackChangedNotifier.AddObserver(string(sm.params.Participant.ID()), func() {})
s.changedNotifier = res.TrackChangedNotifier
st, err := res.Track.AddSubscriber(sm.params.Participant)
require.NoError(t, err)
s.subscribedTrack = st
st.OnClose(func(isExpectedToResume bool) {
sm.handleSubscribedTrackClose(s, isExpectedToResume)
})
res.Track.(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) {
setTestSubscribedTrackClosed(t, st, isExpectedToResume)
})
sm.lock.Lock()
sm.subscriptions["track"] = s
sm.lock.Unlock()
require.False(t, s.needsSubscribe())
require.False(t, s.needsUnsubscribe())
// unsubscribe
sm.UnsubscribeFromTrack("track")
require.False(t, s.isDesired())
require.Eventually(t, func() bool {
if s.needsUnsubscribe() {
return false
}
if sm.pendingUnsubscribes.Load() != 0 {
return false
}
sm.lock.RLock()
subLen := len(sm.subscriptions)
sm.lock.RUnlock()
return subLen == 0
}, subSettleTimeout, subCheckInterval, "Track was not unsubscribed")
// no traces should be left
require.Len(t, sm.GetSubscribedTracks(), 0)
// the observer is dropped on a goroutine of its own
require.Eventually(t, func() bool {
return !res.TrackChangedNotifier.HasObservers()
}, subSettleTimeout, subCheckInterval, "observer was not removed")
tl := sm.params.Participant.GetTelemetryListener().(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackUnsubscribedCallCount())
}
func TestSubscribeStatusChanged(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
numParticipantSubscribed := atomic.Int32{}
numParticipantUnsubscribed := atomic.Int32{}
sm.OnSubscribeStatusChanged(func(pubID livekit.ParticipantID, subscribed bool) {
if subscribed {
numParticipantSubscribed.Add(1)
} else {
numParticipantUnsubscribed.Add(1)
}
})
sm.SubscribeToTrack("track1", false)
sm.SubscribeToTrack("track2", false)
s1 := sm.subscriptions["track1"]
s2 := sm.subscriptions["track2"]
require.Eventually(t, func() bool {
return !s1.needsSubscribe() && !s2.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "track1 and track2 should be subscribed")
st1 := s1.getSubscribedTrack()
st1.OnClose(func(isExpectedToResume bool) {
sm.handleSubscribedTrackClose(s1, isExpectedToResume)
})
st2 := s2.getSubscribedTrack()
st2.OnClose(func(isExpectedToResume bool) {
sm.handleSubscribedTrackClose(s2, isExpectedToResume)
})
st1.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) {
setTestSubscribedTrackClosed(t, st1, isExpectedToResume)
})
st2.MediaTrack().(*typesfakes.FakeMediaTrack).RemoveSubscriberCalls(func(pID livekit.ParticipantID, isExpectedToResume bool) {
setTestSubscribedTrackClosed(t, st2, isExpectedToResume)
})
require.Eventually(t, func() bool {
return numParticipantSubscribed.Load() == 1
}, subSettleTimeout, subCheckInterval, "should be subscribed to publisher")
require.Equal(t, int32(0), numParticipantUnsubscribed.Load())
require.True(t, sm.IsSubscribedTo("pubID"))
// now unsubscribe track2, no event should be fired
sm.UnsubscribeFromTrack("track2")
require.Eventually(t, func() bool {
return !s2.needsUnsubscribe()
}, subSettleTimeout, subCheckInterval, "track2 should be unsubscribed")
require.Equal(t, int32(0), numParticipantUnsubscribed.Load())
// unsubscribe track1, expect event
sm.UnsubscribeFromTrack("track1")
require.Eventually(t, func() bool {
return !s1.needsUnsubscribe()
}, subSettleTimeout, subCheckInterval, "track1 should be unsubscribed")
require.Eventually(t, func() bool {
return numParticipantUnsubscribed.Load() == 1
}, subSettleTimeout, subCheckInterval, "should be subscribed to publisher")
require.False(t, sm.IsSubscribedTo("pubID"))
}
// clients may send update subscribed settings prior to subscription events coming through
// settings should be persisted and used when the subscription does take place.
func TestUpdateSettingsBeforeSubscription(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
settings := &livekit.UpdateTrackSettings{
Disabled: true,
Width: 100,
Height: 100,
}
sm.UpdateSubscribedTrackSettings("track", settings)
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "Track should be subscribed")
st := s.getSubscribedTrack().(*typesfakes.FakeSubscribedTrack)
require.Eventually(t, func() bool {
return st.UpdateSubscriberSettingsCallCount() == 1
}, subSettleTimeout, subCheckInterval, "UpdateSubscriberSettings should be called once")
applied, _ := st.UpdateSubscriberSettingsArgsForCall(0)
require.Equal(t, settings.Disabled, applied.Disabled)
require.Equal(t, settings.Width, applied.Width)
require.Equal(t, settings.Height, applied.Height)
}
func TestSubscriptionLimits(t *testing.T) {
sm := newTestSubscriptionManagerWithParams(testSubscriptionParams{
SubscriptionLimitAudio: 1,
SubscriptionLimitVideo: 1,
})
defer sm.Close(false)
resolver := newTestResolver(true, true, "pub", "pubID")
sm.params.TrackResolver = resolver.Resolve
subCount := atomic.Int32{}
failed := atomic.Bool{}
sm.params.OnTrackSubscribed = func(subTrack types.SubscribedTrack) {
subCount.Add(1)
}
sm.params.OnSubscriptionError = func(trackID livekit.TrackID, fatal bool, err error) {
failed.Store(true)
}
numParticipantSubscribed := atomic.Int32{}
numParticipantUnsubscribed := atomic.Int32{}
sm.OnSubscribeStatusChanged(func(pubID livekit.ParticipantID, subscribed bool) {
if subscribed {
numParticipantSubscribed.Add(1)
} else {
numParticipantUnsubscribed.Add(1)
}
})
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
return subCount.Load() == 1
}, subSettleTimeout, subCheckInterval, "track was not subscribed")
require.NotNil(t, s.getSubscribedTrack())
require.Len(t, sm.GetSubscribedTracks(), 1)
require.Eventually(t, func() bool {
return len(sm.GetSubscribedParticipants()) == 1
}, subSettleTimeout, subCheckInterval, "GetSubscribedParticipants should have returned one item")
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
// ensure telemetry events are sent
tl := sm.params.Participant.GetTelemetryListener().(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
// ensure bound
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
require.Eventually(t, func() bool {
return !s.needsBind()
}, subSettleTimeout, subCheckInterval, "track was not bound")
// telemetry event should have been sent
require.Equal(t, 1, tl.OnTrackSubscribedCallCount())
// reach subscription limit, subscribe pending
sm.SubscribeToTrack("track2", false)
s2 := sm.subscriptions["track2"]
time.Sleep(subscriptionTimeout * 2)
require.True(t, s2.needsSubscribe())
require.Equal(t, 2, tl.OnTrackSubscribeRequestedCallCount())
require.Equal(t, 1, tl.OnTrackSubscribeFailedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
// unsubscribe track1, then track2 should be subscribed
sm.UnsubscribeFromTrack("track")
require.False(t, s.isDesired())
require.True(t, s.needsUnsubscribe())
// wait for unsubscribe to take effect
time.Sleep(reconcileInterval)
setTestSubscribedTrackClosed(t, s.getSubscribedTrack(), false)
require.Nil(t, s.getSubscribedTrack())
time.Sleep(reconcileInterval)
require.True(t, s2.isDesired())
require.False(t, s2.needsSubscribe())
require.EqualValues(t, 2, subCount.Load())
require.NotNil(t, s2.getSubscribedTrack())
require.Equal(t, 2, tl.OnTrackSubscribeRequestedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
// ensure bound
setTestSubscribedTrackBound(t, s2.getSubscribedTrack())
require.Eventually(t, func() bool {
return !s2.needsBind()
}, subSettleTimeout, subCheckInterval, "track was not bound")
// subscribe to track1 again, which should pending
sm.SubscribeToTrack("track", false)
s = sm.subscriptions["track"]
require.True(t, s.isDesired())
time.Sleep(subscriptionTimeout * 2)
require.True(t, s.needsSubscribe())
require.Equal(t, 3, tl.OnTrackSubscribeRequestedCallCount())
require.Equal(t, 2, tl.OnTrackSubscribeFailedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
}
func TestSubscribeDataTrack(t *testing.T) {
t.Run("no track permission", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestDataTrackResolver(false, true, "pub", "pubID")
sm.params.DataTrackResolver = resolver.Resolve
sm.SubscribeToDataTrack("track")
sm.lock.RLock()
s := sm.dataTrackSubscriptions["track"]
sm.lock.RUnlock()
require.Eventually(t, func() bool {
return !s.getHasPermission()
}, subSettleTimeout, subCheckInterval, "should not have permission to subscribe")
time.Sleep(subscriptionTimeout)
// isDesired remains unchanged, no subscriber added to the data track
require.True(t, s.isDesired())
require.True(t, s.needsSubscribe())
require.Equal(t, 0, resolver.dataTrack.AddSubscriberCallCount())
// subscriber should have been notified that permission was denied
p := sm.params.Participant.(*typesfakes.FakeLocalParticipant)
require.Equal(t, 1, p.SendSubscriptionPermissionUpdateCallCount())
_, _, allowed := p.SendSubscriptionPermissionUpdateArgsForCall(0)
require.False(t, allowed)
// give permissions now
resolver.SetHasPermission(true)
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "should be subscribed")
require.NotNil(t, s.getDataDownTrack())
require.Equal(t, 1, resolver.dataTrack.AddSubscriberCallCount())
})
t.Run("permission revoked while subscribed", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
resolver := newTestDataTrackResolver(true, true, "pub", "pubID")
sm.params.DataTrackResolver = resolver.Resolve
sm.SubscribeToDataTrack("track")
sm.lock.RLock()
s := sm.dataTrackSubscriptions["track"]
sm.lock.RUnlock()
require.Eventually(t, func() bool {
return !s.needsSubscribe()
}, subSettleTimeout, subCheckInterval, "should be subscribed")
// revoke permission and close the down track like the publisher would
// (DataTrack.RevokeDisallowedSubscribers -> RemoveSubscriber -> DataDownTrack.Close)
resolver.SetHasPermission(false)
ddt := s.getDataDownTrack()
require.NotNil(t, ddt)
setTestDataDownTrackClosed(t, ddt)
// subscription stays desired but is denied on resubscribe attempts
require.Nil(t, s.getDataDownTrack())
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
return !s.getHasPermission()
}, subSettleTimeout, subCheckInterval, "should not have permission to resubscribe")
require.True(t, s.needsSubscribe())
require.Equal(t, 1, resolver.dataTrack.AddSubscriberCallCount())
// give permission back, should resubscribe
resolver.SetHasPermission(true)
require.Eventually(t, func() bool {
return !s.needsSubscribe() && s.getDataDownTrack() != nil
}, subSettleTimeout, subCheckInterval, "should be resubscribed")
require.Equal(t, 2, resolver.dataTrack.AddSubscriberCallCount())
})
t.Run("unsubscribe before data track resolves", func(t *testing.T) {
sm := newTestSubscriptionManager()
defer sm.Close(false)
// no track available, subscribe attempts fail with ErrTrackNotFound
resolver := newTestDataTrackResolver(true, false, "pub", "pubID")
sm.params.DataTrackResolver = resolver.Resolve
sm.SubscribeToDataTrack("track")
sm.lock.RLock()
s := sm.dataTrackSubscriptions["track"]
sm.lock.RUnlock()
require.NotNil(t, s)
// let the worker attempt (and fail) the subscribe
require.Eventually(t, func() bool {
return s.getNumAttempts() > 0
}, subSettleTimeout, subCheckInterval, "no subscribe attempt was made")
sm.UnsubscribeFromDataTrack("track")
require.Eventually(t, func() bool {
sm.lock.RLock()
_, ok := sm.dataTrackSubscriptions["track"]
sm.lock.RUnlock()
return !ok
}, subSettleTimeout, subCheckInterval, "data track subscription was not cleaned up")
})
}
type testSubscriptionParams struct {
SubscriptionLimitAudio int32
SubscriptionLimitVideo int32
}
func newTestSubscriptionManager() *SubscriptionManager {
return newTestSubscriptionManagerWithParams(testSubscriptionParams{})
}
func newTestSubscriptionManagerWithParams(params testSubscriptionParams) *SubscriptionManager {
p := &typesfakes.FakeLocalParticipant{}
p.CanSubscribeReturns(true)
p.IDReturns("subID")
p.IdentityReturns("sub")
p.KindReturns(livekit.ParticipantInfo_STANDARD)
tl := &typesfakes.FakeParticipantTelemetryListener{}
p.GetTelemetryListenerReturns(tl)
return NewSubscriptionManager(SubscriptionManagerParams{
Participant: p,
Logger: logger.GetLogger(),
OnTrackSubscribed: func(subTrack types.SubscribedTrack) {},
OnTrackUnsubscribed: func(subTrack types.SubscribedTrack) {},
OnSubscriptionError: func(trackID livekit.TrackID, fatal bool, err error) {},
TrackResolver: func(sub types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
return types.MediaResolverResult{}
},
SubscriptionLimitAudio: params.SubscriptionLimitAudio,
SubscriptionLimitVideo: params.SubscriptionLimitVideo,
})
}
type testResolver struct {
lock sync.Mutex
hasPermission bool
hasTrack bool
pubIdentity livekit.ParticipantIdentity
pubID livekit.ParticipantID
paused bool
}
func newTestResolver(hasPermission bool, hasTrack bool, pubIdentity livekit.ParticipantIdentity, pubID livekit.ParticipantID) *testResolver {
return &testResolver{
hasPermission: hasPermission,
hasTrack: hasTrack,
pubIdentity: pubIdentity,
pubID: pubID,
}
}
func (t *testResolver) SetPause(paused bool) {
t.lock.Lock()
defer t.lock.Unlock()
t.paused = paused
}
func (t *testResolver) Resolve(_subscriber types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
t.lock.Lock()
defer t.lock.Unlock()
res := types.MediaResolverResult{
TrackChangedNotifier: utils.NewChangeNotifier(),
TrackRemovedNotifier: utils.NewChangeNotifier(),
HasPermission: t.hasPermission,
PublisherID: t.pubID,
PublisherIdentity: t.pubIdentity,
}
if t.hasTrack && !t.paused {
mt := &typesfakes.FakeMediaTrack{}
st := &typesfakes.FakeSubscribedTrack{}
st.IDReturns(trackID)
st.PublisherIDReturns(t.pubID)
st.PublisherIdentityReturns(t.pubIdentity)
mt.AddSubscriberCalls(func(sub types.LocalParticipant) (types.SubscribedTrack, error) {
st.SubscriberReturns(sub)
return st, nil
})
st.MediaTrackReturns(mt)
res.Track = mt
}
return res
}
type testDataTrackResolver struct {
lock sync.Mutex
hasPermission bool
hasTrack bool
pubIdentity livekit.ParticipantIdentity
pubID livekit.ParticipantID
dataTrack *typesfakes.FakeDataTrack
}
func newTestDataTrackResolver(hasPermission bool, hasTrack bool, pubIdentity livekit.ParticipantIdentity, pubID livekit.ParticipantID) *testDataTrackResolver {
r := &testDataTrackResolver{
hasPermission: hasPermission,
hasTrack: hasTrack,
pubIdentity: pubIdentity,
pubID: pubID,
dataTrack: &typesfakes.FakeDataTrack{},
}
r.dataTrack.PublisherIDReturns(pubID)
r.dataTrack.PublisherIdentityReturns(pubIdentity)
r.dataTrack.AddSubscriberCalls(func(sub types.LocalParticipant) (types.DataDownTrack, error) {
ddt := &typesfakes.FakeDataDownTrack{}
ddt.PublishDataTrackReturns(r.dataTrack)
return ddt, nil
})
return r
}
func (t *testDataTrackResolver) SetHasPermission(hasPermission bool) {
t.lock.Lock()
defer t.lock.Unlock()
t.hasPermission = hasPermission
}
func (t *testDataTrackResolver) Resolve(_subscriber types.LocalParticipant, trackID livekit.TrackID) types.DataResolverResult {
t.lock.Lock()
defer t.lock.Unlock()
res := types.DataResolverResult{
TrackChangedNotifier: utils.NewChangeNotifier(),
TrackRemovedNotifier: utils.NewChangeNotifier(),
HasPermission: t.hasPermission,
PublisherID: t.pubID,
PublisherIdentity: t.pubIdentity,
}
if t.hasTrack {
t.dataTrack.IDReturns(trackID)
res.DataTrack = t.dataTrack
}
return res
}
func setTestSubscribedTrackBound(t *testing.T, st types.SubscribedTrack) {
fst, ok := st.(*typesfakes.FakeSubscribedTrack)
require.True(t, ok)
for i := 0; i < fst.AddOnBindCallCount(); i++ {
fst.AddOnBindArgsForCall(i)(nil)
}
}
func setTestSubscribedTrackClosed(t *testing.T, st types.SubscribedTrack, isExpectedToResume bool) {
fst, ok := st.(*typesfakes.FakeSubscribedTrack)
require.True(t, ok)
fst.OnCloseArgsForCall(0)(isExpectedToResume)
}
func setTestDataDownTrackClosed(t *testing.T, ddt types.DataDownTrack) {
fddt, ok := ddt.(*typesfakes.FakeDataDownTrack)
require.True(t, ok)
fddt.OnCloseArgsForCall(0)()
}

View File

@@ -0,0 +1,183 @@
// 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 supervisor
import (
"sync"
"time"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
const (
monitorInterval = 1 * time.Second
)
type ParticipantSupervisorParams struct {
Logger logger.Logger
}
type trackMonitor struct {
opMon types.OperationMonitor
err error
}
type ParticipantSupervisor struct {
params ParticipantSupervisorParams
lock sync.RWMutex
isPublisherConnected bool
publications map[livekit.TrackID]*trackMonitor
isStopped atomic.Bool
onPublicationError func(trackID livekit.TrackID)
}
func NewParticipantSupervisor(params ParticipantSupervisorParams) *ParticipantSupervisor {
p := &ParticipantSupervisor{
params: params,
publications: make(map[livekit.TrackID]*trackMonitor),
}
go p.checkState()
return p
}
func (p *ParticipantSupervisor) Stop() {
p.isStopped.Store(true)
}
func (p *ParticipantSupervisor) OnPublicationError(f func(trackID livekit.TrackID)) {
p.lock.Lock()
defer p.lock.Unlock()
p.onPublicationError = f
}
func (p *ParticipantSupervisor) getOnPublicationError() func(trackID livekit.TrackID) {
p.lock.RLock()
defer p.lock.RUnlock()
return p.onPublicationError
}
func (p *ParticipantSupervisor) SetPublisherPeerConnectionConnected(isConnected bool) {
p.lock.Lock()
p.isPublisherConnected = isConnected
for _, pm := range p.publications {
pm.opMon.PostEvent(types.OperationMonitorEventPublisherPeerConnectionConnected, p.isPublisherConnected)
}
p.lock.Unlock()
}
func (p *ParticipantSupervisor) AddPublication(trackID livekit.TrackID) {
p.lock.Lock()
pm, ok := p.publications[trackID]
if !ok {
pm = &trackMonitor{
opMon: NewPublicationMonitor(
PublicationMonitorParams{
TrackID: trackID,
IsPeerConnectionConnected: p.isPublisherConnected,
Logger: p.params.Logger,
},
),
}
p.publications[trackID] = pm
}
pm.opMon.PostEvent(types.OperationMonitorEventAddPendingPublication, nil)
p.lock.Unlock()
}
func (p *ParticipantSupervisor) SetPublicationMute(trackID livekit.TrackID, isMuted bool) {
p.lock.Lock()
pm, ok := p.publications[trackID]
if ok {
pm.opMon.PostEvent(types.OperationMonitorEventSetPublicationMute, isMuted)
}
p.lock.Unlock()
}
func (p *ParticipantSupervisor) SetPublishedTrack(trackID livekit.TrackID, pubTrack types.LocalMediaTrack) {
p.lock.RLock()
pm, ok := p.publications[trackID]
if ok {
pm.opMon.PostEvent(types.OperationMonitorEventSetPublishedTrack, pubTrack)
}
p.lock.RUnlock()
}
func (p *ParticipantSupervisor) ClearPublishedTrack(trackID livekit.TrackID, pubTrack types.LocalMediaTrack) {
p.lock.RLock()
pm, ok := p.publications[trackID]
if ok {
pm.opMon.PostEvent(types.OperationMonitorEventClearPublishedTrack, pubTrack)
}
p.lock.RUnlock()
}
func (p *ParticipantSupervisor) checkState() {
ticker := time.NewTicker(monitorInterval)
defer ticker.Stop()
for !p.isStopped.Load() {
<-ticker.C
p.checkPublications()
}
}
func (p *ParticipantSupervisor) checkPublications() {
var erroredPublications []livekit.TrackID
var removablePublications []livekit.TrackID
p.lock.RLock()
for trackID, pm := range p.publications {
if err := pm.opMon.Check(); err != nil {
if pm.err == nil {
p.params.Logger.Errorw("supervisor error on publication", err, "trackID", trackID)
pm.err = err
erroredPublications = append(erroredPublications, trackID)
}
} else {
if pm.err != nil {
p.params.Logger.Infow("supervisor publication recovered", "trackID", trackID)
pm.err = err
}
if pm.opMon.IsIdle() {
removablePublications = append(removablePublications, trackID)
}
}
}
p.lock.RUnlock()
p.lock.Lock()
for _, trackID := range removablePublications {
delete(p.publications, trackID)
}
p.lock.Unlock()
if onPublicationError := p.getOnPublicationError(); onPublicationError != nil {
for _, trackID := range erroredPublications {
onPublicationError(trackID)
}
}
}

View File

@@ -0,0 +1,190 @@
// 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 supervisor
import (
"errors"
"sync"
"time"
"github.com/gammazero/deque"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
const (
publishWaitDuration = 30 * time.Second
)
var (
errPublishTimeout = errors.New("publish time out")
)
type publish struct {
isStart bool
}
type PublicationMonitorParams struct {
TrackID livekit.TrackID
IsPeerConnectionConnected bool
Logger logger.Logger
}
type PublicationMonitor struct {
params PublicationMonitorParams
lock sync.RWMutex
desiredPublishes deque.Deque[*publish]
isConnected bool
publishedTrack types.LocalMediaTrack
isMuted bool
unmutedAt time.Time
}
func NewPublicationMonitor(params PublicationMonitorParams) *PublicationMonitor {
p := &PublicationMonitor{
params: params,
isConnected: params.IsPeerConnectionConnected,
}
p.desiredPublishes.SetBaseCap(4)
return p
}
func (p *PublicationMonitor) PostEvent(ome types.OperationMonitorEvent, omd types.OperationMonitorData) {
switch ome {
case types.OperationMonitorEventPublisherPeerConnectionConnected:
p.setConnected(omd.(bool))
case types.OperationMonitorEventAddPendingPublication:
p.addPending()
case types.OperationMonitorEventSetPublicationMute:
p.setMute(omd.(bool))
case types.OperationMonitorEventSetPublishedTrack:
p.setPublishedTrack(omd.(types.LocalMediaTrack))
case types.OperationMonitorEventClearPublishedTrack:
p.clearPublishedTrack(omd.(types.LocalMediaTrack))
}
}
func (p *PublicationMonitor) addPending() {
p.lock.Lock()
p.desiredPublishes.PushBack(
&publish{
isStart: true,
},
)
// synthesize an end
p.desiredPublishes.PushBack(
&publish{
isStart: false,
},
)
p.update()
p.lock.Unlock()
}
func (p *PublicationMonitor) maybeStartMonitor() {
if p.isConnected && !p.isMuted {
p.unmutedAt = time.Now()
}
}
func (p *PublicationMonitor) setConnected(isConnected bool) {
p.lock.Lock()
p.isConnected = isConnected
p.maybeStartMonitor()
p.lock.Unlock()
}
func (p *PublicationMonitor) setMute(isMuted bool) {
p.lock.Lock()
p.isMuted = isMuted
p.maybeStartMonitor()
p.lock.Unlock()
}
func (p *PublicationMonitor) setPublishedTrack(pubTrack types.LocalMediaTrack) {
p.lock.Lock()
p.publishedTrack = pubTrack
p.update()
p.lock.Unlock()
}
func (p *PublicationMonitor) clearPublishedTrack(pubTrack types.LocalMediaTrack) {
p.lock.Lock()
if p.publishedTrack == pubTrack {
p.publishedTrack = nil
} else {
p.params.Logger.Errorw("supervisor: mismatched published track on clear", nil, "trackID", p.params.TrackID)
}
p.update()
p.lock.Unlock()
}
func (p *PublicationMonitor) Check() error {
p.lock.RLock()
var pub *publish
if p.desiredPublishes.Len() > 0 {
pub = p.desiredPublishes.Front()
}
isMuted := p.isMuted
unmutedAt := p.unmutedAt
p.lock.RUnlock()
if pub == nil {
return nil
}
if pub.isStart && !isMuted && !unmutedAt.IsZero() && time.Since(unmutedAt) > publishWaitDuration {
// timed out waiting for publish
return errPublishTimeout
}
// give more time for publish to happen
// NOTE: synthesized end events do not have a start time, so do not check them for time out
return nil
}
func (p *PublicationMonitor) IsIdle() bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.desiredPublishes.Len() == 0 && p.publishedTrack == nil
}
func (p *PublicationMonitor) update() {
for {
var pub *publish
if p.desiredPublishes.Len() > 0 {
pub = p.desiredPublishes.PopFront()
}
if pub == nil {
return
}
if (pub.isStart && p.publishedTrack == nil) || (!pub.isStart && p.publishedTrack != nil) {
// put it back as the condition is not satisfied
p.desiredPublishes.PushFront(pub)
return
}
}
}

88
pkg/rtc/testutils.go Normal file
View File

@@ -0,0 +1,88 @@
// 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 rtc
import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
)
func NewMockParticipant(
identity livekit.ParticipantIdentity,
protocol types.ProtocolVersion,
hidden bool,
publisher bool,
participantListener types.LocalParticipantListener,
) *typesfakes.FakeLocalParticipant {
p := &typesfakes.FakeLocalParticipant{}
sid := guid.New(utils.ParticipantPrefix)
p.IDReturns(livekit.ParticipantID(sid))
p.IdentityReturns(identity)
p.StateReturns(livekit.ParticipantInfo_JOINED)
p.ProtocolVersionReturns(protocol)
p.CanSubscribeReturns(true)
p.CanPublishSourceReturns(!hidden)
p.CanPublishDataReturns(!hidden)
p.HiddenReturns(hidden)
p.ToProtoReturns(&livekit.ParticipantInfo{
Sid: sid,
Identity: string(identity),
State: livekit.ParticipantInfo_JOINED,
IsPublisher: publisher,
})
p.ToProtoWithVersionReturns(&livekit.ParticipantInfo{
Sid: sid,
Identity: string(identity),
State: livekit.ParticipantInfo_JOINED,
IsPublisher: publisher,
}, utils.TimedVersion(0))
p.SetMetadataCalls(func(m string) {
participantListener.OnParticipantUpdate(p)
})
updateTrack := func() {
participantListener.OnTrackUpdated(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam"))
}
p.SetTrackMutedCalls(func(mute *livekit.MuteTrackRequest, fromServer bool) *livekit.TrackInfo {
updateTrack()
return nil
})
p.AddTrackCalls(func(req *livekit.AddTrackRequest) {
updateTrack()
})
p.GetLoggerReturns(logger.GetLogger())
p.GetReporterReturns(roomobs.NewNoopParticipantSessionReporter())
return p
}
func NewMockTrack(kind livekit.TrackType, name string) *typesfakes.FakeMediaTrack {
t := &typesfakes.FakeMediaTrack{}
t.IDReturns(livekit.TrackID(guid.New(utils.TrackPrefix)))
t.KindReturns(kind)
t.NameReturns(name)
t.ToProtoReturns(&livekit.TrackInfo{
Type: kind,
Name: name,
})
return t
}

3408
pkg/rtc/transport.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
// 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 transport
import (
"errors"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
"github.com/livekit/protocol/livekit"
)
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
var (
ErrNoICECandidateHandler = errors.New("no ICE candidate handler")
ErrNoOfferHandler = errors.New("no offer handler")
ErrNoAnswerHandler = errors.New("no answer handler")
)
//counterfeiter:generate . Handler
type Handler interface {
OnICECandidate(c *webrtc.ICECandidate, target livekit.SignalTarget) error
OnInitialConnected()
OnFullyEstablished()
OnFailed(isShortLived bool, iceConnectionInfo *types.ICEConnectionInfo)
OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver)
OnDataMessage(kind livekit.DataPacket_Kind, data []byte)
OnDataMessageUnlabeled(data []byte)
OnDataTrackMessage(data []byte, arrivalTime int64)
OnDataSendError(err error)
OnOffer(sd webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error
OnSetRemoteDescriptionOffer()
OnAnswer(sd webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error
OnNegotiationStateChanged(state NegotiationState)
OnNegotiationFailed()
OnStreamStateChange(update *streamallocator.StreamStateUpdate) error
OnUnmatchedMedia(numAudios uint32, numVideos uint32) error
}
type UnimplementedHandler struct{}
func (h UnimplementedHandler) OnICECandidate(c *webrtc.ICECandidate, target livekit.SignalTarget) error {
return ErrNoICECandidateHandler
}
func (h UnimplementedHandler) OnInitialConnected() {}
func (h UnimplementedHandler) OnFullyEstablished() {}
func (h UnimplementedHandler) OnFailed(isShortLived bool) {}
func (h UnimplementedHandler) OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver) {}
func (h UnimplementedHandler) OnDataMessage(kind livekit.DataPacket_Kind, data []byte) {}
func (h UnimplementedHandler) OnDataMessageUnlabeled(data []byte) {}
func (h UnimplementedHandler) OnDataTrackMessage(data []byte, arrivalTime int64) {}
func (h UnimplementedHandler) OnDataSendError(err error) {}
func (h UnimplementedHandler) OnOffer(sd webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
return ErrNoOfferHandler
}
func (h UnimplementedHandler) OnSetRemoteDescriptionOffer() {}
func (h UnimplementedHandler) OnAnswer(sd webrtc.SessionDescription, answerId uint32, midToTrackID map[string]string) error {
return ErrNoAnswerHandler
}
func (h UnimplementedHandler) OnNegotiationStateChanged(state NegotiationState) {}
func (h UnimplementedHandler) OnNegotiationFailed() {}
func (h UnimplementedHandler) OnStreamStateChange(update *streamallocator.StreamStateUpdate) error {
return nil
}
func (h UnimplementedHandler) OnUnmatchedMedia(numAudios uint32, numVideos uint32) error {
return nil
}

View File

@@ -0,0 +1,40 @@
// 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 transport
import "fmt"
type NegotiationState int
const (
NegotiationStateNone NegotiationState = iota
// waiting for remote description
NegotiationStateRemote
// need to Negotiate again
NegotiationStateRetry
)
func (n NegotiationState) String() string {
switch n {
case NegotiationStateNone:
return "NONE"
case NegotiationStateRemote:
return "WAITING_FOR_REMOTE"
case NegotiationStateRetry:
return "RETRY"
default:
return fmt.Sprintf("%d", int(n))
}
}

View File

@@ -0,0 +1,807 @@
// Code generated by counterfeiter. DO NOT EDIT.
package transportfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/transport"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
"github.com/livekit/protocol/livekit"
webrtc "github.com/pion/webrtc/v4"
)
type FakeHandler struct {
OnAnswerStub func(webrtc.SessionDescription, uint32, map[string]string) error
onAnswerMutex sync.RWMutex
onAnswerArgsForCall []struct {
arg1 webrtc.SessionDescription
arg2 uint32
arg3 map[string]string
}
onAnswerReturns struct {
result1 error
}
onAnswerReturnsOnCall map[int]struct {
result1 error
}
OnDataMessageStub func(livekit.DataPacket_Kind, []byte)
onDataMessageMutex sync.RWMutex
onDataMessageArgsForCall []struct {
arg1 livekit.DataPacket_Kind
arg2 []byte
}
OnDataMessageUnlabeledStub func([]byte)
onDataMessageUnlabeledMutex sync.RWMutex
onDataMessageUnlabeledArgsForCall []struct {
arg1 []byte
}
OnDataSendErrorStub func(error)
onDataSendErrorMutex sync.RWMutex
onDataSendErrorArgsForCall []struct {
arg1 error
}
OnDataTrackMessageStub func([]byte, int64)
onDataTrackMessageMutex sync.RWMutex
onDataTrackMessageArgsForCall []struct {
arg1 []byte
arg2 int64
}
OnFailedStub func(bool, *types.ICEConnectionInfo)
onFailedMutex sync.RWMutex
onFailedArgsForCall []struct {
arg1 bool
arg2 *types.ICEConnectionInfo
}
OnFullyEstablishedStub func()
onFullyEstablishedMutex sync.RWMutex
onFullyEstablishedArgsForCall []struct {
}
OnICECandidateStub func(*webrtc.ICECandidate, livekit.SignalTarget) error
onICECandidateMutex sync.RWMutex
onICECandidateArgsForCall []struct {
arg1 *webrtc.ICECandidate
arg2 livekit.SignalTarget
}
onICECandidateReturns struct {
result1 error
}
onICECandidateReturnsOnCall map[int]struct {
result1 error
}
OnInitialConnectedStub func()
onInitialConnectedMutex sync.RWMutex
onInitialConnectedArgsForCall []struct {
}
OnNegotiationFailedStub func()
onNegotiationFailedMutex sync.RWMutex
onNegotiationFailedArgsForCall []struct {
}
OnNegotiationStateChangedStub func(transport.NegotiationState)
onNegotiationStateChangedMutex sync.RWMutex
onNegotiationStateChangedArgsForCall []struct {
arg1 transport.NegotiationState
}
OnOfferStub func(webrtc.SessionDescription, uint32, map[string]string) error
onOfferMutex sync.RWMutex
onOfferArgsForCall []struct {
arg1 webrtc.SessionDescription
arg2 uint32
arg3 map[string]string
}
onOfferReturns struct {
result1 error
}
onOfferReturnsOnCall map[int]struct {
result1 error
}
OnSetRemoteDescriptionOfferStub func()
onSetRemoteDescriptionOfferMutex sync.RWMutex
onSetRemoteDescriptionOfferArgsForCall []struct {
}
OnStreamStateChangeStub func(*streamallocator.StreamStateUpdate) error
onStreamStateChangeMutex sync.RWMutex
onStreamStateChangeArgsForCall []struct {
arg1 *streamallocator.StreamStateUpdate
}
onStreamStateChangeReturns struct {
result1 error
}
onStreamStateChangeReturnsOnCall map[int]struct {
result1 error
}
OnTrackStub func(*webrtc.TrackRemote, *webrtc.RTPReceiver)
onTrackMutex sync.RWMutex
onTrackArgsForCall []struct {
arg1 *webrtc.TrackRemote
arg2 *webrtc.RTPReceiver
}
OnUnmatchedMediaStub func(uint32, uint32) error
onUnmatchedMediaMutex sync.RWMutex
onUnmatchedMediaArgsForCall []struct {
arg1 uint32
arg2 uint32
}
onUnmatchedMediaReturns struct {
result1 error
}
onUnmatchedMediaReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeHandler) OnAnswer(arg1 webrtc.SessionDescription, arg2 uint32, arg3 map[string]string) error {
fake.onAnswerMutex.Lock()
ret, specificReturn := fake.onAnswerReturnsOnCall[len(fake.onAnswerArgsForCall)]
fake.onAnswerArgsForCall = append(fake.onAnswerArgsForCall, struct {
arg1 webrtc.SessionDescription
arg2 uint32
arg3 map[string]string
}{arg1, arg2, arg3})
stub := fake.OnAnswerStub
fakeReturns := fake.onAnswerReturns
fake.recordInvocation("OnAnswer", []interface{}{arg1, arg2, arg3})
fake.onAnswerMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeHandler) OnAnswerCallCount() int {
fake.onAnswerMutex.RLock()
defer fake.onAnswerMutex.RUnlock()
return len(fake.onAnswerArgsForCall)
}
func (fake *FakeHandler) OnAnswerCalls(stub func(webrtc.SessionDescription, uint32, map[string]string) error) {
fake.onAnswerMutex.Lock()
defer fake.onAnswerMutex.Unlock()
fake.OnAnswerStub = stub
}
func (fake *FakeHandler) OnAnswerArgsForCall(i int) (webrtc.SessionDescription, uint32, map[string]string) {
fake.onAnswerMutex.RLock()
defer fake.onAnswerMutex.RUnlock()
argsForCall := fake.onAnswerArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeHandler) OnAnswerReturns(result1 error) {
fake.onAnswerMutex.Lock()
defer fake.onAnswerMutex.Unlock()
fake.OnAnswerStub = nil
fake.onAnswerReturns = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnAnswerReturnsOnCall(i int, result1 error) {
fake.onAnswerMutex.Lock()
defer fake.onAnswerMutex.Unlock()
fake.OnAnswerStub = nil
if fake.onAnswerReturnsOnCall == nil {
fake.onAnswerReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.onAnswerReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnDataMessage(arg1 livekit.DataPacket_Kind, arg2 []byte) {
var arg2Copy []byte
if arg2 != nil {
arg2Copy = make([]byte, len(arg2))
copy(arg2Copy, arg2)
}
fake.onDataMessageMutex.Lock()
fake.onDataMessageArgsForCall = append(fake.onDataMessageArgsForCall, struct {
arg1 livekit.DataPacket_Kind
arg2 []byte
}{arg1, arg2Copy})
stub := fake.OnDataMessageStub
fake.recordInvocation("OnDataMessage", []interface{}{arg1, arg2Copy})
fake.onDataMessageMutex.Unlock()
if stub != nil {
fake.OnDataMessageStub(arg1, arg2)
}
}
func (fake *FakeHandler) OnDataMessageCallCount() int {
fake.onDataMessageMutex.RLock()
defer fake.onDataMessageMutex.RUnlock()
return len(fake.onDataMessageArgsForCall)
}
func (fake *FakeHandler) OnDataMessageCalls(stub func(livekit.DataPacket_Kind, []byte)) {
fake.onDataMessageMutex.Lock()
defer fake.onDataMessageMutex.Unlock()
fake.OnDataMessageStub = stub
}
func (fake *FakeHandler) OnDataMessageArgsForCall(i int) (livekit.DataPacket_Kind, []byte) {
fake.onDataMessageMutex.RLock()
defer fake.onDataMessageMutex.RUnlock()
argsForCall := fake.onDataMessageArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeHandler) OnDataMessageUnlabeled(arg1 []byte) {
var arg1Copy []byte
if arg1 != nil {
arg1Copy = make([]byte, len(arg1))
copy(arg1Copy, arg1)
}
fake.onDataMessageUnlabeledMutex.Lock()
fake.onDataMessageUnlabeledArgsForCall = append(fake.onDataMessageUnlabeledArgsForCall, struct {
arg1 []byte
}{arg1Copy})
stub := fake.OnDataMessageUnlabeledStub
fake.recordInvocation("OnDataMessageUnlabeled", []interface{}{arg1Copy})
fake.onDataMessageUnlabeledMutex.Unlock()
if stub != nil {
fake.OnDataMessageUnlabeledStub(arg1)
}
}
func (fake *FakeHandler) OnDataMessageUnlabeledCallCount() int {
fake.onDataMessageUnlabeledMutex.RLock()
defer fake.onDataMessageUnlabeledMutex.RUnlock()
return len(fake.onDataMessageUnlabeledArgsForCall)
}
func (fake *FakeHandler) OnDataMessageUnlabeledCalls(stub func([]byte)) {
fake.onDataMessageUnlabeledMutex.Lock()
defer fake.onDataMessageUnlabeledMutex.Unlock()
fake.OnDataMessageUnlabeledStub = stub
}
func (fake *FakeHandler) OnDataMessageUnlabeledArgsForCall(i int) []byte {
fake.onDataMessageUnlabeledMutex.RLock()
defer fake.onDataMessageUnlabeledMutex.RUnlock()
argsForCall := fake.onDataMessageUnlabeledArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeHandler) OnDataSendError(arg1 error) {
fake.onDataSendErrorMutex.Lock()
fake.onDataSendErrorArgsForCall = append(fake.onDataSendErrorArgsForCall, struct {
arg1 error
}{arg1})
stub := fake.OnDataSendErrorStub
fake.recordInvocation("OnDataSendError", []interface{}{arg1})
fake.onDataSendErrorMutex.Unlock()
if stub != nil {
fake.OnDataSendErrorStub(arg1)
}
}
func (fake *FakeHandler) OnDataSendErrorCallCount() int {
fake.onDataSendErrorMutex.RLock()
defer fake.onDataSendErrorMutex.RUnlock()
return len(fake.onDataSendErrorArgsForCall)
}
func (fake *FakeHandler) OnDataSendErrorCalls(stub func(error)) {
fake.onDataSendErrorMutex.Lock()
defer fake.onDataSendErrorMutex.Unlock()
fake.OnDataSendErrorStub = stub
}
func (fake *FakeHandler) OnDataSendErrorArgsForCall(i int) error {
fake.onDataSendErrorMutex.RLock()
defer fake.onDataSendErrorMutex.RUnlock()
argsForCall := fake.onDataSendErrorArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeHandler) OnDataTrackMessage(arg1 []byte, arg2 int64) {
var arg1Copy []byte
if arg1 != nil {
arg1Copy = make([]byte, len(arg1))
copy(arg1Copy, arg1)
}
fake.onDataTrackMessageMutex.Lock()
fake.onDataTrackMessageArgsForCall = append(fake.onDataTrackMessageArgsForCall, struct {
arg1 []byte
arg2 int64
}{arg1Copy, arg2})
stub := fake.OnDataTrackMessageStub
fake.recordInvocation("OnDataTrackMessage", []interface{}{arg1Copy, arg2})
fake.onDataTrackMessageMutex.Unlock()
if stub != nil {
fake.OnDataTrackMessageStub(arg1, arg2)
}
}
func (fake *FakeHandler) OnDataTrackMessageCallCount() int {
fake.onDataTrackMessageMutex.RLock()
defer fake.onDataTrackMessageMutex.RUnlock()
return len(fake.onDataTrackMessageArgsForCall)
}
func (fake *FakeHandler) OnDataTrackMessageCalls(stub func([]byte, int64)) {
fake.onDataTrackMessageMutex.Lock()
defer fake.onDataTrackMessageMutex.Unlock()
fake.OnDataTrackMessageStub = stub
}
func (fake *FakeHandler) OnDataTrackMessageArgsForCall(i int) ([]byte, int64) {
fake.onDataTrackMessageMutex.RLock()
defer fake.onDataTrackMessageMutex.RUnlock()
argsForCall := fake.onDataTrackMessageArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeHandler) OnFailed(arg1 bool, arg2 *types.ICEConnectionInfo) {
fake.onFailedMutex.Lock()
fake.onFailedArgsForCall = append(fake.onFailedArgsForCall, struct {
arg1 bool
arg2 *types.ICEConnectionInfo
}{arg1, arg2})
stub := fake.OnFailedStub
fake.recordInvocation("OnFailed", []interface{}{arg1, arg2})
fake.onFailedMutex.Unlock()
if stub != nil {
fake.OnFailedStub(arg1, arg2)
}
}
func (fake *FakeHandler) OnFailedCallCount() int {
fake.onFailedMutex.RLock()
defer fake.onFailedMutex.RUnlock()
return len(fake.onFailedArgsForCall)
}
func (fake *FakeHandler) OnFailedCalls(stub func(bool, *types.ICEConnectionInfo)) {
fake.onFailedMutex.Lock()
defer fake.onFailedMutex.Unlock()
fake.OnFailedStub = stub
}
func (fake *FakeHandler) OnFailedArgsForCall(i int) (bool, *types.ICEConnectionInfo) {
fake.onFailedMutex.RLock()
defer fake.onFailedMutex.RUnlock()
argsForCall := fake.onFailedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeHandler) OnFullyEstablished() {
fake.onFullyEstablishedMutex.Lock()
fake.onFullyEstablishedArgsForCall = append(fake.onFullyEstablishedArgsForCall, struct {
}{})
stub := fake.OnFullyEstablishedStub
fake.recordInvocation("OnFullyEstablished", []interface{}{})
fake.onFullyEstablishedMutex.Unlock()
if stub != nil {
fake.OnFullyEstablishedStub()
}
}
func (fake *FakeHandler) OnFullyEstablishedCallCount() int {
fake.onFullyEstablishedMutex.RLock()
defer fake.onFullyEstablishedMutex.RUnlock()
return len(fake.onFullyEstablishedArgsForCall)
}
func (fake *FakeHandler) OnFullyEstablishedCalls(stub func()) {
fake.onFullyEstablishedMutex.Lock()
defer fake.onFullyEstablishedMutex.Unlock()
fake.OnFullyEstablishedStub = stub
}
func (fake *FakeHandler) OnICECandidate(arg1 *webrtc.ICECandidate, arg2 livekit.SignalTarget) error {
fake.onICECandidateMutex.Lock()
ret, specificReturn := fake.onICECandidateReturnsOnCall[len(fake.onICECandidateArgsForCall)]
fake.onICECandidateArgsForCall = append(fake.onICECandidateArgsForCall, struct {
arg1 *webrtc.ICECandidate
arg2 livekit.SignalTarget
}{arg1, arg2})
stub := fake.OnICECandidateStub
fakeReturns := fake.onICECandidateReturns
fake.recordInvocation("OnICECandidate", []interface{}{arg1, arg2})
fake.onICECandidateMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeHandler) OnICECandidateCallCount() int {
fake.onICECandidateMutex.RLock()
defer fake.onICECandidateMutex.RUnlock()
return len(fake.onICECandidateArgsForCall)
}
func (fake *FakeHandler) OnICECandidateCalls(stub func(*webrtc.ICECandidate, livekit.SignalTarget) error) {
fake.onICECandidateMutex.Lock()
defer fake.onICECandidateMutex.Unlock()
fake.OnICECandidateStub = stub
}
func (fake *FakeHandler) OnICECandidateArgsForCall(i int) (*webrtc.ICECandidate, livekit.SignalTarget) {
fake.onICECandidateMutex.RLock()
defer fake.onICECandidateMutex.RUnlock()
argsForCall := fake.onICECandidateArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeHandler) OnICECandidateReturns(result1 error) {
fake.onICECandidateMutex.Lock()
defer fake.onICECandidateMutex.Unlock()
fake.OnICECandidateStub = nil
fake.onICECandidateReturns = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnICECandidateReturnsOnCall(i int, result1 error) {
fake.onICECandidateMutex.Lock()
defer fake.onICECandidateMutex.Unlock()
fake.OnICECandidateStub = nil
if fake.onICECandidateReturnsOnCall == nil {
fake.onICECandidateReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.onICECandidateReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnInitialConnected() {
fake.onInitialConnectedMutex.Lock()
fake.onInitialConnectedArgsForCall = append(fake.onInitialConnectedArgsForCall, struct {
}{})
stub := fake.OnInitialConnectedStub
fake.recordInvocation("OnInitialConnected", []interface{}{})
fake.onInitialConnectedMutex.Unlock()
if stub != nil {
fake.OnInitialConnectedStub()
}
}
func (fake *FakeHandler) OnInitialConnectedCallCount() int {
fake.onInitialConnectedMutex.RLock()
defer fake.onInitialConnectedMutex.RUnlock()
return len(fake.onInitialConnectedArgsForCall)
}
func (fake *FakeHandler) OnInitialConnectedCalls(stub func()) {
fake.onInitialConnectedMutex.Lock()
defer fake.onInitialConnectedMutex.Unlock()
fake.OnInitialConnectedStub = stub
}
func (fake *FakeHandler) OnNegotiationFailed() {
fake.onNegotiationFailedMutex.Lock()
fake.onNegotiationFailedArgsForCall = append(fake.onNegotiationFailedArgsForCall, struct {
}{})
stub := fake.OnNegotiationFailedStub
fake.recordInvocation("OnNegotiationFailed", []interface{}{})
fake.onNegotiationFailedMutex.Unlock()
if stub != nil {
fake.OnNegotiationFailedStub()
}
}
func (fake *FakeHandler) OnNegotiationFailedCallCount() int {
fake.onNegotiationFailedMutex.RLock()
defer fake.onNegotiationFailedMutex.RUnlock()
return len(fake.onNegotiationFailedArgsForCall)
}
func (fake *FakeHandler) OnNegotiationFailedCalls(stub func()) {
fake.onNegotiationFailedMutex.Lock()
defer fake.onNegotiationFailedMutex.Unlock()
fake.OnNegotiationFailedStub = stub
}
func (fake *FakeHandler) OnNegotiationStateChanged(arg1 transport.NegotiationState) {
fake.onNegotiationStateChangedMutex.Lock()
fake.onNegotiationStateChangedArgsForCall = append(fake.onNegotiationStateChangedArgsForCall, struct {
arg1 transport.NegotiationState
}{arg1})
stub := fake.OnNegotiationStateChangedStub
fake.recordInvocation("OnNegotiationStateChanged", []interface{}{arg1})
fake.onNegotiationStateChangedMutex.Unlock()
if stub != nil {
fake.OnNegotiationStateChangedStub(arg1)
}
}
func (fake *FakeHandler) OnNegotiationStateChangedCallCount() int {
fake.onNegotiationStateChangedMutex.RLock()
defer fake.onNegotiationStateChangedMutex.RUnlock()
return len(fake.onNegotiationStateChangedArgsForCall)
}
func (fake *FakeHandler) OnNegotiationStateChangedCalls(stub func(transport.NegotiationState)) {
fake.onNegotiationStateChangedMutex.Lock()
defer fake.onNegotiationStateChangedMutex.Unlock()
fake.OnNegotiationStateChangedStub = stub
}
func (fake *FakeHandler) OnNegotiationStateChangedArgsForCall(i int) transport.NegotiationState {
fake.onNegotiationStateChangedMutex.RLock()
defer fake.onNegotiationStateChangedMutex.RUnlock()
argsForCall := fake.onNegotiationStateChangedArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeHandler) OnOffer(arg1 webrtc.SessionDescription, arg2 uint32, arg3 map[string]string) error {
fake.onOfferMutex.Lock()
ret, specificReturn := fake.onOfferReturnsOnCall[len(fake.onOfferArgsForCall)]
fake.onOfferArgsForCall = append(fake.onOfferArgsForCall, struct {
arg1 webrtc.SessionDescription
arg2 uint32
arg3 map[string]string
}{arg1, arg2, arg3})
stub := fake.OnOfferStub
fakeReturns := fake.onOfferReturns
fake.recordInvocation("OnOffer", []interface{}{arg1, arg2, arg3})
fake.onOfferMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeHandler) OnOfferCallCount() int {
fake.onOfferMutex.RLock()
defer fake.onOfferMutex.RUnlock()
return len(fake.onOfferArgsForCall)
}
func (fake *FakeHandler) OnOfferCalls(stub func(webrtc.SessionDescription, uint32, map[string]string) error) {
fake.onOfferMutex.Lock()
defer fake.onOfferMutex.Unlock()
fake.OnOfferStub = stub
}
func (fake *FakeHandler) OnOfferArgsForCall(i int) (webrtc.SessionDescription, uint32, map[string]string) {
fake.onOfferMutex.RLock()
defer fake.onOfferMutex.RUnlock()
argsForCall := fake.onOfferArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeHandler) OnOfferReturns(result1 error) {
fake.onOfferMutex.Lock()
defer fake.onOfferMutex.Unlock()
fake.OnOfferStub = nil
fake.onOfferReturns = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnOfferReturnsOnCall(i int, result1 error) {
fake.onOfferMutex.Lock()
defer fake.onOfferMutex.Unlock()
fake.OnOfferStub = nil
if fake.onOfferReturnsOnCall == nil {
fake.onOfferReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.onOfferReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnSetRemoteDescriptionOffer() {
fake.onSetRemoteDescriptionOfferMutex.Lock()
fake.onSetRemoteDescriptionOfferArgsForCall = append(fake.onSetRemoteDescriptionOfferArgsForCall, struct {
}{})
stub := fake.OnSetRemoteDescriptionOfferStub
fake.recordInvocation("OnSetRemoteDescriptionOffer", []interface{}{})
fake.onSetRemoteDescriptionOfferMutex.Unlock()
if stub != nil {
fake.OnSetRemoteDescriptionOfferStub()
}
}
func (fake *FakeHandler) OnSetRemoteDescriptionOfferCallCount() int {
fake.onSetRemoteDescriptionOfferMutex.RLock()
defer fake.onSetRemoteDescriptionOfferMutex.RUnlock()
return len(fake.onSetRemoteDescriptionOfferArgsForCall)
}
func (fake *FakeHandler) OnSetRemoteDescriptionOfferCalls(stub func()) {
fake.onSetRemoteDescriptionOfferMutex.Lock()
defer fake.onSetRemoteDescriptionOfferMutex.Unlock()
fake.OnSetRemoteDescriptionOfferStub = stub
}
func (fake *FakeHandler) OnStreamStateChange(arg1 *streamallocator.StreamStateUpdate) error {
fake.onStreamStateChangeMutex.Lock()
ret, specificReturn := fake.onStreamStateChangeReturnsOnCall[len(fake.onStreamStateChangeArgsForCall)]
fake.onStreamStateChangeArgsForCall = append(fake.onStreamStateChangeArgsForCall, struct {
arg1 *streamallocator.StreamStateUpdate
}{arg1})
stub := fake.OnStreamStateChangeStub
fakeReturns := fake.onStreamStateChangeReturns
fake.recordInvocation("OnStreamStateChange", []interface{}{arg1})
fake.onStreamStateChangeMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeHandler) OnStreamStateChangeCallCount() int {
fake.onStreamStateChangeMutex.RLock()
defer fake.onStreamStateChangeMutex.RUnlock()
return len(fake.onStreamStateChangeArgsForCall)
}
func (fake *FakeHandler) OnStreamStateChangeCalls(stub func(*streamallocator.StreamStateUpdate) error) {
fake.onStreamStateChangeMutex.Lock()
defer fake.onStreamStateChangeMutex.Unlock()
fake.OnStreamStateChangeStub = stub
}
func (fake *FakeHandler) OnStreamStateChangeArgsForCall(i int) *streamallocator.StreamStateUpdate {
fake.onStreamStateChangeMutex.RLock()
defer fake.onStreamStateChangeMutex.RUnlock()
argsForCall := fake.onStreamStateChangeArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeHandler) OnStreamStateChangeReturns(result1 error) {
fake.onStreamStateChangeMutex.Lock()
defer fake.onStreamStateChangeMutex.Unlock()
fake.OnStreamStateChangeStub = nil
fake.onStreamStateChangeReturns = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnStreamStateChangeReturnsOnCall(i int, result1 error) {
fake.onStreamStateChangeMutex.Lock()
defer fake.onStreamStateChangeMutex.Unlock()
fake.OnStreamStateChangeStub = nil
if fake.onStreamStateChangeReturnsOnCall == nil {
fake.onStreamStateChangeReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.onStreamStateChangeReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnTrack(arg1 *webrtc.TrackRemote, arg2 *webrtc.RTPReceiver) {
fake.onTrackMutex.Lock()
fake.onTrackArgsForCall = append(fake.onTrackArgsForCall, struct {
arg1 *webrtc.TrackRemote
arg2 *webrtc.RTPReceiver
}{arg1, arg2})
stub := fake.OnTrackStub
fake.recordInvocation("OnTrack", []interface{}{arg1, arg2})
fake.onTrackMutex.Unlock()
if stub != nil {
fake.OnTrackStub(arg1, arg2)
}
}
func (fake *FakeHandler) OnTrackCallCount() int {
fake.onTrackMutex.RLock()
defer fake.onTrackMutex.RUnlock()
return len(fake.onTrackArgsForCall)
}
func (fake *FakeHandler) OnTrackCalls(stub func(*webrtc.TrackRemote, *webrtc.RTPReceiver)) {
fake.onTrackMutex.Lock()
defer fake.onTrackMutex.Unlock()
fake.OnTrackStub = stub
}
func (fake *FakeHandler) OnTrackArgsForCall(i int) (*webrtc.TrackRemote, *webrtc.RTPReceiver) {
fake.onTrackMutex.RLock()
defer fake.onTrackMutex.RUnlock()
argsForCall := fake.onTrackArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeHandler) OnUnmatchedMedia(arg1 uint32, arg2 uint32) error {
fake.onUnmatchedMediaMutex.Lock()
ret, specificReturn := fake.onUnmatchedMediaReturnsOnCall[len(fake.onUnmatchedMediaArgsForCall)]
fake.onUnmatchedMediaArgsForCall = append(fake.onUnmatchedMediaArgsForCall, struct {
arg1 uint32
arg2 uint32
}{arg1, arg2})
stub := fake.OnUnmatchedMediaStub
fakeReturns := fake.onUnmatchedMediaReturns
fake.recordInvocation("OnUnmatchedMedia", []interface{}{arg1, arg2})
fake.onUnmatchedMediaMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeHandler) OnUnmatchedMediaCallCount() int {
fake.onUnmatchedMediaMutex.RLock()
defer fake.onUnmatchedMediaMutex.RUnlock()
return len(fake.onUnmatchedMediaArgsForCall)
}
func (fake *FakeHandler) OnUnmatchedMediaCalls(stub func(uint32, uint32) error) {
fake.onUnmatchedMediaMutex.Lock()
defer fake.onUnmatchedMediaMutex.Unlock()
fake.OnUnmatchedMediaStub = stub
}
func (fake *FakeHandler) OnUnmatchedMediaArgsForCall(i int) (uint32, uint32) {
fake.onUnmatchedMediaMutex.RLock()
defer fake.onUnmatchedMediaMutex.RUnlock()
argsForCall := fake.onUnmatchedMediaArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeHandler) OnUnmatchedMediaReturns(result1 error) {
fake.onUnmatchedMediaMutex.Lock()
defer fake.onUnmatchedMediaMutex.Unlock()
fake.OnUnmatchedMediaStub = nil
fake.onUnmatchedMediaReturns = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) OnUnmatchedMediaReturnsOnCall(i int, result1 error) {
fake.onUnmatchedMediaMutex.Lock()
defer fake.onUnmatchedMediaMutex.Unlock()
fake.OnUnmatchedMediaStub = nil
if fake.onUnmatchedMediaReturnsOnCall == nil {
fake.onUnmatchedMediaReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.onUnmatchedMediaReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeHandler) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeHandler) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ transport.Handler = new(FakeHandler)

805
pkg/rtc/transport_test.go Normal file
View File

@@ -0,0 +1,805 @@
// 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 rtc
import (
"fmt"
"strings"
"sync"
"testing"
"time"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/rtc/transport"
"github.com/livekit/livekit-server/pkg/rtc/transport/transportfakes"
"github.com/livekit/livekit-server/pkg/testutils"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
func TestMissingAnswerDuringICERestart(t *testing.T) {
params := TransportParams{
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
handlerA := &transportfakes.FakeHandler{}
paramsA.Handler = handlerA
transportA, err := NewPCTransport(paramsA)
require.NoError(t, err)
_, err = transportA.pc.CreateDataChannel(ReliableDataChannel, nil)
require.NoError(t, err)
paramsB := params
handlerB := &transportfakes.FakeHandler{}
paramsB.Handler = handlerB
paramsB.IsOfferer = false
transportB, err := NewPCTransport(paramsB)
require.NoError(t, err)
// exchange ICE
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
connectTransports(t, transportA, transportB, handlerA, handlerB, false, 1, 1)
require.Equal(t, webrtc.ICEConnectionStateConnected, transportA.pc.ICEConnectionState())
require.Equal(t, webrtc.ICEConnectionStateConnected, transportB.pc.ICEConnectionState())
var negotiationState atomic.Value
transportA.OnNegotiationStateChanged(func(state transport.NegotiationState) {
negotiationState.Store(state)
})
// offer again, but missed
var offerReceived atomic.Bool
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, _offerId uint32, _midToTrackID map[string]string) error {
require.Equal(t, webrtc.SignalingStateHaveLocalOffer, transportA.pc.SignalingState())
require.Equal(t, transport.NegotiationStateRemote, negotiationState.Load().(transport.NegotiationState))
offerReceived.Store(true)
return nil
})
transportA.Negotiate(true)
require.Eventually(t, func() bool {
return offerReceived.Load()
}, 10*time.Second, time.Millisecond*10, "transportA offer not received")
connectTransports(t, transportA, transportB, handlerA, handlerB, true, 1, 1)
require.Equal(t, webrtc.ICEConnectionStateConnected, transportA.pc.ICEConnectionState())
require.Equal(t, webrtc.ICEConnectionStateConnected, transportB.pc.ICEConnectionState())
transportA.Close()
transportB.Close()
}
func TestNegotiationTiming(t *testing.T) {
params := TransportParams{
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
handlerA := &transportfakes.FakeHandler{}
paramsA.Handler = handlerA
transportA, err := NewPCTransport(paramsA)
require.NoError(t, err)
_, err = transportA.pc.CreateDataChannel(LossyDataChannel, nil)
require.NoError(t, err)
paramsB := params
handlerB := &transportfakes.FakeHandler{}
paramsB.Handler = handlerB
paramsB.IsOfferer = false
transportB, err := NewPCTransport(paramsB)
require.NoError(t, err)
require.False(t, transportA.IsEstablished())
require.False(t, transportB.IsEstablished())
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
firstOffer := atomic.Value{}
firstOfferId := atomic.Uint32{}
secondOffer := atomic.Value{}
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
if _, ok := firstOffer.Load().(*webrtc.SessionDescription); !ok {
firstOffer.Store(&sd)
firstOfferId.Store(offerId)
} else {
secondOffer.Store(&sd)
}
return nil
})
var negotiationState atomic.Value
transportA.OnNegotiationStateChanged(func(state transport.NegotiationState) {
negotiationState.Store(state)
})
// initial offer
transportA.Negotiate(true)
require.Eventually(t, func() bool {
state, ok := negotiationState.Load().(transport.NegotiationState)
if !ok {
return false
}
return state == transport.NegotiationStateRemote
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRemote")
// second try, should've flipped transport status to retry
transportA.Negotiate(true)
require.Eventually(t, func() bool {
state, ok := negotiationState.Load().(transport.NegotiationState)
if !ok {
return false
}
return state == transport.NegotiationStateRetry
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRetry")
// third try, should've stayed at retry
transportA.Negotiate(true)
time.Sleep(100 * time.Millisecond) // some time to process the negotiate event
require.Eventually(t, func() bool {
state, ok := negotiationState.Load().(transport.NegotiationState)
if !ok {
return false
}
return state == transport.NegotiationStateRetry
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRetry")
require.Eventually(t, func() bool {
_, ok := firstOffer.Load().(*webrtc.SessionDescription)
if !ok {
return false
}
if firstOfferId.Load() == 0 {
return false
}
return true
}, 10*time.Second, 10*time.Millisecond, "first offer not received yet")
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
transportA.HandleRemoteDescription(answer, answerId)
return nil
})
transportB.HandleRemoteDescription(*firstOffer.Load().(*webrtc.SessionDescription), firstOfferId.Load())
require.Eventually(t, func() bool {
return transportA.IsEstablished()
}, 10*time.Second, time.Millisecond*10, "transportA is not established")
require.Eventually(t, func() bool {
return transportB.IsEstablished()
}, 10*time.Second, time.Millisecond*10, "transportB is not established")
// offerer should send another offer after processing the answer
// as there were forced negotiations a couple of time above
require.Eventually(t, func() bool {
state, ok := negotiationState.Load().(transport.NegotiationState)
if !ok {
return false
}
return state == transport.NegotiationStateRemote
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRemote")
_, ok := secondOffer.Load().(*webrtc.SessionDescription)
require.True(t, ok)
transportA.Close()
transportB.Close()
}
func TestFirstOfferMissedDuringICERestart(t *testing.T) {
params := TransportParams{
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
handlerA := &transportfakes.FakeHandler{}
paramsA.Handler = handlerA
transportA, err := NewPCTransport(paramsA)
require.NoError(t, err)
_, err = transportA.pc.CreateDataChannel(ReliableDataChannel, nil)
require.NoError(t, err)
paramsB := params
handlerB := &transportfakes.FakeHandler{}
paramsB.Handler = handlerB
paramsB.IsOfferer = false
transportB, err := NewPCTransport(paramsB)
require.NoError(t, err)
// exchange ICE
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
// first offer missed
var firstOfferReceived atomic.Bool
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, _offerId uint32, _midToTrackID map[string]string) error {
firstOfferReceived.Store(true)
return nil
})
transportA.Negotiate(true)
require.Eventually(t, func() bool {
return firstOfferReceived.Load()
}, 10*time.Second, 10*time.Millisecond, "first offer not received")
// set offer/answer with restart ICE, will negotiate twice,
// first one is recover from missed offer
// second one is restartICE
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
transportA.HandleRemoteDescription(answer, answerId)
return nil
})
var offerCount atomic.Int32
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
offerCount.Inc()
// the second offer is a ice restart offer, so we wait transportB complete the ice gathering
if transportB.pc.ICEGatheringState() == webrtc.ICEGatheringStateGathering {
require.Eventually(t, func() bool {
return transportB.pc.ICEGatheringState() == webrtc.ICEGatheringStateComplete
}, 10*time.Second, time.Millisecond*10)
}
transportB.HandleRemoteDescription(sd, offerId)
return nil
})
// first establish connection
transportA.ICERestart()
// ensure we are connected
require.Eventually(t, func() bool {
return transportA.pc.ICEConnectionState() == webrtc.ICEConnectionStateConnected &&
transportB.pc.ICEConnectionState() == webrtc.ICEConnectionStateConnected &&
offerCount.Load() == 2
}, testutils.ConnectTimeout, 10*time.Millisecond, "transport did not connect")
transportA.Close()
transportB.Close()
}
func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
params := TransportParams{
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
handlerA := &transportfakes.FakeHandler{}
paramsA.Handler = handlerA
transportA, err := NewPCTransport(paramsA)
require.NoError(t, err)
_, err = transportA.pc.CreateDataChannel(LossyDataChannel, nil)
require.NoError(t, err)
paramsB := params
handlerB := &transportfakes.FakeHandler{}
paramsB.Handler = handlerB
paramsB.IsOfferer = false
transportB, err := NewPCTransport(paramsB)
require.NoError(t, err)
// exchange ICE
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
// first answer missed
var firstAnswerReceived atomic.Bool
handlerB.OnAnswerCalls(func(sd webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
if firstAnswerReceived.Load() {
transportA.HandleRemoteDescription(sd, answerId)
} else {
// do not send first answer so that remote misses the first answer
firstAnswerReceived.Store(true)
}
return nil
})
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
transportB.HandleRemoteDescription(sd, offerId)
return nil
})
transportA.Negotiate(true)
require.Eventually(t, func() bool {
return transportB.pc.SignalingState() == webrtc.SignalingStateStable && firstAnswerReceived.Load()
}, time.Second, 10*time.Millisecond, "transportB signaling state did not go to stable")
// set offer/answer with restart ICE, will negotiate twice,
// first one is recover from missed offer
// second one is restartICE
var offerCount atomic.Int32
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
offerCount.Inc()
// the second offer is a ice restart offer, so we wait for transportB to complete ICE gathering
if transportB.pc.ICEGatheringState() == webrtc.ICEGatheringStateGathering {
require.Eventually(t, func() bool {
return transportB.pc.ICEGatheringState() == webrtc.ICEGatheringStateComplete
}, 10*time.Second, time.Millisecond*10)
}
transportB.HandleRemoteDescription(sd, offerId)
return nil
})
// first establish connection
transportA.ICERestart()
// ensure we are connected
require.Eventually(t, func() bool {
return transportA.pc.ICEConnectionState() == webrtc.ICEConnectionStateConnected &&
transportB.pc.ICEConnectionState() == webrtc.ICEConnectionStateConnected &&
offerCount.Load() == 2
}, testutils.ConnectTimeout, 10*time.Millisecond, "transport did not connect")
transportA.Close()
transportB.Close()
}
func TestNegotiationFailed(t *testing.T) {
params := TransportParams{
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
handlerA := &transportfakes.FakeHandler{}
paramsA.Handler = handlerA
transportA, err := NewPCTransport(paramsA)
require.NoError(t, err)
_, err = transportA.pc.CreateDataChannel(ReliableDataChannel, nil)
require.NoError(t, err)
paramsB := params
handlerB := &transportfakes.FakeHandler{}
paramsB.Handler = handlerB
paramsB.IsOfferer = false
transportB, err := NewPCTransport(paramsB)
require.NoError(t, err)
// exchange ICE
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
// wait for transport to be connected before maiming the signalling channel
connectTransports(t, transportA, transportB, handlerA, handlerB, false, 1, 1)
// reset OnOffer to force a negotiation failure
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
return nil
})
var failed atomic.Int32
handlerA.OnNegotiationFailedCalls(func() {
failed.Inc()
})
transportA.Negotiate(true)
require.Eventually(t, func() bool {
return failed.Load() == 1
}, negotiationFailedTimeout+time.Second, 10*time.Millisecond, "negotiation failed")
transportA.Close()
}
func TestFilteringCandidates(t *testing.T) {
params := TransportParams{
Config: &WebRTCConfig{},
EnabledPublishCodecs: []*livekit.Codec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeVP8.String()},
{Mime: mime.MimeTypeH264.String()},
},
Handler: &transportfakes.FakeHandler{},
}
transport, err := NewPCTransport(params)
require.NoError(t, err)
_, err = transport.pc.CreateDataChannel(ReliableDataChannel, nil)
require.NoError(t, err)
_, err = transport.pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio)
require.NoError(t, err)
_, err = transport.pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := transport.pc.CreateOffer(nil)
require.NoError(t, err)
offerGatheringComplete := webrtc.GatheringCompletePromise(transport.pc)
require.NoError(t, transport.pc.SetLocalDescription(offer))
<-offerGatheringComplete
// should not filter out UDP candidates if TCP is not preferred
offer = *transport.pc.LocalDescription()
filteredOffer := transport.filterCandidates(offer, false, true)
require.EqualValues(t, offer.SDP, filteredOffer.SDP)
parsed, err := offer.Unmarshal()
require.NoError(t, err)
// add a couple of TCP candidates
done := false
for _, m := range parsed.MediaDescriptions {
for _, a := range m.Attributes {
if a.Key == sdp.AttrKeyCandidate {
for idx, aa := range m.Attributes {
if aa.Key == sdp.AttrKeyEndOfCandidates {
modifiedAttributes := make([]sdp.Attribute, idx)
copy(modifiedAttributes, m.Attributes[:idx])
modifiedAttributes = append(modifiedAttributes, []sdp.Attribute{
{
Key: sdp.AttrKeyCandidate,
Value: "054225987 1 tcp 2124414975 159.203.70.248 7881 typ host tcptype passive",
},
{
Key: sdp.AttrKeyCandidate,
Value: "054225987 2 tcp 2124414975 159.203.70.248 7881 typ host tcptype passive",
},
}...)
m.Attributes = append(modifiedAttributes, m.Attributes[idx:]...)
done = true
break
}
}
}
if done {
break
}
}
if done {
break
}
}
bytes, err := parsed.Marshal()
require.NoError(t, err)
offer.SDP = string(bytes)
parsed, err = offer.Unmarshal()
require.NoError(t, err)
getNumTransportTypeCandidates := func(sd *sdp.SessionDescription) (int, int) {
numUDPCandidates := 0
numTCPCandidates := 0
for _, a := range sd.Attributes {
if a.Key == sdp.AttrKeyCandidate {
if strings.Contains(a.Value, "udp") {
numUDPCandidates++
}
if strings.Contains(a.Value, "tcp") {
numTCPCandidates++
}
}
}
for _, m := range sd.MediaDescriptions {
for _, a := range m.Attributes {
if a.Key == sdp.AttrKeyCandidate {
if strings.Contains(a.Value, "udp") {
numUDPCandidates++
}
if strings.Contains(a.Value, "tcp") {
numTCPCandidates++
}
}
}
}
return numUDPCandidates, numTCPCandidates
}
udp, tcp := getNumTransportTypeCandidates(parsed)
require.NotZero(t, udp)
require.Equal(t, 2, tcp)
transport.SetPreferTCP(true)
filteredOffer = transport.filterCandidates(offer, true, true)
parsed, err = filteredOffer.Unmarshal()
require.NoError(t, err)
udp, tcp = getNumTransportTypeCandidates(parsed)
require.Zero(t, udp)
require.Equal(t, 2, tcp)
transport.Close()
}
func handleICEExchange(t *testing.T, a, b *PCTransport, ah, bh *transportfakes.FakeHandler) {
ah.OnICECandidateCalls(func(candidate *webrtc.ICECandidate, target livekit.SignalTarget) error {
if candidate == nil {
return nil
}
t.Logf("got ICE candidate from A: %v", candidate)
b.AddICECandidate(candidate.ToJSON())
return nil
})
bh.OnICECandidateCalls(func(candidate *webrtc.ICECandidate, target livekit.SignalTarget) error {
if candidate == nil {
return nil
}
t.Logf("got ICE candidate from B: %v", candidate)
a.AddICECandidate(candidate.ToJSON())
return nil
})
}
func connectTransports(t *testing.T, offerer, answerer *PCTransport, offererHandler, answererHandler *transportfakes.FakeHandler, isICERestart bool, expectedOfferCount int32, expectedAnswerCount int32) {
var offerCount atomic.Int32
var answerCount atomic.Int32
answererHandler.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
answerCount.Inc()
offerer.HandleRemoteDescription(answer, answerId)
return nil
})
offererHandler.OnOfferCalls(func(offer webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
offerCount.Inc()
answerer.HandleRemoteDescription(offer, offerId)
return nil
})
if isICERestart {
offerer.ICERestart()
} else {
offerer.Negotiate(true)
}
require.Eventually(t, func() bool {
return offerCount.Load() == expectedOfferCount
}, 10*time.Second, time.Millisecond*10, fmt.Sprintf("offer count mismatch, expected: %d, actual: %d", expectedOfferCount, offerCount.Load()))
require.Eventually(t, func() bool {
return offerer.pc.ICEConnectionState() == webrtc.ICEConnectionStateConnected
}, 10*time.Second, time.Millisecond*10, "offerer did not become connected")
require.Eventually(t, func() bool {
return answerCount.Load() == expectedAnswerCount
}, 10*time.Second, time.Millisecond*10, fmt.Sprintf("answer count mismatch, expected: %d, actual: %d", expectedAnswerCount, answerCount.Load()))
require.Eventually(t, func() bool {
return answerer.pc.ICEConnectionState() == webrtc.ICEConnectionStateConnected
}, 10*time.Second, time.Millisecond*10, "answerer did not become connected")
transportsConnected := untilTransportsConnected(offererHandler, answererHandler)
transportsConnected.Wait()
}
func untilTransportsConnected(transports ...*transportfakes.FakeHandler) *sync.WaitGroup {
var triggered sync.WaitGroup
triggered.Add(len(transports))
for _, t := range transports {
var done atomic.Value
done.Store(false)
hdlr := func() {
if val, ok := done.Load().(bool); ok && !val {
done.Store(true)
triggered.Done()
}
}
if t.OnInitialConnectedCallCount() != 0 {
hdlr()
}
t.OnInitialConnectedCalls(hdlr)
}
return &triggered
}
func TestConfigureAudioTransceiver(t *testing.T) {
for _, testcase := range []struct {
nack bool
stereo bool
}{
{false, false},
{true, false},
{false, true},
{true, true},
} {
t.Run(fmt.Sprintf("nack=%v,stereo=%v", testcase.nack, testcase.stereo), func(t *testing.T) {
var me webrtc.MediaEngine
registerCodecs(&me, []*livekit.Codec{{Mime: mime.MimeTypeOpus.String()}}, RTCPFeedbackConfig{Audio: []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}}, false)
pc, err := webrtc.NewAPI(webrtc.WithMediaEngine(&me)).NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
tr, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
require.NoError(t, err)
configureSenderAudio(tr, testcase.stereo, testcase.nack, nil)
codecs := tr.Sender().GetParameters().Codecs
for _, codec := range codecs {
if mime.IsMimeTypeStringOpus(codec.MimeType) {
require.Equal(t, testcase.stereo, strings.Contains(codec.SDPFmtpLine, "sprop-stereo=1"))
var nackEnabled bool
for _, fb := range codec.RTCPFeedback {
if fb.Type == webrtc.TypeRTCPFBNACK {
nackEnabled = true
break
}
}
require.Equal(t, testcase.nack, nackEnabled)
}
}
})
}
}
// When answering a subscriber offer, the sender's audio payload type must echo
// the payload type the offer assigned (RFC 3264), otherwise Firefox decodes no
// audio. See https://github.com/livekit/livekit/issues/4599.
func TestConfigureAudioTransceiverEchoesOfferPayloadType(t *testing.T) {
var me webrtc.MediaEngine
registerCodecs(&me, []*livekit.Codec{{Mime: mime.MimeTypeOpus.String()}}, RTCPFeedbackConfig{Audio: []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}}, false)
pc, err := webrtc.NewAPI(webrtc.WithMediaEngine(&me)).NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
tr, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
require.NoError(t, err)
// offer mapped Opus to a payload type different from the server MediaEngine's.
const offeredOpusPT = webrtc.PayloadType(109)
configureSenderAudio(tr, false, true, map[mime.MimeType]webrtc.PayloadType{mime.MimeTypeOpus: offeredOpusPT})
var found bool
for _, codec := range tr.Sender().GetParameters().Codecs {
if mime.IsMimeTypeStringOpus(codec.MimeType) {
require.Equal(t, offeredOpusPT, codec.PayloadType)
found = true
}
}
require.True(t, found, "opus codec must be present in sender preferences")
}
// In single-PC mode the publisher PC carries both publish and subscribe
// directions. If the MediaEngine were built only from the publish codec list,
// the SDP offer would not advertise some codecs in the m-section even though
// the subscribe direction is supposed to support it. This regression-tests
// the union behavior in newPeerConnection: build the MediaEngine from publish +
// subscribe codec lists.
func TestSinglePCMediaEngineUnionsCodecs(t *testing.T) {
videoMSectionCodecs := func(transport *PCTransport) []string {
_, err := transport.pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := transport.pc.CreateOffer(nil)
require.NoError(t, err)
parsed, err := offer.Unmarshal()
require.NoError(t, err)
var rtpmaps []string
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media != "video" {
continue
}
for _, a := range m.Attributes {
if a.Key == "rtpmap" {
rtpmaps = append(rtpmaps, a.Value)
}
}
}
return rtpmaps
}
sdpHasH264 := func(rtpmaps []string) bool {
for _, r := range rtpmaps {
if strings.Contains(r, "H264/") {
return true
}
}
return false
}
publishOnly := []*livekit.Codec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeVP8.String()},
}
subscribeOnly := []*livekit.Codec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeVP8.String()},
{Mime: mime.MimeTypeH264.String()},
}
// Control: only publish codecs set (dual-PC publisher PC). H.264 absent.
dualPC, err := NewPCTransport(TransportParams{
Config: &WebRTCConfig{},
EnabledPublishCodecs: publishOnly,
Handler: &transportfakes.FakeHandler{},
})
require.NoError(t, err)
require.False(t, sdpHasH264(videoMSectionCodecs(dualPC)),
"dual-PC publisher must not advertise H.264 when it's stripped from the publish list")
// Single-PC publisher PC: both lists set. H.264 must appear.
singlePC, err := NewPCTransport(TransportParams{
Config: &WebRTCConfig{},
EnabledPublishCodecs: publishOnly,
EnabledSubscribeCodecs: subscribeOnly,
IsSendSide: true,
Handler: &transportfakes.FakeHandler{},
})
require.NoError(t, err)
require.True(t, sdpHasH264(videoMSectionCodecs(singlePC)),
"single-PC publisher must advertise H.264 from the subscribe list even when it's stripped from the publish list")
}
// Regression test for restrictReceiverCodecsToPublishList: subscribe-only
// codecs (e.g., H.264) registered for subscriptions must not leak into the
// recv-side m-section of an answer, or the peer could publish them.
func TestSinglePCAnswerStripsSubscribeOnlyCodecsFromRecvSide(t *testing.T) {
publishCodecs := []*livekit.Codec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeVP8.String()},
}
subscribeCodecs := []*livekit.Codec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeVP8.String()},
{Mime: mime.MimeTypeH264.String()},
}
handler := &transportfakes.FakeHandler{}
server, err := NewPCTransport(TransportParams{
Config: &WebRTCConfig{},
EnabledPublishCodecs: publishCodecs,
EnabledSubscribeCodecs: subscribeCodecs,
IsSendSide: true,
Handler: handler,
})
require.NoError(t, err)
defer server.Close()
var clientME webrtc.MediaEngine
require.NoError(t, registerCodecs(&clientME, subscribeCodecs, RTCPFeedbackConfig{}, false))
client, err := webrtc.NewAPI(webrtc.WithMediaEngine(&clientME)).NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer client.Close()
_, err = client.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
})
require.NoError(t, err)
offer, err := client.CreateOffer(nil)
require.NoError(t, err)
require.Contains(t, offer.SDP, "H264/", "offer must advertise H.264")
require.NoError(t, client.SetLocalDescription(offer))
var answer atomic.Pointer[webrtc.SessionDescription]
handler.OnAnswerCalls(func(sd webrtc.SessionDescription, _ uint32, _ map[string]string) error {
answer.Store(&sd)
return nil
})
require.NoError(t, server.HandleRemoteDescription(*client.LocalDescription(), 1))
require.Eventually(t, func() bool {
return answer.Load() != nil
}, 5*time.Second, 10*time.Millisecond, "server did not produce answer")
parsed, err := answer.Load().Unmarshal()
require.NoError(t, err)
var videoSection *sdp.MediaDescription
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "video" {
videoSection = m
break
}
}
require.NotNil(t, videoSection, "answer missing video m-section")
for _, a := range videoSection.Attributes {
if a.Key != "rtpmap" {
continue
}
require.NotContains(t, a.Value, "H264/",
"answer must not advertise H.264 in recv-side m-section: %s", a.Value)
}
}

1086
pkg/rtc/transportmanager.go Normal file

File diff suppressed because it is too large Load Diff

414
pkg/rtc/types/ice.go Normal file
View File

@@ -0,0 +1,414 @@
/*
* 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 types
import (
"fmt"
"slices"
"strings"
"sync"
"github.com/pion/ice/v4"
"github.com/pion/webrtc/v4"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
)
type ICEConnectionType int
const (
// this is in ICE priority highest -> lowest ordering
// WARNING: Keep this ordering as it is used to find lowest priority connection type.
ICEConnectionTypeUnknown ICEConnectionType = iota
ICEConnectionTypeUDP
ICEConnectionTypeTCP
ICEConnectionTypeTURN
)
func (i ICEConnectionType) String() string {
switch i {
case ICEConnectionTypeUnknown:
return "unknown"
case ICEConnectionTypeUDP:
return "udp"
case ICEConnectionTypeTCP:
return "tcp"
case ICEConnectionTypeTURN:
return "turn"
default:
return "unknown"
}
}
func (i ICEConnectionType) ReporterType() roomobs.ConnectionType {
switch i {
case ICEConnectionTypeUnknown:
return roomobs.ConnectionTypeUndefined
case ICEConnectionTypeUDP:
return roomobs.ConnectionTypeUDP
case ICEConnectionTypeTCP:
return roomobs.ConnectionTypeTCP
case ICEConnectionTypeTURN:
return roomobs.ConnectionTypeTurn
default:
return roomobs.ConnectionTypeUndefined
}
}
// --------------------------------------------
type ICECandidateExtended struct {
Candidate *webrtc.ICECandidate
SelectedOrder int
Filtered bool
Trickle bool
}
// --------------------------------------------
type ICEConnectionInfo struct {
Local []*ICECandidateExtended
Remote []*ICECandidateExtended
Transport livekit.SignalTarget
Type ICEConnectionType
}
func (i *ICEConnectionInfo) HasCandidates() bool {
return len(i.Local) > 0 || len(i.Remote) > 0
}
func ICEConnectionInfosType(infos []*ICEConnectionInfo) ICEConnectionType {
for _, info := range infos {
if info.Type != ICEConnectionTypeUnknown {
return info.Type
}
}
return ICEConnectionTypeUnknown
}
// --------------------------------------------
type ICEConnectionDetails struct {
ICEConnectionInfo
lock sync.Mutex
selectedCount int
logger logger.Logger
}
func NewICEConnectionDetails(transport livekit.SignalTarget, l logger.Logger) *ICEConnectionDetails {
d := &ICEConnectionDetails{
ICEConnectionInfo: ICEConnectionInfo{
Transport: transport,
Type: ICEConnectionTypeUnknown,
},
logger: l,
}
return d
}
func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
d.lock.Lock()
defer d.lock.Unlock()
info := &ICEConnectionInfo{
Transport: d.Transport,
Type: d.Type,
Local: make([]*ICECandidateExtended, 0, len(d.Local)),
Remote: make([]*ICECandidateExtended, 0, len(d.Remote)),
}
for _, c := range d.Local {
info.Local = append(info.Local, &ICECandidateExtended{
Candidate: c.Candidate,
Filtered: c.Filtered,
SelectedOrder: c.SelectedOrder,
Trickle: c.Trickle,
})
}
for _, c := range d.Remote {
info.Remote = append(info.Remote, &ICECandidateExtended{
Candidate: c.Candidate,
Filtered: c.Filtered,
SelectedOrder: c.SelectedOrder,
Trickle: c.Trickle,
})
}
return info
}
func (d *ICEConnectionDetails) GetConnectionType() ICEConnectionType {
d.lock.Lock()
defer d.lock.Unlock()
return d.Type
}
func (d *ICEConnectionDetails) AddLocalCandidate(c *webrtc.ICECandidate, filtered, trickle bool) {
d.lock.Lock()
defer d.lock.Unlock()
compFn := func(e *ICECandidateExtended) bool {
return isCandidateEqualTo(e.Candidate, c)
}
if slices.ContainsFunc(d.Local, compFn) {
return
}
d.Local = append(d.Local, &ICECandidateExtended{
Candidate: c,
Filtered: filtered,
Trickle: trickle,
})
}
func (d *ICEConnectionDetails) AddLocalICECandidate(c ice.Candidate, filtered, trickle bool) {
candidate, err := unmarshalCandidate(c)
if err != nil {
d.logger.Errorw("could not unmarshal ice candidate", err, "candidate", c)
return
}
d.AddLocalCandidate(candidate, filtered, trickle)
}
func (d *ICEConnectionDetails) AddRemoteCandidate(c webrtc.ICECandidateInit, filtered, trickle, canUpdate bool) {
iceCandidate, err := unmarshalICECandidate(c)
if err != nil {
d.logger.Errorw("could not unmarshal candidate", err, "candidate", c)
return
}
d.AddRemoteICECandidate(iceCandidate, filtered, trickle, canUpdate)
}
func (d *ICEConnectionDetails) AddRemoteICECandidate(iceCandidate ice.Candidate, filtered, trickle, canUpdate bool) {
if iceCandidate == nil {
// end-of-candidates candidate
return
}
candidate, err := unmarshalCandidate(iceCandidate)
if err != nil {
d.logger.Errorw("could not unmarshal ice candidate", err, "candidate", iceCandidate)
return
}
d.lock.Lock()
defer d.lock.Unlock()
indexFn := func(e *ICECandidateExtended) bool {
return isCandidateEqualTo(e.Candidate, candidate)
}
if idx := slices.IndexFunc(d.Remote, indexFn); idx != -1 {
if canUpdate {
d.Remote[idx].Filtered = filtered
d.Remote[idx].Trickle = trickle
}
return
}
d.Remote = append(d.Remote, &ICECandidateExtended{
Candidate: candidate,
Filtered: filtered,
Trickle: trickle,
})
d.updateConnectionTypeLocked()
}
func (d *ICEConnectionDetails) Clear() {
d.lock.Lock()
defer d.lock.Unlock()
d.Local = nil
d.Remote = nil
d.Type = ICEConnectionTypeUnknown
}
func (d *ICEConnectionDetails) SetSelectedPair(pair *webrtc.ICECandidatePair) {
d.lock.Lock()
defer d.lock.Unlock()
d.selectedCount++
remoteIdx := slices.IndexFunc(d.Remote, func(e *ICECandidateExtended) bool {
return isCandidateEqualTo(e.Candidate, pair.Remote)
})
if remoteIdx < 0 {
// it's possible for prflx candidates to be generated by Pion, we'll add them
d.Remote = append(d.Remote, &ICECandidateExtended{
Candidate: pair.Remote,
Filtered: false,
Trickle: false,
})
remoteIdx = len(d.Remote) - 1
}
d.Remote[remoteIdx].SelectedOrder = d.selectedCount
d.updateConnectionTypeLocked()
localIdx := slices.IndexFunc(d.Local, func(e *ICECandidateExtended) bool {
return isCandidateEqualTo(e.Candidate, pair.Local)
})
if localIdx < 0 {
d.logger.Errorw("could not match local candidate", nil, "local", pair.Local)
// should not happen
return
}
d.Local[localIdx].SelectedOrder = d.selectedCount
}
func (d *ICEConnectionDetails) updateConnectionTypeLocked() {
highestSelectedOrder := -1
var selectedRemoteCandidate *ICECandidateExtended
for _, remote := range d.Remote {
if remote.SelectedOrder == 0 {
continue
}
if remote.SelectedOrder > highestSelectedOrder {
highestSelectedOrder = remote.SelectedOrder
selectedRemoteCandidate = remote
}
}
if selectedRemoteCandidate == nil {
return
}
remoteCandidate := selectedRemoteCandidate.Candidate
switch remoteCandidate.Protocol {
case webrtc.ICEProtocolUDP:
d.Type = ICEConnectionTypeUDP
case webrtc.ICEProtocolTCP:
d.Type = ICEConnectionTypeTCP
}
switch remoteCandidate.Typ {
case webrtc.ICECandidateTypeRelay:
d.Type = ICEConnectionTypeTURN
case webrtc.ICECandidateTypePrflx:
// if the remote relay candidate pings us *before* we get a relay candidate,
// Pion would have created a prflx candidate with the same address as the relay candidate.
// to report an accurate connection type, we'll compare to see if existing relay candidates match
for _, other := range d.Remote {
or := other.Candidate
if or.Typ == webrtc.ICECandidateTypeRelay &&
remoteCandidate.Address == or.Address &&
// NOTE: port is not compared as relayed address reported by TURN ALLOCATE from
// pion/turn server -> client and later sent from client -> server via ICE Trickle does not
// match port of `prflx` candidate learnt via TURN path. TODO-INVESTIGATE: how and why doesn't
// port match?
// remoteCandidate.Port == or.Port &&
remoteCandidate.Protocol == or.Protocol {
d.Type = ICEConnectionTypeTURN
break
}
}
}
}
// -------------------------------------------------------------
func isCandidateEqualTo(c1, c2 *webrtc.ICECandidate) bool {
if c1 == nil && c2 == nil {
return true
}
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
return false
}
return c1.Typ == c2.Typ &&
c1.Protocol == c2.Protocol &&
c1.Address == c2.Address &&
c1.Port == c2.Port &&
c1.Foundation == c2.Foundation &&
c1.Priority == c2.Priority &&
c1.RelatedAddress == c2.RelatedAddress &&
c1.RelatedPort == c2.RelatedPort &&
c1.TCPType == c2.TCPType
}
func unmarshalICECandidate(c webrtc.ICECandidateInit) (ice.Candidate, error) {
candidateValue := strings.TrimPrefix(c.Candidate, "candidate:")
if candidateValue == "" {
return nil, nil
}
candidate, err := ice.UnmarshalCandidate(candidateValue)
if err != nil {
return nil, err
}
return candidate, nil
}
func unmarshalCandidate(i ice.Candidate) (*webrtc.ICECandidate, error) {
typ, err := convertTypeFromICE(i.Type())
if err != nil {
return nil, err
}
protocol, err := webrtc.NewICEProtocol(i.NetworkType().NetworkShort())
if err != nil {
return nil, err
}
c := webrtc.ICECandidate{
Foundation: i.Foundation(),
Priority: i.Priority(),
Address: i.Address(),
Protocol: protocol,
Port: uint16(i.Port()),
Component: i.Component(),
Typ: typ,
TCPType: i.TCPType().String(),
}
if i.RelatedAddress() != nil {
c.RelatedAddress = i.RelatedAddress().Address
c.RelatedPort = uint16(i.RelatedAddress().Port)
}
return &c, nil
}
func convertTypeFromICE(t ice.CandidateType) (webrtc.ICECandidateType, error) {
switch t {
case ice.CandidateTypeHost:
return webrtc.ICECandidateTypeHost, nil
case ice.CandidateTypeServerReflexive:
return webrtc.ICECandidateTypeSrflx, nil
case ice.CandidateTypePeerReflexive:
return webrtc.ICECandidateTypePrflx, nil
case ice.CandidateTypeRelay:
return webrtc.ICECandidateTypeRelay, nil
default:
return webrtc.ICECandidateType(t), fmt.Errorf("unknown ice candidate type: %s", t)
}
}
func IsCandidateMDNS(candidate webrtc.ICECandidateInit) bool {
c, err := unmarshalICECandidate(candidate)
if err != nil {
return false
}
return IsICECandidateMDNS(c)
}
func IsICECandidateMDNS(candidate ice.Candidate) bool {
if candidate == nil {
// end-of-candidates candidate
return false
}
return strings.HasSuffix(candidate.Address(), ".local") || strings.HasSuffix(candidate.Address(), ".invalid")
}

1049
pkg/rtc/types/interfaces.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
// 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 types
type ProtocolVersion int
const CurrentProtocol = 17
func (v ProtocolVersion) SupportsPackedStreamId() bool {
return v > 0
}
func (v ProtocolVersion) SupportsProtobuf() bool {
return v > 0
}
func (v ProtocolVersion) HandlesDataPackets() bool {
return v > 1
}
// SubscriberAsPrimary indicates clients initiate subscriber connection as primary
func (v ProtocolVersion) SubscriberAsPrimary() bool {
return v > 2
}
// SupportsSpeakerChanged - if client handles speaker info deltas, instead of a comprehensive list
func (v ProtocolVersion) SupportsSpeakerChanged() bool {
return v > 2
}
// SupportsTransceiverReuse - if transceiver reuse is supported, optimizes SDP size
func (v ProtocolVersion) SupportsTransceiverReuse() bool {
return v > 3
}
// SupportsConnectionQuality - avoid sending frequent ConnectionQuality updates for lower protocol versions
func (v ProtocolVersion) SupportsConnectionQuality() bool {
return v > 4
}
func (v ProtocolVersion) SupportsSessionMigrate() bool {
return v > 5
}
func (v ProtocolVersion) SupportsICELite() bool {
return v > 5
}
func (v ProtocolVersion) SupportsUnpublish() bool {
return v > 6
}
// SupportFastStart - if client supports fast start, server side will send media streams
// in the first offer
func (v ProtocolVersion) SupportFastStart() bool {
return v > 7
}
func (v ProtocolVersion) SupportsDisconnectedUpdate() bool {
return v > 8
}
func (v ProtocolVersion) SupportsSyncStreamID() bool {
return v > 9
}
func (v ProtocolVersion) SupportsConnectionQualityLost() bool {
return v > 10
}
func (v ProtocolVersion) SupportsAsyncRoomID() bool {
return v > 11
}
func (v ProtocolVersion) SupportsIdentityBasedReconnection() bool {
return v > 11
}
func (v ProtocolVersion) SupportsRegionsInLeaveRequest() bool {
return v > 12
}
func (v ProtocolVersion) SupportsNonErrorSignalResponse() bool {
return v > 14
}
func (v ProtocolVersion) SupportsMoving() bool {
return v > 15
}
func (v ProtocolVersion) SupportsPacketTrailer() bool {
return v > 16
}

View File

@@ -0,0 +1,161 @@
// 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 types
import (
"time"
"github.com/livekit/protocol/livekit"
)
type TrafficStats struct {
StartTime time.Time
EndTime time.Time
Packets uint32
PacketsLost uint32
PacketsPadding uint32
PacketsOutOfOrder uint32
Bytes uint64
}
type TrafficTypeStats struct {
TrackType livekit.TrackType
StreamType livekit.StreamType
TrafficStats *TrafficStats
}
type TrafficLoad struct {
TrafficTypeStats []*TrafficTypeStats
}
func RTPStatsDiffToTrafficStats(before, after *livekit.RTPStats) *TrafficStats {
if after == nil {
return nil
}
startTime := after.StartTime
if before != nil {
startTime = before.EndTime
}
getAfter := func() *TrafficStats {
return &TrafficStats{
StartTime: startTime.AsTime(),
EndTime: after.EndTime.AsTime(),
Packets: after.Packets,
PacketsLost: after.PacketsLost,
PacketsPadding: after.PacketsPadding,
PacketsOutOfOrder: after.PacketsOutOfOrder,
Bytes: after.Bytes + after.BytesDuplicate + after.BytesPadding,
}
}
if before == nil {
return getAfter()
}
if (after.Packets - before.Packets) > (1 << 31) {
// after packets < before packets, probably got reset, just return after
return getAfter()
}
if ((after.Bytes + after.BytesDuplicate + after.BytesPadding) - (before.Bytes + before.BytesDuplicate + before.BytesPadding)) > (1 << 63) {
// after bytes < before bytes, probably got reset, just return after
return getAfter()
}
packetsLost := uint32(0)
if after.PacketsLost >= before.PacketsLost {
packetsLost = after.PacketsLost - before.PacketsLost
}
return &TrafficStats{
StartTime: startTime.AsTime(),
EndTime: after.EndTime.AsTime(),
Packets: after.Packets - before.Packets,
PacketsLost: packetsLost,
PacketsPadding: after.PacketsPadding - before.PacketsPadding,
PacketsOutOfOrder: after.PacketsOutOfOrder - before.PacketsOutOfOrder,
Bytes: (after.Bytes + after.BytesDuplicate + after.BytesPadding) - (before.Bytes + before.BytesDuplicate + before.BytesPadding),
}
}
func AggregateTrafficStats(statsList ...*TrafficStats) *TrafficStats {
if len(statsList) == 0 {
return nil
}
startTime := time.Time{}
endTime := time.Time{}
packets := uint32(0)
packetsLost := uint32(0)
packetsPadding := uint32(0)
packetsOutOfOrder := uint32(0)
bytes := uint64(0)
for _, stats := range statsList {
if startTime.IsZero() || startTime.After(stats.StartTime) {
startTime = stats.StartTime
}
if endTime.IsZero() || endTime.Before(stats.EndTime) {
endTime = stats.EndTime
}
packets += stats.Packets
packetsLost += stats.PacketsLost
packetsPadding += stats.PacketsPadding
packetsOutOfOrder += stats.PacketsOutOfOrder
bytes += stats.Bytes
}
if endTime.IsZero() {
endTime = time.Now()
}
return &TrafficStats{
StartTime: startTime,
EndTime: endTime,
Packets: packets,
PacketsLost: packetsLost,
PacketsPadding: packetsPadding,
PacketsOutOfOrder: packetsOutOfOrder,
Bytes: bytes,
}
}
func TrafficLoadToTrafficRate(trafficLoad *TrafficLoad) (
packetRateIn float64,
byteRateIn float64,
packetRateOut float64,
byteRateOut float64,
) {
if trafficLoad == nil {
return
}
for _, trafficTypeStat := range trafficLoad.TrafficTypeStats {
elapsed := trafficTypeStat.TrafficStats.EndTime.Sub(trafficTypeStat.TrafficStats.StartTime).Seconds()
packetRate := float64(trafficTypeStat.TrafficStats.Packets) / elapsed
byteRate := float64(trafficTypeStat.TrafficStats.Bytes) / elapsed
switch trafficTypeStat.StreamType {
case livekit.StreamType_UPSTREAM:
packetRateIn += packetRate
byteRateIn += byteRate
case livekit.StreamType_DOWNSTREAM:
packetRateOut += packetRate
byteRateOut += byteRate
}
}
return
}

View File

@@ -0,0 +1,266 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
)
type FakeDataDownTrack struct {
CloseStub func()
closeMutex sync.RWMutex
closeArgsForCall []struct {
}
HandleStub func() uint16
handleMutex sync.RWMutex
handleArgsForCall []struct {
}
handleReturns struct {
result1 uint16
}
handleReturnsOnCall map[int]struct {
result1 uint16
}
OnCloseStub func(func())
onCloseMutex sync.RWMutex
onCloseArgsForCall []struct {
arg1 func()
}
PublishDataTrackStub func() types.DataTrack
publishDataTrackMutex sync.RWMutex
publishDataTrackArgsForCall []struct {
}
publishDataTrackReturns struct {
result1 types.DataTrack
}
publishDataTrackReturnsOnCall map[int]struct {
result1 types.DataTrack
}
UpdateSubscriptionOptionsStub func(*livekit.DataTrackSubscriptionOptions)
updateSubscriptionOptionsMutex sync.RWMutex
updateSubscriptionOptionsArgsForCall []struct {
arg1 *livekit.DataTrackSubscriptionOptions
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeDataDownTrack) Close() {
fake.closeMutex.Lock()
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
}{})
stub := fake.CloseStub
fake.recordInvocation("Close", []interface{}{})
fake.closeMutex.Unlock()
if stub != nil {
fake.CloseStub()
}
}
func (fake *FakeDataDownTrack) CloseCallCount() int {
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
return len(fake.closeArgsForCall)
}
func (fake *FakeDataDownTrack) CloseCalls(stub func()) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = stub
}
func (fake *FakeDataDownTrack) Handle() uint16 {
fake.handleMutex.Lock()
ret, specificReturn := fake.handleReturnsOnCall[len(fake.handleArgsForCall)]
fake.handleArgsForCall = append(fake.handleArgsForCall, struct {
}{})
stub := fake.HandleStub
fakeReturns := fake.handleReturns
fake.recordInvocation("Handle", []interface{}{})
fake.handleMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataDownTrack) HandleCallCount() int {
fake.handleMutex.RLock()
defer fake.handleMutex.RUnlock()
return len(fake.handleArgsForCall)
}
func (fake *FakeDataDownTrack) HandleCalls(stub func() uint16) {
fake.handleMutex.Lock()
defer fake.handleMutex.Unlock()
fake.HandleStub = stub
}
func (fake *FakeDataDownTrack) HandleReturns(result1 uint16) {
fake.handleMutex.Lock()
defer fake.handleMutex.Unlock()
fake.HandleStub = nil
fake.handleReturns = struct {
result1 uint16
}{result1}
}
func (fake *FakeDataDownTrack) HandleReturnsOnCall(i int, result1 uint16) {
fake.handleMutex.Lock()
defer fake.handleMutex.Unlock()
fake.HandleStub = nil
if fake.handleReturnsOnCall == nil {
fake.handleReturnsOnCall = make(map[int]struct {
result1 uint16
})
}
fake.handleReturnsOnCall[i] = struct {
result1 uint16
}{result1}
}
func (fake *FakeDataDownTrack) OnClose(arg1 func()) {
fake.onCloseMutex.Lock()
fake.onCloseArgsForCall = append(fake.onCloseArgsForCall, struct {
arg1 func()
}{arg1})
stub := fake.OnCloseStub
fake.recordInvocation("OnClose", []interface{}{arg1})
fake.onCloseMutex.Unlock()
if stub != nil {
fake.OnCloseStub(arg1)
}
}
func (fake *FakeDataDownTrack) OnCloseCallCount() int {
fake.onCloseMutex.RLock()
defer fake.onCloseMutex.RUnlock()
return len(fake.onCloseArgsForCall)
}
func (fake *FakeDataDownTrack) OnCloseCalls(stub func(func())) {
fake.onCloseMutex.Lock()
defer fake.onCloseMutex.Unlock()
fake.OnCloseStub = stub
}
func (fake *FakeDataDownTrack) OnCloseArgsForCall(i int) func() {
fake.onCloseMutex.RLock()
defer fake.onCloseMutex.RUnlock()
argsForCall := fake.onCloseArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataDownTrack) PublishDataTrack() types.DataTrack {
fake.publishDataTrackMutex.Lock()
ret, specificReturn := fake.publishDataTrackReturnsOnCall[len(fake.publishDataTrackArgsForCall)]
fake.publishDataTrackArgsForCall = append(fake.publishDataTrackArgsForCall, struct {
}{})
stub := fake.PublishDataTrackStub
fakeReturns := fake.publishDataTrackReturns
fake.recordInvocation("PublishDataTrack", []interface{}{})
fake.publishDataTrackMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataDownTrack) PublishDataTrackCallCount() int {
fake.publishDataTrackMutex.RLock()
defer fake.publishDataTrackMutex.RUnlock()
return len(fake.publishDataTrackArgsForCall)
}
func (fake *FakeDataDownTrack) PublishDataTrackCalls(stub func() types.DataTrack) {
fake.publishDataTrackMutex.Lock()
defer fake.publishDataTrackMutex.Unlock()
fake.PublishDataTrackStub = stub
}
func (fake *FakeDataDownTrack) PublishDataTrackReturns(result1 types.DataTrack) {
fake.publishDataTrackMutex.Lock()
defer fake.publishDataTrackMutex.Unlock()
fake.PublishDataTrackStub = nil
fake.publishDataTrackReturns = struct {
result1 types.DataTrack
}{result1}
}
func (fake *FakeDataDownTrack) PublishDataTrackReturnsOnCall(i int, result1 types.DataTrack) {
fake.publishDataTrackMutex.Lock()
defer fake.publishDataTrackMutex.Unlock()
fake.PublishDataTrackStub = nil
if fake.publishDataTrackReturnsOnCall == nil {
fake.publishDataTrackReturnsOnCall = make(map[int]struct {
result1 types.DataTrack
})
}
fake.publishDataTrackReturnsOnCall[i] = struct {
result1 types.DataTrack
}{result1}
}
func (fake *FakeDataDownTrack) UpdateSubscriptionOptions(arg1 *livekit.DataTrackSubscriptionOptions) {
fake.updateSubscriptionOptionsMutex.Lock()
fake.updateSubscriptionOptionsArgsForCall = append(fake.updateSubscriptionOptionsArgsForCall, struct {
arg1 *livekit.DataTrackSubscriptionOptions
}{arg1})
stub := fake.UpdateSubscriptionOptionsStub
fake.recordInvocation("UpdateSubscriptionOptions", []interface{}{arg1})
fake.updateSubscriptionOptionsMutex.Unlock()
if stub != nil {
fake.UpdateSubscriptionOptionsStub(arg1)
}
}
func (fake *FakeDataDownTrack) UpdateSubscriptionOptionsCallCount() int {
fake.updateSubscriptionOptionsMutex.RLock()
defer fake.updateSubscriptionOptionsMutex.RUnlock()
return len(fake.updateSubscriptionOptionsArgsForCall)
}
func (fake *FakeDataDownTrack) UpdateSubscriptionOptionsCalls(stub func(*livekit.DataTrackSubscriptionOptions)) {
fake.updateSubscriptionOptionsMutex.Lock()
defer fake.updateSubscriptionOptionsMutex.Unlock()
fake.UpdateSubscriptionOptionsStub = stub
}
func (fake *FakeDataDownTrack) UpdateSubscriptionOptionsArgsForCall(i int) *livekit.DataTrackSubscriptionOptions {
fake.updateSubscriptionOptionsMutex.RLock()
defer fake.updateSubscriptionOptionsMutex.RUnlock()
argsForCall := fake.updateSubscriptionOptionsArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataDownTrack) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeDataDownTrack) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.DataDownTrack = new(FakeDataDownTrack)

View File

@@ -0,0 +1,863 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
)
type FakeDataTrack struct {
AddDataDownTrackStub func(types.DataTrackSender) error
addDataDownTrackMutex sync.RWMutex
addDataDownTrackArgsForCall []struct {
arg1 types.DataTrackSender
}
addDataDownTrackReturns struct {
result1 error
}
addDataDownTrackReturnsOnCall map[int]struct {
result1 error
}
AddSubscriberStub func(types.LocalParticipant) (types.DataDownTrack, error)
addSubscriberMutex sync.RWMutex
addSubscriberArgsForCall []struct {
arg1 types.LocalParticipant
}
addSubscriberReturns struct {
result1 types.DataDownTrack
result2 error
}
addSubscriberReturnsOnCall map[int]struct {
result1 types.DataDownTrack
result2 error
}
CloseStub func()
closeMutex sync.RWMutex
closeArgsForCall []struct {
}
DeleteDataDownTrackStub func(livekit.ParticipantID)
deleteDataDownTrackMutex sync.RWMutex
deleteDataDownTrackArgsForCall []struct {
arg1 livekit.ParticipantID
}
HandlePacketStub func([]byte, *datatrack.Packet, int64)
handlePacketMutex sync.RWMutex
handlePacketArgsForCall []struct {
arg1 []byte
arg2 *datatrack.Packet
arg3 int64
}
IDStub func() livekit.TrackID
iDMutex sync.RWMutex
iDArgsForCall []struct {
}
iDReturns struct {
result1 livekit.TrackID
}
iDReturnsOnCall map[int]struct {
result1 livekit.TrackID
}
IsSubscriberStub func(livekit.ParticipantID) bool
isSubscriberMutex sync.RWMutex
isSubscriberArgsForCall []struct {
arg1 livekit.ParticipantID
}
isSubscriberReturns struct {
result1 bool
}
isSubscriberReturnsOnCall map[int]struct {
result1 bool
}
NameStub func() string
nameMutex sync.RWMutex
nameArgsForCall []struct {
}
nameReturns struct {
result1 string
}
nameReturnsOnCall map[int]struct {
result1 string
}
PubHandleStub func() uint16
pubHandleMutex sync.RWMutex
pubHandleArgsForCall []struct {
}
pubHandleReturns struct {
result1 uint16
}
pubHandleReturnsOnCall map[int]struct {
result1 uint16
}
PublisherIDStub func() livekit.ParticipantID
publisherIDMutex sync.RWMutex
publisherIDArgsForCall []struct {
}
publisherIDReturns struct {
result1 livekit.ParticipantID
}
publisherIDReturnsOnCall map[int]struct {
result1 livekit.ParticipantID
}
PublisherIdentityStub func() livekit.ParticipantIdentity
publisherIdentityMutex sync.RWMutex
publisherIdentityArgsForCall []struct {
}
publisherIdentityReturns struct {
result1 livekit.ParticipantIdentity
}
publisherIdentityReturnsOnCall map[int]struct {
result1 livekit.ParticipantIdentity
}
RemoveSubscriberStub func(livekit.ParticipantID)
removeSubscriberMutex sync.RWMutex
removeSubscriberArgsForCall []struct {
arg1 livekit.ParticipantID
}
RevokeDisallowedSubscribersStub func([]livekit.ParticipantIdentity) []livekit.ParticipantIdentity
revokeDisallowedSubscribersMutex sync.RWMutex
revokeDisallowedSubscribersArgsForCall []struct {
arg1 []livekit.ParticipantIdentity
}
revokeDisallowedSubscribersReturns struct {
result1 []livekit.ParticipantIdentity
}
revokeDisallowedSubscribersReturnsOnCall map[int]struct {
result1 []livekit.ParticipantIdentity
}
ToProtoStub func() *livekit.DataTrackInfo
toProtoMutex sync.RWMutex
toProtoArgsForCall []struct {
}
toProtoReturns struct {
result1 *livekit.DataTrackInfo
}
toProtoReturnsOnCall map[int]struct {
result1 *livekit.DataTrackInfo
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeDataTrack) AddDataDownTrack(arg1 types.DataTrackSender) error {
fake.addDataDownTrackMutex.Lock()
ret, specificReturn := fake.addDataDownTrackReturnsOnCall[len(fake.addDataDownTrackArgsForCall)]
fake.addDataDownTrackArgsForCall = append(fake.addDataDownTrackArgsForCall, struct {
arg1 types.DataTrackSender
}{arg1})
stub := fake.AddDataDownTrackStub
fakeReturns := fake.addDataDownTrackReturns
fake.recordInvocation("AddDataDownTrack", []interface{}{arg1})
fake.addDataDownTrackMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) AddDataDownTrackCallCount() int {
fake.addDataDownTrackMutex.RLock()
defer fake.addDataDownTrackMutex.RUnlock()
return len(fake.addDataDownTrackArgsForCall)
}
func (fake *FakeDataTrack) AddDataDownTrackCalls(stub func(types.DataTrackSender) error) {
fake.addDataDownTrackMutex.Lock()
defer fake.addDataDownTrackMutex.Unlock()
fake.AddDataDownTrackStub = stub
}
func (fake *FakeDataTrack) AddDataDownTrackArgsForCall(i int) types.DataTrackSender {
fake.addDataDownTrackMutex.RLock()
defer fake.addDataDownTrackMutex.RUnlock()
argsForCall := fake.addDataDownTrackArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrack) AddDataDownTrackReturns(result1 error) {
fake.addDataDownTrackMutex.Lock()
defer fake.addDataDownTrackMutex.Unlock()
fake.AddDataDownTrackStub = nil
fake.addDataDownTrackReturns = struct {
result1 error
}{result1}
}
func (fake *FakeDataTrack) AddDataDownTrackReturnsOnCall(i int, result1 error) {
fake.addDataDownTrackMutex.Lock()
defer fake.addDataDownTrackMutex.Unlock()
fake.AddDataDownTrackStub = nil
if fake.addDataDownTrackReturnsOnCall == nil {
fake.addDataDownTrackReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.addDataDownTrackReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeDataTrack) AddSubscriber(arg1 types.LocalParticipant) (types.DataDownTrack, error) {
fake.addSubscriberMutex.Lock()
ret, specificReturn := fake.addSubscriberReturnsOnCall[len(fake.addSubscriberArgsForCall)]
fake.addSubscriberArgsForCall = append(fake.addSubscriberArgsForCall, struct {
arg1 types.LocalParticipant
}{arg1})
stub := fake.AddSubscriberStub
fakeReturns := fake.addSubscriberReturns
fake.recordInvocation("AddSubscriber", []interface{}{arg1})
fake.addSubscriberMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeDataTrack) AddSubscriberCallCount() int {
fake.addSubscriberMutex.RLock()
defer fake.addSubscriberMutex.RUnlock()
return len(fake.addSubscriberArgsForCall)
}
func (fake *FakeDataTrack) AddSubscriberCalls(stub func(types.LocalParticipant) (types.DataDownTrack, error)) {
fake.addSubscriberMutex.Lock()
defer fake.addSubscriberMutex.Unlock()
fake.AddSubscriberStub = stub
}
func (fake *FakeDataTrack) AddSubscriberArgsForCall(i int) types.LocalParticipant {
fake.addSubscriberMutex.RLock()
defer fake.addSubscriberMutex.RUnlock()
argsForCall := fake.addSubscriberArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrack) AddSubscriberReturns(result1 types.DataDownTrack, result2 error) {
fake.addSubscriberMutex.Lock()
defer fake.addSubscriberMutex.Unlock()
fake.AddSubscriberStub = nil
fake.addSubscriberReturns = struct {
result1 types.DataDownTrack
result2 error
}{result1, result2}
}
func (fake *FakeDataTrack) AddSubscriberReturnsOnCall(i int, result1 types.DataDownTrack, result2 error) {
fake.addSubscriberMutex.Lock()
defer fake.addSubscriberMutex.Unlock()
fake.AddSubscriberStub = nil
if fake.addSubscriberReturnsOnCall == nil {
fake.addSubscriberReturnsOnCall = make(map[int]struct {
result1 types.DataDownTrack
result2 error
})
}
fake.addSubscriberReturnsOnCall[i] = struct {
result1 types.DataDownTrack
result2 error
}{result1, result2}
}
func (fake *FakeDataTrack) Close() {
fake.closeMutex.Lock()
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
}{})
stub := fake.CloseStub
fake.recordInvocation("Close", []interface{}{})
fake.closeMutex.Unlock()
if stub != nil {
fake.CloseStub()
}
}
func (fake *FakeDataTrack) CloseCallCount() int {
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
return len(fake.closeArgsForCall)
}
func (fake *FakeDataTrack) CloseCalls(stub func()) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = stub
}
func (fake *FakeDataTrack) DeleteDataDownTrack(arg1 livekit.ParticipantID) {
fake.deleteDataDownTrackMutex.Lock()
fake.deleteDataDownTrackArgsForCall = append(fake.deleteDataDownTrackArgsForCall, struct {
arg1 livekit.ParticipantID
}{arg1})
stub := fake.DeleteDataDownTrackStub
fake.recordInvocation("DeleteDataDownTrack", []interface{}{arg1})
fake.deleteDataDownTrackMutex.Unlock()
if stub != nil {
fake.DeleteDataDownTrackStub(arg1)
}
}
func (fake *FakeDataTrack) DeleteDataDownTrackCallCount() int {
fake.deleteDataDownTrackMutex.RLock()
defer fake.deleteDataDownTrackMutex.RUnlock()
return len(fake.deleteDataDownTrackArgsForCall)
}
func (fake *FakeDataTrack) DeleteDataDownTrackCalls(stub func(livekit.ParticipantID)) {
fake.deleteDataDownTrackMutex.Lock()
defer fake.deleteDataDownTrackMutex.Unlock()
fake.DeleteDataDownTrackStub = stub
}
func (fake *FakeDataTrack) DeleteDataDownTrackArgsForCall(i int) livekit.ParticipantID {
fake.deleteDataDownTrackMutex.RLock()
defer fake.deleteDataDownTrackMutex.RUnlock()
argsForCall := fake.deleteDataDownTrackArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrack) HandlePacket(arg1 []byte, arg2 *datatrack.Packet, arg3 int64) {
var arg1Copy []byte
if arg1 != nil {
arg1Copy = make([]byte, len(arg1))
copy(arg1Copy, arg1)
}
fake.handlePacketMutex.Lock()
fake.handlePacketArgsForCall = append(fake.handlePacketArgsForCall, struct {
arg1 []byte
arg2 *datatrack.Packet
arg3 int64
}{arg1Copy, arg2, arg3})
stub := fake.HandlePacketStub
fake.recordInvocation("HandlePacket", []interface{}{arg1Copy, arg2, arg3})
fake.handlePacketMutex.Unlock()
if stub != nil {
fake.HandlePacketStub(arg1, arg2, arg3)
}
}
func (fake *FakeDataTrack) HandlePacketCallCount() int {
fake.handlePacketMutex.RLock()
defer fake.handlePacketMutex.RUnlock()
return len(fake.handlePacketArgsForCall)
}
func (fake *FakeDataTrack) HandlePacketCalls(stub func([]byte, *datatrack.Packet, int64)) {
fake.handlePacketMutex.Lock()
defer fake.handlePacketMutex.Unlock()
fake.HandlePacketStub = stub
}
func (fake *FakeDataTrack) HandlePacketArgsForCall(i int) ([]byte, *datatrack.Packet, int64) {
fake.handlePacketMutex.RLock()
defer fake.handlePacketMutex.RUnlock()
argsForCall := fake.handlePacketArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeDataTrack) ID() livekit.TrackID {
fake.iDMutex.Lock()
ret, specificReturn := fake.iDReturnsOnCall[len(fake.iDArgsForCall)]
fake.iDArgsForCall = append(fake.iDArgsForCall, struct {
}{})
stub := fake.IDStub
fakeReturns := fake.iDReturns
fake.recordInvocation("ID", []interface{}{})
fake.iDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) IDCallCount() int {
fake.iDMutex.RLock()
defer fake.iDMutex.RUnlock()
return len(fake.iDArgsForCall)
}
func (fake *FakeDataTrack) IDCalls(stub func() livekit.TrackID) {
fake.iDMutex.Lock()
defer fake.iDMutex.Unlock()
fake.IDStub = stub
}
func (fake *FakeDataTrack) IDReturns(result1 livekit.TrackID) {
fake.iDMutex.Lock()
defer fake.iDMutex.Unlock()
fake.IDStub = nil
fake.iDReturns = struct {
result1 livekit.TrackID
}{result1}
}
func (fake *FakeDataTrack) IDReturnsOnCall(i int, result1 livekit.TrackID) {
fake.iDMutex.Lock()
defer fake.iDMutex.Unlock()
fake.IDStub = nil
if fake.iDReturnsOnCall == nil {
fake.iDReturnsOnCall = make(map[int]struct {
result1 livekit.TrackID
})
}
fake.iDReturnsOnCall[i] = struct {
result1 livekit.TrackID
}{result1}
}
func (fake *FakeDataTrack) IsSubscriber(arg1 livekit.ParticipantID) bool {
fake.isSubscriberMutex.Lock()
ret, specificReturn := fake.isSubscriberReturnsOnCall[len(fake.isSubscriberArgsForCall)]
fake.isSubscriberArgsForCall = append(fake.isSubscriberArgsForCall, struct {
arg1 livekit.ParticipantID
}{arg1})
stub := fake.IsSubscriberStub
fakeReturns := fake.isSubscriberReturns
fake.recordInvocation("IsSubscriber", []interface{}{arg1})
fake.isSubscriberMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) IsSubscriberCallCount() int {
fake.isSubscriberMutex.RLock()
defer fake.isSubscriberMutex.RUnlock()
return len(fake.isSubscriberArgsForCall)
}
func (fake *FakeDataTrack) IsSubscriberCalls(stub func(livekit.ParticipantID) bool) {
fake.isSubscriberMutex.Lock()
defer fake.isSubscriberMutex.Unlock()
fake.IsSubscriberStub = stub
}
func (fake *FakeDataTrack) IsSubscriberArgsForCall(i int) livekit.ParticipantID {
fake.isSubscriberMutex.RLock()
defer fake.isSubscriberMutex.RUnlock()
argsForCall := fake.isSubscriberArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrack) IsSubscriberReturns(result1 bool) {
fake.isSubscriberMutex.Lock()
defer fake.isSubscriberMutex.Unlock()
fake.IsSubscriberStub = nil
fake.isSubscriberReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeDataTrack) IsSubscriberReturnsOnCall(i int, result1 bool) {
fake.isSubscriberMutex.Lock()
defer fake.isSubscriberMutex.Unlock()
fake.IsSubscriberStub = nil
if fake.isSubscriberReturnsOnCall == nil {
fake.isSubscriberReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.isSubscriberReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeDataTrack) Name() string {
fake.nameMutex.Lock()
ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)]
fake.nameArgsForCall = append(fake.nameArgsForCall, struct {
}{})
stub := fake.NameStub
fakeReturns := fake.nameReturns
fake.recordInvocation("Name", []interface{}{})
fake.nameMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) NameCallCount() int {
fake.nameMutex.RLock()
defer fake.nameMutex.RUnlock()
return len(fake.nameArgsForCall)
}
func (fake *FakeDataTrack) NameCalls(stub func() string) {
fake.nameMutex.Lock()
defer fake.nameMutex.Unlock()
fake.NameStub = stub
}
func (fake *FakeDataTrack) NameReturns(result1 string) {
fake.nameMutex.Lock()
defer fake.nameMutex.Unlock()
fake.NameStub = nil
fake.nameReturns = struct {
result1 string
}{result1}
}
func (fake *FakeDataTrack) NameReturnsOnCall(i int, result1 string) {
fake.nameMutex.Lock()
defer fake.nameMutex.Unlock()
fake.NameStub = nil
if fake.nameReturnsOnCall == nil {
fake.nameReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.nameReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *FakeDataTrack) PubHandle() uint16 {
fake.pubHandleMutex.Lock()
ret, specificReturn := fake.pubHandleReturnsOnCall[len(fake.pubHandleArgsForCall)]
fake.pubHandleArgsForCall = append(fake.pubHandleArgsForCall, struct {
}{})
stub := fake.PubHandleStub
fakeReturns := fake.pubHandleReturns
fake.recordInvocation("PubHandle", []interface{}{})
fake.pubHandleMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) PubHandleCallCount() int {
fake.pubHandleMutex.RLock()
defer fake.pubHandleMutex.RUnlock()
return len(fake.pubHandleArgsForCall)
}
func (fake *FakeDataTrack) PubHandleCalls(stub func() uint16) {
fake.pubHandleMutex.Lock()
defer fake.pubHandleMutex.Unlock()
fake.PubHandleStub = stub
}
func (fake *FakeDataTrack) PubHandleReturns(result1 uint16) {
fake.pubHandleMutex.Lock()
defer fake.pubHandleMutex.Unlock()
fake.PubHandleStub = nil
fake.pubHandleReturns = struct {
result1 uint16
}{result1}
}
func (fake *FakeDataTrack) PubHandleReturnsOnCall(i int, result1 uint16) {
fake.pubHandleMutex.Lock()
defer fake.pubHandleMutex.Unlock()
fake.PubHandleStub = nil
if fake.pubHandleReturnsOnCall == nil {
fake.pubHandleReturnsOnCall = make(map[int]struct {
result1 uint16
})
}
fake.pubHandleReturnsOnCall[i] = struct {
result1 uint16
}{result1}
}
func (fake *FakeDataTrack) PublisherID() livekit.ParticipantID {
fake.publisherIDMutex.Lock()
ret, specificReturn := fake.publisherIDReturnsOnCall[len(fake.publisherIDArgsForCall)]
fake.publisherIDArgsForCall = append(fake.publisherIDArgsForCall, struct {
}{})
stub := fake.PublisherIDStub
fakeReturns := fake.publisherIDReturns
fake.recordInvocation("PublisherID", []interface{}{})
fake.publisherIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) PublisherIDCallCount() int {
fake.publisherIDMutex.RLock()
defer fake.publisherIDMutex.RUnlock()
return len(fake.publisherIDArgsForCall)
}
func (fake *FakeDataTrack) PublisherIDCalls(stub func() livekit.ParticipantID) {
fake.publisherIDMutex.Lock()
defer fake.publisherIDMutex.Unlock()
fake.PublisherIDStub = stub
}
func (fake *FakeDataTrack) PublisherIDReturns(result1 livekit.ParticipantID) {
fake.publisherIDMutex.Lock()
defer fake.publisherIDMutex.Unlock()
fake.PublisherIDStub = nil
fake.publisherIDReturns = struct {
result1 livekit.ParticipantID
}{result1}
}
func (fake *FakeDataTrack) PublisherIDReturnsOnCall(i int, result1 livekit.ParticipantID) {
fake.publisherIDMutex.Lock()
defer fake.publisherIDMutex.Unlock()
fake.PublisherIDStub = nil
if fake.publisherIDReturnsOnCall == nil {
fake.publisherIDReturnsOnCall = make(map[int]struct {
result1 livekit.ParticipantID
})
}
fake.publisherIDReturnsOnCall[i] = struct {
result1 livekit.ParticipantID
}{result1}
}
func (fake *FakeDataTrack) PublisherIdentity() livekit.ParticipantIdentity {
fake.publisherIdentityMutex.Lock()
ret, specificReturn := fake.publisherIdentityReturnsOnCall[len(fake.publisherIdentityArgsForCall)]
fake.publisherIdentityArgsForCall = append(fake.publisherIdentityArgsForCall, struct {
}{})
stub := fake.PublisherIdentityStub
fakeReturns := fake.publisherIdentityReturns
fake.recordInvocation("PublisherIdentity", []interface{}{})
fake.publisherIdentityMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) PublisherIdentityCallCount() int {
fake.publisherIdentityMutex.RLock()
defer fake.publisherIdentityMutex.RUnlock()
return len(fake.publisherIdentityArgsForCall)
}
func (fake *FakeDataTrack) PublisherIdentityCalls(stub func() livekit.ParticipantIdentity) {
fake.publisherIdentityMutex.Lock()
defer fake.publisherIdentityMutex.Unlock()
fake.PublisherIdentityStub = stub
}
func (fake *FakeDataTrack) PublisherIdentityReturns(result1 livekit.ParticipantIdentity) {
fake.publisherIdentityMutex.Lock()
defer fake.publisherIdentityMutex.Unlock()
fake.PublisherIdentityStub = nil
fake.publisherIdentityReturns = struct {
result1 livekit.ParticipantIdentity
}{result1}
}
func (fake *FakeDataTrack) PublisherIdentityReturnsOnCall(i int, result1 livekit.ParticipantIdentity) {
fake.publisherIdentityMutex.Lock()
defer fake.publisherIdentityMutex.Unlock()
fake.PublisherIdentityStub = nil
if fake.publisherIdentityReturnsOnCall == nil {
fake.publisherIdentityReturnsOnCall = make(map[int]struct {
result1 livekit.ParticipantIdentity
})
}
fake.publisherIdentityReturnsOnCall[i] = struct {
result1 livekit.ParticipantIdentity
}{result1}
}
func (fake *FakeDataTrack) RemoveSubscriber(arg1 livekit.ParticipantID) {
fake.removeSubscriberMutex.Lock()
fake.removeSubscriberArgsForCall = append(fake.removeSubscriberArgsForCall, struct {
arg1 livekit.ParticipantID
}{arg1})
stub := fake.RemoveSubscriberStub
fake.recordInvocation("RemoveSubscriber", []interface{}{arg1})
fake.removeSubscriberMutex.Unlock()
if stub != nil {
fake.RemoveSubscriberStub(arg1)
}
}
func (fake *FakeDataTrack) RemoveSubscriberCallCount() int {
fake.removeSubscriberMutex.RLock()
defer fake.removeSubscriberMutex.RUnlock()
return len(fake.removeSubscriberArgsForCall)
}
func (fake *FakeDataTrack) RemoveSubscriberCalls(stub func(livekit.ParticipantID)) {
fake.removeSubscriberMutex.Lock()
defer fake.removeSubscriberMutex.Unlock()
fake.RemoveSubscriberStub = stub
}
func (fake *FakeDataTrack) RemoveSubscriberArgsForCall(i int) livekit.ParticipantID {
fake.removeSubscriberMutex.RLock()
defer fake.removeSubscriberMutex.RUnlock()
argsForCall := fake.removeSubscriberArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrack) RevokeDisallowedSubscribers(arg1 []livekit.ParticipantIdentity) []livekit.ParticipantIdentity {
var arg1Copy []livekit.ParticipantIdentity
if arg1 != nil {
arg1Copy = make([]livekit.ParticipantIdentity, len(arg1))
copy(arg1Copy, arg1)
}
fake.revokeDisallowedSubscribersMutex.Lock()
ret, specificReturn := fake.revokeDisallowedSubscribersReturnsOnCall[len(fake.revokeDisallowedSubscribersArgsForCall)]
fake.revokeDisallowedSubscribersArgsForCall = append(fake.revokeDisallowedSubscribersArgsForCall, struct {
arg1 []livekit.ParticipantIdentity
}{arg1Copy})
stub := fake.RevokeDisallowedSubscribersStub
fakeReturns := fake.revokeDisallowedSubscribersReturns
fake.recordInvocation("RevokeDisallowedSubscribers", []interface{}{arg1Copy})
fake.revokeDisallowedSubscribersMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) RevokeDisallowedSubscribersCallCount() int {
fake.revokeDisallowedSubscribersMutex.RLock()
defer fake.revokeDisallowedSubscribersMutex.RUnlock()
return len(fake.revokeDisallowedSubscribersArgsForCall)
}
func (fake *FakeDataTrack) RevokeDisallowedSubscribersCalls(stub func([]livekit.ParticipantIdentity) []livekit.ParticipantIdentity) {
fake.revokeDisallowedSubscribersMutex.Lock()
defer fake.revokeDisallowedSubscribersMutex.Unlock()
fake.RevokeDisallowedSubscribersStub = stub
}
func (fake *FakeDataTrack) RevokeDisallowedSubscribersArgsForCall(i int) []livekit.ParticipantIdentity {
fake.revokeDisallowedSubscribersMutex.RLock()
defer fake.revokeDisallowedSubscribersMutex.RUnlock()
argsForCall := fake.revokeDisallowedSubscribersArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrack) RevokeDisallowedSubscribersReturns(result1 []livekit.ParticipantIdentity) {
fake.revokeDisallowedSubscribersMutex.Lock()
defer fake.revokeDisallowedSubscribersMutex.Unlock()
fake.RevokeDisallowedSubscribersStub = nil
fake.revokeDisallowedSubscribersReturns = struct {
result1 []livekit.ParticipantIdentity
}{result1}
}
func (fake *FakeDataTrack) RevokeDisallowedSubscribersReturnsOnCall(i int, result1 []livekit.ParticipantIdentity) {
fake.revokeDisallowedSubscribersMutex.Lock()
defer fake.revokeDisallowedSubscribersMutex.Unlock()
fake.RevokeDisallowedSubscribersStub = nil
if fake.revokeDisallowedSubscribersReturnsOnCall == nil {
fake.revokeDisallowedSubscribersReturnsOnCall = make(map[int]struct {
result1 []livekit.ParticipantIdentity
})
}
fake.revokeDisallowedSubscribersReturnsOnCall[i] = struct {
result1 []livekit.ParticipantIdentity
}{result1}
}
func (fake *FakeDataTrack) ToProto() *livekit.DataTrackInfo {
fake.toProtoMutex.Lock()
ret, specificReturn := fake.toProtoReturnsOnCall[len(fake.toProtoArgsForCall)]
fake.toProtoArgsForCall = append(fake.toProtoArgsForCall, struct {
}{})
stub := fake.ToProtoStub
fakeReturns := fake.toProtoReturns
fake.recordInvocation("ToProto", []interface{}{})
fake.toProtoMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrack) ToProtoCallCount() int {
fake.toProtoMutex.RLock()
defer fake.toProtoMutex.RUnlock()
return len(fake.toProtoArgsForCall)
}
func (fake *FakeDataTrack) ToProtoCalls(stub func() *livekit.DataTrackInfo) {
fake.toProtoMutex.Lock()
defer fake.toProtoMutex.Unlock()
fake.ToProtoStub = stub
}
func (fake *FakeDataTrack) ToProtoReturns(result1 *livekit.DataTrackInfo) {
fake.toProtoMutex.Lock()
defer fake.toProtoMutex.Unlock()
fake.ToProtoStub = nil
fake.toProtoReturns = struct {
result1 *livekit.DataTrackInfo
}{result1}
}
func (fake *FakeDataTrack) ToProtoReturnsOnCall(i int, result1 *livekit.DataTrackInfo) {
fake.toProtoMutex.Lock()
defer fake.toProtoMutex.Unlock()
fake.ToProtoStub = nil
if fake.toProtoReturnsOnCall == nil {
fake.toProtoReturnsOnCall = make(map[int]struct {
result1 *livekit.DataTrackInfo
})
}
fake.toProtoReturnsOnCall[i] = struct {
result1 *livekit.DataTrackInfo
}{result1}
}
func (fake *FakeDataTrack) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeDataTrack) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.DataTrack = new(FakeDataTrack)

View File

@@ -0,0 +1,148 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
)
type FakeDataTrackSender struct {
SubscriberIDStub func() livekit.ParticipantID
subscriberIDMutex sync.RWMutex
subscriberIDArgsForCall []struct {
}
subscriberIDReturns struct {
result1 livekit.ParticipantID
}
subscriberIDReturnsOnCall map[int]struct {
result1 livekit.ParticipantID
}
WritePacketStub func([]byte, *datatrack.Packet, int64)
writePacketMutex sync.RWMutex
writePacketArgsForCall []struct {
arg1 []byte
arg2 *datatrack.Packet
arg3 int64
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeDataTrackSender) SubscriberID() livekit.ParticipantID {
fake.subscriberIDMutex.Lock()
ret, specificReturn := fake.subscriberIDReturnsOnCall[len(fake.subscriberIDArgsForCall)]
fake.subscriberIDArgsForCall = append(fake.subscriberIDArgsForCall, struct {
}{})
stub := fake.SubscriberIDStub
fakeReturns := fake.subscriberIDReturns
fake.recordInvocation("SubscriberID", []interface{}{})
fake.subscriberIDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrackSender) SubscriberIDCallCount() int {
fake.subscriberIDMutex.RLock()
defer fake.subscriberIDMutex.RUnlock()
return len(fake.subscriberIDArgsForCall)
}
func (fake *FakeDataTrackSender) SubscriberIDCalls(stub func() livekit.ParticipantID) {
fake.subscriberIDMutex.Lock()
defer fake.subscriberIDMutex.Unlock()
fake.SubscriberIDStub = stub
}
func (fake *FakeDataTrackSender) SubscriberIDReturns(result1 livekit.ParticipantID) {
fake.subscriberIDMutex.Lock()
defer fake.subscriberIDMutex.Unlock()
fake.SubscriberIDStub = nil
fake.subscriberIDReturns = struct {
result1 livekit.ParticipantID
}{result1}
}
func (fake *FakeDataTrackSender) SubscriberIDReturnsOnCall(i int, result1 livekit.ParticipantID) {
fake.subscriberIDMutex.Lock()
defer fake.subscriberIDMutex.Unlock()
fake.SubscriberIDStub = nil
if fake.subscriberIDReturnsOnCall == nil {
fake.subscriberIDReturnsOnCall = make(map[int]struct {
result1 livekit.ParticipantID
})
}
fake.subscriberIDReturnsOnCall[i] = struct {
result1 livekit.ParticipantID
}{result1}
}
func (fake *FakeDataTrackSender) WritePacket(arg1 []byte, arg2 *datatrack.Packet, arg3 int64) {
var arg1Copy []byte
if arg1 != nil {
arg1Copy = make([]byte, len(arg1))
copy(arg1Copy, arg1)
}
fake.writePacketMutex.Lock()
fake.writePacketArgsForCall = append(fake.writePacketArgsForCall, struct {
arg1 []byte
arg2 *datatrack.Packet
arg3 int64
}{arg1Copy, arg2, arg3})
stub := fake.WritePacketStub
fake.recordInvocation("WritePacket", []interface{}{arg1Copy, arg2, arg3})
fake.writePacketMutex.Unlock()
if stub != nil {
fake.WritePacketStub(arg1, arg2, arg3)
}
}
func (fake *FakeDataTrackSender) WritePacketCallCount() int {
fake.writePacketMutex.RLock()
defer fake.writePacketMutex.RUnlock()
return len(fake.writePacketArgsForCall)
}
func (fake *FakeDataTrackSender) WritePacketCalls(stub func([]byte, *datatrack.Packet, int64)) {
fake.writePacketMutex.Lock()
defer fake.writePacketMutex.Unlock()
fake.WritePacketStub = stub
}
func (fake *FakeDataTrackSender) WritePacketArgsForCall(i int) ([]byte, *datatrack.Packet, int64) {
fake.writePacketMutex.RLock()
defer fake.writePacketMutex.RUnlock()
argsForCall := fake.writePacketArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeDataTrackSender) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeDataTrackSender) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.DataTrackSender = new(FakeDataTrackSender)

View File

@@ -0,0 +1,114 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
type FakeDataTrackTransport struct {
SendDataTrackMessageStub func([]byte) error
sendDataTrackMessageMutex sync.RWMutex
sendDataTrackMessageArgsForCall []struct {
arg1 []byte
}
sendDataTrackMessageReturns struct {
result1 error
}
sendDataTrackMessageReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeDataTrackTransport) SendDataTrackMessage(arg1 []byte) error {
var arg1Copy []byte
if arg1 != nil {
arg1Copy = make([]byte, len(arg1))
copy(arg1Copy, arg1)
}
fake.sendDataTrackMessageMutex.Lock()
ret, specificReturn := fake.sendDataTrackMessageReturnsOnCall[len(fake.sendDataTrackMessageArgsForCall)]
fake.sendDataTrackMessageArgsForCall = append(fake.sendDataTrackMessageArgsForCall, struct {
arg1 []byte
}{arg1Copy})
stub := fake.SendDataTrackMessageStub
fakeReturns := fake.sendDataTrackMessageReturns
fake.recordInvocation("SendDataTrackMessage", []interface{}{arg1Copy})
fake.sendDataTrackMessageMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeDataTrackTransport) SendDataTrackMessageCallCount() int {
fake.sendDataTrackMessageMutex.RLock()
defer fake.sendDataTrackMessageMutex.RUnlock()
return len(fake.sendDataTrackMessageArgsForCall)
}
func (fake *FakeDataTrackTransport) SendDataTrackMessageCalls(stub func([]byte) error) {
fake.sendDataTrackMessageMutex.Lock()
defer fake.sendDataTrackMessageMutex.Unlock()
fake.SendDataTrackMessageStub = stub
}
func (fake *FakeDataTrackTransport) SendDataTrackMessageArgsForCall(i int) []byte {
fake.sendDataTrackMessageMutex.RLock()
defer fake.sendDataTrackMessageMutex.RUnlock()
argsForCall := fake.sendDataTrackMessageArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeDataTrackTransport) SendDataTrackMessageReturns(result1 error) {
fake.sendDataTrackMessageMutex.Lock()
defer fake.sendDataTrackMessageMutex.Unlock()
fake.SendDataTrackMessageStub = nil
fake.sendDataTrackMessageReturns = struct {
result1 error
}{result1}
}
func (fake *FakeDataTrackTransport) SendDataTrackMessageReturnsOnCall(i int, result1 error) {
fake.sendDataTrackMessageMutex.Lock()
defer fake.sendDataTrackMessageMutex.Unlock()
fake.SendDataTrackMessageStub = nil
if fake.sendDataTrackMessageReturnsOnCall == nil {
fake.sendDataTrackMessageReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.sendDataTrackMessageReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeDataTrackTransport) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeDataTrackTransport) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.DataTrackTransport = new(FakeDataTrackTransport)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,542 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
)
type FakeLocalParticipantHelper struct {
GetCachedReliableDataMessageStub func(map[livekit.ParticipantID]uint32) []*types.DataMessageCache
getCachedReliableDataMessageMutex sync.RWMutex
getCachedReliableDataMessageArgsForCall []struct {
arg1 map[livekit.ParticipantID]uint32
}
getCachedReliableDataMessageReturns struct {
result1 []*types.DataMessageCache
}
getCachedReliableDataMessageReturnsOnCall map[int]struct {
result1 []*types.DataMessageCache
}
GetParticipantInfoStub func(livekit.ParticipantID) *livekit.ParticipantInfo
getParticipantInfoMutex sync.RWMutex
getParticipantInfoArgsForCall []struct {
arg1 livekit.ParticipantID
}
getParticipantInfoReturns struct {
result1 *livekit.ParticipantInfo
}
getParticipantInfoReturnsOnCall map[int]struct {
result1 *livekit.ParticipantInfo
}
GetRegionSettingsStub func(string) *livekit.RegionSettings
getRegionSettingsMutex sync.RWMutex
getRegionSettingsArgsForCall []struct {
arg1 string
}
getRegionSettingsReturns struct {
result1 *livekit.RegionSettings
}
getRegionSettingsReturnsOnCall map[int]struct {
result1 *livekit.RegionSettings
}
GetSubscriberForwarderStateStub func(types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error)
getSubscriberForwarderStateMutex sync.RWMutex
getSubscriberForwarderStateArgsForCall []struct {
arg1 types.LocalParticipant
}
getSubscriberForwarderStateReturns struct {
result1 map[livekit.TrackID]*livekit.RTPForwarderState
result2 error
}
getSubscriberForwarderStateReturnsOnCall map[int]struct {
result1 map[livekit.TrackID]*livekit.RTPForwarderState
result2 error
}
ResolveDataTrackStub func(types.LocalParticipant, livekit.TrackID) types.DataResolverResult
resolveDataTrackMutex sync.RWMutex
resolveDataTrackArgsForCall []struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}
resolveDataTrackReturns struct {
result1 types.DataResolverResult
}
resolveDataTrackReturnsOnCall map[int]struct {
result1 types.DataResolverResult
}
ResolveMediaTrackStub func(types.LocalParticipant, livekit.TrackID) types.MediaResolverResult
resolveMediaTrackMutex sync.RWMutex
resolveMediaTrackArgsForCall []struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}
resolveMediaTrackReturns struct {
result1 types.MediaResolverResult
}
resolveMediaTrackReturnsOnCall map[int]struct {
result1 types.MediaResolverResult
}
ShouldRegressCodecStub func() bool
shouldRegressCodecMutex sync.RWMutex
shouldRegressCodecArgsForCall []struct {
}
shouldRegressCodecReturns struct {
result1 bool
}
shouldRegressCodecReturnsOnCall map[int]struct {
result1 bool
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessage(arg1 map[livekit.ParticipantID]uint32) []*types.DataMessageCache {
fake.getCachedReliableDataMessageMutex.Lock()
ret, specificReturn := fake.getCachedReliableDataMessageReturnsOnCall[len(fake.getCachedReliableDataMessageArgsForCall)]
fake.getCachedReliableDataMessageArgsForCall = append(fake.getCachedReliableDataMessageArgsForCall, struct {
arg1 map[livekit.ParticipantID]uint32
}{arg1})
stub := fake.GetCachedReliableDataMessageStub
fakeReturns := fake.getCachedReliableDataMessageReturns
fake.recordInvocation("GetCachedReliableDataMessage", []interface{}{arg1})
fake.getCachedReliableDataMessageMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageCallCount() int {
fake.getCachedReliableDataMessageMutex.RLock()
defer fake.getCachedReliableDataMessageMutex.RUnlock()
return len(fake.getCachedReliableDataMessageArgsForCall)
}
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageCalls(stub func(map[livekit.ParticipantID]uint32) []*types.DataMessageCache) {
fake.getCachedReliableDataMessageMutex.Lock()
defer fake.getCachedReliableDataMessageMutex.Unlock()
fake.GetCachedReliableDataMessageStub = stub
}
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageArgsForCall(i int) map[livekit.ParticipantID]uint32 {
fake.getCachedReliableDataMessageMutex.RLock()
defer fake.getCachedReliableDataMessageMutex.RUnlock()
argsForCall := fake.getCachedReliableDataMessageArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageReturns(result1 []*types.DataMessageCache) {
fake.getCachedReliableDataMessageMutex.Lock()
defer fake.getCachedReliableDataMessageMutex.Unlock()
fake.GetCachedReliableDataMessageStub = nil
fake.getCachedReliableDataMessageReturns = struct {
result1 []*types.DataMessageCache
}{result1}
}
func (fake *FakeLocalParticipantHelper) GetCachedReliableDataMessageReturnsOnCall(i int, result1 []*types.DataMessageCache) {
fake.getCachedReliableDataMessageMutex.Lock()
defer fake.getCachedReliableDataMessageMutex.Unlock()
fake.GetCachedReliableDataMessageStub = nil
if fake.getCachedReliableDataMessageReturnsOnCall == nil {
fake.getCachedReliableDataMessageReturnsOnCall = make(map[int]struct {
result1 []*types.DataMessageCache
})
}
fake.getCachedReliableDataMessageReturnsOnCall[i] = struct {
result1 []*types.DataMessageCache
}{result1}
}
func (fake *FakeLocalParticipantHelper) GetParticipantInfo(arg1 livekit.ParticipantID) *livekit.ParticipantInfo {
fake.getParticipantInfoMutex.Lock()
ret, specificReturn := fake.getParticipantInfoReturnsOnCall[len(fake.getParticipantInfoArgsForCall)]
fake.getParticipantInfoArgsForCall = append(fake.getParticipantInfoArgsForCall, struct {
arg1 livekit.ParticipantID
}{arg1})
stub := fake.GetParticipantInfoStub
fakeReturns := fake.getParticipantInfoReturns
fake.recordInvocation("GetParticipantInfo", []interface{}{arg1})
fake.getParticipantInfoMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalParticipantHelper) GetParticipantInfoCallCount() int {
fake.getParticipantInfoMutex.RLock()
defer fake.getParticipantInfoMutex.RUnlock()
return len(fake.getParticipantInfoArgsForCall)
}
func (fake *FakeLocalParticipantHelper) GetParticipantInfoCalls(stub func(livekit.ParticipantID) *livekit.ParticipantInfo) {
fake.getParticipantInfoMutex.Lock()
defer fake.getParticipantInfoMutex.Unlock()
fake.GetParticipantInfoStub = stub
}
func (fake *FakeLocalParticipantHelper) GetParticipantInfoArgsForCall(i int) livekit.ParticipantID {
fake.getParticipantInfoMutex.RLock()
defer fake.getParticipantInfoMutex.RUnlock()
argsForCall := fake.getParticipantInfoArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeLocalParticipantHelper) GetParticipantInfoReturns(result1 *livekit.ParticipantInfo) {
fake.getParticipantInfoMutex.Lock()
defer fake.getParticipantInfoMutex.Unlock()
fake.GetParticipantInfoStub = nil
fake.getParticipantInfoReturns = struct {
result1 *livekit.ParticipantInfo
}{result1}
}
func (fake *FakeLocalParticipantHelper) GetParticipantInfoReturnsOnCall(i int, result1 *livekit.ParticipantInfo) {
fake.getParticipantInfoMutex.Lock()
defer fake.getParticipantInfoMutex.Unlock()
fake.GetParticipantInfoStub = nil
if fake.getParticipantInfoReturnsOnCall == nil {
fake.getParticipantInfoReturnsOnCall = make(map[int]struct {
result1 *livekit.ParticipantInfo
})
}
fake.getParticipantInfoReturnsOnCall[i] = struct {
result1 *livekit.ParticipantInfo
}{result1}
}
func (fake *FakeLocalParticipantHelper) GetRegionSettings(arg1 string) *livekit.RegionSettings {
fake.getRegionSettingsMutex.Lock()
ret, specificReturn := fake.getRegionSettingsReturnsOnCall[len(fake.getRegionSettingsArgsForCall)]
fake.getRegionSettingsArgsForCall = append(fake.getRegionSettingsArgsForCall, struct {
arg1 string
}{arg1})
stub := fake.GetRegionSettingsStub
fakeReturns := fake.getRegionSettingsReturns
fake.recordInvocation("GetRegionSettings", []interface{}{arg1})
fake.getRegionSettingsMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalParticipantHelper) GetRegionSettingsCallCount() int {
fake.getRegionSettingsMutex.RLock()
defer fake.getRegionSettingsMutex.RUnlock()
return len(fake.getRegionSettingsArgsForCall)
}
func (fake *FakeLocalParticipantHelper) GetRegionSettingsCalls(stub func(string) *livekit.RegionSettings) {
fake.getRegionSettingsMutex.Lock()
defer fake.getRegionSettingsMutex.Unlock()
fake.GetRegionSettingsStub = stub
}
func (fake *FakeLocalParticipantHelper) GetRegionSettingsArgsForCall(i int) string {
fake.getRegionSettingsMutex.RLock()
defer fake.getRegionSettingsMutex.RUnlock()
argsForCall := fake.getRegionSettingsArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeLocalParticipantHelper) GetRegionSettingsReturns(result1 *livekit.RegionSettings) {
fake.getRegionSettingsMutex.Lock()
defer fake.getRegionSettingsMutex.Unlock()
fake.GetRegionSettingsStub = nil
fake.getRegionSettingsReturns = struct {
result1 *livekit.RegionSettings
}{result1}
}
func (fake *FakeLocalParticipantHelper) GetRegionSettingsReturnsOnCall(i int, result1 *livekit.RegionSettings) {
fake.getRegionSettingsMutex.Lock()
defer fake.getRegionSettingsMutex.Unlock()
fake.GetRegionSettingsStub = nil
if fake.getRegionSettingsReturnsOnCall == nil {
fake.getRegionSettingsReturnsOnCall = make(map[int]struct {
result1 *livekit.RegionSettings
})
}
fake.getRegionSettingsReturnsOnCall[i] = struct {
result1 *livekit.RegionSettings
}{result1}
}
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderState(arg1 types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error) {
fake.getSubscriberForwarderStateMutex.Lock()
ret, specificReturn := fake.getSubscriberForwarderStateReturnsOnCall[len(fake.getSubscriberForwarderStateArgsForCall)]
fake.getSubscriberForwarderStateArgsForCall = append(fake.getSubscriberForwarderStateArgsForCall, struct {
arg1 types.LocalParticipant
}{arg1})
stub := fake.GetSubscriberForwarderStateStub
fakeReturns := fake.getSubscriberForwarderStateReturns
fake.recordInvocation("GetSubscriberForwarderState", []interface{}{arg1})
fake.getSubscriberForwarderStateMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateCallCount() int {
fake.getSubscriberForwarderStateMutex.RLock()
defer fake.getSubscriberForwarderStateMutex.RUnlock()
return len(fake.getSubscriberForwarderStateArgsForCall)
}
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateCalls(stub func(types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error)) {
fake.getSubscriberForwarderStateMutex.Lock()
defer fake.getSubscriberForwarderStateMutex.Unlock()
fake.GetSubscriberForwarderStateStub = stub
}
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateArgsForCall(i int) types.LocalParticipant {
fake.getSubscriberForwarderStateMutex.RLock()
defer fake.getSubscriberForwarderStateMutex.RUnlock()
argsForCall := fake.getSubscriberForwarderStateArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateReturns(result1 map[livekit.TrackID]*livekit.RTPForwarderState, result2 error) {
fake.getSubscriberForwarderStateMutex.Lock()
defer fake.getSubscriberForwarderStateMutex.Unlock()
fake.GetSubscriberForwarderStateStub = nil
fake.getSubscriberForwarderStateReturns = struct {
result1 map[livekit.TrackID]*livekit.RTPForwarderState
result2 error
}{result1, result2}
}
func (fake *FakeLocalParticipantHelper) GetSubscriberForwarderStateReturnsOnCall(i int, result1 map[livekit.TrackID]*livekit.RTPForwarderState, result2 error) {
fake.getSubscriberForwarderStateMutex.Lock()
defer fake.getSubscriberForwarderStateMutex.Unlock()
fake.GetSubscriberForwarderStateStub = nil
if fake.getSubscriberForwarderStateReturnsOnCall == nil {
fake.getSubscriberForwarderStateReturnsOnCall = make(map[int]struct {
result1 map[livekit.TrackID]*livekit.RTPForwarderState
result2 error
})
}
fake.getSubscriberForwarderStateReturnsOnCall[i] = struct {
result1 map[livekit.TrackID]*livekit.RTPForwarderState
result2 error
}{result1, result2}
}
func (fake *FakeLocalParticipantHelper) ResolveDataTrack(arg1 types.LocalParticipant, arg2 livekit.TrackID) types.DataResolverResult {
fake.resolveDataTrackMutex.Lock()
ret, specificReturn := fake.resolveDataTrackReturnsOnCall[len(fake.resolveDataTrackArgsForCall)]
fake.resolveDataTrackArgsForCall = append(fake.resolveDataTrackArgsForCall, struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}{arg1, arg2})
stub := fake.ResolveDataTrackStub
fakeReturns := fake.resolveDataTrackReturns
fake.recordInvocation("ResolveDataTrack", []interface{}{arg1, arg2})
fake.resolveDataTrackMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalParticipantHelper) ResolveDataTrackCallCount() int {
fake.resolveDataTrackMutex.RLock()
defer fake.resolveDataTrackMutex.RUnlock()
return len(fake.resolveDataTrackArgsForCall)
}
func (fake *FakeLocalParticipantHelper) ResolveDataTrackCalls(stub func(types.LocalParticipant, livekit.TrackID) types.DataResolverResult) {
fake.resolveDataTrackMutex.Lock()
defer fake.resolveDataTrackMutex.Unlock()
fake.ResolveDataTrackStub = stub
}
func (fake *FakeLocalParticipantHelper) ResolveDataTrackArgsForCall(i int) (types.LocalParticipant, livekit.TrackID) {
fake.resolveDataTrackMutex.RLock()
defer fake.resolveDataTrackMutex.RUnlock()
argsForCall := fake.resolveDataTrackArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeLocalParticipantHelper) ResolveDataTrackReturns(result1 types.DataResolverResult) {
fake.resolveDataTrackMutex.Lock()
defer fake.resolveDataTrackMutex.Unlock()
fake.ResolveDataTrackStub = nil
fake.resolveDataTrackReturns = struct {
result1 types.DataResolverResult
}{result1}
}
func (fake *FakeLocalParticipantHelper) ResolveDataTrackReturnsOnCall(i int, result1 types.DataResolverResult) {
fake.resolveDataTrackMutex.Lock()
defer fake.resolveDataTrackMutex.Unlock()
fake.ResolveDataTrackStub = nil
if fake.resolveDataTrackReturnsOnCall == nil {
fake.resolveDataTrackReturnsOnCall = make(map[int]struct {
result1 types.DataResolverResult
})
}
fake.resolveDataTrackReturnsOnCall[i] = struct {
result1 types.DataResolverResult
}{result1}
}
func (fake *FakeLocalParticipantHelper) ResolveMediaTrack(arg1 types.LocalParticipant, arg2 livekit.TrackID) types.MediaResolverResult {
fake.resolveMediaTrackMutex.Lock()
ret, specificReturn := fake.resolveMediaTrackReturnsOnCall[len(fake.resolveMediaTrackArgsForCall)]
fake.resolveMediaTrackArgsForCall = append(fake.resolveMediaTrackArgsForCall, struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}{arg1, arg2})
stub := fake.ResolveMediaTrackStub
fakeReturns := fake.resolveMediaTrackReturns
fake.recordInvocation("ResolveMediaTrack", []interface{}{arg1, arg2})
fake.resolveMediaTrackMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackCallCount() int {
fake.resolveMediaTrackMutex.RLock()
defer fake.resolveMediaTrackMutex.RUnlock()
return len(fake.resolveMediaTrackArgsForCall)
}
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackCalls(stub func(types.LocalParticipant, livekit.TrackID) types.MediaResolverResult) {
fake.resolveMediaTrackMutex.Lock()
defer fake.resolveMediaTrackMutex.Unlock()
fake.ResolveMediaTrackStub = stub
}
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackArgsForCall(i int) (types.LocalParticipant, livekit.TrackID) {
fake.resolveMediaTrackMutex.RLock()
defer fake.resolveMediaTrackMutex.RUnlock()
argsForCall := fake.resolveMediaTrackArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackReturns(result1 types.MediaResolverResult) {
fake.resolveMediaTrackMutex.Lock()
defer fake.resolveMediaTrackMutex.Unlock()
fake.ResolveMediaTrackStub = nil
fake.resolveMediaTrackReturns = struct {
result1 types.MediaResolverResult
}{result1}
}
func (fake *FakeLocalParticipantHelper) ResolveMediaTrackReturnsOnCall(i int, result1 types.MediaResolverResult) {
fake.resolveMediaTrackMutex.Lock()
defer fake.resolveMediaTrackMutex.Unlock()
fake.ResolveMediaTrackStub = nil
if fake.resolveMediaTrackReturnsOnCall == nil {
fake.resolveMediaTrackReturnsOnCall = make(map[int]struct {
result1 types.MediaResolverResult
})
}
fake.resolveMediaTrackReturnsOnCall[i] = struct {
result1 types.MediaResolverResult
}{result1}
}
func (fake *FakeLocalParticipantHelper) ShouldRegressCodec() bool {
fake.shouldRegressCodecMutex.Lock()
ret, specificReturn := fake.shouldRegressCodecReturnsOnCall[len(fake.shouldRegressCodecArgsForCall)]
fake.shouldRegressCodecArgsForCall = append(fake.shouldRegressCodecArgsForCall, struct {
}{})
stub := fake.ShouldRegressCodecStub
fakeReturns := fake.shouldRegressCodecReturns
fake.recordInvocation("ShouldRegressCodec", []interface{}{})
fake.shouldRegressCodecMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecCallCount() int {
fake.shouldRegressCodecMutex.RLock()
defer fake.shouldRegressCodecMutex.RUnlock()
return len(fake.shouldRegressCodecArgsForCall)
}
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecCalls(stub func() bool) {
fake.shouldRegressCodecMutex.Lock()
defer fake.shouldRegressCodecMutex.Unlock()
fake.ShouldRegressCodecStub = stub
}
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecReturns(result1 bool) {
fake.shouldRegressCodecMutex.Lock()
defer fake.shouldRegressCodecMutex.Unlock()
fake.ShouldRegressCodecStub = nil
fake.shouldRegressCodecReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalParticipantHelper) ShouldRegressCodecReturnsOnCall(i int, result1 bool) {
fake.shouldRegressCodecMutex.Lock()
defer fake.shouldRegressCodecMutex.Unlock()
fake.ShouldRegressCodecStub = nil
if fake.shouldRegressCodecReturnsOnCall == nil {
fake.shouldRegressCodecReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.shouldRegressCodecReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalParticipantHelper) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeLocalParticipantHelper) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.LocalParticipantHelper = new(FakeLocalParticipantHelper)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,356 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
)
type FakeParticipantListener struct {
OnDataTrackMessageStub func(types.Participant, []byte, *datatrack.Packet)
onDataTrackMessageMutex sync.RWMutex
onDataTrackMessageArgsForCall []struct {
arg1 types.Participant
arg2 []byte
arg3 *datatrack.Packet
}
OnDataTrackPublishedStub func(types.Participant, types.DataTrack)
onDataTrackPublishedMutex sync.RWMutex
onDataTrackPublishedArgsForCall []struct {
arg1 types.Participant
arg2 types.DataTrack
}
OnDataTrackUnpublishedStub func(types.Participant, types.DataTrack)
onDataTrackUnpublishedMutex sync.RWMutex
onDataTrackUnpublishedArgsForCall []struct {
arg1 types.Participant
arg2 types.DataTrack
}
OnMetricsStub func(types.Participant, *livekit.DataPacket)
onMetricsMutex sync.RWMutex
onMetricsArgsForCall []struct {
arg1 types.Participant
arg2 *livekit.DataPacket
}
OnParticipantUpdateStub func(types.Participant)
onParticipantUpdateMutex sync.RWMutex
onParticipantUpdateArgsForCall []struct {
arg1 types.Participant
}
OnTrackPublishedStub func(types.Participant, types.MediaTrack)
onTrackPublishedMutex sync.RWMutex
onTrackPublishedArgsForCall []struct {
arg1 types.Participant
arg2 types.MediaTrack
}
OnTrackUnpublishedStub func(types.Participant, types.MediaTrack)
onTrackUnpublishedMutex sync.RWMutex
onTrackUnpublishedArgsForCall []struct {
arg1 types.Participant
arg2 types.MediaTrack
}
OnTrackUpdatedStub func(types.Participant, types.MediaTrack)
onTrackUpdatedMutex sync.RWMutex
onTrackUpdatedArgsForCall []struct {
arg1 types.Participant
arg2 types.MediaTrack
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeParticipantListener) OnDataTrackMessage(arg1 types.Participant, arg2 []byte, arg3 *datatrack.Packet) {
var arg2Copy []byte
if arg2 != nil {
arg2Copy = make([]byte, len(arg2))
copy(arg2Copy, arg2)
}
fake.onDataTrackMessageMutex.Lock()
fake.onDataTrackMessageArgsForCall = append(fake.onDataTrackMessageArgsForCall, struct {
arg1 types.Participant
arg2 []byte
arg3 *datatrack.Packet
}{arg1, arg2Copy, arg3})
stub := fake.OnDataTrackMessageStub
fake.recordInvocation("OnDataTrackMessage", []interface{}{arg1, arg2Copy, arg3})
fake.onDataTrackMessageMutex.Unlock()
if stub != nil {
fake.OnDataTrackMessageStub(arg1, arg2, arg3)
}
}
func (fake *FakeParticipantListener) OnDataTrackMessageCallCount() int {
fake.onDataTrackMessageMutex.RLock()
defer fake.onDataTrackMessageMutex.RUnlock()
return len(fake.onDataTrackMessageArgsForCall)
}
func (fake *FakeParticipantListener) OnDataTrackMessageCalls(stub func(types.Participant, []byte, *datatrack.Packet)) {
fake.onDataTrackMessageMutex.Lock()
defer fake.onDataTrackMessageMutex.Unlock()
fake.OnDataTrackMessageStub = stub
}
func (fake *FakeParticipantListener) OnDataTrackMessageArgsForCall(i int) (types.Participant, []byte, *datatrack.Packet) {
fake.onDataTrackMessageMutex.RLock()
defer fake.onDataTrackMessageMutex.RUnlock()
argsForCall := fake.onDataTrackMessageArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeParticipantListener) OnDataTrackPublished(arg1 types.Participant, arg2 types.DataTrack) {
fake.onDataTrackPublishedMutex.Lock()
fake.onDataTrackPublishedArgsForCall = append(fake.onDataTrackPublishedArgsForCall, struct {
arg1 types.Participant
arg2 types.DataTrack
}{arg1, arg2})
stub := fake.OnDataTrackPublishedStub
fake.recordInvocation("OnDataTrackPublished", []interface{}{arg1, arg2})
fake.onDataTrackPublishedMutex.Unlock()
if stub != nil {
fake.OnDataTrackPublishedStub(arg1, arg2)
}
}
func (fake *FakeParticipantListener) OnDataTrackPublishedCallCount() int {
fake.onDataTrackPublishedMutex.RLock()
defer fake.onDataTrackPublishedMutex.RUnlock()
return len(fake.onDataTrackPublishedArgsForCall)
}
func (fake *FakeParticipantListener) OnDataTrackPublishedCalls(stub func(types.Participant, types.DataTrack)) {
fake.onDataTrackPublishedMutex.Lock()
defer fake.onDataTrackPublishedMutex.Unlock()
fake.OnDataTrackPublishedStub = stub
}
func (fake *FakeParticipantListener) OnDataTrackPublishedArgsForCall(i int) (types.Participant, types.DataTrack) {
fake.onDataTrackPublishedMutex.RLock()
defer fake.onDataTrackPublishedMutex.RUnlock()
argsForCall := fake.onDataTrackPublishedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantListener) OnDataTrackUnpublished(arg1 types.Participant, arg2 types.DataTrack) {
fake.onDataTrackUnpublishedMutex.Lock()
fake.onDataTrackUnpublishedArgsForCall = append(fake.onDataTrackUnpublishedArgsForCall, struct {
arg1 types.Participant
arg2 types.DataTrack
}{arg1, arg2})
stub := fake.OnDataTrackUnpublishedStub
fake.recordInvocation("OnDataTrackUnpublished", []interface{}{arg1, arg2})
fake.onDataTrackUnpublishedMutex.Unlock()
if stub != nil {
fake.OnDataTrackUnpublishedStub(arg1, arg2)
}
}
func (fake *FakeParticipantListener) OnDataTrackUnpublishedCallCount() int {
fake.onDataTrackUnpublishedMutex.RLock()
defer fake.onDataTrackUnpublishedMutex.RUnlock()
return len(fake.onDataTrackUnpublishedArgsForCall)
}
func (fake *FakeParticipantListener) OnDataTrackUnpublishedCalls(stub func(types.Participant, types.DataTrack)) {
fake.onDataTrackUnpublishedMutex.Lock()
defer fake.onDataTrackUnpublishedMutex.Unlock()
fake.OnDataTrackUnpublishedStub = stub
}
func (fake *FakeParticipantListener) OnDataTrackUnpublishedArgsForCall(i int) (types.Participant, types.DataTrack) {
fake.onDataTrackUnpublishedMutex.RLock()
defer fake.onDataTrackUnpublishedMutex.RUnlock()
argsForCall := fake.onDataTrackUnpublishedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantListener) OnMetrics(arg1 types.Participant, arg2 *livekit.DataPacket) {
fake.onMetricsMutex.Lock()
fake.onMetricsArgsForCall = append(fake.onMetricsArgsForCall, struct {
arg1 types.Participant
arg2 *livekit.DataPacket
}{arg1, arg2})
stub := fake.OnMetricsStub
fake.recordInvocation("OnMetrics", []interface{}{arg1, arg2})
fake.onMetricsMutex.Unlock()
if stub != nil {
fake.OnMetricsStub(arg1, arg2)
}
}
func (fake *FakeParticipantListener) OnMetricsCallCount() int {
fake.onMetricsMutex.RLock()
defer fake.onMetricsMutex.RUnlock()
return len(fake.onMetricsArgsForCall)
}
func (fake *FakeParticipantListener) OnMetricsCalls(stub func(types.Participant, *livekit.DataPacket)) {
fake.onMetricsMutex.Lock()
defer fake.onMetricsMutex.Unlock()
fake.OnMetricsStub = stub
}
func (fake *FakeParticipantListener) OnMetricsArgsForCall(i int) (types.Participant, *livekit.DataPacket) {
fake.onMetricsMutex.RLock()
defer fake.onMetricsMutex.RUnlock()
argsForCall := fake.onMetricsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantListener) OnParticipantUpdate(arg1 types.Participant) {
fake.onParticipantUpdateMutex.Lock()
fake.onParticipantUpdateArgsForCall = append(fake.onParticipantUpdateArgsForCall, struct {
arg1 types.Participant
}{arg1})
stub := fake.OnParticipantUpdateStub
fake.recordInvocation("OnParticipantUpdate", []interface{}{arg1})
fake.onParticipantUpdateMutex.Unlock()
if stub != nil {
fake.OnParticipantUpdateStub(arg1)
}
}
func (fake *FakeParticipantListener) OnParticipantUpdateCallCount() int {
fake.onParticipantUpdateMutex.RLock()
defer fake.onParticipantUpdateMutex.RUnlock()
return len(fake.onParticipantUpdateArgsForCall)
}
func (fake *FakeParticipantListener) OnParticipantUpdateCalls(stub func(types.Participant)) {
fake.onParticipantUpdateMutex.Lock()
defer fake.onParticipantUpdateMutex.Unlock()
fake.OnParticipantUpdateStub = stub
}
func (fake *FakeParticipantListener) OnParticipantUpdateArgsForCall(i int) types.Participant {
fake.onParticipantUpdateMutex.RLock()
defer fake.onParticipantUpdateMutex.RUnlock()
argsForCall := fake.onParticipantUpdateArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeParticipantListener) OnTrackPublished(arg1 types.Participant, arg2 types.MediaTrack) {
fake.onTrackPublishedMutex.Lock()
fake.onTrackPublishedArgsForCall = append(fake.onTrackPublishedArgsForCall, struct {
arg1 types.Participant
arg2 types.MediaTrack
}{arg1, arg2})
stub := fake.OnTrackPublishedStub
fake.recordInvocation("OnTrackPublished", []interface{}{arg1, arg2})
fake.onTrackPublishedMutex.Unlock()
if stub != nil {
fake.OnTrackPublishedStub(arg1, arg2)
}
}
func (fake *FakeParticipantListener) OnTrackPublishedCallCount() int {
fake.onTrackPublishedMutex.RLock()
defer fake.onTrackPublishedMutex.RUnlock()
return len(fake.onTrackPublishedArgsForCall)
}
func (fake *FakeParticipantListener) OnTrackPublishedCalls(stub func(types.Participant, types.MediaTrack)) {
fake.onTrackPublishedMutex.Lock()
defer fake.onTrackPublishedMutex.Unlock()
fake.OnTrackPublishedStub = stub
}
func (fake *FakeParticipantListener) OnTrackPublishedArgsForCall(i int) (types.Participant, types.MediaTrack) {
fake.onTrackPublishedMutex.RLock()
defer fake.onTrackPublishedMutex.RUnlock()
argsForCall := fake.onTrackPublishedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantListener) OnTrackUnpublished(arg1 types.Participant, arg2 types.MediaTrack) {
fake.onTrackUnpublishedMutex.Lock()
fake.onTrackUnpublishedArgsForCall = append(fake.onTrackUnpublishedArgsForCall, struct {
arg1 types.Participant
arg2 types.MediaTrack
}{arg1, arg2})
stub := fake.OnTrackUnpublishedStub
fake.recordInvocation("OnTrackUnpublished", []interface{}{arg1, arg2})
fake.onTrackUnpublishedMutex.Unlock()
if stub != nil {
fake.OnTrackUnpublishedStub(arg1, arg2)
}
}
func (fake *FakeParticipantListener) OnTrackUnpublishedCallCount() int {
fake.onTrackUnpublishedMutex.RLock()
defer fake.onTrackUnpublishedMutex.RUnlock()
return len(fake.onTrackUnpublishedArgsForCall)
}
func (fake *FakeParticipantListener) OnTrackUnpublishedCalls(stub func(types.Participant, types.MediaTrack)) {
fake.onTrackUnpublishedMutex.Lock()
defer fake.onTrackUnpublishedMutex.Unlock()
fake.OnTrackUnpublishedStub = stub
}
func (fake *FakeParticipantListener) OnTrackUnpublishedArgsForCall(i int) (types.Participant, types.MediaTrack) {
fake.onTrackUnpublishedMutex.RLock()
defer fake.onTrackUnpublishedMutex.RUnlock()
argsForCall := fake.onTrackUnpublishedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantListener) OnTrackUpdated(arg1 types.Participant, arg2 types.MediaTrack) {
fake.onTrackUpdatedMutex.Lock()
fake.onTrackUpdatedArgsForCall = append(fake.onTrackUpdatedArgsForCall, struct {
arg1 types.Participant
arg2 types.MediaTrack
}{arg1, arg2})
stub := fake.OnTrackUpdatedStub
fake.recordInvocation("OnTrackUpdated", []interface{}{arg1, arg2})
fake.onTrackUpdatedMutex.Unlock()
if stub != nil {
fake.OnTrackUpdatedStub(arg1, arg2)
}
}
func (fake *FakeParticipantListener) OnTrackUpdatedCallCount() int {
fake.onTrackUpdatedMutex.RLock()
defer fake.onTrackUpdatedMutex.RUnlock()
return len(fake.onTrackUpdatedArgsForCall)
}
func (fake *FakeParticipantListener) OnTrackUpdatedCalls(stub func(types.Participant, types.MediaTrack)) {
fake.onTrackUpdatedMutex.Lock()
defer fake.onTrackUpdatedMutex.Unlock()
fake.OnTrackUpdatedStub = stub
}
func (fake *FakeParticipantListener) OnTrackUpdatedArgsForCall(i int) (types.Participant, types.MediaTrack) {
fake.onTrackUpdatedMutex.RLock()
defer fake.onTrackUpdatedMutex.RUnlock()
argsForCall := fake.onTrackUpdatedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantListener) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeParticipantListener) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.ParticipantListener = new(FakeParticipantListener)

View File

@@ -0,0 +1,666 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
type FakeParticipantTelemetryListener struct {
OnTrackMaxSubscribedVideoQualityStub func(livekit.ParticipantID, *livekit.TrackInfo, mime.MimeType, livekit.VideoQuality)
onTrackMaxSubscribedVideoQualityMutex sync.RWMutex
onTrackMaxSubscribedVideoQualityArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 mime.MimeType
arg4 livekit.VideoQuality
}
OnTrackMutedStub func(livekit.ParticipantID, *livekit.TrackInfo)
onTrackMutedMutex sync.RWMutex
onTrackMutedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}
OnTrackPublishRTPStatsStub func(livekit.ParticipantID, livekit.TrackID, mime.MimeType, int, *livekit.RTPStats)
onTrackPublishRTPStatsMutex sync.RWMutex
onTrackPublishRTPStatsArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 livekit.TrackID
arg3 mime.MimeType
arg4 int
arg5 *livekit.RTPStats
}
OnTrackPublishRequestedStub func(livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool)
onTrackPublishRequestedMutex sync.RWMutex
onTrackPublishRequestedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 livekit.ParticipantIdentity
arg3 *livekit.TrackInfo
arg4 bool
}
OnTrackPublishedStub func(livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool)
onTrackPublishedMutex sync.RWMutex
onTrackPublishedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 livekit.ParticipantIdentity
arg3 *livekit.TrackInfo
arg4 bool
}
OnTrackPublishedUpdateStub func(livekit.ParticipantID, *livekit.TrackInfo)
onTrackPublishedUpdateMutex sync.RWMutex
onTrackPublishedUpdateArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}
OnTrackStatsStub func(telemetry.StatsKey, *livekit.AnalyticsStat)
onTrackStatsMutex sync.RWMutex
onTrackStatsArgsForCall []struct {
arg1 telemetry.StatsKey
arg2 *livekit.AnalyticsStat
}
OnTrackSubscribeFailedStub func(livekit.ParticipantID, livekit.TrackID, error, bool)
onTrackSubscribeFailedMutex sync.RWMutex
onTrackSubscribeFailedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 livekit.TrackID
arg3 error
arg4 bool
}
OnTrackSubscribeRTPStatsStub func(livekit.ParticipantID, livekit.TrackID, mime.MimeType, *livekit.RTPStats)
onTrackSubscribeRTPStatsMutex sync.RWMutex
onTrackSubscribeRTPStatsArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 livekit.TrackID
arg3 mime.MimeType
arg4 *livekit.RTPStats
}
OnTrackSubscribeRequestedStub func(livekit.ParticipantID, *livekit.TrackInfo)
onTrackSubscribeRequestedMutex sync.RWMutex
onTrackSubscribeRequestedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}
OnTrackSubscribeStreamStartedStub func(livekit.ParticipantID, *livekit.TrackInfo, time.Duration)
onTrackSubscribeStreamStartedMutex sync.RWMutex
onTrackSubscribeStreamStartedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 time.Duration
}
OnTrackSubscribedStub func(livekit.ParticipantID, *livekit.TrackInfo, *livekit.ParticipantInfo, bool)
onTrackSubscribedMutex sync.RWMutex
onTrackSubscribedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 *livekit.ParticipantInfo
arg4 bool
}
OnTrackUnmutedStub func(livekit.ParticipantID, *livekit.TrackInfo)
onTrackUnmutedMutex sync.RWMutex
onTrackUnmutedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}
OnTrackUnpublishedStub func(livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool, bool)
onTrackUnpublishedMutex sync.RWMutex
onTrackUnpublishedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 livekit.ParticipantIdentity
arg3 *livekit.TrackInfo
arg4 bool
arg5 bool
}
OnTrackUnsubscribedStub func(livekit.ParticipantID, *livekit.TrackInfo, bool)
onTrackUnsubscribedMutex sync.RWMutex
onTrackUnsubscribedArgsForCall []struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 bool
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeParticipantTelemetryListener) OnTrackMaxSubscribedVideoQuality(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo, arg3 mime.MimeType, arg4 livekit.VideoQuality) {
fake.onTrackMaxSubscribedVideoQualityMutex.Lock()
fake.onTrackMaxSubscribedVideoQualityArgsForCall = append(fake.onTrackMaxSubscribedVideoQualityArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 mime.MimeType
arg4 livekit.VideoQuality
}{arg1, arg2, arg3, arg4})
stub := fake.OnTrackMaxSubscribedVideoQualityStub
fake.recordInvocation("OnTrackMaxSubscribedVideoQuality", []interface{}{arg1, arg2, arg3, arg4})
fake.onTrackMaxSubscribedVideoQualityMutex.Unlock()
if stub != nil {
fake.OnTrackMaxSubscribedVideoQualityStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackMaxSubscribedVideoQualityCallCount() int {
fake.onTrackMaxSubscribedVideoQualityMutex.RLock()
defer fake.onTrackMaxSubscribedVideoQualityMutex.RUnlock()
return len(fake.onTrackMaxSubscribedVideoQualityArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackMaxSubscribedVideoQualityCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo, mime.MimeType, livekit.VideoQuality)) {
fake.onTrackMaxSubscribedVideoQualityMutex.Lock()
defer fake.onTrackMaxSubscribedVideoQualityMutex.Unlock()
fake.OnTrackMaxSubscribedVideoQualityStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackMaxSubscribedVideoQualityArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo, mime.MimeType, livekit.VideoQuality) {
fake.onTrackMaxSubscribedVideoQualityMutex.RLock()
defer fake.onTrackMaxSubscribedVideoQualityMutex.RUnlock()
argsForCall := fake.onTrackMaxSubscribedVideoQualityArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeParticipantTelemetryListener) OnTrackMuted(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo) {
fake.onTrackMutedMutex.Lock()
fake.onTrackMutedArgsForCall = append(fake.onTrackMutedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}{arg1, arg2})
stub := fake.OnTrackMutedStub
fake.recordInvocation("OnTrackMuted", []interface{}{arg1, arg2})
fake.onTrackMutedMutex.Unlock()
if stub != nil {
fake.OnTrackMutedStub(arg1, arg2)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackMutedCallCount() int {
fake.onTrackMutedMutex.RLock()
defer fake.onTrackMutedMutex.RUnlock()
return len(fake.onTrackMutedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackMutedCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo)) {
fake.onTrackMutedMutex.Lock()
defer fake.onTrackMutedMutex.Unlock()
fake.OnTrackMutedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackMutedArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo) {
fake.onTrackMutedMutex.RLock()
defer fake.onTrackMutedMutex.RUnlock()
argsForCall := fake.onTrackMutedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRTPStats(arg1 livekit.ParticipantID, arg2 livekit.TrackID, arg3 mime.MimeType, arg4 int, arg5 *livekit.RTPStats) {
fake.onTrackPublishRTPStatsMutex.Lock()
fake.onTrackPublishRTPStatsArgsForCall = append(fake.onTrackPublishRTPStatsArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 livekit.TrackID
arg3 mime.MimeType
arg4 int
arg5 *livekit.RTPStats
}{arg1, arg2, arg3, arg4, arg5})
stub := fake.OnTrackPublishRTPStatsStub
fake.recordInvocation("OnTrackPublishRTPStats", []interface{}{arg1, arg2, arg3, arg4, arg5})
fake.onTrackPublishRTPStatsMutex.Unlock()
if stub != nil {
fake.OnTrackPublishRTPStatsStub(arg1, arg2, arg3, arg4, arg5)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRTPStatsCallCount() int {
fake.onTrackPublishRTPStatsMutex.RLock()
defer fake.onTrackPublishRTPStatsMutex.RUnlock()
return len(fake.onTrackPublishRTPStatsArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRTPStatsCalls(stub func(livekit.ParticipantID, livekit.TrackID, mime.MimeType, int, *livekit.RTPStats)) {
fake.onTrackPublishRTPStatsMutex.Lock()
defer fake.onTrackPublishRTPStatsMutex.Unlock()
fake.OnTrackPublishRTPStatsStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRTPStatsArgsForCall(i int) (livekit.ParticipantID, livekit.TrackID, mime.MimeType, int, *livekit.RTPStats) {
fake.onTrackPublishRTPStatsMutex.RLock()
defer fake.onTrackPublishRTPStatsMutex.RUnlock()
argsForCall := fake.onTrackPublishRTPStatsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRequested(arg1 livekit.ParticipantID, arg2 livekit.ParticipantIdentity, arg3 *livekit.TrackInfo, arg4 bool) {
fake.onTrackPublishRequestedMutex.Lock()
fake.onTrackPublishRequestedArgsForCall = append(fake.onTrackPublishRequestedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 livekit.ParticipantIdentity
arg3 *livekit.TrackInfo
arg4 bool
}{arg1, arg2, arg3, arg4})
stub := fake.OnTrackPublishRequestedStub
fake.recordInvocation("OnTrackPublishRequested", []interface{}{arg1, arg2, arg3, arg4})
fake.onTrackPublishRequestedMutex.Unlock()
if stub != nil {
fake.OnTrackPublishRequestedStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRequestedCallCount() int {
fake.onTrackPublishRequestedMutex.RLock()
defer fake.onTrackPublishRequestedMutex.RUnlock()
return len(fake.onTrackPublishRequestedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRequestedCalls(stub func(livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool)) {
fake.onTrackPublishRequestedMutex.Lock()
defer fake.onTrackPublishRequestedMutex.Unlock()
fake.OnTrackPublishRequestedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishRequestedArgsForCall(i int) (livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool) {
fake.onTrackPublishRequestedMutex.RLock()
defer fake.onTrackPublishRequestedMutex.RUnlock()
argsForCall := fake.onTrackPublishRequestedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublished(arg1 livekit.ParticipantID, arg2 livekit.ParticipantIdentity, arg3 *livekit.TrackInfo, arg4 bool) {
fake.onTrackPublishedMutex.Lock()
fake.onTrackPublishedArgsForCall = append(fake.onTrackPublishedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 livekit.ParticipantIdentity
arg3 *livekit.TrackInfo
arg4 bool
}{arg1, arg2, arg3, arg4})
stub := fake.OnTrackPublishedStub
fake.recordInvocation("OnTrackPublished", []interface{}{arg1, arg2, arg3, arg4})
fake.onTrackPublishedMutex.Unlock()
if stub != nil {
fake.OnTrackPublishedStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedCallCount() int {
fake.onTrackPublishedMutex.RLock()
defer fake.onTrackPublishedMutex.RUnlock()
return len(fake.onTrackPublishedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedCalls(stub func(livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool)) {
fake.onTrackPublishedMutex.Lock()
defer fake.onTrackPublishedMutex.Unlock()
fake.OnTrackPublishedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedArgsForCall(i int) (livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool) {
fake.onTrackPublishedMutex.RLock()
defer fake.onTrackPublishedMutex.RUnlock()
argsForCall := fake.onTrackPublishedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedUpdate(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo) {
fake.onTrackPublishedUpdateMutex.Lock()
fake.onTrackPublishedUpdateArgsForCall = append(fake.onTrackPublishedUpdateArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}{arg1, arg2})
stub := fake.OnTrackPublishedUpdateStub
fake.recordInvocation("OnTrackPublishedUpdate", []interface{}{arg1, arg2})
fake.onTrackPublishedUpdateMutex.Unlock()
if stub != nil {
fake.OnTrackPublishedUpdateStub(arg1, arg2)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedUpdateCallCount() int {
fake.onTrackPublishedUpdateMutex.RLock()
defer fake.onTrackPublishedUpdateMutex.RUnlock()
return len(fake.onTrackPublishedUpdateArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedUpdateCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo)) {
fake.onTrackPublishedUpdateMutex.Lock()
defer fake.onTrackPublishedUpdateMutex.Unlock()
fake.OnTrackPublishedUpdateStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackPublishedUpdateArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo) {
fake.onTrackPublishedUpdateMutex.RLock()
defer fake.onTrackPublishedUpdateMutex.RUnlock()
argsForCall := fake.onTrackPublishedUpdateArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantTelemetryListener) OnTrackStats(arg1 telemetry.StatsKey, arg2 *livekit.AnalyticsStat) {
fake.onTrackStatsMutex.Lock()
fake.onTrackStatsArgsForCall = append(fake.onTrackStatsArgsForCall, struct {
arg1 telemetry.StatsKey
arg2 *livekit.AnalyticsStat
}{arg1, arg2})
stub := fake.OnTrackStatsStub
fake.recordInvocation("OnTrackStats", []interface{}{arg1, arg2})
fake.onTrackStatsMutex.Unlock()
if stub != nil {
fake.OnTrackStatsStub(arg1, arg2)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackStatsCallCount() int {
fake.onTrackStatsMutex.RLock()
defer fake.onTrackStatsMutex.RUnlock()
return len(fake.onTrackStatsArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackStatsCalls(stub func(telemetry.StatsKey, *livekit.AnalyticsStat)) {
fake.onTrackStatsMutex.Lock()
defer fake.onTrackStatsMutex.Unlock()
fake.OnTrackStatsStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackStatsArgsForCall(i int) (telemetry.StatsKey, *livekit.AnalyticsStat) {
fake.onTrackStatsMutex.RLock()
defer fake.onTrackStatsMutex.RUnlock()
argsForCall := fake.onTrackStatsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeFailed(arg1 livekit.ParticipantID, arg2 livekit.TrackID, arg3 error, arg4 bool) {
fake.onTrackSubscribeFailedMutex.Lock()
fake.onTrackSubscribeFailedArgsForCall = append(fake.onTrackSubscribeFailedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 livekit.TrackID
arg3 error
arg4 bool
}{arg1, arg2, arg3, arg4})
stub := fake.OnTrackSubscribeFailedStub
fake.recordInvocation("OnTrackSubscribeFailed", []interface{}{arg1, arg2, arg3, arg4})
fake.onTrackSubscribeFailedMutex.Unlock()
if stub != nil {
fake.OnTrackSubscribeFailedStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeFailedCallCount() int {
fake.onTrackSubscribeFailedMutex.RLock()
defer fake.onTrackSubscribeFailedMutex.RUnlock()
return len(fake.onTrackSubscribeFailedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeFailedCalls(stub func(livekit.ParticipantID, livekit.TrackID, error, bool)) {
fake.onTrackSubscribeFailedMutex.Lock()
defer fake.onTrackSubscribeFailedMutex.Unlock()
fake.OnTrackSubscribeFailedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeFailedArgsForCall(i int) (livekit.ParticipantID, livekit.TrackID, error, bool) {
fake.onTrackSubscribeFailedMutex.RLock()
defer fake.onTrackSubscribeFailedMutex.RUnlock()
argsForCall := fake.onTrackSubscribeFailedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRTPStats(arg1 livekit.ParticipantID, arg2 livekit.TrackID, arg3 mime.MimeType, arg4 *livekit.RTPStats) {
fake.onTrackSubscribeRTPStatsMutex.Lock()
fake.onTrackSubscribeRTPStatsArgsForCall = append(fake.onTrackSubscribeRTPStatsArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 livekit.TrackID
arg3 mime.MimeType
arg4 *livekit.RTPStats
}{arg1, arg2, arg3, arg4})
stub := fake.OnTrackSubscribeRTPStatsStub
fake.recordInvocation("OnTrackSubscribeRTPStats", []interface{}{arg1, arg2, arg3, arg4})
fake.onTrackSubscribeRTPStatsMutex.Unlock()
if stub != nil {
fake.OnTrackSubscribeRTPStatsStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRTPStatsCallCount() int {
fake.onTrackSubscribeRTPStatsMutex.RLock()
defer fake.onTrackSubscribeRTPStatsMutex.RUnlock()
return len(fake.onTrackSubscribeRTPStatsArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRTPStatsCalls(stub func(livekit.ParticipantID, livekit.TrackID, mime.MimeType, *livekit.RTPStats)) {
fake.onTrackSubscribeRTPStatsMutex.Lock()
defer fake.onTrackSubscribeRTPStatsMutex.Unlock()
fake.OnTrackSubscribeRTPStatsStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRTPStatsArgsForCall(i int) (livekit.ParticipantID, livekit.TrackID, mime.MimeType, *livekit.RTPStats) {
fake.onTrackSubscribeRTPStatsMutex.RLock()
defer fake.onTrackSubscribeRTPStatsMutex.RUnlock()
argsForCall := fake.onTrackSubscribeRTPStatsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRequested(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo) {
fake.onTrackSubscribeRequestedMutex.Lock()
fake.onTrackSubscribeRequestedArgsForCall = append(fake.onTrackSubscribeRequestedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}{arg1, arg2})
stub := fake.OnTrackSubscribeRequestedStub
fake.recordInvocation("OnTrackSubscribeRequested", []interface{}{arg1, arg2})
fake.onTrackSubscribeRequestedMutex.Unlock()
if stub != nil {
fake.OnTrackSubscribeRequestedStub(arg1, arg2)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRequestedCallCount() int {
fake.onTrackSubscribeRequestedMutex.RLock()
defer fake.onTrackSubscribeRequestedMutex.RUnlock()
return len(fake.onTrackSubscribeRequestedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRequestedCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo)) {
fake.onTrackSubscribeRequestedMutex.Lock()
defer fake.onTrackSubscribeRequestedMutex.Unlock()
fake.OnTrackSubscribeRequestedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeRequestedArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo) {
fake.onTrackSubscribeRequestedMutex.RLock()
defer fake.onTrackSubscribeRequestedMutex.RUnlock()
argsForCall := fake.onTrackSubscribeRequestedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeStreamStarted(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo, arg3 time.Duration) {
fake.onTrackSubscribeStreamStartedMutex.Lock()
fake.onTrackSubscribeStreamStartedArgsForCall = append(fake.onTrackSubscribeStreamStartedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 time.Duration
}{arg1, arg2, arg3})
stub := fake.OnTrackSubscribeStreamStartedStub
fake.recordInvocation("OnTrackSubscribeStreamStarted", []interface{}{arg1, arg2, arg3})
fake.onTrackSubscribeStreamStartedMutex.Unlock()
if stub != nil {
fake.OnTrackSubscribeStreamStartedStub(arg1, arg2, arg3)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeStreamStartedCallCount() int {
fake.onTrackSubscribeStreamStartedMutex.RLock()
defer fake.onTrackSubscribeStreamStartedMutex.RUnlock()
return len(fake.onTrackSubscribeStreamStartedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeStreamStartedCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo, time.Duration)) {
fake.onTrackSubscribeStreamStartedMutex.Lock()
defer fake.onTrackSubscribeStreamStartedMutex.Unlock()
fake.OnTrackSubscribeStreamStartedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribeStreamStartedArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo, time.Duration) {
fake.onTrackSubscribeStreamStartedMutex.RLock()
defer fake.onTrackSubscribeStreamStartedMutex.RUnlock()
argsForCall := fake.onTrackSubscribeStreamStartedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribed(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo, arg3 *livekit.ParticipantInfo, arg4 bool) {
fake.onTrackSubscribedMutex.Lock()
fake.onTrackSubscribedArgsForCall = append(fake.onTrackSubscribedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 *livekit.ParticipantInfo
arg4 bool
}{arg1, arg2, arg3, arg4})
stub := fake.OnTrackSubscribedStub
fake.recordInvocation("OnTrackSubscribed", []interface{}{arg1, arg2, arg3, arg4})
fake.onTrackSubscribedMutex.Unlock()
if stub != nil {
fake.OnTrackSubscribedStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribedCallCount() int {
fake.onTrackSubscribedMutex.RLock()
defer fake.onTrackSubscribedMutex.RUnlock()
return len(fake.onTrackSubscribedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribedCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo, *livekit.ParticipantInfo, bool)) {
fake.onTrackSubscribedMutex.Lock()
defer fake.onTrackSubscribedMutex.Unlock()
fake.OnTrackSubscribedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackSubscribedArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo, *livekit.ParticipantInfo, bool) {
fake.onTrackSubscribedMutex.RLock()
defer fake.onTrackSubscribedMutex.RUnlock()
argsForCall := fake.onTrackSubscribedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnmuted(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo) {
fake.onTrackUnmutedMutex.Lock()
fake.onTrackUnmutedArgsForCall = append(fake.onTrackUnmutedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
}{arg1, arg2})
stub := fake.OnTrackUnmutedStub
fake.recordInvocation("OnTrackUnmuted", []interface{}{arg1, arg2})
fake.onTrackUnmutedMutex.Unlock()
if stub != nil {
fake.OnTrackUnmutedStub(arg1, arg2)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnmutedCallCount() int {
fake.onTrackUnmutedMutex.RLock()
defer fake.onTrackUnmutedMutex.RUnlock()
return len(fake.onTrackUnmutedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnmutedCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo)) {
fake.onTrackUnmutedMutex.Lock()
defer fake.onTrackUnmutedMutex.Unlock()
fake.OnTrackUnmutedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnmutedArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo) {
fake.onTrackUnmutedMutex.RLock()
defer fake.onTrackUnmutedMutex.RUnlock()
argsForCall := fake.onTrackUnmutedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnpublished(arg1 livekit.ParticipantID, arg2 livekit.ParticipantIdentity, arg3 *livekit.TrackInfo, arg4 bool, arg5 bool) {
fake.onTrackUnpublishedMutex.Lock()
fake.onTrackUnpublishedArgsForCall = append(fake.onTrackUnpublishedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 livekit.ParticipantIdentity
arg3 *livekit.TrackInfo
arg4 bool
arg5 bool
}{arg1, arg2, arg3, arg4, arg5})
stub := fake.OnTrackUnpublishedStub
fake.recordInvocation("OnTrackUnpublished", []interface{}{arg1, arg2, arg3, arg4, arg5})
fake.onTrackUnpublishedMutex.Unlock()
if stub != nil {
fake.OnTrackUnpublishedStub(arg1, arg2, arg3, arg4, arg5)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnpublishedCallCount() int {
fake.onTrackUnpublishedMutex.RLock()
defer fake.onTrackUnpublishedMutex.RUnlock()
return len(fake.onTrackUnpublishedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnpublishedCalls(stub func(livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool, bool)) {
fake.onTrackUnpublishedMutex.Lock()
defer fake.onTrackUnpublishedMutex.Unlock()
fake.OnTrackUnpublishedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnpublishedArgsForCall(i int) (livekit.ParticipantID, livekit.ParticipantIdentity, *livekit.TrackInfo, bool, bool) {
fake.onTrackUnpublishedMutex.RLock()
defer fake.onTrackUnpublishedMutex.RUnlock()
argsForCall := fake.onTrackUnpublishedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnsubscribed(arg1 livekit.ParticipantID, arg2 *livekit.TrackInfo, arg3 bool) {
fake.onTrackUnsubscribedMutex.Lock()
fake.onTrackUnsubscribedArgsForCall = append(fake.onTrackUnsubscribedArgsForCall, struct {
arg1 livekit.ParticipantID
arg2 *livekit.TrackInfo
arg3 bool
}{arg1, arg2, arg3})
stub := fake.OnTrackUnsubscribedStub
fake.recordInvocation("OnTrackUnsubscribed", []interface{}{arg1, arg2, arg3})
fake.onTrackUnsubscribedMutex.Unlock()
if stub != nil {
fake.OnTrackUnsubscribedStub(arg1, arg2, arg3)
}
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnsubscribedCallCount() int {
fake.onTrackUnsubscribedMutex.RLock()
defer fake.onTrackUnsubscribedMutex.RUnlock()
return len(fake.onTrackUnsubscribedArgsForCall)
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnsubscribedCalls(stub func(livekit.ParticipantID, *livekit.TrackInfo, bool)) {
fake.onTrackUnsubscribedMutex.Lock()
defer fake.onTrackUnsubscribedMutex.Unlock()
fake.OnTrackUnsubscribedStub = stub
}
func (fake *FakeParticipantTelemetryListener) OnTrackUnsubscribedArgsForCall(i int) (livekit.ParticipantID, *livekit.TrackInfo, bool) {
fake.onTrackUnsubscribedMutex.RLock()
defer fake.onTrackUnsubscribedMutex.RUnlock()
argsForCall := fake.onTrackUnsubscribedArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeParticipantTelemetryListener) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeParticipantTelemetryListener) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.ParticipantTelemetryListener = new(FakeParticipantTelemetryListener)

View File

@@ -0,0 +1,541 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/livekit"
)
type FakeRoom struct {
GetLocalParticipantsStub func() []types.LocalParticipant
getLocalParticipantsMutex sync.RWMutex
getLocalParticipantsArgsForCall []struct {
}
getLocalParticipantsReturns struct {
result1 []types.LocalParticipant
}
getLocalParticipantsReturnsOnCall map[int]struct {
result1 []types.LocalParticipant
}
IDStub func() livekit.RoomID
iDMutex sync.RWMutex
iDArgsForCall []struct {
}
iDReturns struct {
result1 livekit.RoomID
}
iDReturnsOnCall map[int]struct {
result1 livekit.RoomID
}
IsDataMessageUserPacketDuplicateStub func(*livekit.UserPacket) bool
isDataMessageUserPacketDuplicateMutex sync.RWMutex
isDataMessageUserPacketDuplicateArgsForCall []struct {
arg1 *livekit.UserPacket
}
isDataMessageUserPacketDuplicateReturns struct {
result1 bool
}
isDataMessageUserPacketDuplicateReturnsOnCall map[int]struct {
result1 bool
}
NameStub func() livekit.RoomName
nameMutex sync.RWMutex
nameArgsForCall []struct {
}
nameReturns struct {
result1 livekit.RoomName
}
nameReturnsOnCall map[int]struct {
result1 livekit.RoomName
}
RemoveParticipantStub func(livekit.ParticipantIdentity, livekit.ParticipantID, types.ParticipantCloseReason)
removeParticipantMutex sync.RWMutex
removeParticipantArgsForCall []struct {
arg1 livekit.ParticipantIdentity
arg2 livekit.ParticipantID
arg3 types.ParticipantCloseReason
}
ResolveDataTrackForSubscriberStub func(types.LocalParticipant, livekit.TrackID) types.DataResolverResult
resolveDataTrackForSubscriberMutex sync.RWMutex
resolveDataTrackForSubscriberArgsForCall []struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}
resolveDataTrackForSubscriberReturns struct {
result1 types.DataResolverResult
}
resolveDataTrackForSubscriberReturnsOnCall map[int]struct {
result1 types.DataResolverResult
}
ResolveMediaTrackForSubscriberStub func(types.LocalParticipant, livekit.TrackID) types.MediaResolverResult
resolveMediaTrackForSubscriberMutex sync.RWMutex
resolveMediaTrackForSubscriberArgsForCall []struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}
resolveMediaTrackForSubscriberReturns struct {
result1 types.MediaResolverResult
}
resolveMediaTrackForSubscriberReturnsOnCall map[int]struct {
result1 types.MediaResolverResult
}
UpdateSubscriptionsStub func(types.LocalParticipant, []livekit.TrackID, []*livekit.ParticipantTracks, bool)
updateSubscriptionsMutex sync.RWMutex
updateSubscriptionsArgsForCall []struct {
arg1 types.LocalParticipant
arg2 []livekit.TrackID
arg3 []*livekit.ParticipantTracks
arg4 bool
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeRoom) GetLocalParticipants() []types.LocalParticipant {
fake.getLocalParticipantsMutex.Lock()
ret, specificReturn := fake.getLocalParticipantsReturnsOnCall[len(fake.getLocalParticipantsArgsForCall)]
fake.getLocalParticipantsArgsForCall = append(fake.getLocalParticipantsArgsForCall, struct {
}{})
stub := fake.GetLocalParticipantsStub
fakeReturns := fake.getLocalParticipantsReturns
fake.recordInvocation("GetLocalParticipants", []interface{}{})
fake.getLocalParticipantsMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRoom) GetLocalParticipantsCallCount() int {
fake.getLocalParticipantsMutex.RLock()
defer fake.getLocalParticipantsMutex.RUnlock()
return len(fake.getLocalParticipantsArgsForCall)
}
func (fake *FakeRoom) GetLocalParticipantsCalls(stub func() []types.LocalParticipant) {
fake.getLocalParticipantsMutex.Lock()
defer fake.getLocalParticipantsMutex.Unlock()
fake.GetLocalParticipantsStub = stub
}
func (fake *FakeRoom) GetLocalParticipantsReturns(result1 []types.LocalParticipant) {
fake.getLocalParticipantsMutex.Lock()
defer fake.getLocalParticipantsMutex.Unlock()
fake.GetLocalParticipantsStub = nil
fake.getLocalParticipantsReturns = struct {
result1 []types.LocalParticipant
}{result1}
}
func (fake *FakeRoom) GetLocalParticipantsReturnsOnCall(i int, result1 []types.LocalParticipant) {
fake.getLocalParticipantsMutex.Lock()
defer fake.getLocalParticipantsMutex.Unlock()
fake.GetLocalParticipantsStub = nil
if fake.getLocalParticipantsReturnsOnCall == nil {
fake.getLocalParticipantsReturnsOnCall = make(map[int]struct {
result1 []types.LocalParticipant
})
}
fake.getLocalParticipantsReturnsOnCall[i] = struct {
result1 []types.LocalParticipant
}{result1}
}
func (fake *FakeRoom) ID() livekit.RoomID {
fake.iDMutex.Lock()
ret, specificReturn := fake.iDReturnsOnCall[len(fake.iDArgsForCall)]
fake.iDArgsForCall = append(fake.iDArgsForCall, struct {
}{})
stub := fake.IDStub
fakeReturns := fake.iDReturns
fake.recordInvocation("ID", []interface{}{})
fake.iDMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRoom) IDCallCount() int {
fake.iDMutex.RLock()
defer fake.iDMutex.RUnlock()
return len(fake.iDArgsForCall)
}
func (fake *FakeRoom) IDCalls(stub func() livekit.RoomID) {
fake.iDMutex.Lock()
defer fake.iDMutex.Unlock()
fake.IDStub = stub
}
func (fake *FakeRoom) IDReturns(result1 livekit.RoomID) {
fake.iDMutex.Lock()
defer fake.iDMutex.Unlock()
fake.IDStub = nil
fake.iDReturns = struct {
result1 livekit.RoomID
}{result1}
}
func (fake *FakeRoom) IDReturnsOnCall(i int, result1 livekit.RoomID) {
fake.iDMutex.Lock()
defer fake.iDMutex.Unlock()
fake.IDStub = nil
if fake.iDReturnsOnCall == nil {
fake.iDReturnsOnCall = make(map[int]struct {
result1 livekit.RoomID
})
}
fake.iDReturnsOnCall[i] = struct {
result1 livekit.RoomID
}{result1}
}
func (fake *FakeRoom) IsDataMessageUserPacketDuplicate(arg1 *livekit.UserPacket) bool {
fake.isDataMessageUserPacketDuplicateMutex.Lock()
ret, specificReturn := fake.isDataMessageUserPacketDuplicateReturnsOnCall[len(fake.isDataMessageUserPacketDuplicateArgsForCall)]
fake.isDataMessageUserPacketDuplicateArgsForCall = append(fake.isDataMessageUserPacketDuplicateArgsForCall, struct {
arg1 *livekit.UserPacket
}{arg1})
stub := fake.IsDataMessageUserPacketDuplicateStub
fakeReturns := fake.isDataMessageUserPacketDuplicateReturns
fake.recordInvocation("IsDataMessageUserPacketDuplicate", []interface{}{arg1})
fake.isDataMessageUserPacketDuplicateMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRoom) IsDataMessageUserPacketDuplicateCallCount() int {
fake.isDataMessageUserPacketDuplicateMutex.RLock()
defer fake.isDataMessageUserPacketDuplicateMutex.RUnlock()
return len(fake.isDataMessageUserPacketDuplicateArgsForCall)
}
func (fake *FakeRoom) IsDataMessageUserPacketDuplicateCalls(stub func(*livekit.UserPacket) bool) {
fake.isDataMessageUserPacketDuplicateMutex.Lock()
defer fake.isDataMessageUserPacketDuplicateMutex.Unlock()
fake.IsDataMessageUserPacketDuplicateStub = stub
}
func (fake *FakeRoom) IsDataMessageUserPacketDuplicateArgsForCall(i int) *livekit.UserPacket {
fake.isDataMessageUserPacketDuplicateMutex.RLock()
defer fake.isDataMessageUserPacketDuplicateMutex.RUnlock()
argsForCall := fake.isDataMessageUserPacketDuplicateArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeRoom) IsDataMessageUserPacketDuplicateReturns(result1 bool) {
fake.isDataMessageUserPacketDuplicateMutex.Lock()
defer fake.isDataMessageUserPacketDuplicateMutex.Unlock()
fake.IsDataMessageUserPacketDuplicateStub = nil
fake.isDataMessageUserPacketDuplicateReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeRoom) IsDataMessageUserPacketDuplicateReturnsOnCall(i int, result1 bool) {
fake.isDataMessageUserPacketDuplicateMutex.Lock()
defer fake.isDataMessageUserPacketDuplicateMutex.Unlock()
fake.IsDataMessageUserPacketDuplicateStub = nil
if fake.isDataMessageUserPacketDuplicateReturnsOnCall == nil {
fake.isDataMessageUserPacketDuplicateReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.isDataMessageUserPacketDuplicateReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeRoom) Name() livekit.RoomName {
fake.nameMutex.Lock()
ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)]
fake.nameArgsForCall = append(fake.nameArgsForCall, struct {
}{})
stub := fake.NameStub
fakeReturns := fake.nameReturns
fake.recordInvocation("Name", []interface{}{})
fake.nameMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRoom) NameCallCount() int {
fake.nameMutex.RLock()
defer fake.nameMutex.RUnlock()
return len(fake.nameArgsForCall)
}
func (fake *FakeRoom) NameCalls(stub func() livekit.RoomName) {
fake.nameMutex.Lock()
defer fake.nameMutex.Unlock()
fake.NameStub = stub
}
func (fake *FakeRoom) NameReturns(result1 livekit.RoomName) {
fake.nameMutex.Lock()
defer fake.nameMutex.Unlock()
fake.NameStub = nil
fake.nameReturns = struct {
result1 livekit.RoomName
}{result1}
}
func (fake *FakeRoom) NameReturnsOnCall(i int, result1 livekit.RoomName) {
fake.nameMutex.Lock()
defer fake.nameMutex.Unlock()
fake.NameStub = nil
if fake.nameReturnsOnCall == nil {
fake.nameReturnsOnCall = make(map[int]struct {
result1 livekit.RoomName
})
}
fake.nameReturnsOnCall[i] = struct {
result1 livekit.RoomName
}{result1}
}
func (fake *FakeRoom) RemoveParticipant(arg1 livekit.ParticipantIdentity, arg2 livekit.ParticipantID, arg3 types.ParticipantCloseReason) {
fake.removeParticipantMutex.Lock()
fake.removeParticipantArgsForCall = append(fake.removeParticipantArgsForCall, struct {
arg1 livekit.ParticipantIdentity
arg2 livekit.ParticipantID
arg3 types.ParticipantCloseReason
}{arg1, arg2, arg3})
stub := fake.RemoveParticipantStub
fake.recordInvocation("RemoveParticipant", []interface{}{arg1, arg2, arg3})
fake.removeParticipantMutex.Unlock()
if stub != nil {
fake.RemoveParticipantStub(arg1, arg2, arg3)
}
}
func (fake *FakeRoom) RemoveParticipantCallCount() int {
fake.removeParticipantMutex.RLock()
defer fake.removeParticipantMutex.RUnlock()
return len(fake.removeParticipantArgsForCall)
}
func (fake *FakeRoom) RemoveParticipantCalls(stub func(livekit.ParticipantIdentity, livekit.ParticipantID, types.ParticipantCloseReason)) {
fake.removeParticipantMutex.Lock()
defer fake.removeParticipantMutex.Unlock()
fake.RemoveParticipantStub = stub
}
func (fake *FakeRoom) RemoveParticipantArgsForCall(i int) (livekit.ParticipantIdentity, livekit.ParticipantID, types.ParticipantCloseReason) {
fake.removeParticipantMutex.RLock()
defer fake.removeParticipantMutex.RUnlock()
argsForCall := fake.removeParticipantArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeRoom) ResolveDataTrackForSubscriber(arg1 types.LocalParticipant, arg2 livekit.TrackID) types.DataResolverResult {
fake.resolveDataTrackForSubscriberMutex.Lock()
ret, specificReturn := fake.resolveDataTrackForSubscriberReturnsOnCall[len(fake.resolveDataTrackForSubscriberArgsForCall)]
fake.resolveDataTrackForSubscriberArgsForCall = append(fake.resolveDataTrackForSubscriberArgsForCall, struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}{arg1, arg2})
stub := fake.ResolveDataTrackForSubscriberStub
fakeReturns := fake.resolveDataTrackForSubscriberReturns
fake.recordInvocation("ResolveDataTrackForSubscriber", []interface{}{arg1, arg2})
fake.resolveDataTrackForSubscriberMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRoom) ResolveDataTrackForSubscriberCallCount() int {
fake.resolveDataTrackForSubscriberMutex.RLock()
defer fake.resolveDataTrackForSubscriberMutex.RUnlock()
return len(fake.resolveDataTrackForSubscriberArgsForCall)
}
func (fake *FakeRoom) ResolveDataTrackForSubscriberCalls(stub func(types.LocalParticipant, livekit.TrackID) types.DataResolverResult) {
fake.resolveDataTrackForSubscriberMutex.Lock()
defer fake.resolveDataTrackForSubscriberMutex.Unlock()
fake.ResolveDataTrackForSubscriberStub = stub
}
func (fake *FakeRoom) ResolveDataTrackForSubscriberArgsForCall(i int) (types.LocalParticipant, livekit.TrackID) {
fake.resolveDataTrackForSubscriberMutex.RLock()
defer fake.resolveDataTrackForSubscriberMutex.RUnlock()
argsForCall := fake.resolveDataTrackForSubscriberArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeRoom) ResolveDataTrackForSubscriberReturns(result1 types.DataResolverResult) {
fake.resolveDataTrackForSubscriberMutex.Lock()
defer fake.resolveDataTrackForSubscriberMutex.Unlock()
fake.ResolveDataTrackForSubscriberStub = nil
fake.resolveDataTrackForSubscriberReturns = struct {
result1 types.DataResolverResult
}{result1}
}
func (fake *FakeRoom) ResolveDataTrackForSubscriberReturnsOnCall(i int, result1 types.DataResolverResult) {
fake.resolveDataTrackForSubscriberMutex.Lock()
defer fake.resolveDataTrackForSubscriberMutex.Unlock()
fake.ResolveDataTrackForSubscriberStub = nil
if fake.resolveDataTrackForSubscriberReturnsOnCall == nil {
fake.resolveDataTrackForSubscriberReturnsOnCall = make(map[int]struct {
result1 types.DataResolverResult
})
}
fake.resolveDataTrackForSubscriberReturnsOnCall[i] = struct {
result1 types.DataResolverResult
}{result1}
}
func (fake *FakeRoom) ResolveMediaTrackForSubscriber(arg1 types.LocalParticipant, arg2 livekit.TrackID) types.MediaResolverResult {
fake.resolveMediaTrackForSubscriberMutex.Lock()
ret, specificReturn := fake.resolveMediaTrackForSubscriberReturnsOnCall[len(fake.resolveMediaTrackForSubscriberArgsForCall)]
fake.resolveMediaTrackForSubscriberArgsForCall = append(fake.resolveMediaTrackForSubscriberArgsForCall, struct {
arg1 types.LocalParticipant
arg2 livekit.TrackID
}{arg1, arg2})
stub := fake.ResolveMediaTrackForSubscriberStub
fakeReturns := fake.resolveMediaTrackForSubscriberReturns
fake.recordInvocation("ResolveMediaTrackForSubscriber", []interface{}{arg1, arg2})
fake.resolveMediaTrackForSubscriberMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeRoom) ResolveMediaTrackForSubscriberCallCount() int {
fake.resolveMediaTrackForSubscriberMutex.RLock()
defer fake.resolveMediaTrackForSubscriberMutex.RUnlock()
return len(fake.resolveMediaTrackForSubscriberArgsForCall)
}
func (fake *FakeRoom) ResolveMediaTrackForSubscriberCalls(stub func(types.LocalParticipant, livekit.TrackID) types.MediaResolverResult) {
fake.resolveMediaTrackForSubscriberMutex.Lock()
defer fake.resolveMediaTrackForSubscriberMutex.Unlock()
fake.ResolveMediaTrackForSubscriberStub = stub
}
func (fake *FakeRoom) ResolveMediaTrackForSubscriberArgsForCall(i int) (types.LocalParticipant, livekit.TrackID) {
fake.resolveMediaTrackForSubscriberMutex.RLock()
defer fake.resolveMediaTrackForSubscriberMutex.RUnlock()
argsForCall := fake.resolveMediaTrackForSubscriberArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeRoom) ResolveMediaTrackForSubscriberReturns(result1 types.MediaResolverResult) {
fake.resolveMediaTrackForSubscriberMutex.Lock()
defer fake.resolveMediaTrackForSubscriberMutex.Unlock()
fake.ResolveMediaTrackForSubscriberStub = nil
fake.resolveMediaTrackForSubscriberReturns = struct {
result1 types.MediaResolverResult
}{result1}
}
func (fake *FakeRoom) ResolveMediaTrackForSubscriberReturnsOnCall(i int, result1 types.MediaResolverResult) {
fake.resolveMediaTrackForSubscriberMutex.Lock()
defer fake.resolveMediaTrackForSubscriberMutex.Unlock()
fake.ResolveMediaTrackForSubscriberStub = nil
if fake.resolveMediaTrackForSubscriberReturnsOnCall == nil {
fake.resolveMediaTrackForSubscriberReturnsOnCall = make(map[int]struct {
result1 types.MediaResolverResult
})
}
fake.resolveMediaTrackForSubscriberReturnsOnCall[i] = struct {
result1 types.MediaResolverResult
}{result1}
}
func (fake *FakeRoom) UpdateSubscriptions(arg1 types.LocalParticipant, arg2 []livekit.TrackID, arg3 []*livekit.ParticipantTracks, arg4 bool) {
var arg2Copy []livekit.TrackID
if arg2 != nil {
arg2Copy = make([]livekit.TrackID, len(arg2))
copy(arg2Copy, arg2)
}
var arg3Copy []*livekit.ParticipantTracks
if arg3 != nil {
arg3Copy = make([]*livekit.ParticipantTracks, len(arg3))
copy(arg3Copy, arg3)
}
fake.updateSubscriptionsMutex.Lock()
fake.updateSubscriptionsArgsForCall = append(fake.updateSubscriptionsArgsForCall, struct {
arg1 types.LocalParticipant
arg2 []livekit.TrackID
arg3 []*livekit.ParticipantTracks
arg4 bool
}{arg1, arg2Copy, arg3Copy, arg4})
stub := fake.UpdateSubscriptionsStub
fake.recordInvocation("UpdateSubscriptions", []interface{}{arg1, arg2Copy, arg3Copy, arg4})
fake.updateSubscriptionsMutex.Unlock()
if stub != nil {
fake.UpdateSubscriptionsStub(arg1, arg2, arg3, arg4)
}
}
func (fake *FakeRoom) UpdateSubscriptionsCallCount() int {
fake.updateSubscriptionsMutex.RLock()
defer fake.updateSubscriptionsMutex.RUnlock()
return len(fake.updateSubscriptionsArgsForCall)
}
func (fake *FakeRoom) UpdateSubscriptionsCalls(stub func(types.LocalParticipant, []livekit.TrackID, []*livekit.ParticipantTracks, bool)) {
fake.updateSubscriptionsMutex.Lock()
defer fake.updateSubscriptionsMutex.Unlock()
fake.UpdateSubscriptionsStub = stub
}
func (fake *FakeRoom) UpdateSubscriptionsArgsForCall(i int) (types.LocalParticipant, []livekit.TrackID, []*livekit.ParticipantTracks, bool) {
fake.updateSubscriptionsMutex.RLock()
defer fake.updateSubscriptionsMutex.RUnlock()
argsForCall := fake.updateSubscriptionsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeRoom) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeRoom) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.Room = new(FakeRoom)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,480 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"io"
"sync"
"time"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
type FakeWebsocketClient struct {
CloseStub func() error
closeMutex sync.RWMutex
closeArgsForCall []struct {
}
closeReturns struct {
result1 error
}
closeReturnsOnCall map[int]struct {
result1 error
}
NextReaderStub func() (int, io.Reader, error)
nextReaderMutex sync.RWMutex
nextReaderArgsForCall []struct {
}
nextReaderReturns struct {
result1 int
result2 io.Reader
result3 error
}
nextReaderReturnsOnCall map[int]struct {
result1 int
result2 io.Reader
result3 error
}
ReadMessageStub func() (int, []byte, error)
readMessageMutex sync.RWMutex
readMessageArgsForCall []struct {
}
readMessageReturns struct {
result1 int
result2 []byte
result3 error
}
readMessageReturnsOnCall map[int]struct {
result1 int
result2 []byte
result3 error
}
SetReadDeadlineStub func(time.Time) error
setReadDeadlineMutex sync.RWMutex
setReadDeadlineArgsForCall []struct {
arg1 time.Time
}
setReadDeadlineReturns struct {
result1 error
}
setReadDeadlineReturnsOnCall map[int]struct {
result1 error
}
WriteControlStub func(int, []byte, time.Time) error
writeControlMutex sync.RWMutex
writeControlArgsForCall []struct {
arg1 int
arg2 []byte
arg3 time.Time
}
writeControlReturns struct {
result1 error
}
writeControlReturnsOnCall map[int]struct {
result1 error
}
WriteMessageStub func(int, []byte) error
writeMessageMutex sync.RWMutex
writeMessageArgsForCall []struct {
arg1 int
arg2 []byte
}
writeMessageReturns struct {
result1 error
}
writeMessageReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeWebsocketClient) Close() error {
fake.closeMutex.Lock()
ret, specificReturn := fake.closeReturnsOnCall[len(fake.closeArgsForCall)]
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
}{})
stub := fake.CloseStub
fakeReturns := fake.closeReturns
fake.recordInvocation("Close", []interface{}{})
fake.closeMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeWebsocketClient) CloseCallCount() int {
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
return len(fake.closeArgsForCall)
}
func (fake *FakeWebsocketClient) CloseCalls(stub func() error) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = stub
}
func (fake *FakeWebsocketClient) CloseReturns(result1 error) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = nil
fake.closeReturns = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) CloseReturnsOnCall(i int, result1 error) {
fake.closeMutex.Lock()
defer fake.closeMutex.Unlock()
fake.CloseStub = nil
if fake.closeReturnsOnCall == nil {
fake.closeReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.closeReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) NextReader() (int, io.Reader, error) {
fake.nextReaderMutex.Lock()
ret, specificReturn := fake.nextReaderReturnsOnCall[len(fake.nextReaderArgsForCall)]
fake.nextReaderArgsForCall = append(fake.nextReaderArgsForCall, struct {
}{})
stub := fake.NextReaderStub
fakeReturns := fake.nextReaderReturns
fake.recordInvocation("NextReader", []interface{}{})
fake.nextReaderMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1, ret.result2, ret.result3
}
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3
}
func (fake *FakeWebsocketClient) NextReaderCallCount() int {
fake.nextReaderMutex.RLock()
defer fake.nextReaderMutex.RUnlock()
return len(fake.nextReaderArgsForCall)
}
func (fake *FakeWebsocketClient) NextReaderCalls(stub func() (int, io.Reader, error)) {
fake.nextReaderMutex.Lock()
defer fake.nextReaderMutex.Unlock()
fake.NextReaderStub = stub
}
func (fake *FakeWebsocketClient) NextReaderReturns(result1 int, result2 io.Reader, result3 error) {
fake.nextReaderMutex.Lock()
defer fake.nextReaderMutex.Unlock()
fake.NextReaderStub = nil
fake.nextReaderReturns = struct {
result1 int
result2 io.Reader
result3 error
}{result1, result2, result3}
}
func (fake *FakeWebsocketClient) NextReaderReturnsOnCall(i int, result1 int, result2 io.Reader, result3 error) {
fake.nextReaderMutex.Lock()
defer fake.nextReaderMutex.Unlock()
fake.NextReaderStub = nil
if fake.nextReaderReturnsOnCall == nil {
fake.nextReaderReturnsOnCall = make(map[int]struct {
result1 int
result2 io.Reader
result3 error
})
}
fake.nextReaderReturnsOnCall[i] = struct {
result1 int
result2 io.Reader
result3 error
}{result1, result2, result3}
}
func (fake *FakeWebsocketClient) ReadMessage() (int, []byte, error) {
fake.readMessageMutex.Lock()
ret, specificReturn := fake.readMessageReturnsOnCall[len(fake.readMessageArgsForCall)]
fake.readMessageArgsForCall = append(fake.readMessageArgsForCall, struct {
}{})
stub := fake.ReadMessageStub
fakeReturns := fake.readMessageReturns
fake.recordInvocation("ReadMessage", []interface{}{})
fake.readMessageMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1, ret.result2, ret.result3
}
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3
}
func (fake *FakeWebsocketClient) ReadMessageCallCount() int {
fake.readMessageMutex.RLock()
defer fake.readMessageMutex.RUnlock()
return len(fake.readMessageArgsForCall)
}
func (fake *FakeWebsocketClient) ReadMessageCalls(stub func() (int, []byte, error)) {
fake.readMessageMutex.Lock()
defer fake.readMessageMutex.Unlock()
fake.ReadMessageStub = stub
}
func (fake *FakeWebsocketClient) ReadMessageReturns(result1 int, result2 []byte, result3 error) {
fake.readMessageMutex.Lock()
defer fake.readMessageMutex.Unlock()
fake.ReadMessageStub = nil
fake.readMessageReturns = struct {
result1 int
result2 []byte
result3 error
}{result1, result2, result3}
}
func (fake *FakeWebsocketClient) ReadMessageReturnsOnCall(i int, result1 int, result2 []byte, result3 error) {
fake.readMessageMutex.Lock()
defer fake.readMessageMutex.Unlock()
fake.ReadMessageStub = nil
if fake.readMessageReturnsOnCall == nil {
fake.readMessageReturnsOnCall = make(map[int]struct {
result1 int
result2 []byte
result3 error
})
}
fake.readMessageReturnsOnCall[i] = struct {
result1 int
result2 []byte
result3 error
}{result1, result2, result3}
}
func (fake *FakeWebsocketClient) SetReadDeadline(arg1 time.Time) error {
fake.setReadDeadlineMutex.Lock()
ret, specificReturn := fake.setReadDeadlineReturnsOnCall[len(fake.setReadDeadlineArgsForCall)]
fake.setReadDeadlineArgsForCall = append(fake.setReadDeadlineArgsForCall, struct {
arg1 time.Time
}{arg1})
stub := fake.SetReadDeadlineStub
fakeReturns := fake.setReadDeadlineReturns
fake.recordInvocation("SetReadDeadline", []interface{}{arg1})
fake.setReadDeadlineMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeWebsocketClient) SetReadDeadlineCallCount() int {
fake.setReadDeadlineMutex.RLock()
defer fake.setReadDeadlineMutex.RUnlock()
return len(fake.setReadDeadlineArgsForCall)
}
func (fake *FakeWebsocketClient) SetReadDeadlineCalls(stub func(time.Time) error) {
fake.setReadDeadlineMutex.Lock()
defer fake.setReadDeadlineMutex.Unlock()
fake.SetReadDeadlineStub = stub
}
func (fake *FakeWebsocketClient) SetReadDeadlineArgsForCall(i int) time.Time {
fake.setReadDeadlineMutex.RLock()
defer fake.setReadDeadlineMutex.RUnlock()
argsForCall := fake.setReadDeadlineArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeWebsocketClient) SetReadDeadlineReturns(result1 error) {
fake.setReadDeadlineMutex.Lock()
defer fake.setReadDeadlineMutex.Unlock()
fake.SetReadDeadlineStub = nil
fake.setReadDeadlineReturns = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) SetReadDeadlineReturnsOnCall(i int, result1 error) {
fake.setReadDeadlineMutex.Lock()
defer fake.setReadDeadlineMutex.Unlock()
fake.SetReadDeadlineStub = nil
if fake.setReadDeadlineReturnsOnCall == nil {
fake.setReadDeadlineReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setReadDeadlineReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) WriteControl(arg1 int, arg2 []byte, arg3 time.Time) error {
var arg2Copy []byte
if arg2 != nil {
arg2Copy = make([]byte, len(arg2))
copy(arg2Copy, arg2)
}
fake.writeControlMutex.Lock()
ret, specificReturn := fake.writeControlReturnsOnCall[len(fake.writeControlArgsForCall)]
fake.writeControlArgsForCall = append(fake.writeControlArgsForCall, struct {
arg1 int
arg2 []byte
arg3 time.Time
}{arg1, arg2Copy, arg3})
stub := fake.WriteControlStub
fakeReturns := fake.writeControlReturns
fake.recordInvocation("WriteControl", []interface{}{arg1, arg2Copy, arg3})
fake.writeControlMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeWebsocketClient) WriteControlCallCount() int {
fake.writeControlMutex.RLock()
defer fake.writeControlMutex.RUnlock()
return len(fake.writeControlArgsForCall)
}
func (fake *FakeWebsocketClient) WriteControlCalls(stub func(int, []byte, time.Time) error) {
fake.writeControlMutex.Lock()
defer fake.writeControlMutex.Unlock()
fake.WriteControlStub = stub
}
func (fake *FakeWebsocketClient) WriteControlArgsForCall(i int) (int, []byte, time.Time) {
fake.writeControlMutex.RLock()
defer fake.writeControlMutex.RUnlock()
argsForCall := fake.writeControlArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeWebsocketClient) WriteControlReturns(result1 error) {
fake.writeControlMutex.Lock()
defer fake.writeControlMutex.Unlock()
fake.WriteControlStub = nil
fake.writeControlReturns = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) WriteControlReturnsOnCall(i int, result1 error) {
fake.writeControlMutex.Lock()
defer fake.writeControlMutex.Unlock()
fake.WriteControlStub = nil
if fake.writeControlReturnsOnCall == nil {
fake.writeControlReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.writeControlReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) WriteMessage(arg1 int, arg2 []byte) error {
var arg2Copy []byte
if arg2 != nil {
arg2Copy = make([]byte, len(arg2))
copy(arg2Copy, arg2)
}
fake.writeMessageMutex.Lock()
ret, specificReturn := fake.writeMessageReturnsOnCall[len(fake.writeMessageArgsForCall)]
fake.writeMessageArgsForCall = append(fake.writeMessageArgsForCall, struct {
arg1 int
arg2 []byte
}{arg1, arg2Copy})
stub := fake.WriteMessageStub
fakeReturns := fake.writeMessageReturns
fake.recordInvocation("WriteMessage", []interface{}{arg1, arg2Copy})
fake.writeMessageMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeWebsocketClient) WriteMessageCallCount() int {
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
return len(fake.writeMessageArgsForCall)
}
func (fake *FakeWebsocketClient) WriteMessageCalls(stub func(int, []byte) error) {
fake.writeMessageMutex.Lock()
defer fake.writeMessageMutex.Unlock()
fake.WriteMessageStub = stub
}
func (fake *FakeWebsocketClient) WriteMessageArgsForCall(i int) (int, []byte) {
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
argsForCall := fake.writeMessageArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeWebsocketClient) WriteMessageReturns(result1 error) {
fake.writeMessageMutex.Lock()
defer fake.writeMessageMutex.Unlock()
fake.WriteMessageStub = nil
fake.writeMessageReturns = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) WriteMessageReturnsOnCall(i int, result1 error) {
fake.writeMessageMutex.Lock()
defer fake.writeMessageMutex.Unlock()
fake.WriteMessageStub = nil
if fake.writeMessageReturnsOnCall == nil {
fake.writeMessageReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.writeMessageReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeWebsocketClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeWebsocketClient) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ types.WebsocketClient = new(FakeWebsocketClient)

View File

@@ -0,0 +1,126 @@
// 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 rtc
import (
"maps"
"slices"
"sync"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/datatrack"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
type UpDataTrackManagerParams struct {
Logger logger.Logger
Participant types.Participant
}
type UpDataTrackManager struct {
params UpDataTrackManagerParams
lock sync.RWMutex
dataTracks map[uint16]types.DataTrack
closed bool
}
func NewUpDataTrackManager(params UpDataTrackManagerParams) *UpDataTrackManager {
return &UpDataTrackManager{
params: params,
dataTracks: make(map[uint16]types.DataTrack),
}
}
func (u *UpDataTrackManager) Close() {
u.lock.Lock()
if u.closed {
u.lock.Unlock()
return
}
u.closed = true
dataTracks := u.dataTracks
u.dataTracks = make(map[uint16]types.DataTrack)
u.lock.Unlock()
for _, t := range dataTracks {
t.Close()
}
}
func (u *UpDataTrackManager) AddPublishedDataTrack(dt types.DataTrack) {
u.lock.Lock()
u.dataTracks[dt.PubHandle()] = dt
u.lock.Unlock()
u.params.Participant.GetParticipantListener().OnDataTrackPublished(u.params.Participant, dt)
}
func (u *UpDataTrackManager) RemovePublishedDataTrack(dt types.DataTrack) {
var found bool
pubHandle := dt.PubHandle()
u.lock.Lock()
if u.dataTracks[pubHandle] == dt {
delete(u.dataTracks, pubHandle)
found = true
}
u.lock.Unlock()
if found {
dt.Close()
u.params.Participant.GetParticipantListener().OnDataTrackUnpublished(u.params.Participant, dt)
}
}
func (u *UpDataTrackManager) GetPublishedDataTracks() []types.DataTrack {
u.lock.RLock()
defer u.lock.RUnlock()
return slices.Collect(maps.Values(u.dataTracks))
}
func (u *UpDataTrackManager) GetPublishedDataTrack(handle uint16) types.DataTrack {
u.lock.RLock()
defer u.lock.RUnlock()
return u.dataTracks[handle]
}
func (u *UpDataTrackManager) HandleReceivedDataTrackMessage(data []byte, packet *datatrack.Packet, arrivalTime int64) {
u.lock.RLock()
dt := u.dataTracks[packet.Handle]
u.lock.RUnlock()
if dt == nil {
return
}
dt.HandlePacket(data, packet, arrivalTime)
}
func (u *UpDataTrackManager) ToProto() []*livekit.DataTrackInfo {
u.lock.RLock()
defer u.lock.RUnlock()
var dataTrackInfos []*livekit.DataTrackInfo
for _, dt := range u.dataTracks {
dataTrackInfos = append(dataTrackInfos, dt.ToProto())
}
return dataTrackInfos
}

459
pkg/rtc/uptrackmanager.go Normal file
View File

@@ -0,0 +1,459 @@
// 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 rtc
import (
"errors"
"maps"
"slices"
"sync"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/protocol/utils"
)
var (
ErrSubscriptionPermissionNeedsId = errors.New("either participant identity or SID needed")
)
type UpTrackManagerParams struct {
Logger logger.Logger
VersionGenerator utils.TimedVersionGenerator
}
// UpTrackManager manages all uptracks from a participant
type UpTrackManager struct {
// utils.TimedVersion is a atomic. To be correctly aligned also on 32bit archs
// 64it atomics need to be at the front of a struct
subscriptionPermissionVersion utils.TimedVersion
params UpTrackManagerParams
closed bool
// publishedTracks that participant is publishing
publishedTracks map[livekit.TrackID]types.MediaTrack
subscriptionPermission *livekit.SubscriptionPermission
// subscriber permission for published tracks (both media and data)
subscriberPermissions map[livekit.ParticipantIdentity]*livekit.TrackPermission // subscriberIdentity => *livekit.TrackPermission
lock sync.RWMutex
// callbacks & handlers
onClose func()
onTrackUpdated func(track types.MediaTrack)
}
func NewUpTrackManager(params UpTrackManagerParams) *UpTrackManager {
return &UpTrackManager{
params: params,
publishedTracks: make(map[livekit.TrackID]types.MediaTrack),
}
}
func (u *UpTrackManager) Close(isExpectedToResume bool) {
u.lock.Lock()
if u.closed {
u.lock.Unlock()
return
}
u.closed = true
publishedTracks := u.publishedTracks
u.publishedTracks = make(map[livekit.TrackID]types.MediaTrack)
u.lock.Unlock()
for _, t := range publishedTracks {
t.Close(isExpectedToResume)
}
if onClose := u.getOnUpTrackManagerClose(); onClose != nil {
onClose()
}
}
func (u *UpTrackManager) OnUpTrackManagerClose(f func()) {
u.lock.Lock()
u.onClose = f
u.lock.Unlock()
}
func (u *UpTrackManager) getOnUpTrackManagerClose() func() {
u.lock.RLock()
defer u.lock.RUnlock()
return u.onClose
}
func (u *UpTrackManager) ToProto() []*livekit.TrackInfo {
u.lock.RLock()
defer u.lock.RUnlock()
var trackInfos []*livekit.TrackInfo
for _, t := range u.publishedTracks {
trackInfos = append(trackInfos, t.ToProto())
}
return trackInfos
}
func (u *UpTrackManager) OnPublishedTrackUpdated(f func(track types.MediaTrack)) {
u.onTrackUpdated = f
}
func (u *UpTrackManager) SetPublishedTrackMuted(trackID livekit.TrackID, muted bool) (types.MediaTrack, bool) {
changed := false
track := u.GetPublishedTrack(trackID)
if track != nil {
currentMuted := track.IsMuted()
track.SetMuted(muted)
if currentMuted != track.IsMuted() {
changed = true
u.params.Logger.Debugw("publisher mute status changed", "trackID", trackID, "muted", track.IsMuted())
if u.onTrackUpdated != nil {
u.onTrackUpdated(track)
}
}
}
return track, changed
}
func (u *UpTrackManager) GetPublishedTrack(trackID livekit.TrackID) types.MediaTrack {
u.lock.RLock()
defer u.lock.RUnlock()
return u.getPublishedTrackLocked(trackID)
}
func (u *UpTrackManager) GetPublishedTracks() []types.MediaTrack {
u.lock.RLock()
defer u.lock.RUnlock()
return slices.Collect(maps.Values(u.publishedTracks))
}
func (u *UpTrackManager) UpdateSubscriptionPermission(
subscriptionPermission *livekit.SubscriptionPermission,
timedVersion utils.TimedVersion,
resolverBySid func(participantID livekit.ParticipantID) types.LocalParticipant,
) error {
u.lock.Lock()
if !timedVersion.IsZero() {
// it's possible for permission updates to come from another node. In that case
// they would be the authority for this participant's permissions
// we do not want to initialize subscriptionPermissionVersion too early since if another machine is the
// owner for the data, we'd prefer to use their TimedVersion
// ignore older version
if !timedVersion.After(u.subscriptionPermissionVersion) {
u.params.Logger.Debugw(
"skipping older subscription permission version",
"existingValue", logger.Proto(u.subscriptionPermission),
"existingVersion", &u.subscriptionPermissionVersion,
"requestingValue", logger.Proto(subscriptionPermission),
"requestingVersion", &timedVersion,
)
u.lock.Unlock()
return nil
}
u.subscriptionPermissionVersion.Update(timedVersion)
} else {
// for requests coming from the current node, use local versions
u.subscriptionPermissionVersion.Update(u.params.VersionGenerator.Next())
}
// store as is for use when migrating
u.subscriptionPermission = subscriptionPermission
if subscriptionPermission == nil {
u.params.Logger.Debugw(
"updating subscription permission, setting to nil",
"version", u.subscriptionPermissionVersion,
)
// possible to get a nil when migrating
u.lock.Unlock()
return nil
}
u.params.Logger.Debugw(
"updating subscription permission",
"permissions", logger.Proto(u.subscriptionPermission),
"version", u.subscriptionPermissionVersion,
)
if err := u.parseSubscriptionPermissionsLocked(subscriptionPermission, func(pID livekit.ParticipantID) types.LocalParticipant {
u.lock.Unlock()
var p types.LocalParticipant
if resolverBySid != nil {
p = resolverBySid(pID)
}
u.lock.Lock()
return p
}); err != nil {
// when failed, do not override previous permissions
u.params.Logger.Errorw("failed updating subscription permission", err)
u.lock.Unlock()
return err
}
u.lock.Unlock()
u.maybeRevokeSubscriptions()
return nil
}
func (u *UpTrackManager) SubscriptionPermission() (*livekit.SubscriptionPermission, utils.TimedVersion) {
u.lock.RLock()
defer u.lock.RUnlock()
if u.subscriptionPermissionVersion.IsZero() {
return nil, u.subscriptionPermissionVersion.Load()
}
return u.subscriptionPermission, u.subscriptionPermissionVersion.Load()
}
func (u *UpTrackManager) HasPermission(trackID livekit.TrackID, subIdentity livekit.ParticipantIdentity) bool {
u.lock.RLock()
defer u.lock.RUnlock()
return u.hasPermissionLocked(trackID, subIdentity)
}
func (u *UpTrackManager) GetAllowedSubscribers(trackID livekit.TrackID) []livekit.ParticipantIdentity {
u.lock.RLock()
defer u.lock.RUnlock()
return u.getAllowedSubscribersLocked(trackID)
}
func (u *UpTrackManager) UpdatePublishedAudioTrack(update *livekit.UpdateLocalAudioTrack) types.MediaTrack {
track := u.GetPublishedTrack(livekit.TrackID(update.TrackSid))
if track != nil {
track.UpdateAudioTrack(update)
if u.onTrackUpdated != nil {
u.onTrackUpdated(track)
}
}
return track
}
func (u *UpTrackManager) UpdatePublishedVideoTrack(update *livekit.UpdateLocalVideoTrack) types.MediaTrack {
track := u.GetPublishedTrack(livekit.TrackID(update.TrackSid))
if track != nil {
track.UpdateVideoTrack(update)
if u.onTrackUpdated != nil {
u.onTrackUpdated(track)
}
}
return track
}
func (u *UpTrackManager) AddPublishedTrack(track types.MediaTrack) {
u.lock.Lock()
if _, ok := u.publishedTracks[track.ID()]; !ok {
u.publishedTracks[track.ID()] = track
}
u.lock.Unlock()
u.params.Logger.Debugw("added published track", "trackID", track.ID(), "trackInfo", logger.Proto(track.ToProto()))
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
u.lock.Unlock()
})
}
func (u *UpTrackManager) RemovePublishedTrack(track types.MediaTrack, isExpectedToResume bool) {
track.Close(isExpectedToResume)
u.lock.Lock()
delete(u.publishedTracks, track.ID())
u.lock.Unlock()
}
func (u *UpTrackManager) getPublishedTrackLocked(trackID livekit.TrackID) types.MediaTrack {
return u.publishedTracks[trackID]
}
func (u *UpTrackManager) parseSubscriptionPermissionsLocked(
subscriptionPermission *livekit.SubscriptionPermission,
resolver func(participantID livekit.ParticipantID) types.LocalParticipant,
) error {
// every update overrides the existing
// all_participants takes precedence
if subscriptionPermission.AllParticipants {
// everything is allowed, nothing else to do
u.subscriberPermissions = nil
return nil
}
// per participant permissions
subscriberPermissions := make(map[livekit.ParticipantIdentity]*livekit.TrackPermission)
for _, trackPerms := range subscriptionPermission.TrackPermissions {
subscriberIdentity := livekit.ParticipantIdentity(trackPerms.ParticipantIdentity)
if subscriberIdentity == "" {
if trackPerms.ParticipantSid == "" {
return ErrSubscriptionPermissionNeedsId
}
sub := resolver(livekit.ParticipantID(trackPerms.ParticipantSid))
if sub == nil {
u.params.Logger.Warnw(
"could not find subscriber for permissions update", nil,
"subscriberID", trackPerms.ParticipantSid,
"subscriptionPermission", logger.Proto(subscriptionPermission),
)
continue
}
subscriberIdentity = sub.Identity()
} else {
if trackPerms.ParticipantSid != "" {
sub := resolver(livekit.ParticipantID(trackPerms.ParticipantSid))
if sub != nil && sub.Identity() != subscriberIdentity {
u.params.Logger.Errorw(
"participant identity mismatch", nil,
"expected", subscriberIdentity,
"got", sub.Identity(),
"subscriptionPermission", logger.Proto(subscriptionPermission),
)
}
if sub == nil {
u.params.Logger.Warnw(
"could not find subscriber for permissions update", nil,
"subscriberID", trackPerms.ParticipantSid,
"subscriptionPermission", logger.Proto(subscriptionPermission),
)
}
}
}
subscriberPermissions[subscriberIdentity] = trackPerms
}
u.subscriberPermissions = subscriberPermissions
return nil
}
func (u *UpTrackManager) hasPermissionLocked(trackID livekit.TrackID, subscriberIdentity livekit.ParticipantIdentity) bool {
if u.subscriberPermissions == nil {
return true
}
perms, ok := u.subscriberPermissions[subscriberIdentity]
if !ok {
return false
}
if perms.AllTracks {
return true
}
for _, sid := range perms.TrackSids {
if livekit.TrackID(sid) == trackID {
return true
}
}
return false
}
// returns a list of participants that are allowed to subscribe to the track. if nil is returned, it means everyone is
// allowed to subscribe to this track
func (u *UpTrackManager) getAllowedSubscribersLocked(trackID livekit.TrackID) []livekit.ParticipantIdentity {
if u.subscriberPermissions == nil {
return nil
}
allowed := make([]livekit.ParticipantIdentity, 0)
for subscriberIdentity, perms := range u.subscriberPermissions {
if perms.AllTracks {
allowed = append(allowed, subscriberIdentity)
continue
}
for _, sid := range perms.TrackSids {
if livekit.TrackID(sid) == trackID {
allowed = append(allowed, subscriberIdentity)
break
}
}
}
return allowed
}
func (u *UpTrackManager) maybeRevokeSubscriptions() {
u.lock.Lock()
defer u.lock.Unlock()
for trackID, track := range u.publishedTracks {
allowed := u.getAllowedSubscribersLocked(trackID)
if allowed == nil {
// no restrictions
continue
}
track.RevokeDisallowedSubscribers(allowed)
}
}
func (u *UpTrackManager) DebugInfo() map[string]any {
info := map[string]any{}
publishedTrackInfo := make(map[livekit.TrackID]any)
u.lock.RLock()
for trackID, track := range u.publishedTracks {
if mt, ok := track.(*MediaTrack); ok {
publishedTrackInfo[trackID] = mt.DebugInfo()
} else {
publishedTrackInfo[trackID] = map[string]any{
"ID": track.ID(),
"Kind": track.Kind().String(),
"PubMuted": track.IsMuted(),
}
}
}
u.lock.RUnlock()
info["PublishedTracks"] = publishedTrackInfo
return info
}
func (u *UpTrackManager) GetAudioLevel() (level float64, active bool) {
level = 0
for _, pt := range u.GetPublishedTracks() {
if pt.Source() == livekit.TrackSource_MICROPHONE {
tl, ta := pt.GetAudioLevel()
if ta {
active = true
if tl > level {
level = tl
}
}
}
}
return
}

View File

@@ -0,0 +1,329 @@
// 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 rtc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
)
var defaultUptrackManagerParams = UpTrackManagerParams{
Logger: logger.GetLogger(),
VersionGenerator: utils.NewDefaultTimedVersionGenerator(),
}
func TestUpdateSubscriptionPermission(t *testing.T) {
t.Run("updates subscription permission", func(t *testing.T) {
um := NewUpTrackManager(defaultUptrackManagerParams)
vg := utils.NewDefaultTimedVersionGenerator()
tra := &typesfakes.FakeMediaTrack{}
tra.IDReturns("audio")
um.publishedTracks["audio"] = tra
trv := &typesfakes.FakeMediaTrack{}
trv.IDReturns("video")
um.publishedTracks["video"] = trv
// no restrictive subscription permission
subscriptionPermission := &livekit.SubscriptionPermission{
AllParticipants: true,
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.Nil(t, um.subscriberPermissions)
// nobody is allowed to subscribe
subscriptionPermission = &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{},
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.NotNil(t, um.subscriberPermissions)
require.Equal(t, 0, len(um.subscriberPermissions))
lp1 := &typesfakes.FakeLocalParticipant{}
lp1.IdentityReturns("p1")
lp2 := &typesfakes.FakeLocalParticipant{}
lp2.IdentityReturns("p2")
sidResolver := func(sid livekit.ParticipantID) types.LocalParticipant {
if sid == "p1" {
return lp1
}
if sid == "p2" {
return lp2
}
return nil
}
// allow all tracks for participants
perms1 := &livekit.TrackPermission{
ParticipantSid: "p1",
AllTracks: true,
}
perms2 := &livekit.TrackPermission{
ParticipantSid: "p2",
AllTracks: true,
}
subscriptionPermission = &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{
perms1,
perms2,
},
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), sidResolver)
require.Equal(t, 2, len(um.subscriberPermissions))
require.EqualValues(t, perms1, um.subscriberPermissions["p1"])
require.EqualValues(t, perms2, um.subscriberPermissions["p2"])
// allow all tracks for some and restrictive for others
perms1 = &livekit.TrackPermission{
ParticipantIdentity: "p1",
AllTracks: true,
}
perms2 = &livekit.TrackPermission{
ParticipantIdentity: "p2",
TrackSids: []string{"audio"},
}
perms3 := &livekit.TrackPermission{
ParticipantIdentity: "p3",
TrackSids: []string{"video"},
}
subscriptionPermission = &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{
perms1,
perms2,
perms3,
},
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.Equal(t, 3, len(um.subscriberPermissions))
require.EqualValues(t, perms1, um.subscriberPermissions["p1"])
require.EqualValues(t, perms2, um.subscriberPermissions["p2"])
require.EqualValues(t, perms3, um.subscriberPermissions["p3"])
})
t.Run("updates subscription permission using both", func(t *testing.T) {
um := NewUpTrackManager(defaultUptrackManagerParams)
vg := utils.NewDefaultTimedVersionGenerator()
tra := &typesfakes.FakeMediaTrack{}
tra.IDReturns("audio")
um.publishedTracks["audio"] = tra
trv := &typesfakes.FakeMediaTrack{}
trv.IDReturns("video")
um.publishedTracks["video"] = trv
lp1 := &typesfakes.FakeLocalParticipant{}
lp1.IdentityReturns("p1")
lp2 := &typesfakes.FakeLocalParticipant{}
lp2.IdentityReturns("p2")
sidResolver := func(sid livekit.ParticipantID) types.LocalParticipant {
if sid == "p1" {
return lp1
}
if sid == "p2" {
return lp2
}
return nil
}
// allow all tracks for participants
perms1 := &livekit.TrackPermission{
ParticipantSid: "p1",
ParticipantIdentity: "p1",
AllTracks: true,
}
perms2 := &livekit.TrackPermission{
ParticipantSid: "p2",
ParticipantIdentity: "p2",
AllTracks: true,
}
subscriptionPermission := &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{
perms1,
perms2,
},
}
err := um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), sidResolver)
require.NoError(t, err)
require.Equal(t, 2, len(um.subscriberPermissions))
require.EqualValues(t, perms1, um.subscriberPermissions["p1"])
require.EqualValues(t, perms2, um.subscriberPermissions["p2"])
// mismatched identities should fail a permission update
badSidResolver := func(sid livekit.ParticipantID) types.LocalParticipant {
if sid == "p1" {
return lp2
}
if sid == "p2" {
return lp1
}
return nil
}
err = um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), badSidResolver)
require.NoError(t, err)
require.Equal(t, 2, len(um.subscriberPermissions))
require.EqualValues(t, perms1, um.subscriberPermissions["p1"])
require.EqualValues(t, perms2, um.subscriberPermissions["p2"])
})
t.Run("update versions", func(t *testing.T) {
um := NewUpTrackManager(defaultUptrackManagerParams)
vg := utils.NewDefaultTimedVersionGenerator()
v0, v1, v2 := vg.Next(), vg.Next(), vg.Next()
um.UpdateSubscriptionPermission(&livekit.SubscriptionPermission{}, v1, nil)
require.Equal(t, v1.Load(), um.subscriptionPermissionVersion.Load(), "first update should be applied")
um.UpdateSubscriptionPermission(&livekit.SubscriptionPermission{}, v2, nil)
require.Equal(t, v2.Load(), um.subscriptionPermissionVersion.Load(), "ordered updates should be applied")
um.UpdateSubscriptionPermission(&livekit.SubscriptionPermission{}, v0, nil)
require.Equal(t, v2.Load(), um.subscriptionPermissionVersion.Load(), "out of order updates should be ignored")
um.UpdateSubscriptionPermission(&livekit.SubscriptionPermission{}, utils.TimedVersion(0), nil)
require.True(t, um.subscriptionPermissionVersion.After(v2), "zero version in updates should use next local version")
})
}
func TestSubscriptionPermission(t *testing.T) {
t.Run("checks subscription permission", func(t *testing.T) {
um := NewUpTrackManager(defaultUptrackManagerParams)
vg := utils.NewDefaultTimedVersionGenerator()
tra := &typesfakes.FakeMediaTrack{}
tra.IDReturns("audio")
um.publishedTracks["audio"] = tra
trv := &typesfakes.FakeMediaTrack{}
trv.IDReturns("video")
um.publishedTracks["video"] = trv
// no restrictive permission
subscriptionPermission := &livekit.SubscriptionPermission{
AllParticipants: true,
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.True(t, um.hasPermissionLocked("audio", "p1"))
require.True(t, um.hasPermissionLocked("audio", "p2"))
// nobody is allowed to subscribe
subscriptionPermission = &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{},
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.False(t, um.hasPermissionLocked("audio", "p1"))
require.False(t, um.hasPermissionLocked("audio", "p2"))
// allow all tracks for participants
subscriptionPermission = &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{
{
ParticipantIdentity: "p1",
AllTracks: true,
},
{
ParticipantIdentity: "p2",
AllTracks: true,
},
},
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.True(t, um.hasPermissionLocked("audio", "p1"))
require.True(t, um.hasPermissionLocked("video", "p1"))
require.True(t, um.hasPermissionLocked("audio", "p2"))
require.True(t, um.hasPermissionLocked("video", "p2"))
// add a new track after permissions are set
trs := &typesfakes.FakeMediaTrack{}
trs.IDReturns("screen")
um.publishedTracks["screen"] = trs
require.True(t, um.hasPermissionLocked("audio", "p1"))
require.True(t, um.hasPermissionLocked("video", "p1"))
require.True(t, um.hasPermissionLocked("screen", "p1"))
require.True(t, um.hasPermissionLocked("audio", "p2"))
require.True(t, um.hasPermissionLocked("video", "p2"))
require.True(t, um.hasPermissionLocked("screen", "p2"))
// allow all tracks for some and restrictive for others
subscriptionPermission = &livekit.SubscriptionPermission{
TrackPermissions: []*livekit.TrackPermission{
{
ParticipantIdentity: "p1",
AllTracks: true,
},
{
ParticipantIdentity: "p2",
TrackSids: []string{"audio"},
},
{
ParticipantIdentity: "p3",
TrackSids: []string{"video"},
},
},
}
um.UpdateSubscriptionPermission(subscriptionPermission, vg.Next(), nil)
require.True(t, um.hasPermissionLocked("audio", "p1"))
require.True(t, um.hasPermissionLocked("video", "p1"))
require.True(t, um.hasPermissionLocked("screen", "p1"))
require.True(t, um.hasPermissionLocked("audio", "p2"))
require.False(t, um.hasPermissionLocked("video", "p2"))
require.False(t, um.hasPermissionLocked("screen", "p2"))
require.False(t, um.hasPermissionLocked("audio", "p3"))
require.True(t, um.hasPermissionLocked("video", "p3"))
require.False(t, um.hasPermissionLocked("screen", "p3"))
// add a new track after restrictive permissions are set
trw := &typesfakes.FakeMediaTrack{}
trw.IDReturns("watch")
um.publishedTracks["watch"] = trw
require.True(t, um.hasPermissionLocked("audio", "p1"))
require.True(t, um.hasPermissionLocked("video", "p1"))
require.True(t, um.hasPermissionLocked("screen", "p1"))
require.True(t, um.hasPermissionLocked("watch", "p1"))
require.True(t, um.hasPermissionLocked("audio", "p2"))
require.False(t, um.hasPermissionLocked("video", "p2"))
require.False(t, um.hasPermissionLocked("screen", "p2"))
require.False(t, um.hasPermissionLocked("watch", "p2"))
require.False(t, um.hasPermissionLocked("audio", "p3"))
require.True(t, um.hasPermissionLocked("video", "p3"))
require.False(t, um.hasPermissionLocked("screen", "p3"))
require.False(t, um.hasPermissionLocked("watch", "p3"))
})
}

View File

@@ -0,0 +1,66 @@
// 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 rtc
import (
"sync"
"github.com/google/uuid"
"github.com/livekit/protocol/livekit"
)
const (
maxSize = 100
)
type UserPacketDeduper struct {
lock sync.Mutex
seen map[uuid.UUID]uuid.UUID
head uuid.UUID
tail uuid.UUID
}
func NewUserPacketDeduper() *UserPacketDeduper {
return &UserPacketDeduper{
seen: make(map[uuid.UUID]uuid.UUID),
}
}
func (u *UserPacketDeduper) IsDuplicate(up *livekit.UserPacket) bool {
id, err := uuid.FromBytes(up.Nonce)
if err != nil {
return false
}
u.lock.Lock()
defer u.lock.Unlock()
if u.head == id {
return true
}
if _, ok := u.seen[id]; ok {
return true
}
u.seen[u.head] = id
u.head = id
if len(u.seen) == maxSize {
tail := u.tail
u.tail = u.seen[tail]
delete(u.seen, tail)
}
return false
}

207
pkg/rtc/utils.go Normal file
View File

@@ -0,0 +1,207 @@
// 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 rtc
import (
"errors"
"io"
"net"
"strings"
"github.com/pion/webrtc/v4"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
const (
trackIdSeparator = "|"
cMinIPTruncateLen = 8
)
func UnpackStreamID(packed string) (participantID livekit.ParticipantID, trackID livekit.TrackID) {
parts := strings.Split(packed, trackIdSeparator)
if len(parts) > 1 {
return livekit.ParticipantID(parts[0]), livekit.TrackID(packed[len(parts[0])+1:])
}
return livekit.ParticipantID(packed), ""
}
func PackStreamID(participantID livekit.ParticipantID, trackID livekit.TrackID) string {
return string(participantID) + trackIdSeparator + string(trackID)
}
func PackSyncStreamID(participantID livekit.ParticipantID, stream string) string {
return string(participantID) + trackIdSeparator + stream
}
func StreamFromTrackSource(source livekit.TrackSource) string {
// group camera/mic, screenshare/audio together
switch source {
case livekit.TrackSource_SCREEN_SHARE:
return "screen"
case livekit.TrackSource_SCREEN_SHARE_AUDIO:
return "screen"
case livekit.TrackSource_CAMERA:
return "camera"
case livekit.TrackSource_MICROPHONE:
return "camera"
}
return "unknown"
}
func PackDataTrackLabel(participantID livekit.ParticipantID, trackID livekit.TrackID, label string) string {
return string(participantID) + trackIdSeparator + string(trackID) + trackIdSeparator + label
}
func UnpackDataTrackLabel(packed string) (participantID livekit.ParticipantID, trackID livekit.TrackID, label string) {
parts := strings.Split(packed, trackIdSeparator)
if len(parts) != 3 {
return "", livekit.TrackID(packed), ""
}
participantID = livekit.ParticipantID(parts[0])
trackID = livekit.TrackID(parts[1])
label = parts[2]
return
}
func ToProtoTrackKind(kind webrtc.RTPCodecType) livekit.TrackType {
switch kind {
case webrtc.RTPCodecTypeVideo:
return livekit.TrackType_VIDEO
case webrtc.RTPCodecTypeAudio:
return livekit.TrackType_AUDIO
}
panic("unsupported track direction")
}
func IsEOF(err error) bool {
return err == io.ErrClosedPipe || err == io.EOF
}
func Recover(l logger.Logger) any {
if l == nil {
l = logger.GetLogger()
}
r := recover()
if r != nil {
var err error
switch e := r.(type) {
case string:
err = errors.New(e)
case error:
err = e
default:
err = errors.New("unknown panic")
}
l.Errorw("recovered panic", err, "panic", r)
}
return r
}
// logger helpers
func LoggerWithParticipant(l logger.Logger, identity livekit.ParticipantIdentity, sid livekit.ParticipantID, isRemote bool) logger.Logger {
values := make([]any, 0, 4)
if identity != "" {
values = append(values, "participant", identity)
}
if sid != "" {
values = append(values, "participantID", sid)
}
values = append(values, "remote", isRemote)
// enable sampling per participant
return l.WithValues(values...)
}
func LoggerWithRoom(l logger.Logger, name livekit.RoomName, roomID livekit.RoomID) logger.Logger {
values := make([]any, 0, 2)
if name != "" {
values = append(values, "room", name)
}
if roomID != "" {
values = append(values, "roomID", roomID)
}
// also sample for the room
return l.WithItemSampler().WithValues(values...)
}
func LoggerWithTrack(l logger.Logger, trackID livekit.TrackID, isRelayed bool) logger.Logger {
// sampling not required because caller already passing in participant's logger
if trackID != "" {
return l.WithValues("trackID", trackID, "relayed", isRelayed)
}
return l
}
func LoggerWithPCTarget(l logger.Logger, target livekit.SignalTarget) logger.Logger {
return l.WithValues("transport", target)
}
func LoggerWithCodecMime(l logger.Logger, mimeType mime.MimeType) logger.Logger {
if mimeType != mime.MimeTypeUnknown {
return l.WithValues("mime", mimeType.String())
}
return l
}
func MaybeTruncateIP(addr string) string {
ipAddr := net.ParseIP(addr)
if ipAddr == nil {
return ""
}
if ipAddr.IsPrivate() || len(addr) <= cMinIPTruncateLen {
return addr
}
return addr[:len(addr)-3] + "..."
}
func IsIPv6(addr string) bool {
ipAddr := net.ParseIP(addr)
return ipAddr != nil && ipAddr.To4() == nil
}
func ChunkProtoBatch[T proto.Message](batch []T, target int) [][]T {
var chunks [][]T
var start, size int
for i, m := range batch {
s := proto.Size(m)
if size+s > target {
if start < i {
chunks = append(chunks, batch[start:i])
}
start = i
size = 0
}
size += s
}
if start < len(batch) {
chunks = append(chunks, batch[start:])
}
return chunks
}
func IsRedEnabled(ti *livekit.TrackInfo) bool {
if len(ti.Codecs) != 0 && ti.Codecs[0].MimeType != "" {
return mime.IsMimeTypeStringRED(ti.Codecs[0].MimeType)
}
return !ti.GetDisableRed()
}

75
pkg/rtc/utils_test.go Normal file
View File

@@ -0,0 +1,75 @@
// 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 rtc
import (
"math/rand/v2"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils/guid"
)
func TestPackStreamId(t *testing.T) {
packed := "PA_123abc|uuid-id"
pID, trackID := UnpackStreamID(packed)
require.Equal(t, livekit.ParticipantID("PA_123abc"), pID)
require.Equal(t, livekit.TrackID("uuid-id"), trackID)
require.Equal(t, packed, PackStreamID(pID, trackID))
}
func TestPackDataTrackLabel(t *testing.T) {
pID := livekit.ParticipantID("PA_123abc")
trackID := livekit.TrackID("TR_b3da25")
label := "trackLabel"
packed := "PA_123abc|TR_b3da25|trackLabel"
require.Equal(t, packed, PackDataTrackLabel(pID, trackID, label))
p, tr, l := UnpackDataTrackLabel(packed)
require.Equal(t, pID, p)
require.Equal(t, trackID, tr)
require.Equal(t, label, l)
}
func TestChunkProtoBatch(t *testing.T) {
rng := rand.New(rand.NewPCG(1, 2))
var updates []*livekit.ParticipantInfo
for range 32 {
updates = append(updates, &livekit.ParticipantInfo{
Sid: guid.New(guid.ParticipantPrefix),
Identity: uuid.NewString(),
Metadata: strings.Repeat("x", rng.IntN(128*1024)),
})
}
target := 64 * 1024
batches := ChunkProtoBatch(updates, target)
var count int
for _, b := range batches {
var sum int
for _, m := range b {
sum += proto.Size(m)
count++
}
require.True(t, sum < target || len(b) == 1, "batch size exceeds target")
}
require.Equal(t, len(updates), count)
}

576
pkg/rtc/wrappedreceiver.go Normal file
View File

@@ -0,0 +1,576 @@
// 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 rtc
import (
"errors"
"maps"
"slices"
"sync"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"github.com/livekit/mediatransportutil/pkg/codec"
protoCodecs "github.com/livekit/protocol/codecs"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
)
// wrapper around WebRTC receiver, overriding its ID
type WrappedReceiverParams struct {
Receivers []*simulcastReceiver
TrackID livekit.TrackID
StreamId string
UpstreamCodecs []webrtc.RTPCodecParameters
Logger logger.Logger
DisableRed bool
IsEncrypted bool
}
type WrappedReceiver struct {
lock sync.Mutex
sfu.TrackReceiver
params WrappedReceiverParams
receivers []sfu.TrackReceiver
codecs []webrtc.RTPCodecParameters
onReadyCallbacks []func()
}
func NewWrappedReceiver(params WrappedReceiverParams) *WrappedReceiver {
sfuReceivers := make([]sfu.TrackReceiver, 0, len(params.Receivers))
for _, r := range params.Receivers {
sfuReceivers = append(sfuReceivers, r)
}
codecs := params.UpstreamCodecs
if len(codecs) == 1 {
normalizedMimeType := mime.NormalizeMimeType(codecs[0].MimeType)
if normalizedMimeType == mime.MimeTypeRED {
// if upstream is opus/red, then add opus to match clients that don't support red
codecs = append(codecs, protoCodecs.OpusCodecParameters)
} else if !params.DisableRed && normalizedMimeType == mime.MimeTypeOpus {
// if upstream is opus only and red enabled, add red to match clients that support red
codecs = append(codecs, protoCodecs.RedCodecParameters)
// prefer red codec
codecs[0], codecs[1] = codecs[1], codecs[0]
}
}
return &WrappedReceiver{
params: params,
receivers: sfuReceivers,
codecs: codecs,
}
}
func (r *WrappedReceiver) TrackID() livekit.TrackID {
return r.params.TrackID
}
func (r *WrappedReceiver) StreamID() string {
return r.params.StreamId
}
// DetermineReceiver determines the receiver of negotiated codec and returns
//
// isAvailable: returns true if given codec is a potential codec from publisher or if an existing published codec can be translated
// needsPublish: indicates if the codec is needed from publisher, some combinations can be achieved via codec translation internally,
//
// example: unencrypted opus -> RED translation and vice-versa can be done without the need for publisher to send the other codec.
func (r *WrappedReceiver) DetermineReceiver(codec webrtc.RTPCodecCapability) (isAvailable bool, needsPublish bool) {
r.lock.Lock()
reason := "no matching receiver"
codecMimeType := mime.NormalizeMimeType(codec.MimeType)
var trackReceiver sfu.TrackReceiver
for _, receiver := range r.receivers {
receiverMimeType := receiver.Mime()
if receiverMimeType == codecMimeType {
trackReceiver = receiver
isAvailable = true
needsPublish = true
break
}
if receiverMimeType == mime.MimeTypeRED && codecMimeType == mime.MimeTypeOpus {
// audio opus/red can match opus only
if !r.params.IsEncrypted { // cannot match encrypted source
trackReceiver = receiver.GetPrimaryReceiverForRed()
isAvailable = true
break
} else {
reason = "encrypted source"
}
} else if receiverMimeType == mime.MimeTypeOpus && codecMimeType == mime.MimeTypeRED {
if !r.params.IsEncrypted { // cannot match encrypted source
trackReceiver = receiver.GetRedReceiver()
isAvailable = true
break
} else {
reason = "encrypted source"
}
}
}
if trackReceiver == nil {
r.lock.Unlock()
r.params.Logger.Warnw(
"can't determine receiver for codec", nil,
"codec", codec.MimeType,
"reason", reason,
)
return
}
r.TrackReceiver = trackReceiver
onReadyCallbacks := r.onReadyCallbacks
r.onReadyCallbacks = nil
r.lock.Unlock()
for _, f := range onReadyCallbacks {
trackReceiver.AddOnReady(f)
}
return
}
func (r *WrappedReceiver) Codecs() []webrtc.RTPCodecParameters {
return slices.Clone(r.codecs)
}
func (r *WrappedReceiver) DeleteDownTrack(participantID livekit.ParticipantID) {
r.lock.Lock()
trackReceiver := r.TrackReceiver
r.lock.Unlock()
if trackReceiver != nil {
trackReceiver.DeleteDownTrack(participantID)
}
}
func (r *WrappedReceiver) AddOnReady(f func()) {
r.lock.Lock()
trackReceiver := r.TrackReceiver
if trackReceiver == nil {
r.onReadyCallbacks = append(r.onReadyCallbacks, f)
r.lock.Unlock()
return
}
r.lock.Unlock()
trackReceiver.AddOnReady(f)
}
// --------------------------------------------
type DummyReceiver struct {
receiver atomic.Value
trackInfo *livekit.TrackInfo
streamId string
codec webrtc.RTPCodecParameters
headerExtensions []webrtc.RTPHeaderExtensionParameter
downTrackLock sync.Mutex
downTracks map[livekit.ParticipantID]sfu.TrackSender
onReadyCallbacks []func()
onCodecStateChange []func(webrtc.RTPCodecParameters, sfu.ReceiverCodecState)
settingsLock sync.Mutex
maxExpectedLayerValid bool
maxExpectedLayer int32
redReceiver, primaryReceiver *DummyRedReceiver
}
func NewDummyReceiver(
trackInfo *livekit.TrackInfo,
streamId string,
codec webrtc.RTPCodecParameters,
headerExtensions []webrtc.RTPHeaderExtensionParameter,
) *DummyReceiver {
return &DummyReceiver{
trackInfo: trackInfo,
streamId: streamId,
codec: codec,
headerExtensions: headerExtensions,
downTracks: make(map[livekit.ParticipantID]sfu.TrackSender),
}
}
func (d *DummyReceiver) Receiver() sfu.TrackReceiver {
r, _ := d.receiver.Load().(sfu.TrackReceiver)
return r
}
func (d *DummyReceiver) Upgrade(receiver sfu.TrackReceiver) {
if !d.receiver.CompareAndSwap(nil, receiver) {
return
}
d.downTrackLock.Lock()
for _, t := range d.downTracks {
receiver.AddDownTrack(t)
}
d.downTracks = make(map[livekit.ParticipantID]sfu.TrackSender)
onReadyCallbacks := d.onReadyCallbacks
d.onReadyCallbacks = nil
codecChange := d.onCodecStateChange
d.onCodecStateChange = nil
d.downTrackLock.Unlock()
for _, f := range onReadyCallbacks {
receiver.AddOnReady(f)
}
for _, f := range codecChange {
receiver.AddOnCodecStateChange(f)
}
d.settingsLock.Lock()
maxExpectedLayerValid := d.maxExpectedLayerValid
d.maxExpectedLayerValid = false
d.settingsLock.Unlock()
if maxExpectedLayerValid {
receiver.SetMaxExpectedSpatialLayer(d.maxExpectedLayer)
}
d.settingsLock.Lock()
if d.primaryReceiver != nil {
d.primaryReceiver.upgrade(receiver)
}
if d.redReceiver != nil {
d.redReceiver.upgrade(receiver)
}
d.settingsLock.Unlock()
}
func (d *DummyReceiver) TrackID() livekit.TrackID {
return livekit.TrackID(d.trackInfo.Sid)
}
func (d *DummyReceiver) StreamID() string {
return d.streamId
}
func (d *DummyReceiver) Codec() webrtc.RTPCodecParameters {
if receiver := d.getReceiver(); receiver != nil {
return receiver.Codec()
}
return d.codec
}
func (d *DummyReceiver) Mime() mime.MimeType {
if receiver := d.getReceiver(); receiver != nil {
return receiver.Mime()
}
return mime.NormalizeMimeType(d.codec.MimeType)
}
func (d *DummyReceiver) VideoLayerMode() livekit.VideoLayer_Mode {
if receiver := d.getReceiver(); receiver != nil {
return receiver.VideoLayerMode()
}
return buffer.GetVideoLayerModeForMimeType(d.Mime(), d.TrackInfo())
}
func (d *DummyReceiver) HeaderExtensions() []webrtc.RTPHeaderExtensionParameter {
if receiver := d.getReceiver(); receiver != nil {
return receiver.HeaderExtensions()
}
return d.headerExtensions
}
func (d *DummyReceiver) ReadRTP(buf []byte, layer uint8, esn uint64) (int, error) {
if receiver := d.getReceiver(); receiver != nil {
return receiver.ReadRTP(buf, layer, esn)
}
return 0, errors.New("no receiver")
}
func (d *DummyReceiver) GetLayeredBitrate() ([]int32, sfu.Bitrates) {
if receiver := d.getReceiver(); receiver != nil {
return receiver.GetLayeredBitrate()
}
return nil, sfu.Bitrates{}
}
func (d *DummyReceiver) GetAudioLevel() (float64, bool) {
if receiver := d.getReceiver(); receiver != nil {
return receiver.GetAudioLevel()
}
return 0, false
}
func (d *DummyReceiver) SendPLI(layer int32, force bool) {
if receiver := d.getReceiver(); receiver != nil {
receiver.SendPLI(layer, force)
}
}
func (d *DummyReceiver) SetMaxExpectedSpatialLayer(layer int32) {
d.settingsLock.Lock()
receiver := d.getReceiver()
if receiver != nil {
d.maxExpectedLayerValid = false
} else {
d.maxExpectedLayerValid = true
d.maxExpectedLayer = layer
}
d.settingsLock.Unlock()
if receiver != nil {
receiver.SetMaxExpectedSpatialLayer(layer)
}
}
func (d *DummyReceiver) AddDownTrack(track sfu.TrackSender) error {
d.downTrackLock.Lock()
defer d.downTrackLock.Unlock()
if receiver := d.getReceiver(); receiver != nil {
receiver.AddDownTrack(track)
} else {
d.downTracks[track.SubscriberID()] = track
}
return nil
}
func (d *DummyReceiver) DeleteDownTrack(subscriberID livekit.ParticipantID) {
d.downTrackLock.Lock()
defer d.downTrackLock.Unlock()
if receiver := d.getReceiver(); receiver != nil {
receiver.DeleteDownTrack(subscriberID)
} else {
delete(d.downTracks, subscriberID)
}
}
func (d *DummyReceiver) GetDownTracks() []sfu.TrackSender {
d.downTrackLock.Lock()
defer d.downTrackLock.Unlock()
if receiver := d.getReceiver(); receiver != nil {
return receiver.GetDownTracks()
}
return slices.Collect(maps.Values(d.downTracks))
}
func (d *DummyReceiver) DebugInfo() map[string]any {
if receiver := d.getReceiver(); receiver != nil {
return receiver.DebugInfo()
}
return nil
}
func (d *DummyReceiver) GetTemporalLayerFpsForSpatial(spatial int32) []float32 {
if receiver := d.getReceiver(); receiver != nil {
return receiver.GetTemporalLayerFpsForSpatial(spatial)
}
return nil
}
func (d *DummyReceiver) TrackInfo() *livekit.TrackInfo {
if receiver := d.getReceiver(); receiver != nil {
return receiver.TrackInfo()
}
return d.trackInfo
}
func (d *DummyReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
if receiver := d.getReceiver(); receiver != nil {
receiver.UpdateTrackInfo(ti)
}
}
func (d *DummyReceiver) IsClosed() bool {
if receiver := d.getReceiver(); receiver != nil {
return receiver.IsClosed()
}
return false
}
func (d *DummyReceiver) GetPrimaryReceiverForRed() sfu.TrackReceiver {
d.settingsLock.Lock()
defer d.settingsLock.Unlock()
if d.primaryReceiver == nil {
d.primaryReceiver = NewDummyRedReceiver(d, false)
if r, ok := d.receiver.Load().(sfu.TrackReceiver); ok {
d.primaryReceiver.upgrade(r)
}
}
return d.primaryReceiver
}
func (d *DummyReceiver) GetRedReceiver() sfu.TrackReceiver {
d.settingsLock.Lock()
defer d.settingsLock.Unlock()
if d.redReceiver == nil {
d.redReceiver = NewDummyRedReceiver(d, true)
if r, ok := d.receiver.Load().(sfu.TrackReceiver); ok {
d.redReceiver.upgrade(r)
}
}
return d.redReceiver
}
func (d *DummyReceiver) GetTrackStats() *livekit.RTPStats {
if receiver := d.getReceiver(); receiver != nil {
return receiver.GetTrackStats()
}
return nil
}
func (d *DummyReceiver) AddOnReady(f func()) {
d.downTrackLock.Lock()
receiver := d.getReceiver()
if receiver == nil {
d.onReadyCallbacks = append(d.onReadyCallbacks, f)
}
d.downTrackLock.Unlock()
if receiver != nil {
receiver.AddOnReady(f)
}
}
func (d *DummyReceiver) AddOnCodecStateChange(f func(codec webrtc.RTPCodecParameters, state sfu.ReceiverCodecState)) {
d.downTrackLock.Lock()
receiver := d.getReceiver()
if receiver == nil {
d.onCodecStateChange = append(d.onCodecStateChange, f)
}
d.downTrackLock.Unlock()
if receiver != nil {
receiver.AddOnCodecStateChange(f)
}
}
func (d *DummyReceiver) CodecState() sfu.ReceiverCodecState {
if receiver := d.getReceiver(); receiver != nil {
return receiver.CodecState()
}
return sfu.ReceiverCodecStateNormal
}
func (d *DummyReceiver) VideoSizes() []codec.VideoSize {
if receiver := d.getReceiver(); receiver != nil {
return receiver.VideoSizes()
}
return nil
}
func (d *DummyReceiver) Restart(reason string) {
if receiver := d.getReceiver(); receiver != nil {
receiver.Restart(reason)
}
}
func (d *DummyReceiver) getReceiver() sfu.TrackReceiver {
if receiver, ok := d.receiver.Load().(sfu.TrackReceiver); ok {
return receiver
}
return nil
}
// --------------------------------------------
type DummyRedReceiver struct {
*DummyReceiver
redReceiver atomic.Value // sfu.TrackReceiver
// indicates this receiver is for RED encoding receiver of primary codec OR
// primary decoding receiver of RED codec
isRedEncoding bool
downTrackLock sync.Mutex
downTracks map[livekit.ParticipantID]sfu.TrackSender
}
func NewDummyRedReceiver(d *DummyReceiver, isRedEncoding bool) *DummyRedReceiver {
return &DummyRedReceiver{
DummyReceiver: d,
isRedEncoding: isRedEncoding,
downTracks: make(map[livekit.ParticipantID]sfu.TrackSender),
}
}
func (d *DummyRedReceiver) AddDownTrack(track sfu.TrackSender) error {
d.downTrackLock.Lock()
defer d.downTrackLock.Unlock()
if r, ok := d.redReceiver.Load().(sfu.TrackReceiver); ok {
r.AddDownTrack(track)
} else {
d.downTracks[track.SubscriberID()] = track
}
return nil
}
func (d *DummyRedReceiver) DeleteDownTrack(subscriberID livekit.ParticipantID) {
d.downTrackLock.Lock()
defer d.downTrackLock.Unlock()
if r, ok := d.redReceiver.Load().(sfu.TrackReceiver); ok {
r.DeleteDownTrack(subscriberID)
} else {
delete(d.downTracks, subscriberID)
}
}
func (d *DummyRedReceiver) GetDownTracks() []sfu.TrackSender {
d.downTrackLock.Lock()
defer d.downTrackLock.Unlock()
if r, ok := d.redReceiver.Load().(sfu.TrackReceiver); ok {
return r.GetDownTracks()
}
return slices.Collect(maps.Values(d.downTracks))
}
func (d *DummyRedReceiver) ReadRTP(buf []byte, layer uint8, esn uint64) (int, error) {
if r, ok := d.redReceiver.Load().(sfu.TrackReceiver); ok {
return r.ReadRTP(buf, layer, esn)
}
return 0, errors.New("no receiver")
}
func (d *DummyRedReceiver) upgrade(receiver sfu.TrackReceiver) {
var redReceiver sfu.TrackReceiver
if d.isRedEncoding {
redReceiver = receiver.GetRedReceiver()
} else {
redReceiver = receiver.GetPrimaryReceiverForRed()
}
d.redReceiver.Store(redReceiver)
d.downTrackLock.Lock()
for _, t := range d.downTracks {
redReceiver.AddDownTrack(t)
}
d.downTracks = make(map[livekit.ParticipantID]sfu.TrackSender)
d.downTrackLock.Unlock()
}