Files
livekit/pkg/rtc/room_test.go
2021-05-22 22:54:47 -07:00

365 lines
11 KiB
Go

package rtc_test
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"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"
livekit "github.com/livekit/livekit-server/proto"
)
const (
numParticipants = 3
defaultDelay = 10 * time.Millisecond
audioUpdateInterval = 25
)
func init() {
logger.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)
rm.Join(pNew, nil)
// expect new participant to get a JoinReply
info, participants, iceServers := pNew.SendJoinResponseArgsForCall(0)
require.Equal(t, info.Sid, rm.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)
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.MaxParticipants = 1
p := newMockParticipant("second", types.ProtocolVersion(0))
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)
},
},
{
"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.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.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.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.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})
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})
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})
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)
time.Sleep(audioUpdateDuration)
// everyone should've received updates
for _, op := range participants {
op := op.(*typesfakes.FakeParticipant)
updates := getActiveSpeakerUpdates(op)
require.Len(t, updates, 1)
}
// after another cycle, we are not getting any new updates since unchanged
time.Sleep(audioUpdateDuration)
for _, op := range participants {
op := op.(*typesfakes.FakeParticipant)
updates := getActiveSpeakerUpdates(op)
require.Len(t, updates, 1)
}
// no longer speaking, send update with empty items
p.GetAudioLevelReturns(127, false)
time.Sleep(audioUpdateDuration)
updates := getActiveSpeakerUpdates(p)
require.Len(t, updates, 2)
require.Empty(t, updates[1].Speakers)
})
}
type testRoomOpts struct {
num int
protocol types.ProtocolVersion
}
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",
},
},
},
audioUpdateInterval,
)
for i := 0; i < opts.num; i++ {
identity := fmt.Sprintf("p%d", i)
participant := newMockParticipant(identity, opts.protocol)
err := rm.Join(participant, &rtc.ParticipantOptions{AutoSubscribe: true})
participant.StateReturns(livekit.ParticipantInfo_ACTIVE)
require.NoError(t, err)
}
return rm
}