Merge commit '6bd7fac875d9e9009915053d8a590abb372c5679' into feature/livekit-upgrade-1.13.1

This commit is contained in:
2026-06-25 23:54:33 +09:00
291 changed files with 37631 additions and 15381 deletions
+44 -7
View File
@@ -12,6 +12,7 @@ import (
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/agent/testutils"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils/guid"
@@ -20,6 +21,7 @@ import (
)
func TestAgent(t *testing.T) {
testAgentName := "test_agent"
t.Run("dispatched jobs are assigned to a worker", func(t *testing.T) {
bus := psrpc.NewLocalMessageBus()
@@ -28,7 +30,7 @@ func TestAgent(t *testing.T) {
t.Cleanup(server.Close)
worker := server.SimulateAgentWorker()
worker.Register("test", livekit.JobType_JT_ROOM)
worker.Register(testAgentName, livekit.JobType_JT_ROOM)
jobAssignments := worker.JobAssignments.Observe()
job := &livekit.Job{
@@ -36,14 +38,19 @@ func TestAgent(t *testing.T) {
DispatchId: guid.New(guid.AgentDispatchPrefix),
Type: livekit.JobType_JT_ROOM,
Room: &livekit.Room{},
AgentName: "test",
AgentName: testAgentName,
}
_, err := client.JobRequest(context.Background(), "test", agent.RoomAgentTopic, job)
_, err := client.JobRequest(context.Background(), testAgentName, agent.RoomAgentTopic, job)
require.NoError(t, err)
select {
case a := <-jobAssignments.Events():
require.EqualValues(t, job.Id, a.Job.Id)
v, err := auth.ParseAPIToken(a.Token)
require.NoError(t, err)
_, claims, err := v.Verify(server.TestAPISecret)
require.NoError(t, err)
require.Equal(t, testAgentName, claims.Attributes[agent.AgentNameAttributeKey])
case <-time.After(time.Second):
require.Fail(t, "job assignment timeout")
}
@@ -108,7 +115,7 @@ func TestAgentLoadBalancing(t *testing.T) {
t.Cleanup(server.Close)
agents := make([]*testutils.AgentWorker, totalWorkers)
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
agents[i] = server.SimulateAgentWorker(
testutils.WithLabel(fmt.Sprintf("agent-%d", i)),
testutils.WithJobLoad(testutils.NewStableJobLoad(0.01)),
@@ -128,7 +135,7 @@ func TestAgentLoadBalancing(t *testing.T) {
}
// check that jobs are distributed normally
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
label := fmt.Sprintf("agent-%d", i)
require.GreaterOrEqual(t, jobCount[label], 0)
require.Less(t, jobCount[label], 35) // three std deviations from the mean is 32
@@ -147,7 +154,7 @@ func TestAgentLoadBalancing(t *testing.T) {
t.Cleanup(server.Close)
agents := make([]*testutils.AgentWorker, totalWorkers)
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
label := fmt.Sprintf("agent-%d", i)
if i%2 == 0 {
// make sure we have some workers that can accept jobs
@@ -169,7 +176,7 @@ func TestAgentLoadBalancing(t *testing.T) {
jobCount[w.Label] = len(w.Jobs())
}
for i := 0; i < totalWorkers; i++ {
for i := range totalWorkers {
label := fmt.Sprintf("agent-%d", i)
if i%2 == 0 {
@@ -181,3 +188,33 @@ func TestAgentLoadBalancing(t *testing.T) {
}
})
}
func TestConnectionClosedOnDispatchError(t *testing.T) {
t.Run("connection closed when unknown message type received", func(t *testing.T) {
bus := psrpc.NewLocalMessageBus()
server := testutils.NewTestServer(bus)
t.Cleanup(server.Close)
// register agent
worker := server.SimulateAgentWorker()
worker.Register("test_agent", livekit.JobType_JT_ROOM)
responses := worker.RegisterWorkerResponses.Observe()
select {
case <-responses.Events():
// registered
case <-time.After(time.Second):
require.Fail(t, "registration timeout")
}
responses.Stop()
// send invalid message (nil Message field triggers ErrUnknownWorkerSignal)
worker.SendMessage(&livekit.WorkerMessage{Message: nil})
select {
case <-worker.Closed():
// connection closed
case <-time.After(time.Second):
require.Fail(t, "connection should have been closed after dispatch error")
}
})
}
+25 -15
View File
@@ -62,11 +62,13 @@ type JobRequest struct {
// only set for participant jobs
Participant *livekit.ParticipantInfo
Metadata string
AgentName string
AgentName string
Deployment string
}
type agentClient struct {
client rpc.AgentInternalClient
config Config
mu sync.RWMutex
@@ -87,7 +89,7 @@ type agentClient struct {
subDone chan struct{}
}
func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
func NewAgentClient(bus psrpc.MessageBus, config Config) (Client, error) {
client, err := rpc.NewAgentInternalClient(bus)
if err != nil {
return nil, err
@@ -95,6 +97,7 @@ func NewAgentClient(bus psrpc.MessageBus) (Client, error) {
c := &agentClient{
client: client,
config: config,
workers: workerpool.New(50),
subDone: make(chan struct{}),
}
@@ -152,21 +155,23 @@ func (c *agentClient) LaunchJob(ctx context.Context, desc *JobRequest) *serverut
}
dispatcher.ForEach(func(curNs string) {
topic := GetAgentTopic(desc.AgentName, curNs)
topic := GetAgentTopic(desc.AgentName, curNs, desc.Deployment)
wg.Add(1)
c.workers.Submit(func() {
defer wg.Done()
// The cached agent parameters do not provide the exact combination of available job type/agent name/namespace, so some of the JobRequest RPC may not trigger any worker
job := &livekit.Job{
Id: utils.NewGuid(utils.AgentJobPrefix),
DispatchId: desc.DispatchId,
Type: desc.JobType,
Room: desc.Room,
Participant: desc.Participant,
Namespace: curNs,
AgentName: desc.AgentName,
Metadata: desc.Metadata,
Id: utils.NewGuid(utils.AgentJobPrefix),
DispatchId: desc.DispatchId,
Type: desc.JobType,
Room: desc.Room,
Participant: desc.Participant,
Namespace: curNs,
AgentName: desc.AgentName,
Metadata: desc.Metadata,
EnableRecording: c.config.EnableUserDataRecording,
Deployment: desc.Deployment,
}
resp, err := c.client.JobRequest(context.Background(), topic, jobTypeTopic, job)
if err != nil {
@@ -323,14 +328,19 @@ func (c *agentClient) Stop() error {
return nil
}
func GetAgentTopic(agentName, namespace string) string {
func GetAgentTopic(agentName, namespace, deployment string) string {
var topic string
if agentName == "" {
// Backward compatibility
return namespace
topic = namespace
} else if namespace == "" {
// Forward compatibility once the namespace field is removed from the worker SDK
return agentName
topic = agentName
} else {
return fmt.Sprintf("%s_%s", agentName, namespace)
topic = fmt.Sprintf("%s_%s", agentName, namespace)
}
if deployment != "" {
topic += "_" + deployment
}
return topic
}
+8
View File
@@ -0,0 +1,8 @@
package agent
const DefaultTargetLoad = 0.7
type Config struct {
EnableUserDataRecording bool `yaml:"enable_user_data_recording"`
TargetLoad float32 `yaml:"target_load,omitempty"`
}
+31 -14
View File
@@ -4,14 +4,15 @@ import (
"context"
"errors"
"io"
"maps"
"math"
"math/rand/v2"
"slices"
"sync"
"time"
"github.com/frostbyte73/core"
"github.com/gammazero/deque"
"golang.org/x/exp/maps"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/config"
@@ -27,18 +28,25 @@ import (
)
type AgentService interface {
HandleConnection(context.Context, agent.SignalConn, agent.WorkerProtocolVersion)
HandleConnection(context.Context, agent.SignalConn, agent.WorkerRegistration)
DrainConnections(time.Duration)
}
type TestServer struct {
AgentService
TestAPIKey string
TestAPISecret string
}
func NewTestServer(bus psrpc.MessageBus) *TestServer {
localNode, _ := routing.NewLocalNode(nil)
return NewTestServerWithService(must.Get(service.NewAgentService(
&config.Config{Region: "test"},
&config.Config{
Region: "test",
Agents: agent.Config{
TargetLoad: agent.DefaultTargetLoad,
},
},
localNode,
bus,
auth.NewSimpleKeyProvider("test", "verysecretsecret"),
@@ -46,7 +54,7 @@ func NewTestServer(bus psrpc.MessageBus) *TestServer {
}
func NewTestServerWithService(s AgentService) *TestServer {
return &TestServer{s}
return &TestServer{s, "test", "verysecretsecret"}
}
type SimulatedWorkerOptions struct {
@@ -126,7 +134,7 @@ func (h *TestServer) SimulateAgentWorker(opts ...SimulatedWorkerOption) *AgentWo
}
ctx := service.WithAPIKey(o.Context, &auth.ClaimGrants{}, "test")
go h.HandleConnection(ctx, w, agent.CurrentProtocol)
go h.HandleConnection(ctx, w, agent.MakeWorkerRegistration())
return w
}
@@ -204,6 +212,15 @@ func (w *AgentWorker) Close() error {
return nil
}
func (w *AgentWorker) CloseWithReason(_ string) error {
return w.Close()
}
// Closed returns a channel that is closed when the connection is closed by the server
func (w *AgentWorker) Closed() <-chan struct{} {
return w.fuse.Watch()
}
func (w *AgentWorker) SetReadDeadline(t time.Time) error {
w.mu.Lock()
defer w.mu.Unlock()
@@ -315,7 +332,7 @@ func (w *AgentWorker) handlePong(m *livekit.WorkerPong) {
w.WorkerPongs.Emit(m)
}
func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
func (w *AgentWorker) SendMessage(m *livekit.WorkerMessage) {
select {
case <-w.fuse.Watch():
case w.workerMessages <- m:
@@ -323,43 +340,43 @@ func (w *AgentWorker) sendMessage(m *livekit.WorkerMessage) {
}
func (w *AgentWorker) SendRegister(m *livekit.RegisterWorkerRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Register{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Register{
Register: m,
}})
}
func (w *AgentWorker) SendAvailability(m *livekit.AvailabilityResponse) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Availability{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Availability{
Availability: m,
}})
}
func (w *AgentWorker) SendUpdateWorker(m *livekit.UpdateWorkerStatus) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateWorker{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateWorker{
UpdateWorker: m,
}})
}
func (w *AgentWorker) SendUpdateJob(m *livekit.UpdateJobStatus) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateJob{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_UpdateJob{
UpdateJob: m,
}})
}
func (w *AgentWorker) SendPing(m *livekit.WorkerPing) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Ping{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_Ping{
Ping: m,
}})
}
func (w *AgentWorker) SendSimulateJob(m *livekit.SimulateJobRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_SimulateJob{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_SimulateJob{
SimulateJob: m,
}})
}
func (w *AgentWorker) SendMigrateJob(m *livekit.MigrateJobRequest) {
w.sendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_MigrateJob{
w.SendMessage(&livekit.WorkerMessage{Message: &livekit.WorkerMessage_MigrateJob{
MigrateJob: m,
}})
}
@@ -411,7 +428,7 @@ func (w *AgentWorker) SimulateRoomJob(roomName string) {
func (w *AgentWorker) Jobs() []*AgentJob {
w.mu.Lock()
defer w.mu.Unlock()
return maps.Values(w.jobs)
return slices.Collect(maps.Values(w.jobs))
}
type stableJobLoad struct {
+72 -34
View File
@@ -18,12 +18,14 @@ import (
"context"
"errors"
"fmt"
"maps"
"sync"
"time"
"go.uber.org/multierr"
"google.golang.org/protobuf/proto"
pagent "github.com/livekit/protocol/agent"
protoagent "github.com/livekit/protocol/agent"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
@@ -43,6 +45,8 @@ var (
ErrDuplicateJobAssignment = errors.New("duplicate job assignment")
)
const AgentNameAttributeKey = "lk.agent_name"
type WorkerProtocolVersion int
const CurrentProtocol = 1
@@ -57,12 +61,15 @@ type SignalConn interface {
ReadWorkerMessage() (*livekit.WorkerMessage, int, error)
SetReadDeadline(time.Time) error
Close() error
CloseWithReason(reason string) error
}
func JobStatusIsEnded(s livekit.JobStatus) bool {
return s == livekit.JobStatus_JS_SUCCESS || s == livekit.JobStatus_JS_FAILED
}
type AssignmentHook func(next func(*livekit.JobAssignment) error) func(*livekit.JobAssignment) error
type WorkerSignalHandler interface {
HandleRegister(*livekit.RegisterWorkerRequest) error
HandleAvailability(*livekit.AvailabilityResponse) error
@@ -141,10 +148,20 @@ type WorkerRegistration struct {
Protocol WorkerProtocolVersion
ID string
Version string
AgentID string
AgentName string
Namespace string
JobType livekit.JobType
Permissions *livekit.ParticipantPermission
ClientIP string
Deployment string
}
func MakeWorkerRegistration() WorkerRegistration {
return WorkerRegistration{
ID: guid.New(guid.AgentWorkerPrefix),
Protocol: CurrentProtocol,
}
}
var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
@@ -152,18 +169,17 @@ var _ WorkerSignalHandler = (*WorkerRegisterer)(nil)
type WorkerRegisterer struct {
WorkerPingHandler
serverInfo *livekit.ServerInfo
protocol WorkerProtocolVersion
deadline time.Time
registration WorkerRegistration
registered bool
}
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, protocol WorkerProtocolVersion) *WorkerRegisterer {
func NewWorkerRegisterer(conn SignalConn, serverInfo *livekit.ServerInfo, base WorkerRegistration) *WorkerRegisterer {
return &WorkerRegisterer{
WorkerPingHandler: WorkerPingHandler{conn: conn},
serverInfo: serverInfo,
protocol: protocol,
registration: base,
deadline: time.Now().Add(RegisterTimeout),
}
}
@@ -185,6 +201,10 @@ func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) er
return ErrUnknownJobType
}
if err := protoagent.ValidateDeployment(req.GetDeployment()); err != nil {
return err
}
permissions := req.AllowedPermissions
if permissions == nil {
permissions = &livekit.ParticipantPermission{
@@ -195,15 +215,12 @@ func (h *WorkerRegisterer) HandleRegister(req *livekit.RegisterWorkerRequest) er
}
}
h.registration = WorkerRegistration{
Protocol: h.protocol,
ID: guid.New(guid.AgentWorkerPrefix),
Version: req.Version,
AgentName: req.AgentName,
Namespace: req.GetNamespace(),
JobType: req.GetType(),
Permissions: permissions,
}
h.registration.Version = req.Version
h.registration.AgentName = req.AgentName
h.registration.Namespace = req.GetNamespace()
h.registration.JobType = req.GetType()
h.registration.Permissions = permissions
h.registration.Deployment = req.GetDeployment()
h.registered = true
_, err := h.conn.WriteServerMessage(&livekit.ServerMessage{
@@ -256,6 +273,9 @@ func NewWorker(
logger: logger.WithValues(
"workerID", registration.ID,
"agentName", registration.AgentName,
"deployment", registration.Deployment,
"agentID", registration.AgentID,
"version", registration.Version,
"jobType", registration.JobType.String(),
),
@@ -268,10 +288,8 @@ func NewWorker(
}
}
func (w *Worker) sendRequest(req *livekit.ServerMessage) {
if _, err := w.conn.WriteServerMessage(req); err != nil {
w.logger.Warnw("error writing to websocket", err)
}
func (w *Worker) APIKey() string {
return w.apiKey
}
func (w *Worker) Status() livekit.WorkerStatus {
@@ -293,11 +311,7 @@ func (w *Worker) Logger() logger.Logger {
func (w *Worker) RunningJobs() map[livekit.JobID]*livekit.Job {
w.mu.Lock()
defer w.mu.Unlock()
jobs := make(map[livekit.JobID]*livekit.Job, len(w.runningJobs))
for k, v := range w.runningJobs {
jobs[k] = v
}
return jobs
return maps.Clone(w.runningJobs)
}
func (w *Worker) RunningJobCount() int {
@@ -316,7 +330,7 @@ func (w *Worker) GetJobState(jobID livekit.JobID) (*livekit.JobState, error) {
return utils.CloneProto(j.State), nil
}
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobState, error) {
func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job, hook AssignmentHook) (*livekit.JobState, error) {
availCh := make(chan *livekit.AvailabilityResponse, 1)
job = utils.CloneProto(job)
jobID := livekit.JobID(job.Id)
@@ -340,13 +354,17 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
job.State = &livekit.JobState{}
}
now := time.Now()
job.State.WorkerId = w.ID
job.State.AgentId = w.AgentID
job.State.UpdatedAt = now.UnixNano()
job.State.StartedAt = now.UnixNano()
job.State.Status = livekit.JobStatus_JS_RUNNING
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Availability{
if _, err := w.conn.WriteServerMessage(&livekit.ServerMessage{Message: &livekit.ServerMessage_Availability{
Availability: &livekit.AvailabilityRequest{Job: job},
}})
}}); err != nil {
return nil, err
}
timeout := time.NewTimer(AssignJobTimeout)
defer timeout.Stop()
@@ -354,20 +372,31 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
// See handleAvailability for the response
select {
case res := <-availCh:
if res.Terminate {
job.State.EndedAt = now.UnixNano()
job.State.Status = livekit.JobStatus_JS_SUCCESS
return job.State, nil
}
if !res.Available {
return nil, ErrWorkerNotAvailable
}
job.State.ParticipantIdentity = res.ParticipantIdentity
attributes := res.ParticipantAttributes
if attributes == nil {
attributes = make(map[string]string)
}
attributes[AgentNameAttributeKey] = w.AgentName
token, err := pagent.BuildAgentToken(
token, err := protoagent.BuildAgentToken(
w.apiKey,
w.apiSecret,
job.Room.Name,
res.ParticipantIdentity,
res.ParticipantName,
res.ParticipantMetadata,
res.ParticipantAttributes,
attributes,
w.Permissions,
)
if err != nil {
@@ -375,10 +404,18 @@ func (w *Worker) AssignJob(ctx context.Context, job *livekit.Job) (*livekit.JobS
return nil, err
}
// In OSS, Url is nil, and the used API Key is the same as the one used to connect the worker
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Assignment{
Assignment: &livekit.JobAssignment{Job: job, Url: nil, Token: token},
}})
send := func(a *livekit.JobAssignment) error {
_, err := w.conn.WriteServerMessage(&livekit.ServerMessage{
Message: &livekit.ServerMessage_Assignment{Assignment: a},
})
return err
}
if hook != nil {
send = hook(send)
}
if err := send(&livekit.JobAssignment{Job: job, Token: token}); err != nil {
return nil, err
}
state := utils.CloneProto(job.State)
@@ -407,7 +444,7 @@ func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason
return nil, ErrJobNotFound
}
w.sendRequest(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
_, writeErr := w.conn.WriteServerMessage(&livekit.ServerMessage{Message: &livekit.ServerMessage_Termination{
Termination: &livekit.JobTermination{
JobId: string(jobID),
},
@@ -420,11 +457,12 @@ func (w *Worker) TerminateJob(jobID livekit.JobID, reason rpc.JobTerminateReason
errorStr = "agent worker left the room"
}
return w.UpdateJobStatus(&livekit.UpdateJobStatus{
state, updateErr := w.UpdateJobStatus(&livekit.UpdateJobStatus{
JobId: string(jobID),
Status: status,
Error: errorStr,
})
return state, multierr.Combine(writeErr, updateErr)
}
func (w *Worker) UpdateMetadata(metadata string) {
@@ -530,7 +568,7 @@ func (w *Worker) HandleSimulateJob(simulate *livekit.SimulateJobRequest) error {
}
go func() {
_, err := w.AssignJob(w.ctx, job)
_, err := w.AssignJob(w.ctx, job, nil)
if err != nil {
w.logger.Errorw("unable to simulate job", err, "jobID", job.Id)
}
+30 -12
View File
@@ -15,30 +15,48 @@
package clientconfiguration
import (
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils/must"
)
// StaticConfigurations list specific device-side limitations that should be disabled at a global level
var StaticConfigurations = []ConfigurationItem{
// {
// Match: &ScriptMatch{Expr: `c.protocol <= 5 || c.browser == "firefox"`},
// Match: must.Get(NewScriptMatch(`c.protocol <= 5 || c.browser == "firefox"`)),
// Configuration: &livekit.ClientConfiguration{ResumeConnection: livekit.ClientConfigSetting_DISABLED},
// Merge: false,
// },
{
Match: &ScriptMatch{Expr: `c.browser == "safari"`},
Configuration: &livekit.ClientConfiguration{DisabledCodecs: &livekit.DisabledCodecs{Codecs: []*livekit.Codec{
{Mime: mime.MimeTypeAV1.String()},
}}},
Merge: false,
},
{
Match: &ScriptMatch{Expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") ||
((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`},
Match: must.Get(NewScriptMatch(`c.browser == "safari"`)),
Configuration: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Publish: []*livekit.Codec{{Mime: mime.MimeTypeH264.String()}},
Codecs: []*livekit.Codec{
{Mime: mime.MimeTypeAV1.String()},
},
},
},
Merge: true,
},
{
Match: must.Get(NewScriptMatch(`c.browser == "safari" && c.browser_version > "18.3"`)),
Configuration: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Publish: []*livekit.Codec{
{Mime: mime.MimeTypeVP9.String()},
},
},
},
Merge: true,
},
{
Match: must.Get(NewScriptMatch(`(c.device_model == "xiaomi 2201117ti" && c.os == "android") ||
((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`)),
Configuration: &livekit.ClientConfiguration{
DisabledCodecs: &livekit.DisabledCodecs{
Publish: []*livekit.Codec{
{Mime: mime.MimeTypeH264.String()},
},
},
},
Merge: false,
@@ -20,13 +20,14 @@ import (
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/utils/must"
)
func TestScriptMatchConfiguration(t *testing.T) {
t.Run("no merge", func(t *testing.T) {
confs := []ConfigurationItem{
{
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
Match: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
Configuration: &livekit.ClientConfiguration{
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
},
@@ -48,14 +49,14 @@ func TestScriptMatchConfiguration(t *testing.T) {
t.Run("merge", func(t *testing.T) {
confs := []ConfigurationItem{
{
Match: &ScriptMatch{Expr: `c.protocol > 5 && c.browser != "firefox"`},
Match: must.Get(NewScriptMatch(`c.protocol > 5 && c.browser != "firefox"`)),
Configuration: &livekit.ClientConfiguration{
ResumeConnection: livekit.ClientConfigSetting_ENABLED,
},
Merge: true,
},
{
Match: &ScriptMatch{Expr: `c.sdk == "android"`},
Match: must.Get(NewScriptMatch(`c.sdk == "android"`)),
Configuration: &livekit.ClientConfiguration{
Video: &livekit.VideoConfiguration{
HardwareEncoder: livekit.ClientConfigSetting_DISABLED,
@@ -81,10 +82,12 @@ func TestScriptMatchConfiguration(t *testing.T) {
func TestScriptMatch(t *testing.T) {
client := &livekit.ClientInfo{
Protocol: 6,
Browser: "chrome",
Sdk: 3, // android
DeviceModel: "12345",
Protocol: 6,
Browser: "chrome",
Sdk: 3, // android
DeviceModel: "12345",
BrowserVersion: "13.2",
Version: "2.17.1",
}
type testcase struct {
@@ -99,12 +102,26 @@ func TestScriptMatch(t *testing.T) {
{name: "invalid expr", expr: `cc.protocol > 5`, err: true},
{name: "unexist field", expr: `c.protocols > 5`, err: true},
{name: "combined condition", expr: `c.protocol > 5 && (c.sdk=="android" || c.sdk=="ios")`, result: true},
{name: "combined condition2", expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android) || ((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`, result: false},
{name: "combined condition2", expr: `(c.device_model == "xiaomi 2201117ti" && c.os == "android") || ((c.browser == "firefox" || c.browser == "firefox mobile") && (c.os == "linux" || c.os == "android"))`, result: false},
{name: "string lesser", expr: `c.browser_version < "11.3"`, result: false},
{name: "string lesser eq", expr: `c.browser_version <= "13.2"`, result: true},
{name: "string greater", expr: `c.browser_version > "11.3"`, result: true},
{name: "string greater eq", expr: `c.browser_version >= "13.2"`, result: true},
{name: "semantic lesser", expr: `c.version < "2.16.10"`, result: false},
{name: "semantic lesser eq", expr: `c.version <= "2.17.1"`, result: true},
{name: "semantic greater", expr: `c.version > "2.16.10"`, result: true},
{name: "semantic greater eq", expr: `c.version >= "2.17.1"`, result: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
match := &ScriptMatch{Expr: c.expr}
match, err := NewScriptMatch(c.expr)
if err != nil {
if !c.err {
require.NoError(t, err)
}
return
}
m, err := match.Match(client)
if c.err {
require.Error(t, err)
@@ -15,11 +15,13 @@
package clientconfiguration
import (
"context"
"errors"
"fmt"
"strings"
"github.com/d5/tengo/v2"
"github.com/d5/tengo/v2/token"
"golang.org/x/mod/semver"
"github.com/livekit/protocol/livekit"
)
@@ -29,7 +31,19 @@ type Match interface {
}
type ScriptMatch struct {
Expr string
compiled *tengo.Compiled
}
func NewScriptMatch(expr string) (*ScriptMatch, error) {
script := tengo.NewScript(fmt.Appendf(nil, "__res__ := (%s)", expr))
if err := script.Add("c", &clientObject{}); err != nil {
return nil, err
}
compiled, err := script.Compile()
if err != nil {
return nil, err
}
return &ScriptMatch{compiled}, nil
}
// use result of eval script expression for match.
@@ -38,17 +52,23 @@ type ScriptMatch struct {
// browser if firefox: c.browser == "firefox"
// combined rule : c.protocol > 5 && c.browser == "firefox"
func (m *ScriptMatch) Match(clientInfo *livekit.ClientInfo) (bool, error) {
res, err := tengo.Eval(context.TODO(), m.Expr, map[string]interface{}{"c": &clientObject{info: clientInfo}})
if err != nil {
clone := m.compiled.Clone()
if err := clone.Set("c", &clientObject{info: clientInfo}); err != nil {
return false, err
}
if err := clone.Run(); err != nil {
return false, err
}
res := clone.Get("__res__").Value()
if val, ok := res.(bool); ok {
return val, nil
}
return false, errors.New("invalid match expression result")
}
// ------------------------------------------------
type clientObject struct {
tengo.ObjectImpl
info *livekit.ClientInfo
@@ -72,7 +92,7 @@ func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error
case "sdk":
return &tengo.String{Value: strings.ToLower(c.info.Sdk.String())}, nil
case "version":
return &tengo.String{Value: c.info.Version}, nil
return &ruleSdkVersion{sdkVersion: c.info.Version}, nil
case "protocol":
return &tengo.Int{Value: int64(c.info.Protocol)}, nil
case "os":
@@ -84,9 +104,74 @@ func (c *clientObject) IndexGet(index tengo.Object) (res tengo.Object, err error
case "browser":
return &tengo.String{Value: strings.ToLower(c.info.Browser)}, nil
case "browser_version":
return &tengo.String{Value: c.info.BrowserVersion}, nil
return &ruleSdkVersion{sdkVersion: c.info.BrowserVersion}, nil
case "address":
return &tengo.String{Value: c.info.Address}, nil
}
return &tengo.Undefined{}, nil
}
// ------------------------------------------
type ruleSdkVersion struct {
tengo.ObjectImpl
sdkVersion string
}
func (r *ruleSdkVersion) TypeName() string {
return "sdkVersion"
}
func (r *ruleSdkVersion) String() string {
return r.sdkVersion
}
func (r *ruleSdkVersion) BinaryOp(op token.Token, rhs tengo.Object) (tengo.Object, error) {
if rhs, ok := rhs.(*tengo.String); ok {
cmp := r.compare(rhs.Value)
isMatch := false
switch op {
case token.Greater:
isMatch = cmp > 0
case token.GreaterEq:
isMatch = cmp >= 0
case token.Less:
isMatch = cmp < 0
case token.LessEq:
isMatch = cmp <= 0
default:
return nil, tengo.ErrInvalidOperator
}
if isMatch {
return tengo.TrueValue, nil
}
return tengo.FalseValue, nil
}
return nil, tengo.ErrInvalidOperator
}
func (r *ruleSdkVersion) Equals(rhs tengo.Object) bool {
if rhs, ok := rhs.(*tengo.String); ok {
return r.compare(rhs.Value) == 0
}
return false
}
func (r *ruleSdkVersion) compare(rhsSdkVersion string) int {
if !semver.IsValid("v"+r.sdkVersion) || !semver.IsValid("v"+rhsSdkVersion) {
// if not valid semver, do string compare
switch {
case r.sdkVersion < rhsSdkVersion:
return -1
case r.sdkVersion > rhsSdkVersion:
return 1
}
} else {
return semver.Compare("v"+r.sdkVersion, "v"+rhsSdkVersion)
}
return 0
}
+242 -104
View File
@@ -18,25 +18,30 @@ import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/metric"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
redisLiveKit "github.com/livekit/protocol/redis"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/webhook"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/metric"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/bwe/remotebwe"
"github.com/livekit/livekit-server/pkg/sfu/bwe/sendsidebwe"
"github.com/livekit/livekit-server/pkg/sfu/pacer"
"github.com/livekit/livekit-server/pkg/sfu/streamallocator"
)
const (
@@ -44,8 +49,9 @@ const (
)
var (
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
ErrKeyFileIncorrectPermission = errors.New("key file others permissions must be set to 0")
ErrTURNSecretFileIncorrectPermission = errors.New("turn secret file others permissions must be set to 0")
ErrKeysNotSet = errors.New("one of key-file or keys must be provided")
)
type Config struct {
@@ -62,7 +68,7 @@ type Config struct {
TURN TURNConfig `yaml:"turn,omitempty"`
Ingress IngressConfig `yaml:"ingress,omitempty"`
SIP SIPConfig `yaml:"sip,omitempty"`
WebHook WebHookConfig `yaml:"webhook,omitempty"`
WebHook webhook.WebHookConfig `yaml:"webhook,omitempty"`
NodeSelector NodeSelectorConfig `yaml:"node_selector,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
Keys map[string]string `yaml:"keys,omitempty"`
@@ -73,10 +79,18 @@ type Config struct {
LogLevel string `yaml:"log_level,omitempty"`
Logging LoggingConfig `yaml:"logging,omitempty"`
Limit LimitConfig `yaml:"limit,omitempty"`
Agents agent.Config `yaml:"agents,omitempty"`
Development bool `yaml:"development,omitempty"`
Metric metric.MetricConfig `yaml:"metric,omitempty"`
Trace TracingConfig `yaml:"trace,omitempty"`
NodeStats NodeStatsConfig `yaml:"node_stats,omitempty"`
EnableDataTracks bool `yaml:"enable_data_tracks,omitempty"`
API APIConfig `yaml:"api,omitempty"`
}
type RTCConfig struct {
@@ -102,6 +116,18 @@ type RTCConfig struct {
// allow TCP and TURN/TLS fallback
AllowTCPFallback *bool `yaml:"allow_tcp_fallback,omitempty"`
// Signaling RTT threshold (in milliseconds) governing ICE/TCP fallback. On a UDP
// failure, ICE/TCP is attempted only while the measured signaling RTT is below this
// value; at or above it, supporting clients fall back directly to TURN/TLS. When 0
// (the default), the RTT check is disabled and ICE/TCP is always attempted (for
// clients that support it). A positive value also gates allow_udp_unstable_fallback.
TCPFallbackRTTThreshold int `yaml:"tcp_fallback_rtt_threshold,omitempty"`
// When enabled, an established UDP connection reporting sustained high packet loss is
// migrated to ICE/TCP or TURN/TLS. Requires tcp_fallback_rtt_threshold to be set
// (> 0). Disabled by default.
AllowUDPUnstableFallback bool `yaml:"allow_udp_unstable_fallback,omitempty"`
// force a reconnect on a publication error
ReconnectOnPublicationError *bool `yaml:"reconnect_on_publication_error,omitempty"`
@@ -118,7 +144,13 @@ type RTCConfig struct {
// be dropped for a slow data channel to avoid blocking the room.
DatachannelSlowThreshold int `yaml:"datachannel_slow_threshold,omitempty"`
// Target latency for lossy data channels, used to drop packets to reduce latency.
DatachannelLossyTargetLatency time.Duration `yaml:"datachannel_lossy_target_latency,omitempty"`
ForwardStats ForwardStatsConfig `yaml:"forward_stats,omitempty"`
// enable rtp stream restart detection for published tracks
EnableRTPStreamRestartDetection bool `yaml:"enable_rtp_stream_restart_detection,omitempty"`
}
type TURNServer struct {
@@ -127,6 +159,14 @@ type TURNServer struct {
Protocol string `yaml:"protocol,omitempty"`
Username string `yaml:"username,omitempty"`
Credential string `yaml:"credential,omitempty"`
// Secret is used for TURN static auth secrets mechanism. When provided,
// dynamic credentials are generated using HMAC-SHA1 instead of static Username/Credential
Secret string `yaml:"secret,omitempty"`
// File containing the secret
SecretFile string `yaml:"secret_file,omitempty"`
// TTL is the time-to-live in seconds for generated credentials when using Secret.
// Defaults to 14400 seconds (4 hours) if not specified
TTL int `yaml:"ttl,omitempty"`
}
type CongestionControlConfig struct {
@@ -139,8 +179,9 @@ type CongestionControlConfig struct {
UseSendSideBWEInterceptor bool `yaml:"use_send_side_bwe_interceptor,omitempty"`
UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"`
SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"`
UseSendSideBWE bool `yaml:"use_send_side_bwe,omitempty"`
SendSideBWEPacer string `yaml:"send_side_bwe_pacer,omitempty"`
SendSideBWE sendsidebwe.SendSideBWEConfig `yaml:"send_side_bwe,omitempty"`
}
type PlayoutDelayConfig struct {
@@ -152,6 +193,8 @@ type PlayoutDelayConfig struct {
type VideoConfig struct {
DynacastPauseDelay time.Duration `yaml:"dynacast_pause_delay,omitempty"`
StreamTrackerManager sfu.StreamTrackerManagerConfig `yaml:"stream_tracker_manager,omitempty"`
CodecRegressionThreshold int `yaml:"codec_regression_threshold,omitempty"`
}
type RoomConfig struct {
@@ -167,6 +210,8 @@ type RoomConfig struct {
CreateRoomEnabled bool `yaml:"create_room_enabled,omitempty"`
CreateRoomTimeout time.Duration `yaml:"create_room_timeout,omitempty"`
CreateRoomAttempts int `yaml:"create_room_attempts,omitempty"`
// target room participant update batch chunk size in bytes
UpdateBatchTargetSize int `yaml:"update_batch_target_size,omitempty"`
// deprecated, moved to limits
MaxMetadataSize uint32 `yaml:"max_metadata_size,omitempty"`
// deprecated, moved to limits
@@ -187,26 +232,33 @@ type LoggingConfig struct {
}
type TURNConfig struct {
Enabled bool `yaml:"enabled,omitempty"`
Domain string `yaml:"domain,omitempty"`
CertFile string `yaml:"cert_file,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
TLSPort int `yaml:"tls_port,omitempty"`
UDPPort int `yaml:"udp_port,omitempty"`
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
ExternalTLS bool `yaml:"external_tls,omitempty"`
}
type WebHookConfig struct {
URLs []string `yaml:"urls,omitempty"`
// key to use for webhook
APIKey string `yaml:"api_key,omitempty"`
Enabled bool `yaml:"enabled,omitempty"`
Domain string `yaml:"domain,omitempty"`
CertFile string `yaml:"cert_file,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
TLSPort int `yaml:"tls_port,omitempty"`
UDPPort int `yaml:"udp_port,omitempty"`
RelayPortRangeStart uint16 `yaml:"relay_range_start,omitempty"`
RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"`
ExternalTLS bool `yaml:"external_tls,omitempty"`
BindAddresses []string `yaml:"bind_addresses,omitempty"`
// TTL of the TURN credentials in seconds - defaults to 300
TTLSeconds int `yaml:"ttl_seconds,omitempty"`
// list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to.
// By default (i. e. empty list), all restricted peer CIDRs are denied access.
// When not empty, only the specified CIDRs are allowed access.
// Note that this check is applied to restricted peer CIDRs only.
AllowRestrictedPeerCIDRs []string `yaml:"allow_restricted_peer_cidrs,omitempty"`
// list of peer CIDRs to deny access to
// This applies to all peer CIDRs, including restricted ones.
// Deny list takes precedence over allow list.
DenyPeerCIDRs []string `yaml:"deny_peer_cidrs,omitempty"`
}
type NodeSelectorConfig struct {
Kind string `yaml:"kind,omitempty"`
SortBy string `yaml:"sort_by,omitempty"`
Algorithm string `yaml:"algorithm,omitempty"`
CPULoadLimit float32 `yaml:"cpu_load_limit,omitempty"`
SysloadLimit float32 `yaml:"sysload_limit,omitempty"`
Regions []RegionConfig `yaml:"regions,omitempty"`
@@ -245,6 +297,10 @@ func (l LimitConfig) CheckRoomNameLength(name string) bool {
return l.MaxRoomNameLength == 0 || len(name) <= l.MaxRoomNameLength
}
func (l LimitConfig) CheckParticipantIdentityLength(identity string) bool {
return l.MaxParticipantIdentityLength == 0 || len(identity) <= l.MaxParticipantIdentityLength
}
func (l LimitConfig) CheckParticipantNameLength(name string) bool {
return l.MaxParticipantNameLength == 0 || len(name) <= l.MaxParticipantNameLength
}
@@ -281,6 +337,9 @@ type APIConfig struct {
// max amount of time to wait before checking for operation complete
MaxCheckInterval time.Duration `yaml:"max_check_interval,omitempty"`
// Backwards compatibility for room service api calls, will enable by default and remove in a future release
EnablePsrpcForGetListParticpants bool `yaml:"enable_psrpc_for_get_list_participants,omitempty"`
}
type PrometheusConfig struct {
@@ -295,6 +354,13 @@ type ForwardStatsConfig struct {
ReportWindow time.Duration `yaml:"report_window,omitempty"`
}
type TracingConfig struct {
// JaegerURL configures Jaeger as a global tracer.
//
// The following formats are supported: <hostname>, <host>:<port>, http(s)://<host>/<path>
JaegerURL string `yaml:"jaeger_url,omitempty"`
}
func DefaultAPIConfig() APIConfig {
return APIConfig{
ExecutionTimeout: 2 * time.Second,
@@ -303,6 +369,18 @@ func DefaultAPIConfig() APIConfig {
}
}
type NodeStatsConfig struct {
StatsUpdateInterval time.Duration `yaml:"stats_update_interval,omitempty"`
StatsRateMeasurementIntervals []time.Duration `yaml:"stats_rate_measurement_intervals,omitempty"`
StatsMaxDelay time.Duration `yaml:"stats_max_delay,omitempty"`
}
var DefaultNodeStatsConfig = NodeStatsConfig{
StatsUpdateInterval: 2 * time.Second,
StatsRateMeasurementIntervals: []time.Duration{10 * time.Second},
StatsMaxDelay: 30 * time.Second,
}
var DefaultConfig = Config{
Port: 7880,
RTC: RTCConfig{
@@ -324,31 +402,37 @@ var DefaultConfig = Config{
RemoteBWE: remotebwe.DefaultRemoteBWEConfig,
UseSendSideBWEInterceptor: false,
UseSendSideBWE: false,
SendSideBWEPacer: string(pacer.PacerBehaviorNoQueue),
SendSideBWE: sendsidebwe.DefaultSendSideBWEConfig,
},
},
Audio: sfu.DefaultAudioConfig,
Video: VideoConfig{
DynacastPauseDelay: 5 * time.Second,
StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig,
DynacastPauseDelay: 5 * time.Second,
StreamTrackerManager: sfu.DefaultStreamTrackerManagerConfig,
CodecRegressionThreshold: 5,
},
Redis: redisLiveKit.RedisConfig{},
Room: RoomConfig{
AutoCreate: true,
EnabledCodecs: []CodecSpec{
{Mime: mime.MimeTypePCMU.String()},
{Mime: mime.MimeTypePCMA.String()},
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeRED.String()},
{Mime: mime.MimeTypeVP8.String()},
{Mime: mime.MimeTypeH264.String()},
{Mime: mime.MimeTypeVP9.String()},
{Mime: mime.MimeTypeAV1.String()},
{Mime: mime.MimeTypeH265.String()},
{Mime: mime.MimeTypeRTX.String()},
},
EmptyTimeout: 5 * 60,
DepartureTimeout: 20,
CreateRoomEnabled: true,
CreateRoomTimeout: 10 * time.Second,
CreateRoomAttempts: 3,
EmptyTimeout: 5 * 60,
DepartureTimeout: 20,
CreateRoomEnabled: true,
CreateRoomTimeout: 10 * time.Second,
CreateRoomAttempts: 3,
UpdateBatchTargetSize: 128 * 1024,
},
Limit: LimitConfig{
MaxMetadataSize: 64000,
@@ -361,13 +445,16 @@ var DefaultConfig = Config{
PionLevel: "error",
},
TURN: TURNConfig{
Enabled: false,
Enabled: false,
BindAddresses: []string{"0.0.0.0"},
TTLSeconds: 300,
},
NodeSelector: NodeSelectorConfig{
Kind: "any",
SortBy: "random",
SysloadLimit: 0.9,
CPULoadLimit: 0.9,
Algorithm: "lowest",
},
SignalRelay: SignalRelayConfig{
RetryTimeout: 7500 * time.Millisecond,
@@ -376,12 +463,19 @@ var DefaultConfig = Config{
StreamBufferSize: 1000,
ConnectAttempts: 3,
},
PSRPC: rpc.DefaultPSRPCConfig,
Keys: map[string]string{},
Metric: metric.DefaultMetricConfig,
Agents: agent.Config{
TargetLoad: agent.DefaultTargetLoad,
},
PSRPC: rpc.DefaultPSRPCConfig,
Keys: map[string]string{},
Metric: metric.DefaultMetricConfig,
WebHook: webhook.DefaultWebHookConfig,
NodeStats: DefaultNodeStatsConfig,
API: DefaultAPIConfig(),
EnableDataTracks: true,
}
func NewConfig(confString string, strictMode bool, c *cli.Context, baseFlags []cli.Flag) (*Config, error) {
func NewConfig(confString string, strictMode bool, c *cli.Command, baseFlags []cli.Flag) (*Config, error) {
// start with defaults
marshalled, err := yaml.Marshal(&DefaultConfig)
if err != nil {
@@ -563,74 +657,110 @@ func (conf *Config) ValidateKeys() error {
return nil
}
func (conf *Config) LoadTURNSecrets() error {
var otherFilter os.FileMode = 0o007
for i, s := range conf.RTC.TURNServers {
if s.SecretFile == "" {
continue
}
if s.Secret != "" {
logger.Warnw("both secret and secret_file are set for TURN server, the hardcoded secret will be used", nil,
"host", s.Host, "port", s.Port)
continue
}
st, err := os.Stat(s.SecretFile)
if err != nil {
return err
}
if st.Mode().Perm()&otherFilter != 0o000 {
return ErrTURNSecretFileIncorrectPermission
}
data, err := os.ReadFile(s.SecretFile)
if err != nil {
return fmt.Errorf("reading turn secret file %q: %w", s.SecretFile, err)
}
conf.RTC.TURNServers[i].Secret = strings.TrimSpace(string(data))
}
return nil
}
func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error) {
blankConfig := &Config{}
defaultConfig := &DefaultConfig
flags := make([]cli.Flag, 0)
for name, value := range blankConfig.ToCLIFlagNames(existingFlags) {
for name, value := range (defaultConfig).ToCLIFlagNames(existingFlags) {
kind := value.Kind()
if kind == reflect.Ptr {
kind = value.Type().Elem().Kind()
}
var flag cli.Flag
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.Replace(name, ".", "_", -1)))
envVar := fmt.Sprintf("LIVEKIT_%s", strings.ToUpper(strings.ReplaceAll(name, ".", "_")))
defaultText := cliDefaultText(value)
switch kind {
case reflect.Bool:
flag = &cli.BoolFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.String:
flag = &cli.StringFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Int, reflect.Int32:
flag = &cli.IntFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Int64:
flag = &cli.Int64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
flag = &cli.UintFlag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Uint64:
flag = &cli.Uint64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Float32:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Float64:
flag = &cli.Float64Flag{
Name: name,
EnvVars: []string{envVar},
Usage: generatedCLIFlagUsage,
Hidden: hidden,
Name: name,
Sources: cli.EnvVars(envVar),
Usage: generatedCLIFlagUsage,
DefaultText: defaultText,
Hidden: hidden,
}
case reflect.Slice:
// TODO
@@ -651,13 +781,39 @@ func GenerateCLIFlags(existingFlags []cli.Flag, hidden bool) ([]cli.Flag, error)
return flags, nil
}
func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
func cliDefaultText(value reflect.Value) string {
if value.Kind() == reflect.Ptr {
if value.IsNil() {
return ""
}
value = value.Elem()
}
switch value.Kind() {
case reflect.Bool:
return strconv.FormatBool(value.Bool())
case reflect.String:
return value.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if value.Type() == reflect.TypeOf(time.Duration(0)) {
return value.Interface().(time.Duration).String()
}
return strconv.FormatInt(value.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(value.Uint(), 10)
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(value.Float(), 'f', -1, 64)
default:
return ""
}
}
func (conf *Config) updateFromCLI(c *cli.Command, baseFlags []cli.Flag) error {
generatedFlagNames := conf.ToCLIFlagNames(baseFlags)
for _, flag := range c.App.Flags {
for _, flag := range c.Flags {
flagName := flag.Names()[0]
// the `c.App.Name != "test"` check is needed because `c.IsSet(...)` is always false in unit tests
if !c.IsSet(flagName) && c.App.Name != "test" {
if !c.IsSet(flagName) {
continue
}
@@ -666,34 +822,16 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
continue
}
kind := configValue.Kind()
if kind == reflect.Ptr {
// instantiate value to be set
if configValue.Kind() == reflect.Ptr {
configValue.Set(reflect.New(configValue.Type().Elem()))
kind = configValue.Type().Elem().Kind()
configValue = configValue.Elem()
}
switch kind {
case reflect.Bool:
configValue.SetBool(c.Bool(flagName))
case reflect.String:
configValue.SetString(c.String(flagName))
case reflect.Int, reflect.Int32, reflect.Int64:
configValue.SetInt(c.Int64(flagName))
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
configValue.SetUint(c.Uint64(flagName))
case reflect.Float32:
configValue.SetFloat(c.Float64(flagName))
case reflect.Float64:
configValue.SetFloat(c.Float64(flagName))
// case reflect.Slice:
// // TODO
// case reflect.Map:
// // TODO
default:
return fmt.Errorf("unsupported generated cli flag type for config: %s is a %s", flagName, kind.String())
value := reflect.ValueOf(c.Value(flagName))
if value.CanConvert(configValue.Type()) {
configValue.Set(value.Convert(configValue.Type()))
} else {
return fmt.Errorf("unsupported generated cli flag type for config: %s (expected %s, got %s)", flagName, configValue.Type(), value.Type())
}
}
@@ -724,7 +862,7 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
conf.TURN.KeyFile = c.String("turn-key")
}
if c.IsSet("node-ip") {
conf.RTC.NodeIP = c.String("node-ip")
conf.RTC.NodeIP.UnmarshalString(c.String("node-ip"))
}
if c.IsSet("udp-port") {
conf.RTC.UDPPort.UnmarshalString(c.String("udp-port"))
@@ -736,7 +874,7 @@ func (conf *Config) updateFromCLI(c *cli.Context, baseFlags []cli.Flag) error {
}
func (conf *Config) unmarshalKeys(keys string) error {
temp := make(map[string]interface{})
temp := make(map[string]any)
if err := yaml.Unmarshal([]byte(keys), temp); err != nil {
return err
}
+10 -13
View File
@@ -15,11 +15,10 @@
package config
import (
"flag"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/livekit/livekit-server/pkg/config/configtest"
)
@@ -53,19 +52,17 @@ func TestGeneratedFlags(t *testing.T) {
generatedFlags, err := GenerateCLIFlags(nil, false)
require.NoError(t, err)
app := cli.NewApp()
app.Name = "test"
app.Flags = append(app.Flags, generatedFlags...)
c := &cli.Command{}
c.Name = "test"
c.Flags = append(c.Flags, generatedFlags...)
set := flag.NewFlagSet("test", 0)
set.Bool("rtc.use_ice_lite", true, "") // bool
set.String("redis.address", "localhost:6379", "") // string
set.Uint("prometheus.port", 9999, "") // uint32
set.Bool("rtc.allow_tcp_fallback", true, "") // pointer
set.Bool("rtc.reconnect_on_publication_error", true, "") // pointer
set.Bool("rtc.reconnect_on_subscription_error", false, "") // pointer
c.Set("rtc.use_ice_lite", "true")
c.Set("redis.address", "localhost:6379")
c.Set("prometheus.port", "9999")
c.Set("rtc.allow_tcp_fallback", "true")
c.Set("rtc.reconnect_on_publication_error", "true")
c.Set("rtc.reconnect_on_subscription_error", "false")
c := cli.NewContext(app, set, nil)
conf, err := NewConfig("", true, c, nil)
require.NoError(t, err)
@@ -18,7 +18,7 @@ import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/sfu/utils"
"github.com/livekit/mediatransportutil/pkg/latency"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
@@ -49,7 +49,7 @@ type MetricTimestamperParams struct {
type MetricTimestamper struct {
params MetricTimestamperParams
lock sync.Mutex
owdEstimator *utils.OWDEstimator
owdEstimator *latency.OWDEstimator
lastOWDEstimatorRunAt time.Time
batchesSinceLastOWDEstimatorRun int
}
@@ -57,7 +57,7 @@ type MetricTimestamper struct {
func NewMetricTimestamper(params MetricTimestamperParams) *MetricTimestamper {
return &MetricTimestamper{
params: params,
owdEstimator: utils.NewOWDEstimator(utils.OWDEstimatorParamsDefault),
owdEstimator: latency.NewOWDEstimator(latency.OWDEstimatorParamsDefault),
lastOWDEstimatorRunAt: time.Now().Add(-params.Config.OneWayDelayEstimatorMinInterval),
}
}
+74 -42
View File
@@ -23,6 +23,7 @@ import (
"go.uber.org/zap/zapcore"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
@@ -94,7 +95,7 @@ type NullMessageSource struct {
func NewNullMessageSource(connID livekit.ConnectionID) *NullMessageSource {
return &NullMessageSource{
connID: connID,
msgChan: make(chan proto.Message, 0),
msgChan: make(chan proto.Message),
}
}
@@ -152,8 +153,13 @@ type StartParticipantSignalResults struct {
type MessageRouter interface {
// CreateRoom starts an rtc room
CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (res *livekit.Room, err error)
// StartParticipantSignal participant signal connection is ready to start
StartParticipantSignal(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit) (res StartParticipantSignalResults, err error)
StartParticipantSignal(
ctx context.Context,
roomName livekit.RoomName,
pi ParticipantInit,
) (res StartParticipantSignalResults, err error)
}
func CreateRouter(
@@ -162,8 +168,9 @@ func CreateRouter(
signalClient SignalClient,
roomManagerClient RoomManagerClient,
kps rpc.KeepalivePubSub,
nodeStatsConfig config.NodeStatsConfig,
) Router {
lr := NewLocalRouter(node, signalClient, roomManagerClient)
lr := NewLocalRouter(node, signalClient, roomManagerClient, nodeStatsConfig)
if rc != nil {
return NewRedisRouter(lr, rc, kps)
@@ -177,19 +184,24 @@ func CreateRouter(
// ------------------------------------------------
type ParticipantInit struct {
Identity livekit.ParticipantIdentity
Name livekit.ParticipantName
Reconnect bool
ReconnectReason livekit.ReconnectReason
AutoSubscribe bool
Client *livekit.ClientInfo
Grants *auth.ClaimGrants
Region string
AdaptiveStream bool
ID livekit.ParticipantID
SubscriberAllowPause *bool
DisableICELite bool
CreateRoom *livekit.CreateRoomRequest
Identity livekit.ParticipantIdentity
Name livekit.ParticipantName
Reconnect bool
ReconnectReason livekit.ReconnectReason
AutoSubscribe bool
AutoSubscribeDataTrack *bool
Client *livekit.ClientInfo
Grants *auth.ClaimGrants
Region string
AdaptiveStream bool
ID livekit.ParticipantID
SubscriberAllowPause *bool
DisableICELite bool
CreateRoom *livekit.CreateRoomRequest
AddTrackRequests []*livekit.AddTrackRequest
PublisherOffer *livekit.SessionDescription
SyncState *livekit.SyncState
UseSinglePeerConnection bool
}
func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
@@ -209,6 +221,7 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
logBoolPtr("Reconnect", &pi.Reconnect)
e.AddString("ReconnectReason", pi.ReconnectReason.String())
logBoolPtr("AutoSubscribe", &pi.AutoSubscribe)
logBoolPtr("AutoSubscribeDataTrack", pi.AutoSubscribeDataTrack)
e.AddObject("Client", logger.Proto(utils.ClientInfoWithoutAddress(pi.Client)))
e.AddObject("Grants", pi.Grants)
e.AddString("Region", pi.Region)
@@ -217,6 +230,10 @@ func (pi *ParticipantInit) MarshalLogObject(e zapcore.ObjectEncoder) error {
logBoolPtr("SubscriberAllowPause", pi.SubscriberAllowPause)
logBoolPtr("DisableICELite", &pi.DisableICELite)
e.AddObject("CreateRoom", logger.Proto(pi.CreateRoom))
e.AddArray("AddTrackRequests", logger.ProtoSlice(pi.AddTrackRequests))
e.AddObject("PublisherOffer", logger.Proto(pi.PublisherOffer))
e.AddObject("SyncState", logger.Proto(pi.SyncState))
logBoolPtr("UseSinglePeerConnection", &pi.UseSinglePeerConnection)
return nil
}
@@ -227,20 +244,27 @@ func (pi *ParticipantInit) ToStartSession(roomName livekit.RoomName, connectionI
}
ss := &livekit.StartSession{
RoomName: string(roomName),
Identity: string(pi.Identity),
Name: string(pi.Name),
// connection id is to allow the RTC node to identify where to route the message back to
ConnectionId: string(connectionID),
Reconnect: pi.Reconnect,
ReconnectReason: pi.ReconnectReason,
AutoSubscribe: pi.AutoSubscribe,
Client: pi.Client,
GrantsJson: string(claims),
AdaptiveStream: pi.AdaptiveStream,
ParticipantId: string(pi.ID),
DisableIceLite: pi.DisableICELite,
CreateRoom: pi.CreateRoom,
RoomName: string(roomName),
Identity: string(pi.Identity),
Name: string(pi.Name),
ConnectionId: string(connectionID),
Reconnect: pi.Reconnect,
ReconnectReason: pi.ReconnectReason,
AutoSubscribe: pi.AutoSubscribe,
Client: pi.Client,
GrantsJson: string(claims),
AdaptiveStream: pi.AdaptiveStream,
ParticipantId: string(pi.ID),
DisableIceLite: pi.DisableICELite,
CreateRoom: pi.CreateRoom,
AddTrackRequests: pi.AddTrackRequests,
PublisherOffer: pi.PublisherOffer,
SyncState: pi.SyncState,
UseSinglePeerConnection: pi.UseSinglePeerConnection,
}
if pi.AutoSubscribeDataTrack != nil {
autoSubscribeDataTrack := *pi.AutoSubscribeDataTrack
ss.AutoSubscribeDataTrack = &autoSubscribeDataTrack
}
if pi.SubscriberAllowPause != nil {
subscriberAllowPause := *pi.SubscriberAllowPause
@@ -257,18 +281,26 @@ func ParticipantInitFromStartSession(ss *livekit.StartSession, region string) (*
}
pi := &ParticipantInit{
Identity: livekit.ParticipantIdentity(ss.Identity),
Name: livekit.ParticipantName(ss.Name),
Reconnect: ss.Reconnect,
ReconnectReason: ss.ReconnectReason,
Client: ss.Client,
AutoSubscribe: ss.AutoSubscribe,
Grants: claims,
Region: region,
AdaptiveStream: ss.AdaptiveStream,
ID: livekit.ParticipantID(ss.ParticipantId),
DisableICELite: ss.DisableIceLite,
CreateRoom: ss.CreateRoom,
Identity: livekit.ParticipantIdentity(ss.Identity),
Name: livekit.ParticipantName(ss.Name),
Reconnect: ss.Reconnect,
ReconnectReason: ss.ReconnectReason,
Client: ss.Client,
AutoSubscribe: ss.AutoSubscribe,
Grants: claims,
Region: region,
AdaptiveStream: ss.AdaptiveStream,
ID: livekit.ParticipantID(ss.ParticipantId),
DisableICELite: ss.DisableIceLite,
CreateRoom: ss.CreateRoom,
AddTrackRequests: ss.AddTrackRequests,
PublisherOffer: ss.PublisherOffer,
SyncState: ss.SyncState,
UseSinglePeerConnection: ss.UseSinglePeerConnection,
}
if ss.AutoSubscribeDataTrack != nil {
autoSubscribeDataTrack := *ss.AutoSubscribeDataTrack
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
}
if ss.SubscriberAllowPause != nil {
subscriberAllowPause := *ss.SubscriberAllowPause
+7 -5
View File
@@ -16,11 +16,11 @@ package routing
import (
"context"
"sync"
"time"
"go.uber.org/atomic"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
@@ -32,8 +32,8 @@ type LocalRouter struct {
currentNode LocalNode
signalClient SignalClient
roomManagerClient RoomManagerClient
nodeStatsConfig config.NodeStatsConfig
lock sync.RWMutex
// channels for each participant
requestChannels map[string]*MessageChannel
responseChannels map[string]*MessageChannel
@@ -44,11 +44,13 @@ func NewLocalRouter(
currentNode LocalNode,
signalClient SignalClient,
roomManagerClient RoomManagerClient,
nodeStatsConfig config.NodeStatsConfig,
) *LocalRouter {
return &LocalRouter{
currentNode: currentNode,
signalClient: signalClient,
roomManagerClient: roomManagerClient,
nodeStatsConfig: nodeStatsConfig,
requestChannels: make(map[string]*MessageChannel),
responseChannels: make(map[string]*MessageChannel),
}
@@ -106,7 +108,8 @@ func (r *LocalRouter) StartParticipantSignal(ctx context.Context, roomName livek
func (r *LocalRouter) StartParticipantSignalWithNodeID(ctx context.Context, roomName livekit.RoomName, pi ParticipantInit, nodeID livekit.NodeID) (res StartParticipantSignalResults, err error) {
connectionID, reqSink, resSource, err := r.signalClient.StartParticipantSignal(ctx, roomName, pi, nodeID)
if err != nil {
logger.Errorw("could not handle new participant", err,
logger.Errorw(
"could not handle new participant", err,
"room", roomName,
"participant", pi.Identity,
"connID", connectionID,
@@ -146,8 +149,7 @@ func (r *LocalRouter) statsWorker() {
if !r.isStarted.Load() {
return
}
// update every 10 seconds
<-time.After(statsUpdateInterval)
<-time.After(r.nodeStatsConfig.StatsUpdateInterval)
r.currentNode.UpdateNodeStats()
}
}
@@ -38,7 +38,7 @@ func TestMessageChannel_WriteMessageClosed(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
for range 100 {
_ = m.WriteMessage(&livekit.SignalRequest{})
}
}()
+14 -17
View File
@@ -20,12 +20,10 @@ import (
"time"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
)
type LocalNode interface {
@@ -45,30 +43,35 @@ type LocalNodeImpl struct {
lock sync.RWMutex
node *livekit.Node
// previous stats for computing averages
prevStats *livekit.NodeStats
nodeStats *NodeStats
}
func NewLocalNode(conf *config.Config) (*LocalNodeImpl, error) {
nodeID := guid.New(utils.NodePrefix)
if conf != nil && conf.RTC.NodeIP == "" {
if conf != nil && conf.RTC.NodeIP.IsEmpty() {
return nil, ErrIPNotSet
}
nowUnix := time.Now().Unix()
l := &LocalNodeImpl{
node: &livekit.Node{
Id: nodeID,
NumCpus: uint32(runtime.NumCPU()),
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
StartedAt: time.Now().Unix(),
UpdatedAt: time.Now().Unix(),
StartedAt: nowUnix,
UpdatedAt: nowUnix,
},
},
}
var nsc *config.NodeStatsConfig
if conf != nil {
l.node.Ip = conf.RTC.NodeIP
l.node.Ip = conf.RTC.NodeIP.PrimaryIP()
l.node.Region = conf.Region
nsc = &conf.NodeStats
}
l.nodeStats = NewNodeStats(nsc, nowUnix)
return l, nil
}
@@ -138,18 +141,12 @@ func (l *LocalNodeImpl) UpdateNodeStats() bool {
l.lock.Lock()
defer l.lock.Unlock()
if l.prevStats == nil {
l.prevStats = l.node.Stats
}
updated, computedAvg, err := prometheus.GetUpdatedNodeStats(l.node.Stats, l.prevStats)
stats, err := l.nodeStats.UpdateAndGetNodeStats()
if err != nil {
logger.Errorw("could not update node stats", err)
return false
}
l.node.Stats = updated
if computedAvg {
l.prevStats = updated
}
l.node.Stats = stats
return true
}
+82
View File
@@ -0,0 +1,82 @@
// 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 routing
import (
"sync"
"time"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
)
type NodeStats struct {
config config.NodeStatsConfig
startedAt int64
lock sync.Mutex
statsHistory []*livekit.NodeStats
statsHistoryWritePtr int
}
func NewNodeStats(conf *config.NodeStatsConfig, startedAt int64) *NodeStats {
n := &NodeStats{
startedAt: startedAt,
}
n.UpdateConfig(conf)
return n
}
func (n *NodeStats) UpdateConfig(conf *config.NodeStatsConfig) {
n.lock.Lock()
defer n.lock.Unlock()
if conf == nil {
conf = &config.DefaultNodeStatsConfig
}
n.config = *conf
// set up stats history to be able to measure different rate windows
var maxInterval time.Duration
for _, rateInterval := range conf.StatsRateMeasurementIntervals {
if rateInterval > maxInterval {
maxInterval = rateInterval
}
}
n.statsHistory = make([]*livekit.NodeStats, (maxInterval+conf.StatsUpdateInterval-1)/conf.StatsUpdateInterval)
n.statsHistoryWritePtr = 0
}
func (n *NodeStats) UpdateAndGetNodeStats() (*livekit.NodeStats, error) {
n.lock.Lock()
defer n.lock.Unlock()
stats, err := prometheus.GetNodeStats(
n.startedAt,
append(n.statsHistory[n.statsHistoryWritePtr:], n.statsHistory[0:n.statsHistoryWritePtr]...),
n.config.StatsRateMeasurementIntervals,
)
if err != nil {
logger.Errorw("could not update node stats", err)
return nil, err
}
n.statsHistory[n.statsHistoryWritePtr] = stats
n.statsHistoryWritePtr = (n.statsHistoryWritePtr + 1) % len(n.statsHistory)
return stats, nil
}
+3 -8
View File
@@ -33,11 +33,6 @@ import (
)
const (
// expire participant mappings after a day
participantMappingTTL = 24 * time.Hour
statsUpdateInterval = 2 * time.Second
statsMaxDelaySeconds = float64(30)
// hash of node_id => Node proto
NodesKey = "nodes"
@@ -209,11 +204,11 @@ func (r *RedisRouter) statsWorker() {
for r.ctx.Err() == nil {
// update periodically
select {
case <-time.After(statsUpdateInterval):
case <-time.After(r.nodeStatsConfig.StatsUpdateInterval):
r.kps.PublishPing(r.ctx, r.currentNode.NodeID(), &rpc.KeepalivePing{Timestamp: time.Now().Unix()})
delaySeconds := r.currentNode.SecondsSinceNodeStatsUpdate()
if delaySeconds > statsMaxDelaySeconds {
if delaySeconds > r.nodeStatsConfig.StatsMaxDelay.Seconds() {
if !goroutineDumped {
goroutineDumped = true
buf := bytes.NewBuffer(nil)
@@ -240,7 +235,7 @@ func (r *RedisRouter) keepaliveWorker(startedChan chan error) {
close(startedChan)
for ping := range pings.Channel() {
if time.Since(time.Unix(ping.Timestamp, 0)) > statsUpdateInterval {
if time.Since(time.Unix(ping.Timestamp, 0)) > r.nodeStatsConfig.StatsUpdateInterval {
logger.Infow("keep alive too old, skipping", "timestamp", ping.Timestamp)
continue
}
@@ -243,14 +243,6 @@ func (fake *FakeMessageSink) WriteMessageReturnsOnCall(i int, result1 error) {
func (fake *FakeMessageSink) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
fake.writeMessageMutex.RLock()
defer fake.writeMessageMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -234,14 +234,6 @@ func (fake *FakeMessageSource) ReadChanReturnsOnCall(i int, result1 <-chan proto
func (fake *FakeMessageSource) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.connectionIDMutex.RLock()
defer fake.connectionIDMutex.RUnlock()
fake.isClosedMutex.RLock()
defer fake.isClosedMutex.RUnlock()
fake.readChanMutex.RLock()
defer fake.readChanMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -129,10 +129,6 @@ func (fake *FakeRoomManagerClient) CreateRoomReturnsOnCall(i int, result1 *livek
func (fake *FakeRoomManagerClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -845,32 +845,6 @@ func (fake *FakeRouter) UnregisterNodeReturnsOnCall(i int, result1 error) {
func (fake *FakeRouter) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.clearRoomStateMutex.RLock()
defer fake.clearRoomStateMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
fake.drainMutex.RLock()
defer fake.drainMutex.RUnlock()
fake.getNodeForRoomMutex.RLock()
defer fake.getNodeForRoomMutex.RUnlock()
fake.getRegionMutex.RLock()
defer fake.getRegionMutex.RUnlock()
fake.listNodesMutex.RLock()
defer fake.listNodesMutex.RUnlock()
fake.registerNodeMutex.RLock()
defer fake.registerNodeMutex.RUnlock()
fake.removeDeadNodesMutex.RLock()
defer fake.removeDeadNodesMutex.RUnlock()
fake.setNodeForRoomMutex.RLock()
defer fake.setNodeForRoomMutex.RUnlock()
fake.startMutex.RLock()
defer fake.startMutex.RUnlock()
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
fake.stopMutex.RLock()
defer fake.stopMutex.RUnlock()
fake.unregisterNodeMutex.RLock()
defer fake.unregisterNodeMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -173,10 +173,6 @@ func (fake *FakeSignalClient) StartParticipantSignalReturnsOnCall(i int, result1
func (fake *FakeSignalClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.activeCountMutex.RLock()
defer fake.activeCountMutex.RUnlock()
fake.startParticipantSignalMutex.RLock()
defer fake.startParticipantSignalMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+3 -2
View File
@@ -20,7 +20,8 @@ import (
// AnySelector selects any available node with no limitations
type AnySelector struct {
SortBy string
SortBy string
Algorithm string
}
func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
@@ -29,5 +30,5 @@ func (s *AnySelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, error) {
return nil, ErrNoAvailableNodes
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
@@ -0,0 +1,287 @@
// 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 selector
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/livekit"
)
func createTestNode(id string, cpuLoad float32, numRooms int32, numClients int32, state livekit.NodeState) *livekit.Node {
return &livekit.Node{
Id: id,
State: state,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix() - 1, // Recent update to be considered available
CpuLoad: cpuLoad,
NumRooms: numRooms,
NumClients: numClients,
NumCpus: 4,
LoadAvgLast1Min: cpuLoad * 4, // Simulate system load
},
}
}
func TestAnySelector_SelectNode_TwoChoice(t *testing.T) {
tests := []struct {
name string
sortBy string
algorithm string
nodes []*livekit.Node
wantErr string
expected string
notExpected string
}{
{
name: "successful selection with cpuload sorting",
sortBy: "cpuload",
algorithm: "twochoice",
nodes: []*livekit.Node{
createTestNode("node1", 0.8, 5, 10, livekit.NodeState_SERVING),
createTestNode("node2", 0.3, 2, 5, livekit.NodeState_SERVING),
createTestNode("node3", 0.6, 3, 8, livekit.NodeState_SERVING),
createTestNode("node4", 0.9, 6, 12, livekit.NodeState_SERVING),
},
wantErr: "",
expected: "", // Not determinstic selection, so no specific expected node
notExpected: "node4", // Node with highest load should not be selected
},
{
name: "successful selection with rooms sorting",
sortBy: "rooms",
algorithm: "twochoice",
nodes: []*livekit.Node{
createTestNode("node1", 0.5, 8, 15, livekit.NodeState_SERVING),
createTestNode("node2", 0.4, 2, 5, livekit.NodeState_SERVING),
createTestNode("node3", 0.6, 12, 20, livekit.NodeState_SERVING),
},
wantErr: "",
expected: "", // Not determinstic selection, so no specific expected node
notExpected: "node3", // Node with highest room count should not be selected
},
{
name: "successful selection with clients sorting",
sortBy: "clients",
algorithm: "twochoice",
nodes: []*livekit.Node{
createTestNode("node1", 0.5, 3, 25, livekit.NodeState_SERVING),
createTestNode("node2", 0.4, 2, 5, livekit.NodeState_SERVING),
createTestNode("node3", 0.6, 4, 30, livekit.NodeState_SERVING),
},
wantErr: "",
expected: "", // Not determinstic selection, so no specific expected node
notExpected: "node3", // Node with highest clients should not be selected
},
{
name: "empty nodes list",
sortBy: "cpuload",
algorithm: "twochoice",
nodes: []*livekit.Node{},
wantErr: "could not find any available nodes",
},
{
name: "no available nodes - all unavailable",
sortBy: "cpuload",
algorithm: "twochoice",
nodes: []*livekit.Node{
{
Id: "node1",
State: livekit.NodeState_SERVING,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix() - 10, // Too old
CpuLoad: 0.3,
},
},
},
wantErr: "could not find any available nodes",
},
{
name: "no available nodes - not serving",
sortBy: "cpuload",
algorithm: "twochoice",
nodes: []*livekit.Node{
{
Id: "node1",
State: livekit.NodeState_SHUTTING_DOWN,
Stats: &livekit.NodeStats{
UpdatedAt: time.Now().Unix() - 1,
CpuLoad: 0.3,
},
},
},
wantErr: "could not find any available nodes",
},
{
name: "single available node",
sortBy: "cpuload",
algorithm: "twochoice",
nodes: []*livekit.Node{
createTestNode("node1", 0.5, 3, 10, livekit.NodeState_SERVING),
},
wantErr: "",
expected: "node1", // Should select the only available node
notExpected: "", // No other nodes to compare against
},
{
name: "two available nodes",
sortBy: "cpuload",
algorithm: "twochoice",
nodes: []*livekit.Node{
createTestNode("node1", 0.8, 5, 15, livekit.NodeState_SERVING),
createTestNode("node2", 0.3, 2, 5, livekit.NodeState_SERVING),
},
wantErr: "",
expected: "node2", // Should select the node with lower load
notExpected: "node1", // Should not select the node with higher load
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selector := &AnySelector{
SortBy: tt.sortBy,
Algorithm: tt.algorithm,
}
node, err := selector.SelectNode(tt.nodes)
if tt.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
require.Nil(t, node)
} else {
require.NoError(t, err)
require.NotNil(t, node)
require.NotEmpty(t, node.Id)
// Verify the selected node is one of the available nodes
found := false
availableNodes := GetAvailableNodes(tt.nodes)
for _, availableNode := range availableNodes {
if availableNode.Id == node.Id {
found = true
break
}
}
require.True(t, found, "Selected node should be one of the available nodes")
if tt.expected != "" {
require.Equal(t, tt.expected, node.Id, "Selected node should match expected")
}
if tt.notExpected != "" {
require.NotEqual(t, tt.notExpected, node.Id, "Selected node should not match not expected")
}
}
})
}
}
func TestAnySelector_SelectNode_TwoChoice_Probabilistic_Behavior(t *testing.T) {
// Test that two-choice algorithm favors nodes with lower metrics
// This test runs multiple iterations to increase confidence in the probabilistic behavior
selector := &AnySelector{
SortBy: "cpuload",
Algorithm: "twochoice",
}
// Create nodes where node2 has significantly lower CPU load
nodes := []*livekit.Node{
createTestNode("node1", 0.95, 10, 20, livekit.NodeState_SERVING), // Very high load
createTestNode("node2", 0.1, 1, 2, livekit.NodeState_SERVING), // Low load
createTestNode("node3", 0.5, 9, 18, livekit.NodeState_SERVING), // Medium load
createTestNode("node4", 0.85, 8, 16, livekit.NodeState_SERVING), // High load
}
// Run multiple selections and count how often the low-load node is selected
iterations := 1000
lowLoadSelections := 0
higestLoadSelections := 0
for range iterations {
node, err := selector.SelectNode(nodes)
require.NoError(t, err)
require.NotNil(t, node)
if node.Id == "node2" {
lowLoadSelections++
}
if node.Id == "node1" {
higestLoadSelections++
}
}
// The low-load node should be selected more often than pure random (25%)
// Due to the two-choice algorithm favoring the better node
selectionRate := float64(lowLoadSelections) / float64(iterations)
require.Greater(t, selectionRate, 0.4, "Two-choice algorithm should favor the low-load node more than random selection")
require.Equal(t, higestLoadSelections, 0, "Two-choice algorithm should never favor the highest load node")
}
func TestAnySelector_SelectNode_InvalidParameters(t *testing.T) {
nodes := []*livekit.Node{
createTestNode("node1", 0.5, 3, 10, livekit.NodeState_SERVING),
}
tests := []struct {
name string
sortBy string
algorithm string
wantErr string
}{
{
name: "empty sortBy",
sortBy: "",
algorithm: "twochoice",
wantErr: "sort by option cannot be blank",
},
{
name: "empty algorithm",
sortBy: "cpuload",
algorithm: "",
wantErr: "node selector algorithm option cannot be blank",
},
{
name: "unknown sortBy",
sortBy: "invalid",
algorithm: "twochoice",
wantErr: "unknown sort by option",
},
{
name: "unknown algorithm",
sortBy: "cpuload",
algorithm: "invalid",
wantErr: "unknown node selector algorithm option",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selector := &AnySelector{
SortBy: tt.sortBy,
Algorithm: tt.algorithm,
}
node, err := selector.SelectNode(nodes)
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
require.Nil(t, node)
})
}
}
@@ -23,6 +23,7 @@ import (
type CPULoadSelector struct {
CPULoadLimit float32
SortBy string
Algorithm string
}
func (s *CPULoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
@@ -50,5 +51,5 @@ func (s *CPULoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, erro
return nil, err
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
@@ -25,7 +25,7 @@ import (
)
func TestCPULoadSelector_SelectNode(t *testing.T) {
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random"}
sel := selector.CPULoadSelector{CPULoadLimit: 0.8, SortBy: "random", Algorithm: "lowest"}
var nodes []*livekit.Node
_, err := sel.SelectNode(nodes)
@@ -39,7 +39,7 @@ func TestCPULoadSelector_SelectNode(t *testing.T) {
// Select a node with low load when available
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
for i := 0; i < 5; i++ {
for range 5 {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
@@ -21,5 +21,7 @@ var (
ErrCurrentRegionNotSet = errors.New("current region cannot be blank")
ErrCurrentRegionUnknownLatLon = errors.New("unknown lat and lon for the current region")
ErrSortByNotSet = errors.New("sort by option cannot be blank")
ErrAlgorithmNotSet = errors.New("node selector algorithm option cannot be blank")
ErrSortByUnknown = errors.New("unknown sort by option")
ErrAlgorithmUnknown = errors.New("unknown node selector algorithm option")
)
@@ -37,19 +37,21 @@ func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
}
switch kind {
case "any":
return &AnySelector{conf.NodeSelector.SortBy}, nil
return &AnySelector{conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm}, nil
case "cpuload":
return &CPULoadSelector{
CPULoadLimit: conf.NodeSelector.CPULoadLimit,
SortBy: conf.NodeSelector.SortBy,
SortBy: conf.NodeSelector.SortBy,
Algorithm: conf.NodeSelector.Algorithm,
}, nil
case "sysload":
return &SystemLoadSelector{
SysloadLimit: conf.NodeSelector.SysloadLimit,
SortBy: conf.NodeSelector.SortBy,
SortBy: conf.NodeSelector.SortBy,
Algorithm: conf.NodeSelector.Algorithm,
}, nil
case "regionaware":
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy)
s, err := NewRegionAwareSelector(conf.Region, conf.NodeSelector.Regions, conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm)
if err != nil {
return nil, err
}
@@ -57,7 +59,7 @@ func CreateNodeSelector(conf *config.Config) (NodeSelector, error) {
return s, nil
case "random":
logger.Warnw("random node selector is deprecated, please switch to \"any\" or another selector", nil)
return &AnySelector{conf.NodeSelector.SortBy}, nil
return &AnySelector{conf.NodeSelector.SortBy, conf.NodeSelector.Algorithm}, nil
default:
return nil, ErrUnsupportedSelector
}
@@ -29,9 +29,10 @@ type RegionAwareSelector struct {
regionDistances map[string]float64
regions []config.RegionConfig
SortBy string
Algorithm string
}
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string) (*RegionAwareSelector, error) {
func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig, sortBy string, algorithm string) (*RegionAwareSelector, error) {
if currentRegion == "" {
return nil, ErrCurrentRegionNotSet
}
@@ -41,6 +42,7 @@ func NewRegionAwareSelector(currentRegion string, regions []config.RegionConfig,
regionDistances: make(map[string]float64),
regions: regions,
SortBy: sortBy,
Algorithm: algorithm,
}
var currentRC *config.RegionConfig
@@ -94,7 +96,7 @@ func (s *RegionAwareSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node,
nodes = nearestNodes
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
// haversine(θ) function
@@ -34,6 +34,7 @@ const (
regionEast = "us-east"
regionSeattle = "seattle"
sortBy = "random"
algorithm = "lowest"
)
func TestRegionAwareRouting(t *testing.T) {
@@ -58,7 +59,7 @@ func TestRegionAwareRouting(t *testing.T) {
nodes := []*livekit.Node{
newTestNodeInRegion("", false),
}
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, nil, sortBy, algorithm)
require.NoError(t, err)
node, err := s.SelectNode(nodes)
@@ -74,7 +75,7 @@ func TestRegionAwareRouting(t *testing.T) {
expectedNode,
newTestNodeInRegion(regionEast, false),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -91,7 +92,7 @@ func TestRegionAwareRouting(t *testing.T) {
newTestNodeInRegion(regionWest, true),
newTestNodeInRegion(regionEast, false),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -107,7 +108,7 @@ func TestRegionAwareRouting(t *testing.T) {
expectedNode,
newTestNodeInRegion(regionEast, true),
}
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -125,7 +126,7 @@ func TestRegionAwareRouting(t *testing.T) {
expectedNode,
expectedNode,
}
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionSeattle, rc, sortBy, algorithm)
require.NoError(t, err)
s.SysloadLimit = loadLimit
@@ -138,7 +139,7 @@ func TestRegionAwareRouting(t *testing.T) {
nodes := []*livekit.Node{
newTestNodeInRegion(regionWest, true),
}
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy)
s, err := selector.NewRegionAwareSelector(regionEast, rc, sortBy, algorithm)
require.NoError(t, err)
node, err := s.SelectNode(nodes)
@@ -23,10 +23,10 @@ import (
)
func SortByTest(t *testing.T, sortBy string) {
sel := selector.SystemLoadSelector{SortBy: sortBy}
sel := selector.SystemLoadSelector{SortBy: sortBy, Algorithm: "lowest"}
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
for i := 0; i < 5; i++ {
for range 5 {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
@@ -38,7 +38,7 @@ func SortByTest(t *testing.T, sortBy string) {
}
func TestSortByErrors(t *testing.T) {
sel := selector.SystemLoadSelector{}
sel := selector.SystemLoadSelector{Algorithm: "lowest"}
nodes := []*livekit.Node{nodeLoadLow, nodeLoadMedium, nodeLoadHigh}
// Test unset sort by option error
@@ -23,6 +23,7 @@ import (
type SystemLoadSelector struct {
SysloadLimit float32
SortBy string
Algorithm string
}
func (s *SystemLoadSelector) filterNodes(nodes []*livekit.Node) ([]*livekit.Node, error) {
@@ -49,5 +50,5 @@ func (s *SystemLoadSelector) SelectNode(nodes []*livekit.Node) (*livekit.Node, e
return nil, err
}
return SelectSortedNode(nodes, s.SortBy)
return SelectSortedNode(nodes, s.SortBy, s.Algorithm)
}
@@ -33,12 +33,16 @@ var (
NumCpus: 1,
CpuLoad: 0.1,
LoadAvgLast1Min: 0.0,
NumRooms: 1,
NumClients: 2,
NumTracksIn: 4,
NumTracksOut: 8,
BytesInPerSec: 1000,
BytesOutPerSec: 2000,
NumRooms: 1,
NumClients: 2,
NumTracksIn: 4,
NumTracksOut: 8,
Rates: []*livekit.NodeStatsRate{
{
BytesIn: 1000,
BytesOut: 2000,
},
},
},
}
@@ -49,12 +53,16 @@ var (
NumCpus: 1,
CpuLoad: 0.5,
LoadAvgLast1Min: 0.5,
NumRooms: 5,
NumClients: 10,
NumTracksIn: 20,
NumTracksOut: 200,
BytesInPerSec: 5000,
BytesOutPerSec: 10000,
NumRooms: 5,
NumClients: 10,
NumTracksIn: 20,
NumTracksOut: 200,
Rates: []*livekit.NodeStatsRate{
{
BytesIn: 5000,
BytesOut: 10000,
},
},
},
}
@@ -65,18 +73,22 @@ var (
NumCpus: 1,
CpuLoad: 0.99,
LoadAvgLast1Min: 2.0,
NumRooms: 10,
NumClients: 20,
NumTracksIn: 40,
NumTracksOut: 800,
BytesInPerSec: 10000,
BytesOutPerSec: 40000,
NumRooms: 10,
NumClients: 20,
NumTracksIn: 40,
NumTracksOut: 800,
Rates: []*livekit.NodeStatsRate{
{
BytesIn: 10000,
BytesOut: 40000,
},
},
},
}
)
func TestSystemLoadSelector_SelectNode(t *testing.T) {
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random"}
sel := selector.SystemLoadSelector{SysloadLimit: 1.0, SortBy: "random", Algorithm: "lowest"}
var nodes []*livekit.Node
_, err := sel.SelectNode(nodes)
@@ -90,7 +102,7 @@ func TestSystemLoadSelector_SelectNode(t *testing.T) {
// Select a node with low load when available
nodes = []*livekit.Node{nodeLoadLow, nodeLoadHigh}
for i := 0; i < 5; i++ {
for range 5 {
node, err := sel.SelectNode(nodes)
if err != nil {
t.Error(err)
+79 -15
View File
@@ -15,7 +15,8 @@
package selector
import (
"sort"
"math/rand/v2"
"slices"
"time"
"github.com/thoas/go-funk"
@@ -23,6 +24,7 @@ import (
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/utils"
)
const AvailableSeconds = 5
@@ -62,54 +64,116 @@ func LimitsReached(limitConfig config.LimitConfig, nodeStats *livekit.NodeStats)
if limitConfig.NumTracks > 0 && limitConfig.NumTracks <= nodeStats.NumTracksIn+nodeStats.NumTracksOut {
return true
}
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= nodeStats.BytesInPerSec+nodeStats.BytesOutPerSec {
rate := &livekit.NodeStatsRate{}
if len(nodeStats.Rates) > 0 {
rate = nodeStats.Rates[0]
}
if limitConfig.BytesPerSec > 0 && limitConfig.BytesPerSec <= rate.BytesIn+rate.BytesOut {
return true
}
return false
}
func SelectSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
func SelectSortedNode(nodes []*livekit.Node, sortBy string, algorithm string) (*livekit.Node, error) {
if sortBy == "" {
return nil, ErrSortByNotSet
}
if algorithm == "" {
return nil, ErrAlgorithmNotSet
}
switch algorithm {
case "lowest": // examine all nodes and select the lowest based on sort criteria
return selectLowestSortedNode(nodes, sortBy)
case "twochoice": // randomly select two nodes and return the lowest based on sort criteria "Power of Two Random Choices"
return selectTwoChoiceSortedNode(nodes, sortBy)
default:
return nil, ErrAlgorithmUnknown
}
}
func selectTwoChoiceSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
if len(nodes) <= 2 {
return selectLowestSortedNode(nodes, sortBy)
}
// randomly select two nodes
node1, node2, err := selectTwoRandomNodes(nodes)
if err != nil {
return nil, err
}
// compare the two nodes based on the sort criteria
if node1 == nil || node2 == nil {
return nil, ErrNoAvailableNodes
}
selectedNode, err := selectLowestSortedNode([]*livekit.Node{node1, node2}, sortBy)
if err != nil {
return nil, err
}
return selectedNode, nil
}
func selectLowestSortedNode(nodes []*livekit.Node, sortBy string) (*livekit.Node, error) {
// Return a node based on what it should be sorted by for priority
switch sortBy {
case "random":
idx := funk.RandomInt(0, len(nodes))
return nodes[idx], nil
case "sysload":
sort.Slice(nodes, func(i, j int) bool {
return GetNodeSysload(nodes[i]) < GetNodeSysload(nodes[j])
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(GetNodeSysload(a) - GetNodeSysload(b))
})
return nodes[0], nil
case "cpuload":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.CpuLoad < nodes[j].Stats.CpuLoad
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(a.Stats.CpuLoad - b.Stats.CpuLoad)
})
return nodes[0], nil
case "rooms":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.NumRooms < nodes[j].Stats.NumRooms
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(a.Stats.NumRooms - b.Stats.NumRooms)
})
return nodes[0], nil
case "clients":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.NumClients < nodes[j].Stats.NumClients
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum(a.Stats.NumClients - b.Stats.NumClients)
})
return nodes[0], nil
case "tracks":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.NumTracksIn+nodes[i].Stats.NumTracksOut < nodes[j].Stats.NumTracksIn+nodes[j].Stats.NumTracksOut
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
return utils.Signum((a.Stats.NumTracksIn + a.Stats.NumTracksOut) - (b.Stats.NumTracksIn + b.Stats.NumTracksOut))
})
return nodes[0], nil
case "bytespersec":
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Stats.BytesInPerSec+nodes[i].Stats.BytesOutPerSec < nodes[j].Stats.BytesInPerSec+nodes[j].Stats.BytesOutPerSec
slices.SortFunc(nodes, func(a, b *livekit.Node) int {
ratea := &livekit.NodeStatsRate{}
if len(a.Stats.Rates) > 0 {
ratea = a.Stats.Rates[0]
}
rateb := &livekit.NodeStatsRate{}
if len(b.Stats.Rates) > 0 {
rateb = b.Stats.Rates[0]
}
return utils.Signum((ratea.BytesIn + ratea.BytesOut) - (rateb.BytesIn + rateb.BytesOut))
})
return nodes[0], nil
default:
return nil, ErrSortByUnknown
}
}
func selectTwoRandomNodes(nodes []*livekit.Node) (*livekit.Node, *livekit.Node, error) {
if len(nodes) < 2 {
return nil, nil, ErrNoAvailableNodes
}
shuffledIndices := rand.Perm(len(nodes))
return nodes[shuffledIndices[0]], nodes[shuffledIndices[1]], nil
}
+25 -8
View File
@@ -25,6 +25,7 @@ import (
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
@@ -50,7 +51,7 @@ type signalClient struct {
}
func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.SignalRelayConfig) (SignalClient, error) {
c, err := rpc.NewTypedSignalClient(
client, err := rpc.NewTypedSignalClient(
nodeID,
bus,
middleware.WithClientMetrics(rpc.PSRPCMetricsObserver{}),
@@ -63,7 +64,7 @@ func NewSignalClient(nodeID livekit.NodeID, bus psrpc.MessageBus, config config.
return &signalClient{
nodeID: nodeID,
config: config,
client: c,
client: client,
}, nil
}
@@ -88,25 +89,27 @@ func (r *signalClient) StartParticipantSignal(
return
}
l := logger.GetLogger().WithValues(
l := utils.GetLogger(ctx).WithValues(
"room", roomName,
"reqNodeID", nodeID,
"participant", pi.Identity,
"connID", connectionID,
"participantInit", &pi,
"startSession", logger.Proto(ss),
)
l.Debugw("starting signal connection")
stream, err := r.client.RelaySignal(ctx, nodeID)
if err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
prometheus.RecordSignalRequestFailure()
return
}
err = stream.Send(&rpc.RelaySignalRequest{StartSession: ss})
if err != nil {
stream.Close(err)
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
prometheus.RecordSignalRequestFailure()
return
}
@@ -130,6 +133,8 @@ func (r *signalClient) StartParticipantSignal(
resChan,
signalResponseMessageReader{},
r.config,
prometheus.RecordSignalResponseSuccess,
prometheus.RecordSignalResponseFailure,
)
l.Debugw("signal stream closed", "error", err)
@@ -139,6 +144,8 @@ func (r *signalClient) StartParticipantSignal(
return connectionID, sink, resChan, nil
}
// ------------------------------
type signalRequestMessageWriter struct{}
func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.Message) *rpc.RelaySignalRequest {
@@ -153,6 +160,8 @@ func (e signalRequestMessageWriter) Write(seq uint64, close bool, msgs []proto.M
return r
}
// -------------------------------
type signalResponseMessageReader struct{}
func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.Message, error) {
@@ -163,6 +172,8 @@ func (e signalResponseMessageReader) Read(rm *rpc.RelaySignalResponse) ([]proto.
return msgs, nil
}
// -----------------------------------------
type RelaySignalMessage interface {
proto.Message
GetSeq() uint64
@@ -182,6 +193,8 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
ch *MessageChannel,
reader SignalMessageReader[RecvType],
config config.SignalRelayConfig,
promSignalSuccess func(),
promSignalFailure func(),
) error {
r := &signalMessageReader[SendType, RecvType]{
reader: reader,
@@ -190,16 +203,16 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
for msg := range stream.Channel() {
res, err := r.Read(msg)
if err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
promSignalFailure()
return err
}
for _, r := range res {
if err = ch.WriteMessage(r); err != nil {
prometheus.MessageCounter.WithLabelValues("signal", "failure").Add(1)
promSignalFailure()
return err
}
prometheus.MessageCounter.WithLabelValues("signal", "success").Add(1)
promSignalSuccess()
}
if msg.GetClose() {
@@ -209,6 +222,8 @@ func CopySignalStreamToMessageChannel[SendType, RecvType RelaySignalMessage](
return stream.Err()
}
// ----------------------------------------
type signalMessageReader[SendType, RecvType RelaySignalMessage] struct {
seq uint64
reader SignalMessageReader[RecvType]
@@ -236,6 +251,8 @@ func (r *signalMessageReader[SendType, RecvType]) Read(msg RecvType) ([]proto.Me
return res, nil
}
// ----------------------------------------
type SignalSinkParams[SendType, RecvType RelaySignalMessage] struct {
Stream psrpc.Stream[SendType, RecvType]
Logger logger.Logger
+30 -10
View File
@@ -15,6 +15,7 @@
package rtc
import (
"slices"
"strconv"
"strings"
@@ -45,11 +46,15 @@ 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) SupportPrflxOverRelay() bool {
func (c ClientInfo) SupportsPrflxOverRelay() bool {
return !c.isFirefox()
}
@@ -93,21 +98,36 @@ func (c ClientInfo) SupportsChangeRTPSenderEncodingActive() bool {
}
func (c ClientInfo) ComplyWithCodecOrderInSDPAnswer() bool {
return !((c.isLinux() || c.isAndroid()) && c.isFirefox())
return (!c.isLinux() && !c.isAndroid()) || !c.isFirefox()
}
// Rust SDK can't decode unknown signal message (TrackSubscribed and ErrorResponse)
func (c ClientInfo) SupportTrackSubscribedEvent() bool {
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_RUST && c.ClientInfo.GetProtocol() < 10)
func (c ClientInfo) SupportsTrackSubscribedEvent() bool {
return c.ClientInfo.GetSdk() != livekit.ClientInfo_RUST || c.ClientInfo.GetProtocol() >= 10
}
func (c ClientInfo) SupportErrorResponse() bool {
return c.SupportTrackSubscribedEvent()
func (c ClientInfo) SupportsRequestResponse() bool {
return c.SupportsTrackSubscribedEvent()
}
func (c ClientInfo) SupportSctpZeroChecksum() bool {
return !(c.ClientInfo.GetSdk() == livekit.ClientInfo_UNKNOWN ||
(c.isGo() && c.compareVersion("2.4.0") < 0))
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
@@ -121,7 +141,7 @@ func (c ClientInfo) compareVersion(version string) int {
parts1 := strings.Split(version, ".")
ints0 := make([]int, 3)
ints1 := make([]int, 3)
for i := 0; i < 3; i++ {
for i := range 3 {
if len(parts0) > i {
ints0[i], _ = strconv.Atoi(parts0[i])
}
+70 -29
View File
@@ -20,13 +20,14 @@ import (
"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 (
frameMarking = "urn:ietf:params:rtp-hdrext:framemarking"
repairedRTPStreamID = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
frameMarkingURI = "urn:ietf:params:rtp-hdrext:framemarking"
repairedRTPStreamIDURI = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
)
type WebRTCConfig struct {
@@ -79,23 +80,82 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
rtcConf.PacketBufferSizeAudio = rtcConf.PacketBufferSize
}
// publisher configuration
publisherConfig := DirectionConfig{
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,
act.AbsCaptureTimeURI,
},
Video: []string{
sdp.SDESMidURI,
sdp.SDESRTPStreamIDURI,
sdp.TransportCCURI,
frameMarking,
frameMarkingURI,
dd.ExtensionURI,
repairedRTPStreamID,
//act.AbsCaptureTimeURI,
repairedRTPStreamIDURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
@@ -110,25 +170,6 @@ func NewWebRTCConfig(conf *config.Config) (*WebRTCConfig, error) {
},
},
}
return &WebRTCConfig{
WebRTCConfig: *webRTCConfig,
Receiver: ReceiverConfig{
PacketBufferSizeVideo: rtcConf.PacketBufferSizeVideo,
PacketBufferSizeAudio: rtcConf.PacketBufferSizeAudio,
},
Publisher: publisherConfig,
Subscriber: getSubscriberConfig(rtcConf.CongestionControl.UseSendSideBWEInterceptor || rtcConf.CongestionControl.UseSendSideBWE),
}, nil
}
func (c *WebRTCConfig) UpdateCongestionControl(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 getSubscriberConfig(enableTWCC bool) DirectionConfig {
@@ -136,10 +177,10 @@ func getSubscriberConfig(enableTWCC bool) DirectionConfig {
RTPHeaderExtension: RTPHeaderExtensionConfig{
Video: []string{
dd.ExtensionURI,
//act.AbsCaptureTimeURI,
act.AbsCaptureTimeURI,
},
Audio: []string{
//act.AbsCaptureTimeURI,
act.AbsCaptureTimeURI,
},
},
RTCPFeedback: RTCPFeedbackConfig{
+109
View File
@@ -0,0 +1,109 @@
// 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"
"time"
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/livekit-server/pkg/rtc/types"
"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
}
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.logger.Infow("closing data down track")
if d.params.BytesTrackStats != nil {
d.params.BytesTrackStats.Stop()
}
d.params.PublishDataTrack.DeleteDataDownTrack(d.SubscriberID())
}
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
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"errors"
"sync"
"github.com/frostbyte73/core"
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/livekit-server/pkg/rtc/types"
sfuutils "github.com/livekit/livekit-server/pkg/sfu/utils"
"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 DataTrack struct {
params DataTrackParams
logger logger.Logger
lock sync.Mutex
dti *livekit.DataTrackInfo
subscribedTracks map[livekit.ParticipantID]types.DataDownTrack
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]types.DataDownTrack),
}
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()] = dataDownTrack
return dataDownTrack, nil
}
func (d *DataTrack) RemoveSubscriber(subID livekit.ParticipantID) {
d.lock.Lock()
dataDownTrack, ok := d.subscribedTracks[subID]
delete(d.subscribedTracks, subID)
d.lock.Unlock()
if ok {
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) 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)
})
}
@@ -0,0 +1,59 @@
// 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 datatrack
import (
"errors"
"github.com/livekit/protocol/livekit"
)
type ExtensionParticipantSid struct {
participantID livekit.ParticipantID
}
func NewExtensionParticipantSid(participantID livekit.ParticipantID) (*ExtensionParticipantSid, error) {
if len(participantID) >= 256 {
return nil, errors.New("participantID too long")
}
return &ExtensionParticipantSid{participantID}, nil
}
func (e *ExtensionParticipantSid) ParticipantID() livekit.ParticipantID {
return e.participantID
}
func (e *ExtensionParticipantSid) Marshal() (Extension, error) {
data := make([]byte, len(e.participantID))
copy(data, e.participantID)
return Extension{
id: uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
data: data,
}, nil
}
func (e *ExtensionParticipantSid) Unmarshal(ext Extension) error {
if ext.id != uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID) {
return errors.New("invalid extension ID")
}
if len(ext.data) == 0 {
return errors.New("empty extension data")
}
e.participantID = livekit.ParticipantID(ext.data)
return nil
}
@@ -0,0 +1,46 @@
// 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 datatrack
import (
"testing"
"github.com/livekit/protocol/livekit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExtensionParticipantSid(t *testing.T) {
longTestParticipantID := livekit.ParticipantID(make([]byte, 256))
_, err := NewExtensionParticipantSid(longTestParticipantID)
require.Error(t, err)
testParticipantID := livekit.ParticipantID("test")
extParticipantSid, err := NewExtensionParticipantSid(testParticipantID)
require.NoError(t, err)
expectedExt := Extension{
id: uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID),
data: []byte{'t', 'e', 's', 't'},
}
ext, err := extParticipantSid.Marshal()
require.NoError(t, err)
require.Equal(t, expectedExt, ext)
var unmarshaled ExtensionParticipantSid
err = unmarshaled.Unmarshal(ext)
require.NoError(t, err)
assert.Equal(t, testParticipantID, unmarshaled.ParticipantID())
}
+301
View File
@@ -0,0 +1,301 @@
// 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 datatrack
import (
"encoding/binary"
"errors"
"fmt"
)
var (
errHeaderSizeInsufficient = errors.New("data track packet header size insufficient")
errBufferSizeInsufficient = errors.New("data track packet buffer size insufficient")
errExtensionSizeInsufficient = errors.New("data track packet extension size insufficient")
errExtensionNotFound = errors.New("data track packet extension not found")
errExtensionSizeTooBig = errors.New("extension size is too big")
)
const (
headerLength = 12
versionShift = 5
versionMask = (1 << 3) - 1
startOfFrameShift = 4
startOfFrameMask = (1 << 1) - 1
finalOfFrameShift = 3
finalOfFrameMask = (1 << 1) - 1
extensionsShift = 2
extensionsMask = (1 << 1) - 1
handleOffset = 2
handleLength = 2
seqNumOffset = 4
seqNumLength = 2
frameNumOffset = 6
frameNumLength = 2
timestampOffset = 8
timestampLength = 4
extensionsSizeOffset = headerLength
extensionsSizeLength = 2
extensionIDLength = 1
extensionSizeLength = 1
)
type Extension struct {
id uint8
data []byte
}
type Header struct {
Version uint8
IsStartOfFrame bool
IsFinalOfFrame bool
HasExtensions bool
Handle uint16
SequenceNumber uint16
FrameNumber uint16
Timestamp uint32
ExtensionsSize uint16
Extensions []Extension
}
/*
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┆* 0 1 2 3
┆* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┆* |V |S|F|X| reserved | handle |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┆* | sequence number | frame number |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┆* | timestamp |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|* Extensions Size if X=1 | Extensions... |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Each extension
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┆* 0 1 2 3
┆* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┆* | Extension ID | Extension size| Extension payload |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
End of all extensions
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|* padded to 4 byte boundary if aggregate of `Extensions Size` |
|* field and all extensions do not end on a 4 byte boundary |
┆* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
func (h *Header) Unmarshal(buf []byte) (int, error) {
if len(buf) < headerLength {
return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength)
}
hdrSize := headerLength
h.Version = buf[0] >> versionShift & versionMask
h.IsStartOfFrame = (buf[0] >> startOfFrameShift & startOfFrameMask) > 0
h.IsFinalOfFrame = (buf[0] >> finalOfFrameShift & finalOfFrameMask) > 0
h.HasExtensions = (buf[0] >> extensionsShift & extensionsMask) > 0
h.Handle = binary.BigEndian.Uint16(buf[handleOffset : handleOffset+handleLength])
h.SequenceNumber = binary.BigEndian.Uint16(buf[seqNumOffset : seqNumOffset+seqNumLength])
h.FrameNumber = binary.BigEndian.Uint16(buf[frameNumOffset : frameNumOffset+frameNumLength])
h.Timestamp = binary.BigEndian.Uint32(buf[timestampOffset : timestampOffset+timestampLength])
if h.HasExtensions {
extensionsSize := (binary.BigEndian.Uint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength])+1)*4 - extensionsSizeLength
hdrSize += extensionsSizeLength
extensionHeaderSize := extensionIDLength + extensionSizeLength
remainingSize := int(extensionsSize)
idx := extensionsSizeOffset + extensionsSizeLength
for remainingSize != 0 {
// read extension header
if len(buf[idx:]) < extensionIDLength || remainingSize < extensionIDLength {
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), extensionIDLength)
}
id := buf[idx]
if id == 0 {
// end of extensions, padding has started
hdrSize += remainingSize
break
}
if len(buf[idx+1:]) < extensionSizeLength || remainingSize < extensionSizeLength {
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), extensionSizeLength)
}
size := int(buf[idx+1])
remainingSize -= extensionHeaderSize
idx += extensionHeaderSize
hdrSize += extensionHeaderSize
// read extension data
if len(buf[idx:]) < size || remainingSize < size {
return 0, fmt.Errorf("%w: %d/%d < %d", errExtensionSizeInsufficient, remainingSize, len(buf[idx:]), size)
}
h.Extensions = append(h.Extensions, Extension{id: id, data: buf[idx : idx+size]})
remainingSize -= size
idx += size
hdrSize += size
}
h.ExtensionsSize = extensionsSize - uint16(remainingSize)
}
return hdrSize, nil
}
func (h *Header) MarshalSize() int {
extensionsSize := 0
if h.HasExtensions {
extensionsSize += extensionsSizeLength
for _, ext := range h.Extensions {
extensionsSize += len(ext.data) + extensionIDLength + extensionSizeLength
}
}
return headerLength + (extensionsSize+3)/4*4
}
func (h *Header) MarshalTo(buf []byte) (int, error) {
if len(buf) < headerLength {
return 0, fmt.Errorf("%w: %d < %d", errHeaderSizeInsufficient, len(buf), headerLength)
}
hdrSize := headerLength
buf[0] = h.Version << versionShift
if h.IsStartOfFrame {
buf[0] |= (1 << startOfFrameShift)
}
if h.IsFinalOfFrame {
buf[0] |= (1 << finalOfFrameShift)
}
if h.HasExtensions {
buf[0] |= (1 << extensionsShift)
}
binary.BigEndian.PutUint16(buf[handleOffset:handleOffset+handleLength], h.Handle)
binary.BigEndian.PutUint16(buf[seqNumOffset:seqNumOffset+seqNumLength], h.SequenceNumber)
binary.BigEndian.PutUint16(buf[frameNumOffset:frameNumOffset+frameNumLength], h.FrameNumber)
binary.BigEndian.PutUint32(buf[timestampOffset:timestampOffset+timestampLength], h.Timestamp)
if h.HasExtensions {
extensionsSize := (extensionsSizeLength + h.ExtensionsSize + 3) / 4 * 4
binary.BigEndian.PutUint16(buf[extensionsSizeOffset:extensionsSizeOffset+extensionsSizeLength], (extensionsSize/4)-1)
hdrSize += extensionsSizeLength
addedSize := 0
idx := extensionsSizeOffset + extensionsSizeLength
for _, ext := range h.Extensions {
buf[idx] = ext.id
if len(ext.data) > 255 {
return 0, fmt.Errorf("%w: %d > 255", errExtensionSizeTooBig, len(ext.data))
}
buf[idx+extensionIDLength] = byte(len(ext.data))
copy(buf[idx+extensionIDLength+extensionSizeLength:], ext.data)
extSize := len(ext.data) + extensionIDLength + extensionSizeLength
idx += extSize
hdrSize += extSize
addedSize += extSize
}
paddingSize := extensionsSize - extensionsSizeLength - uint16(addedSize)
for i := range paddingSize {
buf[idx+int(i)] = 0
}
idx += int(paddingSize)
hdrSize += int(paddingSize)
}
return hdrSize, nil
}
func (h *Header) AddExtension(ext Extension) {
for i, existingExt := range h.Extensions {
if existingExt.id == ext.id {
h.ExtensionsSize -= uint16(len(existingExt.data) + extensionIDLength + extensionSizeLength)
h.Extensions[i].data = ext.data
h.ExtensionsSize += uint16(len(h.Extensions[i].data) + extensionIDLength + extensionSizeLength)
return
}
}
h.Extensions = append(h.Extensions, ext)
h.ExtensionsSize += uint16(len(ext.data) + extensionIDLength + extensionSizeLength)
h.HasExtensions = true
}
func (h *Header) GetExtension(id uint8) (Extension, error) {
for _, ext := range h.Extensions {
if ext.id == id {
return ext, nil
}
}
return Extension{}, fmt.Errorf("%w, id: %d", errExtensionNotFound, id)
}
// ----------------------------------------------------
type Packet struct {
Header
Payload []byte
}
func (p *Packet) Unmarshal(buf []byte) error {
hdrSize, err := p.Header.Unmarshal(buf)
if err != nil {
return err
}
p.Payload = buf[hdrSize:]
return nil
}
func (p *Packet) Marshal() ([]byte, error) {
buf := make([]byte, p.Header.MarshalSize()+len(p.Payload))
if err := p.MarshalTo(buf); err != nil {
return nil, err
}
return buf, nil
}
func (p *Packet) MarshalTo(buf []byte) error {
size := p.Header.MarshalSize() + len(p.Payload)
if len(buf) < size {
return fmt.Errorf("%w: %d < %d", errBufferSizeInsufficient, len(buf), size)
}
hdrSize, err := p.Header.MarshalTo(buf)
if err != nil {
return err
}
copy(buf[hdrSize:], p.Payload)
return nil
}
@@ -0,0 +1,259 @@
// 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 datatrack
import (
"testing"
"github.com/livekit/protocol/livekit"
"github.com/stretchr/testify/require"
)
func TestPacket(t *testing.T) {
t.Run("without extension", func(t *testing.T) {
payload := make([]byte, 6)
for i := range len(payload) {
payload[i] = byte(255 - i)
}
packet := &Packet{
Header: Header{
Version: 0,
IsStartOfFrame: true,
IsFinalOfFrame: true,
Handle: 3333,
SequenceNumber: 6666,
FrameNumber: 9999,
Timestamp: 0xdeadbeef,
},
Payload: payload,
}
rawPacket, err := packet.Marshal()
require.NoError(t, err)
expectedRawPacket := []byte{
0x18, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0xff, 0xfe, 0xfd, 0xfc,
0xfb, 0xfa,
}
require.Equal(t, expectedRawPacket, rawPacket)
var unmarshaled Packet
err = unmarshaled.Unmarshal(rawPacket)
require.NoError(t, err)
require.Equal(t, packet, &unmarshaled)
})
t.Run("with extension", func(t *testing.T) {
payload := make([]byte, 4)
for i := range len(payload) {
payload[i] = byte(255 - i)
}
packet := &Packet{
Header: Header{
Version: 0,
IsStartOfFrame: true,
IsFinalOfFrame: false,
Handle: 3333,
SequenceNumber: 6666,
FrameNumber: 9999,
Timestamp: 0xdeadbeef,
},
Payload: payload,
}
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
if ext, err := extParticipantSid.Marshal(); err == nil {
packet.AddExtension(ext)
}
}
rawPacket, err := packet.Marshal()
require.NoError(t, err)
expectedRawPacket := []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x04, 0x01, 0x10,
0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x72,
0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74,
0xff, 0xfe, 0xfd, 0xfc,
}
require.Equal(t, expectedRawPacket, rawPacket)
var unmarshaled Packet
err = unmarshaled.Unmarshal(rawPacket)
require.NoError(t, err)
require.Equal(t, packet, &unmarshaled)
ext, err := unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
require.NoError(t, err)
var extParticipantSid ExtensionParticipantSid
require.NoError(t, extParticipantSid.Unmarshal(ext))
require.Equal(t, livekit.ParticipantID("test_participant"), extParticipantSid.ParticipantID())
})
t.Run("with extension padding", func(t *testing.T) {
payload := make([]byte, 4)
for i := range len(payload) {
payload[i] = byte(255 - i)
}
packet := &Packet{
Header: Header{
Version: 0,
IsStartOfFrame: true,
IsFinalOfFrame: false,
Handle: 3333,
SequenceNumber: 6666,
FrameNumber: 9999,
Timestamp: 0xdeadbeef,
},
Payload: payload,
}
if extParticipantSid, err := NewExtensionParticipantSid("participant"); err == nil {
if ext, err := extParticipantSid.Marshal(); err == nil {
packet.AddExtension(ext)
}
}
rawPacket, err := packet.Marshal()
require.NoError(t, err)
expectedRawPacket := []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x0b,
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
}
require.Equal(t, expectedRawPacket, rawPacket)
var unmarshaled Packet
err = unmarshaled.Unmarshal(rawPacket)
require.NoError(t, err)
require.Equal(t, packet, &unmarshaled)
ext, err := unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
require.NoError(t, err)
var extParticipantSid ExtensionParticipantSid
require.NoError(t, extParticipantSid.Unmarshal(ext))
require.Equal(t, livekit.ParticipantID("participant"), extParticipantSid.ParticipantID())
})
t.Run("replace extension", func(t *testing.T) {
payload := make([]byte, 4)
for i := range len(payload) {
payload[i] = byte(255 - i)
}
packet := &Packet{
Header: Header{
Version: 0,
IsStartOfFrame: true,
IsFinalOfFrame: false,
Handle: 3333,
SequenceNumber: 6666,
FrameNumber: 9999,
Timestamp: 0xdeadbeef,
},
Payload: payload,
}
if extParticipantSid, err := NewExtensionParticipantSid("participant"); err == nil {
if ext, err := extParticipantSid.Marshal(); err == nil {
packet.AddExtension(ext)
}
}
rawPacket, err := packet.Marshal()
require.NoError(t, err)
expectedRawPacket := []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x0b,
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
}
require.Equal(t, expectedRawPacket, rawPacket)
// replace existing extension ID and ensure that marshalled packet is updated
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
if ext, err := extParticipantSid.Marshal(); err == nil {
packet.AddExtension(ext)
}
}
rawPacket, err = packet.Marshal()
require.NoError(t, err)
expectedRawPacket = []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x04, 0x01, 0x10,
0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x72,
0x74, 0x69, 0x63, 0x69, 0x70, 0x61, 0x6e, 0x74,
0xff, 0xfe, 0xfd, 0xfc,
}
require.Equal(t, expectedRawPacket, rawPacket)
var unmarshaled Packet
err = unmarshaled.Unmarshal(rawPacket)
require.NoError(t, err)
require.Equal(t, packet, &unmarshaled)
ext, err := unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
require.NoError(t, err)
var extParticipantSid ExtensionParticipantSid
require.NoError(t, extParticipantSid.Unmarshal(ext))
require.Equal(t, livekit.ParticipantID("test_participant"), extParticipantSid.ParticipantID())
})
t.Run("bad packet", func(t *testing.T) {
var unmarshaled Packet
// extensions size too small
badPacket := []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x02, 0x01, 0x0b,
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
}
err := unmarshaled.Unmarshal(badPacket)
require.Error(t, err)
// get an invalid extension id
badPacket = []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x02, 0x0b,
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
}
err = unmarshaled.Unmarshal(badPacket)
require.NoError(t, err)
_, err = unmarshaled.GetExtension(uint8(livekit.DataTrackExtensionID_DTEI_PARTICIPANT_SID))
require.Error(t, err)
// extension payload size bigger than payload
badPacket = []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x0d,
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
}
err = unmarshaled.Unmarshal(badPacket)
require.Error(t, err)
// extension payload size smaller than payload
badPacket = []byte{
0x14, 0x00, 0x0d, 0x05, 0x1a, 0x0a, 0x27, 0x0f,
0xde, 0xad, 0xbe, 0xef, 0x00, 0x03, 0x01, 0x07,
0x70, 0x61, 0x72, 0x74, 0x69, 0x63, 0x69, 0x70,
0x61, 0x6e, 0x74, 0x00, 0xff, 0xfe, 0xfd, 0xfc,
}
err = unmarshaled.Unmarshal(badPacket)
require.Error(t, err)
})
}
@@ -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 datatrack
import (
"math/rand"
"time"
)
func GenerateRawDataPackets(handle uint16, seqNum uint16, frameNum uint16, numFrames int, frameSize int, frameDuration time.Duration) [][]byte {
if seqNum == 0 {
seqNum = uint16(rand.Intn(256) + 1)
}
if frameNum == 0 {
frameNum = uint16(rand.Intn(256) + 1)
}
timestamp := uint32(rand.Intn(1024))
packetsPerFrame := (frameSize + 255) / 256 // using 256 bytes of payload per packet
if packetsPerFrame == 0 {
return nil
}
numPackets := packetsPerFrame * numFrames
rawPackets := make([][]byte, 0, numPackets)
for range numFrames {
remainingSize := frameSize
for packetIdx := range packetsPerFrame {
payloadSize := min(remainingSize, 256)
payload := make([]byte, payloadSize)
for i := range len(payload) {
payload[i] = byte(255 - i)
}
packet := &Packet{
Header: Header{
Version: 0,
IsStartOfFrame: packetIdx == 0,
IsFinalOfFrame: packetIdx == packetsPerFrame-1,
Handle: handle,
SequenceNumber: seqNum,
FrameNumber: frameNum,
Timestamp: timestamp,
},
Payload: payload,
}
if extParticipantSid, err := NewExtensionParticipantSid("test_participant"); err == nil {
if ext, err := extParticipantSid.Marshal(); err == nil {
packet.AddExtension(ext)
}
}
rawPacket, err := packet.Marshal()
if err == nil {
rawPackets = append(rawPackets, rawPacket)
}
seqNum++
remainingSize -= payloadSize
}
frameNum++
timestamp += uint32(90000 * frameDuration.Seconds())
}
return rawPackets
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rtc
import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/rtc/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,
)
}
}
@@ -15,7 +15,8 @@
package dynacast
import (
"sort"
"slices"
"strings"
"sync"
"testing"
"time"
@@ -23,20 +24,41 @@ import (
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
func TestSubscribedMaxQuality(t *testing.T) {
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) {
dm := NewDynacastManager(DynacastManagerParams{})
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
@@ -72,21 +94,20 @@ func TestSubscribedMaxQuality(t *testing.T) {
})
t.Run("subscribers max quality", func(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{
DynacastPauseDelay: 100 * time.Millisecond,
})
lock := sync.RWMutex{}
lock.Lock()
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
lock.Unlock()
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
Listener: &testDynacastManagerListener{
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
},
},
})
dm.maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
dm.(*dynacastManagerVideo).maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
mime.MimeTypeVP8: livekit.VideoQuality_LOW,
mime.MimeTypeAV1: livekit.VideoQuality_LOW,
}
@@ -279,98 +300,224 @@ func TestSubscribedMaxQuality(t *testing.T) {
}
func TestCodecRegression(t *testing.T) {
dm := NewDynacastManager(DynacastManagerParams{})
var lock sync.Mutex
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
dm.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, _maxSubscribedQualities []types.SubscribedCodecQuality) {
lock.Lock()
actualSubscribedQualities = subscribedQualities
lock.Unlock()
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)
})
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
t.Run("codec regression audio", func(t *testing.T) {
var lock sync.Mutex
actualSubscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0)
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},
dm := NewDynacastManagerAudio(DynacastManagerAudioParams{
Listener: &testDynacastManagerListener{
onSubscribedAudioCodecChange: func(subscribedCodecs []*livekit.SubscribedAudioCodec) {
lock.Lock()
actualSubscribedCodecs = subscribedCodecs
lock.Unlock()
},
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
})
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
dm.NotifySubscription("s1", mime.MimeTypeRED, true)
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},
expectedSubscribedCodecs := []*livekit.SubscribedAudioCodec{
{
Codec: mime.MimeTypeRED.String(),
Enabled: true,
},
},
{
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 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,
},
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
{
Codec: mime.MimeTypeOpus.String(),
Enabled: true,
},
}
require.Eventually(t, func() bool {
lock.Lock()
defer lock.Unlock()
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
}, 10*time.Second, 100*time.Millisecond)
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)
// 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)
}
func subscribedCodecsAsString(c1 []*livekit.SubscribedCodec) string {
sort.Slice(c1, func(i, j int) bool { return c1[i].Codec < c1[j].Codec })
var s1 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 += c.String()
s1.WriteString(c.String())
}
return s1
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()
}
@@ -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)
})
}
@@ -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))
}
@@ -15,148 +15,85 @@
package dynacast
import (
"sync"
"maps"
"time"
"github.com/bep/debounce"
"golang.org/x/exp/maps"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/utils"
)
type DynacastManagerParams struct {
var _ DynacastManager = (*dynacastManagerVideo)(nil)
var _ dynacastQualityListener = (*dynacastManagerVideo)(nil)
type DynacastManagerVideoParams struct {
DynacastPauseDelay time.Duration
Listener DynacastManagerListener
Logger logger.Logger
}
type DynacastManager struct {
params DynacastManagerParams
type dynacastManagerVideo struct {
params DynacastManagerVideoParams
lock sync.RWMutex
regressedCodec map[mime.MimeType]struct{}
dynacastQuality map[mime.MimeType]*DynacastQuality
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
maxSubscribedQualityDebounce func(func())
maxSubscribedQualityDebouncePending bool
qualityNotifyOpQueue *utils.OpsQueue
isClosed bool
onSubscribedMaxQualityChange func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)
*dynacastManagerBase
}
func NewDynacastManager(params DynacastManagerParams) *DynacastManager {
func NewDynacastManagerVideo(params DynacastManagerVideoParams) DynacastManager {
if params.Logger == nil {
params.Logger = logger.GetLogger()
}
d := &DynacastManager{
d := &dynacastManagerVideo{
params: params,
regressedCodec: make(map[mime.MimeType]struct{}),
dynacastQuality: make(map[mime.MimeType]*DynacastQuality),
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
qualityNotifyOpQueue: utils.NewOpsQueue(utils.OpsQueueParams{
Name: "quality-notify",
MinSize: 64,
FlushOnStop: true,
Logger: params.Logger,
}),
}
if params.DynacastPauseDelay > 0 {
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
}
d.qualityNotifyOpQueue.Start()
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
}
func (d *DynacastManager) OnSubscribedMaxQualityChange(f func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality)) {
d.lock.Lock()
d.onSubscribedMaxQualityChange = f
d.lock.Unlock()
}
func (d *DynacastManager) AddCodec(mime mime.MimeType) {
d.getOrCreateDynacastQuality(mime)
}
func (d *DynacastManager) 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)
d.lock.Unlock()
return
}
d.regressedCodec[fromMime] = struct{}{}
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
}
d.lock.Unlock()
d.update(false)
fromDq.Stop()
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
}
func (d *DynacastManager) Restart() {
d.lock.Lock()
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
dqs := d.getDynacastQualitiesLocked()
d.lock.Unlock()
for _, dq := range dqs {
dq.Restart()
}
}
func (d *DynacastManager) Close() {
d.qualityNotifyOpQueue.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 subscribed quality needs to sent to the provider immediately.
// This bypasses any debouncing and forces a subscribed quality update
// with immediate effect.
func (d *DynacastManager) ForceUpdate() {
d.update(true)
}
// 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 *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
func (d *dynacastManagerVideo) ForceQuality(quality livekit.VideoQuality) {
d.lock.Lock()
defer d.lock.Unlock()
@@ -167,14 +104,21 @@ func (d *DynacastManager) ForceQuality(quality livekit.VideoQuality) {
d.enqueueSubscribedQualityChange()
}
func (d *DynacastManager) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, mime mime.MimeType, quality livekit.VideoQuality) {
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 *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
func (d *dynacastManagerVideo) NotifySubscriberNodeMaxQuality(
nodeID livekit.NodeID,
qualities []types.SubscribedCodecQuality,
) {
for _, quality := range qualities {
dq := d.getOrCreateDynacastQuality(quality.CodecMime)
if dq != nil {
@@ -183,34 +127,10 @@ func (d *DynacastManager) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
}
}
func (d *DynacastManager) 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 := NewDynacastQuality(DynacastQualityParams{
MimeType: mimeType,
Logger: d.params.Logger,
})
dq.OnSubscribedMaxQualityChange(d.updateMaxQualityForMime)
dq.Start()
d.dynacastQuality[mimeType] = dq
return dq
}
func (d *DynacastManager) getDynacastQualitiesLocked() []*DynacastQuality {
return maps.Values(d.dynacastQuality)
}
func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality livekit.VideoQuality) {
func (d *dynacastManagerVideo) OnUpdateMaxQualityForMime(
mime mime.MimeType,
maxQuality livekit.VideoQuality,
) {
d.lock.Lock()
if _, ok := d.regressedCodec[mime]; !ok {
d.maxSubscribedQuality[mime] = maxQuality
@@ -220,10 +140,11 @@ func (d *DynacastManager) updateMaxQualityForMime(mime mime.MimeType, maxQuality
d.update(false)
}
func (d *DynacastManager) update(force bool) {
func (d *dynacastManagerVideo) update(force bool) {
d.lock.Lock()
d.params.Logger.Debugw("processing quality change",
d.params.Logger.Debugw(
"processing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
@@ -260,7 +181,8 @@ func (d *DynacastManager) update(force bool) {
if downgradesOnly && d.maxSubscribedQualityDebounce != nil {
if !d.maxSubscribedQualityDebouncePending {
d.params.Logger.Debugw("debouncing quality downgrade",
d.params.Logger.Debugw(
"debouncing quality downgrade",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
@@ -269,7 +191,8 @@ func (d *DynacastManager) update(force bool) {
})
d.maxSubscribedQualityDebouncePending = true
} else {
d.params.Logger.Debugw("quality downgrade waiting for debounce",
d.params.Logger.Debugw(
"quality downgrade waiting for debounce",
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
)
@@ -285,7 +208,8 @@ func (d *DynacastManager) update(force bool) {
d.maxSubscribedQualityDebouncePending = false
}
d.params.Logger.Debugw("committing quality change",
d.params.Logger.Debugw(
"committing quality change",
"force", force,
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
@@ -293,16 +217,14 @@ func (d *DynacastManager) update(force bool) {
// commit change
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality))
for mime, quality := range d.maxSubscribedQuality {
d.committedMaxSubscribedQuality[mime] = quality
}
maps.Copy(d.committedMaxSubscribedQuality, d.maxSubscribedQuality)
d.enqueueSubscribedQualityChange()
d.lock.Unlock()
}
func (d *DynacastManager) enqueueSubscribedQualityChange() {
if d.isClosed || d.onSubscribedMaxQualityChange == nil {
func (d *dynacastManagerVideo) enqueueSubscribedQualityChange() {
if d.isClosed || d.params.Listener == nil {
return
}
@@ -343,7 +265,7 @@ func (d *DynacastManager) enqueueSubscribedQualityChange() {
"subscribedCodecs", subscribedCodecs,
"maxSubscribedQualities", maxSubscribedQualities,
)
d.qualityNotifyOpQueue.Enqueue(func() {
d.onSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
d.notifyOpsQueue.Enqueue(func() {
d.params.Listener.OnDynacastSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
})
}
@@ -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)
}
@@ -18,23 +18,26 @@ import (
"sync"
"time"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"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 DynacastQualityParams struct {
type dynacastQualityVideoParams struct {
MimeType mime.MimeType
Listener dynacastQualityListener
Logger logger.Logger
}
// DynacastQuality manages max subscribed quality of a single receiver of a media track
type DynacastQuality struct {
params DynacastQualityParams
// 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
@@ -43,38 +46,32 @@ type DynacastQuality struct {
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality
maxSubscribedQuality livekit.VideoQuality
maxQualityTimer *time.Timer
regressTo *DynacastQuality
regressTo dynacastQuality
onSubscribedMaxQualityChange func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)
dynacastQualityNull
}
func NewDynacastQuality(params DynacastQualityParams) *DynacastQuality {
return &DynacastQuality{
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 *DynacastQuality) Start() {
func (d *dynacastQualityVideo) Start() {
d.reset()
}
func (d *DynacastQuality) Restart() {
func (d *dynacastQualityVideo) Restart() {
d.reset()
}
func (d *DynacastQuality) Stop() {
func (d *dynacastQualityVideo) Stop() {
d.stopMaxQualityTimer()
}
func (d *DynacastQuality) OnSubscribedMaxQualityChange(f func(mimeType mime.MimeType, maxSubscribedQuality livekit.VideoQuality)) {
d.lock.Lock()
defer d.lock.Unlock()
d.onSubscribedMaxQualityChange = f
}
func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
func (d *dynacastQualityVideo) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber max quality",
"mime", d.params.MimeType,
@@ -99,7 +96,7 @@ func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.Partic
d.updateQualityChange(false)
}
func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
func (d *dynacastQualityVideo) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
d.params.Logger.Debugw(
"setting subscriber node max quality",
"mime", d.params.MimeType,
@@ -110,8 +107,12 @@ func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
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()
r.params.Logger.Debugw("ignoring node quality change, regressed to another dynacast quality", "mime", d.params.MimeType)
return
}
@@ -125,7 +126,19 @@ func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID,
d.updateQualityChange(false)
}
func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
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
@@ -134,33 +147,41 @@ func (d *DynacastQuality) RegressTo(other *DynacastQuality) {
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
d.lock.Unlock()
other.lock.Lock()
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 otherQuality, ok := other.maxSubscriberQuality[subID]; ok {
if oldQuality, ok := d.maxSubscriberQuality[subID]; ok {
// no QUALITY_OFF in the map
if quality > otherQuality {
other.maxSubscriberQuality[subID] = quality
if quality > oldQuality {
d.maxSubscriberQuality[subID] = quality
}
} else {
other.maxSubscriberQuality[subID] = quality
d.maxSubscriberQuality[subID] = quality
}
}
for nodeID, quality := range maxSubscriberNodeQuality {
if otherQuality, ok := other.maxSubscriberNodeQuality[nodeID]; ok {
if oldQuality, ok := d.maxSubscriberNodeQuality[nodeID]; ok {
// no QUALITY_OFF in the map
if quality > otherQuality {
other.maxSubscriberNodeQuality[nodeID] = quality
if quality > oldQuality {
d.maxSubscriberNodeQuality[nodeID] = quality
}
} else {
other.maxSubscriberNodeQuality[nodeID] = quality
d.maxSubscriberNodeQuality[nodeID] = quality
}
}
other.lock.Unlock()
other.Restart()
d.lock.Unlock()
d.Restart()
}
func (d *DynacastQuality) reset() {
func (d *dynacastQualityVideo) reset() {
d.lock.Lock()
d.initialized = false
d.lock.Unlock()
@@ -168,7 +189,7 @@ func (d *DynacastQuality) reset() {
d.startMaxQualityTimer()
}
func (d *DynacastQuality) updateQualityChange(force bool) {
func (d *dynacastQualityVideo) updateQualityChange(force bool) {
d.lock.Lock()
maxSubscribedQuality := livekit.VideoQuality_OFF
for _, subQuality := range d.maxSubscriberQuality {
@@ -189,22 +210,20 @@ func (d *DynacastQuality) updateQualityChange(force bool) {
d.initialized = true
d.maxSubscribedQuality = maxSubscribedQuality
d.params.Logger.Debugw("notifying quality change",
d.params.Logger.Debugw(
"notifying quality change",
"mime", d.params.MimeType,
"maxSubscriberQuality", d.maxSubscriberQuality,
"maxSubscriberNodeQuality", d.maxSubscriberNodeQuality,
"maxSubscribedQuality", d.maxSubscribedQuality,
"force", force,
)
onSubscribedMaxQualityChange := d.onSubscribedMaxQualityChange
d.lock.Unlock()
if onSubscribedMaxQualityChange != nil {
onSubscribedMaxQualityChange(d.params.MimeType, maxSubscribedQuality)
}
d.params.Listener.OnUpdateMaxQualityForMime(d.params.MimeType, maxSubscribedQuality)
}
func (d *DynacastQuality) startMaxQualityTimer() {
func (d *dynacastQualityVideo) startMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
@@ -219,7 +238,7 @@ func (d *DynacastQuality) startMaxQualityTimer() {
})
}
func (d *DynacastQuality) stopMaxQualityTimer() {
func (d *dynacastQualityVideo) stopMaxQualityTimer() {
d.lock.Lock()
defer d.lock.Unlock()
@@ -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) {}
+22 -20
View File
@@ -29,6 +29,7 @@ import (
type EgressLauncher interface {
StartEgress(context.Context, *rpc.StartEgressRequest) (*livekit.EgressInfo, error)
StopEgress(context.Context, *livekit.StopEgressRequest) (*livekit.EgressInfo, error)
}
func StartParticipantEgress(
@@ -42,16 +43,17 @@ func StartParticipantEgress(
) error {
if req, err := startParticipantEgress(ctx, launcher, opts, identity, roomName, roomID); err != nil {
// send egress failed webhook
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
Event: webhook.EventEgressEnded,
EgressInfo: &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Participant{Participant: req},
},
})
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
@@ -103,16 +105,16 @@ func StartTrackEgress(
) error {
if req, err := startTrackEgress(ctx, launcher, opts, track, roomName, roomID); err != nil {
// send egress failed webhook
ts.NotifyEvent(ctx, &livekit.WebhookEvent{
Event: webhook.EventEgressEnded,
EgressInfo: &livekit.EgressInfo{
RoomId: string(roomID),
RoomName: string(roomName),
Status: livekit.EgressStatus_EGRESS_FAILED,
Error: err.Error(),
Request: &livekit.EgressInfo_Track{Track: req},
},
})
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
-3
View File
@@ -32,9 +32,6 @@ var (
ErrEmptyParticipantID = errors.New("participant ID cannot be empty")
ErrMissingGrants = errors.New("VideoGrant is missing")
ErrInternalError = errors.New("internal error")
ErrNameExceedsLimits = errors.New("name length exceeds limits")
ErrMetadataExceedsLimits = errors.New("metadata size exceeds limits")
ErrAttributesExceedsLimits = errors.New("attributes size exceeds limits")
// Track subscription related
ErrNoTrackPermission = errors.New("participant is not allowed to subscribe to this track")
+117 -122
View File
@@ -20,146 +20,67 @@ import (
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/sfu/mime"
protoCodecs "github.com/livekit/protocol/codecs"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
var OpusCodecCapability = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeOpus.String(),
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
}
var RedCodecCapability = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRED.String(),
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "111/111",
}
var videoRTX = webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRTX.String(),
ClockRate: 90000,
}
func registerCodecs(me *webrtc.MediaEngine, codecs []*livekit.Codec, rtcpFeedback RTCPFeedbackConfig, filterOutH264HighProfile bool) error {
opusCodec := OpusCodecCapability
opusCodec.RTCPFeedback = rtcpFeedback.Audio
var opusPayload webrtc.PayloadType
if IsCodecEnabled(codecs, opusCodec) {
opusPayload = 111
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: opusCodec,
PayloadType: opusPayload,
}, webrtc.RTPCodecTypeAudio); err != nil {
// 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, RedCodecCapability) {
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: RedCodecCapability,
PayloadType: 63,
}, webrtc.RTPCodecTypeAudio); err != nil {
if IsCodecEnabled(codecs, protoCodecs.RedCodecParameters.RTPCodecCapability) {
if err := me.RegisterCodec(protoCodecs.RedCodecParameters, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
}
rtxEnabled := IsCodecEnabled(codecs, videoRTX)
for _, codec := range []webrtc.RTPCodecParameters{protoCodecs.PCMUCodecParameters, protoCodecs.PCMACodecParameters} {
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
continue
}
h264HighProfileFmtp := "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032"
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP8.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP9.String(),
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 98,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeVP9.String(),
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 100,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 125,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f",
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 108,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeH264.String(),
ClockRate: 90000,
SDPFmtpLine: h264HighProfileFmtp,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 123,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeAV1.String(),
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 35,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH265,
ClockRate: 90000,
RTCPFeedback: rtcpFeedback.Video,
},
PayloadType: 116,
},
} {
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == h264HighProfileFmtp {
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)
for _, codec := range protoCodecs.VideoCodecsParameters {
if filterOutH264HighProfile && codec.RTPCodecCapability.SDPFmtpLine == protoCodecs.H264HighProfileFmtp {
continue
}
if mime.IsMimeTypeStringRTX(codec.MimeType) {
continue
}
if IsCodecEnabled(codecs, codec.RTPCodecCapability) {
if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
if rtxEnabled {
if err := me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: mime.MimeTypeRTX.String(),
ClockRate: 90000,
SDPFmtpLine: fmt.Sprintf("apt=%d", codec.PayloadType),
},
PayloadType: codec.PayloadType + 1,
}, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
if !IsCodecEnabled(codecs, codec.RTPCodecCapability) {
continue
}
cp := codec
cp.RTPCodecCapability.RTCPFeedback = rtcpFeedback.Video
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
if !rtxEnabled {
continue
}
cp = protoCodecs.VideoRTXCodecParameters
cp.RTPCodecCapability.SDPFmtpLine = fmt.Sprintf("apt=%d", codec.PayloadType)
cp.PayloadType = codec.PayloadType + 1
if err := me.RegisterCodec(cp, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
return nil
@@ -215,3 +136,77 @@ func selectAlternativeVideoCodec(enabledCodecs []*livekit.Codec) string {
// 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
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
)
+348 -122
View File
@@ -15,7 +15,6 @@
package rtc
import (
"context"
"math"
"sync"
"time"
@@ -24,8 +23,11 @@ import (
"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"
@@ -33,77 +35,100 @@ import (
"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/mime"
"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
numUpTracks atomic.Uint32
buffer *buffer.Buffer
everSubscribed atomic.Bool
*MediaTrackReceiver
*MediaLossProxy
dynacastManager *dynacast.DynacastManager
dynacastManager dynacast.DynacastManager
lock sync.RWMutex
rttFromXR atomic.Bool
enableRegression bool
backupCodecPolicy livekit.BackupCodecPolicy
regressionTargetCodec mime.MimeType
regressionTargetCodecReceived 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
}
type MediaTrackParams struct {
SignalCid string
SdpCid string
ParticipantID livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
BufferFactory *buffer.Factory
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
PLIThrottleConfig sfu.PLIThrottleConfig
AudioConfig sfu.AudioConfig
VideoConfig config.VideoConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
SimTracks map[uint32]SimulcastTrackInfo
OnRTCP func([]rtcp.Packet)
ForwardStats *sfu.ForwardStats
OnTrackEverSubscribed func(livekit.TrackID)
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 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
}
func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
t := &MediaTrack{
params: params,
params: params,
backupCodecPolicy: ti.BackupCodecPolicy,
}
// TODO: disable codec regression until simulcast-codec clients knows that
if ti.BackupCodecPolicy == livekit.BackupCodecPolicy_REGRESSION && len(ti.Codecs) > 1 {
t.enableRegression = true
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,
Telemetry: params.Telemetry,
Logger: params.Logger,
RegressionTargetCodec: t.regressionTargetCodec,
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 {
@@ -118,27 +143,57 @@ func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
t.MediaTrackReceiver.OnMediaLossFeedback(t.MediaLossProxy.HandleMaxLossFeedback)
}
if ti.Type == livekit.TrackType_VIDEO {
t.dynacastManager = dynacast.NewDynacastManager(dynacast.DynacastManagerParams{
switch ti.Type {
case livekit.TrackType_VIDEO:
t.dynacastManager = dynacast.NewDynacastManagerVideo(dynacast.DynacastManagerVideoParams{
DynacastPauseDelay: params.VideoConfig.DynacastPauseDelay,
Listener: t,
Logger: params.Logger,
})
t.MediaTrackReceiver.OnSetupReceiver(func(mime mime.MimeType) {
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) {
}
})
t.MediaTrackReceiver.OnSubscriberMaxQualityChange(
func(subscriberID livekit.ParticipantID, mimeType mime.MimeType, layer int32) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriberMaxQuality(
subscriberID,
mimeType,
buffer.SpatialLayerToVideoQuality(layer, t.MediaTrackReceiver.TrackInfo()),
buffer.GetVideoQualityForSpatialLayer(
mimeType,
layer,
t.MediaTrackReceiver.TrackInfo(),
),
)
},
)
t.MediaTrackReceiver.OnCodecRegression(func(old, new webrtc.RTPCodecParameters) {
t.dynacastManager.HandleCodecRegression(mime.NormalizeMimeType(old.MimeType), mime.NormalizeMimeType(new.MimeType))
})
}
}
},
)
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
}
@@ -151,24 +206,20 @@ func (t *MediaTrack) OnSubscribedMaxQualityChange(
maxSubscribedQualities []types.SubscribedCodecQuality,
) error,
) {
if t.dynacastManager == nil {
return
}
t.lock.Lock()
t.onSubscribedMaxQualityChange = f
t.lock.Unlock()
}
handler := func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) {
if f != nil && !t.IsMuted() {
_ = f(t.ID(), t.ToProto(), subscribedQualities, maxSubscribedQualities)
}
for _, q := range maxSubscribedQualities {
receiver := t.Receiver(q.CodecMime)
if receiver != nil {
receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(q.Quality, t.MediaTrackReceiver.TrackInfo()))
}
}
}
t.dynacastManager.OnSubscribedMaxQualityChange(handler)
func (t *MediaTrack) OnSubscribedAudioCodecChange(
f func(
trackID livekit.TrackID,
codecs []*livekit.SubscribedAudioCodec,
) error,
) {
t.lock.Lock()
t.onSubscribedAudioCodecChange = f
t.lock.Unlock()
}
func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) {
@@ -177,47 +228,84 @@ func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quali
}
}
func (t *MediaTrack) SignalCid() string {
return t.params.SignalCid
func (t *MediaTrack) NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec) {
if t.dynacastManager != nil {
t.dynacastManager.NotifySubscriptionNode(nodeID, codecs)
}
}
func (t *MediaTrack) HasSdpCid(cid string) bool {
if t.params.SdpCid == cid {
return true
func (t *MediaTrack) ClearSubscriberNodes() {
if t.dynacastManager != nil {
t.dynacastManager.ClearSubscriberNodes()
}
}
ti := t.MediaTrackReceiver.TrackInfoClone()
for _, c := range ti.Codecs {
if c.Cid == cid {
return true
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()
}
func (t *MediaTrack) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
t.MediaTrackReceiver.UpdateCodecCid(codecs)
}
// AddReceiver adds a new RTP receiver to the track, returns true when receiver represents a new codec
func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRemote, mid string) bool {
// 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
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)
t.params.Logger.Errorw("could not unmarshal RTCP", err, "size", len(bytes))
return
}
@@ -226,7 +314,13 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
case *rtcp.SourceDescription:
case *rtcp.SenderReport:
if pkt.SSRC == uint32(track.SSRC()) {
buff.SetSenderReportData(pkt.RTPTime, pkt.NTPTime, pkt.PacketCount, pkt.OctetCount)
buff.SetSenderReportData(&livekit.RTCPSenderReportState{
RtpTimestamp: pkt.RTPTime,
NtpTimestamp: pkt.NTPTime,
Packets: pkt.PacketCount,
Octets: uint64(pkt.OctetCount),
At: mono.UnixNano(),
})
}
case *rtcp.ExtendedReport:
rttFromXR:
@@ -255,13 +349,27 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
t.lock.Lock()
var regressCodec bool
mimeType := mime.NormalizeMimeType(track.Codec().MimeType)
layer := buffer.RidToSpatialLayer(track.RID(), ti)
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.lock.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 {
@@ -276,6 +384,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
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
@@ -285,9 +398,13 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
}
}
if priority < 0 {
t.params.Logger.Warnw("could not find codec for webrtc receiver", nil, "webrtcCodec", mimeType, "track", logger.Proto(ti))
t.params.Logger.Warnw(
"could not find codec for webrtc receiver", nil,
"mime", mimeType,
"track", logger.Proto(ti),
)
t.lock.Unlock()
return false
return newCodec, false
}
newWR := sfu.NewWebRTCReceiver(
@@ -300,11 +417,11 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
sfu.WithPliThrottleConfig(t.params.PLIThrottleConfig),
sfu.WithAudioConfig(t.params.AudioConfig),
sfu.WithLoadBalanceThreshold(20),
sfu.WithStreamTrackers(),
sfu.WithForwardStats(t.params.ForwardStats),
sfu.WithEnableRTPStreamRestartDetection(t.params.EnableRTPStreamRestartDetection),
)
newWR.OnCloseHandler(func() {
t.MediaTrackReceiver.SetClosing()
t.MediaTrackReceiver.SetClosing(false)
t.MediaTrackReceiver.ClearReceiver(mimeType, false)
if t.MediaTrackReceiver.TryClose() {
if t.dynacastManager != nil {
@@ -312,15 +429,61 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
}
}
})
// SIMULCAST-CODEC-TODO: these need to be receiver/mime aware, setting it up only for primary now
if priority == 0 {
newWR.OnStatsUpdate(func(_ *sfu.WebRTCReceiver, stat *livekit.AnalyticsStat) {
key := telemetry.StatsKeyForTrack(livekit.StreamType_UPSTREAM, t.PublisherID(), t.ID(), ti.Source, ti.Type)
t.params.Telemetry.TrackStats(key, stat)
})
newWR.OnMaxLayerChange(t.onMaxLayerChange)
// 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
t.lock.RLock()
regressionTargetCodecReceived := t.regressionTargetCodecReceived
t.lock.RUnlock()
if priority == 0 || regressionTargetCodecReceived {
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
t.lock.RLock()
regressionTargetCodecReceived := t.regressionTargetCodecReceived
t.lock.RUnlock()
if priority == 0 || regressionTargetCodecReceived {
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))
@@ -345,8 +508,8 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
t.MediaTrackReceiver.SetupReceiver(newWR, priority, mid)
for ssrc, info := range t.params.SimTracks {
if info.Mid == mid {
t.MediaTrackReceiver.SetLayerSsrc(mimeType, info.Rid, ssrc)
if info.Mid == mid && !info.IsRepairStream {
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, info.StreamID, ssrc, info.RepairSSRC)
}
}
wr = newWR
@@ -355,9 +518,18 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
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 && t.enableRegression {
if newCodec && t.enableRegression() {
if mimeType == t.regressionTargetCodec {
t.params.Logger.Infow("regression target codec received", "codec", mimeType)
t.regressionTargetCodecReceived = true
@@ -377,21 +549,28 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
"newCodec", newCodec,
)
buff.Close()
return false
return newCodec, false
}
// LK-TODO: can remove this completely when VideoLayers protocol becomes the default as it has info from client or if we decide to use TrackInfo.Simulcast
if t.numUpTracks.Inc() > 1 || track.RID() != "" {
// cannot only rely on numUpTracks since we fire metadata events immediately after the first layer
t.SetSimulcast(true)
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
}
var bitrates int
if len(ti.Layers) > int(layer) {
bitrates = int(ti.Layers[layer].GetBitrate())
}
t.MediaTrackReceiver.SetLayerSsrc(mimeType, track.RID(), uint32(track.SSRC()))
t.MediaTrackReceiver.MaybeSetSimulcast()
t.MediaTrackReceiver.SetLayerSsrcsForRid(mimeType, track.RID(), uint32(track.SSRC()), 0)
if regressCodec {
for _, c := range ti.Codecs {
@@ -408,7 +587,7 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
}
}
buff.Bind(receiver.GetParameters(), track.Codec().RTPCodecCapability, bitrates)
buff.OnNotifyRTX(t.MediaTrackReceiver.setLayerRtxInfo)
// if subscriber request fps before fps calculated, update them after fps updated.
buff.OnFpsChanged(func() {
@@ -416,20 +595,19 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
})
buff.OnFinalRtpStats(func(stats *livekit.RTPStats) {
t.params.Telemetry.TrackPublishRTPStats(
context.Background(),
t.params.ParticipantID,
t.params.TelemetryListener.OnTrackPublishRTPStats(
t.params.ParticipantID(),
t.ID(),
mimeType,
int(layer),
stats,
)
})
return newCodec
return newCodec, true
}
func (t *MediaTrack) GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality) {
receiver := t.PrimaryReceiver()
receiver := t.ActiveReceiver()
if rtcReceiver, ok := receiver.(*sfu.WebRTCReceiver); ok {
return rtcReceiver.GetConnectionScoreAndQuality()
}
@@ -447,10 +625,6 @@ func (t *MediaTrack) HasPendingCodec() bool {
return t.MediaTrackReceiver.PrimaryReceiver() == nil
}
func (t *MediaTrack) onMaxLayerChange(maxLayer int32) {
t.MediaTrackReceiver.NotifyMaxLayerChange(maxLayer)
}
func (t *MediaTrack) Restart() {
t.MediaTrackReceiver.Restart()
@@ -460,11 +634,10 @@ func (t *MediaTrack) Restart() {
}
func (t *MediaTrack) Close(isExpectedToResume bool) {
t.MediaTrackReceiver.SetClosing()
t.MediaTrackReceiver.SetClosing(isExpectedToResume)
if t.dynacastManager != nil {
t.dynacastManager.Close()
}
t.MediaTrackReceiver.ClearAllReceivers(isExpectedToResume)
t.MediaTrackReceiver.Close(isExpectedToResume)
}
@@ -486,3 +659,56 @@ func (t *MediaTrack) OnTrackSubscribed() {
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,
) {
t.lock.RLock()
onSubscribedMaxQualityChange := t.onSubscribedMaxQualityChange
t.lock.RUnlock()
if onSubscribedMaxQualityChange != nil && !t.IsMuted() {
_ = 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) {
t.lock.RLock()
onSubscribedAudioCodecChange := t.onSubscribedAudioCodecChange
t.lock.RUnlock()
if onSubscribedAudioCodecChange != nil {
_ = onSubscribedAudioCodecChange(t.ID(), codecs)
}
}
+97 -74
View File
@@ -19,7 +19,9 @@ import (
"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) {
@@ -47,128 +49,149 @@ func TestTrackInfo(t *testing.T) {
require.Equal(t, ti.Width, outInfo.Width)
require.Equal(t, ti.Height, outInfo.Height)
require.Equal(t, ti.Simulcast, outInfo.Simulcast)
// make it simulcasted
mt.SetSimulcast(true)
require.True(t, mt.ToProto().Simulcast)
}
func TestGetQualityForDimension(t *testing.T) {
t.Run("landscape source", func(t *testing.T) {
mt := NewMediaTrack(MediaTrackParams{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 200))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(200, 250))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(700, 480))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(500, 1000))
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{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 540,
Height: 960,
})
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(200, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 400))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(400, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(600, 900))
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{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
Codecs: []*livekit.SimulcastCodecInfo{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 960,
Height: 540,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
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(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
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{}, &livekit.TrackInfo{
mt := NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
Codecs: []*livekit.SimulcastCodecInfo{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 1080,
Height: 720,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
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(120, 120))
require.Equal(t, livekit.VideoQuality_LOW, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
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{}, &livekit.TrackInfo{
mt = NewMediaTrack(MediaTrackParams{
Logger: logger.GetLogger(),
}, &livekit.TrackInfo{
Type: livekit.TrackType_VIDEO,
Width: 1080,
Height: 720,
Layers: []*livekit.VideoLayer{
Codecs: []*livekit.SimulcastCodecInfo{
{
Quality: livekit.VideoQuality_LOW,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_MEDIUM,
Width: 480,
Height: 270,
},
{
Quality: livekit.VideoQuality_HIGH,
Width: 1080,
Height: 720,
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(120, 120))
require.Equal(t, livekit.VideoQuality_MEDIUM, mt.GetQualityForDimension(300, 300))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(800, 500))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1000, 700))
require.Equal(t, livekit.VideoQuality_HIGH, mt.GetQualityForDimension(1200, 800))
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))
})
}
+327 -94
View File
@@ -15,19 +15,19 @@
package rtc
import (
"context"
"errors"
"fmt"
"sort"
"slices"
"strings"
"sync"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v4"
"go.uber.org/atomic"
"golang.org/x/exp/slices"
"google.golang.org/protobuf/proto"
"github.com/livekit/mediatransportutil/pkg/codec"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
@@ -35,9 +35,8 @@ import (
"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/mime"
"github.com/livekit/livekit-server/pkg/sfu/rtpstats"
"github.com/livekit/livekit-server/pkg/telemetry"
sutils "github.com/livekit/livekit-server/pkg/utils"
)
const (
@@ -118,17 +117,18 @@ func (r *simulcastReceiver) IsRegressed() bool {
// -----------------------------------------------------
type MediaTrackReceiverParams struct {
MediaTrack types.MediaTrack
IsRelayed bool
ParticipantID livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
AudioConfig sfu.AudioConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
RegressionTargetCodec mime.MimeType
MediaTrack types.MediaTrack
IsRelayed bool
ParticipantID func() livekit.ParticipantID
ParticipantIdentity livekit.ParticipantIdentity
ParticipantVersion uint32
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
AudioConfig sfu.AudioConfig
TelemetryListener types.ParticipantTelemetryListener
Logger logger.Logger
RegressionTargetCodec mime.MimeType
PreferVideoSizeFromMedia bool
}
type MediaTrackReceiver struct {
@@ -161,21 +161,15 @@ func NewMediaTrackReceiver(params MediaTrackReceiverParams, ti *livekit.TrackInf
IsRelayed: params.IsRelayed,
ReceiverConfig: params.ReceiverConfig,
SubscriberConfig: params.SubscriberConfig,
Telemetry: params.Telemetry,
Logger: params.Logger,
})
t.MediaTrackSubscriptions.OnDownTrackCreated(t.onDownTrackCreated)
if ti.Muted {
t.SetMuted(true)
}
return t
}
func (t *MediaTrackReceiver) Restart() {
hq := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.TrackInfo())
for _, receiver := range t.loadReceivers() {
hq := buffer.GetSpatialLayerForVideoQuality(receiver.Mime(), livekit.VideoQuality_HIGH, t.TrackInfo())
receiver.SetMaxExpectedSpatialLayer(hq)
}
}
@@ -213,8 +207,8 @@ func (t *MediaTrackReceiver) SetupReceiver(receiver sfu.TrackReceiver, priority
receivers = append(receivers, &simulcastReceiver{TrackReceiver: receiver, priority: priority})
}
sort.Slice(receivers, func(i, j int) bool {
return receivers[i].Priority() < receivers[j].Priority()
slices.SortFunc(receivers, func(a, b *simulcastReceiver) int {
return sutils.Signum(a.Priority() - b.Priority())
})
if mid != "" {
@@ -308,12 +302,16 @@ func (t *MediaTrackReceiver) HandleReceiverCodecChange(r sfu.TrackReceiver, code
return
}
t.params.Logger.Infow("regressing codec", "from", codec.MimeType, "to", backupCodecReceiver.Codec().MimeType)
t.params.Logger.Infow(
"regressing codec",
"from", codec.MimeType,
"to", backupCodecReceiver.Codec().MimeType,
)
// remove old codec from potential codecs
for i, c := range t.potentialCodecs {
if strings.EqualFold(c.MimeType, codec.MimeType) {
slices.Delete(t.potentialCodecs, i, i+1)
t.potentialCodecs = slices.Delete(t.potentialCodecs, i, i+1)
break
}
}
@@ -351,8 +349,8 @@ func (t *MediaTrackReceiver) SetPotentialCodecs(codecs []webrtc.RTPCodecParamete
})
}
}
sort.Slice(receivers, func(i, j int) bool {
return receivers[i].Priority() < receivers[j].Priority()
slices.SortFunc(receivers, func(a, b *simulcastReceiver) int {
return sutils.Signum(a.Priority() - b.Priority())
})
t.receivers = receivers
t.lock.Unlock()
@@ -376,7 +374,7 @@ func (t *MediaTrackReceiver) ClearReceiver(mime mime.MimeType, isExpectedToResum
}
func (t *MediaTrackReceiver) ClearAllReceivers(isExpectedToResume bool) {
t.params.Logger.Debugw("clearing all receivers")
t.params.Logger.Debugw("clearing all receivers", "isExpectedToResume", isExpectedToResume)
t.lock.Lock()
receivers := t.receivers
t.receivers = nil
@@ -408,12 +406,15 @@ func (t *MediaTrackReceiver) IsOpen() bool {
return true
}
func (t *MediaTrackReceiver) SetClosing() {
func (t *MediaTrackReceiver) SetClosing(isExpectedToResume bool) {
t.lock.Lock()
defer t.lock.Unlock()
if t.state == mediaTrackReceiverStateOpen {
t.state = mediaTrackReceiverStateClosing
}
t.isExpectedToResume = isExpectedToResume
}
func (t *MediaTrackReceiver) TryClose() bool {
@@ -444,6 +445,8 @@ func (t *MediaTrackReceiver) TryClose() bool {
}
func (t *MediaTrackReceiver) Close(isExpectedToResume bool) {
t.ClearAllReceivers(isExpectedToResume)
t.lock.Lock()
if t.state == mediaTrackReceiverStateClosed {
t.lock.Unlock()
@@ -476,7 +479,7 @@ func (t *MediaTrackReceiver) Stream() string {
}
func (t *MediaTrackReceiver) PublisherID() livekit.ParticipantID {
return t.params.ParticipantID
return t.params.ParticipantID()
}
func (t *MediaTrackReceiver) PublisherIdentity() livekit.ParticipantIdentity {
@@ -487,19 +490,6 @@ func (t *MediaTrackReceiver) PublisherVersion() uint32 {
return t.params.ParticipantVersion
}
func (t *MediaTrackReceiver) IsSimulcast() bool {
return t.TrackInfo().Simulcast
}
func (t *MediaTrackReceiver) SetSimulcast(simulcast bool) {
t.lock.Lock()
defer t.lock.Unlock()
trackInfo := t.TrackInfoClone()
trackInfo.Simulcast = simulcast
t.trackInfo.Store(trackInfo)
}
func (t *MediaTrackReceiver) Name() string {
return t.TrackInfo().Name
}
@@ -513,21 +503,19 @@ func (t *MediaTrackReceiver) SetMuted(muted bool) {
trackInfo := t.TrackInfoClone()
trackInfo.Muted = muted
t.trackInfo.Store(trackInfo)
receivers := t.receivers
t.lock.Unlock()
for _, receiver := range receivers {
receiver.SetUpTrackPaused(muted)
}
t.MediaTrackSubscriptions.SetMuted(muted)
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) IsEncrypted() bool {
return t.TrackInfo().Encryption != livekit.Encryption_NONE
}
func (t *MediaTrackReceiver) HasPacketTrailer() bool {
return len(t.TrackInfo().GetPacketTrailerFeatures()) > 0
}
func (t *MediaTrackReceiver) AddOnClose(f func(isExpectedToResume bool)) {
if f == nil {
return
@@ -588,8 +576,10 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su
StreamId: streamId,
UpstreamCodecs: potentialCodecs,
Logger: tLogger,
DisableRed: t.TrackInfo().GetDisableRed() || !t.params.AudioConfig.ActiveREDEncoding,
DisableRed: !IsRedEnabled(t.TrackInfo()) || !t.params.AudioConfig.ActiveREDEncoding,
IsEncrypted: t.IsEncrypted(),
})
subID := sub.ID()
subTrack, err := t.MediaTrackSubscriptions.AddSubscriber(sub, wr)
// media track could have been closed while adding subscription
@@ -603,7 +593,12 @@ func (t *MediaTrackReceiver) AddSubscriber(sub types.LocalParticipant) (types.Su
t.lock.RUnlock()
if remove {
_ = t.MediaTrackSubscriptions.RemoveSubscriber(sub.ID(), isExpectedToResume)
t.params.Logger.Debugw(
"removing subscriber on a not-open track",
"subscriberID", subID,
"isExpectedToResume", isExpectedToResume,
)
_ = t.MediaTrackSubscriptions.RemoveSubscriber(subID, isExpectedToResume)
return nil, ErrNotOpen
}
@@ -617,7 +612,7 @@ func (t *MediaTrackReceiver) RemoveSubscriber(subscriberID livekit.ParticipantID
}
func (t *MediaTrackReceiver) removeAllSubscribersForMime(mime mime.MimeType, isExpectedToResume bool) {
t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime)
t.params.Logger.Debugw("removing all subscribers for mime", "mime", mime, "isExpectedToResume", isExpectedToResume)
for _, subscriberID := range t.MediaTrackSubscriptions.GetAllSubscribersForMime(mime) {
t.RemoveSubscriber(subscriberID, isExpectedToResume)
}
@@ -632,14 +627,7 @@ func (t *MediaTrackReceiver) RevokeDisallowedSubscribers(allowedSubscriberIdenti
continue
}
found := false
for _, allowedIdentity := range allowedSubscriberIdentities {
if subTrack.SubscriberIdentity() == allowedIdentity {
found = true
break
}
}
found := slices.Contains(allowedSubscriberIdentities, subTrack.SubscriberIdentity())
if !found {
t.params.Logger.Infow("revoking subscription",
"subscriber", subTrack.SubscriberIdentity(),
@@ -658,17 +646,42 @@ func (t *MediaTrackReceiver) updateTrackInfoOfReceivers() {
for _, r := range t.loadReceivers() {
r.UpdateTrackInfo(ti)
}
t.MediaTrackSubscriptions.SetMuted(ti.GetMuted())
}
func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ssrc uint32) {
func (t *MediaTrackReceiver) MaybeSetSimulcast() {
// only primary receiver (i.e. receiver at index 0) for legacy use case
primaryReceiver := t.PrimaryReceiver()
if primaryReceiver == nil {
return
}
if wr, ok := primaryReceiver.(*sfu.WebRTCReceiver); !ok || wr.NumUpTracks() < 2 {
return
}
t.lock.Lock()
trackInfo := t.TrackInfoClone()
layer := buffer.RidToSpatialLayer(rid, trackInfo)
if trackInfo.Simulcast {
t.lock.Unlock()
return
}
trackInfo.Simulcast = true
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) SetLayerSsrcsForRid(mimeType mime.MimeType, rid string, ssrc uint32, repairSSRC uint32) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
layer := buffer.GetSpatialLayerForRid(mimeType, rid, trackInfo)
if layer == buffer.InvalidLayerSpatial {
// non-simulcast case will not have `rid`
layer = 0
}
quality := buffer.SpatialLayerToVideoQuality(layer, trackInfo)
quality := buffer.GetVideoQualityForSpatialLayer(mimeType, layer, trackInfo)
// set video layer ssrc info
for i, ci := range trackInfo.Codecs {
if mime.NormalizeMimeType(ci.MimeType) != mimeType {
@@ -689,6 +702,20 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ss
}
if !ssrcFound && matchingLayer != nil {
matchingLayer.Ssrc = ssrc
if repairSSRC != 0 {
matchingLayer.RepairSsrc = repairSSRC
}
}
if ssrcFound && (matchingLayer.Ssrc != ssrc || matchingLayer.RepairSsrc != repairSSRC) {
t.params.Logger.Warnw(
"not overriding ssrc", nil,
"rid", rid,
"ssrc", ssrc,
"existingSSRC", matchingLayer.Ssrc,
"repairSSRC", repairSSRC,
"existingRepairSSRC", matchingLayer.RepairSsrc,
"trackInfo", logger.Proto(trackInfo),
)
}
// for client don't use simulcast codecs (old client version or single codec)
@@ -703,13 +730,99 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ss
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
func (t *MediaTrackReceiver) setLayerRtxInfo(ssrc uint32, repairSSRC uint32, rsid string) {
t.params.Logger.Debugw("rtx notification", "ssrc", ssrc, "repairSSRC", repairSSRC, "rsid", rsid)
if ssrc == 0 || repairSSRC == 0 || rsid == "" {
return
}
t.lock.Lock()
trackInfo := t.TrackInfoClone()
done:
for _, ci := range trackInfo.Codecs {
for _, l := range ci.Layers {
if l.Ssrc == ssrc {
if (l.RepairSsrc != 0 && l.RepairSsrc != repairSSRC) || (l.Rid != "" && l.Rid != rsid) {
t.params.Logger.Warnw(
"not overriding rtx info", nil,
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"existingRepairSSRC", l.RepairSsrc,
"rsid", rsid,
"existingRid", l.Rid,
"trackInfo", logger.Proto(trackInfo),
)
} else {
l.RepairSsrc = repairSSRC
t.params.Logger.Debugw(
"set rtx info",
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"rsid", rsid,
"trackInfo", logger.Proto(trackInfo),
)
}
break done
}
}
}
// backwards compatibility
for _, l := range trackInfo.Layers {
if l.Ssrc == ssrc {
if (l.RepairSsrc != 0 && l.RepairSsrc != repairSSRC) || (l.Rid != "" && l.Rid != rsid) {
t.params.Logger.Warnw(
"not overriding rtx info", nil,
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"existingRepairSSRC", l.RepairSsrc,
"rsid", rsid,
"existingRid", l.Rid,
"trackInfo", logger.Proto(trackInfo),
)
} else {
l.RepairSsrc = repairSSRC
t.params.Logger.Debugw(
"set rtx info",
"ssrc", ssrc,
"repairSSRC", repairSSRC,
"rsid", rsid,
"trackInfo", logger.Proto(trackInfo),
)
}
break
}
}
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
// change not propagated as it is internal
}
func (t *MediaTrackReceiver) UpdateCodecInfo(codecs []*livekit.SimulcastCodec) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
for _, c := range codecs {
for _, origin := range trackInfo.Codecs {
if mime.GetMimeTypeCodec(origin.MimeType) == mime.NormalizeMimeTypeCodec(c.Codec) {
origin.Cid = c.Cid
if len(c.Layers) != 0 {
clonedLayers := make([]*livekit.VideoLayer, 0, len(c.Layers))
for _, l := range c.Layers {
clonedLayers = append(clonedLayers, utils.CloneProto(l))
}
origin.Layers = clonedLayers
mimeType := mime.NormalizeMimeType(origin.MimeType)
for _, layer := range origin.Layers {
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(mimeType, layer.Quality, trackInfo)
layer.Rid = buffer.VideoQualityToRid(mimeType, layer.Quality, trackInfo, buffer.DefaultVideoLayersRid)
}
}
break
}
}
@@ -720,13 +833,49 @@ func (t *MediaTrackReceiver) UpdateCodecCid(codecs []*livekit.SimulcastCodec) {
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateCodecSdpCid(mimeType mime.MimeType, sdpCid string) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
for _, origin := range trackInfo.Codecs {
if mime.NormalizeMimeType(origin.MimeType) == mimeType {
if sdpCid != origin.Cid {
origin.SdpCid = sdpCid
}
}
}
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateCodecRids(mimeType mime.MimeType, rids buffer.VideoLayersRid) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
for _, origin := range trackInfo.Codecs {
originMimeType := mime.NormalizeMimeType(origin.MimeType)
if originMimeType != mimeType {
continue
}
for _, layer := range origin.Layers {
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(mimeType, layer.Quality, trackInfo)
layer.Rid = buffer.VideoQualityToRid(mimeType, layer.Quality, trackInfo, rids)
}
break
}
t.trackInfo.Store(trackInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
}
func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
updateMute := false
clonedInfo := utils.CloneProto(ti)
t.lock.Lock()
trackInfo := t.TrackInfo()
// patch Mid and SSRC of codecs/layers by keeping original if available
// patch Mid/Rid and Ssrc/RtxSsrc of codecs/layers by keeping original if available
for i, ci := range clonedInfo.Codecs {
for _, originCi := range trackInfo.Codecs {
if !mime.IsMimeTypeStringEqual(ci.MimeType, originCi.MimeType) {
@@ -743,6 +892,13 @@ func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
if originLayer.Ssrc != 0 {
layer.Ssrc = originLayer.Ssrc
}
if originLayer.Rid != "" {
layer.Rid = originLayer.Rid
}
if originLayer.RepairSsrc != 0 {
layer.RepairSsrc = originLayer.RepairSsrc
}
break
}
}
@@ -755,16 +911,9 @@ func (t *MediaTrackReceiver) UpdateTrackInfo(ti *livekit.TrackInfo) {
clonedInfo.Layers = ci.Layers
}
}
if trackInfo.Muted != clonedInfo.Muted {
updateMute = true
}
t.trackInfo.Store(clonedInfo)
t.lock.Unlock()
if updateMute {
t.SetMuted(clonedInfo.Muted)
}
t.updateTrackInfoOfReceivers()
}
@@ -776,7 +925,9 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
t.lock.Lock()
trackInfo := t.TrackInfo()
clonedInfo := utils.CloneProto(trackInfo)
clonedInfo.AudioFeatures = update.Features
clonedInfo.AudioFeatures = sutils.DedupeSlice(update.Features)
clonedInfo.Stereo = false
clonedInfo.DisableDtx = false
for _, feature := range update.Features {
@@ -787,6 +938,7 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
clonedInfo.DisableDtx = true
}
}
if proto.Equal(trackInfo, clonedInfo) {
t.lock.Unlock()
return
@@ -797,7 +949,7 @@ func (t *MediaTrackReceiver) UpdateAudioTrack(update *livekit.UpdateLocalAudioTr
t.updateTrackInfoOfReceivers()
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), clonedInfo)
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
t.params.Logger.Debugw("updated audio track", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
}
@@ -821,10 +973,56 @@ func (t *MediaTrackReceiver) UpdateVideoTrack(update *livekit.UpdateLocalVideoTr
t.updateTrackInfoOfReceivers()
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), clonedInfo)
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
t.params.Logger.Debugw("updated video track", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
}
func (t *MediaTrackReceiver) UpdateVideoSize(mimeType mime.MimeType, sizes []codec.VideoSize) {
var changed bool
t.lock.Lock()
trackInfo := t.TrackInfo()
clonedInfo := utils.CloneProto(trackInfo)
var maxWidth, maxHeight uint32
for _, size := range sizes {
if size.Width > maxWidth {
maxWidth = size.Width
maxHeight = size.Height
}
}
if clonedInfo.Width != maxWidth || clonedInfo.Height != maxHeight {
clonedInfo.Width = maxWidth
clonedInfo.Height = maxHeight
changed = true
}
for _, c := range clonedInfo.Codecs {
if mime.NormalizeMimeType(c.MimeType) == mimeType {
for i, l := range c.Layers {
if i < len(sizes) && (sizes[i].Width != 0 || sizes[i].Height != 0) &&
(l.Width != sizes[i].Width || l.Height != sizes[i].Height) {
l.Width = sizes[i].Width
l.Height = sizes[i].Height
changed = true
}
}
}
}
if !changed {
t.lock.Unlock()
return
}
t.trackInfo.Store(clonedInfo)
t.lock.Unlock()
t.updateTrackInfoOfReceivers()
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), clonedInfo)
t.params.Logger.Debugw("updated video sizes", "before", logger.Proto(trackInfo), "after", logger.Proto(clonedInfo))
}
func (t *MediaTrackReceiver) TrackInfo() *livekit.TrackInfo {
return t.trackInfo.Load()
}
@@ -833,16 +1031,17 @@ func (t *MediaTrackReceiver) TrackInfoClone() *livekit.TrackInfo {
return utils.CloneProto(t.TrackInfo())
}
func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
func (t *MediaTrackReceiver) NotifyMaxLayerChange(mimeType mime.MimeType, maxLayer int32) {
trackInfo := t.TrackInfo()
quality := buffer.SpatialLayerToVideoQuality(maxLayer, trackInfo)
quality := buffer.GetVideoQualityForSpatialLayer(mimeType, maxLayer, trackInfo)
ti := &livekit.TrackInfo{
Sid: trackInfo.Sid,
Type: trackInfo.Type,
Layers: []*livekit.VideoLayer{{Quality: quality}},
}
if quality != livekit.VideoQuality_OFF {
for _, layer := range trackInfo.Layers {
layers := buffer.GetVideoLayersForMimeType(mimeType, trackInfo)
for _, layer := range layers {
if layer.Quality == quality {
ti.Layers[0].Width = layer.Width
ti.Layers[0].Height = layer.Height
@@ -851,12 +1050,12 @@ func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
}
}
t.params.Telemetry.TrackPublishedUpdate(context.Background(), t.PublisherID(), ti)
t.params.TelemetryListener.OnTrackPublishedUpdate(t.PublisherID(), ti)
}
// GetQualityForDimension finds the closest quality to use for desired dimensions
// affords a 20% tolerance on dimension
func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) livekit.VideoQuality {
func (t *MediaTrackReceiver) GetQualityForDimension(mimeType mime.MimeType, width, height uint32) livekit.VideoQuality {
quality := livekit.VideoQuality_HIGH
if t.Kind() == livekit.TrackType_AUDIO {
return quality
@@ -864,7 +1063,12 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
trackInfo := t.TrackInfo()
if trackInfo.Height == 0 {
var mediaSizes []codec.VideoSize
if receiver := t.Receiver(mimeType); receiver != nil {
mediaSizes = receiver.VideoSizes()
}
if trackInfo.Height == 0 && len(mediaSizes) == 0 {
return quality
}
origSize := trackInfo.Height
@@ -875,19 +1079,38 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
requestedSize = width
}
if origSize == 0 {
for i := len(mediaSizes) - 1; i >= 0; i-- {
if mediaSizes[i].Height > 0 {
origSize = min(mediaSizes[i].Width, mediaSizes[i].Height)
break
}
}
}
// default sizes representing qualities low - high
layerSizes := []uint32{180, 360, origSize}
var providedSizes []uint32
for _, layer := range trackInfo.Layers {
for _, layer := range buffer.GetVideoLayersForMimeType(mimeType, trackInfo) {
providedSizes = append(providedSizes, layer.Height)
}
if len(providedSizes) == 0 || providedSizes[0] == 0 || t.params.PreferVideoSizeFromMedia {
if len(mediaSizes) > 0 {
providedSizes = providedSizes[:0]
for _, size := range mediaSizes {
providedSizes = append(providedSizes, size.Height)
}
} else {
t.params.Logger.Debugw("no video sizes provided by receiver, using track info sizes")
}
}
if len(providedSizes) > 0 {
layerSizes = providedSizes
// comparing height always
requestedSize = height
sort.Slice(layerSizes, func(i, j int) bool {
return layerSizes[i] < layerSizes[j]
})
slices.Sort(layerSizes)
}
// finds the highest layer with smallest dimensions that still satisfy client demands
@@ -905,7 +1128,7 @@ func (t *MediaTrackReceiver) GetQualityForDimension(width, height uint32) liveki
}
func (t *MediaTrackReceiver) GetAudioLevel() (float64, bool) {
receiver := t.PrimaryReceiver()
receiver := t.ActiveReceiver()
if receiver == nil {
return 0, false
}
@@ -923,8 +1146,8 @@ func (t *MediaTrackReceiver) onDownTrackCreated(downTrack *sfu.DownTrack) {
}
}
func (t *MediaTrackReceiver) DebugInfo() map[string]interface{} {
info := map[string]interface{}{
func (t *MediaTrackReceiver) DebugInfo() map[string]any {
info := map[string]any{
"ID": t.ID(),
"Kind": t.Kind().String(),
"PubMuted": t.IsMuted(),
@@ -950,6 +1173,16 @@ func (t *MediaTrackReceiver) PrimaryReceiver() sfu.TrackReceiver {
return receivers[0].TrackReceiver
}
func (t *MediaTrackReceiver) ActiveReceiver() sfu.TrackReceiver {
for _, r := range t.loadReceivers() {
if r.IsRegressed() {
return r.TrackReceiver
}
}
return t.PrimaryReceiver()
}
func (t *MediaTrackReceiver) Receiver(mime mime.MimeType) sfu.TrackReceiver {
for _, r := range t.loadReceivers() {
if r.Mime() == mime {
@@ -985,8 +1218,8 @@ func (t *MediaTrackReceiver) SetRTT(rtt uint32) {
}
}
func (t *MediaTrackReceiver) GetTemporalLayerForSpatialFps(spatial int32, fps uint32, mime mime.MimeType) int32 {
receiver := t.Receiver(mime)
func (t *MediaTrackReceiver) GetTemporalLayerForSpatialFps(mimeType mime.MimeType, spatial int32, fps uint32) int32 {
receiver := t.Receiver(mimeType)
if receiver == nil {
return buffer.DefaultMaxLayerTemporal
}
+36 -136
View File
@@ -16,21 +16,17 @@ package rtc
import (
"errors"
"slices"
"sync"
"github.com/pion/rtcp"
"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/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/mime"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
)
var (
@@ -47,6 +43,7 @@ type MediaTrackSubscriptions struct {
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 {
@@ -56,8 +53,6 @@ type MediaTrackSubscriptionsParams struct {
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Telemetry telemetry.TelemetryService
Logger logger.Logger
}
@@ -76,6 +71,10 @@ func (t *MediaTrackSubscriptions) OnSubscriberMaxQualityChange(f func(subscriber
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() {
@@ -104,86 +103,32 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
}
t.subscribedTracksMu.Unlock()
var rtcpFeedback []webrtc.RTCPFeedback
var maxTrack int
switch t.params.MediaTrack.Kind() {
case livekit.TrackType_AUDIO:
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Audio
maxTrack = t.params.ReceiverConfig.PacketBufferSizeAudio
case livekit.TrackType_VIDEO:
rtcpFeedback = t.params.SubscriberConfig.RTCPFeedback.Video
maxTrack = t.params.ReceiverConfig.PacketBufferSizeVideo
}
codecs := wr.Codecs()
for _, c := range codecs {
c.RTCPFeedback = rtcpFeedback
}
streamID := wr.StreamID()
if sub.SupportsSyncStreamID() && t.params.MediaTrack.Stream() != "" {
streamID = PackSyncStreamID(t.params.MediaTrack.PublisherID(), t.params.MediaTrack.Stream())
}
var trailer []byte
if t.params.MediaTrack.IsEncrypted() {
trailer = sub.GetTrailer()
}
downTrack, err := sfu.NewDownTrack(sfu.DowntrackParams{
Codecs: codecs,
Source: t.params.MediaTrack.Source(),
Receiver: wr,
BufferFactory: sub.GetBufferFactory(),
SubID: subscriberID,
StreamID: streamID,
MaxTrack: maxTrack,
PlayoutDelayLimit: sub.GetPlayoutDelayConfig(),
Pacer: sub.GetPacer(),
Trailer: trailer,
Logger: LoggerWithTrack(sub.GetLogger().WithComponent(sutils.ComponentSub), trackID, t.params.IsRelayed),
RTCPWriter: sub.WriteSubscriberRTCP,
DisableSenderReportPassThrough: sub.GetDisableSenderReportPassThrough(),
SupportsCodecChange: sub.SupportsCodecChange(),
subTrack, err := NewSubscribedTrack(SubscribedTrackParams{
ReceiverConfig: t.params.ReceiverConfig,
SubscriberConfig: t.params.SubscriberConfig,
Subscriber: sub,
MediaTrack: t.params.MediaTrack,
AdaptiveStream: sub.GetAdaptiveStream(),
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
}
if t.onDownTrackCreated != nil {
t.onDownTrackCreated(downTrack)
}
subTrack := NewSubscribedTrack(SubscribedTrackParams{
PublisherID: t.params.MediaTrack.PublisherID(),
PublisherIdentity: t.params.MediaTrack.PublisherIdentity(),
PublisherVersion: t.params.MediaTrack.PublisherVersion(),
Subscriber: sub,
MediaTrack: t.params.MediaTrack,
DownTrack: downTrack,
AdaptiveStream: sub.GetAdaptiveStream(),
})
if !sub.Hidden() {
subTrack.AddOnBind(func(err error) {
if err == nil {
t.params.MediaTrack.OnTrackSubscribed()
}
})
}
// Bind callback can happen from replaceTrack, so set it up early
var reusingTransceiver atomic.Bool
var dtState sfu.DownTrackState
downTrack.OnCodecNegotiated(func(codec webrtc.RTPCodecCapability) {
if !wr.DetermineReceiver(codec) {
if t.onSubscriberMaxQualityChange != nil {
go func() {
spatial := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto())
t.onSubscriberMaxQualityChange(downTrack.SubscriberID(), mime.NormalizeMimeType(codec.MimeType), spatial)
}()
}
}
})
downTrack := subTrack.DownTrack()
downTrack.OnBinding(func(err error) {
if err != nil {
go subTrack.Bound(err)
@@ -207,25 +152,6 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
subTrack.SetPublisherMuted(t.params.MediaTrack.IsMuted())
})
downTrack.OnStatsUpdate(func(_ *sfu.DownTrack, stat *livekit.AnalyticsStat) {
key := telemetry.StatsKeyForTrack(livekit.StreamType_DOWNSTREAM, subscriberID, trackID, t.params.MediaTrack.Source(), t.params.MediaTrack.Kind())
t.params.Telemetry.TrackStats(key, stat)
})
downTrack.OnMaxLayerChanged(func(dt *sfu.DownTrack, layer int32) {
if t.onSubscriberMaxQualityChange != nil {
t.onSubscriberMaxQualityChange(dt.SubscriberID(), dt.Mime(), layer)
}
})
downTrack.OnRttUpdate(func(_ *sfu.DownTrack, rtt uint32) {
go sub.UpdateMediaRTT(rtt)
})
downTrack.AddReceiverReportListener(func(dt *sfu.DownTrack, report *rtcp.ReceiverReport) {
sub.HandleReceiverReport(dt, report)
})
var transceiver *webrtc.RTPTransceiver
var sender *webrtc.RTPSender
@@ -277,15 +203,16 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
if transceiver == nil {
info := t.params.MediaTrack.ToProto()
addTrackParams := types.AddTrackParams{
Stereo: info.Stereo,
Red: !info.DisableRed,
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() {
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
@@ -320,6 +247,7 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
// 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 {
@@ -330,10 +258,6 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
downTrack.SetTransceiver(transceiver)
downTrack.OnCloseHandler(func(isExpectedToResume bool) {
t.downTrackClosed(sub, subTrack, isExpectedToResume)
})
t.subscribedTracksMu.Lock()
t.subscribedTracks[subscriberID] = subTrack
t.subscribedTracksMu.Unlock()
@@ -361,10 +285,10 @@ func (t *MediaTrackSubscriptions) closeSubscribedTrack(subTrack types.Subscribed
}
if isExpectedToResume {
dt.CloseWithFlush(false)
dt.CloseWithFlush(false, false)
} else {
// flushing blocks, avoid blocking when publisher removes all its subscribers
go dt.CloseWithFlush(true)
go dt.CloseWithFlush(true, true)
}
}
@@ -429,8 +353,8 @@ func (t *MediaTrackSubscriptions) getAllSubscribedTracksLocked() []types.Subscri
return subTracks
}
func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
subscribedTrackInfo := make([]map[string]interface{}, 0)
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())
@@ -439,27 +363,3 @@ func (t *MediaTrackSubscriptions) DebugInfo() []map[string]interface{} {
return subscribedTrackInfo
}
func (t *MediaTrackSubscriptions) downTrackClosed(
sub types.LocalParticipant,
subTrack types.SubscribedTrack,
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 {
dt := subTrack.DownTrack()
tr := dt.GetTransceiver()
if tr != nil {
sub.CacheDownTrack(subTrack.ID(), tr, dt.GetState())
}
}
go func() {
t.subscribedTracksMu.Lock()
delete(t.subscribedTracks, sub.ID())
t.subscribedTracksMu.Unlock()
subTrack.Close(isExpectedToResume)
}()
}
@@ -0,0 +1,59 @@
package rtc
import (
"time"
"github.com/livekit/protocol/livekit"
)
type MigrationDataCacheState int
const (
MigrationDataCacheStateWaiting MigrationDataCacheState = iota
MigrationDataCacheStateTimeout
MigrationDataCacheStateDone
)
type MigrationDataCache struct {
lastSeq uint32
pkts []*livekit.DataPacket
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, 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) 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)
if time.Now().After(c.expiredAt) {
c.state = MigrationDataCacheStateTimeout
}
return c.state
}
func (c *MigrationDataCache) Get() []*livekit.DataPacket {
return c.pkts
}
@@ -0,0 +1,38 @@
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)
require.Equal(t, MigrationDataCacheStateWaiting, state)
require.Empty(t, cache.Get())
pkt2 := &livekit.DataPacket{Sequence: 11}
state = cache.Add(pkt2)
require.Equal(t, MigrationDataCacheStateDone, state)
require.Empty(t, cache.Get())
pkt3 := &livekit.DataPacket{Sequence: 12}
state = cache.Add(pkt3)
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)
require.Equal(t, MigrationDataCacheStateTimeout, state)
require.Len(t, cache2.Get(), 1)
require.Equal(t, uint32(22), cache2.Get()[0].Sequence)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,169 @@
// 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/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
}
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,
}
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.params.TelemetryListener,
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)
}
func (p *ParticipantImpl) GetNextSubscribedDataTrackHandle() uint16 {
p.lock.Lock()
defer p.lock.Unlock()
p.nextSubscribedDataTrackHandle++
if p.nextSubscribedDataTrackHandle == 0 {
p.nextSubscribedDataTrackHandle++
}
return p.nextSubscribedDataTrackHandle
}
@@ -26,11 +26,14 @@ import (
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
"github.com/livekit/protocol/auth"
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/protocol/observability/roomobs"
lksdp "github.com/livekit/protocol/sdp"
"github.com/livekit/protocol/signalling"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
@@ -81,14 +84,14 @@ func TestTrackPublishing(t *testing.T) {
track.IDReturns("id")
published := false
updated := false
p.OnTrackUpdated(func(p types.LocalParticipant, track types.MediaTrack) {
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackUpdatedCalls(func(p types.Participant, track types.MediaTrack) {
updated = true
})
p.OnTrackPublished(func(p types.LocalParticipant, track types.MediaTrack) {
p.listener().(*typesfakes.FakeLocalParticipantListener).OnTrackPublishedCalls(func(p types.Participant, track types.MediaTrack) {
published = true
})
p.UpTrackManager.AddPublishedTrack(track)
p.handleTrackPublished(track)
p.handleTrackPublished(track, false)
require.True(t, published)
require.False(t, updated)
require.Len(t, p.UpTrackManager.publishedTracks, 1)
@@ -129,7 +132,8 @@ func TestTrackPublishing(t *testing.T) {
Type: livekit.TrackType_AUDIO,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
// error response on duplicate adds a message
require.Equal(t, 2, sink.WriteMessageCallCount())
})
t.Run("should queue adding of duplicate tracks if already published by client id in signalling", func(t *testing.T) {
@@ -137,7 +141,7 @@ func TestTrackPublishing(t *testing.T) {
sink := p.params.Sink.(*routingfakes.FakeMessageSink)
track := &typesfakes.FakeLocalMediaTrack{}
track.SignalCidReturns("cid")
track.HasSignalCidCalls(func(s string) bool { return s == "cid" })
track.ToProtoReturns(&livekit.TrackInfo{})
// directly add to publishedTracks without lock - for testing purpose only
p.UpTrackManager.publishedTracks["cid"] = track
@@ -147,7 +151,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `queued` `RequestResponse` should add a message
require.Equal(t, 1, sink.WriteMessageCallCount())
require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))
// add again - it should be added to the queue
@@ -156,7 +161,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `queued` `RequestResponse`s should have been sent for duplicate additions
require.Equal(t, 2, sink.WriteMessageCallCount())
require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))
// check SID is the same
@@ -178,7 +184,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `queued` `RequestResponse` should add a message
require.Equal(t, 1, sink.WriteMessageCallCount())
require.Equal(t, 1, len(p.pendingTracks["cid"].trackInfos))
// add again - it should be added to the queue
@@ -187,7 +194,8 @@ func TestTrackPublishing(t *testing.T) {
Name: "webcam",
Type: livekit.TrackType_VIDEO,
})
require.Equal(t, 0, sink.WriteMessageCallCount())
// `queued` `RequestResponse`s should have been sent for duplicate additions
require.Equal(t, 2, sink.WriteMessageCallCount())
require.Equal(t, 2, len(p.pendingTracks["cid"].trackInfos))
// check SID is the same
@@ -217,7 +225,8 @@ func TestTrackPublishing(t *testing.T) {
Type: livekit.TrackType_AUDIO,
Source: livekit.TrackSource_MICROPHONE,
})
require.Equal(t, 1, sink.WriteMessageCallCount())
// an error response for disallowed source should send a `RequestResponse`.
require.Equal(t, 2, sink.WriteMessageCallCount())
})
}
@@ -225,7 +234,7 @@ func TestOutOfOrderUpdates(t *testing.T) {
p := newParticipantForTest("test")
p.updateState(livekit.ParticipantInfo_JOINED)
p.SetMetadata("initial metadata")
sink := p.getResponseSink().(*routingfakes.FakeMessageSink)
sink := p.GetResponseSink().(*routingfakes.FakeMessageSink)
pi1 := p.ToProto()
p.SetMetadata("second update")
pi2 := p.ToProto()
@@ -255,7 +264,7 @@ func TestDisconnectTiming(t *testing.T) {
}()
track := &typesfakes.FakeMediaTrack{}
p.UpTrackManager.AddPublishedTrack(track)
p.handleTrackPublished(track)
p.handleTrackPublished(track, false)
// close channel and then try to Negotiate
msg.Close()
@@ -275,7 +284,10 @@ func TestMuteSetting(t *testing.T) {
ti := &livekit.TrackInfo{Sid: "testTrack"}
p.pendingTracks["cid"] = &pendingTrackInfo{trackInfos: []*livekit.TrackInfo{ti}}
p.SetTrackMuted(livekit.TrackID(ti.Sid), true, false)
p.SetTrackMuted(&livekit.MuteTrackRequest{
Sid: ti.Sid,
Muted: true,
}, false)
require.True(t, p.pendingTracks["cid"].trackInfos[0].Muted)
})
@@ -287,7 +299,7 @@ func TestMuteSetting(t *testing.T) {
Muted: true,
})
_, ti, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
_, ti, _, _, _ := p.getPendingTrack("cid", livekit.TrackType_AUDIO, false)
require.NotNil(t, ti)
require.True(t, ti.Muted)
})
@@ -362,28 +374,36 @@ func TestDisableCodecs(t *testing.T) {
}
}
require.True(t, found264)
offerId := uint32(42)
// negotiated codec should not contain h264
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
var answer webrtc.SessionDescription
var answerId uint32
var answerReceived atomic.Bool
var answerIdReceived atomic.Uint32
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
answerReceived.Store(true)
answerIdReceived.Store(answerId)
}
}
return nil
})
participant.HandleOffer(sdp)
participant.HandleOffer(&livekit.SessionDescription{
Type: webrtc.SDPTypeOffer.String(),
Sdp: sdp.SDP,
Id: offerId,
})
testutils.WithTimeout(t, func() string {
if answerReceived.Load() {
if answerReceived.Load() && answerIdReceived.Load() == offerId {
return ""
} else {
return "answer not received"
return "answer not received OR answer id mismatch"
}
})
require.NoError(t, pc.SetRemoteDescription(answer), answer.SDP, sdp.SDP)
@@ -415,7 +435,7 @@ func TestDisablePublishCodec(t *testing.T) {
}
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
var publishReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
@@ -463,89 +483,138 @@ func TestDisablePublishCodec(t *testing.T) {
require.Eventually(t, func() bool { return publishReceived.Load() }, 5*time.Second, 10*time.Millisecond)
}
func TestPreferVideoCodecForPublisher(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
for i := 0; i < 2; i++ {
// publish h264 track without client preferred codec
trackCid := fmt.Sprintf("preferh264video%d", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
Name: "video",
Width: 1280,
Height: 720,
Source: livekit.TrackSource_CAMERA,
SimulcastCodecs: []*livekit.SimulcastCodec{
{
Codec: "h264",
Cid: trackCid,
},
func TestPreferMediaCodecForPublisher(t *testing.T) {
testCases := []struct {
name string
mediaKind string
trackBaseCid string
preferredCodec string
addTrack *livekit.AddTrackRequest
mimeTypeStringChecker func(string) bool
mimeTypeCodecStringChecker func(string) bool
transceiverMimeType mime.MimeType
}{
{
name: "video",
mediaKind: "video",
trackBaseCid: "preferH264Video",
preferredCodec: "h264",
addTrack: &livekit.AddTrackRequest{
Type: livekit.TrackType_VIDEO,
Name: "video",
Width: 1280,
Height: 720,
Source: livekit.TrackSource_CAMERA,
},
})
mimeTypeStringChecker: mime.IsMimeTypeStringH264,
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringH264,
transceiverMimeType: mime.MimeTypeVP8,
},
{
name: "audio",
mediaKind: "audio",
trackBaseCid: "preferPCMAAudio",
preferredCodec: "pcma",
addTrack: &livekit.AddTrackRequest{
Type: livekit.TrackType_AUDIO,
Name: "audio",
Source: livekit.TrackSource_MICROPHONE,
},
mimeTypeStringChecker: mime.IsMimeTypeStringPCMA,
mimeTypeCodecStringChecker: mime.IsMimeTypeCodecStringPCMA,
transceiverMimeType: mime.MimeTypeOpus,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
participant := newParticipantForTestWithOpts("123", &participantOpts{
publisher: true,
})
participant.SetMigrateState(types.MigrateStateComplete)
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Receiver().GetParameters().Codecs
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
if i > 0 {
// the negotiated codecs order could be updated by first negotiation, reorder to make h264 not preferred
for mime.IsMimeTypeStringH264(codecs[0].MimeType) {
codecs = append(codecs[1:], codecs[0])
}
}
// h264 should not be preferred
require.False(t, mime.IsMimeTypeStringH264(codecs[0].MimeType), "codecs", codecs)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
require.NoError(t, pc.SetLocalDescription(sdp))
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
var answer webrtc.SessionDescription
var answerReceived atomic.Bool
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
for i := 0; i < 2; i++ {
// publish preferred track without client using setCodecPreferences()
trackCid := fmt.Sprintf("%s-%d", tc.trackBaseCid, i)
req := utils.CloneProto(tc.addTrack)
req.SimulcastCodecs = []*livekit.SimulcastCodec{
{
Codec: tc.preferredCodec,
Cid: trackCid,
},
}
}
return nil
})
participant.HandleOffer(sdp)
participant.AddTrack(req)
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: tc.transceiverMimeType.String()}, trackCid, trackCid)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
require.NoError(t, err)
codecs := transceiver.Receiver().GetParameters().Codecs
var h264Preferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var videoSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "video" {
if videoSectionIndex == i {
codecs, err := codecsFromMediaDescription(m)
require.NoError(t, err)
if mime.IsMimeTypeCodecStringH264(codecs[0].Name) {
h264Preferred = true
break
if i > 0 {
// the negotiated codecs order could be updated by first negotiation,
// reorder to make tested preferred codec not preferred
for tc.mimeTypeStringChecker(codecs[0].MimeType) {
codecs = append(codecs[1:], codecs[0])
}
}
videoSectionIndex++
}
}
// preferred codec should not be preferred in `offer`
require.False(t, tc.mimeTypeStringChecker(codecs[0].MimeType), "codecs", codecs)
require.Truef(t, h264Preferred, "h264 should be preferred for video section %d, answer sdp: \n%s", i, answer.SDP)
sdp, err := pc.CreateOffer(nil)
require.NoError(t, err)
require.NoError(t, pc.SetLocalDescription(sdp))
offerId := uint32(23)
sink := &routingfakes.FakeMessageSink{}
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
var answer webrtc.SessionDescription
var answerId uint32
var answerReceived atomic.Bool
var answerIdReceived atomic.Uint32
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
answerIdReceived.Store(answerId)
}
}
return nil
})
participant.HandleOffer(&livekit.SessionDescription{
Type: webrtc.SDPTypeOffer.String(),
Sdp: sdp.SDP,
Id: offerId,
})
require.Eventually(t, func() bool { return answerReceived.Load() && answerIdReceived.Load() == offerId }, 5*time.Second, 10*time.Millisecond)
var havePreferred bool
parsed, err := answer.Unmarshal()
require.NoError(t, err)
var mediaSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == tc.mediaKind {
if mediaSectionIndex == i {
codecs, err := lksdp.CodecsFromMediaDescription(m)
require.NoError(t, err)
if tc.mimeTypeCodecStringChecker(codecs[0].Name) {
havePreferred = true
break
}
}
mediaSectionIndex++
}
}
require.Truef(t, havePreferred, "%s should be preferred for %s section %d, answer sdp: \n%s", tc.preferredCodec, tc.mediaKind, i, answer.SDP)
}
})
}
}
@@ -556,33 +625,59 @@ func TestPreferAudioCodecForRed(t *testing.T) {
participant.SetMigrateState(types.MigrateStateComplete)
me := webrtc.MediaEngine{}
me.RegisterDefaultCodecs()
require.NoError(t, me.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: RedCodecCapability,
PayloadType: 63,
}, webrtc.RTPCodecTypeAudio))
opusCodecParameters := protoCodecs.OpusCodecParameters
opusCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
require.NoError(t, me.RegisterCodec(opusCodecParameters, webrtc.RTPCodecTypeAudio))
redCodecParameters := protoCodecs.RedCodecParameters
redCodecParameters.RTPCodecCapability.RTCPFeedback = []webrtc.RTCPFeedback{{Type: webrtc.TypeRTCPFBNACK}}
require.NoError(t, me.RegisterCodec(redCodecParameters, webrtc.RTPCodecTypeAudio))
api := webrtc.NewAPI(webrtc.WithMediaEngine(&me))
pc, err := api.NewPeerConnection(webrtc.Configuration{})
require.NoError(t, err)
defer pc.Close()
for i, disableRed := range []bool{false, true} {
for idx, disableRed := range []bool{false, true, false, true} {
t.Run(fmt.Sprintf("disableRed=%v", disableRed), func(t *testing.T) {
trackCid := fmt.Sprintf("audiotrack%d", i)
participant.AddTrack(&livekit.AddTrackRequest{
Type: livekit.TrackType_AUDIO,
DisableRed: disableRed,
Cid: trackCid,
})
track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "audio/opus"}, trackCid, trackCid)
trackCid := fmt.Sprintf("audiotrack%d", idx)
req := &livekit.AddTrackRequest{
Type: livekit.TrackType_AUDIO,
Cid: trackCid,
}
if idx < 2 {
req.DisableRed = disableRed
} else {
codec := "red"
if disableRed {
codec = "opus"
}
req.SimulcastCodecs = []*livekit.SimulcastCodec{
{
Codec: codec,
Cid: trackCid,
},
}
}
participant.AddTrack(req)
track, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{MimeType: "audio/opus"},
trackCid,
trackCid,
)
require.NoError(t, err)
transceiver, err := pc.AddTransceiverFromTrack(track, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv})
transceiver, err := pc.AddTransceiverFromTrack(
track,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv},
)
require.NoError(t, err)
codecs := transceiver.Sender().GetParameters().Codecs
for i, c := range codecs {
if c.MimeType == "audio/opus" && i != 0 {
codecs[0], codecs[i] = codecs[i], codecs[0]
if c.MimeType == "audio/opus" {
if i != 0 {
codecs[0], codecs[i] = codecs[i], codecs[0]
}
break
}
}
@@ -592,24 +687,38 @@ func TestPreferAudioCodecForRed(t *testing.T) {
pc.SetLocalDescription(sdp)
// opus should be preferred
require.Equal(t, codecs[0].MimeType, "audio/opus", sdp)
offerId := uint32(0xffffff)
sink := &routingfakes.FakeMessageSink{}
participant.SetResponseSink(sink)
participant.SwapResponseSink(sink, types.SignallingCloseReasonUnknown)
var answer webrtc.SessionDescription
var answerId uint32
var answerReceived atomic.Bool
var answerIdReceived atomic.Uint32
sink.WriteMessageCalls(func(msg proto.Message) error {
if res, ok := msg.(*livekit.SignalResponse); ok {
if res.GetAnswer() != nil {
answer = FromProtoSessionDescription(res.GetAnswer())
answer, answerId, _ = signalling.FromProtoSessionDescription(res.GetAnswer())
pc.SetRemoteDescription(answer)
answerReceived.Store(true)
answerIdReceived.Store(answerId)
}
}
return nil
})
participant.HandleOffer(sdp)
require.Eventually(t, func() bool { return answerReceived.Load() }, 5*time.Second, 10*time.Millisecond)
participant.HandleOffer(&livekit.SessionDescription{
Type: webrtc.SDPTypeOffer.String(),
Sdp: sdp.SDP,
Id: offerId,
})
require.Eventually(
t,
func() bool {
return answerReceived.Load() && answerIdReceived.Load() == offerId
},
5*time.Second,
10*time.Millisecond,
)
var redPreferred bool
parsed, err := answer.Unmarshal()
@@ -617,8 +726,8 @@ func TestPreferAudioCodecForRed(t *testing.T) {
var audioSectionIndex int
for _, m := range parsed.MediaDescriptions {
if m.MediaName.Media == "audio" {
if audioSectionIndex == i {
codecs, err := codecsFromMediaDescription(m)
if audioSectionIndex == idx {
codecs, err := lksdp.CodecsFromMediaDescription(m)
require.NoError(t, err)
// nack is always enabled. if red is preferred, server will not generate nack request
var nackEnabled bool
@@ -702,8 +811,11 @@ func newParticipantForTestWithOpts(identity livekit.ParticipantIdentity, opts *p
ClientConf: opts.clientConf,
ClientInfo: ClientInfo{ClientInfo: opts.clientInfo},
Logger: LoggerWithParticipant(logger.GetLogger(), identity, sid, false),
Telemetry: &telemetryfakes.FakeTelemetryService{},
Reporter: roomobs.NewNoopParticipantSessionReporter(),
TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{},
VersionGenerator: utils.NewDefaultTimedVersionGenerator(),
ParticipantListener: &typesfakes.FakeLocalParticipantListener{},
ParticipantHelper: &typesfakes.FakeLocalParticipantHelper{},
})
p.isPublisher.Store(opts.publisher)
p.updateState(livekit.ParticipantInfo_ACTIVE)
+268 -134
View File
@@ -16,44 +16,279 @@ package rtc
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v4"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"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) setCodecPreferencesForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
offer = p.setCodecPreferencesOpusRedForPublisher(offer)
offer = p.setCodecPreferencesVideoForPublisher(offer)
return offer
}
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
}
func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
parsed, unmatchAudios, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "audio")
if err != nil || len(unmatchAudios) == 0 {
return offer
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()
_, info, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
// if RED is disabled for this track, don't prefer RED codec in offer
disableRed := info != nil && info.DisableRed
_, ti, _, _, _ := p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
if ti == nil {
continue
}
codecs, err := codecsFromMediaDescription(unmatchAudio)
codecs, err := lksdp.CodecsFromMediaDescription(unmatchAudio)
if err != nil {
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchAudio)
p.pubLogger.Errorw(
"extract codecs from media section failed", err,
"media", unmatchAudio,
"parsedOffer", parsedOffer,
)
continue
}
@@ -68,122 +303,17 @@ func (p *ParticipantImpl) setCodecPreferencesOpusRedForPublisher(offer webrtc.Se
continue
}
var preferredCodecs, leftCodecs []string
preferRED := IsRedEnabled(ti)
// if RED is enabled for this track, prefer RED codec in offer
for _, codec := range codecs {
// codec contain opus/red
if !disableRed && mime.IsMimeTypeCodecStringRED(codec.Name) && strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(codec.PayloadType), 10))
}
}
// ensure nack enabled for audio in publisher offer
var nackFound bool
for _, attr := range unmatchAudio.Attributes {
if attr.Key == "rtcp-fb" && strings.Contains(attr.Value, fmt.Sprintf("%d nack", opusPayload)) {
nackFound = true
if preferRED &&
mime.IsMimeTypeCodecStringRED(codec.Name) &&
strings.Contains(codec.Fmtp, strconv.FormatInt(int64(opusPayload), 10)) {
configureReceiverCodecs(transceiver, "audio/red", true)
break
}
}
if !nackFound {
unmatchAudio.Attributes = append(unmatchAudio.Attributes, sdp.Attribute{
Key: "rtcp-fb",
Value: fmt.Sprintf("%d nack", opusPayload),
})
}
// no opus/red found
if len(preferredCodecs) == 0 {
continue
}
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats[:0], preferredCodecs...)
unmatchAudio.MediaName.Formats = append(unmatchAudio.MediaName.Formats, leftCodecs...)
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Errorw("failed to marshal offer", err)
return offer
}
return webrtc.SessionDescription{
Type: offer.Type,
SDP: string(bytes),
}
}
func (p *ParticipantImpl) setCodecPreferencesVideoForPublisher(offer webrtc.SessionDescription) webrtc.SessionDescription {
parsed, unmatchVideos, err := p.TransportManager.GetUnmatchMediaForOffer(offer, "video")
if err != nil || len(unmatchVideos) == 0 {
return offer
}
// unmatched video is pending for publish, set codec preference
for _, unmatchVideo := range unmatchVideos {
streamID, ok := lksdp.ExtractStreamID(unmatchVideo)
if !ok {
continue
}
var info *livekit.TrackInfo
p.pendingTracksLock.RLock()
mt := p.getPublishedTrackBySdpCid(streamID)
if mt != nil {
info = mt.ToProto()
} else {
_, info, _, _ = p.getPendingTrack(streamID, livekit.TrackType_VIDEO, false)
}
if info == nil {
p.pendingTracksLock.RUnlock()
continue
}
var mimeType string
for _, c := range info.Codecs {
if c.Cid == streamID {
mimeType = c.MimeType
break
}
}
if mimeType == "" && len(info.Codecs) > 0 {
mimeType = info.Codecs[0].MimeType
}
p.pendingTracksLock.RUnlock()
if mimeType != "" {
codecs, err := codecsFromMediaDescription(unmatchVideo)
if err != nil {
p.pubLogger.Errorw("extract codecs from media section failed", err, "media", unmatchVideo)
continue
}
var preferredCodecs, leftCodecs []string
for _, c := range codecs {
if mime.GetMimeTypeCodec(mimeType) == mime.NormalizeMimeTypeCodec(c.Name) {
preferredCodecs = append(preferredCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
} else {
leftCodecs = append(leftCodecs, strconv.FormatInt(int64(c.PayloadType), 10))
}
}
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats[:0], preferredCodecs...)
// if the client don't comply with codec order in SDP answer, only keep preferred codecs to force client to use it
if p.params.ClientInfo.ComplyWithCodecOrderInSDPAnswer() {
unmatchVideo.MediaName.Formats = append(unmatchVideo.MediaName.Formats, leftCodecs...)
}
}
}
bytes, err := parsed.Marshal()
if err != nil {
p.pubLogger.Errorw("failed to marshal offer", err)
return offer
}
return webrtc.SessionDescription{
Type: offer.Type,
SDP: string(bytes),
}
}
@@ -195,12 +325,12 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
return answer
}
parsed, err := answer.Unmarshal()
parsedAnswer, err := answer.Unmarshal()
if err != nil {
return answer
}
for _, m := range parsed.MediaDescriptions {
for _, m := range parsedAnswer.MediaDescriptions {
switch m.MediaName.Media {
case "audio":
_, ok := m.Attribute(sdp.AttrKeyInactive)
@@ -227,7 +357,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
track, _ := p.getPublishedTrackBySdpCid(streamID).(*MediaTrack)
if track == nil {
p.pendingTracksLock.RLock()
_, ti, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
_, ti, _, _, _ = p.getPendingTrack(streamID, livekit.TrackType_AUDIO, false)
p.pendingTracksLock.RUnlock()
} else {
ti = track.ToProto()
@@ -236,12 +366,12 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
}
}
if ti == nil || (ti.DisableDtx && !ti.Stereo) {
if ti == nil {
// no need to configure
continue
}
opusPT, err := parsed.GetPayloadTypeForCodec(sdp.Codec{Name: mime.MimeTypeCodecOpus.String()})
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
@@ -249,11 +379,15 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
for i, attr := range m.Attributes {
if strings.HasPrefix(attr.String(), fmt.Sprintf("fmtp:%d", opusPT)) {
if !ti.DisableDtx {
if !slices.Contains(ti.AudioFeatures, livekit.AudioTrackFeature_TF_NO_DTX) {
attr.Value += ";usedtx=1"
} else {
attr.Value = strings.ReplaceAll(attr.Value, ";usedtx=1", "")
}
if ti.Stereo {
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
}
@@ -264,7 +398,7 @@ func (p *ParticipantImpl) configurePublisherAnswer(answer webrtc.SessionDescript
}
}
bytes, err := parsed.Marshal()
bytes, err := parsedAnswer.Marshal()
if err != nil {
p.pubLogger.Infow("failed to marshal answer", "error", err)
return answer
+124 -155
View File
@@ -15,30 +15,28 @@
package rtc
import (
"fmt"
"time"
"github.com/pion/webrtc/v4"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
protosignalling "github.com/livekit/protocol/signalling"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
func (p *ParticipantImpl) getResponseSink() routing.MessageSink {
p.resSinkMu.Lock()
defer p.resSinkMu.Unlock()
return p.resSink
func (p *ParticipantImpl) SwapResponseSink(sink routing.MessageSink, reason types.SignallingCloseReason) {
p.signaller.SwapResponseSink(sink, reason)
}
func (p *ParticipantImpl) SetResponseSink(sink routing.MessageSink) {
p.resSinkMu.Lock()
defer p.resSinkMu.Unlock()
p.resSink = sink
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 {
@@ -55,11 +53,7 @@ func (p *ParticipantImpl) SendJoinResponse(joinResponse *livekit.JoinResponse) e
p.updateLock.Unlock()
// send Join response
err := p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Join{
Join: joinResponse,
},
})
err := p.signaller.WriteMessage(p.signalling.SignalJoinResponse(joinResponse))
if err != nil {
return err
}
@@ -111,7 +105,7 @@ func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.
isValid = false
}
}
if pi.Permission != nil && pi.Permission.Hidden && pi.Sid != string(p.params.SID) {
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
}
@@ -127,17 +121,7 @@ func (p *ParticipantImpl) SendParticipantUpdate(participantsToUpdate []*livekit.
}
p.updateLock.Unlock()
if len(validUpdates) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: validUpdates,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(validUpdates))
}
// SendSpeakerUpdate notifies participant changes to speakers. only send members that have changed since last update
@@ -158,47 +142,23 @@ func (p *ParticipantImpl) SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, for
}
}
if len(scopedSpeakers) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_SpeakersChanged{
SpeakersChanged: &livekit.SpeakersChanged{
Speakers: scopedSpeakers,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalSpeakerUpdate(scopedSpeakers))
}
func (p *ParticipantImpl) SendRoomUpdate(room *livekit.Room) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RoomUpdate{
RoomUpdate: &livekit.RoomUpdate{
Room: room,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalRoomUpdate(room))
}
func (p *ParticipantImpl) SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_ConnectionQuality{
ConnectionQuality: update,
},
})
return p.signaller.WriteMessage(p.signalling.SignalConnectionQualityUpdate(update))
}
func (p *ParticipantImpl) SendRefreshToken(token string) error {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RefreshToken{
RefreshToken: token,
},
})
return p.signaller.WriteMessage(p.signalling.SignalRefreshToken(token))
}
func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestResponse) error {
if requestResponse.RequestId == 0 || !p.params.ClientInfo.SupportErrorResponse() {
func (p *ParticipantImpl) sendRequestResponse(requestResponse *livekit.RequestResponse) error {
if !p.params.ClientInfo.SupportsRequestResponse() {
return nil
}
@@ -206,11 +166,11 @@ func (p *ParticipantImpl) SendRequestResponse(requestResponse *livekit.RequestRe
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_RequestResponse{
RequestResponse: requestResponse,
},
})
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 {
@@ -219,15 +179,11 @@ func (p *ParticipantImpl) HandleReconnectAndSendResponse(reconnectReason livekit
if !p.params.ClientInfo.CanHandleReconnectResponse() {
return nil
}
if err := p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Reconnect{
Reconnect: reconnectResponse,
},
}); err != nil {
if err := p.signaller.WriteMessage(p.signalling.SignalReconnectResponse(reconnectResponse)); err != nil {
return err
}
if p.params.ProtocolVersion.SupportHandlesDisconnectedUpdate() {
if p.params.ProtocolVersion.SupportsDisconnectedUpdate() {
return p.sendDisconnectUpdatesForReconnect()
}
@@ -255,17 +211,7 @@ func (p *ParticipantImpl) sendDisconnectUpdatesForReconnect() error {
}
p.updateLock.Unlock()
if len(disconnectedParticipants) == 0 {
return nil
}
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Update{
Update: &livekit.ParticipantUpdate{
Participants: disconnectedParticipants,
},
},
})
return p.signaller.WriteMessage(p.signalling.SignalParticipantUpdate(disconnectedParticipants))
}
func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livekit.SignalTarget) error {
@@ -274,90 +220,43 @@ func (p *ParticipantImpl) sendICECandidate(ic *webrtc.ICECandidate, target livek
return nil
}
trickle := ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
trickle := protosignalling.ToProtoTrickle(prevIC.ToJSON(), target, ic == nil)
p.params.Logger.Debugw("sending ICE candidate", "transport", target, "trickle", logger.Proto(trickle))
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Trickle{
Trickle: trickle,
},
})
return p.signaller.WriteMessage(p.signalling.SignalICECandidate(trickle))
}
func (p *ParticipantImpl) sendTrackMuted(trackID livekit.TrackID, muted bool) {
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Mute{
Mute: &livekit.MuteTrackRequest{
Sid: string(trackID),
Muted: muted,
},
},
})
_ = 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.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackUnpublished{
TrackUnpublished: &livekit.TrackUnpublishedResponse{
TrackSid: string(trackID),
},
},
})
_ = p.signaller.WriteMessage(p.signalling.SignalTrackUnpublished(&livekit.TrackUnpublishedResponse{
TrackSid: string(trackID),
}))
}
func (p *ParticipantImpl) sendTrackHasBeenSubscribed(trackID livekit.TrackID) {
if !p.params.ClientInfo.SupportTrackSubscribedEvent() {
if !p.params.ClientInfo.SupportsTrackSubscribedEvent() {
return
}
_ = p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_TrackSubscribed{
TrackSubscribed: &livekit.TrackSubscribed{
TrackSid: string(trackID),
},
},
})
_ = p.signaller.WriteMessage(p.signalling.SignalTrackSubscribed(&livekit.TrackSubscribed{
TrackSid: string(trackID),
}))
p.params.Logger.Debugw("track has been subscribed", "trackID", trackID)
}
func (p *ParticipantImpl) writeMessage(msg *livekit.SignalResponse) error {
if p.IsDisconnected() || (!p.IsReady() && msg.GetJoin() == nil) {
return nil
}
sink := p.getResponseSink()
if sink == nil {
p.params.Logger.Debugw("could not send message to participant", "messageType", fmt.Sprintf("%T", msg.Message))
return nil
}
err := sink.WriteMessage(msg)
if utils.ErrorIsOneOf(err, psrpc.Canceled, routing.ErrChannelClosed) {
p.params.Logger.Debugw(
"could not send message to participant",
"error", err,
"messageType", fmt.Sprintf("%T", msg.Message),
)
return nil
} else if err != nil {
p.params.Logger.Warnw(
"could not send message to participant", err,
"messageType", fmt.Sprintf("%T", msg.Message),
)
return err
}
return nil
}
// closes signal connection to notify client to resume/reconnect
func (p *ParticipantImpl) CloseSignalConnection(reason types.SignallingCloseReason) {
sink := p.getResponseSink()
if sink != nil {
p.params.Logger.Debugw("closing signal connection", "reason", reason, "connID", sink.ConnectionID())
sink.Close()
p.SetResponseSink(nil)
}
}
func (p *ParticipantImpl) sendLeaveRequest(
reason types.ParticipantCloseReason,
isExpectedToResume bool,
@@ -377,9 +276,9 @@ func (p *ParticipantImpl) sendLeaveRequest(
default:
leave.Action = livekit.LeaveRequest_DISCONNECT
}
if leave.Action != livekit.LeaveRequest_DISCONNECT && p.params.GetRegionSettings != nil {
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.params.GetRegionSettings(p.params.ClientInfo.Address)
leave.Regions = p.helper().GetRegionSettings(p.params.ClientInfo.Address)
}
} else {
if !sendOnlyIfSupportingLeaveRequestWithAction {
@@ -390,12 +289,82 @@ func (p *ParticipantImpl) sendLeaveRequest(
}
}
if leave != nil {
return p.writeMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
})
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,
}))
}
File diff suppressed because it is too large Load Diff
+82 -57
View File
@@ -22,6 +22,7 @@ import (
"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"
@@ -89,7 +90,7 @@ func TestJoinedState(t *testing.T) {
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)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
_ = rm.Join(pNew, nil, nil, iceServersForRoom)
@@ -104,14 +105,14 @@ func TestRoomJoin(t *testing.T) {
t.Run("subscribe to existing channels upon join", func(t *testing.T) {
numExisting := 3
rm := newRoomWithParticipants(t, testRoomOpts{num: numExisting})
p := NewMockParticipant("new", types.CurrentProtocol, false, false)
lpl := rm.LocalParticipantListener()
p := NewMockParticipant("new", types.CurrentProtocol, false, false, lpl)
err := rm.Join(p, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
stateChangeCB := p.OnStateChangeArgsForCall(0)
require.NotNil(t, stateChangeCB)
stateChangeCB(p, livekit.ParticipantInfo_ACTIVE)
p.StateReturns(livekit.ParticipantInfo_ACTIVE)
lpl.OnStateChange(p)
// it should become a subscriber when connectivity changes
numTracks := 0
@@ -128,7 +129,7 @@ func TestRoomJoin(t *testing.T) {
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.LocalParticipant) {
rm.OnParticipantChanged(func(participant types.Participant) {
changedParticipant = participant
})
participants := rm.GetParticipants()
@@ -159,7 +160,7 @@ func TestRoomJoin(t *testing.T) {
rm.lock.Lock()
rm.protoRoom.MaxParticipants = 1
rm.lock.Unlock()
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false)
p := NewMockParticipant("second", types.ProtocolVersion(0), false, false, rm.LocalParticipantListener())
err := rm.Join(p, nil, nil, iceServersForRoom)
require.Equal(t, ErrMaxParticipantsExceeded, err)
@@ -177,7 +178,7 @@ func TestParticipantUpdate(t *testing.T) {
"track mutes are sent to everyone",
true,
func(p types.LocalParticipant) {
p.SetTrackMuted("", true, false)
p.SetTrackMuted(&livekit.MuteTrackRequest{Muted: true}, false)
},
},
{
@@ -270,14 +271,14 @@ func TestPushAndDequeueUpdates(t *testing.T) {
pi *livekit.ParticipantInfo
closeReason types.ParticipantCloseReason
immediate bool
existing *participantUpdate
expected []*participantUpdate
validate func(t *testing.T, rm *Room, updates []*participantUpdate)
existing *ParticipantUpdate
expected []*ParticipantUpdate
validate func(t *testing.T, rm *Room, updates []*ParticipantUpdate)
}{
{
name: "publisher updates are immediate",
pi: publisher1v1,
expected: []*participantUpdate{{pi: publisher1v1}},
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v1}},
},
{
name: "subscriber updates are queued",
@@ -286,20 +287,20 @@ func TestPushAndDequeueUpdates(t *testing.T) {
{
name: "last version is enqueued",
pi: subscriber1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)}, // clone the existing value since it can be modified when setting to disconnected
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
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.pi)
requirePIEquals(t, subscriber1v2, queued.ParticipantInfo)
},
},
{
name: "latest version when immediate",
pi: subscriber1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
immediate: true,
expected: []*participantUpdate{{pi: subscriber1v2}},
validate: func(t *testing.T, rm *Room, _ []*participantUpdate) {
expected: []*ParticipantUpdate{{ParticipantInfo: subscriber1v2}},
validate: func(t *testing.T, rm *Room, _ []*ParticipantUpdate) {
queued := rm.batchedUpdates[livekit.ParticipantIdentity(identity)]
require.Nil(t, queued)
},
@@ -307,37 +308,37 @@ func TestPushAndDequeueUpdates(t *testing.T) {
{
name: "out of order updates are rejected",
pi: subscriber1v1,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v2)},
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
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.pi)
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{pi: utils.CloneProto(subscriber1v2), closeReason: types.ParticipantCloseReasonStale},
expected: []*participantUpdate{
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v2), CloseReason: types.ParticipantCloseReasonStale},
expected: []*ParticipantUpdate{
{
pi: &livekit.ParticipantInfo{
ParticipantInfo: &livekit.ParticipantInfo{
Identity: identity,
Sid: "1",
Version: 2,
State: livekit.ParticipantInfo_DISCONNECTED,
},
isSynthesizedDisconnect: true,
closeReason: types.ParticipantCloseReasonStale,
IsSynthesizedDisconnect: true,
CloseReason: types.ParticipantCloseReasonStale,
},
{pi: publisher2, closeReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
{ParticipantInfo: publisher2, CloseReason: types.ParticipantCloseReasonServiceRequestRemoveParticipant},
},
},
{
name: "when switching to publisher, queue is cleared",
pi: publisher1v2,
existing: &participantUpdate{pi: utils.CloneProto(subscriber1v1)},
expected: []*participantUpdate{{pi: publisher1v2}},
validate: func(t *testing.T, rm *Room, updates []*participantUpdate) {
existing: &ParticipantUpdate{ParticipantInfo: utils.CloneProto(subscriber1v1)},
expected: []*ParticipantUpdate{{ParticipantInfo: publisher1v2}},
validate: func(t *testing.T, rm *Room, updates []*ParticipantUpdate) {
require.Empty(t, rm.batchedUpdates)
},
},
@@ -347,14 +348,22 @@ func TestPushAndDequeueUpdates(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 1})
if tc.existing != nil {
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.pi.Identity)] = tc.existing
rm.batchedUpdates[livekit.ParticipantIdentity(tc.existing.ParticipantInfo.Identity)] = tc.existing
}
updates := rm.pushAndDequeueUpdates(tc.pi, tc.closeReason, tc.immediate)
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.pi, updates[i].pi)
require.Equal(t, item.isSynthesizedDisconnect, updates[i].isSynthesizedDisconnect)
require.Equal(t, item.closeReason, updates[i].closeReason)
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 {
@@ -417,6 +426,8 @@ func TestRoomClosure(t *testing.T) {
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)
@@ -427,9 +438,8 @@ func TestNewTrack(t *testing.T) {
// pub adds track
track := NewMockTrack(livekit.TrackType_VIDEO, "webcam")
trackCB := pub.OnTrackPublishedArgsForCall(0)
require.NotNil(t, trackCB)
trackCB(pub, track)
lpl.OnTrackPublished(pub, track)
// only p1 should've been subscribed to
require.Equal(t, 0, p0.SubscribeToTrackCallCount())
require.Equal(t, 1, p1.SubscribeToTrackCallCount())
@@ -441,7 +451,7 @@ func TestActiveSpeakers(t *testing.T) {
getActiveSpeakerUpdates := func(p *typesfakes.FakeLocalParticipant) [][]*livekit.SpeakerInfo {
var updates [][]*livekit.SpeakerInfo
numCalls := p.SendSpeakerUpdateCallCount()
for i := 0; i < numCalls; i++ {
for i := range numCalls {
infos, _ := p.SendSpeakerUpdateArgsForCall(i)
updates = append(updates, infos)
}
@@ -518,6 +528,7 @@ func TestActiveSpeakers(t *testing.T) {
t.Run("audio level is smoothed", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, protocol: 3, audioSmoothIntervals: 3})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
op := participants[1].(*typesfakes.FakeLocalParticipant)
@@ -611,6 +622,9 @@ func TestDataChannel(t *testing.T) {
t.Run(modeNames[mode], func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 3})
defer rm.Close(types.ParticipantCloseReasonNone)
lpl := rm.LocalParticipantListener()
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
@@ -631,17 +645,17 @@ func TestDataChannel(t *testing.T) {
}
encoded, _ := proto.Marshal(packetExp)
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
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.SendDataPacketCallCount())
require.Zero(t, fp.SendDataMessageCallCount())
continue
}
require.Equal(t, 1, fp.SendDataPacketCallCount())
_, got := fp.SendDataPacketArgsForCall(0)
require.Equal(t, 1, fp.SendDataMessageCallCount())
_, got, _, _ := fp.SendDataMessageArgsForCall(0)
require.Equal(t, encoded, got)
}
})
@@ -654,6 +668,9 @@ func TestDataChannel(t *testing.T) {
t.Run(modeNames[mode], func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 4})
defer rm.Close(types.ParticipantCloseReasonNone)
lpl := rm.LocalParticipantListener()
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p1 := participants[1].(*typesfakes.FakeLocalParticipant)
@@ -678,17 +695,17 @@ func TestDataChannel(t *testing.T) {
}
encoded, _ := proto.Marshal(packetExp)
p.OnDataPacketArgsForCall(0)(p, packet.Kind, packet)
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.SendDataPacketCallCount())
require.Zero(t, fp.SendDataMessageCallCount())
}
}
require.Equal(t, 1, p1.SendDataPacketCallCount())
_, got := p1.SendDataPacketArgsForCall(0)
require.Equal(t, 1, p1.SendDataMessageCallCount())
_, got, _, _ := p1.SendDataMessageArgsForCall(0)
require.Equal(t, encoded, got)
})
}
@@ -697,6 +714,7 @@ func TestDataChannel(t *testing.T) {
t.Run("publishing disallowed", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
defer rm.Close(types.ParticipantCloseReasonNone)
participants := rm.GetParticipants()
p := participants[0].(*typesfakes.FakeLocalParticipant)
p.CanPublishDataReturns(false)
@@ -710,13 +728,14 @@ func TestDataChannel(t *testing.T) {
},
}
if p.CanPublishData() {
p.OnDataPacketArgsForCall(0)(p, packet.Kind, &packet)
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.SendDataPacketCallCount())
require.Zero(t, fp.SendDataMessageCallCount())
}
})
}
@@ -726,7 +745,7 @@ func TestHiddenParticipants(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2, numHidden: 1})
defer rm.Close(types.ParticipantCloseReasonNone)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false)
pNew := NewMockParticipant("new", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
rm.Join(pNew, nil, nil, iceServersForRoom)
// expect new participant to get a JoinReply
@@ -740,14 +759,14 @@ func TestHiddenParticipants(t *testing.T) {
t.Run("hidden participant subscribes to tracks", func(t *testing.T) {
rm := newRoomWithParticipants(t, testRoomOpts{num: 2})
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false)
lpl := rm.LocalParticipantListener()
hidden := NewMockParticipant("hidden", types.CurrentProtocol, true, false, lpl)
err := rm.Join(hidden, nil, &ParticipantOptions{AutoSubscribe: true}, iceServersForRoom)
require.NoError(t, err)
stateChangeCB := hidden.OnStateChangeArgsForCall(0)
require.NotNil(t, stateChangeCB)
stateChangeCB(hidden, livekit.ParticipantInfo_ACTIVE)
hidden.StateReturns(livekit.ParticipantInfo_ACTIVE)
lpl.OnStateChange(hidden)
require.Eventually(t, func() bool { return hidden.SubscribeToTrackCallCount() == 2 }, 5*time.Second, 10*time.Millisecond)
})
@@ -761,7 +780,7 @@ func TestRoomUpdate(t *testing.T) {
p1 := rm.GetParticipants()[0].(*typesfakes.FakeLocalParticipant)
require.Equal(t, 0, p1.SendRoomUpdateCallCount())
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false)
p2 := NewMockParticipant("p2", types.CurrentProtocol, false, false, rm.LocalParticipantListener())
require.NoError(t, rm.Join(p2, nil, nil, iceServersForRoom))
// p1 should have received an update
@@ -795,6 +814,12 @@ type testRoomOpts struct {
}
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,
@@ -816,12 +841,12 @@ func newRoomWithParticipants(t *testing.T, opts testRoomOpts) *Room {
NodeId: "testnode",
Region: "testregion",
},
telemetry.NewTelemetryService(webhook.NewDefaultNotifier("", "", nil), &telemetryfakes.FakeAnalyticsService{}),
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)
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)
+153 -15
View File
@@ -22,14 +22,18 @@ import (
"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
tracks map[livekit.TrackID][]*TrackInfo
dataTracks map[livekit.TrackID][]*DataTrackInfo
}
type TrackInfo struct {
@@ -38,54 +42,101 @@ type TrackInfo struct {
PublisherID livekit.ParticipantID
}
func NewRoomTrackManager() *RoomTrackManager {
type DataTrackInfo struct {
DataTrack types.DataTrack
PublisherIdentity livekit.ParticipantIdentity
PublisherID livekit.ParticipantID
}
func NewRoomTrackManager(logger logger.Logger) *RoomTrackManager {
return &RoomTrackManager{
tracks: make(map[livekit.TrackID]*TrackInfo),
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()
r.tracks[track.ID()] = &TrackInfo{
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(track.ID())
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
info, ok := r.tracks[track.ID()]
if !ok || info.Track != track {
infos, ok := r.tracks[trackID]
if !ok {
r.lock.Unlock()
return
}
delete(r.tracks, track.ID())
r.lock.Unlock()
n := r.removedNotifier.GetNotifier(string(track.ID()))
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(track.ID()), true)
r.removedNotifier.RemoveNotifier(string(track.ID()), true)
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()
info := r.tracks[trackID]
if info == nil {
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
@@ -123,3 +174,90 @@ func (r *RoomTrackManager) GetOrCreateTrackChangeNotifier(trackID livekit.TrackI
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)
}
@@ -0,0 +1,307 @@
// 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 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 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,
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) 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))
}
-159
View File
@@ -1,159 +0,0 @@
// 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/livekit-server/pkg/rtc/types"
)
func HandleParticipantSignal(room types.Room, participant types.LocalParticipant, req *livekit.SignalRequest, pLogger logger.Logger) error {
participant.UpdateLastSeenSignal()
switch msg := req.GetMessage().(type) {
case *livekit.SignalRequest_Offer:
participant.HandleOffer(FromProtoSessionDescription(msg.Offer))
case *livekit.SignalRequest_Answer:
participant.HandleAnswer(FromProtoSessionDescription(msg.Answer))
case *livekit.SignalRequest_Trickle:
candidateInit, err := FromProtoTrickle(msg.Trickle)
if err != nil {
pLogger.Warnw("could not decode trickle", err)
return nil
}
participant.AddICECandidate(candidateInit, msg.Trickle.Target)
case *livekit.SignalRequest_AddTrack:
pLogger.Debugw("add track request", "trackID", msg.AddTrack.Cid)
participant.AddTrack(msg.AddTrack)
case *livekit.SignalRequest_Mute:
participant.SetTrackMuted(livekit.TrackID(msg.Mute.Sid), msg.Mute.Muted, false)
case *livekit.SignalRequest_Subscription:
// allow participant to indicate their interest in the subscription
// permission check happens later in SubscriptionManager
room.UpdateSubscriptions(
participant,
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) {
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
}
pLogger.Debugw("client leaving room", "reason", reason)
room.RemoveParticipant(participant.Identity(), participant.ID(), reason)
case *livekit.SignalRequest_SubscriptionPermission:
err := room.UpdateSubscriptionPermission(participant, msg.SubscriptionPermission)
if err != nil {
pLogger.Warnw("could not update subscription permission", err,
"permissions", msg.SubscriptionPermission)
}
case *livekit.SignalRequest_SyncState:
err := room.SyncState(participant, msg.SyncState)
if err != nil {
pLogger.Warnw("could not sync state", err,
"state", msg.SyncState)
}
case *livekit.SignalRequest_Simulate:
err := room.SimulateScenario(participant, msg.Simulate)
if err != nil {
pLogger.Warnw("could not simulate scenario", err,
"simulate", msg.Simulate)
}
case *livekit.SignalRequest_PingReq:
if msg.PingReq.Rtt > 0 {
participant.UpdateSignalingRTT(uint32(msg.PingReq.Rtt))
}
case *livekit.SignalRequest_UpdateMetadata:
requestResponse := &livekit.RequestResponse{
RequestId: msg.UpdateMetadata.RequestId,
Reason: livekit.RequestResponse_OK,
}
if participant.ClaimGrants().Video.GetCanUpdateOwnMetadata() {
if err := participant.CheckMetadataLimits(
msg.UpdateMetadata.Name,
msg.UpdateMetadata.Metadata,
msg.UpdateMetadata.Attributes,
); err == nil {
if msg.UpdateMetadata.Name != "" {
participant.SetName(msg.UpdateMetadata.Name)
}
if msg.UpdateMetadata.Metadata != "" {
participant.SetMetadata(msg.UpdateMetadata.Metadata)
}
if msg.UpdateMetadata.Attributes != nil {
participant.SetAttributes(msg.UpdateMetadata.Attributes)
}
} else {
pLogger.Warnw("could not update metadata", err)
switch err {
case ErrNameExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds name length limit"
case ErrMetadataExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds metadata size limit"
case ErrAttributesExceedsLimits:
requestResponse.Reason = livekit.RequestResponse_LIMIT_EXCEEDED
requestResponse.Message = "exceeds attributes size limit"
}
}
} else {
requestResponse.Reason = livekit.RequestResponse_NOT_ALLOWED
requestResponse.Message = "does not have permission to update own metadata"
}
participant.SendRequestResponse(requestResponse)
case *livekit.SignalRequest_UpdateAudioTrack:
if err := participant.UpdateAudioTrack(msg.UpdateAudioTrack); err != nil {
pLogger.Warnw("could not update audio track", err, "update", msg.UpdateAudioTrack)
}
case *livekit.SignalRequest_UpdateVideoTrack:
if err := participant.UpdateVideoTrack(msg.UpdateVideoTrack); err != nil {
pLogger.Warnw("could not update video track", err, "update", msg.UpdateVideoTrack)
}
}
return nil
}
@@ -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")
)
@@ -0,0 +1,65 @@
// 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)
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
}
@@ -0,0 +1,157 @@
// 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)
}
return nil
}
@@ -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
}
@@ -0,0 +1,114 @@
// 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
}
type signallerAsync struct {
params SignallerAsyncParams
*signallerAsyncBase
}
func NewSignallerAsync(params SignallerAsyncParams) ParticipantSignaller {
return &signallerAsync{
params: params,
signallerAsyncBase: newSignallerAsyncBase(signallerAsyncBaseParams{Logger: params.Logger}),
}
}
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
}
if !s.params.Participant.IsReady() {
if typed, ok := msg.(*livekit.SignalResponse); !ok {
s.params.Logger.Warnw(
"unknown message type", nil,
"messageType", fmt.Sprintf("%T", msg),
)
} else {
if typed.GetJoin() == nil {
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 {
s.params.Logger.Debugw("sent signal response", "response", logger.Proto(msg))
}
return nil
}
@@ -0,0 +1,79 @@
// 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"
"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
}
type signallerAsyncBase struct {
signallerUnimplemented
params signallerAsyncBaseParams
resSinkMu sync.Mutex
resSink routing.MessageSink
}
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
s.resSinkMu.Unlock()
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()
}
}
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)
}
@@ -0,0 +1,39 @@
// 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) WriteMessage(msg proto.Message) error {
return nil
}
@@ -0,0 +1,260 @@
// 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,
},
}
}
@@ -0,0 +1,129 @@
// 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
}
+210 -24
View File
@@ -23,9 +23,12 @@ import (
"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"
@@ -37,19 +40,27 @@ const (
subscriptionDebounceInterval = 100 * time.Millisecond
)
var _ types.SubscribedTrack = (*SubscribedTrack)(nil)
type SubscribedTrackParams struct {
PublisherID livekit.ParticipantID
PublisherIdentity livekit.ParticipantIdentity
PublisherVersion uint32
Subscriber types.LocalParticipant
MediaTrack types.MediaTrack
DownTrack *sfu.DownTrack
AdaptiveStream bool
ReceiverConfig ReceiverConfig
SubscriberConfig DirectionConfig
Subscriber types.LocalParticipant
MediaTrack types.MediaTrack
AdaptiveStream 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
@@ -65,21 +76,98 @@ type SubscribedTrack struct {
onClose atomic.Value // func(bool)
debouncer func(func())
statsKey telemetry.StatsKey
reporter roomobs.TrackReporter
}
func NewSubscribedTrack(params SubscribedTrackParams) *SubscribedTrack {
func NewSubscribedTrack(params SubscribedTrackParams) (*SubscribedTrack, error) {
s := &SubscribedTrack{
params: params,
logger: params.Subscriber.GetLogger().WithComponent(sutils.ComponentSub).WithValues(
"trackID", params.DownTrack.ID(),
"publisherID", params.PublisherID,
"publisher", params.PublisherIdentity,
"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()),
}
return s
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(),
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)) {
@@ -121,6 +209,7 @@ func (t *SubscribedTrack) Bound(err error) {
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.AdaptiveStream {
@@ -128,6 +217,7 @@ func (t *SubscribedTrack) Bound(err error) {
} 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()
@@ -141,7 +231,7 @@ func (t *SubscribedTrack) Bound(err error) {
// for DownTrack callback to notify us that it's closed
func (t *SubscribedTrack) Close(isExpectedToResume bool) {
if onClose := t.onClose.Load(); onClose != nil {
go onClose.(func(bool))(isExpectedToResume)
onClose.(func(bool))(isExpectedToResume)
}
}
@@ -157,19 +247,19 @@ func (t *SubscribedTrack) IsBound() bool {
}
func (t *SubscribedTrack) ID() livekit.TrackID {
return livekit.TrackID(t.params.DownTrack.ID())
return livekit.TrackID(t.downTrack.ID())
}
func (t *SubscribedTrack) PublisherID() livekit.ParticipantID {
return t.params.PublisherID
return t.params.MediaTrack.PublisherID()
}
func (t *SubscribedTrack) PublisherIdentity() livekit.ParticipantIdentity {
return t.params.PublisherIdentity
return t.params.MediaTrack.PublisherIdentity()
}
func (t *SubscribedTrack) PublisherVersion() uint32 {
return t.params.PublisherVersion
return t.params.MediaTrack.PublisherVersion()
}
func (t *SubscribedTrack) SubscriberID() livekit.ParticipantID {
@@ -185,7 +275,7 @@ func (t *SubscribedTrack) Subscriber() types.LocalParticipant {
}
func (t *SubscribedTrack) DownTrack() *sfu.DownTrack {
return t.params.DownTrack
return t.downTrack
}
func (t *SubscribedTrack) MediaTrack() types.MediaTrack {
@@ -209,18 +299,20 @@ func (t *SubscribedTrack) isMutedLocked() bool {
}
func (t *SubscribedTrack) SetPublisherMuted(muted bool) {
t.DownTrack().PubMute(muted)
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 {
@@ -242,7 +334,6 @@ func (t *SubscribedTrack) applySettings() {
return
}
t.logger.Debugw("updating subscriber track settings", "settings", logger.Proto(t.settings))
t.settingsVersion = t.versionGenerator.Next()
settingsVersion := t.settingsVersion
t.settingsLock.Unlock()
@@ -253,23 +344,25 @@ func (t *SubscribedTrack) applySettings() {
if dt.Kind() == webrtc.RTPCodecTypeVideo {
mt := t.MediaTrack()
quality := t.settings.Quality
mimeType := dt.Mime()
if t.settings.Width > 0 {
quality = mt.GetQualityForDimension(t.settings.Width, t.settings.Height)
quality = mt.GetQualityForDimension(mimeType, t.settings.Width, t.settings.Height)
}
spatial = buffer.VideoQualityToSpatialLayer(quality, mt.ToProto())
spatial = buffer.GetSpatialLayerForVideoQuality(mimeType, quality, mt.ToProto())
if t.settings.Fps > 0 {
temporal = mt.GetTemporalLayerForSpatialFps(spatial, t.settings.Fps, dt.Mime())
temporal = mt.GetTemporalLayerForSpatialFps(mimeType, spatial, t.settings.Fps)
}
}
t.settingsLock.Lock()
if settingsVersion != t.settingsVersion {
// a newer settings has superceded this one
// 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()
@@ -302,3 +395,96 @@ func (t *SubscribedTrack) RTPSender() *webrtc.RTPSender {
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() {
t.params.TelemetryListener.OnTrackSubscribeStreamStarted(t.params.Subscriber.ID(), t.params.MediaTrack.ToProto())
}
File diff suppressed because it is too large Load Diff
@@ -26,7 +26,6 @@ import (
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
@@ -67,7 +66,7 @@ func TestSubscribe(t *testing.T) {
}
})
sm.SubscribeToTrack("track")
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
@@ -83,8 +82,8 @@ func TestSubscribe(t *testing.T) {
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
// ensure telemetry events are sent
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
// ensure bound
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
@@ -93,7 +92,7 @@ func TestSubscribe(t *testing.T) {
}, subSettleTimeout, subCheckInterval, "track was not bound")
// telemetry event should have been sent
require.Equal(t, 1, tm.TrackSubscribedCallCount())
require.Equal(t, 1, tl.OnTrackSubscribedCallCount())
time.Sleep(notFoundTimeout)
require.False(t, failed.Load())
@@ -127,7 +126,7 @@ func TestSubscribe(t *testing.T) {
failed.Store(true)
}
sm.SubscribeToTrack("track")
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.getHasPermission()
@@ -142,9 +141,9 @@ func TestSubscribe(t *testing.T) {
require.Len(t, sm.GetSubscribedTracks(), 0)
// trackSubscribed telemetry not sent
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 0, tm.TrackSubscribedCallCount())
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
require.Equal(t, 0, tl.OnTrackSubscribedCallCount())
// give permissions now
resolver.lock.Lock()
@@ -168,7 +167,7 @@ func TestSubscribe(t *testing.T) {
failed.Store(true)
}
sm.SubscribeToTrack("track")
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
return !s.needsSubscribe()
@@ -197,15 +196,17 @@ func TestUnsubscribe(t *testing.T) {
resolver := newTestResolver(true, true, "pub", "pubID")
s := &trackSubscription{
trackID: "track",
desired: true,
subscriberID: sm.params.Participant.ID(),
publisherID: "pubID",
publisherIdentity: "pub",
hasPermission: true,
bound: true,
logger: logger.GetLogger(),
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)
@@ -242,18 +243,15 @@ func TestUnsubscribe(t *testing.T) {
sm.lock.RLock()
subLen := len(sm.subscriptions)
sm.lock.RUnlock()
if subLen != 0 {
return false
}
return true
return subLen == 0
}, subSettleTimeout, subCheckInterval, "Track was not unsubscribed")
// no traces should be left
require.Len(t, sm.GetSubscribedTracks(), 0)
require.False(t, res.TrackChangedNotifier.HasObservers())
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackUnsubscribedCallCount())
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackUnsubscribedCallCount())
}
func TestSubscribeStatusChanged(t *testing.T) {
@@ -271,8 +269,8 @@ func TestSubscribeStatusChanged(t *testing.T) {
}
})
sm.SubscribeToTrack("track1")
sm.SubscribeToTrack("track2")
sm.SubscribeToTrack("track1", false)
sm.SubscribeToTrack("track2", false)
s1 := sm.subscriptions["track1"]
s2 := sm.subscriptions["track2"]
require.Eventually(t, func() bool {
@@ -332,7 +330,7 @@ func TestUpdateSettingsBeforeSubscription(t *testing.T) {
}
sm.UpdateSubscribedTrackSettings("track", settings)
sm.SubscribeToTrack("track")
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.Eventually(t, func() bool {
@@ -376,7 +374,7 @@ func TestSubscriptionLimits(t *testing.T) {
}
})
sm.SubscribeToTrack("track")
sm.SubscribeToTrack("track", false)
s := sm.subscriptions["track"]
require.True(t, s.isDesired())
require.Eventually(t, func() bool {
@@ -392,8 +390,8 @@ func TestSubscriptionLimits(t *testing.T) {
require.Equal(t, "pubID", string(sm.GetSubscribedParticipants()[0]))
// ensure telemetry events are sent
tm := sm.params.Telemetry.(*telemetryfakes.FakeTelemetryService)
require.Equal(t, 1, tm.TrackSubscribeRequestedCallCount())
tl := sm.params.TelemetryListener.(*typesfakes.FakeParticipantTelemetryListener)
require.Equal(t, 1, tl.OnTrackSubscribeRequestedCallCount())
// ensure bound
setTestSubscribedTrackBound(t, s.getSubscribedTrack())
@@ -402,15 +400,15 @@ func TestSubscriptionLimits(t *testing.T) {
}, subSettleTimeout, subCheckInterval, "track was not bound")
// telemetry event should have been sent
require.Equal(t, 1, tm.TrackSubscribedCallCount())
require.Equal(t, 1, tl.OnTrackSubscribedCallCount())
// reach subscription limit, subscribe pending
sm.SubscribeToTrack("track2")
sm.SubscribeToTrack("track2", false)
s2 := sm.subscriptions["track2"]
time.Sleep(subscriptionTimeout * 2)
require.True(t, s2.needsSubscribe())
require.Equal(t, 2, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 1, tm.TrackSubscribeFailedCallCount())
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
@@ -427,7 +425,7 @@ func TestSubscriptionLimits(t *testing.T) {
require.False(t, s2.needsSubscribe())
require.EqualValues(t, 2, subCount.Load())
require.NotNil(t, s2.getSubscribedTrack())
require.Equal(t, 2, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 2, tl.OnTrackSubscribeRequestedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
// ensure bound
@@ -437,13 +435,13 @@ func TestSubscriptionLimits(t *testing.T) {
}, subSettleTimeout, subCheckInterval, "track was not bound")
// subscribe to track1 again, which should pending
sm.SubscribeToTrack("track")
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, tm.TrackSubscribeRequestedCallCount())
require.Equal(t, 2, tm.TrackSubscribeFailedCallCount())
require.Equal(t, 3, tl.OnTrackSubscribeRequestedCallCount())
require.Equal(t, 2, tl.OnTrackSubscribeFailedCallCount())
require.Len(t, sm.GetSubscribedTracks(), 1)
}
@@ -471,7 +469,7 @@ func newTestSubscriptionManagerWithParams(params testSubscriptionParams) *Subscr
TrackResolver: func(sub types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
return types.MediaResolverResult{}
},
Telemetry: &telemetryfakes.FakeTelemetryService{},
TelemetryListener: &typesfakes.FakeParticipantTelemetryListener{},
SubscriptionLimitAudio: params.SubscriptionLimitAudio,
SubscriptionLimitVideo: params.SubscriptionLimitVideo,
})
+12 -16
View File
@@ -17,6 +17,7 @@ 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"
@@ -24,7 +25,13 @@ import (
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
)
func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.ProtocolVersion, hidden bool, publisher bool) *typesfakes.FakeLocalParticipant {
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))
@@ -49,25 +56,13 @@ func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.Pro
}, utils.TimedVersion(0))
p.SetMetadataCalls(func(m string) {
var f func(participant types.LocalParticipant)
if p.OnParticipantUpdateCallCount() > 0 {
f = p.OnParticipantUpdateArgsForCall(p.OnParticipantUpdateCallCount() - 1)
}
if f != nil {
f(p)
}
participantListener.OnParticipantUpdate(p)
})
updateTrack := func() {
var f func(participant types.LocalParticipant, track types.MediaTrack)
if p.OnTrackUpdatedCallCount() > 0 {
f = p.OnTrackUpdatedArgsForCall(p.OnTrackUpdatedCallCount() - 1)
}
if f != nil {
f(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam"))
}
participantListener.OnTrackUpdated(p, NewMockTrack(livekit.TrackType_VIDEO, "testcam"))
}
p.SetTrackMutedCalls(func(sid livekit.TrackID, muted bool, fromServer bool) *livekit.TrackInfo {
p.SetTrackMutedCalls(func(mute *livekit.MuteTrackRequest, fromServer bool) *livekit.TrackInfo {
updateTrack()
return nil
})
@@ -75,6 +70,7 @@ func NewMockParticipant(identity livekit.ParticipantIdentity, protocol types.Pro
updateTrack()
})
p.GetLoggerReturns(logger.GetLogger())
p.GetReporterReturns(roomobs.NewNoopParticipantSessionReporter())
return p
}
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -39,13 +39,17 @@ type Handler interface {
OnFullyEstablished()
OnFailed(isShortLived bool, iceConnectionInfo *types.ICEConnectionInfo)
OnTrack(track *webrtc.TrackRemote, rtpReceiver *webrtc.RTPReceiver)
OnDataPacket(kind livekit.DataPacket_Kind, data []byte)
OnDataMessage(kind livekit.DataPacket_Kind, data []byte)
OnDataMessageUnlabeled(data []byte)
OnDataTrackMessage(data []byte, arrivalTime int64)
OnDataSendError(err error)
OnOffer(sd webrtc.SessionDescription) error
OnAnswer(sd webrtc.SessionDescription) 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{}
@@ -57,12 +61,15 @@ 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) OnDataPacket(kind livekit.DataPacket_Kind, data []byte) {}
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) error {
func (h UnimplementedHandler) OnOffer(sd webrtc.SessionDescription, offerId uint32, midToTrackID map[string]string) error {
return ErrNoOfferHandler
}
func (h UnimplementedHandler) OnAnswer(sd webrtc.SessionDescription) error {
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) {}
@@ -70,3 +77,6 @@ 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
}
@@ -12,10 +12,12 @@ import (
)
type FakeHandler struct {
OnAnswerStub func(webrtc.SessionDescription) error
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
@@ -23,17 +25,28 @@ type FakeHandler struct {
onAnswerReturnsOnCall map[int]struct {
result1 error
}
OnDataPacketStub func(livekit.DataPacket_Kind, []byte)
onDataPacketMutex sync.RWMutex
onDataPacketArgsForCall []struct {
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 {
@@ -69,10 +82,12 @@ type FakeHandler struct {
onNegotiationStateChangedArgsForCall []struct {
arg1 transport.NegotiationState
}
OnOfferStub func(webrtc.SessionDescription) error
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
@@ -80,6 +95,10 @@ type FakeHandler struct {
onOfferReturnsOnCall map[int]struct {
result1 error
}
OnSetRemoteDescriptionOfferStub func()
onSetRemoteDescriptionOfferMutex sync.RWMutex
onSetRemoteDescriptionOfferArgsForCall []struct {
}
OnStreamStateChangeStub func(*streamallocator.StreamStateUpdate) error
onStreamStateChangeMutex sync.RWMutex
onStreamStateChangeArgsForCall []struct {
@@ -97,22 +116,36 @@ type FakeHandler 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) error {
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
}{arg1})
arg2 uint32
arg3 map[string]string
}{arg1, arg2, arg3})
stub := fake.OnAnswerStub
fakeReturns := fake.onAnswerReturns
fake.recordInvocation("OnAnswer", []interface{}{arg1})
fake.recordInvocation("OnAnswer", []interface{}{arg1, arg2, arg3})
fake.onAnswerMutex.Unlock()
if stub != nil {
return stub(arg1)
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
@@ -126,17 +159,17 @@ func (fake *FakeHandler) OnAnswerCallCount() int {
return len(fake.onAnswerArgsForCall)
}
func (fake *FakeHandler) OnAnswerCalls(stub func(webrtc.SessionDescription) error) {
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 {
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
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeHandler) OnAnswerReturns(result1 error) {
@@ -162,44 +195,81 @@ func (fake *FakeHandler) OnAnswerReturnsOnCall(i int, result1 error) {
}{result1}
}
func (fake *FakeHandler) OnDataPacket(arg1 livekit.DataPacket_Kind, arg2 []byte) {
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.onDataPacketMutex.Lock()
fake.onDataPacketArgsForCall = append(fake.onDataPacketArgsForCall, struct {
fake.onDataMessageMutex.Lock()
fake.onDataMessageArgsForCall = append(fake.onDataMessageArgsForCall, struct {
arg1 livekit.DataPacket_Kind
arg2 []byte
}{arg1, arg2Copy})
stub := fake.OnDataPacketStub
fake.recordInvocation("OnDataPacket", []interface{}{arg1, arg2Copy})
fake.onDataPacketMutex.Unlock()
stub := fake.OnDataMessageStub
fake.recordInvocation("OnDataMessage", []interface{}{arg1, arg2Copy})
fake.onDataMessageMutex.Unlock()
if stub != nil {
fake.OnDataPacketStub(arg1, arg2)
fake.OnDataMessageStub(arg1, arg2)
}
}
func (fake *FakeHandler) OnDataPacketCallCount() int {
fake.onDataPacketMutex.RLock()
defer fake.onDataPacketMutex.RUnlock()
return len(fake.onDataPacketArgsForCall)
func (fake *FakeHandler) OnDataMessageCallCount() int {
fake.onDataMessageMutex.RLock()
defer fake.onDataMessageMutex.RUnlock()
return len(fake.onDataMessageArgsForCall)
}
func (fake *FakeHandler) OnDataPacketCalls(stub func(livekit.DataPacket_Kind, []byte)) {
fake.onDataPacketMutex.Lock()
defer fake.onDataPacketMutex.Unlock()
fake.OnDataPacketStub = stub
func (fake *FakeHandler) OnDataMessageCalls(stub func(livekit.DataPacket_Kind, []byte)) {
fake.onDataMessageMutex.Lock()
defer fake.onDataMessageMutex.Unlock()
fake.OnDataMessageStub = stub
}
func (fake *FakeHandler) OnDataPacketArgsForCall(i int) (livekit.DataPacket_Kind, []byte) {
fake.onDataPacketMutex.RLock()
defer fake.onDataPacketMutex.RUnlock()
argsForCall := fake.onDataPacketArgsForCall[i]
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 {
@@ -232,6 +302,44 @@ func (fake *FakeHandler) OnDataSendErrorArgsForCall(i int) error {
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 {
@@ -431,18 +539,20 @@ func (fake *FakeHandler) OnNegotiationStateChangedArgsForCall(i int) transport.N
return argsForCall.arg1
}
func (fake *FakeHandler) OnOffer(arg1 webrtc.SessionDescription) error {
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
}{arg1})
arg2 uint32
arg3 map[string]string
}{arg1, arg2, arg3})
stub := fake.OnOfferStub
fakeReturns := fake.onOfferReturns
fake.recordInvocation("OnOffer", []interface{}{arg1})
fake.recordInvocation("OnOffer", []interface{}{arg1, arg2, arg3})
fake.onOfferMutex.Unlock()
if stub != nil {
return stub(arg1)
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
@@ -456,17 +566,17 @@ func (fake *FakeHandler) OnOfferCallCount() int {
return len(fake.onOfferArgsForCall)
}
func (fake *FakeHandler) OnOfferCalls(stub func(webrtc.SessionDescription) error) {
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 {
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
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeHandler) OnOfferReturns(result1 error) {
@@ -492,6 +602,30 @@ func (fake *FakeHandler) OnOfferReturnsOnCall(i int, 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)]
@@ -586,33 +720,71 @@ func (fake *FakeHandler) OnTrackArgsForCall(i int) (*webrtc.TrackRemote, *webrtc
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()
fake.onAnswerMutex.RLock()
defer fake.onAnswerMutex.RUnlock()
fake.onDataPacketMutex.RLock()
defer fake.onDataPacketMutex.RUnlock()
fake.onDataSendErrorMutex.RLock()
defer fake.onDataSendErrorMutex.RUnlock()
fake.onFailedMutex.RLock()
defer fake.onFailedMutex.RUnlock()
fake.onFullyEstablishedMutex.RLock()
defer fake.onFullyEstablishedMutex.RUnlock()
fake.onICECandidateMutex.RLock()
defer fake.onICECandidateMutex.RUnlock()
fake.onInitialConnectedMutex.RLock()
defer fake.onInitialConnectedMutex.RUnlock()
fake.onNegotiationFailedMutex.RLock()
defer fake.onNegotiationFailedMutex.RUnlock()
fake.onNegotiationStateChangedMutex.RLock()
defer fake.onNegotiationStateChangedMutex.RUnlock()
fake.onOfferMutex.RLock()
defer fake.onOfferMutex.RUnlock()
fake.onStreamStateChangeMutex.RLock()
defer fake.onStreamStateChangeMutex.RUnlock()
fake.onTrackMutex.RLock()
defer fake.onTrackMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+209 -56
View File
@@ -28,17 +28,15 @@ import (
"github.com/livekit/livekit-server/pkg/rtc/transport"
"github.com/livekit/livekit-server/pkg/rtc/transport/transportfakes"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"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{
ParticipantID: "id",
ParticipantIdentity: "identity",
Config: &WebRTCConfig{},
IsOfferer: true,
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
@@ -70,7 +68,7 @@ func TestMissingAnswerDuringICERestart(t *testing.T) {
// offer again, but missed
var offerReceived atomic.Bool
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
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)
@@ -91,10 +89,8 @@ func TestMissingAnswerDuringICERestart(t *testing.T) {
func TestNegotiationTiming(t *testing.T) {
params := TransportParams{
ParticipantID: "id",
ParticipantIdentity: "identity",
Config: &WebRTCConfig{},
IsOfferer: true,
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
@@ -116,9 +112,16 @@ func TestNegotiationTiming(t *testing.T) {
require.False(t, transportB.IsEstablished())
handleICEExchange(t, transportA, transportB, handlerA, handlerB)
offer := atomic.Value{}
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
offer.Store(&sd)
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
})
@@ -161,15 +164,22 @@ func TestNegotiationTiming(t *testing.T) {
return state == transport.NegotiationStateRetry
}, 10*time.Second, 10*time.Millisecond, "negotiation state does not match NegotiateStateRetry")
time.Sleep(5 * time.Millisecond)
actualOffer, ok := offer.Load().(*webrtc.SessionDescription)
require.True(t, ok)
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) error {
transportA.HandleRemoteDescription(answer)
handlerB.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
transportA.HandleRemoteDescription(answer, answerId)
return nil
})
transportB.HandleRemoteDescription(*actualOffer)
transportB.HandleRemoteDescription(*firstOffer.Load().(*webrtc.SessionDescription), firstOfferId.Load())
require.Eventually(t, func() bool {
return transportA.IsEstablished()
@@ -178,11 +188,18 @@ func TestNegotiationTiming(t *testing.T) {
return transportB.IsEstablished()
}, 10*time.Second, time.Millisecond*10, "transportB is not established")
// it should still be negotiating again
require.Equal(t, transport.NegotiationStateRemote, negotiationState.Load().(transport.NegotiationState))
offer2, ok := offer.Load().(*webrtc.SessionDescription)
// 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)
require.False(t, offer2 == actualOffer)
transportA.Close()
transportB.Close()
@@ -190,10 +207,8 @@ func TestNegotiationTiming(t *testing.T) {
func TestFirstOfferMissedDuringICERestart(t *testing.T) {
params := TransportParams{
ParticipantID: "id",
ParticipantIdentity: "identity",
Config: &WebRTCConfig{},
IsOfferer: true,
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
@@ -216,7 +231,7 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
// first offer missed
var firstOfferReceived atomic.Bool
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, _offerId uint32, _midToTrackID map[string]string) error {
firstOfferReceived.Store(true)
return nil
})
@@ -228,13 +243,13 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
// 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) error {
transportA.HandleRemoteDescription(answer)
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) error {
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
@@ -244,7 +259,7 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
}, 10*time.Second, time.Millisecond*10)
}
transportB.HandleRemoteDescription(sd)
transportB.HandleRemoteDescription(sd, offerId)
return nil
})
@@ -264,10 +279,8 @@ func TestFirstOfferMissedDuringICERestart(t *testing.T) {
func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
params := TransportParams{
ParticipantID: "id",
ParticipantIdentity: "identity",
Config: &WebRTCConfig{},
IsOfferer: true,
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
@@ -290,17 +303,17 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
// first answer missed
var firstAnswerReceived atomic.Bool
handlerB.OnAnswerCalls(func(sd webrtc.SessionDescription) error {
handlerB.OnAnswerCalls(func(sd webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
if firstAnswerReceived.Load() {
transportA.HandleRemoteDescription(sd)
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) error {
transportB.HandleRemoteDescription(sd)
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
transportB.HandleRemoteDescription(sd, offerId)
return nil
})
@@ -313,7 +326,7 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
// first one is recover from missed offer
// second one is restartICE
var offerCount atomic.Int32
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error {
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
@@ -323,7 +336,7 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
}, 10*time.Second, time.Millisecond*10)
}
transportB.HandleRemoteDescription(sd)
transportB.HandleRemoteDescription(sd, offerId)
return nil
})
@@ -343,10 +356,8 @@ func TestFirstAnswerMissedDuringICERestart(t *testing.T) {
func TestNegotiationFailed(t *testing.T) {
params := TransportParams{
ParticipantID: "id",
ParticipantIdentity: "identity",
Config: &WebRTCConfig{},
IsOfferer: true,
Config: &WebRTCConfig{},
IsOfferer: true,
}
paramsA := params
@@ -371,7 +382,9 @@ func TestNegotiationFailed(t *testing.T) {
connectTransports(t, transportA, transportB, handlerA, handlerB, false, 1, 1)
// reset OnOffer to force a negotiation failure
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription) error { return nil })
handlerA.OnOfferCalls(func(sd webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
return nil
})
var failed atomic.Int32
handlerA.OnNegotiationFailedCalls(func() {
failed.Inc()
@@ -386,10 +399,8 @@ func TestNegotiationFailed(t *testing.T) {
func TestFilteringCandidates(t *testing.T) {
params := TransportParams{
ParticipantID: "id",
ParticipantIdentity: "identity",
Config: &WebRTCConfig{},
EnabledCodecs: []*livekit.Codec{
Config: &WebRTCConfig{},
EnabledPublishCodecs: []*livekit.Codec{
{Mime: mime.MimeTypeOpus.String()},
{Mime: mime.MimeTypeVP8.String()},
{Mime: mime.MimeTypeH264.String()},
@@ -527,15 +538,15 @@ func handleICEExchange(t *testing.T, a, b *PCTransport, ah, bh *transportfakes.F
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) error {
answererHandler.OnAnswerCalls(func(answer webrtc.SessionDescription, answerId uint32, _midToTrackID map[string]string) error {
answerCount.Inc()
offerer.HandleRemoteDescription(answer)
offerer.HandleRemoteDescription(answer, answerId)
return nil
})
offererHandler.OnOfferCalls(func(offer webrtc.SessionDescription) error {
offererHandler.OnOfferCalls(func(offer webrtc.SessionDescription, offerId uint32, _midToTrackID map[string]string) error {
offerCount.Inc()
answerer.HandleRemoteDescription(offer)
answerer.HandleRemoteDescription(offer, offerId)
return nil
})
@@ -606,7 +617,7 @@ func TestConfigureAudioTransceiver(t *testing.T) {
tr, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly})
require.NoError(t, err)
configureAudioTransceiver(tr, testcase.stereo, testcase.nack)
configureSenderAudio(tr, testcase.stereo, testcase.nack)
codecs := tr.Sender().GetParameters().Codecs
for _, codec := range codecs {
if mime.IsMimeTypeStringOpus(codec.MimeType) {
@@ -624,3 +635,145 @@ func TestConfigureAudioTransceiver(t *testing.T) {
})
}
}
// 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)
}
}
+398 -179
View File
@@ -39,8 +39,8 @@ import (
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/datachannel"
"github.com/livekit/livekit-server/pkg/sfu/interceptor"
"github.com/livekit/livekit-server/pkg/sfu/pacer"
"github.com/livekit/livekit-server/pkg/telemetry"
)
const (
@@ -71,43 +71,33 @@ func (h TransportManagerTransportHandler) OnFailed(isShortLived bool, iceConnect
// -------------------------------
type TransportManagerPublisherTransportHandler struct {
TransportManagerTransportHandler
}
func (h TransportManagerPublisherTransportHandler) OnAnswer(sd webrtc.SessionDescription) error {
h.t.lastPublisherAnswer.Store(sd)
return h.Handler.OnAnswer(sd)
}
// -------------------------------
type TransportManagerParams struct {
Identity livekit.ParticipantIdentity
SID livekit.ParticipantID
SubscriberAsPrimary bool
Config *WebRTCConfig
Twcc *twcc.Responder
ProtocolVersion types.ProtocolVersion
CongestionControlConfig config.CongestionControlConfig
EnabledSubscribeCodecs []*livekit.Codec
EnabledPublishCodecs []*livekit.Codec
SimTracks map[uint32]SimulcastTrackInfo
ClientInfo ClientInfo
Migration bool
AllowTCPFallback bool
TCPFallbackRTTThreshold int
AllowUDPUnstableFallback bool
TURNSEnabled bool
AllowPlayoutDelay bool
DataChannelMaxBufferedAmount uint64
DatachannelSlowThreshold int
Logger logger.Logger
PublisherHandler transport.Handler
SubscriberHandler transport.Handler
DataChannelStats *telemetry.BytesTrackStats
UseOneShotSignallingMode bool
FireOnTrackBySdp bool
SubscriberAsPrimary bool
UseSinglePeerConnection bool
Config *WebRTCConfig
Twcc *twcc.Responder
ProtocolVersion types.ProtocolVersion
CongestionControlConfig config.CongestionControlConfig
EnabledSubscribeCodecs []*livekit.Codec
EnabledPublishCodecs []*livekit.Codec
SimTracks map[uint32]interceptor.SimulcastTrackInfo
ClientInfo ClientInfo
Migration bool
AllowTCPFallback bool
TCPFallbackRTTThreshold int
AllowUDPUnstableFallback bool
TURNSEnabled bool
AllowPlayoutDelay bool
DataChannelMaxBufferedAmount uint64
DatachannelSlowThreshold int
DatachannelLossyTargetLatency time.Duration
Logger logger.Logger
PublisherHandler transport.Handler
SubscriberHandler transport.Handler
DataChannelStats *BytesTrackStats
UseOneShotSignallingMode bool
FireOnTrackBySdp bool
EnableDataTracks bool
}
type TransportManager struct {
@@ -124,9 +114,8 @@ type TransportManager struct {
signalSourceValid atomic.Bool
pendingOfferPublisher *webrtc.SessionDescription
pendingOfferIdPublisher uint32
pendingDataChannelsPublisher []*livekit.DataChannelInfo
lastPublisherAnswer atomic.Value
lastPublisherOffer atomic.Value
iceConfig *livekit.ICEConfig
mediaLossProxy *MediaLossProxy
@@ -135,7 +124,8 @@ type TransportManager struct {
onICEConfigChanged func(iceConfig *livekit.ICEConfig)
droppedBySlowReaderCount atomic.Uint32
dataChannelSendErrorDroppedBySlowReaderCount atomic.Uint32
dataChannelSendErrorCount atomic.Uint32
}
func NewTransportManager(params TransportManagerParams) (*TransportManager, error) {
@@ -150,53 +140,67 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
t.mediaLossProxy.OnMediaLossUpdate(t.onMediaLossUpdate)
lgr := LoggerWithPCTarget(params.Logger, livekit.SignalTarget_PUBLISHER)
// In single-PC mode the one PC carries both directions, so it needs both
// codec lists registered on its MediaEngine. In dual-PC mode the publisher
// PC is recvonly, so subscribe codecs are left nil.
var publisherSubscribeCodecs []*livekit.Codec
if params.UseSinglePeerConnection || params.UseOneShotSignallingMode {
publisherSubscribeCodecs = params.EnabledSubscribeCodecs
}
publisher, err := NewPCTransport(TransportParams{
ParticipantID: params.SID,
ParticipantIdentity: params.Identity,
ProtocolVersion: params.ProtocolVersion,
Config: params.Config,
Twcc: params.Twcc,
DirectionConfig: params.Config.Publisher,
CongestionControlConfig: params.CongestionControlConfig,
EnabledCodecs: params.EnabledPublishCodecs,
Logger: lgr,
SimTracks: params.SimTracks,
ClientInfo: params.ClientInfo,
Transport: livekit.SignalTarget_PUBLISHER,
Handler: TransportManagerPublisherTransportHandler{TransportManagerTransportHandler{params.PublisherHandler, t, lgr}},
UseOneShotSignallingMode: params.UseOneShotSignallingMode,
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
FireOnTrackBySdp: params.FireOnTrackBySdp,
ProtocolVersion: params.ProtocolVersion,
Config: params.Config,
Twcc: params.Twcc,
DirectionConfig: params.Config.Publisher,
CongestionControlConfig: params.CongestionControlConfig,
EnabledPublishCodecs: params.EnabledPublishCodecs,
EnabledSubscribeCodecs: publisherSubscribeCodecs,
Logger: lgr,
SimTracks: params.SimTracks,
ClientInfo: params.ClientInfo,
IsSendSide: params.UseOneShotSignallingMode || params.UseSinglePeerConnection,
AllowPlayoutDelay: params.AllowPlayoutDelay,
Transport: livekit.SignalTarget_PUBLISHER,
Handler: TransportManagerTransportHandler{params.PublisherHandler, t, lgr},
UseOneShotSignallingMode: params.UseOneShotSignallingMode,
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
DatachannelLossyTargetLatency: params.DatachannelLossyTargetLatency,
FireOnTrackBySdp: params.FireOnTrackBySdp,
EnableDataTracks: params.EnableDataTracks,
})
if err != nil {
return nil, err
}
t.publisher = publisher
lgr = LoggerWithPCTarget(params.Logger, livekit.SignalTarget_SUBSCRIBER)
subscriber, err := NewPCTransport(TransportParams{
ParticipantID: params.SID,
ParticipantIdentity: params.Identity,
ProtocolVersion: params.ProtocolVersion,
Config: params.Config,
DirectionConfig: params.Config.Subscriber,
CongestionControlConfig: params.CongestionControlConfig,
EnabledCodecs: params.EnabledSubscribeCodecs,
Logger: lgr,
ClientInfo: params.ClientInfo,
IsOfferer: true,
IsSendSide: true,
AllowPlayoutDelay: params.AllowPlayoutDelay,
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
Transport: livekit.SignalTarget_SUBSCRIBER,
Handler: TransportManagerTransportHandler{params.SubscriberHandler, t, lgr},
})
if err != nil {
return nil, err
if !t.params.UseOneShotSignallingMode && !t.params.UseSinglePeerConnection {
lgr := LoggerWithPCTarget(params.Logger, livekit.SignalTarget_SUBSCRIBER)
subscriber, err := NewPCTransport(TransportParams{
ProtocolVersion: params.ProtocolVersion,
Config: params.Config,
DirectionConfig: params.Config.Subscriber,
CongestionControlConfig: params.CongestionControlConfig,
EnabledSubscribeCodecs: params.EnabledSubscribeCodecs,
Logger: lgr,
ClientInfo: params.ClientInfo,
IsOfferer: true,
IsSendSide: true,
AllowPlayoutDelay: params.AllowPlayoutDelay,
DataChannelMaxBufferedAmount: params.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: params.DatachannelSlowThreshold,
DatachannelLossyTargetLatency: params.DatachannelLossyTargetLatency,
Transport: livekit.SignalTarget_SUBSCRIBER,
Handler: TransportManagerTransportHandler{params.SubscriberHandler, t, lgr},
FireOnTrackBySdp: params.FireOnTrackBySdp,
EnableDataTracks: params.EnableDataTracks,
})
if err != nil {
return nil, err
}
t.subscriber = subscriber
}
t.subscriber = subscriber
if !t.params.Migration {
if !t.params.Migration && t.params.SubscriberAsPrimary {
if err := t.createDataChannelsForSubscriber(nil); err != nil {
return nil, err
}
@@ -207,8 +211,12 @@ func NewTransportManager(params TransportManagerParams) (*TransportManager, erro
}
func (t *TransportManager) Close() {
t.publisher.Close()
t.subscriber.Close()
if t.publisher != nil {
t.publisher.Close()
}
if t.subscriber != nil {
t.subscriber.Close()
}
}
func (t *TransportManager) SubscriberClose() {
@@ -231,6 +239,10 @@ func (t *TransportManager) GetPublisherMid(rtpReceiver *webrtc.RTPReceiver) stri
return t.publisher.GetMid(rtpReceiver)
}
func (t *TransportManager) GetPublisherRTPTransceiver(mid string) *webrtc.RTPTransceiver {
return t.publisher.GetRTPTransceiver(mid)
}
func (t *TransportManager) GetPublisherRTPReceiver(mid string) *webrtc.RTPReceiver {
return t.publisher.GetRTPReceiver(mid)
}
@@ -240,37 +252,49 @@ func (t *TransportManager) WritePublisherRTCP(pkts []rtcp.Packet) error {
}
func (t *TransportManager) GetSubscriberRTT() (float64, bool) {
return t.subscriber.GetRTT()
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.GetRTT()
} else {
return t.subscriber.GetRTT()
}
}
func (t *TransportManager) HasSubscriberEverConnected() bool {
return t.subscriber.HasEverConnected()
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.HasEverConnected()
} else {
return t.subscriber.HasEverConnected()
}
}
func (t *TransportManager) AddTrackLocal(
trackLocal webrtc.TrackLocal,
params types.AddTrackParams,
enabledCodecs []*livekit.Codec,
rtcpFeedbackConfig RTCPFeedbackConfig,
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
if t.params.UseOneShotSignallingMode {
return t.publisher.AddTrack(trackLocal, params)
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.AddTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
} else {
return t.subscriber.AddTrack(trackLocal, params)
return t.subscriber.AddTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
}
}
func (t *TransportManager) AddTransceiverFromTrackLocal(
trackLocal webrtc.TrackLocal,
params types.AddTrackParams,
enabledCodecs []*livekit.Codec,
rtcpFeedbackConfig RTCPFeedbackConfig,
) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error) {
if t.params.UseOneShotSignallingMode {
return t.publisher.AddTransceiverFromTrack(trackLocal, params)
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
} else {
return t.subscriber.AddTransceiverFromTrack(trackLocal, params)
return t.subscriber.AddTransceiverFromTrack(trackLocal, params, enabledCodecs, rtcpFeedbackConfig)
}
}
func (t *TransportManager) RemoveTrackLocal(sender *webrtc.RTPSender) error {
if t.params.UseOneShotSignallingMode {
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.RemoveTrack(sender)
} else {
return t.subscriber.RemoveTrack(sender)
@@ -278,7 +302,7 @@ func (t *TransportManager) RemoveTrackLocal(sender *webrtc.RTPSender) error {
}
func (t *TransportManager) WriteSubscriberRTCP(pkts []rtcp.Packet) error {
if t.params.UseOneShotSignallingMode {
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.WriteRTCP(pkts)
} else {
return t.subscriber.WriteRTCP(pkts)
@@ -286,36 +310,85 @@ func (t *TransportManager) WriteSubscriberRTCP(pkts []rtcp.Packet) error {
}
func (t *TransportManager) GetSubscriberPacer() pacer.Pacer {
return t.subscriber.GetPacer()
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
return t.publisher.GetPacer()
} else {
return t.subscriber.GetPacer()
}
}
func (t *TransportManager) AddSubscribedTrack(subTrack types.SubscribedTrack) {
t.subscriber.AddTrackToStreamAllocator(subTrack)
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
t.publisher.AddTrackToStreamAllocator(subTrack)
} else {
t.subscriber.AddTrackToStreamAllocator(subTrack)
}
}
func (t *TransportManager) RemoveSubscribedTrack(subTrack types.SubscribedTrack) {
t.subscriber.RemoveTrackFromStreamAllocator(subTrack)
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
t.publisher.RemoveTrackFromStreamAllocator(subTrack)
} else {
t.subscriber.RemoveTrackFromStreamAllocator(subTrack)
}
}
func (t *TransportManager) SendDataPacket(kind livekit.DataPacket_Kind, encoded []byte) error {
func (t *TransportManager) SendDataMessage(kind livekit.DataPacket_Kind, data []byte) error {
// downstream data is sent via primary peer connection
err := t.getTransport(true).SendDataPacket(kind, encoded)
return t.handleSendDataResult(t.getTransport(true).SendDataMessage(kind, data), kind.String(), len(data))
}
func (t *TransportManager) SendDataMessageUnlabeled(data []byte, useRaw bool, sender livekit.ParticipantIdentity) error {
// downstream data is sent via primary peer connection
return t.handleSendDataResult(
t.getTransport(true).SendDataMessageUnlabeled(data, useRaw, sender),
"unlabeled",
len(data),
)
}
func (t *TransportManager) handleSendDataResult(err error, kind string, size int) error {
if err != nil {
if !utils.ErrorIsOneOf(err, io.ErrClosedPipe, sctp.ErrStreamClosed, ErrTransportFailure, ErrDataChannelBufferFull, context.DeadlineExceeded) {
if !utils.ErrorIsOneOf(
err,
io.ErrClosedPipe,
sctp.ErrStreamClosed,
ErrTransportFailure,
ErrDataChannelUnavailable,
context.DeadlineExceeded,
datachannel.ErrDataDroppedByHighBufferedAmount,
) {
if errors.Is(err, datachannel.ErrDataDroppedBySlowReader) {
droppedBySlowReaderCount := t.droppedBySlowReaderCount.Inc()
droppedBySlowReaderCount := t.dataChannelSendErrorDroppedBySlowReaderCount.Inc()
if (droppedBySlowReaderCount-1)%100 == 0 {
t.params.Logger.Infow("drop data packet by slow reader", "error", err, "kind", kind, "count", droppedBySlowReaderCount)
t.params.Logger.Infow(
"drop data message by slow reader",
"error", err,
"kind", kind,
"count", droppedBySlowReaderCount,
)
}
} else {
t.params.Logger.Warnw("send data packet error", err)
count := t.dataChannelSendErrorCount.Inc()
if (count-1)%100 == 0 {
t.params.Logger.Infow(
"send data message error",
"error", err,
"kind", kind,
"count", count,
)
}
}
}
if utils.ErrorIsOneOf(err, sctp.ErrStreamClosed, io.ErrClosedPipe) {
t.params.SubscriberHandler.OnDataSendError(err)
if t.params.SubscriberAsPrimary {
t.params.SubscriberHandler.OnDataSendError(err)
} else {
t.params.PublisherHandler.OnDataSendError(err)
}
}
} else {
t.params.DataChannelStats.AddBytes(uint64(len(encoded)), true)
t.params.DataChannelStats.AddBytes(uint64(size), true)
}
return err
@@ -323,8 +396,8 @@ func (t *TransportManager) SendDataPacket(kind livekit.DataPacket_Kind, encoded
func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels []*livekit.DataChannelInfo) error {
var (
reliableID, lossyID uint16
reliableIDPtr, lossyIDPtr *uint16
reliableID, lossyID, dataTrackID uint16
reliableIDPtr, lossyIDPtr, dataTrackIDPtr *uint16
)
//
@@ -335,13 +408,17 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
// For new version migration clients, we create data channels with new ID and negotiate with client
//
for _, dc := range pendingDataChannels {
if dc.Label == ReliableDataChannel {
// pion use step 2 for auto generated ID, so we need to add 4 to avoid conflict
reliableID = uint16(dc.Id) + 4
switch dc.Label {
case ReliableDataChannel:
// pion use step 2 for auto generated ID, so we need to add 6 to avoid conflict
reliableID = uint16(dc.Id) + 6
reliableIDPtr = &reliableID
} else if dc.Label == LossyDataChannel {
lossyID = uint16(dc.Id) + 4
case LossyDataChannel:
lossyID = uint16(dc.Id) + 6
lossyIDPtr = &lossyID
case DataTrackDataChannel:
dataTrackID = uint16(dc.Id) + 6
dataTrackIDPtr = &dataTrackID
}
}
@@ -355,6 +432,7 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
return err
}
ordered = false
retransmits := uint16(0)
negotiated = t.params.Migration && lossyIDPtr == nil
if err := t.subscriber.CreateDataChannel(LossyDataChannel, &webrtc.DataChannelInit{
@@ -365,26 +443,28 @@ func (t *TransportManager) createDataChannelsForSubscriber(pendingDataChannels [
}); err != nil {
return err
}
negotiated = t.params.Migration && dataTrackIDPtr == nil
if err := t.subscriber.CreateDataChannel(DataTrackDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
MaxRetransmits: &retransmits,
ID: dataTrackIDPtr,
Negotiated: &negotiated,
}); err != nil {
return err
}
return nil
}
func (t *TransportManager) GetUnmatchMediaForOffer(offer webrtc.SessionDescription, mediaType string) (parsed *sdp.SessionDescription, unmatched []*sdp.MediaDescription, err error) {
// prefer codec from offer for clients that don't support setCodecPreferences
parsed, err = offer.Unmarshal()
if err != nil {
t.params.Logger.Errorw("failed to parse offer for codec preference", err)
return
}
func (t *TransportManager) GetUnmatchMediaForOffer(parsedOffer *sdp.SessionDescription, mediaType string) (unmatched []*sdp.MediaDescription, err error) {
var lastMatchedMid string
lastAnswer := t.lastPublisherAnswer.Load()
if lastAnswer != nil {
answer := lastAnswer.(webrtc.SessionDescription)
parsedAnswer, err1 := answer.Unmarshal()
if lastAnswer := t.publisher.CurrentLocalDescription(); lastAnswer != nil {
parsedAnswer, err1 := lastAnswer.Unmarshal()
if err1 != nil {
// should not happen
t.params.Logger.Errorw("failed to parse last answer", err)
return
t.params.Logger.Errorw("failed to parse last answer", err1)
return unmatched, err1
}
for i := len(parsedAnswer.MediaDescriptions) - 1; i >= 0; i-- {
@@ -396,8 +476,8 @@ func (t *TransportManager) GetUnmatchMediaForOffer(offer webrtc.SessionDescripti
}
}
for i := len(parsed.MediaDescriptions) - 1; i >= 0; i-- {
media := parsed.MediaDescriptions[i]
for i := len(parsedOffer.MediaDescriptions) - 1; i >= 0; i-- {
media := parsedOffer.MediaDescriptions[i]
if media.MediaName.Media == mediaType {
mid, _ := media.Attribute(sdp.AttrKeyMID)
if mid == lastMatchedMid {
@@ -410,56 +490,76 @@ func (t *TransportManager) GetUnmatchMediaForOffer(offer webrtc.SessionDescripti
return
}
func (t *TransportManager) LastPublisherOffer() webrtc.SessionDescription {
if sd := t.lastPublisherOffer.Load(); sd != nil {
return sd.(webrtc.SessionDescription)
}
return webrtc.SessionDescription{}
func (t *TransportManager) LastPublisherOffer() *webrtc.SessionDescription {
return t.publisher.CurrentRemoteDescription()
}
func (t *TransportManager) HandleOffer(offer webrtc.SessionDescription, shouldPend bool) error {
func (t *TransportManager) LastPublisherOfferPending() *webrtc.SessionDescription {
return t.publisher.PendingRemoteDescription()
}
func (t *TransportManager) HandleOffer(offer webrtc.SessionDescription, offerId uint32, shouldPend bool) error {
t.lock.Lock()
if shouldPend {
t.pendingOfferPublisher = &offer
t.pendingOfferIdPublisher = offerId
t.lock.Unlock()
return nil
}
t.lock.Unlock()
t.lastPublisherOffer.Store(offer)
return t.publisher.HandleRemoteDescription(offer)
return t.publisher.HandleRemoteDescription(offer, offerId)
}
func (t *TransportManager) GetAnswer() (webrtc.SessionDescription, error) {
answer, err := t.publisher.GetAnswer()
if err == nil {
t.lastPublisherAnswer.Store(answer)
}
return answer, err
func (t *TransportManager) GetAnswer() (webrtc.SessionDescription, uint32, error) {
return t.publisher.GetAnswer()
}
func (t *TransportManager) GetPublisherICESessionUfrag() (string, error) {
return t.publisher.GetICESessionUfrag()
}
func (t *TransportManager) HandleICETrickleSDPFragment(sdpFragment string) error {
return t.publisher.HandleICETrickleSDPFragment(sdpFragment)
}
func (t *TransportManager) HandleICERestartSDPFragment(sdpFragment string) (string, error) {
return t.publisher.HandleICERestartSDPFragment(sdpFragment)
}
func (t *TransportManager) ProcessPendingPublisherOffer() {
t.lock.Lock()
pendingOffer := t.pendingOfferPublisher
t.pendingOfferPublisher = nil
pendingOfferId := t.pendingOfferIdPublisher
t.pendingOfferIdPublisher = 0
t.lock.Unlock()
if pendingOffer != nil {
t.HandleOffer(*pendingOffer, false)
t.HandleOffer(*pendingOffer, pendingOfferId, false)
}
}
func (t *TransportManager) HandleAnswer(answer webrtc.SessionDescription) {
t.subscriber.HandleRemoteDescription(answer)
func (t *TransportManager) HandleAnswer(answer webrtc.SessionDescription, answerId uint32) {
t.subscriber.HandleRemoteDescription(answer, answerId)
}
// AddICECandidate adds candidates for remote peer
func (t *TransportManager) AddICECandidate(candidate webrtc.ICECandidateInit, target livekit.SignalTarget) {
switch target {
case livekit.SignalTarget_PUBLISHER:
t.publisher.AddICECandidate(candidate)
if t.publisher != nil {
t.publisher.AddICECandidate(candidate)
} else {
t.params.Logger.Warnw("ice candidate for publisher, but no peer connection", nil, "candidate", candidate)
}
case livekit.SignalTarget_SUBSCRIBER:
t.subscriber.AddICECandidate(candidate)
if t.subscriber != nil {
t.subscriber.AddICECandidate(candidate)
} else {
t.params.Logger.Warnw("ice candidate for subscriber, but no peer connection", nil, "candidate", candidate)
}
default:
err := errors.New("unknown signal target")
t.params.Logger.Errorw("ice candidate for unknown signal target", err, "target", target)
@@ -467,7 +567,11 @@ func (t *TransportManager) AddICECandidate(candidate webrtc.ICECandidateInit, ta
}
func (t *TransportManager) NegotiateSubscriber(force bool) {
t.subscriber.Negotiate(force)
if t.subscriber != nil {
t.subscriber.Negotiate(force)
} else {
t.publisher.Negotiate(force)
}
}
func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason) {
@@ -478,12 +582,16 @@ func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason)
)
switch reason {
case livekit.ReconnectReason_RR_PUBLISHER_FAILED:
resetShortConnection = true
isShort, duration = t.publisher.IsShortConnection(time.Now())
if t.publisher != nil {
resetShortConnection = true
isShort, duration = t.publisher.IsShortConnection(time.Now())
}
case livekit.ReconnectReason_RR_SUBSCRIBER_FAILED:
resetShortConnection = true
isShort, duration = t.subscriber.IsShortConnection(time.Now())
if t.subscriber != nil {
resetShortConnection = true
isShort, duration = t.subscriber.IsShortConnection(time.Now())
}
}
if isShort {
@@ -495,15 +603,23 @@ func (t *TransportManager) HandleClientReconnect(reason livekit.ReconnectReason)
}
if resetShortConnection {
t.publisher.ResetShortConnOnICERestart()
t.subscriber.ResetShortConnOnICERestart()
if t.publisher != nil {
t.publisher.ResetShortConnOnICERestart()
}
if t.subscriber != nil {
t.subscriber.ResetShortConnOnICERestart()
}
}
}
func (t *TransportManager) ICERestart(iceConfig *livekit.ICEConfig) error {
t.SetICEConfig(iceConfig)
return t.subscriber.ICERestart()
if t.subscriber != nil {
return t.subscriber.ICERestart()
}
return nil
}
func (t *TransportManager) OnICEConfigChanged(f func(iceConfig *livekit.ICEConfig)) {
@@ -555,8 +671,12 @@ func (t *TransportManager) configureICE(iceConfig *livekit.ICEConfig, reset bool
t.mediaLossProxy.OnMediaLossUpdate(nil)
}
t.publisher.SetPreferTCP(iceConfig.PreferencePublisher == livekit.ICECandidateType_ICT_TCP)
t.subscriber.SetPreferTCP(iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TCP)
if t.publisher != nil {
t.publisher.SetPreferTCP(iceConfig.PreferencePublisher == livekit.ICECandidateType_ICT_TCP)
}
if t.subscriber != nil {
t.subscriber.SetPreferTCP(iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TCP)
}
if onICEConfigChanged != nil {
onICEConfigChanged(iceConfig)
@@ -570,6 +690,10 @@ func (t *TransportManager) SubscriberAsPrimary() bool {
func (t *TransportManager) GetICEConnectionInfo() []*types.ICEConnectionInfo {
infos := make([]*types.ICEConnectionInfo, 0, 2)
for _, pc := range []*PCTransport{t.publisher, t.subscriber} {
if pc == nil {
continue
}
info := pc.GetICEConnectionInfo()
if info.HasCandidates() {
infos = append(infos, info)
@@ -578,13 +702,43 @@ func (t *TransportManager) GetICEConnectionInfo() []*types.ICEConnectionInfo {
return infos
}
func (t *TransportManager) getTransport(isPrimary bool) *PCTransport {
pcTransport := t.publisher
if (isPrimary && t.params.SubscriberAsPrimary) || (!isPrimary && !t.params.SubscriberAsPrimary) {
pcTransport = t.subscriber
}
func (t *TransportManager) GetDataTrackTransport() types.DataTrackTransport {
return t.getTransport(true)
}
return pcTransport
func (t *TransportManager) getTransport(isPrimary bool) *PCTransport {
switch {
case t.publisher == nil:
return t.subscriber
case t.subscriber == nil:
return t.publisher
default:
pcTransport := t.publisher
if (isPrimary && t.params.SubscriberAsPrimary) || (!isPrimary && !t.params.SubscriberAsPrimary) {
pcTransport = t.subscriber
}
return pcTransport
}
}
func (t *TransportManager) getLowestPriorityConnectionType() types.ICEConnectionType {
switch {
case t.publisher == nil:
return t.subscriber.GetICEConnectionType()
case t.subscriber == nil:
return t.publisher.GetICEConnectionType()
default:
ctype := t.publisher.GetICEConnectionType()
if stype := t.subscriber.GetICEConnectionType(); stype > ctype {
ctype = stype
}
return ctype
}
}
func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
@@ -614,6 +768,8 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
return
}
lowestPriorityConnectionType := t.getLowestPriorityConnectionType()
//
// Checking only `PreferenceSubscriber` field although any connection failure (PUBLISHER OR SUBSCRIBER) will
// flow through here.
@@ -621,13 +777,36 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
// As both transports are switched to the same type on any failure, checking just subscriber should be fine.
//
getNext := func(ic *livekit.ICEConfig) livekit.ICECandidateType {
if ic.PreferenceSubscriber == livekit.ICECandidateType_ICT_NONE && t.params.ClientInfo.SupportsICETCP() && t.canUseICETCP() {
return livekit.ICECandidateType_ICT_TCP
} else if ic.PreferenceSubscriber != livekit.ICECandidateType_ICT_TLS && t.params.TURNSEnabled {
return livekit.ICECandidateType_ICT_TLS
} else {
return livekit.ICECandidateType_ICT_NONE
switch lowestPriorityConnectionType {
case types.ICEConnectionTypeUDP:
// try ICE/TCP if ICE/UDP failed
if ic.PreferenceSubscriber == livekit.ICECandidateType_ICT_NONE {
if t.params.ClientInfo.SupportsICETCP() && t.canUseICETCP() {
return livekit.ICECandidateType_ICT_TCP
} else if t.params.TURNSEnabled {
// fallback to TURN/TLS if TCP is not supported
return livekit.ICECandidateType_ICT_TLS
}
}
case types.ICEConnectionTypeTCP:
// try TURN/TLS if ICE/TCP failed,
// the configuration could have been ICT_NONE or ICT_TCP,
// in either case, fallback to TURN/TLS
if t.params.TURNSEnabled {
return livekit.ICECandidateType_ICT_TLS
} else {
// keep the current config
return ic.PreferenceSubscriber
}
case types.ICEConnectionTypeTURN:
// TURN/TLS is the most permissive option, if that fails there is nowhere to go to
// the configuration could have been ICT_NONE or ICT_TLS,
// keep the current config
return ic.PreferenceSubscriber
}
return livekit.ICECandidateType_ICT_NONE
}
var preferNext livekit.ICECandidateType
@@ -672,7 +851,11 @@ func (t *TransportManager) handleConnectionFailed(isShortLived bool) {
}, false)
}
func (t *TransportManager) SetMigrateInfo(previousOffer, previousAnswer *webrtc.SessionDescription, dataChannels []*livekit.DataChannelInfo) {
func (t *TransportManager) SetMigrateInfo(
previousOffer *webrtc.SessionDescription,
previousAnswer *webrtc.SessionDescription,
dataChannels []*livekit.DataChannelInfo,
) {
t.lock.Lock()
t.pendingDataChannelsPublisher = make([]*livekit.DataChannelInfo, 0, len(dataChannels))
pendingDataChannelsSubscriber := make([]*livekit.DataChannelInfo, 0, len(dataChannels))
@@ -691,7 +874,11 @@ func (t *TransportManager) SetMigrateInfo(previousOffer, previousAnswer *webrtc.
}
}
t.subscriber.SetPreviousSdp(previousOffer, previousAnswer)
if t.params.UseSinglePeerConnection {
t.publisher.SetPreviousSdp(previousAnswer, previousOffer)
} else {
t.subscriber.SetPreviousSdp(previousOffer, previousAnswer)
}
}
func (t *TransportManager) ProcessPendingPublisherDataChannels() {
@@ -710,7 +897,16 @@ func (t *TransportManager) ProcessPendingPublisherDataChannels() {
dcExisting bool
err error
)
if ci.Label == LossyDataChannel {
switch ci.Label {
case ReliableDataChannel:
id := uint16(ci.GetId())
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(ReliableDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
Negotiated: &negotiated,
ID: &id,
})
case LossyDataChannel:
ordered = false
retransmits := uint16(0)
id := uint16(ci.GetId())
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(LossyDataChannel, &webrtc.DataChannelInit{
@@ -719,12 +915,15 @@ func (t *TransportManager) ProcessPendingPublisherDataChannels() {
Negotiated: &negotiated,
ID: &id,
})
} else if ci.Label == ReliableDataChannel {
case DataTrackDataChannel:
ordered = false
retransmits := uint16(0)
id := uint16(ci.GetId())
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(ReliableDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
Negotiated: &negotiated,
ID: &id,
dcLabel, dcID, dcExisting, err = t.publisher.CreateDataChannelIfEmpty(DataTrackDataChannel, &webrtc.DataChannelInit{
Ordered: &ordered,
MaxRetransmits: &retransmits,
Negotiated: &negotiated,
ID: &id,
})
}
if err != nil {
@@ -755,7 +954,11 @@ func (t *TransportManager) onMediaLossUpdate(loss uint8) {
t.lock.Unlock()
t.params.Logger.Infow("udp connection unstable, switch to tcp", "signalingRTT", t.signalingRTT)
t.params.SubscriberHandler.OnFailed(true, t.subscriber.GetICEConnectionInfo())
if t.params.UseSinglePeerConnection {
t.params.PublisherHandler.OnFailed(true, t.publisher.GetICEConnectionInfo())
} else {
t.params.SubscriberHandler.OnFailed(true, t.subscriber.GetICEConnectionInfo())
}
return
}
}
@@ -767,8 +970,12 @@ func (t *TransportManager) UpdateSignalingRTT(rtt uint32) {
t.lock.Lock()
t.signalingRTT = rtt
t.lock.Unlock()
t.publisher.SetSignalingRTT(rtt)
t.subscriber.SetSignalingRTT(rtt)
if t.publisher != nil {
t.publisher.SetSignalingRTT(rtt)
}
if t.subscriber != nil {
t.subscriber.SetSignalingRTT(rtt)
}
// TODO: considering using tcp rtt to calculate ice connection cost, if ice connection can't be established
// within 5 * tcp rtt(at least 5s), means udp traffic might be block/dropped, switch to tcp.
@@ -813,13 +1020,25 @@ func (t *TransportManager) SetSignalSourceValid(valid bool) {
}
func (t *TransportManager) SetSubscriberAllowPause(allowPause bool) {
t.subscriber.SetAllowPauseOfStreamAllocator(allowPause)
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
t.publisher.SetAllowPauseOfStreamAllocator(allowPause)
} else {
t.subscriber.SetAllowPauseOfStreamAllocator(allowPause)
}
}
func (t *TransportManager) SetSubscriberChannelCapacity(channelCapacity int64) {
t.subscriber.SetChannelCapacityOfStreamAllocator(channelCapacity)
if t.params.UseOneShotSignallingMode || t.params.UseSinglePeerConnection {
t.publisher.SetChannelCapacityOfStreamAllocator(channelCapacity)
} else {
t.subscriber.SetChannelCapacityOfStreamAllocator(channelCapacity)
}
}
func (t *TransportManager) hasRecentSignalLocked() bool {
return time.Since(t.lastSignalAt) < PingTimeoutSeconds*time.Second
}
func (t *TransportManager) RTPStreamPublished(ssrc uint32, mid, rid string) {
t.publisher.RTPStreamPublished(ssrc, mid, rid)
}
+149 -103
View File
@@ -18,30 +18,63 @@ package types
import (
"fmt"
"slices"
"strings"
"sync"
"github.com/pion/ice/v4"
"github.com/pion/webrtc/v4"
"golang.org/x/exp/slices"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/observability/roomobs"
)
type ICEConnectionType string
type ICEConnectionType int
const (
ICEConnectionTypeUDP ICEConnectionType = "udp"
ICEConnectionTypeTCP ICEConnectionType = "tcp"
ICEConnectionTypeTURN ICEConnectionType = "turn"
ICEConnectionTypeUnknown ICEConnectionType = "unknown"
// 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 {
// only one of local or remote is set. This is due to type foo in Pion
Local *webrtc.ICECandidate
Remote ice.Candidate
Candidate *webrtc.ICECandidate
SelectedOrder int
Filtered bool
Trickle bool
@@ -60,6 +93,15 @@ 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 {
@@ -91,7 +133,7 @@ func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
}
for _, c := range d.Local {
info.Local = append(info.Local, &ICECandidateExtended{
Local: c.Local,
Candidate: c.Candidate,
Filtered: c.Filtered,
SelectedOrder: c.SelectedOrder,
Trickle: c.Trickle,
@@ -99,7 +141,7 @@ func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
}
for _, c := range d.Remote {
info.Remote = append(info.Remote, &ICECandidateExtended{
Remote: c.Remote,
Candidate: c.Candidate,
Filtered: c.Filtered,
SelectedOrder: c.SelectedOrder,
Trickle: c.Trickle,
@@ -108,19 +150,26 @@ func (d *ICEConnectionDetails) GetInfo() *ICEConnectionInfo {
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.Local, c)
return isCandidateEqualTo(e.Candidate, c)
}
if slices.ContainsFunc(d.Local, compFn) {
return
}
d.Local = append(d.Local, &ICECandidateExtended{
Local: c,
Filtered: filtered,
Trickle: trickle,
Candidate: c,
Filtered: filtered,
Trickle: trickle,
})
}
@@ -135,24 +184,30 @@ func (d *ICEConnectionDetails) AddLocalICECandidate(c ice.Candidate, filtered, t
}
func (d *ICEConnectionDetails) AddRemoteCandidate(c webrtc.ICECandidateInit, filtered, trickle, canUpdate bool) {
candidate, err := unmarshalICECandidate(c)
iceCandidate, err := unmarshalICECandidate(c)
if err != nil {
d.logger.Errorw("could not unmarshal candidate", err, "candidate", c)
return
}
d.AddRemoteICECandidate(candidate, filtered, trickle, canUpdate)
d.AddRemoteICECandidate(iceCandidate, filtered, trickle, canUpdate)
}
func (d *ICEConnectionDetails) AddRemoteICECandidate(candidate ice.Candidate, filtered, trickle, canUpdate bool) {
if candidate == nil {
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 isICECandidateEqualTo(e.Remote, candidate)
return isCandidateEqualTo(e.Candidate, candidate)
}
if idx := slices.IndexFunc(d.Remote, indexFn); idx != -1 {
if canUpdate {
@@ -162,10 +217,11 @@ func (d *ICEConnectionDetails) AddRemoteICECandidate(candidate ice.Candidate, fi
return
}
d.Remote = append(d.Remote, &ICECandidateExtended{
Remote: candidate,
Filtered: filtered,
Trickle: trickle,
Candidate: candidate,
Filtered: filtered,
Trickle: trickle,
})
d.updateConnectionTypeLocked()
}
func (d *ICEConnectionDetails) Clear() {
@@ -183,56 +239,78 @@ func (d *ICEConnectionDetails) SetSelectedPair(pair *webrtc.ICECandidatePair) {
d.selectedCount++
remoteIdx := slices.IndexFunc(d.Remote, func(e *ICECandidateExtended) bool {
return isICECandidateEqualToCandidate(e.Remote, pair.Remote)
return isCandidateEqualTo(e.Candidate, pair.Remote)
})
if remoteIdx < 0 {
// it's possible for prflx candidates to be generated by Pion, we'll add them
candidate, err := unmarshalICECandidate(pair.Remote.ToJSON())
if err != nil {
d.logger.Errorw("could not unmarshal remote candidate", err, "candidate", pair.Remote)
return
}
if candidate == nil {
return
}
d.Remote = append(d.Remote, &ICECandidateExtended{
Remote: candidate,
Filtered: false,
Trickle: true,
Candidate: pair.Remote,
Filtered: false,
Trickle: false,
})
remoteIdx = len(d.Remote) - 1
}
remote := d.Remote[remoteIdx]
remote.SelectedOrder = d.selectedCount
d.Remote[remoteIdx].SelectedOrder = d.selectedCount
d.updateConnectionTypeLocked()
localIdx := slices.IndexFunc(d.Local, func(e *ICECandidateExtended) bool {
return isCandidateEqualTo(e.Local, pair.Local)
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
}
local := d.Local[localIdx]
local.SelectedOrder = d.selectedCount
d.Local[localIdx].SelectedOrder = d.selectedCount
}
d.Type = ICEConnectionTypeUDP
if pair.Remote.Protocol == webrtc.ICEProtocolTCP {
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
}
if pair.Remote.Typ == webrtc.ICECandidateTypeRelay {
switch remoteCandidate.Typ {
case webrtc.ICECandidateTypeRelay:
d.Type = ICEConnectionTypeTURN
} else if pair.Remote.Typ == webrtc.ICECandidateTypePrflx {
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.Remote
if or.Type() == ice.CandidateTypeRelay &&
pair.Remote.Address == or.Address() &&
pair.Remote.Port == uint16(or.Port()) &&
pair.Remote.Protocol.String() == or.NetworkType().NetworkShort() {
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
}
}
}
@@ -258,39 +336,6 @@ func isCandidateEqualTo(c1, c2 *webrtc.ICECandidate) bool {
c1.TCPType == c2.TCPType
}
func isICECandidateEqualTo(c1, c2 ice.Candidate) bool {
if c1 == nil && c2 == nil {
return true
}
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
return false
}
return c1.Type() == c2.Type() &&
c1.NetworkType() == c2.NetworkType() &&
c1.Address() == c2.Address() &&
c1.Port() == c2.Port() &&
c1.Foundation() == c2.Foundation() &&
c1.Priority() == c2.Priority() &&
c1.RelatedAddress().Equal(c2.RelatedAddress()) &&
c1.TCPType() == c2.TCPType()
}
func isICECandidateEqualToCandidate(c1 ice.Candidate, c2 *webrtc.ICECandidate) bool {
if c1 == nil && c2 == nil {
return true
}
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
return false
}
return c1.Type().String() == c2.Typ.String() &&
c1.NetworkType().NetworkShort() == c2.Protocol.String() &&
c1.Address() == c2.Address &&
c1.Port() == int(c2.Port) &&
c1.Foundation() == c2.Foundation &&
c1.Priority() == c2.Priority &&
c1.TCPType().String() == c2.TCPType
}
func unmarshalICECandidate(c webrtc.ICECandidateInit) (ice.Candidate, error) {
candidateValue := strings.TrimPrefix(c.Candidate, "candidate:")
if candidateValue == "" {
@@ -306,28 +351,14 @@ func unmarshalICECandidate(c webrtc.ICECandidateInit) (ice.Candidate, error) {
}
func unmarshalCandidate(i ice.Candidate) (*webrtc.ICECandidate, error) {
var typ webrtc.ICECandidateType
switch i.Type() {
case ice.CandidateTypeHost:
typ = webrtc.ICECandidateTypeHost
case ice.CandidateTypeServerReflexive:
typ = webrtc.ICECandidateTypeSrflx
case ice.CandidateTypePeerReflexive:
typ = webrtc.ICECandidateTypePrflx
case ice.CandidateTypeRelay:
typ = webrtc.ICECandidateTypeRelay
default:
return nil, fmt.Errorf("unknown candidate type: %s", i.Type())
typ, err := convertTypeFromICE(i.Type())
if err != nil {
return nil, err
}
var protocol webrtc.ICEProtocol
switch strings.ToLower(i.NetworkType().NetworkShort()) {
case "udp":
protocol = webrtc.ICEProtocolUDP
case "tcp":
protocol = webrtc.ICEProtocolTCP
default:
return nil, fmt.Errorf("unknown network type: %s", i.NetworkType())
protocol, err := webrtc.NewICEProtocol(i.NetworkType().NetworkShort())
if err != nil {
return nil, err
}
c := webrtc.ICECandidate{
@@ -349,6 +380,21 @@ func unmarshalCandidate(i ice.Candidate) (*webrtc.ICECandidate, error) {
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 {
@@ -364,5 +410,5 @@ func IsICECandidateMDNS(candidate ice.Candidate) bool {
return false
}
return strings.HasSuffix(candidate.Address(), ".local")
return strings.HasSuffix(candidate.Address(), ".local") || strings.HasSuffix(candidate.Address(), ".invalid")
}
+361 -48
View File
@@ -22,15 +22,20 @@ import (
"github.com/pion/webrtc/v4"
"github.com/livekit/protocol/auth"
"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/routing"
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/livekit-server/pkg/sfu/pacer"
"github.com/livekit/livekit-server/pkg/telemetry"
"google.golang.org/protobuf/proto"
)
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
@@ -111,6 +116,8 @@ const (
ParticipantCloseReasonRoomClosed
ParticipantCloseReasonUserUnavailable
ParticipantCloseReasonUserRejected
ParticipantCloseReasonMoveFailed
ParticipantCloseReasonAgentError
)
func (p ParticipantCloseReason) String() string {
@@ -169,6 +176,10 @@ func (p ParticipantCloseReason) String() string {
return "USER_UNAVAILABLE"
case ParticipantCloseReasonUserRejected:
return "USER_REJECTED"
case ParticipantCloseReasonMoveFailed:
return "MOVE_FAILED"
case ParticipantCloseReasonAgentError:
return "AGENT_ERROR"
default:
return fmt.Sprintf("%d", int(p))
}
@@ -184,7 +195,7 @@ func (p ParticipantCloseReason) ToDisconnectReason() livekit.DisconnectReason {
// expected to be connected but is not
return livekit.DisconnectReason_JOIN_FAILURE
case ParticipantCloseReasonPeerConnectionDisconnected:
return livekit.DisconnectReason_STATE_MISMATCH
return livekit.DisconnectReason_CONNECTION_TIMEOUT
case ParticipantCloseReasonDuplicateIdentity, ParticipantCloseReasonStale:
return livekit.DisconnectReason_DUPLICATE_IDENTITY
case ParticipantCloseReasonMigrationRequested, ParticipantCloseReasonMigrationComplete, ParticipantCloseReasonSimulateMigration:
@@ -195,7 +206,8 @@ func (p ParticipantCloseReason) ToDisconnectReason() livekit.DisconnectReason {
return livekit.DisconnectReason_ROOM_DELETED
case ParticipantCloseReasonSimulateNodeFailure, ParticipantCloseReasonSimulateServerLeave:
return livekit.DisconnectReason_SERVER_SHUTDOWN
case ParticipantCloseReasonNegotiateFailed, ParticipantCloseReasonPublicationError, ParticipantCloseReasonSubscriptionError, ParticipantCloseReasonDataChannelError, ParticipantCloseReasonMigrateCodecMismatch:
case ParticipantCloseReasonNegotiateFailed, ParticipantCloseReasonPublicationError, ParticipantCloseReasonSubscriptionError,
ParticipantCloseReasonDataChannelError, ParticipantCloseReasonMigrateCodecMismatch, ParticipantCloseReasonMoveFailed:
return livekit.DisconnectReason_STATE_MISMATCH
case ParticipantCloseReasonSignalSourceClose:
return livekit.DisconnectReason_SIGNAL_CLOSE
@@ -205,12 +217,31 @@ func (p ParticipantCloseReason) ToDisconnectReason() livekit.DisconnectReason {
return livekit.DisconnectReason_USER_UNAVAILABLE
case ParticipantCloseReasonUserRejected:
return livekit.DisconnectReason_USER_REJECTED
case ParticipantCloseReasonAgentError:
return livekit.DisconnectReason_AGENT_ERROR
default:
// the other types will map to unknown reason
return livekit.DisconnectReason_UNKNOWN_REASON
}
}
// IsIntentionalDisconnect reports whether a disconnect reason represents an
// intentional/expected closure (client leaving, admin action, room teardown,
// migration, etc.) as opposed to a connection failure.
func IsIntentionalDisconnect(reason livekit.DisconnectReason) bool {
switch reason {
case livekit.DisconnectReason_CLIENT_INITIATED,
livekit.DisconnectReason_SERVER_SHUTDOWN,
livekit.DisconnectReason_DUPLICATE_IDENTITY,
livekit.DisconnectReason_MIGRATION,
livekit.DisconnectReason_PARTICIPANT_REMOVED,
livekit.DisconnectReason_ROOM_DELETED,
livekit.DisconnectReason_ROOM_CLOSED:
return true
}
return false
}
// ---------------------------------------------
type SignallingCloseReason int
@@ -258,6 +289,12 @@ func (s SignallingCloseReason) String() string {
}
}
// ---------------------------------------------
const (
ParticipantCloseKeyNormal = "normal"
ParticipantCloseKeyWHIP = "whip"
)
// ---------------------------------------------
//counterfeiter:generate . Participant
@@ -268,18 +305,26 @@ type Participant interface {
ConnectedAt() time.Time
CloseReason() ParticipantCloseReason
Kind() livekit.ParticipantInfo_Kind
KindDetails() []livekit.ParticipantInfo_KindDetail
IsRecorder() bool
IsDependent() bool
IsAgent() bool
GetLogger() logger.Logger
CanSkipBroadcast() bool
Version() utils.TimedVersion
ToProto() *livekit.ParticipantInfo
ToProtoWithVersion() (*livekit.ParticipantInfo, utils.TimedVersion)
IsPublisher() bool
GetPublishedTrack(trackID livekit.TrackID) MediaTrack
GetPublishedTracks() []MediaTrack
RemovePublishedTrack(track MediaTrack, isExpectedToResume bool, shouldClose bool)
RemovePublishedTrack(track MediaTrack, isExpectedToResume bool)
GetPublishedDataTracks() []DataTrack
GetPublishedDataTrack(handle uint16) DataTrack
RemovePublishedDataTrack(track DataTrack)
GetAudioLevel() (smoothedLevel float64, active bool)
@@ -290,7 +335,11 @@ type Participant interface {
// permissions
Hidden() bool
MigrateState() MigrateState
Close(sendLeave bool, reason ParticipantCloseReason, isExpectedToResume bool) error
IsClosed() bool
IsDisconnected() bool
SubscriptionPermission() (*livekit.SubscriptionPermission, utils.TimedVersion)
@@ -301,9 +350,11 @@ type Participant interface {
resolverBySid func(participantID livekit.ParticipantID) LocalParticipant,
) error
DebugInfo() map[string]interface{}
DebugInfo() map[string]any
OnMetrics(callback func(Participant, *livekit.DataPacket))
HandleReceivedDataTrackMessage([]byte, *datatrack.Packet, int64)
GetParticipantListener() ParticipantListener
}
// -------------------------------------------------------
@@ -313,22 +364,51 @@ type AddTrackParams struct {
Red bool
}
type MoveToRoomParams struct {
RoomName livekit.RoomName
ParticipantID livekit.ParticipantID
Listener LocalParticipantListener
Helper LocalParticipantHelper
}
type DataMessageCache struct {
Data []byte
SenderID livekit.ParticipantID
Seq uint32
DestIdentities []livekit.ParticipantIdentity
}
//counterfeiter:generate . LocalParticipantHelper
type LocalParticipantHelper interface {
ResolveMediaTrack(LocalParticipant, livekit.TrackID) MediaResolverResult
ResolveDataTrack(LocalParticipant, livekit.TrackID) DataResolverResult
GetParticipantInfo(pID livekit.ParticipantID) *livekit.ParticipantInfo
GetRegionSettings(ip string) *livekit.RegionSettings
GetSubscriberForwarderState(p LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error)
ShouldRegressCodec() bool
GetCachedReliableDataMessage(seqs map[livekit.ParticipantID]uint32) []*DataMessageCache
}
//counterfeiter:generate . LocalParticipant
type LocalParticipant interface {
Participant
ToProtoWithVersion() (*livekit.ParticipantInfo, utils.TimedVersion)
TelemetryGuard() *telemetry.ReferenceGuard
GetTelemetryListener() ParticipantTelemetryListener
// getters
GetCountry() string
GetTrailer() []byte
GetLogger() logger.Logger
GetLoggerResolver() logger.DeferredFieldResolver
GetReporter() roomobs.ParticipantSessionReporter
GetReporterResolver() roomobs.ParticipantReporterResolver
GetAdaptiveStream() bool
ProtocolVersion() ProtocolVersion
SupportsSyncStreamID() bool
SupportsTransceiverReuse() bool
IsClosed() bool
SupportsTransceiverReuse(mt MediaTrack) bool
IsUsingSinglePeerConnection() bool
IsReady() bool
IsDisconnected() bool
ActiveAt() time.Time
Disconnected() <-chan struct{}
IsIdle() bool
SubscriberAsPrimary() bool
@@ -340,15 +420,19 @@ type LocalParticipant interface {
GetICEConnectionInfo() []*ICEConnectionInfo
HasConnected() bool
GetEnabledPublishCodecs() []*livekit.Codec
GetPublisherICESessionUfrag() (string, error)
SupportsMoving() error
GetLastReliableSequence(migrateOut bool) uint32
SetResponseSink(sink routing.MessageSink)
SwapResponseSink(sink routing.MessageSink, reason SignallingCloseReason)
GetResponseSink() routing.MessageSink
CloseSignalConnection(reason SignallingCloseReason)
UpdateLastSeenSignal()
SetSignalSourceValid(valid bool)
HandleSignalSourceClose()
// updates
CheckMetadataLimits(name string, metadata string, attributes map[string]string) error
UpdateMetadata(update *livekit.UpdateParticipantMetadata, fromAdmin bool) error
SetName(name string)
SetMetadata(metadata string)
SetAttributes(attributes map[string]string)
@@ -364,13 +448,15 @@ type LocalParticipant interface {
CanPublishData() bool
// PeerConnection
AddICECandidate(candidate webrtc.ICECandidateInit, target livekit.SignalTarget)
HandleOffer(sdp webrtc.SessionDescription) error
GetAnswer() (webrtc.SessionDescription, error)
HandleICETrickle(trickleRequest *livekit.TrickleRequest)
HandleOffer(sd *livekit.SessionDescription) error
GetAnswer() (webrtc.SessionDescription, uint32, error)
HandleICETrickleSDPFragment(sdpFragment string) error
HandleICERestartSDPFragment(sdpFragment string) (string, error)
AddTrack(req *livekit.AddTrackRequest)
SetTrackMuted(trackID livekit.TrackID, muted bool, fromAdmin bool) *livekit.TrackInfo
SetTrackMuted(mute *livekit.MuteTrackRequest, fromAdmin bool) *livekit.TrackInfo
HandleAnswer(sdp webrtc.SessionDescription)
HandleAnswer(sd *livekit.SessionDescription)
Negotiate(force bool)
ICERestart(iceConfig *livekit.ICEConfig)
AddTrackLocal(trackLocal webrtc.TrackLocal, params AddTrackParams) (*webrtc.RTPSender, *webrtc.RTPTransceiver, error)
@@ -380,11 +466,14 @@ type LocalParticipant interface {
WriteSubscriberRTCP(pkts []rtcp.Packet) error
// subscriptions
SubscribeToTrack(trackID livekit.TrackID)
SubscribeToTrack(trackID livekit.TrackID, isSync bool)
UnsubscribeFromTrack(trackID livekit.TrackID)
UpdateSubscribedTrackSettings(trackID livekit.TrackID, settings *livekit.UpdateTrackSettings)
GetSubscribedTracks() []SubscribedTrack
IsTrackNameSubscribed(publisherIdentity livekit.ParticipantIdentity, trackName string) bool
SubscribeToDataTrack(trackID livekit.TrackID)
UnsubscribeFromDataTrack(trackID livekit.TrackID)
UpdateDataTrackSubscriptionOptions(trackID livekit.TrackID, subscriptionOptions *livekit.DataTrackSubscriptionOptions)
Verify() bool
VerifySubscribeParticipantInfo(pID livekit.ParticipantID, version uint32)
// WaitUntilSubscribed waits until all subscriptions have been settled, or if the timeout
@@ -403,29 +492,18 @@ type LocalParticipant interface {
SendJoinResponse(joinResponse *livekit.JoinResponse) error
SendParticipantUpdate(participants []*livekit.ParticipantInfo) error
SendSpeakerUpdate(speakers []*livekit.SpeakerInfo, force bool) error
SendDataPacket(kind livekit.DataPacket_Kind, encoded []byte) error
SendDataMessage(kind livekit.DataPacket_Kind, data []byte, senderID livekit.ParticipantID, seq uint32) error
SendDataMessageUnlabeled(data []byte, useRaw bool, sender livekit.ParticipantIdentity) error
SendRoomUpdate(room *livekit.Room) error
SendConnectionQualityUpdate(update *livekit.ConnectionQualityUpdate) error
SubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool)
SendSubscriptionPermissionUpdate(publisherID livekit.ParticipantID, trackID livekit.TrackID, allowed bool) error
SendRefreshToken(token string) error
SendRequestResponse(requestResponse *livekit.RequestResponse) error
HandleReconnectAndSendResponse(reconnectReason livekit.ReconnectReason, reconnectResponse *livekit.ReconnectResponse) error
IssueFullReconnect(reason ParticipantCloseReason)
SendRoomMovedResponse(moved *livekit.RoomMovedResponse) error
SendDataTrackSubscriberHandles(handles map[uint32]*livekit.DataTrackSubscriberHandles_PublishedDataTrack) error
// callbacks
OnStateChange(func(p LocalParticipant, state livekit.ParticipantInfo_State))
OnMigrateStateChange(func(p LocalParticipant, migrateState MigrateState))
// OnTrackPublished - remote added a track
OnTrackPublished(func(LocalParticipant, MediaTrack))
// OnTrackUpdated - one of its publishedTracks changed in status
OnTrackUpdated(callback func(LocalParticipant, MediaTrack))
// OnTrackUnpublished - a track was unpublished
OnTrackUnpublished(callback func(LocalParticipant, MediaTrack))
// OnParticipantUpdate - metadata or permission is updated
OnParticipantUpdate(callback func(LocalParticipant))
OnDataPacket(callback func(LocalParticipant, livekit.DataPacket_Kind, *livekit.DataPacket))
OnSubscribeStatusChanged(fn func(publisherID livekit.ParticipantID, subscribed bool))
OnClose(callback func(LocalParticipant))
AddOnClose(key string, callback func(LocalParticipant))
OnClaimsChanged(callback func(LocalParticipant))
HandleReceiverReport(dt *sfu.DownTrack, report *rtcp.ReceiverReport)
@@ -434,13 +512,16 @@ type LocalParticipant interface {
MaybeStartMigration(force bool, onStart func()) bool
NotifyMigration()
SetMigrateState(s MigrateState)
MigrateState() MigrateState
SetMigrateInfo(
previousOffer, previousAnswer *webrtc.SessionDescription,
previousOffer *webrtc.SessionDescription,
previousAnswer *webrtc.SessionDescription,
mediaTracks []*livekit.TrackPublishedResponse,
dataChannels []*livekit.DataChannelInfo,
dataChannelReceiveState []*livekit.DataChannelReceiveState,
dataTracks []*livekit.PublishDataTrackResponse,
)
IsReconnect() bool
MoveToRoom(params MoveToRoomParams)
UpdateMediaRTT(rtt uint32)
UpdateSignalingRTT(rtt uint32)
@@ -454,6 +535,7 @@ type LocalParticipant interface {
OnICEConfigChanged(callback func(participant LocalParticipant, iceConfig *livekit.ICEConfig))
UpdateSubscribedQuality(nodeID livekit.NodeID, trackID livekit.TrackID, maxQualities []SubscribedCodecQuality) error
UpdateSubscribedAudioCodecs(nodeID livekit.NodeID, trackID livekit.TrackID, codecs []*livekit.SubscribedAudioCodec) error
UpdateMediaLoss(nodeID livekit.NodeID, trackID livekit.TrackID, fractionalLoss uint32) error
// down stream bandwidth management
@@ -465,8 +547,177 @@ type LocalParticipant interface {
GetDisableSenderReportPassThrough() bool
HandleMetrics(senderParticipantID livekit.ParticipantID, batch *livekit.MetricsBatch) error
HandleUpdateSubscriptions(
[]livekit.TrackID,
[]*livekit.ParticipantTracks,
bool,
)
HandleUpdateSubscriptionPermission(*livekit.SubscriptionPermission) error
HandleSyncState(*livekit.SyncState) error
HandleSimulateScenario(*livekit.SimulateScenario) error
HandleLeaveRequest(reason ParticipantCloseReason)
HandlePublishDataTrackRequest(*livekit.PublishDataTrackRequest)
HandleUnpublishDataTrackRequest(*livekit.UnpublishDataTrackRequest)
HandleUpdateDataSubscription(*livekit.UpdateDataSubscription)
HandleSignalMessage(msg proto.Message) error
PerformRpc(req *livekit.PerformRpcRequest, resultCh chan string, errorCh chan error)
GetDataTrackTransport() DataTrackTransport
ClearParticipantListener()
GetNextSubscribedDataTrackHandle() uint16
}
// ---------------------------------------------
//counterfeiter:generate . ParticipantListener
type ParticipantListener interface {
OnParticipantUpdate(Participant)
OnTrackPublished(Participant, MediaTrack)
OnTrackUpdated(Participant, MediaTrack)
OnTrackUnpublished(Participant, MediaTrack)
OnDataTrackPublished(Participant, DataTrack)
OnDataTrackUnpublished(Participant, DataTrack)
OnDataTrackMessage(Participant, []byte, *datatrack.Packet)
OnMetrics(Participant, *livekit.DataPacket)
}
var _ ParticipantListener = (*NullParticipantListener)(nil)
type NullParticipantListener struct{}
func (*NullParticipantListener) OnParticipantUpdate(Participant) {}
func (*NullParticipantListener) OnTrackPublished(Participant, MediaTrack) {}
func (*NullParticipantListener) OnTrackUpdated(Participant, MediaTrack) {}
func (*NullParticipantListener) OnTrackUnpublished(Participant, MediaTrack) {}
func (*NullParticipantListener) OnDataTrackPublished(Participant, DataTrack) {}
func (*NullParticipantListener) OnDataTrackUnpublished(Participant, DataTrack) {}
func (*NullParticipantListener) OnDataTrackMessage(Participant, []byte, *datatrack.Packet) {}
func (*NullParticipantListener) OnMetrics(Participant, *livekit.DataPacket) {}
// ---------------------------------------------
//counterfeiter:generate . LocalParticipantListener
type LocalParticipantListener interface {
ParticipantListener
OnStateChange(LocalParticipant)
OnSubscriberReady(LocalParticipant)
OnMigrateStateChange(LocalParticipant, MigrateState)
OnDataMessage(LocalParticipant, livekit.DataPacket_Kind, *livekit.DataPacket)
OnDataMessageUnlabeled(LocalParticipant, []byte)
OnSubscribeStatusChanged(LocalParticipant, livekit.ParticipantID, bool)
OnUpdateSubscriptions(
LocalParticipant,
[]livekit.TrackID,
[]*livekit.ParticipantTracks,
bool,
)
OnUpdateSubscriptionPermission(LocalParticipant, *livekit.SubscriptionPermission) error
OnUpdateDataSubscriptions(LocalParticipant, *livekit.UpdateDataSubscription)
OnSyncState(LocalParticipant, *livekit.SyncState) error
OnSimulateScenario(LocalParticipant, *livekit.SimulateScenario) error
OnLeave(LocalParticipant, ParticipantCloseReason)
}
var _ LocalParticipantListener = (*NullLocalParticipantListener)(nil)
type NullLocalParticipantListener struct {
NullParticipantListener
}
func (*NullLocalParticipantListener) OnStateChange(LocalParticipant) {}
func (*NullLocalParticipantListener) OnSubscriberReady(LocalParticipant) {}
func (*NullLocalParticipantListener) OnMigrateStateChange(LocalParticipant, MigrateState) {}
func (*NullLocalParticipantListener) OnDataMessage(LocalParticipant, livekit.DataPacket_Kind, *livekit.DataPacket) {
}
func (*NullLocalParticipantListener) OnDataMessageUnlabeled(LocalParticipant, []byte) {}
func (*NullLocalParticipantListener) OnSubscribeStatusChanged(LocalParticipant, livekit.ParticipantID, bool) {
}
func (*NullLocalParticipantListener) OnUpdateSubscriptions(
LocalParticipant,
[]livekit.TrackID,
[]*livekit.ParticipantTracks,
bool,
) {
}
func (*NullLocalParticipantListener) OnUpdateSubscriptionPermission(LocalParticipant, *livekit.SubscriptionPermission) error {
return nil
}
func (*NullLocalParticipantListener) OnUpdateDataSubscriptions(LocalParticipant, *livekit.UpdateDataSubscription) {
}
func (*NullLocalParticipantListener) OnSyncState(LocalParticipant, *livekit.SyncState) error {
return nil
}
func (*NullLocalParticipantListener) OnSimulateScenario(LocalParticipant, *livekit.SimulateScenario) error {
return nil
}
func (*NullLocalParticipantListener) OnLeave(LocalParticipant, ParticipantCloseReason) {}
// ---------------------------------------------
//counterfeiter:generate . ParticipantTelemetryListener
type ParticipantTelemetryListener interface {
OnTrackPublishRequested(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo)
OnTrackPublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool)
OnTrackUnpublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool)
OnTrackSubscribeRequested(pID livekit.ParticipantID, ti *livekit.TrackInfo)
OnTrackSubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, publisherInfo *livekit.ParticipantInfo, shouldSendEvent bool)
OnTrackUnsubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, shouldSendEvent bool)
OnTrackSubscribeFailed(pID livekit.ParticipantID, trackID livekit.TrackID, err error, isUserError bool)
OnTrackSubscribeStreamStarted(pID livekit.ParticipantID, ti *livekit.TrackInfo)
OnTrackMuted(pID livekit.ParticipantID, ti *livekit.TrackInfo)
OnTrackUnmuted(pID livekit.ParticipantID, ti *livekit.TrackInfo)
OnTrackPublishedUpdate(pID livekit.ParticipantID, ti *livekit.TrackInfo)
OnTrackMaxSubscribedVideoQuality(pID livekit.ParticipantID, ti *livekit.TrackInfo, mime mime.MimeType, maxQuality livekit.VideoQuality)
OnTrackPublishRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, layer int, stats *livekit.RTPStats)
OnTrackSubscribeRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, stats *livekit.RTPStats)
OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat)
}
var _ ParticipantTelemetryListener = (*NullParticipantTelemetryListener)(nil)
type NullParticipantTelemetryListener struct{}
func (NullParticipantTelemetryListener) OnTrackPublishRequested(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo) {
}
func (NullParticipantTelemetryListener) OnTrackPublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool) {
}
func (NullParticipantTelemetryListener) OnTrackUnpublished(pID livekit.ParticipantID, identity livekit.ParticipantIdentity, ti *livekit.TrackInfo, shouldSendEvent bool) {
}
func (NullParticipantTelemetryListener) OnTrackSubscribeRequested(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
}
func (NullParticipantTelemetryListener) OnTrackSubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, publisherInfo *livekit.ParticipantInfo, shouldSendEvent bool) {
}
func (NullParticipantTelemetryListener) OnTrackUnsubscribed(pID livekit.ParticipantID, ti *livekit.TrackInfo, shouldSendEvent bool) {
}
func (NullParticipantTelemetryListener) OnTrackSubscribeFailed(pID livekit.ParticipantID, trackID livekit.TrackID, err error, isUserError bool) {
}
func (NullParticipantTelemetryListener) OnTrackSubscribeStreamStarted(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
}
func (NullParticipantTelemetryListener) OnTrackMuted(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
}
func (NullParticipantTelemetryListener) OnTrackUnmuted(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
}
func (NullParticipantTelemetryListener) OnTrackPublishedUpdate(pID livekit.ParticipantID, ti *livekit.TrackInfo) {
}
func (NullParticipantTelemetryListener) OnTrackMaxSubscribedVideoQuality(pID livekit.ParticipantID, ti *livekit.TrackInfo, mime mime.MimeType, maxQuality livekit.VideoQuality) {
}
func (NullParticipantTelemetryListener) OnTrackPublishRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, layer int, stats *livekit.RTPStats) {
}
func (NullParticipantTelemetryListener) OnTrackSubscribeRTPStats(pID livekit.ParticipantID, trackID livekit.TrackID, mimeType mime.MimeType, stats *livekit.RTPStats) {
}
func (NullParticipantTelemetryListener) OnTrackStats(key telemetry.StatsKey, stat *livekit.AnalyticsStat) {
}
// ---------------------------------------------
// Room is a container of participants, and can provide room-level actions
//
//counterfeiter:generate . Room
@@ -474,11 +725,14 @@ type Room interface {
Name() livekit.RoomName
ID() livekit.RoomID
RemoveParticipant(identity livekit.ParticipantIdentity, pID livekit.ParticipantID, reason ParticipantCloseReason)
UpdateSubscriptions(participant LocalParticipant, trackIDs []livekit.TrackID, participantTracks []*livekit.ParticipantTracks, subscribe bool)
UpdateSubscriptionPermission(participant LocalParticipant, permissions *livekit.SubscriptionPermission) error
SyncState(participant LocalParticipant, state *livekit.SyncState) error
SimulateScenario(participant LocalParticipant, scenario *livekit.SimulateScenario) error
UpdateSubscriptions(
participant LocalParticipant,
trackIDs []livekit.TrackID,
participantTracks []*livekit.ParticipantTracks,
subscribe bool,
)
ResolveMediaTrackForSubscriber(sub LocalParticipant, trackID livekit.TrackID) MediaResolverResult
ResolveDataTrackForSubscriber(sub LocalParticipant, trackID livekit.TrackID) DataResolverResult
GetLocalParticipants() []LocalParticipant
IsDataMessageUserPacketDuplicate(ip *livekit.UserPacket) bool
}
@@ -501,12 +755,11 @@ type MediaTrack interface {
PublisherID() livekit.ParticipantID
PublisherIdentity() livekit.ParticipantIdentity
PublisherVersion() uint32
Logger() logger.Logger
IsMuted() bool
SetMuted(muted bool)
IsSimulcast() bool
GetAudioLevel() (level float64, active bool)
Close(isExpectedToResume bool)
@@ -525,15 +778,16 @@ type MediaTrack interface {
OnTrackSubscribed()
// returns quality information that's appropriate for width & height
GetQualityForDimension(width, height uint32) livekit.VideoQuality
GetQualityForDimension(mimeType mime.MimeType, width, height uint32) livekit.VideoQuality
// returns temporal layer that's appropriate for fps
GetTemporalLayerForSpatialFps(spatial int32, fps uint32, mime mime.MimeType) int32
GetTemporalLayerForSpatialFps(mimeType mime.MimeType, spatial int32, fps uint32) int32
Receivers() []sfu.TrackReceiver
ClearAllReceivers(isExpectedToResume bool)
IsEncrypted() bool
HasPacketTrailer() bool
}
//counterfeiter:generate . LocalMediaTrack
@@ -542,7 +796,7 @@ type LocalMediaTrack interface {
Restart()
SignalCid() string
HasSignalCid(cid string) bool
HasSdpCid(cid string) bool
GetConnectionScoreAndQuality() (float32, livekit.ConnectionQuality)
@@ -551,9 +805,57 @@ type LocalMediaTrack interface {
SetRTT(rtt uint32)
NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []SubscribedCodecQuality)
NotifySubscriptionNode(nodeID livekit.NodeID, codecs []*livekit.SubscribedAudioCodec)
ClearSubscriberNodes()
NotifySubscriberNodeMediaLoss(nodeID livekit.NodeID, fractionalLoss uint8)
}
// DataTrack represents a data track
//
//counterfeiter:generate . DataTrack
type DataTrack interface {
ID() livekit.TrackID
PubHandle() uint16
Name() string
ToProto() *livekit.DataTrackInfo
PublisherID() livekit.ParticipantID
PublisherIdentity() livekit.ParticipantIdentity
AddSubscriber(sub LocalParticipant) (DataDownTrack, error)
RemoveSubscriber(participantID livekit.ParticipantID)
IsSubscriber(subID livekit.ParticipantID) bool
AddDataDownTrack(sender DataTrackSender) error
DeleteDataDownTrack(subscriberID livekit.ParticipantID)
HandlePacket(data []byte, packet *datatrack.Packet, arrivalTime int64)
Close()
}
//counterfeiter:generate . DataDownTrack
type DataDownTrack interface {
Close()
Handle() uint16
PublishDataTrack() DataTrack
UpdateSubscriptionOptions(subscriptionOptions *livekit.DataTrackSubscriptionOptions)
}
//counterfeiter:generate . DataTrackSender
type DataTrackSender interface {
SubscriberID() livekit.ParticipantID
WritePacket(data []byte, packet *datatrack.Packet, arrivalTime int64)
}
//counterfeiter:generate . DataTrackTransport
type DataTrackTransport interface {
SendDataTrackMessage(data []byte) error
}
//counterfeiter:generate . SubscribedTrack
type SubscribedTrack interface {
AddOnBind(f func(error))
@@ -595,9 +897,20 @@ type MediaResolverResult struct {
PublisherIdentity livekit.ParticipantIdentity
}
type DataResolverResult struct {
TrackChangedNotifier ChangeNotifier
TrackRemovedNotifier ChangeNotifier
DataTrack DataTrack
PublisherID livekit.ParticipantID
PublisherIdentity livekit.ParticipantIdentity
}
// MediaTrackResolver locates a specific media track for a subscriber
type MediaTrackResolver func(LocalParticipant, livekit.TrackID) MediaResolverResult
// DataTrackResolver locates a specific data track for a subscriber
type DataTrackResolver func(LocalParticipant, livekit.TrackID) DataResolverResult
// Supervisor/operation monitor related definitions
type OperationMonitorEvent int
@@ -626,7 +939,7 @@ func (o OperationMonitorEvent) String() string {
}
}
type OperationMonitorData interface{}
type OperationMonitorData any
type OperationMonitor interface {
PostEvent(ome OperationMonitorEvent, omd OperationMonitorData)
@@ -16,7 +16,7 @@ package types
type ProtocolVersion int
const CurrentProtocol = 15
const CurrentProtocol = 17
func (v ProtocolVersion) SupportsPackedStreamId() bool {
return v > 0
@@ -68,11 +68,11 @@ func (v ProtocolVersion) SupportFastStart() bool {
return v > 7
}
func (v ProtocolVersion) SupportHandlesDisconnectedUpdate() bool {
func (v ProtocolVersion) SupportsDisconnectedUpdate() bool {
return v > 8
}
func (v ProtocolVersion) SupportSyncStreamID() bool {
func (v ProtocolVersion) SupportsSyncStreamID() bool {
return v > 9
}
@@ -95,3 +95,11 @@ func (v ProtocolVersion) SupportsRegionsInLeaveRequest() bool {
func (v ProtocolVersion) SupportsNonErrorSignalResponse() bool {
return v > 14
}
func (v ProtocolVersion) SupportsMoving() bool {
return v > 15
}
func (v ProtocolVersion) SupportsPacketTrailer() bool {
return v > 16
}
@@ -0,0 +1,229 @@
// 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
}
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) 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)
@@ -0,0 +1,786 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/livekit-server/pkg/rtc/types"
"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
}
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) 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)
@@ -0,0 +1,148 @@
// Code generated by counterfeiter. DO NOT EDIT.
package typesfakes
import (
"sync"
"github.com/livekit/livekit-server/pkg/rtc/datatrack"
"github.com/livekit/livekit-server/pkg/rtc/types"
"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)
@@ -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)
@@ -6,8 +6,9 @@ import (
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/sfu/mime"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
type FakeLocalMediaTrack struct {
@@ -34,6 +35,10 @@ type FakeLocalMediaTrack struct {
clearAllReceiversArgsForCall []struct {
arg1 bool
}
ClearSubscriberNodesStub func()
clearSubscriberNodesMutex sync.RWMutex
clearSubscriberNodesArgsForCall []struct {
}
CloseStub func(bool)
closeMutex sync.RWMutex
closeArgsForCall []struct {
@@ -83,11 +88,12 @@ type FakeLocalMediaTrack struct {
getNumSubscribersReturnsOnCall map[int]struct {
result1 int
}
GetQualityForDimensionStub func(uint32, uint32) livekit.VideoQuality
GetQualityForDimensionStub func(mime.MimeType, uint32, uint32) livekit.VideoQuality
getQualityForDimensionMutex sync.RWMutex
getQualityForDimensionArgsForCall []struct {
arg1 uint32
arg1 mime.MimeType
arg2 uint32
arg3 uint32
}
getQualityForDimensionReturns struct {
result1 livekit.VideoQuality
@@ -95,12 +101,12 @@ type FakeLocalMediaTrack struct {
getQualityForDimensionReturnsOnCall map[int]struct {
result1 livekit.VideoQuality
}
GetTemporalLayerForSpatialFpsStub func(int32, uint32, mime.MimeType) int32
GetTemporalLayerForSpatialFpsStub func(mime.MimeType, int32, uint32) int32
getTemporalLayerForSpatialFpsMutex sync.RWMutex
getTemporalLayerForSpatialFpsArgsForCall []struct {
arg1 int32
arg2 uint32
arg3 mime.MimeType
arg1 mime.MimeType
arg2 int32
arg3 uint32
}
getTemporalLayerForSpatialFpsReturns struct {
result1 int32
@@ -118,6 +124,16 @@ type FakeLocalMediaTrack struct {
getTrackStatsReturnsOnCall map[int]struct {
result1 *livekit.RTPStats
}
HasPacketTrailerStub func() bool
hasPacketTrailerMutex sync.RWMutex
hasPacketTrailerArgsForCall []struct {
}
hasPacketTrailerReturns struct {
result1 bool
}
hasPacketTrailerReturnsOnCall map[int]struct {
result1 bool
}
HasSdpCidStub func(string) bool
hasSdpCidMutex sync.RWMutex
hasSdpCidArgsForCall []struct {
@@ -129,6 +145,17 @@ type FakeLocalMediaTrack struct {
hasSdpCidReturnsOnCall map[int]struct {
result1 bool
}
HasSignalCidStub func(string) bool
hasSignalCidMutex sync.RWMutex
hasSignalCidArgsForCall []struct {
arg1 string
}
hasSignalCidReturns struct {
result1 bool
}
hasSignalCidReturnsOnCall map[int]struct {
result1 bool
}
IDStub func() livekit.TrackID
iDMutex sync.RWMutex
iDArgsForCall []struct {
@@ -169,16 +196,6 @@ type FakeLocalMediaTrack struct {
isOpenReturnsOnCall map[int]struct {
result1 bool
}
IsSimulcastStub func() bool
isSimulcastMutex sync.RWMutex
isSimulcastArgsForCall []struct {
}
isSimulcastReturns struct {
result1 bool
}
isSimulcastReturnsOnCall map[int]struct {
result1 bool
}
IsSubscriberStub func(livekit.ParticipantID) bool
isSubscriberMutex sync.RWMutex
isSubscriberArgsForCall []struct {
@@ -200,6 +217,16 @@ type FakeLocalMediaTrack struct {
kindReturnsOnCall map[int]struct {
result1 livekit.TrackType
}
LoggerStub func() logger.Logger
loggerMutex sync.RWMutex
loggerArgsForCall []struct {
}
loggerReturns struct {
result1 logger.Logger
}
loggerReturnsOnCall map[int]struct {
result1 logger.Logger
}
NameStub func() string
nameMutex sync.RWMutex
nameArgsForCall []struct {
@@ -222,6 +249,12 @@ type FakeLocalMediaTrack struct {
arg1 livekit.NodeID
arg2 uint8
}
NotifySubscriptionNodeStub func(livekit.NodeID, []*livekit.SubscribedAudioCodec)
notifySubscriptionNodeMutex sync.RWMutex
notifySubscriptionNodeArgsForCall []struct {
arg1 livekit.NodeID
arg2 []*livekit.SubscribedAudioCodec
}
OnTrackSubscribedStub func()
onTrackSubscribedMutex sync.RWMutex
onTrackSubscribedArgsForCall []struct {
@@ -297,16 +330,6 @@ type FakeLocalMediaTrack struct {
setRTTArgsForCall []struct {
arg1 uint32
}
SignalCidStub func() string
signalCidMutex sync.RWMutex
signalCidArgsForCall []struct {
}
signalCidReturns struct {
result1 string
}
signalCidReturnsOnCall map[int]struct {
result1 string
}
SourceStub func() livekit.TrackSource
sourceMutex sync.RWMutex
sourceArgsForCall []struct {
@@ -484,6 +507,30 @@ func (fake *FakeLocalMediaTrack) ClearAllReceiversArgsForCall(i int) bool {
return argsForCall.arg1
}
func (fake *FakeLocalMediaTrack) ClearSubscriberNodes() {
fake.clearSubscriberNodesMutex.Lock()
fake.clearSubscriberNodesArgsForCall = append(fake.clearSubscriberNodesArgsForCall, struct {
}{})
stub := fake.ClearSubscriberNodesStub
fake.recordInvocation("ClearSubscriberNodes", []interface{}{})
fake.clearSubscriberNodesMutex.Unlock()
if stub != nil {
fake.ClearSubscriberNodesStub()
}
}
func (fake *FakeLocalMediaTrack) ClearSubscriberNodesCallCount() int {
fake.clearSubscriberNodesMutex.RLock()
defer fake.clearSubscriberNodesMutex.RUnlock()
return len(fake.clearSubscriberNodesArgsForCall)
}
func (fake *FakeLocalMediaTrack) ClearSubscriberNodesCalls(stub func()) {
fake.clearSubscriberNodesMutex.Lock()
defer fake.clearSubscriberNodesMutex.Unlock()
fake.ClearSubscriberNodesStub = stub
}
func (fake *FakeLocalMediaTrack) Close(arg1 bool) {
fake.closeMutex.Lock()
fake.closeArgsForCall = append(fake.closeArgsForCall, struct {
@@ -734,19 +781,20 @@ func (fake *FakeLocalMediaTrack) GetNumSubscribersReturnsOnCall(i int, result1 i
}{result1}
}
func (fake *FakeLocalMediaTrack) GetQualityForDimension(arg1 uint32, arg2 uint32) livekit.VideoQuality {
func (fake *FakeLocalMediaTrack) GetQualityForDimension(arg1 mime.MimeType, arg2 uint32, arg3 uint32) livekit.VideoQuality {
fake.getQualityForDimensionMutex.Lock()
ret, specificReturn := fake.getQualityForDimensionReturnsOnCall[len(fake.getQualityForDimensionArgsForCall)]
fake.getQualityForDimensionArgsForCall = append(fake.getQualityForDimensionArgsForCall, struct {
arg1 uint32
arg1 mime.MimeType
arg2 uint32
}{arg1, arg2})
arg3 uint32
}{arg1, arg2, arg3})
stub := fake.GetQualityForDimensionStub
fakeReturns := fake.getQualityForDimensionReturns
fake.recordInvocation("GetQualityForDimension", []interface{}{arg1, arg2})
fake.recordInvocation("GetQualityForDimension", []interface{}{arg1, arg2, arg3})
fake.getQualityForDimensionMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1
@@ -760,17 +808,17 @@ func (fake *FakeLocalMediaTrack) GetQualityForDimensionCallCount() int {
return len(fake.getQualityForDimensionArgsForCall)
}
func (fake *FakeLocalMediaTrack) GetQualityForDimensionCalls(stub func(uint32, uint32) livekit.VideoQuality) {
func (fake *FakeLocalMediaTrack) GetQualityForDimensionCalls(stub func(mime.MimeType, uint32, uint32) livekit.VideoQuality) {
fake.getQualityForDimensionMutex.Lock()
defer fake.getQualityForDimensionMutex.Unlock()
fake.GetQualityForDimensionStub = stub
}
func (fake *FakeLocalMediaTrack) GetQualityForDimensionArgsForCall(i int) (uint32, uint32) {
func (fake *FakeLocalMediaTrack) GetQualityForDimensionArgsForCall(i int) (mime.MimeType, uint32, uint32) {
fake.getQualityForDimensionMutex.RLock()
defer fake.getQualityForDimensionMutex.RUnlock()
argsForCall := fake.getQualityForDimensionArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeLocalMediaTrack) GetQualityForDimensionReturns(result1 livekit.VideoQuality) {
@@ -796,13 +844,13 @@ func (fake *FakeLocalMediaTrack) GetQualityForDimensionReturnsOnCall(i int, resu
}{result1}
}
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFps(arg1 int32, arg2 uint32, arg3 mime.MimeType) int32 {
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFps(arg1 mime.MimeType, arg2 int32, arg3 uint32) int32 {
fake.getTemporalLayerForSpatialFpsMutex.Lock()
ret, specificReturn := fake.getTemporalLayerForSpatialFpsReturnsOnCall[len(fake.getTemporalLayerForSpatialFpsArgsForCall)]
fake.getTemporalLayerForSpatialFpsArgsForCall = append(fake.getTemporalLayerForSpatialFpsArgsForCall, struct {
arg1 int32
arg2 uint32
arg3 mime.MimeType
arg1 mime.MimeType
arg2 int32
arg3 uint32
}{arg1, arg2, arg3})
stub := fake.GetTemporalLayerForSpatialFpsStub
fakeReturns := fake.getTemporalLayerForSpatialFpsReturns
@@ -823,13 +871,13 @@ func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsCallCount() int {
return len(fake.getTemporalLayerForSpatialFpsArgsForCall)
}
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsCalls(stub func(int32, uint32, mime.MimeType) int32) {
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsCalls(stub func(mime.MimeType, int32, uint32) int32) {
fake.getTemporalLayerForSpatialFpsMutex.Lock()
defer fake.getTemporalLayerForSpatialFpsMutex.Unlock()
fake.GetTemporalLayerForSpatialFpsStub = stub
}
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsArgsForCall(i int) (int32, uint32, mime.MimeType) {
func (fake *FakeLocalMediaTrack) GetTemporalLayerForSpatialFpsArgsForCall(i int) (mime.MimeType, int32, uint32) {
fake.getTemporalLayerForSpatialFpsMutex.RLock()
defer fake.getTemporalLayerForSpatialFpsMutex.RUnlock()
argsForCall := fake.getTemporalLayerForSpatialFpsArgsForCall[i]
@@ -912,6 +960,59 @@ func (fake *FakeLocalMediaTrack) GetTrackStatsReturnsOnCall(i int, result1 *live
}{result1}
}
func (fake *FakeLocalMediaTrack) HasPacketTrailer() bool {
fake.hasPacketTrailerMutex.Lock()
ret, specificReturn := fake.hasPacketTrailerReturnsOnCall[len(fake.hasPacketTrailerArgsForCall)]
fake.hasPacketTrailerArgsForCall = append(fake.hasPacketTrailerArgsForCall, struct {
}{})
stub := fake.HasPacketTrailerStub
fakeReturns := fake.hasPacketTrailerReturns
fake.recordInvocation("HasPacketTrailer", []interface{}{})
fake.hasPacketTrailerMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalMediaTrack) HasPacketTrailerCallCount() int {
fake.hasPacketTrailerMutex.RLock()
defer fake.hasPacketTrailerMutex.RUnlock()
return len(fake.hasPacketTrailerArgsForCall)
}
func (fake *FakeLocalMediaTrack) HasPacketTrailerCalls(stub func() bool) {
fake.hasPacketTrailerMutex.Lock()
defer fake.hasPacketTrailerMutex.Unlock()
fake.HasPacketTrailerStub = stub
}
func (fake *FakeLocalMediaTrack) HasPacketTrailerReturns(result1 bool) {
fake.hasPacketTrailerMutex.Lock()
defer fake.hasPacketTrailerMutex.Unlock()
fake.HasPacketTrailerStub = nil
fake.hasPacketTrailerReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalMediaTrack) HasPacketTrailerReturnsOnCall(i int, result1 bool) {
fake.hasPacketTrailerMutex.Lock()
defer fake.hasPacketTrailerMutex.Unlock()
fake.HasPacketTrailerStub = nil
if fake.hasPacketTrailerReturnsOnCall == nil {
fake.hasPacketTrailerReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.hasPacketTrailerReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalMediaTrack) HasSdpCid(arg1 string) bool {
fake.hasSdpCidMutex.Lock()
ret, specificReturn := fake.hasSdpCidReturnsOnCall[len(fake.hasSdpCidArgsForCall)]
@@ -973,6 +1074,67 @@ func (fake *FakeLocalMediaTrack) HasSdpCidReturnsOnCall(i int, result1 bool) {
}{result1}
}
func (fake *FakeLocalMediaTrack) HasSignalCid(arg1 string) bool {
fake.hasSignalCidMutex.Lock()
ret, specificReturn := fake.hasSignalCidReturnsOnCall[len(fake.hasSignalCidArgsForCall)]
fake.hasSignalCidArgsForCall = append(fake.hasSignalCidArgsForCall, struct {
arg1 string
}{arg1})
stub := fake.HasSignalCidStub
fakeReturns := fake.hasSignalCidReturns
fake.recordInvocation("HasSignalCid", []interface{}{arg1})
fake.hasSignalCidMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalMediaTrack) HasSignalCidCallCount() int {
fake.hasSignalCidMutex.RLock()
defer fake.hasSignalCidMutex.RUnlock()
return len(fake.hasSignalCidArgsForCall)
}
func (fake *FakeLocalMediaTrack) HasSignalCidCalls(stub func(string) bool) {
fake.hasSignalCidMutex.Lock()
defer fake.hasSignalCidMutex.Unlock()
fake.HasSignalCidStub = stub
}
func (fake *FakeLocalMediaTrack) HasSignalCidArgsForCall(i int) string {
fake.hasSignalCidMutex.RLock()
defer fake.hasSignalCidMutex.RUnlock()
argsForCall := fake.hasSignalCidArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeLocalMediaTrack) HasSignalCidReturns(result1 bool) {
fake.hasSignalCidMutex.Lock()
defer fake.hasSignalCidMutex.Unlock()
fake.HasSignalCidStub = nil
fake.hasSignalCidReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalMediaTrack) HasSignalCidReturnsOnCall(i int, result1 bool) {
fake.hasSignalCidMutex.Lock()
defer fake.hasSignalCidMutex.Unlock()
fake.HasSignalCidStub = nil
if fake.hasSignalCidReturnsOnCall == nil {
fake.hasSignalCidReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.hasSignalCidReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalMediaTrack) ID() livekit.TrackID {
fake.iDMutex.Lock()
ret, specificReturn := fake.iDReturnsOnCall[len(fake.iDArgsForCall)]
@@ -1185,59 +1347,6 @@ func (fake *FakeLocalMediaTrack) IsOpenReturnsOnCall(i int, result1 bool) {
}{result1}
}
func (fake *FakeLocalMediaTrack) IsSimulcast() bool {
fake.isSimulcastMutex.Lock()
ret, specificReturn := fake.isSimulcastReturnsOnCall[len(fake.isSimulcastArgsForCall)]
fake.isSimulcastArgsForCall = append(fake.isSimulcastArgsForCall, struct {
}{})
stub := fake.IsSimulcastStub
fakeReturns := fake.isSimulcastReturns
fake.recordInvocation("IsSimulcast", []interface{}{})
fake.isSimulcastMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalMediaTrack) IsSimulcastCallCount() int {
fake.isSimulcastMutex.RLock()
defer fake.isSimulcastMutex.RUnlock()
return len(fake.isSimulcastArgsForCall)
}
func (fake *FakeLocalMediaTrack) IsSimulcastCalls(stub func() bool) {
fake.isSimulcastMutex.Lock()
defer fake.isSimulcastMutex.Unlock()
fake.IsSimulcastStub = stub
}
func (fake *FakeLocalMediaTrack) IsSimulcastReturns(result1 bool) {
fake.isSimulcastMutex.Lock()
defer fake.isSimulcastMutex.Unlock()
fake.IsSimulcastStub = nil
fake.isSimulcastReturns = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalMediaTrack) IsSimulcastReturnsOnCall(i int, result1 bool) {
fake.isSimulcastMutex.Lock()
defer fake.isSimulcastMutex.Unlock()
fake.IsSimulcastStub = nil
if fake.isSimulcastReturnsOnCall == nil {
fake.isSimulcastReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.isSimulcastReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *FakeLocalMediaTrack) IsSubscriber(arg1 livekit.ParticipantID) bool {
fake.isSubscriberMutex.Lock()
ret, specificReturn := fake.isSubscriberReturnsOnCall[len(fake.isSubscriberArgsForCall)]
@@ -1352,6 +1461,59 @@ func (fake *FakeLocalMediaTrack) KindReturnsOnCall(i int, result1 livekit.TrackT
}{result1}
}
func (fake *FakeLocalMediaTrack) Logger() logger.Logger {
fake.loggerMutex.Lock()
ret, specificReturn := fake.loggerReturnsOnCall[len(fake.loggerArgsForCall)]
fake.loggerArgsForCall = append(fake.loggerArgsForCall, struct {
}{})
stub := fake.LoggerStub
fakeReturns := fake.loggerReturns
fake.recordInvocation("Logger", []interface{}{})
fake.loggerMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalMediaTrack) LoggerCallCount() int {
fake.loggerMutex.RLock()
defer fake.loggerMutex.RUnlock()
return len(fake.loggerArgsForCall)
}
func (fake *FakeLocalMediaTrack) LoggerCalls(stub func() logger.Logger) {
fake.loggerMutex.Lock()
defer fake.loggerMutex.Unlock()
fake.LoggerStub = stub
}
func (fake *FakeLocalMediaTrack) LoggerReturns(result1 logger.Logger) {
fake.loggerMutex.Lock()
defer fake.loggerMutex.Unlock()
fake.LoggerStub = nil
fake.loggerReturns = struct {
result1 logger.Logger
}{result1}
}
func (fake *FakeLocalMediaTrack) LoggerReturnsOnCall(i int, result1 logger.Logger) {
fake.loggerMutex.Lock()
defer fake.loggerMutex.Unlock()
fake.LoggerStub = nil
if fake.loggerReturnsOnCall == nil {
fake.loggerReturnsOnCall = make(map[int]struct {
result1 logger.Logger
})
}
fake.loggerReturnsOnCall[i] = struct {
result1 logger.Logger
}{result1}
}
func (fake *FakeLocalMediaTrack) Name() string {
fake.nameMutex.Lock()
ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)]
@@ -1476,6 +1638,44 @@ func (fake *FakeLocalMediaTrack) NotifySubscriberNodeMediaLossArgsForCall(i int)
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeLocalMediaTrack) NotifySubscriptionNode(arg1 livekit.NodeID, arg2 []*livekit.SubscribedAudioCodec) {
var arg2Copy []*livekit.SubscribedAudioCodec
if arg2 != nil {
arg2Copy = make([]*livekit.SubscribedAudioCodec, len(arg2))
copy(arg2Copy, arg2)
}
fake.notifySubscriptionNodeMutex.Lock()
fake.notifySubscriptionNodeArgsForCall = append(fake.notifySubscriptionNodeArgsForCall, struct {
arg1 livekit.NodeID
arg2 []*livekit.SubscribedAudioCodec
}{arg1, arg2Copy})
stub := fake.NotifySubscriptionNodeStub
fake.recordInvocation("NotifySubscriptionNode", []interface{}{arg1, arg2Copy})
fake.notifySubscriptionNodeMutex.Unlock()
if stub != nil {
fake.NotifySubscriptionNodeStub(arg1, arg2)
}
}
func (fake *FakeLocalMediaTrack) NotifySubscriptionNodeCallCount() int {
fake.notifySubscriptionNodeMutex.RLock()
defer fake.notifySubscriptionNodeMutex.RUnlock()
return len(fake.notifySubscriptionNodeArgsForCall)
}
func (fake *FakeLocalMediaTrack) NotifySubscriptionNodeCalls(stub func(livekit.NodeID, []*livekit.SubscribedAudioCodec)) {
fake.notifySubscriptionNodeMutex.Lock()
defer fake.notifySubscriptionNodeMutex.Unlock()
fake.NotifySubscriptionNodeStub = stub
}
func (fake *FakeLocalMediaTrack) NotifySubscriptionNodeArgsForCall(i int) (livekit.NodeID, []*livekit.SubscribedAudioCodec) {
fake.notifySubscriptionNodeMutex.RLock()
defer fake.notifySubscriptionNodeMutex.RUnlock()
argsForCall := fake.notifySubscriptionNodeArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeLocalMediaTrack) OnTrackSubscribed() {
fake.onTrackSubscribedMutex.Lock()
fake.onTrackSubscribedArgsForCall = append(fake.onTrackSubscribedArgsForCall, struct {
@@ -1899,59 +2099,6 @@ func (fake *FakeLocalMediaTrack) SetRTTArgsForCall(i int) uint32 {
return argsForCall.arg1
}
func (fake *FakeLocalMediaTrack) SignalCid() string {
fake.signalCidMutex.Lock()
ret, specificReturn := fake.signalCidReturnsOnCall[len(fake.signalCidArgsForCall)]
fake.signalCidArgsForCall = append(fake.signalCidArgsForCall, struct {
}{})
stub := fake.SignalCidStub
fakeReturns := fake.signalCidReturns
fake.recordInvocation("SignalCid", []interface{}{})
fake.signalCidMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeLocalMediaTrack) SignalCidCallCount() int {
fake.signalCidMutex.RLock()
defer fake.signalCidMutex.RUnlock()
return len(fake.signalCidArgsForCall)
}
func (fake *FakeLocalMediaTrack) SignalCidCalls(stub func() string) {
fake.signalCidMutex.Lock()
defer fake.signalCidMutex.Unlock()
fake.SignalCidStub = stub
}
func (fake *FakeLocalMediaTrack) SignalCidReturns(result1 string) {
fake.signalCidMutex.Lock()
defer fake.signalCidMutex.Unlock()
fake.SignalCidStub = nil
fake.signalCidReturns = struct {
result1 string
}{result1}
}
func (fake *FakeLocalMediaTrack) SignalCidReturnsOnCall(i int, result1 string) {
fake.signalCidMutex.Lock()
defer fake.signalCidMutex.Unlock()
fake.SignalCidStub = nil
if fake.signalCidReturnsOnCall == nil {
fake.signalCidReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.signalCidReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *FakeLocalMediaTrack) Source() livekit.TrackSource {
fake.sourceMutex.Lock()
ret, specificReturn := fake.sourceReturnsOnCall[len(fake.sourceArgsForCall)]
@@ -2210,84 +2357,6 @@ func (fake *FakeLocalMediaTrack) UpdateVideoTrackArgsForCall(i int) *livekit.Upd
func (fake *FakeLocalMediaTrack) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.addOnCloseMutex.RLock()
defer fake.addOnCloseMutex.RUnlock()
fake.addSubscriberMutex.RLock()
defer fake.addSubscriberMutex.RUnlock()
fake.clearAllReceiversMutex.RLock()
defer fake.clearAllReceiversMutex.RUnlock()
fake.closeMutex.RLock()
defer fake.closeMutex.RUnlock()
fake.getAllSubscribersMutex.RLock()
defer fake.getAllSubscribersMutex.RUnlock()
fake.getAudioLevelMutex.RLock()
defer fake.getAudioLevelMutex.RUnlock()
fake.getConnectionScoreAndQualityMutex.RLock()
defer fake.getConnectionScoreAndQualityMutex.RUnlock()
fake.getNumSubscribersMutex.RLock()
defer fake.getNumSubscribersMutex.RUnlock()
fake.getQualityForDimensionMutex.RLock()
defer fake.getQualityForDimensionMutex.RUnlock()
fake.getTemporalLayerForSpatialFpsMutex.RLock()
defer fake.getTemporalLayerForSpatialFpsMutex.RUnlock()
fake.getTrackStatsMutex.RLock()
defer fake.getTrackStatsMutex.RUnlock()
fake.hasSdpCidMutex.RLock()
defer fake.hasSdpCidMutex.RUnlock()
fake.iDMutex.RLock()
defer fake.iDMutex.RUnlock()
fake.isEncryptedMutex.RLock()
defer fake.isEncryptedMutex.RUnlock()
fake.isMutedMutex.RLock()
defer fake.isMutedMutex.RUnlock()
fake.isOpenMutex.RLock()
defer fake.isOpenMutex.RUnlock()
fake.isSimulcastMutex.RLock()
defer fake.isSimulcastMutex.RUnlock()
fake.isSubscriberMutex.RLock()
defer fake.isSubscriberMutex.RUnlock()
fake.kindMutex.RLock()
defer fake.kindMutex.RUnlock()
fake.nameMutex.RLock()
defer fake.nameMutex.RUnlock()
fake.notifySubscriberNodeMaxQualityMutex.RLock()
defer fake.notifySubscriberNodeMaxQualityMutex.RUnlock()
fake.notifySubscriberNodeMediaLossMutex.RLock()
defer fake.notifySubscriberNodeMediaLossMutex.RUnlock()
fake.onTrackSubscribedMutex.RLock()
defer fake.onTrackSubscribedMutex.RUnlock()
fake.publisherIDMutex.RLock()
defer fake.publisherIDMutex.RUnlock()
fake.publisherIdentityMutex.RLock()
defer fake.publisherIdentityMutex.RUnlock()
fake.publisherVersionMutex.RLock()
defer fake.publisherVersionMutex.RUnlock()
fake.receiversMutex.RLock()
defer fake.receiversMutex.RUnlock()
fake.removeSubscriberMutex.RLock()
defer fake.removeSubscriberMutex.RUnlock()
fake.restartMutex.RLock()
defer fake.restartMutex.RUnlock()
fake.revokeDisallowedSubscribersMutex.RLock()
defer fake.revokeDisallowedSubscribersMutex.RUnlock()
fake.setMutedMutex.RLock()
defer fake.setMutedMutex.RUnlock()
fake.setRTTMutex.RLock()
defer fake.setRTTMutex.RUnlock()
fake.signalCidMutex.RLock()
defer fake.signalCidMutex.RUnlock()
fake.sourceMutex.RLock()
defer fake.sourceMutex.RUnlock()
fake.streamMutex.RLock()
defer fake.streamMutex.RUnlock()
fake.toProtoMutex.RLock()
defer fake.toProtoMutex.RUnlock()
fake.updateAudioTrackMutex.RLock()
defer fake.updateAudioTrackMutex.RUnlock()
fake.updateTrackInfoMutex.RLock()
defer fake.updateTrackInfoMutex.RUnlock()
fake.updateVideoTrackMutex.RLock()
defer fake.updateVideoTrackMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
File diff suppressed because it is too large Load Diff
@@ -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)

Some files were not shown because too many files have changed in this diff Show More