mirror of
https://github.com/livekit/livekit.git
synced 2026-04-01 00:05:40 +00:00
570 lines
17 KiB
Go
570 lines
17 KiB
Go
package rtc_test
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
livekit "github.com/livekit/protocol/proto"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/livekit/livekit-server/pkg/config"
|
|
serverlogger "github.com/livekit/livekit-server/pkg/logger"
|
|
"github.com/livekit/livekit-server/pkg/rtc"
|
|
"github.com/livekit/livekit-server/pkg/rtc/types"
|
|
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
|
|
"github.com/livekit/livekit-server/pkg/testutils"
|
|
)
|
|
|
|
const (
|
|
numParticipants = 3
|
|
defaultDelay = 10 * time.Millisecond
|
|
audioUpdateInterval = 25
|
|
)
|
|
|
|
func init() {
|
|
serverlogger.InitDevelopment("")
|
|
}
|
|
|
|
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.Equal(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())
|
|
require.Equal(t, s, rm.LastLeftAt())
|
|
})
|
|
|
|
t.Run("LastLeftAt should not 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())
|
|
require.EqualValues(t, 0, rm.LastLeftAt())
|
|
})
|
|
}
|
|
|
|
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.DefaultProtocol, false)
|
|
|
|
rm.Join(pNew, nil)
|
|
|
|
// expect new participant to get a JoinReply
|
|
info, participants, iceServers := pNew.SendJoinResponseArgsForCall(0)
|
|
require.Equal(t, info.Sid, rm.Room.Sid)
|
|
require.Len(t, participants, numParticipants)
|
|
require.Len(t, rm.GetParticipants(), numParticipants+1)
|
|
require.NotEmpty(t, iceServers)
|
|
})
|
|
|
|
t.Run("subscribe to existing channels upon join", func(t *testing.T) {
|
|
numExisting := 3
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: numExisting})
|
|
p := newMockParticipant("new", types.DefaultProtocol, false)
|
|
|
|
err := rm.Join(p, &rtc.ParticipantOptions{AutoSubscribe: true})
|
|
require.NoError(t, err)
|
|
|
|
stateChangeCB := p.OnStateChangeArgsForCall(0)
|
|
require.NotNil(t, stateChangeCB)
|
|
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
|
stateChangeCB(p, livekit.ParticipantInfo_JOINED)
|
|
|
|
// it should become a subscriber when connectivity changes
|
|
for _, op := range rm.GetParticipants() {
|
|
if p == op {
|
|
continue
|
|
}
|
|
mockP := op.(*typesfakes.FakeParticipant)
|
|
require.NotZero(t, mockP.AddSubscriberCallCount())
|
|
// last call should be to add the newest participant
|
|
require.Equal(t, p, mockP.AddSubscriberArgsForCall(mockP.AddSubscriberCallCount()-1))
|
|
}
|
|
})
|
|
|
|
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.FakeParticipant)
|
|
disconnectedParticipant := participants[1].(*typesfakes.FakeParticipant)
|
|
disconnectedParticipant.StateReturns(livekit.ParticipantInfo_DISCONNECTED)
|
|
|
|
rm.RemoveParticipant(p.Identity())
|
|
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.FakeParticipant)
|
|
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.Room.MaxParticipants = 1
|
|
p := newMockParticipant("second", types.ProtocolVersion(0), false)
|
|
|
|
err := rm.Join(p, nil)
|
|
require.Equal(t, rtc.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.Participant)
|
|
}{
|
|
{
|
|
"track mutes are sent to everyone",
|
|
true,
|
|
func(p types.Participant) {
|
|
p.SetTrackMuted("", true, false)
|
|
},
|
|
},
|
|
{
|
|
"track metadata updates are sent to everyone",
|
|
true,
|
|
func(p types.Participant) {
|
|
p.SetMetadata("")
|
|
},
|
|
},
|
|
{
|
|
"track publishes are sent to existing participants",
|
|
true,
|
|
func(p types.Participant) {
|
|
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[string]int)
|
|
for _, p := range rm.GetParticipants() {
|
|
fp := p.(*typesfakes.FakeParticipant)
|
|
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.FakeParticipant)
|
|
require.Equal(t, expected, fp.SendParticipantUpdateCallCount())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRoomClosure(t *testing.T) {
|
|
t.Run("room closes after participant leaves", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
|
|
isClosed := false
|
|
rm.OnClose(func() {
|
|
isClosed = true
|
|
})
|
|
p := rm.GetParticipants()[0]
|
|
// allows immediate close after
|
|
rm.Room.EmptyTimeout = 0
|
|
rm.RemoveParticipant(p.Identity())
|
|
|
|
time.Sleep(defaultDelay)
|
|
|
|
rm.CloseIfEmpty()
|
|
require.Len(t, rm.GetParticipants(), 0)
|
|
require.True(t, isClosed)
|
|
|
|
require.Equal(t, rtc.ErrRoomClosed, rm.Join(p, nil))
|
|
})
|
|
|
|
t.Run("room does not close before empty timeout", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
|
|
isClosed := false
|
|
rm.OnClose(func() {
|
|
isClosed = true
|
|
})
|
|
require.NotZero(t, rm.Room.EmptyTimeout)
|
|
rm.CloseIfEmpty()
|
|
require.False(t, isClosed)
|
|
})
|
|
|
|
t.Run("room closes after empty timeout", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 0})
|
|
isClosed := false
|
|
rm.OnClose(func() {
|
|
isClosed = true
|
|
})
|
|
rm.Room.EmptyTimeout = 1
|
|
|
|
time.Sleep(1010 * time.Millisecond)
|
|
rm.CloseIfEmpty()
|
|
require.True(t, isClosed)
|
|
})
|
|
}
|
|
|
|
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})
|
|
participants := rm.GetParticipants()
|
|
p0 := participants[0].(*typesfakes.FakeParticipant)
|
|
p0.StateReturns(livekit.ParticipantInfo_JOINED)
|
|
p1 := participants[1].(*typesfakes.FakeParticipant)
|
|
p1.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
|
|
|
pub := participants[2].(*typesfakes.FakeParticipant)
|
|
|
|
// p3 adds track
|
|
track := newMockTrack(livekit.TrackType_VIDEO, "webcam")
|
|
trackCB := pub.OnTrackPublishedArgsForCall(0)
|
|
require.NotNil(t, trackCB)
|
|
trackCB(pub, track)
|
|
// only p2 should've been called
|
|
require.Equal(t, 1, track.AddSubscriberCallCount())
|
|
require.Equal(t, p1, track.AddSubscriberArgsForCall(0))
|
|
})
|
|
}
|
|
|
|
func TestActiveSpeakers(t *testing.T) {
|
|
t.Parallel()
|
|
getActiveSpeakerUpdates := func(p *typesfakes.FakeParticipant) []*livekit.ActiveSpeakerUpdate {
|
|
var updates []*livekit.ActiveSpeakerUpdate
|
|
numCalls := p.SendDataPacketCallCount()
|
|
for i := 0; i < numCalls; i++ {
|
|
dp := p.SendDataPacketArgsForCall(i)
|
|
switch val := dp.Value.(type) {
|
|
case *livekit.DataPacket_Speaker:
|
|
updates = append(updates, val.Speaker)
|
|
}
|
|
}
|
|
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: types.DefaultProtocol})
|
|
defer rm.Close()
|
|
p := rm.GetParticipants()[0].(*typesfakes.FakeParticipant)
|
|
require.Empty(t, rm.GetActiveSpeakers())
|
|
|
|
time.Sleep(audioUpdateDuration)
|
|
|
|
updates := getActiveSpeakerUpdates(p)
|
|
require.Empty(t, updates)
|
|
})
|
|
|
|
t.Run("speakers should be sorted by loudness (protocol 0)", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
|
|
defer rm.Close()
|
|
participants := rm.GetParticipants()
|
|
p := participants[0].(*typesfakes.FakeParticipant)
|
|
p2 := participants[1].(*typesfakes.FakeParticipant)
|
|
p.GetAudioLevelReturns(10, true)
|
|
p2.GetAudioLevelReturns(20, true)
|
|
|
|
speakers := rm.GetActiveSpeakers()
|
|
require.Len(t, speakers, 2)
|
|
require.Equal(t, p.ID(), speakers[0].Sid)
|
|
require.Equal(t, p2.ID(), speakers[1].Sid)
|
|
})
|
|
|
|
t.Run("participants are getting audio updates (protocol 2)", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: types.DefaultProtocol})
|
|
defer rm.Close()
|
|
participants := rm.GetParticipants()
|
|
p := participants[0].(*typesfakes.FakeParticipant)
|
|
time.Sleep(time.Millisecond) // let the first update cycle run
|
|
p.GetAudioLevelReturns(30, true)
|
|
|
|
speakers := rm.GetActiveSpeakers()
|
|
require.NotEmpty(t, speakers)
|
|
require.Equal(t, p.ID(), speakers[0].Sid)
|
|
|
|
testutils.WithTimeout(t, "ensure everyone has gotten an audio update", func() bool {
|
|
for _, op := range participants {
|
|
op := op.(*typesfakes.FakeParticipant)
|
|
updates := getActiveSpeakerUpdates(op)
|
|
if len(updates) == 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
// no longer speaking, send update with empty items
|
|
p.GetAudioLevelReturns(127, false)
|
|
|
|
testutils.WithTimeout(t, "ensure no one is speaking", func() bool {
|
|
updates := getActiveSpeakerUpdates(p)
|
|
lastUpdate := updates[len(updates)-1]
|
|
return len(lastUpdate.Speakers) == 0
|
|
})
|
|
})
|
|
|
|
t.Run("audio level is smoothed", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: types.DefaultProtocol, audioSmoothIntervals: 3})
|
|
defer rm.Close()
|
|
participants := rm.GetParticipants()
|
|
p := participants[0].(*typesfakes.FakeParticipant)
|
|
op := participants[1].(*typesfakes.FakeParticipant)
|
|
p.GetAudioLevelReturns(30, true)
|
|
convertedLevel := rtc.ConvertAudioLevel(30)
|
|
|
|
testutils.WithTimeout(t, "checking first update is received", func() bool {
|
|
updates := getActiveSpeakerUpdates(op)
|
|
if len(updates) == 0 {
|
|
return false
|
|
}
|
|
lastSpeakers := updates[len(updates)-1].Speakers
|
|
if len(lastSpeakers) == 0 {
|
|
return false
|
|
}
|
|
if lastSpeakers[0].Level > convertedLevel {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
testutils.WithTimeout(t, "eventually reaches actual levels", func() bool {
|
|
updates := getActiveSpeakerUpdates(op)
|
|
if len(updates) == 0 {
|
|
return false
|
|
}
|
|
lastSpeakers := updates[len(updates)-1].Speakers
|
|
if len(lastSpeakers) == 0 {
|
|
return false
|
|
}
|
|
if lastSpeakers[0].Level > convertedLevel {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
p.GetAudioLevelReturns(127, false)
|
|
|
|
testutils.WithTimeout(t, "eventually goes back to 0", func() bool {
|
|
updates := getActiveSpeakerUpdates(op)
|
|
if len(updates) == 0 {
|
|
return false
|
|
}
|
|
lastSpeakers := updates[len(updates)-1].Speakers
|
|
if len(lastSpeakers) == 0 {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDataChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("participants should receive data", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
|
|
defer rm.Close()
|
|
participants := rm.GetParticipants()
|
|
p := participants[0].(*typesfakes.FakeParticipant)
|
|
|
|
packet := livekit.DataPacket{
|
|
Kind: livekit.DataPacket_RELIABLE,
|
|
Value: &livekit.DataPacket_User{
|
|
User: &livekit.UserPacket{
|
|
ParticipantSid: p.ID(),
|
|
Payload: []byte("message.."),
|
|
},
|
|
},
|
|
}
|
|
p.OnDataPacketArgsForCall(0)(p, &packet)
|
|
|
|
// ensure everyone has received the packet
|
|
for _, op := range participants {
|
|
fp := op.(*typesfakes.FakeParticipant)
|
|
if fp == p {
|
|
require.Zero(t, fp.SendDataPacketCallCount())
|
|
continue
|
|
}
|
|
require.Equal(t, 1, fp.SendDataPacketCallCount())
|
|
require.Equal(t, packet.Value, fp.SendDataPacketArgsForCall(0).Value)
|
|
}
|
|
})
|
|
|
|
t.Run("only one participant should receive the data", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 4})
|
|
defer rm.Close()
|
|
participants := rm.GetParticipants()
|
|
p := participants[0].(*typesfakes.FakeParticipant)
|
|
p1 := participants[1].(*typesfakes.FakeParticipant)
|
|
|
|
packet := livekit.DataPacket{
|
|
Kind: livekit.DataPacket_RELIABLE,
|
|
Value: &livekit.DataPacket_User{
|
|
User: &livekit.UserPacket{
|
|
ParticipantSid: p.ID(),
|
|
Payload: []byte("message to p1.."),
|
|
DestinationSids: []string{p1.ID()},
|
|
},
|
|
},
|
|
}
|
|
p.OnDataPacketArgsForCall(0)(p, &packet)
|
|
|
|
// only p1 should receive the data
|
|
for _, op := range participants {
|
|
fp := op.(*typesfakes.FakeParticipant)
|
|
if fp != p1 {
|
|
require.Zero(t, fp.SendDataPacketCallCount())
|
|
}
|
|
}
|
|
require.Equal(t, 1, p1.SendDataPacketCallCount())
|
|
require.Equal(t, packet.Value, p1.SendDataPacketArgsForCall(0).Value)
|
|
})
|
|
|
|
t.Run("publishing disallowed", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
|
|
defer rm.Close()
|
|
participants := rm.GetParticipants()
|
|
p := participants[0].(*typesfakes.FakeParticipant)
|
|
p.CanPublishDataReturns(false)
|
|
|
|
packet := livekit.DataPacket{
|
|
Kind: livekit.DataPacket_RELIABLE,
|
|
Value: &livekit.DataPacket_User{
|
|
User: &livekit.UserPacket{
|
|
Payload: []byte{},
|
|
},
|
|
},
|
|
}
|
|
p.OnDataPacketArgsForCall(0)(p, &packet)
|
|
|
|
// no one should've been sent packet
|
|
for _, op := range participants {
|
|
fp := op.(*typesfakes.FakeParticipant)
|
|
require.Zero(t, fp.SendDataPacketCallCount())
|
|
}
|
|
})
|
|
}
|
|
|
|
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()
|
|
|
|
pNew := newMockParticipant("new", types.DefaultProtocol, false)
|
|
rm.Join(pNew, nil)
|
|
|
|
// expect new participant to get a JoinReply
|
|
info, participants, iceServers := pNew.SendJoinResponseArgsForCall(0)
|
|
require.Equal(t, info.Sid, rm.Room.Sid)
|
|
require.Len(t, participants, 2)
|
|
require.Len(t, rm.GetParticipants(), 4)
|
|
require.NotEmpty(t, iceServers)
|
|
})
|
|
|
|
t.Run("hidden participant subscribes to tracks", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, numHidden: 1})
|
|
p := newMockParticipant("new", types.DefaultProtocol, false)
|
|
|
|
err := rm.Join(p, &rtc.ParticipantOptions{AutoSubscribe: true})
|
|
require.NoError(t, err)
|
|
|
|
stateChangeCB := p.OnStateChangeArgsForCall(0)
|
|
require.NotNil(t, stateChangeCB)
|
|
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
|
stateChangeCB(p, livekit.ParticipantInfo_JOINED)
|
|
|
|
// it should become a subscriber when connectivity changes
|
|
for _, op := range rm.GetParticipants() {
|
|
if p == op {
|
|
continue
|
|
}
|
|
mockP := op.(*typesfakes.FakeParticipant)
|
|
require.NotZero(t, mockP.AddSubscriberCallCount())
|
|
// last call should be to add the newest participant
|
|
require.Equal(t, p, mockP.AddSubscriberArgsForCall(mockP.AddSubscriberCallCount()-1))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRoomUpdate(t *testing.T) {
|
|
t.Run("participants should receive metadata update", func(t *testing.T) {
|
|
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
|
|
defer rm.Close()
|
|
|
|
rm.SetMetadata("test metadata...")
|
|
|
|
for _, op := range rm.GetParticipants() {
|
|
fp := op.(*typesfakes.FakeParticipant)
|
|
require.Equal(t, 1, fp.SendRoomUpdateCallCount())
|
|
}
|
|
})
|
|
}
|
|
|
|
type testRoomOpts struct {
|
|
num int
|
|
numHidden int
|
|
protocol types.ProtocolVersion
|
|
audioSmoothIntervals uint32
|
|
}
|
|
|
|
func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *rtc.Room {
|
|
rm := rtc.NewRoom(
|
|
&livekit.Room{Name: "room"},
|
|
rtc.WebRTCConfig{},
|
|
[]*livekit.ICEServer{
|
|
{
|
|
Urls: []string{
|
|
"stun:stun.l.google.com:19302",
|
|
},
|
|
},
|
|
},
|
|
&config.AudioConfig{
|
|
UpdateInterval: audioUpdateInterval,
|
|
SmoothIntervals: opts.audioSmoothIntervals,
|
|
},
|
|
)
|
|
for i := 0; i < opts.num+opts.numHidden; i++ {
|
|
identity := fmt.Sprintf("p%d", i)
|
|
participant := newMockParticipant(identity, opts.protocol, i >= opts.num)
|
|
err := rm.Join(participant, &rtc.ParticipantOptions{AutoSubscribe: true})
|
|
participant.StateReturns(livekit.ParticipantInfo_ACTIVE)
|
|
participant.IsReadyReturns(true)
|
|
require.NoError(t, err)
|
|
}
|
|
return rm
|
|
}
|