snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
This commit is contained in:
158
pkg/rtc/clientinfo.go
Normal file
158
pkg/rtc/clientinfo.go
Normal 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
|
||||
}
|
||||
59
pkg/rtc/clientinfo_test.go
Normal file
59
pkg/rtc/clientinfo_test.go
Normal 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
207
pkg/rtc/config.go
Normal 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
137
pkg/rtc/datadowntrack.go
Normal 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
228
pkg/rtc/datatrack.go
Normal 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
110
pkg/rtc/datatrack_stats.go
Normal 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
71
pkg/rtc/datatrack_test.go
Normal 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()))
|
||||
}
|
||||
523
pkg/rtc/dynacast/dynacastmanager_test.go
Normal file
523
pkg/rtc/dynacast/dynacastmanager_test.go
Normal 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()
|
||||
}
|
||||
198
pkg/rtc/dynacast/dynacastmanageraudio.go
Normal file
198
pkg/rtc/dynacast/dynacastmanageraudio.go
Normal 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)
|
||||
})
|
||||
}
|
||||
165
pkg/rtc/dynacast/dynacastmanagerbase.go
Normal file
165
pkg/rtc/dynacast/dynacastmanagerbase.go
Normal 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))
|
||||
}
|
||||
271
pkg/rtc/dynacast/dynacastmanagervideo.go
Normal file
271
pkg/rtc/dynacast/dynacastmanagervideo.go
Normal 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)
|
||||
})
|
||||
}
|
||||
168
pkg/rtc/dynacast/dynacastqualityaudio.go
Normal file
168
pkg/rtc/dynacast/dynacastqualityaudio.go
Normal 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)
|
||||
}
|
||||
249
pkg/rtc/dynacast/dynacastqualityvideo.go
Normal file
249
pkg/rtc/dynacast/dynacastqualityvideo.go
Normal 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
|
||||
}
|
||||
}
|
||||
185
pkg/rtc/dynacast/interfaces.go
Normal file
185
pkg/rtc/dynacast/interfaces.go
Normal 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
176
pkg/rtc/egress.go
Normal 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
44
pkg/rtc/errors.go
Normal 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
245
pkg/rtc/mediaengine.go
Normal 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
224
pkg/rtc/mediaengine_test.go
Normal 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
105
pkg/rtc/medialossproxy.go
Normal 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
701
pkg/rtc/mediatrack.go
Normal 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
197
pkg/rtc/mediatrack_test.go
Normal 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))
|
||||
})
|
||||
|
||||
}
|
||||
1248
pkg/rtc/mediatrackreceiver.go
Normal file
1248
pkg/rtc/mediatrackreceiver.go
Normal file
File diff suppressed because it is too large
Load Diff
366
pkg/rtc/mediatracksubscriptions.go
Normal file
366
pkg/rtc/mediatracksubscriptions.go
Normal 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
|
||||
}
|
||||
69
pkg/rtc/migrationdatacache.go
Normal file
69
pkg/rtc/migrationdatacache.go
Normal 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
|
||||
}
|
||||
57
pkg/rtc/migrationdatacache_test.go
Normal file
57
pkg/rtc/migrationdatacache_test.go
Normal 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
4570
pkg/rtc/participant.go
Normal file
File diff suppressed because it is too large
Load Diff
90
pkg/rtc/participant_data_blob.go
Normal file
90
pkg/rtc/participant_data_blob.go
Normal 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
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
123
pkg/rtc/participant_data_blob_handler.go
Normal file
123
pkg/rtc/participant_data_blob_handler.go
Normal 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()
|
||||
}
|
||||
300
pkg/rtc/participant_data_blob_handler_test.go
Normal file
300
pkg/rtc/participant_data_blob_handler_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
175
pkg/rtc/participant_data_blob_test.go
Normal file
175
pkg/rtc/participant_data_blob_test.go
Normal 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()
|
||||
}
|
||||
212
pkg/rtc/participant_data_track.go
Normal file
212
pkg/rtc/participant_data_track.go
Normal 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
|
||||
}
|
||||
1049
pkg/rtc/participant_internal_test.go
Normal file
1049
pkg/rtc/participant_internal_test.go
Normal file
File diff suppressed because it is too large
Load Diff
408
pkg/rtc/participant_sdp.go
Normal file
408
pkg/rtc/participant_sdp.go
Normal 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
|
||||
}
|
||||
436
pkg/rtc/participant_signal.go
Normal file
436
pkg/rtc/participant_signal.go
Normal 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
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
869
pkg/rtc/room_test.go
Normal 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
263
pkg/rtc/roomtrackmanager.go
Normal 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)
|
||||
}
|
||||
311
pkg/rtc/signalanddatastats.go
Normal file
311
pkg/rtc/signalanddatastats.go
Normal 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))
|
||||
}
|
||||
27
pkg/rtc/signalling/errors.go
Normal file
27
pkg/rtc/signalling/errors.go
Normal 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")
|
||||
)
|
||||
74
pkg/rtc/signalling/interfaces.go
Normal file
74
pkg/rtc/signalling/interfaces.go
Normal 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
|
||||
}
|
||||
163
pkg/rtc/signalling/signalhandler.go
Normal file
163
pkg/rtc/signalling/signalhandler.go
Normal 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
|
||||
}
|
||||
27
pkg/rtc/signalling/signalhandlerunimplemented.go
Normal file
27
pkg/rtc/signalling/signalhandlerunimplemented.go
Normal 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
|
||||
}
|
||||
140
pkg/rtc/signalling/signallerasync.go
Normal file
140
pkg/rtc/signalling/signallerasync.go
Normal 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
|
||||
}
|
||||
185
pkg/rtc/signalling/signallerasyncbase.go
Normal file
185
pkg/rtc/signalling/signallerasyncbase.go
Normal 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)
|
||||
}
|
||||
157
pkg/rtc/signalling/signallerasyncbase_test.go
Normal file
157
pkg/rtc/signalling/signallerasyncbase_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
45
pkg/rtc/signalling/signallerunimplemented.go
Normal file
45
pkg/rtc/signalling/signallerunimplemented.go
Normal 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
|
||||
}
|
||||
276
pkg/rtc/signalling/signalling.go
Normal file
276
pkg/rtc/signalling/signalling.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
137
pkg/rtc/signalling/signallingunimplemented.go
Normal file
137
pkg/rtc/signalling/signallingunimplemented.go
Normal 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
505
pkg/rtc/subscribedtrack.go
Normal 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)
|
||||
}
|
||||
}
|
||||
1681
pkg/rtc/subscriptionmanager.go
Normal file
1681
pkg/rtc/subscriptionmanager.go
Normal file
File diff suppressed because it is too large
Load Diff
714
pkg/rtc/subscriptionmanager_test.go
Normal file
714
pkg/rtc/subscriptionmanager_test.go
Normal 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)()
|
||||
}
|
||||
183
pkg/rtc/supervisor/participant_supervisor.go
Normal file
183
pkg/rtc/supervisor/participant_supervisor.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
190
pkg/rtc/supervisor/publication_monitor.go
Normal file
190
pkg/rtc/supervisor/publication_monitor.go
Normal 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
88
pkg/rtc/testutils.go
Normal 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
3408
pkg/rtc/transport.go
Normal file
File diff suppressed because it is too large
Load Diff
82
pkg/rtc/transport/handler.go
Normal file
82
pkg/rtc/transport/handler.go
Normal 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
|
||||
}
|
||||
40
pkg/rtc/transport/negotiationstate.go
Normal file
40
pkg/rtc/transport/negotiationstate.go
Normal 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))
|
||||
}
|
||||
}
|
||||
807
pkg/rtc/transport/transportfakes/fake_handler.go
Normal file
807
pkg/rtc/transport/transportfakes/fake_handler.go
Normal 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
805
pkg/rtc/transport_test.go
Normal 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
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
414
pkg/rtc/types/ice.go
Normal 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
1049
pkg/rtc/types/interfaces.go
Normal file
File diff suppressed because it is too large
Load Diff
105
pkg/rtc/types/protocol_version.go
Normal file
105
pkg/rtc/types/protocol_version.go
Normal 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
|
||||
}
|
||||
161
pkg/rtc/types/trafficstats.go
Normal file
161
pkg/rtc/types/trafficstats.go
Normal 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
|
||||
}
|
||||
266
pkg/rtc/types/typesfakes/fake_data_down_track.go
Normal file
266
pkg/rtc/types/typesfakes/fake_data_down_track.go
Normal 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)
|
||||
863
pkg/rtc/types/typesfakes/fake_data_track.go
Normal file
863
pkg/rtc/types/typesfakes/fake_data_track.go
Normal 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)
|
||||
148
pkg/rtc/types/typesfakes/fake_data_track_sender.go
Normal file
148
pkg/rtc/types/typesfakes/fake_data_track_sender.go
Normal 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)
|
||||
114
pkg/rtc/types/typesfakes/fake_data_track_transport.go
Normal file
114
pkg/rtc/types/typesfakes/fake_data_track_transport.go
Normal 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)
|
||||
2579
pkg/rtc/types/typesfakes/fake_local_media_track.go
Normal file
2579
pkg/rtc/types/typesfakes/fake_local_media_track.go
Normal file
File diff suppressed because it is too large
Load Diff
10338
pkg/rtc/types/typesfakes/fake_local_participant.go
Normal file
10338
pkg/rtc/types/typesfakes/fake_local_participant.go
Normal file
File diff suppressed because it is too large
Load Diff
542
pkg/rtc/types/typesfakes/fake_local_participant_helper.go
Normal file
542
pkg/rtc/types/typesfakes/fake_local_participant_helper.go
Normal 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)
|
||||
1026
pkg/rtc/types/typesfakes/fake_local_participant_listener.go
Normal file
1026
pkg/rtc/types/typesfakes/fake_local_participant_listener.go
Normal file
File diff suppressed because it is too large
Load Diff
1884
pkg/rtc/types/typesfakes/fake_media_track.go
Normal file
1884
pkg/rtc/types/typesfakes/fake_media_track.go
Normal file
File diff suppressed because it is too large
Load Diff
2296
pkg/rtc/types/typesfakes/fake_participant.go
Normal file
2296
pkg/rtc/types/typesfakes/fake_participant.go
Normal file
File diff suppressed because it is too large
Load Diff
356
pkg/rtc/types/typesfakes/fake_participant_listener.go
Normal file
356
pkg/rtc/types/typesfakes/fake_participant_listener.go
Normal 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)
|
||||
666
pkg/rtc/types/typesfakes/fake_participant_telemetry_listener.go
Normal file
666
pkg/rtc/types/typesfakes/fake_participant_telemetry_listener.go
Normal 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)
|
||||
541
pkg/rtc/types/typesfakes/fake_room.go
Normal file
541
pkg/rtc/types/typesfakes/fake_room.go
Normal 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)
|
||||
1112
pkg/rtc/types/typesfakes/fake_subscribed_track.go
Normal file
1112
pkg/rtc/types/typesfakes/fake_subscribed_track.go
Normal file
File diff suppressed because it is too large
Load Diff
480
pkg/rtc/types/typesfakes/fake_websocket_client.go
Normal file
480
pkg/rtc/types/typesfakes/fake_websocket_client.go
Normal 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)
|
||||
126
pkg/rtc/updatatrackmanager.go
Normal file
126
pkg/rtc/updatatrackmanager.go
Normal 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
459
pkg/rtc/uptrackmanager.go
Normal 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
|
||||
}
|
||||
329
pkg/rtc/uptrackmanager_test.go
Normal file
329
pkg/rtc/uptrackmanager_test.go
Normal 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"))
|
||||
})
|
||||
}
|
||||
66
pkg/rtc/user_packet_deduper.go
Normal file
66
pkg/rtc/user_packet_deduper.go
Normal 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
207
pkg/rtc/utils.go
Normal 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
75
pkg/rtc/utils_test.go
Normal 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
576
pkg/rtc/wrappedreceiver.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user