mirror of
https://github.com/livekit/livekit.git
synced 2026-10-01 02:08:55 +00:00
* test: stop registering the global DefaultConfig with the logger InitLoggerFromConfig hands the pointer to zaputil.ComponentLeveler as its level resolver, and protocol a879e94 gave logger.Config a ResolveComponentLevel that takes c.lock. The two test packages passed &config.DefaultConfig.Logging, so every first-time component level resolution locked a mutex inside the global DefaultConfig while config.NewConfig marshalled that same global from another goroutine: Read at 0x39baa08 yaml.Marshal -> pkg/config/config.go:633 (NewConfig) Write at 0x39baa08 ComponentLeveler.resolve -> zaputil/leveler.go:117 That tripped the race detector in pkg/rtc TestPreferMediaCodecForPublisher and broke CI on master. Register a test-local LoggingConfig instead, so the logger never touches the global that NewConfig reads. DefaultConfig.Logging only ever sets PionLevel, so behavior is unchanged. A plain struct copy is not an option here: logger.Config holds a mutex and vet's copylocks would reject it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test: make RTCClient callbacks safe to set while the client runs The scenario tests assign c2.OnDataReceived after waitUntilConnected, so the assignment raced the data channel goroutine already reading the field in handleDataMessage: Read test/client.(*RTCClient).handleDataMessage client.go:1146 Write test.scenarioDataPublish.func1 scenarios.go:160 This is what failed TestMultinodeDataPublishing on master. Replace the three exported callback fields with atomic pointers behind SetOnConnected/SetOnDataReceived/SetOnDataUnlabeledReceived. OnConnected and OnDataUnlabeledReceived have no writers today, but they are read from the same background goroutines and would race the moment one appeared, so all three move together rather than leaving a split API on one struct. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1826 lines
54 KiB
Go
1826 lines
54 KiB
Go
// 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 test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jxskiss/base62"
|
|
"github.com/pion/sdp/v3"
|
|
"github.com/pion/stun/v3"
|
|
"github.com/pion/turn/v5"
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/thoas/go-funk"
|
|
"github.com/twitchtv/twirp"
|
|
"go.uber.org/atomic"
|
|
|
|
"github.com/livekit/protocol/auth"
|
|
"github.com/livekit/protocol/codecs/mime"
|
|
"github.com/livekit/protocol/livekit"
|
|
"github.com/livekit/protocol/logger"
|
|
|
|
"github.com/livekit/livekit-server/pkg/config"
|
|
"github.com/livekit/livekit-server/pkg/rtc"
|
|
"github.com/livekit/livekit-server/pkg/rtc/types"
|
|
"github.com/livekit/livekit-server/pkg/service"
|
|
"github.com/livekit/livekit-server/pkg/sfu"
|
|
"github.com/livekit/livekit-server/pkg/sfu/datachannel"
|
|
"github.com/livekit/livekit-server/pkg/testutils"
|
|
testclient "github.com/livekit/livekit-server/test/client"
|
|
)
|
|
|
|
const (
|
|
waitTick = 10 * time.Millisecond
|
|
waitTimeout = 5 * time.Second
|
|
)
|
|
|
|
func TestClientCouldConnect(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("TestClientCouldConnect")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, nil)
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1, c2)
|
|
|
|
// ensure they both see each other
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c1.RemoteParticipants()) == 0 {
|
|
return "c1 did not see c2"
|
|
}
|
|
if len(c2.RemoteParticipants()) == 0 {
|
|
return "c2 did not see c1"
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClientConnectDuplicate(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("TestClientConnectDuplicate")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
grant := &auth.VideoGrant{RoomJoin: true, Room: testRoom}
|
|
grant.SetCanPublish(true)
|
|
grant.SetCanSubscribe(true)
|
|
token := joinTokenWithGrant("c1", grant)
|
|
c1 := createRTCClientWithToken(token, defaultServerPort, testRTCServicePath, nil)
|
|
|
|
// publish 2 tracks
|
|
t1, err := c1.AddStaticTrack("audio/opus", "audio", "webcam")
|
|
require.NoError(t, err)
|
|
defer t1.Stop()
|
|
t2, err := c1.AddStaticTrack("video/vp8", "video", "webcam")
|
|
require.NoError(t, err)
|
|
defer t2.Stop()
|
|
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1, c2)
|
|
|
|
opts := &testclient.Options{
|
|
Publish: "duplicate_connection",
|
|
}
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()) == 0 {
|
|
return "c2 didn't subscribe to anything"
|
|
}
|
|
// should have received two tracks
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
|
return "c2 didn't subscribe to both tracks from c1"
|
|
}
|
|
|
|
// participant ID can be appended with '#..' . but should contain orig id as prefix
|
|
tr1 := c2.SubscribedTracks()[c1.ID()][0]
|
|
participantId1, _ := rtc.UnpackStreamID(tr1.StreamID())
|
|
require.Equal(t, c1.ID(), participantId1)
|
|
tr2 := c2.SubscribedTracks()[c1.ID()][1]
|
|
participantId2, _ := rtc.UnpackStreamID(tr2.StreamID())
|
|
require.Equal(t, c1.ID(), participantId2)
|
|
return ""
|
|
})
|
|
|
|
c1Dup := createRTCClientWithToken(token, defaultServerPort, testRTCServicePath, opts)
|
|
|
|
waitUntilConnected(t, c1Dup)
|
|
|
|
t3, err := c1Dup.AddStaticTrack("video/vp8", "video", "webcam")
|
|
require.NoError(t, err)
|
|
defer t3.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()[c1Dup.ID()]) != 1 {
|
|
return "c2 was not subscribed to track from duplicated c1"
|
|
}
|
|
|
|
tr3 := c2.SubscribedTracks()[c1Dup.ID()][0]
|
|
participantId3, _ := rtc.UnpackStreamID(tr3.StreamID())
|
|
require.Contains(t, c1Dup.ID(), participantId3)
|
|
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSinglePublisher(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
s, finish := setupSingleNodeTest("TestSinglePublisher")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, nil)
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1, c2)
|
|
|
|
// publish an audio and video track and ensure clients receive it ok
|
|
t1, err := c1.AddStaticTrack("audio/OPUS", "audio", "webcamaudio")
|
|
require.NoError(t, err)
|
|
defer t1.Stop()
|
|
t2, err := c1.AddStaticTrack("video/vp8", "video", "webcamvideo")
|
|
require.NoError(t, err)
|
|
defer t2.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()) == 0 {
|
|
return "c2 was not subscribed to anything"
|
|
}
|
|
// should have received two tracks
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
|
return "c2 didn't subscribe to both tracks from c1"
|
|
}
|
|
|
|
tr1 := c2.SubscribedTracks()[c1.ID()][0]
|
|
participantId, _ := rtc.UnpackStreamID(tr1.StreamID())
|
|
require.Equal(t, c1.ID(), participantId)
|
|
return ""
|
|
})
|
|
// ensure mime type is received
|
|
remoteC1 := c2.GetRemoteParticipant(c1.ID())
|
|
audioTrack := funk.Find(remoteC1.Tracks, func(ti *livekit.TrackInfo) bool {
|
|
return ti.Name == "webcamaudio"
|
|
}).(*livekit.TrackInfo)
|
|
require.Equal(t, "audio/opus", audioTrack.MimeType)
|
|
|
|
// a new client joins and should get the initial stream
|
|
c3 := createRTCClient("c3", defaultServerPort, testRTCServicePath, nil)
|
|
|
|
// ensure that new client that has joined also received tracks
|
|
waitUntilConnected(t, c3)
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c3.SubscribedTracks()) == 0 {
|
|
return "c3 didn't subscribe to anything"
|
|
}
|
|
// should have received two tracks
|
|
if len(c3.SubscribedTracks()[c1.ID()]) != 2 {
|
|
return "c3 didn't subscribe to tracks from c1"
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// ensure that the track ids are generated by server
|
|
tracks := c3.SubscribedTracks()[c1.ID()]
|
|
for _, tr := range tracks {
|
|
require.True(t, strings.HasPrefix(tr.ID(), "TR_"), "track should begin with TR")
|
|
}
|
|
|
|
// when c3 disconnects, ensure subscriber is cleaned up correctly
|
|
c3.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
room := s.RoomManager().GetRoom(context.Background(), testRoom)
|
|
p := room.GetParticipant("c1")
|
|
require.NotNil(t, p)
|
|
|
|
for _, t := range p.GetPublishedTracks() {
|
|
if t.IsSubscriber(c3.ID()) {
|
|
return "c3 was not a subscriber of c1's tracks"
|
|
}
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConnectionStats(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
s, finish := setupSingleNodeTest("TestConnectionStats")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, nil)
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1, c2)
|
|
defer func() {
|
|
c1.Stop()
|
|
c2.Stop()
|
|
}()
|
|
|
|
// both clients publish audio + video
|
|
t1, err := c1.AddStaticTrack("audio/opus", "audio", "c1audio")
|
|
require.NoError(t, err)
|
|
defer t1.Stop()
|
|
t2, err := c1.AddStaticTrack("video/vp8", "video", "c1video")
|
|
require.NoError(t, err)
|
|
defer t2.Stop()
|
|
|
|
t3, err := c2.AddStaticTrack("audio/opus", "audio", "c2audio")
|
|
require.NoError(t, err)
|
|
defer t3.Stop()
|
|
t4, err := c2.AddStaticTrack("video/vp8", "video", "c2video")
|
|
require.NoError(t, err)
|
|
defer t4.Stop()
|
|
|
|
// wait for cross-subscriptions: each client should receive 2 tracks from the other
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c1.SubscribedTracks()[c2.ID()]) != 2 {
|
|
return "c1 did not subscribe to both tracks from c2"
|
|
}
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
|
return "c2 did not subscribe to both tracks from c1"
|
|
}
|
|
return ""
|
|
})
|
|
|
|
room := s.RoomManager().GetRoom(context.Background(), testRoom)
|
|
require.NotNil(t, room)
|
|
|
|
// hook the upstream WebRTCReceiver.OnStatsUpdate and downstream DownTrack.OnStatsUpdate
|
|
// callbacks so we can verify the AnalyticsStat delivered through each carries valid
|
|
// delta data. MediaTrack.Receivers() returns one entry per potential codec; only those
|
|
// matching the actually published codec are *sfu.WebRTCReceiver, the rest are
|
|
// placeholder *rtc.DummyReceiver instances that we skip.
|
|
type statCapture struct {
|
|
lock sync.Mutex
|
|
stat *livekit.AnalyticsStat
|
|
}
|
|
receiverCaptures := make(map[livekit.TrackID]*statCapture)
|
|
downTrackCaptures := make(map[livekit.ParticipantIdentity]map[livekit.TrackID]*statCapture)
|
|
for _, identity := range []livekit.ParticipantIdentity{"c1", "c2"} {
|
|
p := room.GetParticipant(identity)
|
|
require.NotNil(t, p, "participant %s not found", identity)
|
|
for _, mt := range p.GetPublishedTracks() {
|
|
rc := &statCapture{}
|
|
receiverCaptures[mt.ID()] = rc
|
|
var hooked int
|
|
for _, r := range mt.Receivers() {
|
|
if dr, ok := r.(*rtc.DummyReceiver); ok {
|
|
underlying := dr.Receiver()
|
|
if underlying == nil {
|
|
continue
|
|
}
|
|
r = underlying
|
|
}
|
|
wr, ok := r.(*sfu.WebRTCReceiver)
|
|
if !ok {
|
|
continue
|
|
}
|
|
wr.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
|
|
rc.lock.Lock()
|
|
rc.stat = stat
|
|
rc.lock.Unlock()
|
|
})
|
|
hooked++
|
|
}
|
|
require.Greater(t, hooked, 0, "no live WebRTCReceiver found for published track %s", mt.ID())
|
|
}
|
|
|
|
dtCaps := make(map[livekit.TrackID]*statCapture)
|
|
downTrackCaptures[identity] = dtCaps
|
|
for _, st := range p.GetSubscribedTracks() {
|
|
dt := st.DownTrack()
|
|
require.NotNil(t, dt, "subscribed track %s has no DownTrack", st.ID())
|
|
dc := &statCapture{}
|
|
dtCaps[st.ID()] = dc
|
|
dt.OnStatsUpdate(func(_ *sfu.DownTrack, stat *livekit.AnalyticsStat) {
|
|
dc.lock.Lock()
|
|
dc.stat = stat
|
|
dc.lock.Unlock()
|
|
})
|
|
}
|
|
}
|
|
|
|
validateAnalyticsStat := func(stat *livekit.AnalyticsStat) string {
|
|
if stat == nil {
|
|
return "stat nil"
|
|
}
|
|
if len(stat.Streams) == 0 {
|
|
return "stat has no streams"
|
|
}
|
|
var totalPackets uint32
|
|
var totalBytes uint64
|
|
for _, s := range stat.Streams {
|
|
totalPackets += s.PrimaryPackets
|
|
totalBytes += s.PrimaryBytes
|
|
}
|
|
if totalPackets == 0 {
|
|
return "stat has no packets across streams"
|
|
}
|
|
if totalBytes == 0 {
|
|
return "stat has no bytes across streams"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// wait for cumulative + delta + OnStatsUpdate-derived stats. the
|
|
// connection-quality update interval is 5s, so allow plenty of time for
|
|
// the receiver OnStatsUpdate callback to fire at least once and for
|
|
// the downstream connection-quality scorer to compute a real score.
|
|
testutils.WithTimeout(t, func() string {
|
|
for _, identity := range []livekit.ParticipantIdentity{"c1", "c2"} {
|
|
p := room.GetParticipant(identity)
|
|
if p == nil {
|
|
return fmt.Sprintf("participant %s not found", identity)
|
|
}
|
|
|
|
// upstream (publisher) cumulative stats
|
|
published := p.GetPublishedTracks()
|
|
if len(published) != 2 {
|
|
return fmt.Sprintf("%s expected 2 published tracks, got %d", identity, len(published))
|
|
}
|
|
for _, mt := range published {
|
|
lmt, ok := mt.(types.LocalMediaTrack)
|
|
if !ok {
|
|
return fmt.Sprintf("%s published track %s is not a LocalMediaTrack", identity, mt.ID())
|
|
}
|
|
stats := lmt.GetTrackStats()
|
|
if stats == nil {
|
|
return fmt.Sprintf("%s upstream cumulative stats nil for track %s", identity, mt.ID())
|
|
}
|
|
if stats.Packets == 0 {
|
|
return fmt.Sprintf("%s upstream cumulative stats has no packets for track %s", identity, mt.ID())
|
|
}
|
|
if stats.Bytes == 0 {
|
|
return fmt.Sprintf("%s upstream cumulative stats has no bytes for track %s", identity, mt.ID())
|
|
}
|
|
|
|
// upstream delta stats fed into the receiver OnStatsUpdate path
|
|
rc, ok := receiverCaptures[mt.ID()]
|
|
if !ok {
|
|
return fmt.Sprintf("%s missing receiver capture for track %s", identity, mt.ID())
|
|
}
|
|
rc.lock.Lock()
|
|
stat := rc.stat
|
|
rc.lock.Unlock()
|
|
if msg := validateAnalyticsStat(stat); msg != "" {
|
|
return fmt.Sprintf("%s upstream OnStatsUpdate %s for track %s", identity, msg, mt.ID())
|
|
}
|
|
}
|
|
|
|
// downstream (subscriber) cumulative stats and DownTrack OnStatsUpdate
|
|
// delta stats captured from the listener path
|
|
subscribed := p.GetSubscribedTracks()
|
|
if len(subscribed) != 2 {
|
|
return fmt.Sprintf("%s expected 2 subscribed tracks, got %d", identity, len(subscribed))
|
|
}
|
|
for _, st := range subscribed {
|
|
dt := st.DownTrack()
|
|
if dt == nil {
|
|
return fmt.Sprintf("%s subscribed track %s has no DownTrack", identity, st.ID())
|
|
}
|
|
stats := dt.GetTrackStats()
|
|
if stats == nil {
|
|
return fmt.Sprintf("%s downstream cumulative stats nil for track %s", identity, st.ID())
|
|
}
|
|
if stats.Packets == 0 {
|
|
return fmt.Sprintf("%s downstream cumulative stats has no packets for track %s", identity, st.ID())
|
|
}
|
|
if stats.Bytes == 0 {
|
|
return fmt.Sprintf("%s downstream cumulative stats has no bytes for track %s", identity, st.ID())
|
|
}
|
|
|
|
// downstream delta stats fed into the DownTrack OnStatsUpdate path
|
|
dc, ok := downTrackCaptures[identity][st.ID()]
|
|
if !ok {
|
|
return fmt.Sprintf("%s missing DownTrack capture for track %s", identity, st.ID())
|
|
}
|
|
dc.lock.Lock()
|
|
stat := dc.stat
|
|
dc.lock.Unlock()
|
|
if msg := validateAnalyticsStat(stat); msg != "" {
|
|
return fmt.Sprintf("%s downstream OnStatsUpdate %s for track %s", identity, msg, st.ID())
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}, 15*time.Second)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_WhenAutoSubscriptionDisabled_ClientShouldNotReceiveAnyPublishedTracks(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("Test_WhenAutoSubscriptionDisabled_ClientShouldNotReceiveAnyPublishedTracks")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
opts := testclient.Options{AutoSubscribe: false}
|
|
publisher := createRTCClient("publisher", defaultServerPort, testRTCServicePath, &opts)
|
|
client := createRTCClient("client", defaultServerPort, testRTCServicePath, &opts)
|
|
defer publisher.Stop()
|
|
defer client.Stop()
|
|
waitUntilConnected(t, publisher, client)
|
|
|
|
track, err := publisher.AddStaticTrack("audio/opus", "audio", "webcam")
|
|
require.NoError(t, err)
|
|
defer track.Stop()
|
|
|
|
time.Sleep(syncDelay)
|
|
|
|
require.Empty(t, client.SubscribedTracks()[publisher.ID()])
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_RenegotiationWithDifferentCodecs(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("TestRenegotiationWithDifferentCodecs")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, nil)
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1, c2)
|
|
|
|
// publish a vp8 video track and ensure clients receive it ok
|
|
t1, err := c1.AddStaticTrack("audio/opus", "audio", "webcam")
|
|
require.NoError(t, err)
|
|
defer t1.Stop()
|
|
t2, err := c1.AddStaticTrack("video/vp8", "video", "webcam")
|
|
require.NoError(t, err)
|
|
defer t2.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()) == 0 {
|
|
return "c2 was not subscribed to anything"
|
|
}
|
|
// should have received two tracks
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
|
return "c2 was not subscribed to tracks from c1"
|
|
}
|
|
|
|
tracks := c2.SubscribedTracks()[c1.ID()]
|
|
for _, t := range tracks {
|
|
if mime.IsMimeTypeStringVP8(t.Codec().MimeType) {
|
|
return ""
|
|
|
|
}
|
|
}
|
|
return "did not receive track with vp8"
|
|
})
|
|
|
|
t3, err := c1.AddStaticTrackWithCodec(webrtc.RTPCodecCapability{
|
|
MimeType: "video/h264",
|
|
ClockRate: 90000,
|
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
|
}, "videoscreen", "screen")
|
|
defer t3.Stop()
|
|
require.NoError(t, err)
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()) == 0 {
|
|
return "c2's not subscribed to anything"
|
|
}
|
|
// should have received three tracks
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 3 {
|
|
return "c2's not subscribed to 3 tracks from c1"
|
|
}
|
|
|
|
var vp8Found, h264Found bool
|
|
tracks := c2.SubscribedTracks()[c1.ID()]
|
|
for _, t := range tracks {
|
|
if mime.IsMimeTypeStringVP8(t.Codec().MimeType) {
|
|
vp8Found = true
|
|
} else if mime.IsMimeTypeStringH264(t.Codec().MimeType) {
|
|
h264Found = true
|
|
}
|
|
}
|
|
if !vp8Found {
|
|
return "did not receive track with vp8"
|
|
}
|
|
if !h264Found {
|
|
return "did not receive track with h264"
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleNodeRoomList(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
_, finish := setupSingleNodeTest("TestSingleNodeRoomList")
|
|
defer finish()
|
|
|
|
roomServiceListRoom(t)
|
|
}
|
|
|
|
func TestSingleNodeUpdateParticipant(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
_, finish := setupSingleNodeTest("TestSingleNodeRoomList")
|
|
defer finish()
|
|
|
|
adminCtx := contextWithToken(adminRoomToken(testRoom))
|
|
t.Run("update nonexistent participant", func(t *testing.T) {
|
|
_, err := roomClient.UpdateParticipant(adminCtx, &livekit.UpdateParticipantRequest{
|
|
Room: testRoom,
|
|
Identity: "nonexistent",
|
|
Permission: &livekit.ParticipantPermission{
|
|
CanPublish: true,
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
var twErr twirp.Error
|
|
require.True(t, errors.As(err, &twErr))
|
|
require.Equal(t, twirp.NotFound, twErr.Code())
|
|
})
|
|
}
|
|
|
|
// Ensure that CORS headers are returned
|
|
func TestSingleNodeCORS(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
s, finish := setupSingleNodeTest("TestSingleNodeCORS")
|
|
defer finish()
|
|
|
|
req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d", s.HTTPPort()), nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("Authorization", "bearer xyz")
|
|
req.Header.Set("Origin", "testhost.com")
|
|
res, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "testhost.com", res.Header.Get("Access-Control-Allow-Origin"))
|
|
}
|
|
|
|
func TestSingleNodeDoubleSlash(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
s, finish := setupSingleNodeTest("TestSingleNodeDoubleSlash")
|
|
defer finish()
|
|
// client contains trailing slash in URL, causing path to contain double //
|
|
// without our middleware, this would cause a 302 redirect
|
|
roomClient = livekit.NewRoomServiceJSONClient(fmt.Sprintf("http://localhost:%d/", s.HTTPPort()), &http.Client{})
|
|
_, err := roomClient.ListRooms(contextWithToken(listRoomToken()), &livekit.ListRoomsRequest{})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPingPong(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
_, finish := setupSingleNodeTest("TestPingPong")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1)
|
|
|
|
require.NoError(t, c1.SendPing())
|
|
require.Eventually(t, func() bool {
|
|
return c1.PongReceivedAt() > 0
|
|
}, time.Second, 10*time.Millisecond)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleNodeJoinAfterClose(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("TestJoinAfterClose")
|
|
defer finish()
|
|
|
|
scenarioJoinClosedRoom(t)
|
|
}
|
|
|
|
func TestSingleNodeCloseNonRTCRoom(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("closeNonRTCRoom")
|
|
defer finish()
|
|
|
|
closeNonRTCRoom(t)
|
|
}
|
|
|
|
func TestAutoCreate(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
disableAutoCreate := func(conf *config.Config) {
|
|
conf.Room.AutoCreate = false
|
|
}
|
|
t.Run("cannot join if room isn't created", func(t *testing.T) {
|
|
s := createSingleNodeServer(disableAutoCreate)
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
defer s.Stop(true)
|
|
|
|
waitForServerToStart(s)
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
token := joinToken(testRoom, "start-before-create", nil)
|
|
opts := &testclient.Options{}
|
|
testRTCServicePathToTestClientOptions(testRTCServicePath, opts)
|
|
_, err := testclient.NewWebSocketConn(
|
|
fmt.Sprintf("ws://localhost:%d", defaultServerPort),
|
|
token,
|
|
opts,
|
|
)
|
|
require.Error(t, err)
|
|
|
|
// second join should also fail
|
|
token = joinToken(testRoom, "start-before-create-2", nil)
|
|
_, err = testclient.NewWebSocketConn(
|
|
fmt.Sprintf("ws://localhost:%d", defaultServerPort),
|
|
token,
|
|
opts,
|
|
)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("join with explicit createRoom", func(t *testing.T) {
|
|
s := createSingleNodeServer(disableAutoCreate)
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
defer s.Stop(true)
|
|
|
|
waitForServerToStart(s)
|
|
|
|
// explicitly create
|
|
_, err := roomClient.CreateRoom(contextWithToken(createRoomToken()), &livekit.CreateRoomRequest{Name: testRoom})
|
|
require.NoError(t, err)
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("join-after-create", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, c1)
|
|
|
|
c1.Stop()
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// don't give user subscribe permissions initially, and ensure autosubscribe is triggered afterwards
|
|
func TestSingleNodeUpdateSubscriptionPermissions(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
_, finish := setupSingleNodeTest("TestSingleNodeUpdateSubscriptionPermissions")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, nil)
|
|
|
|
grant := &auth.VideoGrant{RoomJoin: true, Room: testRoom}
|
|
grant.SetCanSubscribe(false)
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
AddGrant(grant).
|
|
SetIdentity("sub")
|
|
token, err := at.ToJWT()
|
|
require.NoError(t, err)
|
|
sub := createRTCClientWithToken(token, defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
AutoSubscribeDataTrack: true,
|
|
})
|
|
|
|
waitUntilConnected(t, pub, sub)
|
|
|
|
writers := publishTracksForClients(t, pub)
|
|
defer stopWriters(writers...)
|
|
|
|
// publish a data track as well
|
|
dtw, err := pub.PublishDataTrack()
|
|
require.NoError(t, err)
|
|
defer dtw.Stop()
|
|
|
|
// wait sub receives tracks
|
|
testutils.WithTimeout(t, func() string {
|
|
pubRemote := sub.GetRemoteParticipant(pub.ID())
|
|
if pubRemote == nil {
|
|
return "could not find remote publisher"
|
|
}
|
|
if len(pubRemote.Tracks) != 2 {
|
|
return "did not receive metadata for published tracks"
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// no subscriptions should have been made while canSubscribe is false
|
|
require.Empty(t, sub.SubscribedTracks()[pub.ID()])
|
|
require.Empty(t, sub.SubscribedDataTracks()[pub.ID()])
|
|
|
|
// set permissions out of band
|
|
ctx := contextWithToken(adminRoomToken(testRoom))
|
|
_, err = roomClient.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
|
Room: testRoom,
|
|
Identity: "sub",
|
|
Permission: &livekit.ParticipantPermission{
|
|
CanSubscribe: true,
|
|
CanPublish: true,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
tracks := sub.SubscribedTracks()[pub.ID()]
|
|
if len(tracks) != 2 {
|
|
return fmt.Sprintf("expected 2 tracks subscribed, actual: %d", len(tracks))
|
|
}
|
|
if len(sub.SubscribedDataTracks()[pub.ID()]) != 1 {
|
|
return "expected data track to be subscribed"
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleNodeAttributes(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
_, finish := setupSingleNodeTest("TestSingleNodeAttributes")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
Attributes: map[string]string{
|
|
"b": "2",
|
|
"c": "3",
|
|
},
|
|
TokenCustomizer: func(token *auth.AccessToken, grants *auth.VideoGrant) {
|
|
T := true
|
|
grants.CanUpdateOwnMetadata = &T
|
|
token.SetAttributes(map[string]string{
|
|
"a": "0",
|
|
"b": "1",
|
|
})
|
|
},
|
|
})
|
|
|
|
grant := &auth.VideoGrant{RoomJoin: true, Room: testRoom}
|
|
grant.SetCanSubscribe(false)
|
|
at := auth.NewAccessToken(testApiKey, testApiSecret).
|
|
SetVideoGrant(grant).
|
|
SetIdentity("sub")
|
|
token, err := at.ToJWT()
|
|
require.NoError(t, err)
|
|
sub := createRTCClientWithToken(token, defaultServerPort, testRTCServicePath, nil)
|
|
|
|
waitUntilConnected(t, pub, sub)
|
|
|
|
// wait sub receives initial attributes
|
|
testutils.WithTimeout(t, func() string {
|
|
pubRemote := sub.GetRemoteParticipant(pub.ID())
|
|
if pubRemote == nil {
|
|
return "could not find remote publisher"
|
|
}
|
|
attrs := pubRemote.Attributes
|
|
if !reflect.DeepEqual(attrs, map[string]string{
|
|
"a": "0",
|
|
"b": "2",
|
|
"c": "3",
|
|
}) {
|
|
return fmt.Sprintf("did not receive expected attributes: %v", attrs)
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDeviceCodecOverride checks that codecs that are incompatible with a device is not
|
|
// negotiated by the server
|
|
func TestDeviceCodecOverride(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("TestDeviceCodecOverride")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
// simulate device that isn't compatible with H.264
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
ClientInfo: &livekit.ClientInfo{
|
|
Os: "android",
|
|
DeviceModel: "Xiaomi 2201117TI",
|
|
},
|
|
})
|
|
defer c1.Stop()
|
|
waitUntilConnected(t, c1)
|
|
|
|
// it doesn't really matter what the codec set here is, uses default Pion MediaEngine codecs
|
|
tw, err := c1.AddStaticTrack("video/h264", "video", "webcam")
|
|
require.NoError(t, err)
|
|
defer stopWriters(tw)
|
|
|
|
var desc *sdp.MediaDescription
|
|
require.Eventually(t, func() bool {
|
|
lastAnswer := c1.LastAnswer()
|
|
if lastAnswer == nil {
|
|
return false
|
|
}
|
|
|
|
sd := webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer,
|
|
SDP: lastAnswer.SDP,
|
|
}
|
|
answer, err := sd.Unmarshal()
|
|
require.NoError(t, err)
|
|
|
|
// video and data channel
|
|
if len(answer.MediaDescriptions) < 2 {
|
|
return false
|
|
}
|
|
|
|
for _, md := range answer.MediaDescriptions {
|
|
if md.MediaName.Media == "video" {
|
|
desc = md
|
|
break
|
|
}
|
|
}
|
|
return desc != nil
|
|
}, waitTimeout, waitTick, "did not receive answer")
|
|
|
|
hasSeenVP8 := false
|
|
for _, a := range desc.Attributes {
|
|
if a.Key == "rtpmap" {
|
|
require.NotContains(t, a.Value, mime.MimeTypeCodecH264.String(), "should not contain H264 codec")
|
|
if strings.Contains(a.Value, mime.MimeTypeCodecVP8.String()) {
|
|
hasSeenVP8 = true
|
|
}
|
|
}
|
|
}
|
|
require.True(t, hasSeenVP8, "should have seen VP8 codec in SDP")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSubscribeToCodecUnsupported(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupSingleNodeTest("TestSubscribeToCodecUnsupported")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, nil)
|
|
// create a client that doesn't support H264
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
DisabledCodecs: []webrtc.RTPCodecCapability{
|
|
{MimeType: "video/H264"},
|
|
},
|
|
})
|
|
waitUntilConnected(t, c1, c2)
|
|
|
|
// publish a vp8 video track and ensure c2 receives it ok
|
|
t1, err := c1.AddStaticTrack("audio/opus", "audio", "webcam")
|
|
require.NoError(t, err)
|
|
defer t1.Stop()
|
|
t2, err := c1.AddStaticTrack("video/vp8", "video", "webcam")
|
|
require.NoError(t, err)
|
|
defer t2.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()) == 0 {
|
|
return "c2 was not subscribed to anything"
|
|
}
|
|
// should have received two tracks
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 2 {
|
|
return "c2 was not subscribed to tracks from c1"
|
|
}
|
|
|
|
tracks := c2.SubscribedTracks()[c1.ID()]
|
|
for _, t := range tracks {
|
|
if mime.IsMimeTypeStringVP8(t.Codec().MimeType) {
|
|
return ""
|
|
}
|
|
}
|
|
return "did not receive track with vp8"
|
|
})
|
|
require.Nil(t, c2.GetSubscriptionResponseAndClear())
|
|
|
|
// publish a h264 track and ensure c2 got subscription error
|
|
t3, err := c1.AddStaticTrackWithCodec(webrtc.RTPCodecCapability{
|
|
MimeType: "video/h264",
|
|
ClockRate: 90000,
|
|
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
|
}, "videoscreen", "screen")
|
|
defer t3.Stop()
|
|
require.NoError(t, err)
|
|
|
|
var h264TrackID string
|
|
require.Eventually(t, func() bool {
|
|
remoteC1 := c2.GetRemoteParticipant(c1.ID())
|
|
require.NotNil(t, remoteC1)
|
|
for _, track := range remoteC1.Tracks {
|
|
if mime.IsMimeTypeStringH264(track.MimeType) {
|
|
h264TrackID = track.Sid
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}, time.Second, 10*time.Millisecond, "did not receive track info with h264")
|
|
|
|
require.Eventually(t, func() bool {
|
|
sr := c2.GetSubscriptionResponseAndClear()
|
|
if sr == nil {
|
|
return false
|
|
}
|
|
require.Equal(t, h264TrackID, sr.TrackSid)
|
|
require.Equal(t, livekit.SubscriptionError_SE_CODEC_UNSUPPORTED, sr.Err)
|
|
return true
|
|
}, 5*time.Second, 10*time.Millisecond, "did not receive subscription response")
|
|
|
|
// publish another vp8 track again, ensure the transport recovered by sfu and c2 can receive it
|
|
t4, err := c1.AddStaticTrack("video/vp8", "video2", "webcam2")
|
|
require.NoError(t, err)
|
|
defer t4.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedTracks()) == 0 {
|
|
return "c2 was not subscribed to anything"
|
|
}
|
|
// should have received two tracks
|
|
if len(c2.SubscribedTracks()[c1.ID()]) != 3 {
|
|
return "c2 was not subscribed to tracks from c1"
|
|
}
|
|
|
|
var vp8Count int
|
|
tracks := c2.SubscribedTracks()[c1.ID()]
|
|
for _, t := range tracks {
|
|
if mime.IsMimeTypeStringVP8(t.Codec().MimeType) {
|
|
vp8Count++
|
|
}
|
|
}
|
|
if vp8Count == 2 {
|
|
return ""
|
|
}
|
|
return "did not 2 receive track with vp8"
|
|
})
|
|
require.Nil(t, c2.GetSubscriptionResponseAndClear())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDataPublishSlowSubscriber(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
dataChannelSlowThreshold := 21024
|
|
|
|
logger.Infow("----------------STARTING TEST----------------", "test", t.Name())
|
|
s := createSingleNodeServer(func(c *config.Config) {
|
|
c.RTC.DatachannelSlowThreshold = dataChannelSlowThreshold
|
|
})
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
|
|
waitForServerToStart(s)
|
|
|
|
defer func() {
|
|
s.Stop(true)
|
|
logger.Infow("----------------FINISHING TEST----------------", "test", t.Name())
|
|
}()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, nil)
|
|
fastSub := createRTCClient("fastSub", defaultServerPort, testRTCServicePath, nil)
|
|
slowSubNotDrop := createRTCClient("slowSubNotDrop", defaultServerPort, testRTCServicePath, nil)
|
|
slowSubDrop := createRTCClient("slowSubDrop", defaultServerPort, testRTCServicePath, nil)
|
|
waitUntilConnected(t, pub, fastSub, slowSubDrop, slowSubNotDrop)
|
|
defer func() {
|
|
pub.Stop()
|
|
fastSub.Stop()
|
|
slowSubNotDrop.Stop()
|
|
slowSubDrop.Stop()
|
|
}()
|
|
|
|
// no data should be dropped for fast subscriber
|
|
var fastDataIndex atomic.Uint64
|
|
fastSub.SetOnDataReceived(func(data []byte, sid string) {
|
|
idx := binary.BigEndian.Uint64(data[len(data)-8:])
|
|
require.Equal(t, fastDataIndex.Load()+1, idx)
|
|
fastDataIndex.Store(idx)
|
|
})
|
|
|
|
// no data should be dropped for slow subscriber that is above threshold
|
|
var slowNoDropDataIndex atomic.Uint64
|
|
var drainSlowSubNotDrop atomic.Bool
|
|
slowNoDropReader := testclient.NewDataChannelReader(dataChannelSlowThreshold * 2)
|
|
slowSubNotDrop.SetOnDataReceived(func(data []byte, sid string) {
|
|
idx := binary.BigEndian.Uint64(data[len(data)-8:])
|
|
require.Equal(t, slowNoDropDataIndex.Load()+1, idx)
|
|
slowNoDropDataIndex.Store(idx)
|
|
if !drainSlowSubNotDrop.Load() {
|
|
slowNoDropReader.Read(data, sid)
|
|
}
|
|
})
|
|
|
|
// data should be dropped for slow subscriber that is below threshold
|
|
var slowDropDataIndex atomic.Uint64
|
|
dropped := make(chan struct{})
|
|
slowDropReader := testclient.NewDataChannelReader(dataChannelSlowThreshold / 2)
|
|
slowSubDrop.SetOnDataReceived(func(data []byte, sid string) {
|
|
select {
|
|
case <-dropped:
|
|
return
|
|
default:
|
|
}
|
|
idx := binary.BigEndian.Uint64(data[len(data)-8:])
|
|
if idx != slowDropDataIndex.Load()+1 {
|
|
close(dropped)
|
|
}
|
|
slowDropDataIndex.Store(idx)
|
|
slowDropReader.Read(data, sid)
|
|
})
|
|
|
|
// publisher sends data as fast as possible, it will block by the slowest subscriber above the slow threshold
|
|
var (
|
|
blocked atomic.Bool
|
|
stopWrite atomic.Bool
|
|
writeIdx atomic.Uint64
|
|
)
|
|
writeStopped := make(chan struct{})
|
|
go func() {
|
|
defer close(writeStopped)
|
|
var i int
|
|
buf := make([]byte, 100)
|
|
for !stopWrite.Load() {
|
|
i++
|
|
binary.BigEndian.PutUint64(buf[len(buf)-8:], uint64(i))
|
|
if err := pub.PublishData(buf, livekit.DataPacket_RELIABLE); err != nil {
|
|
if errors.Is(err, datachannel.ErrDataDroppedBySlowReader) {
|
|
blocked.Store(true)
|
|
i--
|
|
continue
|
|
} else {
|
|
t.Log("error writing", err)
|
|
break
|
|
}
|
|
}
|
|
writeIdx.Store(uint64(i))
|
|
}
|
|
}()
|
|
|
|
<-dropped
|
|
|
|
time.Sleep(time.Second)
|
|
blocked.Store(false)
|
|
require.Eventually(t, func() bool { return blocked.Load() }, 30*time.Second, 100*time.Millisecond)
|
|
stopWrite.Store(true)
|
|
<-writeStopped
|
|
drainSlowSubNotDrop.Store(true)
|
|
require.Eventually(t, func() bool {
|
|
return writeIdx.Load() == fastDataIndex.Load() &&
|
|
writeIdx.Load() == slowNoDropDataIndex.Load()
|
|
}, 10*time.Second, 50*time.Millisecond, "writeIdx %d, fast %d, slowNoDrop %d", writeIdx.Load(), fastDataIndex.Load(), slowNoDropDataIndex.Load())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFireTrackBySdp(t *testing.T) {
|
|
_, finish := setupSingleNodeTest("TestFireTrackBySdp")
|
|
defer finish()
|
|
|
|
var cases = []struct {
|
|
name string
|
|
codecs []webrtc.RTPCodecCapability
|
|
pubSDK livekit.ClientInfo_SDK
|
|
}{
|
|
{
|
|
name: "js client could pub a/v tracks",
|
|
codecs: []webrtc.RTPCodecCapability{
|
|
{MimeType: mime.MimeTypeH264.String()},
|
|
{MimeType: mime.MimeTypeOpus.String()},
|
|
},
|
|
pubSDK: livekit.ClientInfo_JS,
|
|
},
|
|
{
|
|
name: "go client could pub audio tracks",
|
|
codecs: []webrtc.RTPCodecCapability{
|
|
{MimeType: "audio/opus"},
|
|
},
|
|
pubSDK: livekit.ClientInfo_GO,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
codecs, sdk := c.codecs, c.pubSDK
|
|
t.Run(c.name, func(t *testing.T) {
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient(c.name+"_c1", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
ClientInfo: &livekit.ClientInfo{
|
|
Sdk: sdk,
|
|
},
|
|
})
|
|
c2 := createRTCClient(c.name+"_c2", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
ClientInfo: &livekit.ClientInfo{
|
|
Sdk: livekit.ClientInfo_JS,
|
|
},
|
|
})
|
|
waitUntilConnected(t, c1, c2)
|
|
defer func() {
|
|
c1.Stop()
|
|
c2.Stop()
|
|
}()
|
|
|
|
// publish tracks and don't write any packets
|
|
for _, codec := range codecs {
|
|
_, err := c1.AddStaticTrackWithCodec(codec, codec.MimeType, codec.MimeType, testclient.AddTrackNoWriter())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.Eventually(t, func() bool {
|
|
return len(c2.SubscribedTracks()[c1.ID()]) == len(codecs)
|
|
}, 5*time.Second, 10*time.Millisecond)
|
|
|
|
var found int
|
|
for _, pubTrack := range c1.GetPublishedTrackIDs() {
|
|
t.Log("pub track", pubTrack)
|
|
tracks := c2.SubscribedTracks()[c1.ID()]
|
|
for _, track := range tracks {
|
|
t.Log("sub track", track.ID(), track.Codec())
|
|
if track.Codec().PayloadType == 0 && track.ID() == pubTrack {
|
|
found++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
require.Equal(t, len(codecs), found)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSinglePublisherDataTrack(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
s, finish := setupSingleNodeTest("TestSinglePublisherDataTrack")
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
c1 := createRTCClient("c1", defaultServerPort, testRTCServicePath, &testclient.Options{AutoSubscribeDataTrack: true})
|
|
c2 := createRTCClient("c2", defaultServerPort, testRTCServicePath, &testclient.Options{AutoSubscribeDataTrack: true})
|
|
waitUntilConnected(t, c1, c2)
|
|
|
|
// publish a couple of data tracks and ensure clients receive it ok
|
|
dt1, err := c1.PublishDataTrack()
|
|
require.NoError(t, err)
|
|
defer dt1.Stop()
|
|
|
|
dt2, err := c1.PublishDataTrack()
|
|
require.NoError(t, err)
|
|
defer dt2.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c2.SubscribedDataTracks()) == 0 {
|
|
return "c2 was not subscribed to any data tracks"
|
|
}
|
|
// should have received two data tracks
|
|
if len(c2.SubscribedDataTracks()[c1.ID()]) != 2 {
|
|
return "c2 didn't subscribe to both data tracks from c1"
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// a new client joins and should get the initial stream
|
|
c3 := createRTCClient("c3", defaultServerPort, testRTCServicePath, &testclient.Options{AutoSubscribeDataTrack: true})
|
|
|
|
// ensure that new client that has joined also received data tracks
|
|
waitUntilConnected(t, c3)
|
|
testutils.WithTimeout(t, func() string {
|
|
if len(c3.SubscribedDataTracks()) == 0 {
|
|
return "c3 didn't subscribe to any data tracks"
|
|
}
|
|
// should have received two data tracks
|
|
if len(c3.SubscribedDataTracks()[c1.ID()]) != 2 {
|
|
return "c3 didn't subscribe to tracks from c1"
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// ensure that the data track ids are generated by server
|
|
tracks := c3.SubscribedDataTracks()[c1.ID()]
|
|
for _, tr := range tracks {
|
|
require.True(t, strings.HasPrefix(string(tr.ID()), "DTR_"), "data track should begin with DTR")
|
|
}
|
|
|
|
// when c3 disconnects, ensure subscriber is cleaned up correctly
|
|
c3.Stop()
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
room := s.RoomManager().GetRoom(context.Background(), testRoom)
|
|
p := room.GetParticipant("c1")
|
|
require.NotNil(t, p)
|
|
|
|
for _, t := range p.GetPublishedDataTracks() {
|
|
if t.IsSubscriber(c3.ID()) {
|
|
return "c3 was not a subscriber of c1's data tracks"
|
|
}
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTurnRelay(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
allowRestrictedPeerCIDRs []string
|
|
denyPeerCIDRs []string
|
|
expectedToConnect bool
|
|
}{
|
|
{
|
|
"allow",
|
|
[]string{"10.0.0.0/8", "192.168.0.0/16"},
|
|
nil,
|
|
true,
|
|
},
|
|
{
|
|
"not-allowed",
|
|
nil,
|
|
nil,
|
|
false,
|
|
},
|
|
{
|
|
"denied-overrides-allowed",
|
|
[]string{"10.0.0.0/8", "192.168.0.0/16"},
|
|
[]string{"10.0.0.0/8", "192.168.0.0/16"},
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := createSingleNodeServer(func(c *config.Config) {
|
|
c.TURN.Enabled = true
|
|
c.TURN.UDPPort = 3478
|
|
c.TURN.AllowRestrictedPeerCIDRs = tc.allowRestrictedPeerCIDRs
|
|
c.TURN.DenyPeerCIDRs = tc.denyPeerCIDRs
|
|
})
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
defer s.Stop(true)
|
|
|
|
waitForServerToStart(s)
|
|
|
|
c1 := createRTCClient("relay_c1", defaultServerPort, testRTCServicePathv0, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
ForceRelay: true,
|
|
})
|
|
defer c1.Stop()
|
|
|
|
if tc.expectedToConnect {
|
|
waitUntilConnected(t, c1)
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
if !c1.IsLocalCandidateRelaySelected() {
|
|
return "expected local candidate to be relay"
|
|
}
|
|
return ""
|
|
})
|
|
} else {
|
|
ensureNotConnected(t, c1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTurnAuthFailure(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
const turnUDPPort = 3478
|
|
|
|
s := createSingleNodeServer(func(c *config.Config) {
|
|
c.TURN.Enabled = true
|
|
c.TURN.UDPPort = turnUDPPort
|
|
})
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
defer s.Stop(true)
|
|
|
|
waitForServerToStart(s)
|
|
|
|
// build a known-good username/password pair so individual cases can mutate
|
|
// only the part they are exercising.
|
|
pID := livekit.ParticipantID("PA_authfail")
|
|
authHandler := service.NewTURNAuthHandler(auth.NewSimpleKeyProvider(testApiKey, testApiSecret))
|
|
validUsername, validExpiry := authHandler.CreateUsername(testApiKey, pID, 300)
|
|
validPassword, err := authHandler.CreatePassword(testApiKey, pID, validExpiry)
|
|
require.NoError(t, err)
|
|
|
|
// username encoded with an already-expired timestamp; constructed directly
|
|
// because CreateUsername floors non-positive TTLs to the default.
|
|
expiredUsername := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", testApiKey, pID, time.Now().Add(-time.Minute).Unix()))
|
|
|
|
// username encoded with an api key the server does not know about.
|
|
unknownAPIKeyUsername, _ := authHandler.CreateUsername("unknown-api-key", pID, 300)
|
|
|
|
// password whose hash was generated for an expiry that doesn't match the
|
|
// one encoded in the username. The server reconstructs the password using
|
|
// the username's expiry, so the integrity check fails.
|
|
mismatchedExpiryPassword, err := authHandler.CreatePassword(testApiKey, pID, validExpiry+60)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, validPassword, mismatchedExpiryPassword)
|
|
|
|
// username carrying expiry=0 must be rejected outright; constructed
|
|
// directly because CreateUsername always stamps a real expiry.
|
|
zeroExpiryUsername := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", testApiKey, pID, 0))
|
|
|
|
// username with only apiKey|pID (no expiry component) is the legacy
|
|
// pre-expiry form and must be rejected.
|
|
twoPartUsername := base62.EncodeToString(fmt.Appendf(nil, "%s|%s", testApiKey, pID))
|
|
|
|
testCases := []struct {
|
|
name string
|
|
username string
|
|
password string
|
|
}{
|
|
{
|
|
name: "unparseable-username",
|
|
username: "not-base62!!!",
|
|
password: validPassword,
|
|
},
|
|
{
|
|
name: "wrong-password",
|
|
username: validUsername,
|
|
password: "wrongpassword",
|
|
},
|
|
{
|
|
name: "expired-username",
|
|
username: expiredUsername,
|
|
password: validPassword,
|
|
},
|
|
{
|
|
name: "unknown-api-key",
|
|
username: unknownAPIKeyUsername,
|
|
password: validPassword,
|
|
},
|
|
{
|
|
name: "password-expiry-mismatch",
|
|
username: validUsername,
|
|
password: mismatchedExpiryPassword,
|
|
},
|
|
{
|
|
name: "zero-expiry-username",
|
|
username: zeroExpiryUsername,
|
|
password: validPassword,
|
|
},
|
|
{
|
|
name: "two-part-username",
|
|
username: twoPartUsername,
|
|
password: validPassword,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
conn, err := net.ListenPacket("udp4", "0.0.0.0:0")
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
client, err := turn.NewClient(&turn.ClientConfig{
|
|
TURNServerAddr: fmt.Sprintf("127.0.0.1:%d", turnUDPPort),
|
|
Username: tc.username,
|
|
Password: tc.password,
|
|
Realm: service.LivekitRealm,
|
|
Conn: conn,
|
|
})
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
require.NoError(t, client.Listen())
|
|
|
|
_, allocErr := client.Allocate()
|
|
require.Error(t, allocErr)
|
|
|
|
// pion's TURN server replies with 400 Bad Request for any
|
|
// authenticated-allocate failure (unknown user or integrity check
|
|
// mismatch); the initial unauthenticated probe is what returns 401.
|
|
var turnErr *stun.TurnError
|
|
require.ErrorAs(t, allocErr, &turnErr)
|
|
require.Equal(t, stun.CodeBadRequest, turnErr.ErrorCodeAttr.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// dataBlobCapture buffers RequestResponse, StoreDataBlobResponse, and GetDataBlobResponse messages
|
|
// sent to a test client so they can be asserted on. Other messages flow through to the
|
|
// default handler.
|
|
type dataBlobCapture struct {
|
|
mu sync.Mutex
|
|
requestResponses []*livekit.RequestResponse
|
|
storeResponses []*livekit.StoreDataBlobResponse
|
|
blobResponses []*livekit.GetDataBlobResponse
|
|
}
|
|
|
|
func (c *dataBlobCapture) interceptor() testclient.SignalResponseInterceptor {
|
|
return func(msg *livekit.SignalResponse, next testclient.SignalResponseHandler) error {
|
|
switch m := msg.Message.(type) {
|
|
case *livekit.SignalResponse_RequestResponse:
|
|
c.mu.Lock()
|
|
c.requestResponses = append(c.requestResponses, m.RequestResponse)
|
|
c.mu.Unlock()
|
|
case *livekit.SignalResponse_StoreDataBlobResponse:
|
|
c.mu.Lock()
|
|
c.storeResponses = append(c.storeResponses, m.StoreDataBlobResponse)
|
|
c.mu.Unlock()
|
|
case *livekit.SignalResponse_GetDataBlobResponse:
|
|
c.mu.Lock()
|
|
c.blobResponses = append(c.blobResponses, m.GetDataBlobResponse)
|
|
c.mu.Unlock()
|
|
}
|
|
return next(msg)
|
|
}
|
|
}
|
|
|
|
func (c *dataBlobCapture) takeRequestResponse() *livekit.RequestResponse {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.requestResponses) == 0 {
|
|
return nil
|
|
}
|
|
rr := c.requestResponses[0]
|
|
c.requestResponses = c.requestResponses[1:]
|
|
return rr
|
|
}
|
|
|
|
func (c *dataBlobCapture) takeStoreResponse() *livekit.StoreDataBlobResponse {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.storeResponses) == 0 {
|
|
return nil
|
|
}
|
|
sr := c.storeResponses[0]
|
|
c.storeResponses = c.storeResponses[1:]
|
|
return sr
|
|
}
|
|
|
|
func (c *dataBlobCapture) takeBlobResponse() *livekit.GetDataBlobResponse {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.blobResponses) == 0 {
|
|
return nil
|
|
}
|
|
sr := c.blobResponses[0]
|
|
c.blobResponses = c.blobResponses[1:]
|
|
return sr
|
|
}
|
|
|
|
func (c *dataBlobCapture) requestResponseCount() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return len(c.requestResponses)
|
|
}
|
|
|
|
func setupDataBlobServer(t *testing.T, name string, enable bool) (*service.LivekitServer, func()) {
|
|
logger.Infow("----------------STARTING TEST----------------", "test", name)
|
|
s := createSingleNodeServer(func(c *config.Config) {
|
|
c.EnableParticipantDataBlob = enable
|
|
c.Limit.MaxDataBlobSize = 1024
|
|
})
|
|
go func() {
|
|
if err := s.Start(); err != nil {
|
|
logger.Errorw("server returned error", err)
|
|
}
|
|
}()
|
|
waitForServerToStart(s)
|
|
return s, func() {
|
|
s.Stop(true)
|
|
logger.Infow("----------------FINISHING TEST----------------", "test", name)
|
|
}
|
|
}
|
|
|
|
func TestSingleNodeDataBlob(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupDataBlobServer(t, "TestSingleNodeDataBlob", true)
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
pubCapture := &dataBlobCapture{}
|
|
subCapture := &dataBlobCapture{}
|
|
|
|
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
SignalResponseInterceptor: pubCapture.interceptor(),
|
|
})
|
|
sub := createRTCClient("sub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
SignalResponseInterceptor: subCapture.interceptor(),
|
|
})
|
|
waitUntilConnected(t, pub, sub)
|
|
defer stopClients(pub, sub)
|
|
|
|
key := &livekit.DataBlobKey{
|
|
Key: &livekit.DataBlobKey_Generic{
|
|
Generic: "blob-1",
|
|
},
|
|
}
|
|
contents := []byte("definition-bytes")
|
|
|
|
// publisher stores a blob
|
|
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
|
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
|
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
|
RequestId: 1,
|
|
Blob: &livekit.DataBlob{
|
|
Key: key,
|
|
Contents: contents,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
resp := pubCapture.takeStoreResponse()
|
|
if resp == nil {
|
|
return "publisher did not receive store response"
|
|
}
|
|
if resp.RequestId != 1 {
|
|
return fmt.Sprintf("expected store response request id 1, got %d", resp.RequestId)
|
|
}
|
|
if resp.Key == nil {
|
|
return "store response missing key"
|
|
}
|
|
if resp.Key.String() != key.String() {
|
|
return fmt.Sprintf("expected stored blob key %s, got %s", key.String(), resp.Key.String())
|
|
}
|
|
return ""
|
|
})
|
|
require.Equal(t, 0, pubCapture.requestResponseCount(), "publisher should not receive an error response on success")
|
|
|
|
// subscriber asks for the blob
|
|
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
|
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
|
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
|
ParticipantIdentity: "pub",
|
|
Key: key,
|
|
},
|
|
},
|
|
}))
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
resp := subCapture.takeBlobResponse()
|
|
if resp == nil {
|
|
return "subscriber did not receive blob response"
|
|
}
|
|
if resp.Blob == nil {
|
|
return "blob response missing blob"
|
|
}
|
|
if resp.Blob.Key.String() != key.String() {
|
|
return fmt.Sprintf("expected blob key %s, got %s", key.String(), resp.Blob.Key.String())
|
|
}
|
|
if string(resp.Blob.Contents) != string(contents) {
|
|
return fmt.Sprintf("expected contents %q, got %q", contents, resp.Blob.Contents)
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// subscriber asks for an unknown blob on a known publisher
|
|
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
|
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
|
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
|
ParticipantIdentity: "pub",
|
|
Key: &livekit.DataBlobKey{
|
|
Key: &livekit.DataBlobKey_Generic{
|
|
Generic: "does-not-exist",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
rr := subCapture.takeRequestResponse()
|
|
if rr == nil {
|
|
return "subscriber did not receive RequestResponse for missing blob"
|
|
}
|
|
if rr.Reason != livekit.RequestResponse_NOT_FOUND {
|
|
return fmt.Sprintf("expected NOT_FOUND, got %s", rr.Reason)
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// subscriber asks for a blob on an unknown publisher identity
|
|
require.NoError(t, sub.SendRequest(&livekit.SignalRequest{
|
|
Message: &livekit.SignalRequest_GetDataBlobRequest{
|
|
GetDataBlobRequest: &livekit.GetDataBlobRequest{
|
|
ParticipantIdentity: "unknown-publisher",
|
|
Key: key,
|
|
},
|
|
},
|
|
}))
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
rr := subCapture.takeRequestResponse()
|
|
if rr == nil {
|
|
return "subscriber did not receive RequestResponse for unknown publisher"
|
|
}
|
|
if rr.Reason != livekit.RequestResponse_NOT_FOUND {
|
|
return fmt.Sprintf("expected NOT_FOUND, got %s", rr.Reason)
|
|
}
|
|
return ""
|
|
})
|
|
|
|
// publisher sends an invalid blob (empty key)
|
|
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
|
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
|
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
|
Blob: &livekit.DataBlob{
|
|
Contents: contents,
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
rr := pubCapture.takeRequestResponse()
|
|
if rr == nil {
|
|
return "publisher did not receive RequestResponse for invalid define"
|
|
}
|
|
if rr.Reason != livekit.RequestResponse_INVALID_REQUEST {
|
|
return fmt.Sprintf("expected INVALID_REQUEST, got %s", rr.Reason)
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSingleNodeDataBlobDisabled(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
return
|
|
}
|
|
|
|
_, finish := setupDataBlobServer(t, "TestSingleNodeDataBlobDisabled", false)
|
|
defer finish()
|
|
|
|
for _, testRTCServicePath := range testRTCServicePaths {
|
|
t.Run(fmt.Sprintf("testRTCServicePath=%s", testRTCServicePath.String()), func(t *testing.T) {
|
|
pubCapture := &dataBlobCapture{}
|
|
pub := createRTCClient("pub", defaultServerPort, testRTCServicePath, &testclient.Options{
|
|
AutoSubscribe: true,
|
|
SignalResponseInterceptor: pubCapture.interceptor(),
|
|
})
|
|
waitUntilConnected(t, pub)
|
|
defer stopClients(pub)
|
|
|
|
require.NoError(t, pub.SendRequest(&livekit.SignalRequest{
|
|
Message: &livekit.SignalRequest_StoreDataBlobRequest{
|
|
StoreDataBlobRequest: &livekit.StoreDataBlobRequest{
|
|
Blob: &livekit.DataBlob{
|
|
Key: &livekit.DataBlobKey{
|
|
Key: &livekit.DataBlobKey_Generic{
|
|
Generic: "blob-1",
|
|
},
|
|
},
|
|
Contents: []byte("definition-bytes"),
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
testutils.WithTimeout(t, func() string {
|
|
rr := pubCapture.takeRequestResponse()
|
|
if rr == nil {
|
|
return "publisher did not receive RequestResponse"
|
|
}
|
|
if rr.Reason != livekit.RequestResponse_NOT_ALLOWED {
|
|
return fmt.Sprintf("expected NOT_ALLOWED, got %s", rr.Reason)
|
|
}
|
|
return ""
|
|
})
|
|
})
|
|
}
|
|
}
|