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
@@ -16,11 +16,16 @@ package service
import (
"context"
"fmt"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/protocol/agent"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
)
type AgentDispatchService struct {
@@ -45,11 +50,16 @@ func NewAgentDispatchService(
}
func (ag *AgentDispatchService) CreateDispatch(ctx context.Context, req *livekit.CreateAgentDispatchRequest) (*livekit.AgentDispatch, error) {
AppendLogFields(ctx, "room", req.Room, "request", logger.Proto(redactCreateAgentDispatchRequest(req)))
err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, twirpAuthError(err)
}
if err := agent.ValidateDeployment(req.GetDeployment()); err != nil {
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
}
if ag.roomAllocator.AutoCreateEnabled(ctx) {
err := ag.roomAllocator.SelectRoomNode(ctx, livekit.RoomName(req.Room), "")
if err != nil {
@@ -63,15 +73,18 @@ func (ag *AgentDispatchService) CreateDispatch(ctx context.Context, req *livekit
}
dispatch := &livekit.AgentDispatch{
Id: guid.New(guid.AgentDispatchPrefix),
AgentName: req.AgentName,
Room: req.Room,
Metadata: req.Metadata,
Id: guid.New(guid.AgentDispatchPrefix),
AgentName: req.AgentName,
Room: req.Room,
Metadata: req.Metadata,
RestartPolicy: req.RestartPolicy,
Deployment: req.Deployment,
}
return ag.agentDispatchClient.CreateDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), dispatch)
}
func (ag *AgentDispatchService) DeleteDispatch(ctx context.Context, req *livekit.DeleteAgentDispatchRequest) (*livekit.AgentDispatch, error) {
AppendLogFields(ctx, "room", req.Room, "request", logger.Proto(req))
err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, twirpAuthError(err)
@@ -81,6 +94,7 @@ func (ag *AgentDispatchService) DeleteDispatch(ctx context.Context, req *livekit
}
func (ag *AgentDispatchService) ListDispatch(ctx context.Context, req *livekit.ListAgentDispatchRequest) (*livekit.ListAgentDispatchResponse, error) {
AppendLogFields(ctx, "room", req.Room, "request", logger.Proto(req))
err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, twirpAuthError(err)
@@ -88,3 +102,18 @@ func (ag *AgentDispatchService) ListDispatch(ctx context.Context, req *livekit.L
return ag.agentDispatchClient.ListDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
}
func redactCreateAgentDispatchRequest(req *livekit.CreateAgentDispatchRequest) *livekit.CreateAgentDispatchRequest {
if req.Metadata == "" {
return req
}
clone := utils.CloneProto(req)
// replace with size of metadata to provide visibility on request size
if clone.Metadata != "" {
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
}
return clone
}
+63 -50
View File
@@ -17,7 +17,6 @@ package service
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"slices"
@@ -43,13 +42,19 @@ import (
"github.com/livekit/psrpc"
)
const agentWorkerLoadTarget = 0.65
type AgentSocketUpgrader struct {
websocket.Upgrader
}
func (u AgentSocketUpgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, agent.WorkerProtocolVersion, bool) {
func (u AgentSocketUpgrader) Upgrade(
w http.ResponseWriter,
r *http.Request,
responseHeader http.Header,
) (
conn *websocket.Conn,
registration agent.WorkerRegistration,
ok bool,
) {
if u.CheckOrigin == nil {
// allow connections from any origin, since script may be hosted anywhere
// security is enforced by access tokens
@@ -61,29 +66,31 @@ func (u AgentSocketUpgrader) Upgrade(w http.ResponseWriter, r *http.Request, res
// reject non websocket requests
if !websocket.IsWebSocketUpgrade(r) {
w.WriteHeader(404)
return nil, 0, false
return
}
// require a claim
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil || !claims.Video.Agent {
handleError(w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return nil, 0, false
HandleError(w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return
}
registration = agent.MakeWorkerRegistration()
registration.ClientIP = GetClientIP(r)
// upgrade
conn, err := u.Upgrader.Upgrade(w, r, responseHeader)
if err != nil {
handleError(w, r, http.StatusInternalServerError, err)
return nil, 0, false
HandleError(w, r, http.StatusInternalServerError, err)
return
}
var protocol agent.WorkerProtocolVersion = agent.CurrentProtocol
if pv, err := strconv.Atoi(r.FormValue("protocol")); err == nil {
protocol = agent.WorkerProtocolVersion(pv)
registration.Protocol = agent.WorkerProtocolVersion(pv)
}
return conn, protocol, true
return conn, registration, true
}
func DispatchAgentWorkerSignal(c agent.SignalConn, h agent.WorkerSignalHandler, l logger.Logger) bool {
@@ -105,8 +112,8 @@ func DispatchAgentWorkerSignal(c agent.SignalConn, h agent.WorkerSignalHandler,
return true
}
func HandshakeAgentWorker(c agent.SignalConn, serverInfo *livekit.ServerInfo, protocol agent.WorkerProtocolVersion, l logger.Logger) (r agent.WorkerRegistration, ok bool) {
wr := agent.NewWorkerRegisterer(c, serverInfo, protocol)
func HandshakeAgentWorker(c agent.SignalConn, serverInfo *livekit.ServerInfo, registration agent.WorkerRegistration, l logger.Logger) (r agent.WorkerRegistration, ok bool) {
wr := agent.NewWorkerRegisterer(c, serverInfo, registration)
if err := c.SetReadDeadline(wr.Deadline()); err != nil {
return
}
@@ -136,6 +143,7 @@ type AgentHandler struct {
workers map[string]*agent.Worker
jobToWorker map[livekit.JobID]*agent.Worker
keyProvider auth.KeyProvider
targetLoad float32
namespaceWorkers map[workerKey][]*agent.Worker
roomKeyCount int
@@ -150,12 +158,14 @@ type AgentHandler struct {
}
type workerKey struct {
agentName string
namespace string
jobType livekit.JobType
agentName string
namespace string
jobType livekit.JobType
deployment string
}
func NewAgentService(conf *config.Config,
func NewAgentService(
conf *config.Config,
currentNode routing.LocalNode,
bus psrpc.MessageBus,
keyProvider auth.KeyProvider,
@@ -180,6 +190,7 @@ func NewAgentService(conf *config.Config,
keyProvider,
logger.GetLogger(),
serverInfo,
conf.Agents.TargetLoad,
agent.RoomAgentTopic,
agent.PublisherAgentTopic,
agent.ParticipantAgentTopic,
@@ -187,9 +198,9 @@ func NewAgentService(conf *config.Config,
return s, nil
}
func (s *AgentService) ServeHTTP(writer http.ResponseWriter, r *http.Request) {
if conn, protocol, ok := s.upgrader.Upgrade(writer, r, nil); ok {
s.HandleConnection(r.Context(), NewWSSignalConnection(conn), protocol)
func (s *AgentService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if conn, registration, ok := s.upgrader.Upgrade(w, r, nil); ok {
s.HandleConnection(r.Context(), NewWSSignalConnection(conn), registration)
conn.Close()
}
}
@@ -199,6 +210,7 @@ func NewAgentHandler(
keyProvider auth.KeyProvider,
logger logger.Logger,
serverInfo *livekit.ServerInfo,
targetLoad float32,
roomTopic string,
publisherTopic string,
participantTopic string,
@@ -211,14 +223,15 @@ func NewAgentHandler(
namespaceWorkers: make(map[workerKey][]*agent.Worker),
serverInfo: serverInfo,
keyProvider: keyProvider,
targetLoad: targetLoad,
roomTopic: roomTopic,
publisherTopic: publisherTopic,
participantTopic: participantTopic,
}
}
func (h *AgentHandler) HandleConnection(ctx context.Context, conn agent.SignalConn, protocol agent.WorkerProtocolVersion) {
registration, ok := HandshakeAgentWorker(conn, h.serverInfo, protocol, h.logger)
func (h *AgentHandler) HandleConnection(ctx context.Context, conn agent.SignalConn, registration agent.WorkerRegistration) {
registration, ok := HandshakeAgentWorker(conn, h.serverInfo, registration, h.logger)
if !ok {
return
}
@@ -243,13 +256,13 @@ func (h *AgentHandler) registerWorker(w *agent.Worker) {
h.workers[w.ID] = w
key := workerKey{w.AgentName, w.Namespace, w.JobType}
key := workerKey{w.AgentName, w.Namespace, w.JobType, w.Deployment}
workers := h.namespaceWorkers[key]
created := len(workers) == 0
if created {
nameTopic := agent.GetAgentTopic(w.AgentName, w.Namespace)
nameTopic := agent.GetAgentTopic(w.AgentName, w.Namespace, w.Deployment)
var typeTopic string
switch w.JobType {
case livekit.JobType_JT_ROOM:
@@ -260,8 +273,6 @@ func (h *AgentHandler) registerWorker(w *agent.Worker) {
typeTopic = h.participantTopic
}
fmt.Println(">>> register worker", typeTopic)
err := h.agentServer.RegisterJobRequestTopic(nameTopic, typeTopic)
if err != nil {
h.mu.Unlock()
@@ -310,7 +321,7 @@ func (h *AgentHandler) deregisterWorker(w *agent.Worker) {
delete(h.workers, w.ID)
key := workerKey{w.AgentName, w.Namespace, w.JobType}
key := workerKey{w.AgentName, w.Namespace, w.JobType, w.Deployment}
workers, ok := h.namespaceWorkers[key]
if !ok {
@@ -332,7 +343,7 @@ func (h *AgentHandler) deregisterWorker(w *agent.Worker) {
)
delete(h.namespaceWorkers, key)
topic := agent.GetAgentTopic(w.AgentName, w.Namespace)
topic := agent.GetAgentTopic(w.AgentName, w.Namespace, w.Deployment)
switch w.JobType {
case livekit.JobType_JT_ROOM:
@@ -383,7 +394,7 @@ func (h *AgentHandler) JobRequest(ctx context.Context, job *livekit.Job) (*rpc.J
logger = logger.WithValues("participant", job.Participant.Identity)
}
key := workerKey{job.AgentName, job.Namespace, job.Type}
key := workerKey{job.AgentName, job.Namespace, job.Type, job.Deployment}
attempted := make(map[*agent.Worker]struct{})
for {
selected, err := h.selectWorkerWeightedByLoad(key, attempted)
@@ -395,28 +406,30 @@ func (h *AgentHandler) JobRequest(ctx context.Context, job *livekit.Job) (*rpc.J
logger := logger.WithValues("workerID", selected.ID)
attempted[selected] = struct{}{}
state, err := selected.AssignJob(ctx, job)
if err != nil {
state, err := selected.AssignJob(ctx, job, nil)
switch state.GetStatus() {
case livekit.JobStatus_JS_RUNNING:
logger.Infow("assigned job to worker", "apiKey", selected.APIKey())
h.mu.Lock()
h.jobToWorker[livekit.JobID(job.Id)] = selected
h.mu.Unlock()
err = h.agentServer.RegisterJobTerminateTopic(job.Id)
if err != nil {
logger.Errorw("failed to register JobTerminate handler", err)
}
fallthrough
case livekit.JobStatus_JS_SUCCESS:
return &rpc.JobRequestResponse{
State: state,
}, nil
default:
retry := utils.ErrorIsOneOf(err, agent.ErrWorkerNotAvailable, agent.ErrWorkerClosed)
logger.Warnw("failed to assign job to worker", err, "retry", retry)
if retry {
continue // Try another worker
if !retry {
return nil, err
}
return nil, err
}
logger.Infow("assigned job to worker")
h.mu.Lock()
h.jobToWorker[livekit.JobID(job.Id)] = selected
h.mu.Unlock()
err = h.agentServer.RegisterJobTerminateTopic(job.Id)
if err != nil {
logger.Errorw("failed to register JobTerminate handler", err)
}
return &rpc.JobRequestResponse{
State: state,
}, nil
}
}
@@ -426,12 +439,12 @@ func (h *AgentHandler) JobRequestAffinity(ctx context.Context, job *livekit.Job)
var affinity float32
for _, w := range h.workers {
if w.AgentName != job.AgentName || w.Namespace != job.Namespace || w.JobType != job.Type {
if w.AgentName != job.AgentName || w.Namespace != job.Namespace || w.JobType != job.Type || w.Deployment != job.Deployment {
continue
}
if w.Status() == livekit.WorkerStatus_WS_AVAILABLE {
affinity += max(0, agentWorkerLoadTarget-w.Load())
affinity += max(0, h.targetLoad-w.Load())
}
}
+19 -6
View File
@@ -58,7 +58,7 @@ func NewAPIKeyAuthMiddleware(provider auth.KeyProvider) *APIKeyAuthMiddleware {
}
func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if r.URL != nil && r.URL.Path == "/rtc/validate" {
if r.URL != nil && (r.URL.Path == "/rtc/validate" || r.URL.Path == "/rtc/v1/validate") {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
@@ -67,7 +67,7 @@ func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
if authHeader != "" {
if !strings.HasPrefix(authHeader, bearerPrefix) {
handleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
HandleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
return
}
@@ -80,19 +80,19 @@ func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
if authToken != "" {
v, err := auth.ParseAPIToken(authToken)
if err != nil {
handleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
HandleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
return
}
secret := m.provider.GetSecret(v.APIKey())
if secret == "" {
handleError(w, r, http.StatusUnauthorized, errors.New("invalid API key: "+v.APIKey()))
HandleError(w, r, http.StatusUnauthorized, errors.New("invalid API key: "+v.APIKey()))
return
}
grants, err := v.Verify(secret)
_, grants, err := v.Verify(secret)
if err != nil {
handleError(w, r, http.StatusUnauthorized, errors.New("invalid token: "+authToken+", error: "+err.Error()))
HandleError(w, r, http.StatusUnauthorized, errors.New("invalid token: "+authToken+", error: "+err.Error()))
return
}
@@ -219,6 +219,19 @@ func EnsureSIPCallPermission(ctx context.Context) error {
return nil
}
func EnsureDestRoomPermission(ctx context.Context, source livekit.RoomName, destination livekit.RoomName) error {
claims := GetGrants(ctx)
if claims == nil || claims.Video == nil {
return ErrPermissionDenied
}
if !claims.Video.RoomAdmin || source != livekit.RoomName(claims.Video.Room) || destination != livekit.RoomName(claims.Video.DestinationRoom) {
return ErrPermissionDenied
}
return nil
}
// wraps authentication errors around Twirp
func twirpAuthError(err error) error {
return twirp.NewError(twirp.Unauthenticated, err.Error())
-42
View File
@@ -19,12 +19,8 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
)
//counterfeiter:generate . IOClient
@@ -35,41 +31,3 @@ type IOClient interface {
CreateIngress(ctx context.Context, req *livekit.IngressInfo) (*emptypb.Empty, error)
UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error)
}
type egressLauncher struct {
client rpc.EgressClient
io IOClient
}
func NewEgressLauncher(client rpc.EgressClient, io IOClient) rtc.EgressLauncher {
if client == nil {
return nil
}
return &egressLauncher{
client: client,
io: io,
}
}
func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
// Ensure we have an Egress ID
if req.EgressId == "" {
req.EgressId = guid.New(utils.EgressPrefix)
}
info, err := s.client.StartEgress(ctx, "", req)
if err != nil {
return nil, err
}
_, err = s.io.CreateEgress(ctx, info)
if err != nil {
logger.Errorw("failed to create egress", err)
}
return info, nil
}
+17 -10
View File
@@ -15,27 +15,31 @@
package service_test
import (
"context"
"fmt"
"log"
"net"
"os"
"testing"
"time"
"go.uber.org/atomic"
"github.com/ory/dockertest/v3"
mobyclient "github.com/moby/moby/client"
"github.com/ory/dockertest/v4"
)
var Docker *dockertest.Pool
var Docker dockertest.ClosablePool
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
ctx := context.Background()
pool, err := dockertest.NewPool(ctx, "")
if err != nil {
log.Fatalf("Could not construct pool: %s", err)
}
// uses pool to try to connect to Docker
err = pool.Client.Ping()
_, err = pool.Client().Ping(ctx, mobyclient.PingOptions{})
if err != nil {
log.Fatalf("Could not connect to Docker: %s", err)
}
@@ -46,7 +50,7 @@ func TestMain(m *testing.M) {
}
func waitTCPPort(t testing.TB, addr string) {
if err := Docker.Retry(func() error {
if err := Docker.Retry(t.Context(), 30*time.Second, func() error {
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Log(err)
@@ -62,15 +66,18 @@ func waitTCPPort(t testing.TB, addr string) {
var redisLast atomic.Uint32
func runRedis(t testing.TB) string {
c, err := Docker.RunWithOptions(&dockertest.RunOptions{
Name: fmt.Sprintf("lktest-redis-%d", redisLast.Inc()),
Repository: "redis", Tag: "latest",
})
c, err := Docker.Run(t.Context(),
"redis",
dockertest.WithName(fmt.Sprintf("lktest-redis-%d", redisLast.Inc())),
dockertest.WithTag("latest"),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = Docker.Purge(c)
// t.Context() is canceled before cleanup funcs run, so use a
// non-canceled context to let the container stop/remove complete.
_ = c.Close(context.Background())
})
addr := c.GetHostPort("6379/tcp")
waitTCPPort(t, addr)
+129 -33
View File
@@ -17,15 +17,21 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"github.com/twitchtv/twirp"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/protocol/egress"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/rtc"
)
type EgressService struct {
@@ -33,27 +39,41 @@ type EgressService struct {
client rpc.EgressClient
io IOClient
roomService livekit.RoomService
store ServiceStore
}
type egressLauncher struct {
client rpc.EgressClient
io IOClient
store ServiceStore
}
func NewEgressService(
client rpc.EgressClient,
launcher rtc.EgressLauncher,
store ServiceStore,
io IOClient,
rs livekit.RoomService,
) *EgressService {
return &EgressService{
client: client,
store: store,
io: io,
roomService: rs,
launcher: launcher,
}
}
func NewEgressLauncher(client rpc.EgressClient, io IOClient, store ServiceStore) rtc.EgressLauncher {
if client == nil {
return nil
}
return &egressLauncher{
client: client,
io: io,
store: store,
}
}
func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livekit.RoomCompositeEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"room", req.RoomName,
"baseUrl", req.CustomBaseUrl,
"outputType", egress.GetOutputType(req),
@@ -61,7 +81,10 @@ func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livek
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_RoomComposite{
RoomComposite: req,
},
@@ -69,19 +92,23 @@ func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livek
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartWebEgress(ctx context.Context, req *livekit.WebEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"url", req.Url,
"outputType", egress.GetOutputType(req),
}
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, "", &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_Web{
Web: req,
},
@@ -89,12 +116,14 @@ func (s *EgressService) StartWebEgress(ctx context.Context, req *livekit.WebEgre
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartParticipantEgress(ctx context.Context, req *livekit.ParticipantEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"room", req.RoomName,
"identity", req.Identity,
"outputType", egress.GetOutputType(req),
@@ -102,7 +131,9 @@ func (s *EgressService) StartParticipantEgress(ctx context.Context, req *livekit
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_Participant{
Participant: req,
},
@@ -110,12 +141,14 @@ func (s *EgressService) StartParticipantEgress(ctx context.Context, req *livekit
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartTrackCompositeEgress(ctx context.Context, req *livekit.TrackCompositeEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{
fields := []any{
"room", req.RoomName,
"audioTrackID", req.AudioTrackId,
"videoTrackID", req.VideoTrackId,
@@ -124,7 +157,9 @@ func (s *EgressService) StartTrackCompositeEgress(ctx context.Context, req *live
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_TrackComposite{
TrackComposite: req,
},
@@ -132,19 +167,23 @@ func (s *EgressService) StartTrackCompositeEgress(ctx context.Context, req *live
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) StartTrackEgress(ctx context.Context, req *livekit.TrackEgressRequest) (*livekit.EgressInfo, error) {
fields := []interface{}{"room", req.RoomName, "trackID", req.TrackId}
fields := []any{"room", req.RoomName, "trackID", req.TrackId}
if t := reflect.TypeOf(req.Output); t != nil {
fields = append(fields, "outputType", t.String())
}
defer func() {
AppendLogFields(ctx, fields...)
}()
ei, err := s.startEgress(ctx, livekit.RoomName(req.RoomName), &rpc.StartEgressRequest{
egressID, idFromCtx := EgressID(ctx)
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
EgressId: egressID,
Request: &rpc.StartEgressRequest_Track{
Track: req,
},
@@ -152,26 +191,76 @@ func (s *EgressService) StartTrackEgress(ctx context.Context, req *livekit.Track
if err != nil {
return nil, err
}
fields = append(fields, "egressID", ei.EgressId)
if !idFromCtx {
fields = append(fields, "egressID", ei.EgressId)
}
return ei, err
}
func (s *EgressService) startEgress(ctx context.Context, roomName livekit.RoomName, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
func (s *EgressService) startEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
if err := EnsureRecordPermission(ctx); err != nil {
return nil, twirpAuthError(err)
} else if s.launcher == nil {
return nil, ErrEgressNotConnected
}
if roomName != "" {
room, _, err := s.store.LoadRoom(ctx, roomName, false)
if err != nil {
return nil, err
}
req.RoomId = room.Sid
}
return s.launcher.StartEgress(ctx, req)
}
func (s *egressLauncher) StartEgress(ctx context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
// Ensure we have an Egress ID
if req.EgressId == "" {
req.EgressId = guid.New(utils.EgressPrefix)
}
if req.RoomId == "" {
var roomName string
switch v := req.Request.(type) {
case *rpc.StartEgressRequest_RoomComposite:
roomName = v.RoomComposite.RoomName
case *rpc.StartEgressRequest_Web:
// no room name
case *rpc.StartEgressRequest_Participant:
roomName = v.Participant.RoomName
case *rpc.StartEgressRequest_TrackComposite:
roomName = v.TrackComposite.RoomName
case *rpc.StartEgressRequest_Track:
roomName = v.Track.RoomName
}
if roomName != "" {
room, _, err := s.store.LoadRoom(ctx, livekit.RoomName(roomName), false)
if err != nil {
return nil, err
}
req.RoomId = room.Sid
}
}
info, err := s.client.StartEgress(ctx, "", req)
if err != nil {
return nil, err
}
_, err = s.io.CreateEgress(ctx, info)
if err != nil {
logger.Errorw("failed to create egress", err)
}
return info, nil
}
func (s *egressLauncher) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
return s.client.StopEgress(ctx, req.EgressId, req)
}
type LayoutMetadata struct {
Layout string `json:"layout"`
}
@@ -249,17 +338,20 @@ func (s *EgressService) ListEgress(ctx context.Context, req *livekit.ListEgressR
return s.io.ListEgress(ctx, req)
}
func (s *EgressService) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (*livekit.EgressInfo, error) {
func (s *EgressService) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (info *livekit.EgressInfo, err error) {
defer func() {
if errors.Is(err, psrpc.ErrNoResponse) {
// Do not map cases where the context times out to 503
err = psrpc.ErrRequestTimedOut
}
}()
AppendLogFields(ctx, "egressID", req.EgressId)
if err := EnsureRecordPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.client == nil {
return nil, ErrEgressNotConnected
}
info, err := s.client.StopEgress(ctx, req.EgressId, req)
info, err = s.launcher.StopEgress(ctx, req)
if err != nil {
var loadErr error
info, loadErr = s.io.GetEgress(ctx, &rpc.GetEgressRequest{EgressId: req.EgressId})
@@ -279,3 +371,7 @@ func (s *EgressService) StopEgress(ctx context.Context, req *livekit.StopEgressR
return info, nil
}
func (s *EgressService) StartEgress(ctx context.Context, req *livekit.StartEgressRequest) (*livekit.EgressInfo, error) {
return nil, errors.New("not implemented")
}
+44
View File
@@ -0,0 +1,44 @@
package service
import (
"context"
"strings"
"github.com/twitchtv/twirp"
"github.com/livekit/protocol/utils/guid"
)
type egressIDKey struct{}
func TwirpEgressID() *twirp.ServerHooks {
return &twirp.ServerHooks{
RequestRouted: func(ctx context.Context) (context.Context, error) {
// generate egressID for start egress methods for tracing egress failure before it reaches egress service.
if isStartEgressMethod(ctx) {
egressID := guid.New(guid.EgressPrefix)
ctx = WithEgressID(ctx, egressID)
AppendLogFields(ctx, "egressID", egressID)
}
return ctx, nil
},
}
}
func WithEgressID(ctx context.Context, egressID string) context.Context {
return context.WithValue(ctx, egressIDKey{}, egressID)
}
func EgressID(ctx context.Context) (string, bool) {
if egressID, ok := ctx.Value(egressIDKey{}).(string); ok && egressID != "" {
return egressID, true
}
return "", false
}
func isStartEgressMethod(ctx context.Context) bool {
if methodName, ok := twirp.MethodName(ctx); ok && strings.HasPrefix(methodName, "Start") && strings.HasSuffix(methodName, "Egress") {
return true
}
return false
}
+7
View File
@@ -22,14 +22,17 @@ var (
ErrEgressNotFound = psrpc.NewErrorf(psrpc.NotFound, "egress does not exist")
ErrEgressNotConnected = psrpc.NewErrorf(psrpc.Internal, "egress not connected (redis required)")
ErrIdentityEmpty = psrpc.NewErrorf(psrpc.InvalidArgument, "identity cannot be empty")
ErrParticipantSidEmpty = psrpc.NewErrorf(psrpc.InvalidArgument, "participant sid cannot be empty")
ErrIngressNotConnected = psrpc.NewErrorf(psrpc.Internal, "ingress not connected (redis required)")
ErrIngressNotFound = psrpc.NewErrorf(psrpc.NotFound, "ingress does not exist")
ErrIngressNonReusable = psrpc.NewErrorf(psrpc.InvalidArgument, "ingress is not reusable and cannot be modified")
ErrNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "name length exceeds limits")
ErrMetadataExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "metadata size exceeds limits")
ErrAttributeExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "attribute size exceeds limits")
ErrNoRoomName = psrpc.NewErrorf(psrpc.InvalidArgument, "no room name")
ErrRoomNameExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "room name length exceeds limits")
ErrParticipantIdentityExceedsLimits = psrpc.NewErrorf(psrpc.InvalidArgument, "participant identity length exceeds limits")
ErrDestinationSameAsSourceRoom = psrpc.NewErrorf(psrpc.InvalidArgument, "destination room cannot be the same as source room")
ErrOperationFailed = psrpc.NewErrorf(psrpc.Internal, "operation cannot be completed")
ErrParticipantNotFound = psrpc.NewErrorf(psrpc.NotFound, "participant does not exist")
ErrRoomNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested room does not exist")
@@ -42,4 +45,8 @@ var (
ErrSIPTrunkNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip trunk does not exist")
ErrSIPDispatchRuleNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip dispatch rule does not exist")
ErrSIPParticipantNotFound = psrpc.NewErrorf(psrpc.NotFound, "requested sip participant does not exist")
ErrInvalidMessageType = psrpc.NewErrorf(psrpc.Internal, "invalid message type")
ErrNoConnectRequest = psrpc.NewErrorf(psrpc.InvalidArgument, "no connect request")
ErrNoConnectResponse = psrpc.NewErrorf(psrpc.InvalidArgument, "no connect response")
ErrDestinationIdentityRequired = psrpc.NewErrorf(psrpc.InvalidArgument, "destination identity is required")
)
+16 -17
View File
@@ -85,7 +85,7 @@ func NewIngressService(
}
func (s *IngressService) CreateIngress(ctx context.Context, req *livekit.CreateIngressRequest) (*livekit.IngressInfo, error) {
fields := []interface{}{
fields := []any{
"inputType", req.InputType,
"name", req.Name,
}
@@ -190,20 +190,19 @@ func (s *IngressService) CreateIngressWithUrl(ctx context.Context, urlStr string
if err != nil {
return info, err
}
}
// TODO Remove this store Ingress call for URL pull as it is redundant since
// the ingress service sends a CreateIngress RPC
_, err = s.io.CreateIngress(ctx, info)
switch err {
case nil:
break
case ingress.ErrIngressOutOfDate:
// Error returned if the ingress was already created by the ingress service
err = nil
default:
logger.Errorw("could not create ingress object", err)
return nil, err
// The Ingress instance will create the ingress object when handling the URL pull ingress
} else {
_, err = s.io.CreateIngress(ctx, info)
switch err {
case nil:
break
case ingress.ErrIngressOutOfDate:
// Error returned if the ingress was already created by the ingress service
err = nil
default:
logger.Errorw("could not create ingress object", err)
return nil, err
}
}
return info, nil
@@ -218,7 +217,7 @@ func (s *IngressService) LaunchPullIngress(ctx context.Context, info *livekit.In
}
func updateEnableTranscoding(info *livekit.IngressInfo) {
// Set BypassTranscoding as well for backward compatiblity
// Set BypassTranscoding as well for backward compatibility
if info.EnableTranscoding != nil {
info.BypassTranscoding = !*info.EnableTranscoding
return
@@ -276,7 +275,7 @@ func updateInfoUsingRequest(req *livekit.UpdateIngressRequest, info *livekit.Ing
}
func (s *IngressService) UpdateIngress(ctx context.Context, req *livekit.UpdateIngressRequest) (*livekit.IngressInfo, error) {
fields := []interface{}{
fields := []any{
"ingress", req.IngressId,
"name", req.Name,
}
+7 -1
View File
@@ -28,6 +28,7 @@ import (
//counterfeiter:generate . ObjectStore
type ObjectStore interface {
ServiceStore
OSSServiceStore
// enable locking on a specific room to prevent race
// returns a (lock uuid, error)
@@ -43,11 +44,16 @@ type ObjectStore interface {
//counterfeiter:generate . ServiceStore
type ServiceStore interface {
LoadRoom(ctx context.Context, roomName livekit.RoomName, includeInternal bool) (*livekit.Room, *livekit.RoomInternal, error)
DeleteRoom(ctx context.Context, roomName livekit.RoomName) error
RoomExists(ctx context.Context, roomName livekit.RoomName) (bool, error)
// ListRooms returns currently active rooms. if names is not nil, it'll filter and return
// only rooms that match
ListRooms(ctx context.Context, roomNames []livekit.RoomName) ([]*livekit.Room, error)
}
type OSSServiceStore interface {
DeleteRoom(ctx context.Context, roomName livekit.RoomName) error
HasParticipant(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (bool, error)
LoadParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)
ListParticipants(ctx context.Context, roomName livekit.RoomName) ([]*livekit.ParticipantInfo, error)
}
+25 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/livekit/livekit-server/pkg/telemetry"
)
@@ -54,7 +55,9 @@ func NewIOInfoService(
}
if bus != nil {
ioServer, err := rpc.NewIOInfoServer(s, bus)
ioServer, err := rpc.NewIOInfoServer(s, bus,
otelpsrpc.ServerOptions(otelpsrpc.Config{}),
)
if err != nil {
return nil, err
}
@@ -86,6 +89,10 @@ func (s *IOInfoService) Stop() {
}
func (s *IOInfoService) CreateEgress(ctx context.Context, info *livekit.EgressInfo) (*emptypb.Empty, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
// check if egress already exists to avoid duplicate EgressStarted event
if _, err := s.es.LoadEgress(ctx, info.EgressId); err == nil {
return &emptypb.Empty{}, nil
@@ -103,6 +110,10 @@ func (s *IOInfoService) CreateEgress(ctx context.Context, info *livekit.EgressIn
}
func (s *IOInfoService) UpdateEgress(ctx context.Context, info *livekit.EgressInfo) (*emptypb.Empty, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
err := s.es.UpdateEgress(ctx, info)
switch info.Status {
@@ -126,6 +137,10 @@ func (s *IOInfoService) UpdateEgress(ctx context.Context, info *livekit.EgressIn
}
func (s *IOInfoService) GetEgress(ctx context.Context, req *rpc.GetEgressRequest) (*livekit.EgressInfo, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
info, err := s.es.LoadEgress(ctx, req.EgressId)
if err != nil {
logger.Errorw("failed to load egress", err)
@@ -136,6 +151,10 @@ func (s *IOInfoService) GetEgress(ctx context.Context, req *rpc.GetEgressRequest
}
func (s *IOInfoService) ListEgress(ctx context.Context, req *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error) {
if s.es == nil {
return nil, ErrEgressNotConnected
}
if req.EgressId != "" {
info, err := s.es.LoadEgress(ctx, req.EgressId)
if err != nil {
@@ -168,3 +187,8 @@ func (s *IOInfoService) UpdateSIPCallState(ctx context.Context, req *rpc.UpdateS
// TODO: placeholder
return &emptypb.Empty{}, nil
}
func (s *IOInfoService) RecordCallContext(context.Context, *rpc.RecordCallContextRequest) (*emptypb.Empty, error) {
// TODO: placeholder
return &emptypb.Empty{}, nil
}
@@ -25,6 +25,10 @@ import (
)
func (s *IOInfoService) CreateIngress(ctx context.Context, info *livekit.IngressInfo) (*emptypb.Empty, error) {
if s.is == nil {
return nil, ErrIngressNotConnected
}
err := s.is.StoreIngress(ctx, info)
if err != nil {
return nil, err
@@ -45,6 +49,10 @@ func (s *IOInfoService) GetIngressInfo(ctx context.Context, req *rpc.GetIngressI
}
func (s *IOInfoService) loadIngressFromInfoRequest(req *rpc.GetIngressInfoRequest) (info *livekit.IngressInfo, err error) {
if s.is == nil {
return nil, ErrIngressNotConnected
}
if req.IngressId != "" {
info, err = s.is.LoadIngress(context.Background(), req.IngressId)
} else if req.StreamKey != "" {
@@ -56,6 +64,10 @@ func (s *IOInfoService) loadIngressFromInfoRequest(req *rpc.GetIngressInfoReques
}
func (s *IOInfoService) UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error) {
if s.is == nil {
return nil, ErrIngressNotConnected
}
info, err := s.is.LoadIngress(ctx, req.IngressId)
if err != nil {
return nil, err
+18 -19
View File
@@ -30,21 +30,21 @@ import (
// matchSIPTrunk finds a SIP Trunk definition matching the request.
// Returns nil if no rules matched or an error if there are conflicting definitions.
func (s *IOInfoService) matchSIPTrunk(ctx context.Context, trunkID, calling, called string, srcIP netip.Addr) (*livekit.SIPInboundTrunkInfo, error) {
func (s *IOInfoService) matchSIPTrunk(ctx context.Context, trunkID string, call *rpc.SIPCall) (*livekit.SIPInboundTrunkInfo, error) {
if s.ss == nil {
return nil, ErrSIPNotConnected
}
if trunkID != "" {
// This is a best-effort optimization. Fallthrough to listing trunks if it doesn't work.
if tr, err := s.ss.LoadSIPInboundTrunk(ctx, trunkID); err == nil {
tr, err = sip.MatchTrunkIter(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), srcIP, calling, called)
tr, err = sip.MatchTrunkIter(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), call)
if err == nil {
return tr, nil
}
}
}
it := s.SelectSIPInboundTrunk(ctx, called)
return sip.MatchTrunkIter(it, srcIP, calling, called)
it := s.SelectSIPInboundTrunk(ctx, call.To.User)
return sip.MatchTrunkIter(it, call)
}
func (s *IOInfoService) SelectSIPInboundTrunk(ctx context.Context, called string) iters.Iter[*livekit.SIPInboundTrunkInfo] {
@@ -82,18 +82,19 @@ func (s *IOInfoService) SelectSIPDispatchRule(ctx context.Context, trunkID strin
}
func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
call := req.SIPCall()
log := logger.GetLogger()
log = log.WithValues("toUser", req.CalledNumber, "fromUser", req.CallingNumber, "src", req.SrcAddress)
if req.SrcAddress == "" {
log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
if call.SourceIp == "" {
log.Warnw("source address is not set", nil)
// TODO: return error in the next release
}
srcIP, err := netip.ParseAddr(req.SrcAddress)
if req.SrcAddress != "" && err != nil {
_, err := netip.ParseAddr(call.SourceIp)
if call.SourceIp != "" && err != nil {
log.Errorw("cannot parse source IP", err)
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
trunk, err := s.matchSIPTrunk(ctx, req.SipTrunkId, req.CallingNumber, req.CalledNumber, srcIP)
trunk, err := s.matchSIPTrunk(ctx, req.SipTrunkId, call)
if err != nil {
return nil, err
}
@@ -122,23 +123,25 @@ func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.E
if err != nil {
return nil, err
}
resp.Upgrade()
resp.SipTrunkId = trunkID
return resp, err
}
func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.GetSIPTrunkAuthenticationRequest) (*rpc.GetSIPTrunkAuthenticationResponse, error) {
call := req.SIPCall()
log := logger.GetLogger()
log = log.WithValues("toUser", req.To, "fromUser", req.From, "src", req.SrcAddress)
if req.SrcAddress == "" {
log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
if call.SourceIp == "" {
log.Warnw("source address is not set", nil)
// TODO: return error in the next release
}
srcIP, err := netip.ParseAddr(req.SrcAddress)
if req.SrcAddress != "" && err != nil {
_, err := netip.ParseAddr(call.SourceIp)
if call.SourceIp != "" && err != nil {
log.Errorw("cannot parse source IP", err)
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
trunk, err := s.matchSIPTrunk(ctx, "", req.From, req.To, srcIP)
trunk, err := s.matchSIPTrunk(ctx, "", call)
if err != nil {
return nil, err
}
@@ -147,9 +150,5 @@ func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.
return &rpc.GetSIPTrunkAuthenticationResponse{}, nil
}
log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId)
return &rpc.GetSIPTrunkAuthenticationResponse{
SipTrunkId: trunk.SipTrunkId,
Username: trunk.AuthUsername,
Password: trunk.AuthPassword,
}, nil
return sip.InboundTrunkAuthPrompt(trunk)
}
+19 -4
View File
@@ -25,6 +25,8 @@ import (
"github.com/livekit/protocol/utils"
)
var _ OSSServiceStore = (*LocalStore)(nil)
// encapsulates CRUD operations for room settings
type LocalStore struct {
// map of roomName => room
@@ -84,6 +86,16 @@ func (s *LocalStore) LoadRoom(_ context.Context, roomName livekit.RoomName, incl
return room, internal, nil
}
func (s *LocalStore) RoomExists(ctx context.Context, roomName livekit.RoomName) (bool, error) {
_, _, err := s.LoadRoom(ctx, roomName, false)
if err == ErrRoomNotFound {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (s *LocalStore) ListRooms(_ context.Context, roomNames []livekit.RoomName) ([]*livekit.Room, error) {
s.lock.RLock()
defer s.lock.RUnlock()
@@ -153,6 +165,11 @@ func (s *LocalStore) LoadParticipant(_ context.Context, roomName livekit.RoomNam
return participant, nil
}
func (s *LocalStore) HasParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (bool, error) {
p, err := s.LoadParticipant(ctx, roomName, identity)
return p != nil, utils.ScreenError(err, ErrParticipantNotFound)
}
func (s *LocalStore) ListParticipants(_ context.Context, roomName livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
s.lock.RLock()
defer s.lock.RUnlock()
@@ -224,10 +241,8 @@ func (s *LocalStore) ListAgentDispatches(ctx context.Context, roomName livekit.R
agentJobs := s.agentJobs[roomName]
var js []*livekit.Job
if agentJobs != nil {
for _, j := range agentJobs {
js = append(js, utils.CloneProto(j))
}
for _, j := range agentJobs {
js = append(js, utils.CloneProto(j))
}
var ds []*livekit.AgentDispatch
+20 -3
View File
@@ -68,6 +68,8 @@ const (
maxRetries = 5
)
var _ OSSServiceStore = (*RedisStore)(nil)
type RedisStore struct {
rc redis.UniversalClient
unlockScript *redis.Script
@@ -195,6 +197,16 @@ func (s *RedisStore) LoadRoom(_ context.Context, roomName livekit.RoomName, incl
return room, internal, nil
}
func (s *RedisStore) RoomExists(ctx context.Context, roomName livekit.RoomName) (bool, error) {
_, _, err := s.LoadRoom(ctx, roomName, false)
if err == ErrRoomNotFound {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func (s *RedisStore) ListRooms(_ context.Context, roomNames []livekit.RoomName) ([]*livekit.Room, error) {
var items []string
var err error
@@ -205,7 +217,7 @@ func (s *RedisStore) ListRooms(_ context.Context, roomNames []livekit.RoomName)
}
} else {
names := livekit.IDsAsStrings(roomNames)
var results []interface{}
var results []any
results, err = s.rc.HMGet(s.ctx, RoomsKey, names...).Result()
if err != nil && err != redis.Nil {
return nil, errors.Wrap(err, "could not get rooms by names")
@@ -314,6 +326,11 @@ func (s *RedisStore) LoadParticipant(_ context.Context, roomName livekit.RoomNam
return &pi, nil
}
func (s *RedisStore) HasParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (bool, error) {
p, err := s.LoadParticipant(ctx, roomName, identity)
return p != nil, utils.ScreenError(err, ErrParticipantNotFound)
}
func (s *RedisStore) ListParticipants(_ context.Context, roomName livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
key := RoomParticipantsPrefix + string(roomName)
items, err := s.rc.HVals(s.ctx, key).Result()
@@ -585,7 +602,7 @@ func (s *RedisStore) storeIngress(_ context.Context, info *livekit.IngressInfo)
}
// Retry if the key has been changed.
for i := 0; i < maxRetries; i++ {
for range maxRetries {
err := s.rc.Watch(s.ctx, txf, IngressKey)
switch err {
case redis.TxFailedErr:
@@ -660,7 +677,7 @@ func (s *RedisStore) storeIngressState(_ context.Context, ingressId string, stat
}
// Retry if the key has been changed.
for i := 0; i < maxRetries; i++ {
for range maxRetries {
err := s.rc.Watch(s.ctx, txf, IngressStatePrefix+ingressId)
switch err {
case redis.TxFailedErr:
+13 -6
View File
@@ -106,12 +106,19 @@ func (s *RedisStore) LoadSIPOutboundTrunk(ctx context.Context, id string) (*live
}
func (s *RedisStore) DeleteSIPTrunk(ctx context.Context, id string) error {
tx := s.rc.TxPipeline()
tx.HDel(s.ctx, SIPTrunkKey, id)
tx.HDel(s.ctx, SIPInboundTrunkKey, id)
tx.HDel(s.ctx, SIPOutboundTrunkKey, id)
_, err := tx.Exec(ctx)
return err
err1 := s.rc.HDel(s.ctx, SIPTrunkKey, id).Err()
err2 := s.rc.HDel(s.ctx, SIPInboundTrunkKey, id).Err()
err3 := s.rc.HDel(s.ctx, SIPOutboundTrunkKey, id).Err()
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
if err3 != nil {
return err3
}
return nil
}
func (s *RedisStore) listSIPLegacyTrunk(ctx context.Context, page *livekit.Pagination) ([]*livekit.SIPTrunkInfo, error) {
@@ -320,7 +320,7 @@ func testIter[T listItem](
) {
ctx := context.Background()
var all []string
for i := 0; i < 250; i++ {
for i := range 250 {
id := fmt.Sprintf("%05d", i)
all = append(all, id)
err := create(ctx, id)
+6 -4
View File
@@ -174,7 +174,7 @@ func (r *StandardRoomAllocator) SelectRoomNode(ctx context.Context, roomName liv
func (r *StandardRoomAllocator) ValidateCreateRoom(ctx context.Context, roomName livekit.RoomName) error {
// when auto create is disabled, we'll check to ensure it's already created
if !r.config.Room.AutoCreate {
if !r.config.Room.AutoCreate && EnsureCreatePermission(ctx) != nil {
_, _, err := r.roomStore.LoadRoom(ctx, roomName, false)
if err != nil {
return err
@@ -208,17 +208,16 @@ func (r *StandardRoomAllocator) applyNamedRoomConfiguration(req *livekit.CreateR
conf, ok := r.config.Room.RoomConfigurations[req.RoomPreset]
if !ok {
return req, psrpc.NewErrorf(psrpc.InvalidArgument, "unknown room confguration in create room request")
return req, psrpc.NewErrorf(psrpc.InvalidArgument, "unknown room configuration in create room request")
}
clone := utils.CloneProto(req)
// Request overwrites conf
if clone.EmptyTimeout == 0 {
clone.EmptyTimeout = conf.EmptyTimeout
}
if clone.DepartureTimeout == 0 {
clone.DepartureTimeout = req.DepartureTimeout
clone.DepartureTimeout = conf.DepartureTimeout
}
if clone.MaxParticipants == 0 {
clone.MaxParticipants = conf.MaxParticipants
@@ -241,6 +240,9 @@ func (r *StandardRoomAllocator) applyNamedRoomConfiguration(req *livekit.CreateR
if !clone.SyncStreams {
clone.SyncStreams = conf.SyncStreams
}
if clone.Metadata == "" {
clone.Metadata = conf.Metadata
}
return clone, nil
}
+307 -108
View File
@@ -16,21 +16,27 @@ package service
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"maps"
"net"
"os"
"slices"
"strconv"
"sync"
"time"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/sfu"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"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"
"github.com/livekit/protocol/observability/roomobs"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/utils/guid"
@@ -38,6 +44,10 @@ import (
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/sfu"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/livekit-server/pkg/clientconfiguration"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
@@ -53,8 +63,6 @@ const (
tokenDefaultTTL = 10 * time.Minute
)
var affinityEpoch = time.Date(2000, 0, 0, 0, 0, 0, 0, time.UTC)
type iceConfigCacheKey struct {
roomName livekit.RoomName
participantIdentity livekit.ParticipantIdentity
@@ -72,6 +80,7 @@ type RoomManager struct {
router routing.Router
roomAllocator RoomAllocator
roomManagerServer rpc.TypedRoomManagerServer
whipServer rpc.WHIPServer[livekit.NodeID]
roomStore ObjectStore
telemetry telemetry.TelemetryService
clientConfManager clientconfiguration.ClientConfigurationManager
@@ -84,13 +93,19 @@ type RoomManager struct {
rooms map[livekit.RoomName]*rtc.Room
roomServers utils.MultitonService[rpc.RoomTopic]
agentDispatchServers utils.MultitonService[rpc.RoomTopic]
participantServers utils.MultitonService[rpc.ParticipantTopic]
roomServers utils.MultitonService[rpc.RoomTopic]
agentDispatchServers utils.MultitonService[rpc.RoomTopic]
participantServers utils.MultitonService[rpc.ParticipantTopic]
httpSignalParticipantServers utils.MultitonService[rpc.ParticipantTopic]
whipParticipantServers utils.MultitonService[rpc.ParticipantTopic]
iceConfigCache *sutils.IceConfigCache[iceConfigCacheKey]
forwardStats *sfu.ForwardStats
rpc.UnimplementedParticipantServer
rpc.UnimplementedRoomServer
rpc.UnimplementedRoomManagerServer
}
func NewLocalRoomManager(
@@ -100,7 +115,6 @@ func NewLocalRoomManager(
router routing.Router,
roomAllocator RoomAllocator,
telemetry telemetry.TelemetryService,
clientConfManager clientconfiguration.ClientConfigurationManager,
agentClient agent.Client,
agentStore AgentStore,
egressLauncher rtc.EgressLauncher,
@@ -122,7 +136,7 @@ func NewLocalRoomManager(
roomAllocator: roomAllocator,
roomStore: roomStore,
telemetry: telemetry,
clientConfManager: clientConfManager,
clientConfManager: clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations),
egressLauncher: egressLauncher,
agentClient: agentClient,
agentStore: agentStore,
@@ -153,6 +167,18 @@ func NewLocalRoomManager(
return nil, err
}
whipService, err := newWhipService(r)
if err != nil {
return nil, err
}
r.whipServer, err = rpc.NewWHIPServer[livekit.NodeID](whipService, bus, rpc.WithDefaultServerOptions(conf.PSRPC, logger.GetLogger()))
if err != nil {
return nil, err
}
if err := r.whipServer.RegisterAllCommonTopics(currentNode.NodeID()); err != nil {
return nil, err
}
return r, nil
}
@@ -193,7 +219,7 @@ func (r *RoomManager) deleteRoom(ctx context.Context, roomName livekit.RoomName)
func (r *RoomManager) CloseIdleRooms() {
r.lock.RLock()
rooms := maps.Values(r.rooms)
rooms := slices.Collect(maps.Values(r.rooms))
r.lock.RUnlock()
for _, room := range rooms {
@@ -216,7 +242,7 @@ func (r *RoomManager) HasParticipants() bool {
func (r *RoomManager) Stop() {
// disconnect all clients
r.lock.RLock()
rooms := maps.Values(r.rooms)
rooms := slices.Collect(maps.Values(r.rooms))
r.lock.RUnlock()
for _, room := range rooms {
@@ -224,9 +250,12 @@ func (r *RoomManager) Stop() {
}
r.roomManagerServer.Kill()
r.whipServer.Kill()
r.roomServers.Kill()
r.agentDispatchServers.Kill()
r.participantServers.Kill()
r.httpSignalParticipantServers.Kill()
r.whipParticipantServers.Kill()
if r.rtcConfig != nil {
if r.rtcConfig.UDPMux != nil {
@@ -318,10 +347,12 @@ func (r *RoomManager) StartSession(
Leave: leave,
},
})
prometheus.IncrementParticipantRtcCanceled(1)
return errors.New("could not restart closed participant")
}
participant.GetLogger().Infow("resuming RTC session",
participant.GetLogger().Infow(
"resuming RTC session",
"nodeID", r.currentNode.NodeID(),
"participantInit", &pi,
"numParticipants", room.GetParticipantCount(),
@@ -343,6 +374,9 @@ func (r *RoomManager) StartSession(
return err
}
r.telemetry.ParticipantResumed(ctx, room.ToProto(), participant.ToProto(), r.currentNode.NodeID(), pi.ReconnectReason)
go room.HandleSyncState(participant, pi.SyncState)
go r.rtcSessionWorker(room, participant, requestSource)
return nil
}
@@ -371,6 +405,7 @@ func (r *RoomManager) StartSession(
Leave: leave,
},
})
prometheus.IncrementParticipantRtcCanceled(1)
return errors.New("could not restart participant")
}
@@ -381,7 +416,8 @@ func (r *RoomManager) StartSession(
sid,
false,
)
pLogger.Infow("starting RTC session",
pLogger.Infow(
"starting RTC session",
"room", room.Name(),
"nodeID", r.currentNode.NodeID(),
"numParticipants", room.GetParticipantCount(),
@@ -396,78 +432,98 @@ func (r *RoomManager) StartSession(
if pi.DisableICELite {
rtcConf.SettingEngine.SetLite(false)
}
rtcConf.UpdatePublisherConfig(pi.UseSinglePeerConnection)
// default allow forceTCP
allowFallback := true
if r.config.RTC.AllowTCPFallback != nil {
allowFallback = *r.config.RTC.AllowTCPFallback
}
// default do not force full reconnect on a publication error
reconnectOnPublicationError := false
if r.config.RTC.ReconnectOnPublicationError != nil {
reconnectOnPublicationError = *r.config.RTC.ReconnectOnPublicationError
}
// default do not force full reconnect on a subscription error
reconnectOnSubscriptionError := false
if r.config.RTC.ReconnectOnSubscriptionError != nil {
reconnectOnSubscriptionError = *r.config.RTC.ReconnectOnSubscriptionError
}
// default do not force full reconnect on a data channel error
reconnectOnDataChannelError := false
if r.config.RTC.ReconnectOnDataChannelError != nil {
reconnectOnDataChannelError = *r.config.RTC.ReconnectOnDataChannelError
}
subscriberAllowPause := r.config.RTC.CongestionControl.AllowPause
if pi.SubscriberAllowPause != nil {
subscriberAllowPause = *pi.SubscriberAllowPause
}
enabledCodecs := protoRoom.EnabledCodecs
if !slices.ContainsFunc(enabledCodecs, func(codec *livekit.Codec) bool {
return mime.IsMimeTypeStringRTX(codec.Mime)
}) {
enabledCodecs = append(enabledCodecs, &livekit.Codec{Mime: mime.MimeTypeRTX.String()})
}
participant, err = rtc.NewParticipant(rtc.ParticipantParams{
Identity: pi.Identity,
Name: pi.Name,
SID: sid,
Config: &rtcConf,
Sink: responseSink,
AudioConfig: r.config.Audio,
VideoConfig: r.config.Video,
LimitConfig: r.config.Limit,
ProtocolVersion: pv,
SessionStartTime: sessionStartTime,
Telemetry: r.telemetry,
Trailer: room.Trailer(),
PLIThrottleConfig: r.config.RTC.PLIThrottle,
CongestionControlConfig: r.config.RTC.CongestionControl,
PublishEnabledCodecs: protoRoom.EnabledCodecs,
SubscribeEnabledCodecs: protoRoom.EnabledCodecs,
Grants: pi.Grants,
Reconnect: pi.Reconnect,
Logger: pLogger,
ClientConf: clientConf,
ClientInfo: rtc.ClientInfo{ClientInfo: pi.Client},
Region: pi.Region,
AdaptiveStream: pi.AdaptiveStream,
AllowTCPFallback: allowFallback,
TURNSEnabled: r.config.IsTURNSEnabled(),
GetParticipantInfo: func(pID livekit.ParticipantID) *livekit.ParticipantInfo {
if p := room.GetParticipantByID(pID); p != nil {
return p.ToProto()
}
return nil
Identity: pi.Identity,
Name: pi.Name,
SID: sid,
Config: &rtcConf,
Sink: responseSink,
AudioConfig: r.config.Audio,
VideoConfig: r.config.Video,
LimitConfig: r.config.Limit,
ProtocolVersion: pv,
SessionStartTime: sessionStartTime,
SessionTimer: observability.NewSessionTimer(sessionStartTime),
TelemetryListener: room.ParticipantTelemetryListener(),
Trailer: room.Trailer(),
PLIThrottleConfig: r.config.RTC.PLIThrottle,
CongestionControlConfig: r.config.RTC.CongestionControl,
PublishEnabledCodecs: enabledCodecs,
SubscribeEnabledCodecs: enabledCodecs,
Grants: pi.Grants,
Reconnect: pi.Reconnect,
Logger: pLogger,
Reporter: roomobs.NewNoopParticipantSessionReporter(),
ClientConf: clientConf,
ClientInfo: rtc.ClientInfo{ClientInfo: pi.Client},
Region: pi.Region,
AdaptiveStream: pi.AdaptiveStream,
AllowTCPFallback: allowFallback,
TCPFallbackRTTThreshold: r.config.RTC.TCPFallbackRTTThreshold,
AllowUDPUnstableFallback: r.config.RTC.AllowUDPUnstableFallback,
TURNSEnabled: r.config.IsTURNSEnabled(),
ParticipantListener: room.LocalParticipantListener(),
ParticipantHelper: &roomManagerParticipantHelper{
room: room,
codecRegressionThreshold: r.config.Video.CodecRegressionThreshold,
},
ReconnectOnPublicationError: reconnectOnPublicationError,
ReconnectOnSubscriptionError: reconnectOnSubscriptionError,
ReconnectOnDataChannelError: reconnectOnDataChannelError,
VersionGenerator: r.versionGenerator,
TrackResolver: room.ResolveMediaTrackForSubscriber,
SubscriberAllowPause: subscriberAllowPause,
SubscriptionLimitAudio: r.config.Limit.SubscriptionLimitAudio,
SubscriptionLimitVideo: r.config.Limit.SubscriptionLimitVideo,
PlayoutDelay: roomInternal.GetPlayoutDelay(),
SyncStreams: roomInternal.GetSyncStreams(),
ForwardStats: r.forwardStats,
MetricConfig: r.config.Metric,
UseOneShotSignallingMode: useOneShotSignallingMode,
DataChannelMaxBufferedAmount: r.config.RTC.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: r.config.RTC.DatachannelSlowThreshold,
FireOnTrackBySdp: true,
ReconnectOnPublicationError: reconnectOnPublicationError,
ReconnectOnSubscriptionError: reconnectOnSubscriptionError,
ReconnectOnDataChannelError: reconnectOnDataChannelError,
VersionGenerator: r.versionGenerator,
SubscriberAllowPause: subscriberAllowPause,
SubscriptionLimitAudio: r.config.Limit.SubscriptionLimitAudio,
SubscriptionLimitVideo: r.config.Limit.SubscriptionLimitVideo,
PlayoutDelay: roomInternal.GetPlayoutDelay(),
SyncStreams: roomInternal.GetSyncStreams(),
ForwardStats: r.forwardStats,
MetricConfig: r.config.Metric,
UseOneShotSignallingMode: useOneShotSignallingMode,
DataChannelMaxBufferedAmount: r.config.RTC.DataChannelMaxBufferedAmount,
DatachannelSlowThreshold: r.config.RTC.DatachannelSlowThreshold,
DatachannelLossyTargetLatency: r.config.RTC.DatachannelLossyTargetLatency,
FireOnTrackBySdp: true,
UseSinglePeerConnection: pi.UseSinglePeerConnection,
EnableDataTracks: r.config.EnableDataTracks,
EnableRTPStreamRestartDetection: r.config.RTC.EnableRTPStreamRestartDetection,
})
if err != nil {
return err
@@ -478,6 +534,9 @@ func (r *RoomManager) StartSession(
opts := rtc.ParticipantOptions{
AutoSubscribe: pi.AutoSubscribe,
}
if pi.AutoSubscribeDataTrack != nil {
opts.AutoSubscribeDataTrack = *pi.AutoSubscribeDataTrack
}
iceServers := r.iceServersForParticipant(apiKey, participant, iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TLS)
if err = room.Join(participant, requestSource, &opts, iceServers); err != nil {
pLogger.Errorw("could not join room", err)
@@ -485,16 +544,28 @@ func (r *RoomManager) StartSession(
return err
}
var participantServerClosers utils.Closers
participantTopic := rpc.FormatParticipantTopic(room.Name(), participant.Identity())
participantServer := must.Get(rpc.NewTypedParticipantServer(r, r.bus))
killParticipantServer := r.participantServers.Replace(participantTopic, participantServer)
participantServerClosers = append(participantServerClosers, utils.CloseFunc(r.participantServers.Replace(participantTopic, participantServer)))
if err := participantServer.RegisterAllParticipantTopics(participantTopic); err != nil {
killParticipantServer()
participantServerClosers.Close()
pLogger.Errorw("could not join register participant topic", err)
_ = participant.Close(true, types.ParticipantCloseReasonMessageBusFailed, false)
return err
}
if useOneShotSignallingMode {
whipParticipantServer := must.Get(rpc.NewTypedWHIPParticipantServer(whipParticipantService{r}, r.bus))
participantServerClosers = append(participantServerClosers, utils.CloseFunc(r.whipParticipantServers.Replace(participantTopic, whipParticipantServer)))
if err := whipParticipantServer.RegisterAllCommonTopics(participantTopic); err != nil {
participantServerClosers.Close()
pLogger.Errorw("could not join register participant topic for rtc rest participant server", err)
_ = participant.Close(true, types.ParticipantCloseReasonMessageBusFailed, false)
return err
}
}
if err = r.roomStore.StoreParticipant(ctx, room.Name(), participant.ToProto()); err != nil {
pLogger.Errorw("could not store participant", err)
}
@@ -512,9 +583,9 @@ func (r *RoomManager) StartSession(
persistRoomForParticipantCount(room.ToProto())
clientMeta := &livekit.AnalyticsClientMeta{Region: r.currentNode.Region(), Node: string(r.currentNode.NodeID())}
r.telemetry.ParticipantJoined(ctx, protoRoom, participant.ToProto(), pi.Client, clientMeta, true)
participant.OnClose(func(p types.LocalParticipant) {
killParticipantServer()
r.telemetry.ParticipantJoined(ctx, protoRoom, participant.ToProto(), pi.Client, clientMeta, true, participant.TelemetryGuard())
participant.AddOnClose(types.ParticipantCloseKeyNormal, func(p types.LocalParticipant) {
participantServerClosers.Close()
if err := r.roomStore.DeleteParticipant(ctx, room.Name(), p.Identity()); err != nil {
pLogger.Errorw("could not delete participant", err)
@@ -523,7 +594,7 @@ func (r *RoomManager) StartSession(
// update room store with new numParticipants
proto := room.ToProto()
persistRoomForParticipantCount(proto)
r.telemetry.ParticipantLeft(ctx, proto, p.ToProto(), true)
r.telemetry.ParticipantLeft(ctx, proto, p.ToProto(), true, participant.TelemetryGuard())
})
participant.OnClaimsChanged(func(participant types.LocalParticipant) {
pLogger.Debugw("refreshing client token after claims change")
@@ -535,6 +606,13 @@ func (r *RoomManager) StartSession(
r.iceConfigCache.Put(iceConfigCacheKey{room.Name(), participant.Identity()}, iceConfig)
})
for _, addTrackRequest := range pi.AddTrackRequests {
participant.AddTrack(addTrackRequest)
}
if pi.PublisherOffer != nil {
participant.HandleOffer(pi.PublisherOffer)
}
go r.rtcSessionWorker(room, participant, requestSource)
return nil
}
@@ -599,22 +677,22 @@ func (r *RoomManager) getOrCreateRoom(ctx context.Context, createRoom *livekit.C
r.telemetry.RoomEnded(ctx, roomInfo)
prometheus.RoomEnded(time.Unix(roomInfo.CreationTime, 0))
if err := r.deleteRoom(ctx, roomName); err != nil {
newRoom.Logger.Errorw("could not delete room", err)
newRoom.Logger().Errorw("could not delete room", err)
}
newRoom.Logger.Infow("room closed")
newRoom.Logger().Infow("room closed")
})
newRoom.OnRoomUpdated(func() {
if err := r.roomStore.StoreRoom(ctx, newRoom.ToProto(), newRoom.Internal()); err != nil {
newRoom.Logger.Errorw("could not handle metadata update", err)
newRoom.Logger().Errorw("could not handle metadata update", err)
}
})
newRoom.OnParticipantChanged(func(p types.LocalParticipant) {
newRoom.OnParticipantChanged(func(p types.Participant) {
if !p.IsDisconnected() {
if err := r.roomStore.StoreParticipant(ctx, roomName, p.ToProto()); err != nil {
newRoom.Logger.Errorw("could not handle participant change", err)
newRoom.Logger().Errorw("could not handle participant change", err)
}
}
})
@@ -648,12 +726,7 @@ func (r *RoomManager) getOrCreateRoom(ctx context.Context, createRoom *livekit.C
// manages an RTC session for a participant, runs on the RTC node
func (r *RoomManager) rtcSessionWorker(room *rtc.Room, participant types.LocalParticipant, requestSource routing.MessageSource) {
pLogger := rtc.LoggerWithParticipant(
rtc.LoggerWithRoom(logger.GetLogger(), room.Name(), room.ID()),
participant.Identity(),
participant.ID(),
false,
)
pLogger := participant.GetLogger()
defer func() {
pLogger.Debugw("RTC session finishing", "connID", requestSource.ConnectionID())
requestSource.Close()
@@ -673,11 +746,13 @@ func (r *RoomManager) rtcSessionWorker(room *rtc.Room, participant types.LocalPa
select {
case <-participant.Disconnected():
return
case <-tokenTicker.C:
// refresh token with the first API Key/secret pair
if err := r.refreshToken(participant); err != nil {
pLogger.Errorw("could not refresh token", err, "connID", requestSource.ConnectionID())
}
case obj := <-requestSource.ReadChan():
if obj == nil {
if room.GetParticipantRequestSource(participant.Identity()) == requestSource {
@@ -686,8 +761,7 @@ func (r *RoomManager) rtcSessionWorker(room *rtc.Room, participant types.LocalPa
return
}
req := obj.(*livekit.SignalRequest)
if err := rtc.HandleParticipantSignal(room, participant, req, pLogger); err != nil {
if err := participant.HandleSignalMessage(obj); err != nil {
// more specific errors are already logged
// treat errors returned as fatal
return
@@ -715,6 +789,37 @@ func (r *RoomManager) roomAndParticipantForReq(ctx context.Context, req particip
return room, participant, nil
}
func (r *RoomManager) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (*livekit.ListParticipantsResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
participants := room.GetParticipants()
items := make([]*livekit.ParticipantInfo, 0, len(participants))
for _, p := range participants {
items = append(items, p.ToProto())
}
return &livekit.ListParticipantsResponse{
Participants: items,
}, nil
}
func (r *RoomManager) GetParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.ParticipantInfo, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
participant := room.GetParticipant(livekit.ParticipantIdentity(req.Identity))
if participant == nil {
return nil, ErrParticipantNotFound
}
return participant.ToProto(), nil
}
func (r *RoomManager) RemoveParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.RemoveParticipantResponse, error) {
room, participant, err := r.roomAndParticipantForReq(ctx, req)
if err != nil {
@@ -738,7 +843,10 @@ func (r *RoomManager) MutePublishedTrack(ctx context.Context, req *livekit.MuteR
participant.GetLogger().Errorw("cannot unmute track, remote unmute is disabled", nil)
return nil, ErrRemoteUnmuteNoteEnabled
}
track := participant.SetTrackMuted(livekit.TrackID(req.TrackSid), req.Muted, true)
track := participant.SetTrackMuted(&livekit.MuteTrackRequest{
Sid: req.TrackSid,
Muted: req.Muted,
}, true)
return &livekit.MuteRoomTrackResponse{Track: track}, nil
}
@@ -748,31 +856,58 @@ func (r *RoomManager) UpdateParticipant(ctx context.Context, req *livekit.Update
return nil, err
}
participant.GetLogger().Debugw("updating participant",
"metadata", req.Metadata,
"permission", req.Permission,
"attributes", req.Attributes,
)
if err = participant.CheckMetadataLimits(req.Name, req.Metadata, req.Attributes); err != nil {
if err = participant.UpdateMetadata(&livekit.UpdateParticipantMetadata{
Name: req.Name,
Metadata: req.Metadata,
Attributes: req.Attributes,
}, true); err != nil {
return nil, err
}
if req.Name != "" {
participant.SetName(req.Name)
}
if req.Metadata != "" {
participant.SetMetadata(req.Metadata)
}
if req.Attributes != nil {
participant.SetAttributes(req.Attributes)
}
if req.Permission != nil {
participant.GetLogger().Debugw(
"updating participant permission",
"permission", req.Permission,
)
participant.SetPermission(req.Permission)
}
return participant.ToProto(), nil
}
func (r *RoomManager) ForwardParticipant(ctx context.Context, req *livekit.ForwardParticipantRequest) (*livekit.ForwardParticipantResponse, error) {
return nil, errors.New("not implemented")
}
func (r *RoomManager) MoveParticipant(ctx context.Context, req *livekit.MoveParticipantRequest) (*livekit.MoveParticipantResponse, error) {
return nil, errors.New("not implemented")
}
func (r *RoomManager) PerformRpc(ctx context.Context, req *livekit.PerformRpcRequest) (*livekit.PerformRpcResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.GetRoom()))
if room == nil {
return nil, ErrRoomNotFound
}
participant := room.GetParticipant(livekit.ParticipantIdentity(req.GetDestinationIdentity()))
if participant == nil {
return nil, ErrParticipantNotFound
}
resultChan := make(chan string, 1)
errorChan := make(chan error, 1)
participant.PerformRpc(req, resultChan, errorChan)
select {
case result := <-resultChan:
return &livekit.PerformRpcResponse{Payload: result}, nil
case err := <-errorChan:
return nil, err
}
}
func (r *RoomManager) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomRequest) (*livekit.DeleteRoomResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
@@ -784,7 +919,7 @@ func (r *RoomManager) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomReq
return nil, err
}
} else {
room.Logger.Infow("deleting room")
room.Logger().Infow("deleting room")
room.Close(types.ParticipantCloseReasonServiceRequestDeleteRoom)
}
return &livekit.DeleteRoomResponse{}, nil
@@ -812,7 +947,7 @@ func (r *RoomManager) SendData(ctx context.Context, req *livekit.SendDataRequest
return nil, ErrRoomNotFound
}
room.Logger.Debugw("api send data", "size", len(req.Data))
room.Logger().Debugw("api send data", "size", len(req.Data))
room.SendDataPacket(&livekit.DataPacket{
Kind: req.Kind,
DestinationIdentities: req.DestinationIdentities,
@@ -835,7 +970,7 @@ func (r *RoomManager) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat
return nil, ErrRoomNotFound
}
room.Logger.Debugw("updating room")
room.Logger().Debugw("updating room")
done := room.SetMetadata(req.Metadata)
// wait till the update is applied
<-done
@@ -903,19 +1038,20 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types.
if r.config.TURN.UDPPort > 0 && !tlsOnly {
// UDP TURN is used as STUN
hasSTUN = true
urls = append(urls, fmt.Sprintf("turn:%s:%d?transport=udp", r.config.RTC.NodeIP, r.config.TURN.UDPPort))
for _, ip := range r.config.RTC.NodeIP.ToStringSlice() {
urls = append(urls, fmt.Sprintf("turn:%s?transport=udp", net.JoinHostPort(ip, strconv.Itoa(int(r.config.TURN.UDPPort)))))
}
}
if r.config.TURN.TLSPort > 0 {
urls = append(urls, fmt.Sprintf("turns:%s:443?transport=tcp", r.config.TURN.Domain))
}
if len(urls) > 0 {
username := r.turnAuthHandler.CreateUsername(apiKey, participant.ID())
password, err := r.turnAuthHandler.CreatePassword(apiKey, participant.ID())
username, expiry := r.turnAuthHandler.CreateUsername(apiKey, participant.ID(), r.config.TURN.TTLSeconds)
password, err := r.turnAuthHandler.CreatePassword(apiKey, participant.ID(), expiry)
if err != nil {
participant.GetLogger().Warnw("could not create turn password", err)
hasSTUN = false
} else {
logger.Infow("created TURN password", "username", username, "password", password)
iceServers = append(iceServers, &livekit.ICEServer{
Urls: urls,
Username: username,
@@ -930,17 +1066,41 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types.
for _, s := range r.config.RTC.TURNServers {
scheme := "turn"
transport := "tcp"
if s.Protocol == "tls" {
switch s.Protocol {
case "tls":
scheme = "turns"
} else if s.Protocol == "udp" {
case "udp":
transport = "udp"
}
var username, credential string
if s.Secret != "" {
// Generate dynamic credentials using TURN static auth secrets
ttl := s.TTL
if ttl == 0 {
ttl = 14400 // Default 4 hours
}
expiry := time.Now().Add(time.Duration(ttl) * time.Second).Unix()
participantID := string(participant.ID())
username = fmt.Sprintf("%d:%s", expiry, participantID)
// HMAC-SHA1 signature
h := hmac.New(sha1.New, []byte(s.Secret))
h.Write([]byte(username))
credential = base64.StdEncoding.EncodeToString(h.Sum(nil))
} else {
// Use static credentials
username = s.Username
credential = s.Credential
}
is := &livekit.ICEServer{
Urls: []string{
fmt.Sprintf("%s:%s:%d?transport=%s", scheme, s.Host, s.Port, transport),
},
Username: s.Username,
Credential: s.Credential,
Username: username,
Credential: credential,
}
iceServers = append(iceServers, is)
}
@@ -967,6 +1127,7 @@ func (r *RoomManager) refreshToken(participant types.LocalParticipant) error {
token := auth.NewAccessToken(key, secret)
token.SetName(grants.Name).
SetIdentity(string(participant.Identity())).
SetKind(grants.GetParticipantKind()).
SetValidFor(tokenDefaultTTL).
SetMetadata(grants.Metadata).
SetAttributes(grants.Attributes).
@@ -1010,3 +1171,41 @@ func iceServerForStunServers(servers []string) *livekit.ICEServer {
}
return iceServer
}
// ------------------------------------
type roomManagerParticipantHelper struct {
room *rtc.Room
codecRegressionThreshold int
}
func (h *roomManagerParticipantHelper) GetParticipantInfo(pID livekit.ParticipantID) *livekit.ParticipantInfo {
if p := h.room.GetParticipantByID(pID); p != nil {
return p.ToProto()
}
return nil
}
func (h *roomManagerParticipantHelper) GetRegionSettings(ip string) *livekit.RegionSettings {
return nil
}
func (h *roomManagerParticipantHelper) GetSubscriberForwarderState(lp types.LocalParticipant) (map[livekit.TrackID]*livekit.RTPForwarderState, error) {
return nil, nil
}
func (h *roomManagerParticipantHelper) ResolveMediaTrack(lp types.LocalParticipant, trackID livekit.TrackID) types.MediaResolverResult {
return h.room.ResolveMediaTrackForSubscriber(lp, trackID)
}
func (h *roomManagerParticipantHelper) ResolveDataTrack(lp types.LocalParticipant, trackID livekit.TrackID) types.DataResolverResult {
return h.room.ResolveDataTrackForSubscriber(lp, trackID)
}
func (h *roomManagerParticipantHelper) ShouldRegressCodec() bool {
return h.codecRegressionThreshold == 0 || h.room.GetParticipantCount() < h.codecRegressionThreshold
}
func (h *roomManagerParticipantHelper) GetCachedReliableDataMessage(seqs map[livekit.ParticipantID]uint32) []*types.DataMessageCache {
return h.room.GetCachedReliableDataMessage(seqs)
}
@@ -0,0 +1,337 @@
package service
import (
"context"
"errors"
"fmt"
"time"
"github.com/pion/webrtc/v4"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/psrpc"
)
const (
whipSessionNotifyInterval = 10 * time.Second
)
type whipService struct {
*RoomManager
ingressRpcCli rpc.IngressHandlerClient
rpc.UnimplementedWHIPServer
}
func newWhipService(rm *RoomManager) (*whipService, error) {
cli, err := rpc.NewIngressHandlerClient(rm.bus, rpc.WithDefaultClientOptions(logger.GetLogger()))
if err != nil {
return nil, err
}
return &whipService{
RoomManager: rm,
ingressRpcCli: cli,
}, nil
}
func (s whipService) Create(ctx context.Context, req *rpc.WHIPCreateRequest) (*rpc.WHIPCreateResponse, error) {
pi, err := routing.ParticipantInitFromStartSession(req.StartSession, s.RoomManager.currentNode.Region())
if err != nil {
logger.Errorw("whip service: could not create participant init", err)
return nil, err
}
prometheus.IncrementParticipantRtcInit(1)
if err = s.RoomManager.StartSession(
ctx,
*pi,
routing.NewNullMessageSource(livekit.ConnectionID(req.StartSession.ConnectionId)), // no requestSource
routing.NewNullMessageSink(livekit.ConnectionID(req.StartSession.ConnectionId)), // no responseSink
true, // useOneShotSignallingMode
); err != nil {
logger.Errorw("whip service: could not start session", err)
return nil, err
}
room := s.RoomManager.GetRoom(ctx, livekit.RoomName(req.StartSession.RoomName))
if room == nil {
logger.Errorw("whip service: could not find room", nil, "room", req.StartSession.RoomName)
return nil, ErrRoomNotFound
}
lp := room.GetParticipant(pi.Identity)
if lp == nil {
room.Logger().Errorw("whip service: could not find local participant", nil, "participant", pi.Identity)
return nil, ErrParticipantNotFound
}
if err := lp.HandleOffer(&livekit.SessionDescription{
Type: webrtc.SDPTypeOffer.String(),
Sdp: req.OfferSdp,
Id: 0,
}); err != nil {
lp.GetLogger().Errorw("whip service: could not handle offer", err)
return nil, err
}
// wait for subscriptions to resolve
// NOTE: this is outside the WHIP spec, but added as a convenience for clients doing
// one-shot signalling (i. e. send an offer and get an answer once) to publish and subscribe to
// well-known tracks (i. e. remote participant identity and track names are well known)
eg, _ := errgroup.WithContext(ctx)
for publisherIdentity, trackList := range req.SubscribedParticipantTracks {
for _, trackName := range trackList.TrackNames {
eg.Go(func() error {
for {
if lp.IsTrackNameSubscribed(livekit.ParticipantIdentity(publisherIdentity), trackName) {
return nil
}
time.Sleep(50 * time.Millisecond)
}
})
}
}
err = eg.Wait()
if err != nil {
lp.GetLogger().Errorw("whip service: could not subscribe to tracks", err)
return nil, err
}
answer, _, err := lp.GetAnswer()
if err != nil {
lp.GetLogger().Errorw("whip service: could not get answer", err)
return nil, err
}
iceSessionID, err := lp.GetPublisherICESessionUfrag()
if err != nil {
lp.GetLogger().Errorw("whip service: could not get ICE session ID", err)
return nil, err
}
if req.FromIngress {
aliveCtx, cancel := context.WithCancel(context.Background())
lp.AddOnClose(types.ParticipantCloseKeyWHIP, func(lp types.LocalParticipant) {
cancel()
go func() {
lp.GetLogger().Debugw("whip service: notify participant closed")
video, audio := getMediaStateForParticipant(lp)
_, err := s.ingressRpcCli.WHIPRTCConnectionNotify(context.Background(), string(lp.ID()), &rpc.WHIPRTCConnectionNotifyRequest{
ParticipantId: string(lp.ID()),
Closed: true,
Audio: audio,
Video: video,
}, psrpc.WithRequestTimeout(rpc.DefaultPSRPCConfig.Timeout))
if err != nil {
lp.GetLogger().Warnw("whip service: could not notify ingress of participant closed", err)
}
}()
})
go func() {
if err := s.notifySession(aliveCtx, lp); err != nil {
cancel()
}
}()
}
var iceServers []*livekit.ICEServer
apiKey, _, err := s.RoomManager.getFirstKeyPair()
if err == nil {
iceServers = s.RoomManager.iceServersForParticipant(
apiKey,
lp,
false,
)
}
return &rpc.WHIPCreateResponse{
AnswerSdp: answer.SDP,
ParticipantId: string(lp.ID()),
IceServers: iceServers,
IceSessionId: iceSessionID,
}, nil
}
func (s whipService) notifySession(ctx context.Context, participant types.Participant) error {
ticker := time.NewTicker(whipSessionNotifyInterval)
defer ticker.Stop()
err := s.sendConnectionNotify(ctx, participant)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
}
for {
select {
case <-ticker.C:
err := s.sendConnectionNotify(ctx, participant)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil
}
}
case <-ctx.Done():
return nil
}
}
}
func (s whipService) sendConnectionNotify(ctx context.Context, participant types.Participant) error {
video, audio := getMediaStateForParticipant(participant)
_, err := s.ingressRpcCli.WHIPRTCConnectionNotify(ctx, string(participant.ID()), &rpc.WHIPRTCConnectionNotifyRequest{
ParticipantId: string(participant.ID()),
Video: video,
Audio: audio,
}, psrpc.WithRequestTimeout(rpc.DefaultPSRPCConfig.Timeout))
return err
}
func getMediaStateForParticipant(participant types.Participant) (*livekit.InputVideoState, *livekit.InputAudioState) {
pParticipant := participant.ToProto()
var video *livekit.InputVideoState
var audio *livekit.InputAudioState
for _, v := range pParticipant.Tracks {
if v == nil {
continue
}
if v.Type != livekit.TrackType_VIDEO {
continue
}
video = &livekit.InputVideoState{}
video.MimeType = v.MimeType
video.Height = v.Height
video.Width = v.Width
break
}
for _, a := range pParticipant.Tracks {
if a == nil {
continue
}
if a.Type != livekit.TrackType_AUDIO {
continue
}
audio = &livekit.InputAudioState{}
audio.MimeType = a.MimeType
audio.Channels = 1
if a.Stereo {
audio.Channels = 2
}
break
}
return video, audio
}
// -------------------------------------------
type whipParticipantService struct {
*RoomManager
}
func (r whipParticipantService) ICETrickle(ctx context.Context, req *rpc.WHIPParticipantICETrickleRequest) (*emptypb.Empty, error) {
room := r.RoomManager.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
lp := room.GetParticipantByID(livekit.ParticipantID(req.ParticipantId))
if lp == nil {
return nil, ErrParticipantNotFound
}
iceSessionID, err := lp.GetPublisherICESessionUfrag()
if err != nil {
lp.GetLogger().Warnw("whipParticipant service ice-trickle: could not get ICE session ufrag", err)
return nil, psrpc.NewError(psrpc.Internal, err)
}
if req.IceSessionId != iceSessionID {
return nil, psrpc.NewError(
psrpc.FailedPrecondition,
fmt.Errorf("ice session mismatch, expected: %s, got: %s", iceSessionID, req.IceSessionId),
)
}
if err := lp.HandleICETrickleSDPFragment(req.SdpFragment); err != nil {
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
}
return &emptypb.Empty{}, nil
}
func (r whipParticipantService) ICERestart(ctx context.Context, req *rpc.WHIPParticipantICERestartRequest) (*rpc.WHIPParticipantICERestartResponse, error) {
room := r.RoomManager.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
lp := room.GetParticipantByID(livekit.ParticipantID(req.ParticipantId))
if lp == nil {
return nil, ErrParticipantNotFound
}
sdpFragment, err := lp.HandleICERestartSDPFragment(req.SdpFragment)
if err != nil {
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
}
iceSessionID, err := lp.GetPublisherICESessionUfrag()
if err != nil {
lp.GetLogger().Warnw("whipParticipant service ice-restart: could not get ICE session ufrag", err)
return nil, psrpc.NewError(psrpc.Internal, err)
}
return &rpc.WHIPParticipantICERestartResponse{
IceSessionId: iceSessionID,
SdpFragment: sdpFragment,
}, nil
}
func (r whipParticipantService) DeleteSession(ctx context.Context, req *rpc.WHIPParticipantDeleteSessionRequest) (*emptypb.Empty, error) {
room := r.RoomManager.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
lp := room.GetParticipantByID(livekit.ParticipantID(req.ParticipantId))
if lp != nil {
lp.AddOnClose(types.ParticipantCloseKeyWHIP, nil)
room.RemoveParticipant(
lp.Identity(),
lp.ID(),
types.ParticipantCloseReasonClientRequestLeave,
)
}
return &emptypb.Empty{}, nil
}
// --------------------------------
+98 -90
View File
@@ -24,11 +24,10 @@ import (
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/protocol/egress"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"github.com/livekit/psrpc"
)
type RoomService struct {
@@ -41,6 +40,9 @@ type RoomService struct {
topicFormatter rpc.TopicFormatter
roomClient rpc.TypedRoomClient
participantClient rpc.TypedParticipantClient
rpc.UnimplementedRoomServer
rpc.UnimplementedParticipantServer
}
func NewRoomService(
@@ -69,10 +71,9 @@ func NewRoomService(
}
func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (*livekit.Room, error) {
redactedReq := redactCreateRoomRequest(req)
RecordRequest(ctx, redactedReq)
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Name, "request", logger.Proto(redactedReq))
AppendLogFields(ctx, "room", req.Name, "request", logger.Proto(req))
if err := EnsureCreatePermission(ctx); err != nil {
return nil, twirpAuthError(err)
} else if req.Egress != nil && s.egressLauncher == nil {
@@ -127,13 +128,15 @@ func (s *RoomService) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomReq
return nil, twirpAuthError(err)
}
_, _, err := s.roomStore.LoadRoom(ctx, livekit.RoomName(req.Room), false)
exists, err := s.roomStore.RoomExists(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, err
} else if !exists {
return nil, ErrRoomNotFound
}
// ensure at least one node is available to handle the request
_, err = s.router.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: req.Room})
room, err := s.router.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: req.Room})
if err != nil {
return nil, err
}
@@ -143,13 +146,15 @@ func (s *RoomService) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomReq
return nil, err
}
err = s.roomStore.DeleteRoom(ctx, livekit.RoomName(req.Room))
if os, ok := s.roomStore.(OSSServiceStore); ok {
err = os.DeleteRoom(ctx, livekit.RoomName(req.Room))
}
res := &livekit.DeleteRoomResponse{}
RecordResponse(ctx, res)
RecordResponse(ctx, room)
return res, err
}
func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (*livekit.ListParticipantsResponse, error) {
func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (res *livekit.ListParticipantsResponse, err error) {
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room)
@@ -157,19 +162,29 @@ func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListPar
return nil, twirpAuthError(err)
}
participants, err := s.roomStore.ListParticipants(ctx, livekit.RoomName(req.Room))
if s.apiConf.EnablePsrpcForGetListParticpants {
res, err = s.roomClient.ListParticipants(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
} else if store, ok := s.roomStore.(OSSServiceStore); ok {
var participants []*livekit.ParticipantInfo
participants, err = store.ListParticipants(ctx, livekit.RoomName(req.Room))
if err == nil {
res = &livekit.ListParticipantsResponse{
Participants: participants,
}
}
} else {
err = psrpc.ErrUnimplemented
}
if err != nil {
return nil, err
}
res := &livekit.ListParticipantsResponse{
Participants: participants,
}
RecordResponse(ctx, res)
return res, nil
}
func (s *RoomService) GetParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.ParticipantInfo, error) {
func (s *RoomService) GetParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (participant *livekit.ParticipantInfo, err error) {
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity)
@@ -177,7 +192,14 @@ func (s *RoomService) GetParticipant(ctx context.Context, req *livekit.RoomParti
return nil, twirpAuthError(err)
}
participant, err := s.roomStore.LoadParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
if s.apiConf.EnablePsrpcForGetListParticpants {
participant, err = s.roomClient.GetParticipant(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
} else if store, ok := s.roomStore.(OSSServiceStore); ok {
participant, err = store.LoadParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
} else {
err = psrpc.ErrUnimplemented
}
if err != nil {
return nil, err
}
@@ -195,8 +217,13 @@ func (s *RoomService) RemoveParticipant(ctx context.Context, req *livekit.RoomPa
return nil, twirpAuthError(err)
}
if _, err := s.roomStore.LoadParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)); err == ErrParticipantNotFound {
return nil, twirp.NotFoundError("participant not found")
if os, ok := s.roomStore.(OSSServiceStore); ok {
found, err := os.HasParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
if err != nil {
return nil, err
} else if !found {
return nil, ErrParticipantNotFound
}
}
res, err := s.participantClient.RemoveParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
@@ -218,7 +245,7 @@ func (s *RoomService) MutePublishedTrack(ctx context.Context, req *livekit.MuteR
}
func (s *RoomService) UpdateParticipant(ctx context.Context, req *livekit.UpdateParticipantRequest) (*livekit.ParticipantInfo, error) {
RecordRequest(ctx, redactUpdateParticipantRequest(req))
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity)
@@ -238,6 +265,15 @@ func (s *RoomService) UpdateParticipant(ctx context.Context, req *livekit.Update
return nil, twirpAuthError(err)
}
if os, ok := s.roomStore.(OSSServiceStore); ok {
found, err := os.HasParticipant(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity))
if err != nil {
return nil, err
} else if !found {
return nil, ErrParticipantNotFound
}
}
res, err := s.participantClient.UpdateParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
RecordResponse(ctx, res)
return res, err
@@ -262,7 +298,7 @@ func (s *RoomService) UpdateSubscriptions(ctx context.Context, req *livekit.Upda
}
func (s *RoomService) SendData(ctx context.Context, req *livekit.SendDataRequest) (*livekit.SendDataResponse, error) {
RecordRequest(ctx, redactSendDataRequest(req))
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "size", len(req.Data))
@@ -281,7 +317,7 @@ func (s *RoomService) SendData(ctx context.Context, req *livekit.SendDataRequest
}
func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.UpdateRoomMetadataRequest) (*livekit.Room, error) {
RecordRequest(ctx, redactUpdateRoomMetadataRequest(req))
RecordRequest(ctx, req)
AppendLogFields(ctx, "room", req.Room, "size", len(req.Metadata))
maxMetadataSize := int(s.limitConf.MaxMetadataSize)
@@ -293,9 +329,11 @@ func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat
return nil, twirpAuthError(err)
}
_, _, err := s.roomStore.LoadRoom(ctx, livekit.RoomName(req.Room), false)
exists, err := s.roomStore.RoomExists(ctx, livekit.RoomName(req.Room))
if err != nil {
return nil, err
} else if !exists {
return nil, ErrRoomNotFound
}
room, err := s.roomClient.UpdateRoomMetadata(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
@@ -307,86 +345,56 @@ func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.Updat
return room, nil
}
func redactCreateRoomRequest(req *livekit.CreateRoomRequest) *livekit.CreateRoomRequest {
if req.Egress == nil && req.Metadata == "" {
// nothing to redact
return req
func (s *RoomService) ForwardParticipant(ctx context.Context, req *livekit.ForwardParticipantRequest) (*livekit.ForwardParticipantResponse, error) {
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "participant", req.Identity)
if err := EnsureDestRoomPermission(ctx, roomName, livekit.RoomName(req.DestinationRoom)); err != nil {
return nil, twirpAuthError(err)
}
clone := utils.CloneProto(req)
if clone.Egress != nil {
if clone.Egress.Room != nil {
egress.RedactEncodedOutputs(clone.Egress.Room)
}
if clone.Egress.Participant != nil {
egress.RedactAutoEncodedOutput(clone.Egress.Participant)
}
if clone.Egress.Tracks != nil {
egress.RedactUpload(clone.Egress.Tracks)
}
if req.Room == req.DestinationRoom {
return nil, twirp.InvalidArgumentError(ErrDestinationSameAsSourceRoom.Error(), "")
}
// replace with size of metadata to provide visibility on request size
if clone.Metadata != "" {
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
}
return clone
res, err := s.participantClient.ForwardParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
RecordResponse(ctx, res)
return res, err
}
func redactUpdateParticipantRequest(req *livekit.UpdateParticipantRequest) *livekit.UpdateParticipantRequest {
if req.Metadata == "" && len(req.Attributes) == 0 {
return req
func (s *RoomService) MoveParticipant(ctx context.Context, req *livekit.MoveParticipantRequest) (*livekit.MoveParticipantResponse, error) {
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "participant", req.Identity)
if err := EnsureDestRoomPermission(ctx, roomName, livekit.RoomName(req.DestinationRoom)); err != nil {
return nil, twirpAuthError(err)
}
clone := utils.CloneProto(req)
// replace with size of metadata/attributes to provide visibility on request size
if clone.Metadata != "" {
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
if req.Room == req.DestinationRoom {
return nil, twirp.InvalidArgumentError(ErrDestinationSameAsSourceRoom.Error(), "")
}
if len(clone.Attributes) != 0 {
keysSize := 0
valuesSize := 0
for k, v := range clone.Attributes {
keysSize += len(k)
valuesSize += len(v)
}
clone.Attributes = map[string]string{
"__num_elements": fmt.Sprintf("%d", len(clone.Attributes)),
"__keys_size": fmt.Sprintf("%d", keysSize),
"__values_size": fmt.Sprintf("%d", valuesSize),
}
}
return clone
res, err := s.participantClient.MoveParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
RecordResponse(ctx, res)
return res, err
}
func redactSendDataRequest(req *livekit.SendDataRequest) *livekit.SendDataRequest {
if len(req.Data) == 0 {
return req
func (s *RoomService) PerformRpc(ctx context.Context, req *livekit.PerformRpcRequest) (*livekit.PerformRpcResponse, error) {
RecordRequest(ctx, req)
roomName := livekit.RoomName(req.Room)
AppendLogFields(ctx, "room", roomName, "participant", req.DestinationIdentity)
if err := EnsureAdminPermission(ctx, roomName); err != nil {
return nil, twirpAuthError(err)
}
if req.DestinationIdentity == "" {
return nil, ErrDestinationIdentityRequired
}
clone := utils.CloneProto(req)
// replace with size of data to provide visibility on request size
clone.Data = []byte(fmt.Sprintf("__size: %d", len(clone.Data)))
return clone
}
func redactUpdateRoomMetadataRequest(req *livekit.UpdateRoomMetadataRequest) *livekit.UpdateRoomMetadataRequest {
if req.Metadata == "" {
return req
}
clone := utils.CloneProto(req)
// replace with size of metadata to provide visibility on request size
clone.Metadata = fmt.Sprintf("__size: %d", len(clone.Metadata))
return clone
res, err := s.participantClient.PerformRpc(ctx, s.topicFormatter.ParticipantTopic(ctx, roomName, livekit.ParticipantIdentity(req.DestinationIdentity)), req)
RecordResponse(ctx, res)
return res, err
}
+329 -213
View File
@@ -16,8 +16,11 @@ package service
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"maps"
"math/rand"
"net/http"
"os"
@@ -26,31 +29,28 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/ua-parser/uap-go/uaparser"
"go.uber.org/atomic"
"golang.org/x/exp/maps"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/routing/selector"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/psrpc"
)
type RTCService struct {
router routing.MessageRouter
roomAllocator RoomAllocator
store ServiceStore
upgrader websocket.Upgrader
currentNode routing.LocalNode
config *config.Config
isDev bool
limits config.LimitConfig
parser *uaparser.Parser
telemetry telemetry.TelemetryService
mu sync.Mutex
@@ -60,20 +60,15 @@ type RTCService struct {
func NewRTCService(
conf *config.Config,
ra RoomAllocator,
store ServiceStore,
router routing.MessageRouter,
currentNode routing.LocalNode,
telemetry telemetry.TelemetryService,
) *RTCService {
s := &RTCService{
router: router,
roomAllocator: ra,
store: store,
currentNode: currentNode,
config: conf,
isDev: conf.Development,
limits: conf.Limit,
parser: uaparser.NewFromSaved(),
telemetry: telemetry,
connections: map[*websocket.Conn]struct{}{},
}
@@ -92,145 +87,294 @@ func NewRTCService(
}
func (s *RTCService) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/rtc/validate", s.validate)
mux.HandleFunc("/rtc", s.v0)
mux.HandleFunc("/rtc/validate", s.v0Validate)
mux.HandleFunc("/rtc/v1", s.v1)
mux.HandleFunc("/rtc/v1/validate", s.v1Validate)
}
func (s *RTCService) validate(w http.ResponseWriter, r *http.Request) {
_, _, code, err := s.validateInternal(r)
func (s *RTCService) v0Validate(w http.ResponseWriter, r *http.Request) {
lgr := utils.GetLogger(r.Context())
_, _, code, err := s.validateInternal(lgr, r, false, true)
if err != nil {
handleError(w, r, code, err)
HandleError(w, r, code, err)
return
}
_, _ = w.Write([]byte("success"))
}
func (s *RTCService) validateInternal(r *http.Request) (livekit.RoomName, routing.ParticipantInit, int, error) {
claims := GetGrants(r.Context())
var pi routing.ParticipantInit
// require a claim
if claims == nil || claims.Video == nil {
return "", pi, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
onlyName, err := EnsureJoinPermission(r.Context())
func (s *RTCService) v1Validate(w http.ResponseWriter, r *http.Request) {
lgr := utils.GetLogger(r.Context())
_, _, code, err := s.validateInternal(lgr, r, true, true)
if err != nil {
return "", pi, http.StatusUnauthorized, err
HandleError(w, r, code, err)
return
}
if claims.Identity == "" {
return "", pi, http.StatusBadRequest, ErrIdentityEmpty
}
if limit := s.config.Limit.MaxParticipantIdentityLength; limit > 0 && len(claims.Identity) > limit {
return "", pi, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, limit)
}
roomName := livekit.RoomName(r.FormValue("room"))
reconnectParam := r.FormValue("reconnect")
reconnectReason, _ := strconv.Atoi(r.FormValue("reconnect_reason")) // 0 means unknown reason
autoSubParam := r.FormValue("auto_subscribe")
publishParam := r.FormValue("publish")
adaptiveStreamParam := r.FormValue("adaptive_stream")
participantID := r.FormValue("sid")
subscriberAllowPauseParam := r.FormValue("subscriber_allow_pause")
disableICELite := r.FormValue("disable_ice_lite")
if onlyName != "" {
roomName = onlyName
}
if limit := s.config.Limit.MaxRoomNameLength; limit > 0 && len(roomName) > limit {
return "", pi, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, limit)
}
// this is new connection for existing participant - with publish only permissions
if publishParam != "" {
// Make sure grant has GetCanPublish set,
if !claims.Video.GetCanPublish() {
return "", routing.ParticipantInit{}, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
// Make sure by default subscribe is off
claims.Video.SetCanSubscribe(false)
claims.Identity += "#" + publishParam
}
// room allocator validations
err = s.roomAllocator.ValidateCreateRoom(r.Context(), roomName)
if err != nil {
if errors.Is(err, ErrRoomNotFound) {
return "", pi, http.StatusNotFound, err
} else {
return "", pi, http.StatusInternalServerError, err
}
}
region := ""
if router, ok := s.router.(routing.Router); ok {
region = router.GetRegion()
if foundNode, err := router.GetNodeForRoom(r.Context(), roomName); err == nil {
if selector.LimitsReached(s.limits, foundNode.Stats) {
return "", pi, http.StatusServiceUnavailable, rtc.ErrLimitExceeded
}
}
}
createRequest := &livekit.CreateRoomRequest{
Name: string(roomName),
RoomPreset: claims.RoomPreset,
}
SetRoomConfiguration(createRequest, claims.GetRoomConfiguration())
pi = routing.ParticipantInit{
Reconnect: boolValue(reconnectParam),
ReconnectReason: livekit.ReconnectReason(reconnectReason),
Identity: livekit.ParticipantIdentity(claims.Identity),
Name: livekit.ParticipantName(claims.Name),
AutoSubscribe: true,
Client: s.ParseClientInfo(r),
Grants: claims,
Region: region,
CreateRoom: createRequest,
}
if pi.Reconnect {
pi.ID = livekit.ParticipantID(participantID)
}
if autoSubParam != "" {
pi.AutoSubscribe = boolValue(autoSubParam)
}
if adaptiveStreamParam != "" {
pi.AdaptiveStream = boolValue(adaptiveStreamParam)
}
if subscriberAllowPauseParam != "" {
subscriberAllowPause := boolValue(subscriberAllowPauseParam)
pi.SubscriberAllowPause = &subscriberAllowPause
}
if disableICELite != "" {
pi.DisableICELite = boolValue(disableICELite)
}
return roomName, pi, http.StatusOK, nil
_, _ = w.Write([]byte("success"))
}
func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func decodeAttributes(str string) (map[string]string, error) {
data, err := base64.URLEncoding.DecodeString(str)
if err != nil {
return nil, err
}
var attrs map[string]string
if err := json.Unmarshal(data, &attrs); err != nil {
return nil, err
}
return attrs, nil
}
var errJoinRequestTooLarge = errors.New("join request too large")
func (s *RTCService) validateInternal(
lgr logger.Logger,
r *http.Request,
needsJoinRequest bool,
strict bool,
) (livekit.RoomName, routing.ParticipantInit, int, error) {
if claims := GetGrants(r.Context()); claims == nil || claims.Video == nil {
return "", routing.ParticipantInit{}, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
var params ValidateConnectRequestParams
useSinglePeerConnection := false
joinRequest := &livekit.JoinRequest{}
wrappedJoinRequestBase64 := r.FormValue("join_request")
if wrappedJoinRequestBase64 == "" {
if needsJoinRequest {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("join_request is required")
}
params.publish = r.FormValue("publish")
attributesStrParam := r.FormValue("attributes")
if attributesStrParam != "" {
attrs, err := decodeAttributes(attributesStrParam)
if err != nil {
if strict {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot decode attributes")
}
lgr.Debugw("failed to decode attributes", "error", err)
// attrs will be empty here, so just proceed
}
params.attributes = attrs
}
} else {
useSinglePeerConnection = true
if wrappedProtoBytes, err := base64.URLEncoding.DecodeString(wrappedJoinRequestBase64); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot base64 decode wrapped join request")
} else {
wrappedJoinRequest := &livekit.WrappedJoinRequest{}
if err := proto.Unmarshal(wrappedProtoBytes, wrappedJoinRequest); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot unmarshal wrapped join request")
}
switch wrappedJoinRequest.Compression {
case livekit.WrappedJoinRequest_NONE:
if len(wrappedJoinRequest.JoinRequest) > http.DefaultMaxHeaderBytes {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errJoinRequestTooLarge
}
if err := proto.Unmarshal(wrappedJoinRequest.JoinRequest, joinRequest); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot unmarshal join request")
}
case livekit.WrappedJoinRequest_GZIP:
protoBytes, err := DecompressGzip(wrappedJoinRequest.JoinRequest)
if err != nil {
switch {
case errors.Is(err, ErrGzipTooLarge):
err = errJoinRequestTooLarge
case errors.Is(err, ErrGzipReadFailed):
err = errors.New("cannot read decompressed join request")
}
return "", routing.ParticipantInit{}, http.StatusBadRequest, err
}
if err := proto.Unmarshal(protoBytes, joinRequest); err != nil {
return "", routing.ParticipantInit{}, http.StatusBadRequest, errors.New("cannot unmarshal join request")
}
}
params.metadata = joinRequest.Metadata
params.attributes = joinRequest.ParticipantAttributes
}
}
res, code, err := ValidateConnectRequest(
lgr,
r,
s.limits,
params,
s.router,
s.roomAllocator,
)
if err != nil {
return res.roomName, routing.ParticipantInit{}, code, err
}
pi := routing.ParticipantInit{
Identity: livekit.ParticipantIdentity(res.grants.Identity),
Name: livekit.ParticipantName(res.grants.Name),
Grants: res.grants,
Region: res.region,
CreateRoom: res.createRoomRequest,
UseSinglePeerConnection: useSinglePeerConnection,
}
if wrappedJoinRequestBase64 == "" {
pi.Reconnect = boolValue(r.FormValue("reconnect"))
pi.Client = ParseClientInfo(r)
pi.AutoSubscribe = true
if autoSubscribeParam := r.FormValue("auto_subscribe"); autoSubscribeParam != "" {
pi.AutoSubscribe = boolValue(autoSubscribeParam)
}
if autoSubscribeDataTrackParam := r.FormValue("auto_subscribe_data_track"); autoSubscribeDataTrackParam != "" {
autoSubscribeDataTrack := boolValue(autoSubscribeDataTrackParam)
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
}
pi.AdaptiveStream = boolValue(r.FormValue("adaptive_stream"))
pi.DisableICELite = boolValue(r.FormValue("disable_ice_lite"))
reconnectReason, _ := strconv.Atoi(r.FormValue("reconnect_reason")) // 0 means unknown reason
pi.ReconnectReason = livekit.ReconnectReason(reconnectReason)
if pi.Reconnect {
pi.ID = livekit.ParticipantID(r.FormValue("sid"))
}
if subscriberAllowPauseParam := r.FormValue("subscriber_allow_pause"); subscriberAllowPauseParam != "" {
subscriberAllowPause := boolValue(subscriberAllowPauseParam)
pi.SubscriberAllowPause = &subscriberAllowPause
}
} else {
lgr.Debugw("processing join request", "joinRequest", logger.Proto(joinRequest))
if joinRequest.ClientInfo == nil {
joinRequest.ClientInfo = &livekit.ClientInfo{}
}
AugmentClientInfo(joinRequest.ClientInfo, r)
pi.Client = joinRequest.ClientInfo
pi.AutoSubscribe = joinRequest.GetConnectionSettings().GetAutoSubscribe()
autoSubscribeDataTrack := joinRequest.GetConnectionSettings().GetAutoSubscribeDataTrack()
pi.AutoSubscribeDataTrack = &autoSubscribeDataTrack
pi.AdaptiveStream = joinRequest.GetConnectionSettings().GetAdaptiveStream()
pi.DisableICELite = joinRequest.GetConnectionSettings().GetDisableIceLite()
subscriberAllowPause := joinRequest.GetConnectionSettings().GetSubscriberAllowPause()
pi.SubscriberAllowPause = &subscriberAllowPause
pi.AddTrackRequests = joinRequest.AddTrackRequests
pi.PublisherOffer = joinRequest.PublisherOffer
pi.Reconnect = joinRequest.Reconnect
pi.ReconnectReason = joinRequest.ReconnectReason
pi.ID = livekit.ParticipantID(joinRequest.ParticipantSid)
}
return res.roomName, pi, code, err
}
func (s *RTCService) v0(w http.ResponseWriter, r *http.Request) {
s.serve(w, r, false)
}
func (s *RTCService) v1(w http.ResponseWriter, r *http.Request) {
s.serve(w, r, true)
}
func (s *RTCService) serve(w http.ResponseWriter, r *http.Request, needsJoinRequest bool) {
// reject non websocket requests
if !websocket.IsWebSocketUpgrade(r) {
w.WriteHeader(404)
return
}
roomName, pi, code, err := s.validateInternal(r)
startedAt := time.Now()
var (
roomName livekit.RoomName
roomID livekit.RoomID
participantIdentity livekit.ParticipantIdentity
pID livekit.ParticipantID
joinDuration time.Duration
loggerResolved bool
pi routing.ParticipantInit
code int
err error
)
pLogger, loggerResolver := utils.GetLogger(r.Context()).WithDeferredValues()
getLoggerFields := func() []any {
return []any{
"room", roomName,
"roomID", roomID,
"participant", participantIdentity,
"participantID", pID,
"joinDuration", joinDuration,
}
}
resolveLogger := func(force bool) {
if loggerResolved {
return
}
if force {
if roomName == "" {
roomName = "unresolved"
}
if roomID == "" {
roomID = "unresolved"
}
if participantIdentity == "" {
participantIdentity = "unresolved"
}
if pID == "" {
pID = "unresolved"
}
if joinDuration == 0 {
joinDuration = time.Since(startedAt)
}
}
if roomName != "" && roomID != "" && participantIdentity != "" && pID != "" {
loggerResolved = true
loggerResolver.Resolve(getLoggerFields()...)
}
}
resetLogger := func() {
loggerResolver.Reset()
roomName = ""
roomID = ""
participantIdentity = ""
pID = ""
loggerResolved = false
}
roomName, pi, code, err = s.validateInternal(pLogger, r, needsJoinRequest, false)
if err != nil {
handleError(w, r, code, err)
prometheus.IncrementParticipantJoinValidationFail(1)
resolveLogger(true)
HandleError(w, r, code, err, getLoggerFields()...)
return
}
loggerFields := []any{
"participant", pi.Identity,
"pID", pi.ID,
"room", roomName,
"remote", false,
participantIdentity = pi.Identity
if pi.ID != "" {
pID = pi.ID
}
pLogger := utils.GetLogger(r.Context()).WithValues(loggerFields...)
pLogger.Debugw("join request validated", append(getLoggerFields(), "participantInit", &pi)...)
// give it a few attempts to start session
var cr connectionResult
@@ -251,17 +395,28 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if errors.As(err, &psrpcErr) {
status = psrpcErr.ToHttp()
}
handleError(w, r, status, err, loggerFields...)
resolveLogger(true)
HandleError(w, r, status, err, getLoggerFields()...)
return
}
prometheus.IncrementParticipantJoin(1)
joinDuration = time.Since(startedAt)
pLogger = pLogger.WithValues("connID", cr.ConnectionID)
if !pi.Reconnect && initialResponse.GetJoin() != nil {
joinRoomID := livekit.RoomID(initialResponse.GetJoin().GetRoom().GetSid())
if joinRoomID != "" {
roomID = joinRoomID
}
pi.ID = livekit.ParticipantID(initialResponse.GetJoin().GetParticipant().GetSid())
pID = pi.ID
resolveLogger(false)
}
signalStats := telemetry.NewBytesSignalStats(r.Context(), s.telemetry)
signalStats := rtc.NewBytesSignalStats(r.Context(), s.telemetry)
if join := initialResponse.GetJoin(); join != nil {
signalStats.ResolveRoom(join.GetRoom())
signalStats.ResolveParticipant(join.GetParticipant())
@@ -277,10 +432,8 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
done := make(chan struct{})
// function exits when websocket terminates, it'll close the event reading off of request sink and response source as well
defer func() {
pLogger.Debugw("finishing WS connection",
"connID", cr.ConnectionID,
"closedByClient", closedByClient.Load(),
)
resolveLogger(true)
pLogger.Debugw("finishing WS connection", "closedByClient", closedByClient.Load())
cr.ResponseSource.Close()
cr.RequestSink.Close()
close(done)
@@ -291,7 +444,8 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// upgrade only once the basics are good to go
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
handleError(w, r, http.StatusInternalServerError, err, loggerFields...)
resolveLogger(true)
HandleError(w, r, http.StatusInternalServerError, err, getLoggerFields()...)
return
}
@@ -307,15 +461,17 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// websocket established
sigConn := NewWSSignalConnection(conn)
pLogger.Debugw("sending initial response", "response", logger.Proto(initialResponse))
count, err := sigConn.WriteResponse(initialResponse)
if err != nil {
resolveLogger(true)
pLogger.Warnw("could not write initial response", err)
return
}
signalStats.AddBytes(uint64(count), true)
pLogger.Debugw("new client WS connected",
"connID", cr.ConnectionID,
pLogger.Debugw(
"new client WS connected",
"reconnect", pi.Reconnect,
"reconnectReason", pi.ReconnectReason,
"adaptiveStream", pi.AdaptiveStream,
@@ -343,7 +499,8 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
case msg := <-cr.ResponseSource.ReadChan():
if msg == nil {
pLogger.Debugw("nothing to read from response source", "connID", cr.ConnectionID)
resolveLogger(true)
pLogger.Debugw("nothing to read from response source")
return
}
res, ok := msg.(*livekit.SignalResponse)
@@ -351,7 +508,6 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pLogger.Errorw(
"unexpected message type", nil,
"type", fmt.Sprintf("%T", msg),
"connID", cr.ConnectionID,
)
continue
}
@@ -359,17 +515,46 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch m := res.Message.(type) {
case *livekit.SignalResponse_Offer:
pLogger.Debugw("sending offer", "offer", m)
case *livekit.SignalResponse_Answer:
pLogger.Debugw("sending answer", "answer", m)
case *livekit.SignalResponse_Join:
pLogger.Debugw("sending join", "join", m)
signalStats.ResolveRoom(m.Join.GetRoom())
signalStats.ResolveParticipant(m.Join.GetParticipant())
case *livekit.SignalResponse_RoomUpdate:
updateRoomID := livekit.RoomID(m.RoomUpdate.GetRoom().GetSid())
if updateRoomID != "" {
roomID = updateRoomID
resolveLogger(false)
}
pLogger.Debugw("sending room update", "roomUpdate", m)
signalStats.ResolveRoom(m.RoomUpdate.GetRoom())
case *livekit.SignalResponse_Update:
pLogger.Debugw("sending participant update", "participantUpdate", m)
case *livekit.SignalResponse_RoomMoved:
resetLogger()
signalStats.Reset()
roomName = livekit.RoomName(m.RoomMoved.GetRoom().GetName())
moveRoomID := livekit.RoomID(m.RoomMoved.GetRoom().GetSid())
if moveRoomID != "" {
roomID = moveRoomID
}
participantIdentity = livekit.ParticipantIdentity(m.RoomMoved.GetParticipant().GetIdentity())
pID = livekit.ParticipantID(m.RoomMoved.GetParticipant().GetSid())
resolveLogger(false)
signalStats.ResolveRoom(m.RoomMoved.GetRoom())
signalStats.ResolveParticipant(m.RoomMoved.GetParticipant())
pLogger.Debugw("sending room moved", "roomMoved", m)
default:
pLogger.Debugw("sending signal response", "response", m)
}
if count, err := sigConn.WriteResponse(res); err != nil {
@@ -389,7 +574,7 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if IsWebSocketCloseError(err) {
closedByClient.Store(true)
} else {
pLogger.Errorw("error reading from websocket", err, "connID", cr.ConnectionID)
pLogger.Errorw("error reading from websocket", err)
}
return
}
@@ -428,86 +613,17 @@ func (s *RTCService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
pLogger.Debugw("received offer", "offer", m)
case *livekit.SignalRequest_Answer:
pLogger.Debugw("received answer", "answer", m)
default:
pLogger.Debugw("received signal request", "request", m)
}
if err := cr.RequestSink.WriteMessage(req); err != nil {
pLogger.Warnw("error writing to request sink", err, "connID", cr.ConnectionID)
pLogger.Warnw("error writing to request sink", err)
return
}
}
}
func (s *RTCService) ParseClientInfo(r *http.Request) *livekit.ClientInfo {
values := r.Form
ci := &livekit.ClientInfo{}
if pv, err := strconv.Atoi(values.Get("protocol")); err == nil {
ci.Protocol = int32(pv)
}
sdkString := values.Get("sdk")
switch sdkString {
case "js":
ci.Sdk = livekit.ClientInfo_JS
case "ios", "swift":
ci.Sdk = livekit.ClientInfo_SWIFT
case "android":
ci.Sdk = livekit.ClientInfo_ANDROID
case "flutter":
ci.Sdk = livekit.ClientInfo_FLUTTER
case "go":
ci.Sdk = livekit.ClientInfo_GO
case "unity":
ci.Sdk = livekit.ClientInfo_UNITY
case "reactnative":
ci.Sdk = livekit.ClientInfo_REACT_NATIVE
case "rust":
ci.Sdk = livekit.ClientInfo_RUST
case "python":
ci.Sdk = livekit.ClientInfo_PYTHON
case "cpp":
ci.Sdk = livekit.ClientInfo_CPP
case "unityweb":
ci.Sdk = livekit.ClientInfo_UNITY_WEB
case "node":
ci.Sdk = livekit.ClientInfo_NODE
}
ci.Version = values.Get("version")
ci.Os = values.Get("os")
ci.OsVersion = values.Get("os_version")
ci.Browser = values.Get("browser")
ci.BrowserVersion = values.Get("browser_version")
ci.DeviceModel = values.Get("device_model")
ci.Network = values.Get("network")
// get real address (forwarded http header) - check Cloudflare headers first, fall back to X-Forwarded-For
ci.Address = GetClientIP(r)
// attempt to parse types for SDKs that support browser as a platform
if ci.Sdk == livekit.ClientInfo_JS ||
ci.Sdk == livekit.ClientInfo_REACT_NATIVE ||
ci.Sdk == livekit.ClientInfo_FLUTTER ||
ci.Sdk == livekit.ClientInfo_UNITY {
client := s.parser.Parse(r.UserAgent())
if ci.Browser == "" {
ci.Browser = client.UserAgent.Family
ci.BrowserVersion = client.UserAgent.ToVersionString()
}
if ci.Os == "" {
ci.Os = client.Os.Family
ci.OsVersion = client.Os.ToVersionString()
}
if ci.DeviceModel == "" {
model := client.Device.Family
if model != "" && client.Device.Model != "" && model != client.Device.Model {
model += " " + client.Device.Model
}
ci.DeviceModel = model
}
}
return ci
}
func (s *RTCService) DrainConnections(interval time.Duration) {
s.mu.Lock()
conns := maps.Clone(s.connections)
+12 -6
View File
@@ -27,7 +27,7 @@ import (
"strconv"
"time"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/cors"
"github.com/twitchtv/twirp"
@@ -49,6 +49,7 @@ type LivekitServer struct {
config *config.Config
ioService *IOInfoService
rtcService *RTCService
whipService *WHIPService
agentService *AgentService
httpServer *http.Server
promServer *http.Server
@@ -70,6 +71,7 @@ func NewLivekitServer(conf *config.Config,
sipService *SIPService,
ioService *IOInfoService,
rtcService *RTCService,
whipService *WHIPService,
agentService *AgentService,
keyProvider auth.KeyProvider,
router routing.Router,
@@ -82,6 +84,7 @@ func NewLivekitServer(conf *config.Config,
config: conf,
ioService: ioService,
rtcService: rtcService,
whipService: whipService,
agentService: agentService,
router: router,
roomManager: roomManager,
@@ -100,7 +103,9 @@ func NewLivekitServer(conf *config.Config,
AllowOriginFunc: func(origin string) bool {
return true
},
AllowedMethods: []string{"OPTIONS", "HEAD", "GET", "POST", "PATCH", "DELETE"},
AllowedHeaders: []string{"*"},
ExposedHeaders: []string{"*"},
// allow preflight to be cached for a day
MaxAge: 86400,
}),
@@ -110,9 +115,10 @@ func NewLivekitServer(conf *config.Config,
middlewares = append(middlewares, NewAPIKeyAuthMiddleware(keyProvider))
}
serverOptions := []interface{}{
serverOptions := []any{
twirp.WithServerHooks(twirp.ChainHooks(
TwirpLogger(),
TwirpEgressID(),
TwirpRequestStatusReporter(),
)),
}
@@ -138,8 +144,8 @@ func NewLivekitServer(conf *config.Config,
xtwirp.RegisterServer(mux, egressServer)
xtwirp.RegisterServer(mux, ingressServer)
xtwirp.RegisterServer(mux, sipServer)
mux.Handle("/rtc", rtcService)
rtcService.SetupRoutes(mux)
whipService.SetupRoutes(mux)
mux.Handle("/agent", agentService)
mux.HandleFunc("/", s.defaultHandler)
@@ -231,7 +237,7 @@ func (s *LivekitServer) Start() error {
}
}
values := []interface{}{
values := []any{
"portHttp", s.config.Port,
"nodeID", s.currentNode.NodeID(),
"nodeIP", s.currentNode.NodeIP(),
@@ -342,7 +348,7 @@ func (s *LivekitServer) debugGoroutines(w http.ResponseWriter, _ *http.Request)
func (s *LivekitServer) debugInfo(w http.ResponseWriter, _ *http.Request) {
s.roomManager.lock.RLock()
info := make([]map[string]interface{}, 0, len(s.roomManager.rooms))
info := make([]map[string]any, 0, len(s.roomManager.rooms))
for _, room := range s.roomManager.rooms {
info = append(info, room.DebugInfo())
}
@@ -372,7 +378,7 @@ func (s *LivekitServer) healthCheck(w http.ResponseWriter, _ *http.Request) {
}
if time.Since(updatedAt) > 4*time.Second {
w.WriteHeader(http.StatusNotAcceptable)
_, _ = w.Write([]byte(fmt.Sprintf("Not Ready\nNode Updated At %s", updatedAt)))
_, _ = fmt.Fprintf(w, "Not Ready\nNode Updated At %s", updatedAt)
return
}
@@ -392,16 +392,6 @@ func (fake *FakeAgentStore) StoreAgentJobReturnsOnCall(i int, result1 error) {
func (fake *FakeAgentStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteAgentDispatchMutex.RLock()
defer fake.deleteAgentDispatchMutex.RUnlock()
fake.deleteAgentJobMutex.RLock()
defer fake.deleteAgentJobMutex.RUnlock()
fake.listAgentDispatchesMutex.RLock()
defer fake.listAgentDispatchesMutex.RUnlock()
fake.storeAgentDispatchMutex.RLock()
defer fake.storeAgentDispatchMutex.RUnlock()
fake.storeAgentJobMutex.RLock()
defer fake.storeAgentJobMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -325,14 +325,6 @@ func (fake *FakeEgressStore) UpdateEgressReturnsOnCall(i int, result1 error) {
func (fake *FakeEgressStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.listEgressMutex.RLock()
defer fake.listEgressMutex.RUnlock()
fake.loadEgressMutex.RLock()
defer fake.loadEgressMutex.RUnlock()
fake.storeEgressMutex.RLock()
defer fake.storeEgressMutex.RUnlock()
fake.updateEgressMutex.RLock()
defer fake.updateEgressMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -552,20 +552,6 @@ func (fake *FakeIngressStore) UpdateIngressStateReturnsOnCall(i int, result1 err
func (fake *FakeIngressStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteIngressMutex.RLock()
defer fake.deleteIngressMutex.RUnlock()
fake.listIngressMutex.RLock()
defer fake.listIngressMutex.RUnlock()
fake.loadIngressMutex.RLock()
defer fake.loadIngressMutex.RUnlock()
fake.loadIngressFromStreamKeyMutex.RLock()
defer fake.loadIngressFromStreamKeyMutex.RUnlock()
fake.storeIngressMutex.RLock()
defer fake.storeIngressMutex.RUnlock()
fake.updateIngressMutex.RLock()
defer fake.updateIngressMutex.RUnlock()
fake.updateIngressStateMutex.RLock()
defer fake.updateIngressStateMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -414,16 +414,6 @@ func (fake *FakeIOClient) UpdateIngressStateReturnsOnCall(i int, result1 *emptyp
func (fake *FakeIOClient) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.createEgressMutex.RLock()
defer fake.createEgressMutex.RUnlock()
fake.createIngressMutex.RLock()
defer fake.createIngressMutex.RUnlock()
fake.getEgressMutex.RLock()
defer fake.getEgressMutex.RUnlock()
fake.listEgressMutex.RLock()
defer fake.listEgressMutex.RUnlock()
fake.updateIngressStateMutex.RLock()
defer fake.updateIngressStateMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -36,6 +36,21 @@ type FakeObjectStore struct {
deleteRoomReturnsOnCall map[int]struct {
result1 error
}
HasParticipantStub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (bool, error)
hasParticipantMutex sync.RWMutex
hasParticipantArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}
hasParticipantReturns struct {
result1 bool
result2 error
}
hasParticipantReturnsOnCall map[int]struct {
result1 bool
result2 error
}
ListParticipantsStub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)
listParticipantsMutex sync.RWMutex
listParticipantsArgsForCall []struct {
@@ -111,6 +126,20 @@ type FakeObjectStore struct {
result1 string
result2 error
}
RoomExistsStub func(context.Context, livekit.RoomName) (bool, error)
roomExistsMutex sync.RWMutex
roomExistsArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
roomExistsReturns struct {
result1 bool
result2 error
}
roomExistsReturnsOnCall map[int]struct {
result1 bool
result2 error
}
StoreParticipantStub func(context.Context, livekit.RoomName, *livekit.ParticipantInfo) error
storeParticipantMutex sync.RWMutex
storeParticipantArgsForCall []struct {
@@ -279,6 +308,72 @@ func (fake *FakeObjectStore) DeleteRoomReturnsOnCall(i int, result1 error) {
}{result1}
}
func (fake *FakeObjectStore) HasParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.ParticipantIdentity) (bool, error) {
fake.hasParticipantMutex.Lock()
ret, specificReturn := fake.hasParticipantReturnsOnCall[len(fake.hasParticipantArgsForCall)]
fake.hasParticipantArgsForCall = append(fake.hasParticipantArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}{arg1, arg2, arg3})
stub := fake.HasParticipantStub
fakeReturns := fake.hasParticipantReturns
fake.recordInvocation("HasParticipant", []interface{}{arg1, arg2, arg3})
fake.hasParticipantMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeObjectStore) HasParticipantCallCount() int {
fake.hasParticipantMutex.RLock()
defer fake.hasParticipantMutex.RUnlock()
return len(fake.hasParticipantArgsForCall)
}
func (fake *FakeObjectStore) HasParticipantCalls(stub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (bool, error)) {
fake.hasParticipantMutex.Lock()
defer fake.hasParticipantMutex.Unlock()
fake.HasParticipantStub = stub
}
func (fake *FakeObjectStore) HasParticipantArgsForCall(i int) (context.Context, livekit.RoomName, livekit.ParticipantIdentity) {
fake.hasParticipantMutex.RLock()
defer fake.hasParticipantMutex.RUnlock()
argsForCall := fake.hasParticipantArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeObjectStore) HasParticipantReturns(result1 bool, result2 error) {
fake.hasParticipantMutex.Lock()
defer fake.hasParticipantMutex.Unlock()
fake.HasParticipantStub = nil
fake.hasParticipantReturns = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) HasParticipantReturnsOnCall(i int, result1 bool, result2 error) {
fake.hasParticipantMutex.Lock()
defer fake.hasParticipantMutex.Unlock()
fake.HasParticipantStub = nil
if fake.hasParticipantReturnsOnCall == nil {
fake.hasParticipantReturnsOnCall = make(map[int]struct {
result1 bool
result2 error
})
}
fake.hasParticipantReturnsOnCall[i] = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) ListParticipants(arg1 context.Context, arg2 livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
fake.listParticipantsMutex.Lock()
ret, specificReturn := fake.listParticipantsReturnsOnCall[len(fake.listParticipantsArgsForCall)]
@@ -615,6 +710,71 @@ func (fake *FakeObjectStore) LockRoomReturnsOnCall(i int, result1 string, result
}{result1, result2}
}
func (fake *FakeObjectStore) RoomExists(arg1 context.Context, arg2 livekit.RoomName) (bool, error) {
fake.roomExistsMutex.Lock()
ret, specificReturn := fake.roomExistsReturnsOnCall[len(fake.roomExistsArgsForCall)]
fake.roomExistsArgsForCall = append(fake.roomExistsArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.RoomExistsStub
fakeReturns := fake.roomExistsReturns
fake.recordInvocation("RoomExists", []interface{}{arg1, arg2})
fake.roomExistsMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeObjectStore) RoomExistsCallCount() int {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
return len(fake.roomExistsArgsForCall)
}
func (fake *FakeObjectStore) RoomExistsCalls(stub func(context.Context, livekit.RoomName) (bool, error)) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = stub
}
func (fake *FakeObjectStore) RoomExistsArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
argsForCall := fake.roomExistsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeObjectStore) RoomExistsReturns(result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
fake.roomExistsReturns = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) RoomExistsReturnsOnCall(i int, result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
if fake.roomExistsReturnsOnCall == nil {
fake.roomExistsReturnsOnCall = make(map[int]struct {
result1 bool
result2 error
})
}
fake.roomExistsReturnsOnCall[i] = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeObjectStore) StoreParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 *livekit.ParticipantInfo) error {
fake.storeParticipantMutex.Lock()
ret, specificReturn := fake.storeParticipantReturnsOnCall[len(fake.storeParticipantArgsForCall)]
@@ -807,26 +967,6 @@ func (fake *FakeObjectStore) UnlockRoomReturnsOnCall(i int, result1 error) {
func (fake *FakeObjectStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteParticipantMutex.RLock()
defer fake.deleteParticipantMutex.RUnlock()
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
fake.listRoomsMutex.RLock()
defer fake.listRoomsMutex.RUnlock()
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
fake.loadRoomMutex.RLock()
defer fake.loadRoomMutex.RUnlock()
fake.lockRoomMutex.RLock()
defer fake.lockRoomMutex.RUnlock()
fake.storeParticipantMutex.RLock()
defer fake.storeParticipantMutex.RUnlock()
fake.storeRoomMutex.RLock()
defer fake.storeRoomMutex.RUnlock()
fake.unlockRoomMutex.RLock()
defer fake.unlockRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -330,14 +330,6 @@ func (fake *FakeRoomAllocator) ValidateCreateRoomReturnsOnCall(i int, result1 er
func (fake *FakeRoomAllocator) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.autoCreateEnabledMutex.RLock()
defer fake.autoCreateEnabledMutex.RUnlock()
fake.createRoomMutex.RLock()
defer fake.createRoomMutex.RUnlock()
fake.selectRoomNodeMutex.RLock()
defer fake.selectRoomNodeMutex.RUnlock()
fake.validateCreateRoomMutex.RLock()
defer fake.validateCreateRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -10,32 +10,6 @@ import (
)
type FakeServiceStore struct {
DeleteRoomStub func(context.Context, livekit.RoomName) error
deleteRoomMutex sync.RWMutex
deleteRoomArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
deleteRoomReturns struct {
result1 error
}
deleteRoomReturnsOnCall map[int]struct {
result1 error
}
ListParticipantsStub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)
listParticipantsMutex sync.RWMutex
listParticipantsArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
listParticipantsReturns struct {
result1 []*livekit.ParticipantInfo
result2 error
}
listParticipantsReturnsOnCall map[int]struct {
result1 []*livekit.ParticipantInfo
result2 error
}
ListRoomsStub func(context.Context, []livekit.RoomName) ([]*livekit.Room, error)
listRoomsMutex sync.RWMutex
listRoomsArgsForCall []struct {
@@ -50,21 +24,6 @@ type FakeServiceStore struct {
result1 []*livekit.Room
result2 error
}
LoadParticipantStub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)
loadParticipantMutex sync.RWMutex
loadParticipantArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}
loadParticipantReturns struct {
result1 *livekit.ParticipantInfo
result2 error
}
loadParticipantReturnsOnCall map[int]struct {
result1 *livekit.ParticipantInfo
result2 error
}
LoadRoomStub func(context.Context, livekit.RoomName, bool) (*livekit.Room, *livekit.RoomInternal, error)
loadRoomMutex sync.RWMutex
loadRoomArgsForCall []struct {
@@ -82,137 +41,24 @@ type FakeServiceStore struct {
result2 *livekit.RoomInternal
result3 error
}
RoomExistsStub func(context.Context, livekit.RoomName) (bool, error)
roomExistsMutex sync.RWMutex
roomExistsArgsForCall []struct {
arg1 context.Context
arg2 livekit.RoomName
}
roomExistsReturns struct {
result1 bool
result2 error
}
roomExistsReturnsOnCall map[int]struct {
result1 bool
result2 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeServiceStore) DeleteRoom(arg1 context.Context, arg2 livekit.RoomName) error {
fake.deleteRoomMutex.Lock()
ret, specificReturn := fake.deleteRoomReturnsOnCall[len(fake.deleteRoomArgsForCall)]
fake.deleteRoomArgsForCall = append(fake.deleteRoomArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.DeleteRoomStub
fakeReturns := fake.deleteRoomReturns
fake.recordInvocation("DeleteRoom", []interface{}{arg1, arg2})
fake.deleteRoomMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeServiceStore) DeleteRoomCallCount() int {
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
return len(fake.deleteRoomArgsForCall)
}
func (fake *FakeServiceStore) DeleteRoomCalls(stub func(context.Context, livekit.RoomName) error) {
fake.deleteRoomMutex.Lock()
defer fake.deleteRoomMutex.Unlock()
fake.DeleteRoomStub = stub
}
func (fake *FakeServiceStore) DeleteRoomArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
argsForCall := fake.deleteRoomArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeServiceStore) DeleteRoomReturns(result1 error) {
fake.deleteRoomMutex.Lock()
defer fake.deleteRoomMutex.Unlock()
fake.DeleteRoomStub = nil
fake.deleteRoomReturns = struct {
result1 error
}{result1}
}
func (fake *FakeServiceStore) DeleteRoomReturnsOnCall(i int, result1 error) {
fake.deleteRoomMutex.Lock()
defer fake.deleteRoomMutex.Unlock()
fake.DeleteRoomStub = nil
if fake.deleteRoomReturnsOnCall == nil {
fake.deleteRoomReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.deleteRoomReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeServiceStore) ListParticipants(arg1 context.Context, arg2 livekit.RoomName) ([]*livekit.ParticipantInfo, error) {
fake.listParticipantsMutex.Lock()
ret, specificReturn := fake.listParticipantsReturnsOnCall[len(fake.listParticipantsArgsForCall)]
fake.listParticipantsArgsForCall = append(fake.listParticipantsArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.ListParticipantsStub
fakeReturns := fake.listParticipantsReturns
fake.recordInvocation("ListParticipants", []interface{}{arg1, arg2})
fake.listParticipantsMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeServiceStore) ListParticipantsCallCount() int {
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
return len(fake.listParticipantsArgsForCall)
}
func (fake *FakeServiceStore) ListParticipantsCalls(stub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)) {
fake.listParticipantsMutex.Lock()
defer fake.listParticipantsMutex.Unlock()
fake.ListParticipantsStub = stub
}
func (fake *FakeServiceStore) ListParticipantsArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
argsForCall := fake.listParticipantsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeServiceStore) ListParticipantsReturns(result1 []*livekit.ParticipantInfo, result2 error) {
fake.listParticipantsMutex.Lock()
defer fake.listParticipantsMutex.Unlock()
fake.ListParticipantsStub = nil
fake.listParticipantsReturns = struct {
result1 []*livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) ListParticipantsReturnsOnCall(i int, result1 []*livekit.ParticipantInfo, result2 error) {
fake.listParticipantsMutex.Lock()
defer fake.listParticipantsMutex.Unlock()
fake.ListParticipantsStub = nil
if fake.listParticipantsReturnsOnCall == nil {
fake.listParticipantsReturnsOnCall = make(map[int]struct {
result1 []*livekit.ParticipantInfo
result2 error
})
}
fake.listParticipantsReturnsOnCall[i] = struct {
result1 []*livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) ListRooms(arg1 context.Context, arg2 []livekit.RoomName) ([]*livekit.Room, error) {
var arg2Copy []livekit.RoomName
if arg2 != nil {
@@ -283,72 +129,6 @@ func (fake *FakeServiceStore) ListRoomsReturnsOnCall(i int, result1 []*livekit.R
}{result1, result2}
}
func (fake *FakeServiceStore) LoadParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error) {
fake.loadParticipantMutex.Lock()
ret, specificReturn := fake.loadParticipantReturnsOnCall[len(fake.loadParticipantArgsForCall)]
fake.loadParticipantArgsForCall = append(fake.loadParticipantArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
arg3 livekit.ParticipantIdentity
}{arg1, arg2, arg3})
stub := fake.LoadParticipantStub
fakeReturns := fake.loadParticipantReturns
fake.recordInvocation("LoadParticipant", []interface{}{arg1, arg2, arg3})
fake.loadParticipantMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeServiceStore) LoadParticipantCallCount() int {
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
return len(fake.loadParticipantArgsForCall)
}
func (fake *FakeServiceStore) LoadParticipantCalls(stub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)) {
fake.loadParticipantMutex.Lock()
defer fake.loadParticipantMutex.Unlock()
fake.LoadParticipantStub = stub
}
func (fake *FakeServiceStore) LoadParticipantArgsForCall(i int) (context.Context, livekit.RoomName, livekit.ParticipantIdentity) {
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
argsForCall := fake.loadParticipantArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
}
func (fake *FakeServiceStore) LoadParticipantReturns(result1 *livekit.ParticipantInfo, result2 error) {
fake.loadParticipantMutex.Lock()
defer fake.loadParticipantMutex.Unlock()
fake.LoadParticipantStub = nil
fake.loadParticipantReturns = struct {
result1 *livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) LoadParticipantReturnsOnCall(i int, result1 *livekit.ParticipantInfo, result2 error) {
fake.loadParticipantMutex.Lock()
defer fake.loadParticipantMutex.Unlock()
fake.LoadParticipantStub = nil
if fake.loadParticipantReturnsOnCall == nil {
fake.loadParticipantReturnsOnCall = make(map[int]struct {
result1 *livekit.ParticipantInfo
result2 error
})
}
fake.loadParticipantReturnsOnCall[i] = struct {
result1 *livekit.ParticipantInfo
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) LoadRoom(arg1 context.Context, arg2 livekit.RoomName, arg3 bool) (*livekit.Room, *livekit.RoomInternal, error) {
fake.loadRoomMutex.Lock()
ret, specificReturn := fake.loadRoomReturnsOnCall[len(fake.loadRoomArgsForCall)]
@@ -418,19 +198,74 @@ func (fake *FakeServiceStore) LoadRoomReturnsOnCall(i int, result1 *livekit.Room
}{result1, result2, result3}
}
func (fake *FakeServiceStore) RoomExists(arg1 context.Context, arg2 livekit.RoomName) (bool, error) {
fake.roomExistsMutex.Lock()
ret, specificReturn := fake.roomExistsReturnsOnCall[len(fake.roomExistsArgsForCall)]
fake.roomExistsArgsForCall = append(fake.roomExistsArgsForCall, struct {
arg1 context.Context
arg2 livekit.RoomName
}{arg1, arg2})
stub := fake.RoomExistsStub
fakeReturns := fake.roomExistsReturns
fake.recordInvocation("RoomExists", []interface{}{arg1, arg2})
fake.roomExistsMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeServiceStore) RoomExistsCallCount() int {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
return len(fake.roomExistsArgsForCall)
}
func (fake *FakeServiceStore) RoomExistsCalls(stub func(context.Context, livekit.RoomName) (bool, error)) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = stub
}
func (fake *FakeServiceStore) RoomExistsArgsForCall(i int) (context.Context, livekit.RoomName) {
fake.roomExistsMutex.RLock()
defer fake.roomExistsMutex.RUnlock()
argsForCall := fake.roomExistsArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeServiceStore) RoomExistsReturns(result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
fake.roomExistsReturns = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) RoomExistsReturnsOnCall(i int, result1 bool, result2 error) {
fake.roomExistsMutex.Lock()
defer fake.roomExistsMutex.Unlock()
fake.RoomExistsStub = nil
if fake.roomExistsReturnsOnCall == nil {
fake.roomExistsReturnsOnCall = make(map[int]struct {
result1 bool
result2 error
})
}
fake.roomExistsReturnsOnCall[i] = struct {
result1 bool
result2 error
}{result1, result2}
}
func (fake *FakeServiceStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteRoomMutex.RLock()
defer fake.deleteRoomMutex.RUnlock()
fake.listParticipantsMutex.RLock()
defer fake.listParticipantsMutex.RUnlock()
fake.listRoomsMutex.RLock()
defer fake.listRoomsMutex.RUnlock()
fake.loadParticipantMutex.RLock()
defer fake.loadParticipantMutex.RUnlock()
fake.loadRoomMutex.RLock()
defer fake.loadRoomMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -171,10 +171,6 @@ func (fake *FakeSessionHandler) LoggerReturnsOnCall(i int, result1 logger.Logger
func (fake *FakeSessionHandler) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.handleSessionMutex.RLock()
defer fake.handleSessionMutex.RUnlock()
fake.loggerMutex.RLock()
defer fake.loggerMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
@@ -146,34 +146,6 @@ type FakeSIPStore struct {
result1 *livekit.SIPTrunkInfo
result2 error
}
SelectSIPDispatchRuleStub func(context.Context, string) ([]*livekit.SIPDispatchRuleInfo, error)
selectSIPDispatchRuleMutex sync.RWMutex
selectSIPDispatchRuleArgsForCall []struct {
arg1 context.Context
arg2 string
}
selectSIPDispatchRuleReturns struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}
selectSIPDispatchRuleReturnsOnCall map[int]struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}
SelectSIPInboundTrunkStub func(context.Context, string) ([]*livekit.SIPInboundTrunkInfo, error)
selectSIPInboundTrunkMutex sync.RWMutex
selectSIPInboundTrunkArgsForCall []struct {
arg1 context.Context
arg2 string
}
selectSIPInboundTrunkReturns struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}
selectSIPInboundTrunkReturnsOnCall map[int]struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}
StoreSIPDispatchRuleStub func(context.Context, *livekit.SIPDispatchRuleInfo) error
storeSIPDispatchRuleMutex sync.RWMutex
storeSIPDispatchRuleArgsForCall []struct {
@@ -870,136 +842,6 @@ func (fake *FakeSIPStore) LoadSIPTrunkReturnsOnCall(i int, result1 *livekit.SIPT
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPDispatchRule(arg1 context.Context, arg2 string) ([]*livekit.SIPDispatchRuleInfo, error) {
fake.selectSIPDispatchRuleMutex.Lock()
ret, specificReturn := fake.selectSIPDispatchRuleReturnsOnCall[len(fake.selectSIPDispatchRuleArgsForCall)]
fake.selectSIPDispatchRuleArgsForCall = append(fake.selectSIPDispatchRuleArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.SelectSIPDispatchRuleStub
fakeReturns := fake.selectSIPDispatchRuleReturns
fake.recordInvocation("SelectSIPDispatchRule", []interface{}{arg1, arg2})
fake.selectSIPDispatchRuleMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleCallCount() int {
fake.selectSIPDispatchRuleMutex.RLock()
defer fake.selectSIPDispatchRuleMutex.RUnlock()
return len(fake.selectSIPDispatchRuleArgsForCall)
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleCalls(stub func(context.Context, string) ([]*livekit.SIPDispatchRuleInfo, error)) {
fake.selectSIPDispatchRuleMutex.Lock()
defer fake.selectSIPDispatchRuleMutex.Unlock()
fake.SelectSIPDispatchRuleStub = stub
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleArgsForCall(i int) (context.Context, string) {
fake.selectSIPDispatchRuleMutex.RLock()
defer fake.selectSIPDispatchRuleMutex.RUnlock()
argsForCall := fake.selectSIPDispatchRuleArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleReturns(result1 []*livekit.SIPDispatchRuleInfo, result2 error) {
fake.selectSIPDispatchRuleMutex.Lock()
defer fake.selectSIPDispatchRuleMutex.Unlock()
fake.SelectSIPDispatchRuleStub = nil
fake.selectSIPDispatchRuleReturns = struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPDispatchRuleReturnsOnCall(i int, result1 []*livekit.SIPDispatchRuleInfo, result2 error) {
fake.selectSIPDispatchRuleMutex.Lock()
defer fake.selectSIPDispatchRuleMutex.Unlock()
fake.SelectSIPDispatchRuleStub = nil
if fake.selectSIPDispatchRuleReturnsOnCall == nil {
fake.selectSIPDispatchRuleReturnsOnCall = make(map[int]struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
})
}
fake.selectSIPDispatchRuleReturnsOnCall[i] = struct {
result1 []*livekit.SIPDispatchRuleInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPInboundTrunk(arg1 context.Context, arg2 string) ([]*livekit.SIPInboundTrunkInfo, error) {
fake.selectSIPInboundTrunkMutex.Lock()
ret, specificReturn := fake.selectSIPInboundTrunkReturnsOnCall[len(fake.selectSIPInboundTrunkArgsForCall)]
fake.selectSIPInboundTrunkArgsForCall = append(fake.selectSIPInboundTrunkArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.SelectSIPInboundTrunkStub
fakeReturns := fake.selectSIPInboundTrunkReturns
fake.recordInvocation("SelectSIPInboundTrunk", []interface{}{arg1, arg2})
fake.selectSIPInboundTrunkMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkCallCount() int {
fake.selectSIPInboundTrunkMutex.RLock()
defer fake.selectSIPInboundTrunkMutex.RUnlock()
return len(fake.selectSIPInboundTrunkArgsForCall)
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkCalls(stub func(context.Context, string) ([]*livekit.SIPInboundTrunkInfo, error)) {
fake.selectSIPInboundTrunkMutex.Lock()
defer fake.selectSIPInboundTrunkMutex.Unlock()
fake.SelectSIPInboundTrunkStub = stub
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkArgsForCall(i int) (context.Context, string) {
fake.selectSIPInboundTrunkMutex.RLock()
defer fake.selectSIPInboundTrunkMutex.RUnlock()
argsForCall := fake.selectSIPInboundTrunkArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkReturns(result1 []*livekit.SIPInboundTrunkInfo, result2 error) {
fake.selectSIPInboundTrunkMutex.Lock()
defer fake.selectSIPInboundTrunkMutex.Unlock()
fake.SelectSIPInboundTrunkStub = nil
fake.selectSIPInboundTrunkReturns = struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) SelectSIPInboundTrunkReturnsOnCall(i int, result1 []*livekit.SIPInboundTrunkInfo, result2 error) {
fake.selectSIPInboundTrunkMutex.Lock()
defer fake.selectSIPInboundTrunkMutex.Unlock()
fake.SelectSIPInboundTrunkStub = nil
if fake.selectSIPInboundTrunkReturnsOnCall == nil {
fake.selectSIPInboundTrunkReturnsOnCall = make(map[int]struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
})
}
fake.selectSIPInboundTrunkReturnsOnCall[i] = struct {
result1 []*livekit.SIPInboundTrunkInfo
result2 error
}{result1, result2}
}
func (fake *FakeSIPStore) StoreSIPDispatchRule(arg1 context.Context, arg2 *livekit.SIPDispatchRuleInfo) error {
fake.storeSIPDispatchRuleMutex.Lock()
ret, specificReturn := fake.storeSIPDispatchRuleReturnsOnCall[len(fake.storeSIPDispatchRuleArgsForCall)]
@@ -1251,38 +1093,6 @@ func (fake *FakeSIPStore) StoreSIPTrunkReturnsOnCall(i int, result1 error) {
func (fake *FakeSIPStore) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.deleteSIPDispatchRuleMutex.RLock()
defer fake.deleteSIPDispatchRuleMutex.RUnlock()
fake.deleteSIPTrunkMutex.RLock()
defer fake.deleteSIPTrunkMutex.RUnlock()
fake.listSIPDispatchRuleMutex.RLock()
defer fake.listSIPDispatchRuleMutex.RUnlock()
fake.listSIPInboundTrunkMutex.RLock()
defer fake.listSIPInboundTrunkMutex.RUnlock()
fake.listSIPOutboundTrunkMutex.RLock()
defer fake.listSIPOutboundTrunkMutex.RUnlock()
fake.listSIPTrunkMutex.RLock()
defer fake.listSIPTrunkMutex.RUnlock()
fake.loadSIPDispatchRuleMutex.RLock()
defer fake.loadSIPDispatchRuleMutex.RUnlock()
fake.loadSIPInboundTrunkMutex.RLock()
defer fake.loadSIPInboundTrunkMutex.RUnlock()
fake.loadSIPOutboundTrunkMutex.RLock()
defer fake.loadSIPOutboundTrunkMutex.RUnlock()
fake.loadSIPTrunkMutex.RLock()
defer fake.loadSIPTrunkMutex.RUnlock()
fake.selectSIPDispatchRuleMutex.RLock()
defer fake.selectSIPDispatchRuleMutex.RUnlock()
fake.selectSIPInboundTrunkMutex.RLock()
defer fake.selectSIPInboundTrunkMutex.RUnlock()
fake.storeSIPDispatchRuleMutex.RLock()
defer fake.storeSIPDispatchRuleMutex.RUnlock()
fake.storeSIPInboundTrunkMutex.RLock()
defer fake.storeSIPInboundTrunkMutex.RUnlock()
fake.storeSIPOutboundTrunkMutex.RLock()
defer fake.storeSIPOutboundTrunkMutex.RUnlock()
fake.storeSIPTrunkMutex.RLock()
defer fake.storeSIPTrunkMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
+6 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"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"
@@ -86,7 +87,7 @@ type defaultSessionHandler struct {
}
func (s *defaultSessionHandler) Logger(ctx context.Context) logger.Logger {
return logger.GetLogger()
return utils.GetLogger(ctx)
}
func (s *defaultSessionHandler) HandleSession(
@@ -119,8 +120,8 @@ func (s *SignalServer) Start() error {
return s.server.RegisterAllNodeTopics(s.nodeID)
}
func (r *SignalServer) Stop() {
r.server.Kill()
func (s *SignalServer) Stop() {
s.server.Kill()
}
type signalService struct {
@@ -167,6 +168,8 @@ func (r *signalService) RelaySignal(stream psrpc.ServerStream[*rpc.RelaySignalRe
reqChan,
signalRequestMessageReader{},
r.config,
prometheus.RecordSignalRequestSuccess,
prometheus.RecordSignalRequestFailure,
)
l.Debugw("signal stream closed", "error", err)
+1 -1
View File
@@ -21,7 +21,6 @@ import (
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/livekit/livekit-server/pkg/config"
@@ -31,6 +30,7 @@ import (
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/protojson"
"github.com/livekit/psrpc"
)
+235 -24
View File
@@ -16,6 +16,7 @@ package service
import (
"context"
"errors"
"time"
"github.com/dennwc/iters"
@@ -86,6 +87,9 @@ func (s *SIPService) CreateSIPTrunk(ctx context.Context, req *livekit.CreateSIPT
Name: req.Name,
Metadata: req.Metadata,
}
if err := info.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
// Validate all trunks including the new one first.
it, err := ListSIPInboundTrunk(ctx, s.store, &livekit.ListSIPInboundTrunkRequest{}, info.AsInbound())
@@ -120,7 +124,7 @@ func (s *SIPService) CreateSIPInboundTrunk(ctx context.Context, req *livekit.Cre
if info.SipTrunkId != "" {
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID must be empty")
}
AppendLogFields(ctx, "trunk", logger.Proto(req.Trunk))
AppendLogFields(ctx, "trunk", logger.Proto(info))
// Keep ID empty still, so that validation can print "<new>" instead of a non-existent ID in the error.
@@ -159,7 +163,7 @@ func (s *SIPService) CreateSIPOutboundTrunk(ctx context.Context, req *livekit.Cr
if info.SipTrunkId != "" {
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID must be empty")
}
AppendLogFields(ctx, "trunk", logger.Proto(req.Trunk))
AppendLogFields(ctx, "trunk", logger.Proto(info))
// No additional validation needed for outbound.
info.SipTrunkId = guid.New(utils.SIPTrunkPrefix)
@@ -169,6 +173,100 @@ func (s *SIPService) CreateSIPOutboundTrunk(ctx context.Context, req *livekit.Cr
return info, nil
}
func (s *SIPService) UpdateSIPInboundTrunk(ctx context.Context, req *livekit.UpdateSIPInboundTrunkRequest) (*livekit.SIPInboundTrunkInfo, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.store == nil {
return nil, ErrSIPNotConnected
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
AppendLogFields(ctx,
"request", logger.Proto(req),
"trunkID", req.SipTrunkId,
)
// Validate all trunks including the new one first.
info, err := s.store.LoadSIPInboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
switch a := req.Action.(type) {
default:
return nil, errors.New("missing or unsupported action")
case livekit.UpdateSIPInboundTrunkRequestAction:
info, err = a.Apply(info)
if err != nil {
return nil, err
}
}
it, err := ListSIPInboundTrunk(ctx, s.store, &livekit.ListSIPInboundTrunkRequest{
Numbers: info.Numbers,
})
if err != nil {
return nil, err
}
defer it.Close()
if err = sip.ValidateTrunksIter(it, sip.WithTrunkReplace(func(t *livekit.SIPInboundTrunkInfo) *livekit.SIPInboundTrunkInfo {
if req.SipTrunkId == t.SipTrunkId {
return info // updated one
}
return t
})); err != nil {
return nil, err
}
if err := s.store.StoreSIPInboundTrunk(ctx, info); err != nil {
return nil, err
}
return info, nil
}
func (s *SIPService) UpdateSIPOutboundTrunk(ctx context.Context, req *livekit.UpdateSIPOutboundTrunkRequest) (*livekit.SIPOutboundTrunkInfo, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.store == nil {
return nil, ErrSIPNotConnected
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
AppendLogFields(ctx,
"request", logger.Proto(req),
"trunkID", req.SipTrunkId,
)
info, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
switch a := req.Action.(type) {
default:
return nil, errors.New("missing or unsupported action")
case livekit.UpdateSIPOutboundTrunkRequestAction:
info, err = a.Apply(info)
if err != nil {
return nil, err
}
}
// No additional validation needed for outbound.
if err := s.store.StoreSIPOutboundTrunk(ctx, info); err != nil {
return nil, err
}
return info, nil
}
func (s *SIPService) GetSIPInboundTrunk(ctx context.Context, req *livekit.GetSIPInboundTrunkRequest) (*livekit.GetSIPInboundTrunkResponse, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
@@ -183,6 +281,9 @@ func (s *SIPService) GetSIPInboundTrunk(ctx context.Context, req *livekit.GetSIP
trunk, err := s.store.LoadSIPInboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
@@ -203,6 +304,9 @@ func (s *SIPService) GetSIPOutboundTrunk(ctx context.Context, req *livekit.GetSI
trunk, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
@@ -317,6 +421,7 @@ func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.Cre
if s.store == nil {
return nil, ErrSIPNotConnected
}
req.DispatchRule.Upgrade()
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
@@ -326,16 +431,8 @@ func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.Cre
"trunkID", req.TrunkIds,
)
// Keep ID empty, so that validation can print "<new>" instead of a non-existent ID in the error.
info := &livekit.SIPDispatchRuleInfo{
Rule: req.Rule,
TrunkIds: req.TrunkIds,
InboundNumbers: req.InboundNumbers,
HidePhoneNumber: req.HidePhoneNumber,
Name: req.Name,
Metadata: req.Metadata,
Attributes: req.Attributes,
RoomConfig: req.RoomConfig,
}
info := req.DispatchRuleInfo()
info.SipDispatchRuleId = ""
// Validate all rules including the new one first.
it, err := ListSIPDispatchRule(ctx, s.store, &livekit.ListSIPDispatchRuleRequest{
@@ -357,6 +454,62 @@ func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.Cre
return info, nil
}
func (s *SIPService) UpdateSIPDispatchRule(ctx context.Context, req *livekit.UpdateSIPDispatchRuleRequest) (*livekit.SIPDispatchRuleInfo, error) {
if err := EnsureSIPAdminPermission(ctx); err != nil {
return nil, twirpAuthError(err)
}
if s.store == nil {
return nil, ErrSIPNotConnected
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
AppendLogFields(ctx,
"request", logger.Proto(req),
"ruleID", req.SipDispatchRuleId,
)
// Validate all trunks including the new one first.
info, err := s.store.LoadSIPDispatchRule(ctx, req.SipDispatchRuleId)
if err != nil {
if errors.Is(err, ErrSIPDispatchRuleNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
switch a := req.Action.(type) {
default:
return nil, errors.New("missing or unsupported action")
case livekit.UpdateSIPDispatchRuleRequestAction:
info, err = a.Apply(info)
if err != nil {
return nil, err
}
}
it, err := ListSIPDispatchRule(ctx, s.store, &livekit.ListSIPDispatchRuleRequest{
TrunkIds: info.TrunkIds,
})
if err != nil {
return nil, err
}
defer it.Close()
if _, err = sip.ValidateDispatchRulesIter(it, sip.WithDispatchRuleReplace(func(t *livekit.SIPDispatchRuleInfo) *livekit.SIPDispatchRuleInfo {
if req.SipDispatchRuleId == t.SipDispatchRuleId {
return info // updated one
}
return t
})); err != nil {
return nil, err
}
if err := s.store.StoreSIPDispatchRule(ctx, info); err != nil {
return nil, err
}
return info, nil
}
func ListSIPDispatchRule(ctx context.Context, s SIPStore, req *livekit.ListSIPDispatchRuleRequest, add ...*livekit.SIPDispatchRuleInfo) (iters.Iter[*livekit.SIPDispatchRuleInfo], error) {
if s == nil {
return nil, ErrSIPNotConnected
@@ -402,6 +555,9 @@ func (s *SIPService) DeleteSIPDispatchRule(ctx context.Context, req *livekit.Del
info, err := s.store.LoadSIPDispatchRule(ctx, req.SipDispatchRuleId)
if err != nil {
if errors.Is(err, ErrSIPDispatchRuleNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
@@ -428,7 +584,7 @@ func (s *SIPService) CreateSIPParticipant(ctx context.Context, req *livekit.Crea
ireq, err := s.CreateSIPParticipantRequest(ctx, req, "", "", "", "")
if err != nil {
unlikelyLogger.Errorw("cannot create sip participant request", err)
return nil, err
return nil, wrapSIPContextError(err)
}
unlikelyLogger = unlikelyLogger.WithValues(
"callID", ireq.SipCallId,
@@ -457,7 +613,7 @@ func (s *SIPService) CreateSIPParticipant(ctx context.Context, req *livekit.Crea
resp, err := s.psrpcClient.CreateSIPParticipant(ctx, "", ireq, psrpc.WithRequestTimeout(timeout))
if err != nil {
unlikelyLogger.Errorw("cannot create sip participant", err)
return nil, err
return nil, wrapSIPContextError(err)
}
return &livekit.SIPParticipantInfo{
ParticipantId: resp.ParticipantId,
@@ -474,6 +630,10 @@ func (s *SIPService) CreateSIPParticipantRequest(ctx context.Context, req *livek
if s.store == nil {
return nil, ErrSIPNotConnected
}
req.Upgrade()
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
callID := sip.NewCallID()
log := logger.GetLogger().WithUnlikelyValues(
"callID", callID,
@@ -485,10 +645,22 @@ func (s *SIPService) CreateSIPParticipantRequest(ctx context.Context, req *livek
log = log.WithValues("projectID", projectID)
}
trunk, err := s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
log.Errorw("cannot get trunk to update sip participant", err)
return nil, err
var trunk *livekit.SIPOutboundTrunkInfo
if req.SipTrunkId != "" {
var err error
trunk, err = s.store.LoadSIPOutboundTrunk(ctx, req.SipTrunkId)
if err != nil {
log.Errorw("cannot get trunk to update sip participant", err)
if errors.Is(err, ErrSIPTrunkNotFound) {
return nil, twirp.NewError(twirp.NotFound, err.Error())
}
return nil, err
}
}
if trunk != nil && trunk.FromHost != "" {
host = trunk.FromHost
} else if t := req.Trunk; t != nil && t.FromHost != "" {
host = t.FromHost
}
return rpc.NewCreateSIPParticipantRequest(projectID, callID, host, wsUrl, token, req, trunk)
}
@@ -510,12 +682,25 @@ func (s *SIPService) TransferSIPParticipant(ctx context.Context, req *livekit.Tr
ireq, err := s.transferSIPParticipantRequest(ctx, req)
if err != nil {
log.Errorw("cannot create transfer sip participant request", err)
return nil, err
return nil, wrapSIPContextError(err)
}
// by default we set the timeout to be 30 seconds.
// this timeout covers:
// - a network failure between this process and the LiveKit SIP bridge
// - the SIP transfer target not returning 200 OK fast enough.
// WARN: any timeout/cancellation of a SIP transfer risks leaving
// either the SIP bridge, or the SIP REFER exchange, in a "unknown" state.
timeout := 30 * time.Second
if req.RingingTimeout != nil {
timeout = req.RingingTimeout.AsDuration()
}
// it's also possible the ctx has a Deadline.
// in that case we want to use that deadline,
// or our timeout, whichover is soonest.
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
timeout = min(timeout, time.Until(deadline))
} else {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, timeout)
@@ -524,7 +709,7 @@ func (s *SIPService) TransferSIPParticipant(ctx context.Context, req *livekit.Tr
_, err = s.psrpcClient.TransferSIPParticipant(ctx, ireq.SipCallId, ireq, psrpc.WithRequestTimeout(timeout))
if err != nil {
log.Errorw("cannot transfer sip participant", err)
return nil, err
return nil, wrapSIPContextError(err)
}
return &emptypb.Empty{}, nil
@@ -545,6 +730,9 @@ func (s *SIPService) transferSIPParticipantRequest(ctx context.Context, req *liv
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.RoomName)); err != nil {
return nil, twirpAuthError(err)
}
if err := req.Validate(); err != nil {
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
resp, err := s.roomService.GetParticipant(ctx, &livekit.RoomParticipantIdentity{
Room: req.RoomName,
@@ -561,8 +749,31 @@ func (s *SIPService) transferSIPParticipantRequest(ctx context.Context, req *liv
}
return &rpc.InternalTransferSIPParticipantRequest{
SipCallId: callID,
TransferTo: req.TransferTo,
PlayDialtone: req.PlayDialtone,
SipCallId: callID,
TransferTo: req.TransferTo,
PlayDialtone: req.PlayDialtone,
Headers: req.Headers,
RingingTimeout: req.RingingTimeout,
}, nil
}
// wrapSIPContextError converts raw context.DeadlineExceeded / context.Canceled
// into psrpc-coded errors so they aren't surfaced as @code:unknown / HTTP 500
// at the Twirp boundary. psrpc errors and any error that already carries a
// gRPC status are passed through unchanged.
func wrapSIPContextError(err error) error {
if err == nil {
return nil
}
var psErr psrpc.Error
if errors.As(err, &psErr) {
return err
}
switch {
case errors.Is(err, context.DeadlineExceeded):
return psrpc.NewError(psrpc.DeadlineExceeded, err)
case errors.Is(err, context.Canceled):
return psrpc.NewError(psrpc.Canceled, err)
}
return err
}
+182 -88
View File
@@ -21,9 +21,11 @@ import (
"net"
"strconv"
"strings"
"time"
"github.com/jxskiss/base62"
"github.com/pion/turn/v4"
"github.com/pion/stun/v3"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/livekit/protocol/auth"
@@ -40,10 +42,10 @@ const (
LivekitRealm = "livekit"
allocateRetries = 50
turnMinPort = 1024
turnMaxPort = 30000
)
var ErrExpired = errors.New("expired")
func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone bool) (*turn.Server, error) {
turnConf := conf.TURN
if !turnConf.Enabled {
@@ -52,29 +54,7 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
if turnConf.TLSPort <= 0 && turnConf.UDPPort <= 0 {
return nil, errors.New("invalid TURN ports")
}
serverConfig := turn.ServerConfig{
Realm: LivekitRealm,
AuthHandler: authHandler,
LoggerFactory: pionlogger.NewLoggerFactory(logger.GetLogger()),
}
var relayAddrGen turn.RelayAddressGenerator = &turn.RelayAddressGeneratorPortRange{
RelayAddress: net.ParseIP(conf.RTC.NodeIP),
Address: "0.0.0.0",
MinPort: turnConf.RelayPortRangeStart,
MaxPort: turnConf.RelayPortRangeEnd,
MaxRetries: allocateRetries,
}
if standalone {
relayAddrGen = telemetry.NewRelayAddressGenerator(relayAddrGen)
}
var logValues []interface{}
logValues = append(logValues, "turn.relay_range_start", turnConf.RelayPortRangeStart)
logValues = append(logValues, "turn.relay_range_end", turnConf.RelayPortRangeEnd)
if turnConf.TLSPort > 0 {
} else if turnConf.TLSPort > 0 {
if turnConf.Domain == "" {
return nil, errors.New("TURN domain required")
}
@@ -82,64 +62,129 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone
if !IsValidDomain(turnConf.Domain) {
return nil, errors.New("TURN domain is not correct")
}
if !turnConf.ExternalTLS {
cert, err := tls.LoadX509KeyPair(turnConf.CertFile, turnConf.KeyFile)
if err != nil {
return nil, errors.Wrap(err, "TURN tls cert required")
}
tlsListener, err := tls.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(turnConf.TLSPort),
&tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
})
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN TCP port")
}
if standalone {
tlsListener = telemetry.NewListener(tlsListener)
}
listenerConfig := turn.ListenerConfig{
Listener: tlsListener,
RelayAddressGenerator: relayAddrGen,
}
serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig)
} else {
tcpListener, err := net.Listen("tcp4", "0.0.0.0:"+strconv.Itoa(turnConf.TLSPort))
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN TCP port")
}
if standalone {
tcpListener = telemetry.NewListener(tcpListener)
}
listenerConfig := turn.ListenerConfig{
Listener: tcpListener,
RelayAddressGenerator: relayAddrGen,
}
serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig)
}
logValues = append(logValues, "turn.portTLS", turnConf.TLSPort, "turn.externalTLS", turnConf.ExternalTLS)
}
if turnConf.UDPPort > 0 {
udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(turnConf.UDPPort))
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN UDP port")
serverConfig := turn.ServerConfig{
Realm: LivekitRealm,
AuthHandler: authHandler,
LoggerFactory: pionlogger.NewLoggerFactory(logger.GetLogger()),
}
var logValues []any
logValues = append(logValues, "turn.relay_range_start", turnConf.RelayPortRangeStart)
logValues = append(logValues, "turn.relay_range_end", turnConf.RelayPortRangeEnd)
for _, addr := range turnConf.BindAddresses {
var nodeIP string
if net.ParseIP(addr).To4() != nil {
nodeIP = conf.RTC.NodeIP.V4
} else {
nodeIP = conf.RTC.NodeIP.V6
}
if nodeIP == "" {
return nil, errors.New("no matching node IP for relay")
}
var relayAddrGen turn.RelayAddressGenerator = &turn.RelayAddressGeneratorPortRange{
RelayAddress: net.ParseIP(nodeIP),
Address: addr,
MinPort: turnConf.RelayPortRangeStart,
MaxPort: turnConf.RelayPortRangeEnd,
MaxRetries: allocateRetries,
}
if standalone {
udpListener = telemetry.NewPacketConn(udpListener, prometheus.Incoming)
relayAddrGen = telemetry.NewRelayAddressGenerator(relayAddrGen)
}
packetConfig := turn.PacketConnConfig{
PacketConn: udpListener,
RelayAddressGenerator: relayAddrGen,
permissionHandler := func(_clientAddr net.Addr, peerIP net.IP) bool {
// restricted peer IP is denied by default, unless allowed by the allow list,
if peerIP.IsLoopback() ||
peerIP.IsLinkLocalUnicast() ||
peerIP.IsLinkLocalMulticast() ||
peerIP.IsMulticast() ||
peerIP.IsPrivate() ||
peerIP.IsUnspecified() {
allowed := false
for _, cidr := range turnConf.AllowRestrictedPeerCIDRs {
if _, ipnet, err := net.ParseCIDR(cidr); err == nil {
if ipnet.Contains(peerIP) {
allowed = true
break
}
}
}
if !allowed {
return false
}
// if allowed, check deny list for overrides
}
for _, cidr := range turnConf.DenyPeerCIDRs {
if _, ipnet, err := net.ParseCIDR(cidr); err == nil {
if ipnet.Contains(peerIP) {
return false
}
}
}
return true
}
if turnConf.TLSPort > 0 {
var listener net.Listener
var listenerErr error
if turnConf.ExternalTLS {
listener, listenerErr = net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort)))
} else {
cert, err := tls.LoadX509KeyPair(turnConf.CertFile, turnConf.KeyFile)
if err != nil {
return nil, errors.Wrap(err, "TURN tls cert required")
}
listener, listenerErr = tls.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort)),
&tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
})
}
if listenerErr != nil {
return nil, errors.Wrap(listenerErr, "could not listen on TURN TCP port")
}
if standalone {
listener = telemetry.NewListener(listener)
}
listenerConfig := turn.ListenerConfig{
Listener: listener,
RelayAddressGenerator: relayAddrGen,
PermissionHandler: permissionHandler,
}
serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig)
logValues = append(logValues, "turn.portTLS", turnConf.TLSPort, "turn.externalTLS", turnConf.ExternalTLS)
}
if turnConf.UDPPort > 0 {
udpListener, err := net.ListenPacket("udp", net.JoinHostPort(addr, strconv.Itoa(turnConf.UDPPort)))
if err != nil {
return nil, errors.Wrap(err, "could not listen on TURN UDP port")
}
if standalone {
udpListener = telemetry.NewPacketConn(udpListener, prometheus.Incoming)
}
packetConfig := turn.PacketConnConfig{
PacketConn: udpListener,
RelayAddressGenerator: relayAddrGen,
PermissionHandler: permissionHandler,
}
serverConfig.PacketConnConfigs = append(serverConfig.PacketConnConfigs, packetConfig)
logValues = append(logValues, "turn.portUDP", turnConf.UDPPort)
}
serverConfig.PacketConnConfigs = append(serverConfig.PacketConnConfigs, packetConfig)
logValues = append(logValues, "turn.portUDP", turnConf.UDPPort)
}
logger.Infow("Starting TURN server", logValues...)
@@ -160,33 +205,82 @@ func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler {
}
}
func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID) string {
return base62.EncodeToString([]byte(fmt.Sprintf("%s|%s", apiKey, pID)))
func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) (string, int64) {
expiry := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Unix()
return base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", apiKey, pID, expiry)), expiry
}
func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID) (string, error) {
func (h *TURNAuthHandler) ParseUsername(username string) (string, livekit.ParticipantID, int64, error) {
decoded, err := base62.DecodeString(username)
if err != nil {
return "", "", 0, err
}
parts := strings.Split(string(decoded), "|")
if len(parts) != 3 {
return "", "", 0, errors.New("invalid username")
}
expiry, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return "", "", 0, err
}
if expiry == 0 {
return "", "", 0, ErrExpired
}
return parts[0], livekit.ParticipantID(parts[1]), expiry, nil
}
func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) {
if expiry == 0 || time.Now().After(time.Unix(expiry, 0)) {
return "", ErrExpired
}
return h.computePassword(apiKey, pID, expiry)
}
func (h *TURNAuthHandler) computePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) {
secret := h.keyProvider.GetSecret(apiKey)
if secret == "" {
return "", ErrInvalidAPIKey
}
keyInput := fmt.Sprintf("%s|%s", secret, pID)
keyInput := fmt.Sprintf("%s|%s|%d", secret, pID, expiry)
sum := sha256.Sum256([]byte(keyInput))
return base62.EncodeToString(sum[:]), nil
}
func (h *TURNAuthHandler) HandleAuth(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
func (h *TURNAuthHandler) HandleAuth(ra *turn.RequestAttributes) (userID string, key []byte, ok bool) {
username := ra.Username
decoded, err := base62.DecodeString(username)
if err != nil {
return nil, false
return "", nil, false
}
parts := strings.Split(string(decoded), "|")
if len(parts) != 2 {
return nil, false
if len(parts) != 3 {
return "", nil, false
}
password, err := h.CreatePassword(parts[0], livekit.ParticipantID(parts[1]))
expiry, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
logger.Warnw("could not create TURN password", err, "username", username)
return nil, false
return "", nil, false
}
return turn.GenerateAuthKey(username, LivekitRealm, password), true
if expiry == 0 {
return "", nil, false
}
expiryTime := time.Unix(expiry, 0)
if time.Now().After(expiryTime) {
// TTL only applies to initial allocation. Refresh / CreatePermission /
// ChannelBind / Send / Data requests are still authenticated against the
// username/password but skip the TTL check so long-running sessions can
// keep refreshing past the credential expiry.
if ra.Method == stun.MethodAllocate {
logger.Infow("TURN credential expired", "username", decoded, "participantID", parts[1], "expiry", expiryTime, "method", ra.Method)
return "", nil, false
}
}
password, err := h.computePassword(parts[0], livekit.ParticipantID(parts[1]), expiry)
if err != nil {
logger.Warnw("could not create TURN password", err, "username", decoded)
return "", nil, false
}
return parts[1], turn.GenerateAuthKey(username, LivekitRealm, password), true
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"fmt"
"net"
"testing"
"github.com/jxskiss/base62"
"github.com/pion/stun/v3"
"github.com/pion/turn/v5"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
)
const (
turnTestAPIKey = "APITestKey"
turnTestAPISecret = "TestSecret"
)
func newTestTurnAuthHandler() *TURNAuthHandler {
return NewTURNAuthHandler(auth.NewSimpleKeyProvider(turnTestAPIKey, turnTestAPISecret))
}
func mustAuthCreds(t *testing.T, h *TURNAuthHandler, pID livekit.ParticipantID, ttlSeconds int) (username string, key []byte) {
t.Helper()
username, expiry := h.CreateUsername(turnTestAPIKey, pID, ttlSeconds)
password, err := h.CreatePassword(turnTestAPIKey, pID, expiry)
require.NoError(t, err)
return username, turn.GenerateAuthKey(username, LivekitRealm, password)
}
func TestTURNAuthHandler_HandleAuth_ValidCredentials(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_valid")
username, expectedKey := mustAuthCreds(t, h, pID, 300)
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
userID, key, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.True(t, ok)
require.Equal(t, string(pID), userID)
require.Equal(t, expectedKey, key)
})
}
}
func TestTURNAuthHandler_HandleAuth_ExpiredAllocateRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_expired_alloc")
username, _ := h.CreateUsername(turnTestAPIKey, pID, -60)
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: stun.MethodAllocate,
})
require.False(t, ok, "Allocate request with expired credentials must be rejected")
}
func TestTURNAuthHandler_HandleAuth_ExpiredNonAllocateAllowed(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_expired_refresh")
username, expiry := h.CreateUsername(turnTestAPIKey, pID, -60)
// CreatePassword still enforces ErrExpired on its own, but the server hands
// the same key it generated at allocation time — reproduce that by directly
// hashing without going through CreatePassword's expiry guard.
password, err := h.computePassword(turnTestAPIKey, pID, expiry)
require.NoError(t, err)
expectedKey := turn.GenerateAuthKey(username, LivekitRealm, password)
for _, method := range []stun.Method{
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
userID, key, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.True(t, ok, "Non-allocate request with expired credentials must succeed")
require.Equal(t, string(pID), userID)
require.Equal(t, expectedKey, key)
})
}
}
func TestTURNAuthHandler_HandleAuth_WrongUsernameRejected(t *testing.T) {
h := newTestTurnAuthHandler()
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: "not-base62!!!",
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: stun.MethodRefresh,
})
require.False(t, ok)
}
func TestTURNAuthHandler_HandleAuth_TwoPartUsernameRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_two_part")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s", turnTestAPIKey, pID))
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.False(t, ok, "Two-part username must be rejected")
})
}
}
func TestTURNAuthHandler_HandleAuth_ZeroExpiryRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_zero_expiry")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, 0))
for _, method := range []stun.Method{
stun.MethodAllocate,
stun.MethodRefresh,
stun.MethodCreatePermission,
stun.MethodChannelBind,
stun.MethodSend,
} {
t.Run(method.String(), func(t *testing.T) {
_, _, ok := h.HandleAuth(&turn.RequestAttributes{
Username: username,
Realm: LivekitRealm,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234},
Method: method,
})
require.False(t, ok, "Username with expiry=0 must be rejected")
})
}
}
func TestTURNAuthHandler_ParseUsername_TwoPartRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_parse_two_part")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s", turnTestAPIKey, pID))
_, _, _, err := h.ParseUsername(username)
require.Error(t, err)
}
func TestTURNAuthHandler_ParseUsername_ZeroExpiryRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_parse_zero_expiry")
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, 0))
_, _, _, err := h.ParseUsername(username)
require.ErrorIs(t, err, ErrExpired)
}
func TestTURNAuthHandler_CreatePassword_ZeroExpiryRejected(t *testing.T) {
h := newTestTurnAuthHandler()
pID := livekit.ParticipantID("PA_password_zero_expiry")
_, err := h.CreatePassword(turnTestAPIKey, pID, 0)
require.ErrorIs(t, err, ErrExpired)
}
+32 -12
View File
@@ -26,10 +26,11 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/livekit/protocol/livekit"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
)
type twirpRequestFields struct {
@@ -46,9 +47,9 @@ type twirpLoggerKey struct{}
// License: Apache-2.0
func TwirpLogger() *twirp.ServerHooks {
loggerPool := &sync.Pool{
New: func() interface{} {
New: func() any {
return &twirpLogger{
fieldsOrig: make([]interface{}, 0, 30),
fieldsOrig: make([]any, 0, 30),
}
},
}
@@ -67,12 +68,20 @@ func TwirpLogger() *twirp.ServerHooks {
type twirpLogger struct {
twirpRequestFields
fieldsOrig []interface{}
fields []interface{}
fieldsOrig []any
fields []any
startedAt time.Time
deadline time.Time
}
func AppendLogFields(ctx context.Context, fields ...interface{}) {
func (t *twirpLogger) reset() {
t.fields = t.fieldsOrig
t.error = nil
t.startedAt = time.Time{}
t.deadline = time.Time{}
}
func AppendLogFields(ctx context.Context, fields ...any) {
r, ok := ctx.Value(twirpLoggerKey{}).(*twirpLogger)
if !ok || r == nil {
return
@@ -84,6 +93,9 @@ func AppendLogFields(ctx context.Context, fields ...interface{}) {
func loggerRequestReceived(ctx context.Context, twirpLoggerPool *sync.Pool) (context.Context, error) {
r := twirpLoggerPool.Get().(*twirpLogger)
r.startedAt = time.Now()
if deadline, ok := ctx.Deadline(); ok {
r.deadline = deadline
}
r.fields = r.fieldsOrig
r.error = nil
@@ -114,7 +126,14 @@ func loggerResponseSent(ctx context.Context, twirpLoggerPool *sync.Pool) {
return
}
r.fields = append(r.fields, "duration", time.Since(r.startedAt))
duration := time.Since(r.startedAt)
r.fields = append(r.fields, "duration", duration)
if !r.deadline.IsZero() {
r.fields = append(r.fields, "requestedTimeout", r.deadline.Sub(r.startedAt))
}
if deadline, ok := ctx.Deadline(); ok {
r.fields = append(r.fields, "modifiedTimeout", deadline.Sub(r.startedAt))
}
if status, ok := twirp.StatusCode(ctx); ok {
r.fields = append(r.fields, "status", status)
@@ -126,10 +145,10 @@ func loggerResponseSent(ctx context.Context, twirpLoggerPool *sync.Pool) {
serviceMethod := "API " + r.service + "." + r.method
utils.GetLogger(ctx).WithComponent(utils.ComponentAPI).Infow(serviceMethod, r.fields...)
prometheus.RecordTwirpRequestLatency(r.service, r.method, duration)
r.fields = r.fieldsOrig
r.error = nil
// reset fields and return to pool
r.reset()
twirpLoggerPool.Put(r)
}
@@ -203,7 +222,7 @@ func statusReporterResponseSent(ctx context.Context) {
code = r.error.Code()
}
prometheus.TwirpRequestStatusCounter.WithLabelValues(r.service, r.method, statusFamily, string(code)).Add(1)
prometheus.RecordTwirpRequestStatus(r.service, r.method, statusFamily, code)
}
func statusReporterErrorReceived(ctx context.Context, e twirp.Error) context.Context {
@@ -231,6 +250,7 @@ func TwirpTelemetry(
ResponseSent: func(ctx context.Context) {
telemetryResponseSent(ctx, nodeID, getProjectID, telemetry)
},
RequestRouted: telemetryRequestRouted,
}
}
@@ -390,7 +410,7 @@ func telemetryResponseSent(
}
a.NodeId = string(nodeID)
if statusCode, ok := twirp.StatusCode(ctx); ok {
if status, err := strconv.Atoi(statusCode); err == nil {
if status, err := strconv.ParseInt(statusCode, 10, 32); err == nil {
a.Status = int32(status)
}
}
+340 -2
View File
@@ -15,29 +15,85 @@
package service
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"github.com/ua-parser/uap-go/uaparser"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/routing/selector"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
func handleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...interface{}) {
var (
ErrGzipReadFailed = errors.New("cannot read decompressed data")
ErrGzipTooLarge = errors.New("decompressed data too large")
)
var gzipReaderPool = sync.Pool{
New: func() any { return &gzip.Reader{} },
}
func DecompressGzip(compressed []byte) ([]byte, error) {
reader := gzipReaderPool.Get().(*gzip.Reader)
defer gzipReaderPool.Put(reader)
if err := reader.Reset(bytes.NewReader(compressed)); err != nil {
return nil, fmt.Errorf("%w: %w", ErrGzipReadFailed, err)
}
out, err := io.ReadAll(io.LimitReader(reader, http.DefaultMaxHeaderBytes+1))
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrGzipReadFailed, err)
}
if len(out) > http.DefaultMaxHeaderBytes {
return nil, ErrGzipTooLarge
}
return out, nil
}
func handleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...any) {
keysAndValues = append(keysAndValues, "status", status)
if r != nil && r.URL != nil {
keysAndValues = append(keysAndValues, "method", r.Method, "path", r.URL.Path)
}
if !errors.Is(err, context.Canceled) && !errors.Is(r.Context().Err(), context.Canceled) {
logger.GetLogger().WithCallDepth(1).Warnw("error handling request", err, keysAndValues...)
utils.GetLogger(r.Context()).WithCallDepth(1).Warnw("error handling request", err, keysAndValues...)
}
w.WriteHeader(status)
}
func HandleError(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...any) {
handleError(w, r, status, err, keysAndValues...)
_, _ = w.Write([]byte(err.Error()))
}
func HandleErrorJson(w http.ResponseWriter, r *http.Request, status int, err error, keysAndValues ...any) {
handleError(w, r, status, err, keysAndValues...)
json.NewEncoder(w).Encode(struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
w.Header().Add("Content-type", "application/json")
}
func boolValue(s string) bool {
return s == "1" || s == "true"
}
@@ -81,4 +137,286 @@ func SetRoomConfiguration(createRequest *livekit.CreateRoomRequest, conf *liveki
createRequest.MinPlayoutDelay = conf.MinPlayoutDelay
createRequest.MaxPlayoutDelay = conf.MaxPlayoutDelay
createRequest.SyncStreams = conf.SyncStreams
createRequest.Metadata = conf.Metadata
createRequest.Tags = conf.Tags
}
func ParseClientInfo(r *http.Request) *livekit.ClientInfo {
values := r.Form
ci := &livekit.ClientInfo{}
if pv, err := strconv.ParseInt(values.Get("protocol"), 10, 32); err == nil {
ci.Protocol = int32(pv)
}
if cp, err := strconv.ParseInt(values.Get("client_protocol"), 10, 32); err == nil {
ci.ClientProtocol = int32(cp)
}
sdkString := values.Get("sdk")
switch sdkString {
case "js":
ci.Sdk = livekit.ClientInfo_JS
case "ios", "swift":
ci.Sdk = livekit.ClientInfo_SWIFT
case "android":
ci.Sdk = livekit.ClientInfo_ANDROID
case "flutter":
ci.Sdk = livekit.ClientInfo_FLUTTER
case "go":
ci.Sdk = livekit.ClientInfo_GO
case "unity":
ci.Sdk = livekit.ClientInfo_UNITY
case "reactnative":
ci.Sdk = livekit.ClientInfo_REACT_NATIVE
case "rust":
ci.Sdk = livekit.ClientInfo_RUST
case "python":
ci.Sdk = livekit.ClientInfo_PYTHON
case "cpp":
ci.Sdk = livekit.ClientInfo_CPP
case "unityweb":
ci.Sdk = livekit.ClientInfo_UNITY_WEB
case "node":
ci.Sdk = livekit.ClientInfo_NODE
case "esp32":
ci.Sdk = livekit.ClientInfo_ESP32
}
ci.Version = values.Get("version")
ci.Os = values.Get("os")
ci.OsVersion = values.Get("os_version")
ci.Browser = values.Get("browser")
ci.BrowserVersion = values.Get("browser_version")
ci.DeviceModel = values.Get("device_model")
ci.Network = values.Get("network")
if capStr := values.Get("capabilities"); capStr != "" {
for _, name := range strings.Split(capStr, ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if v, ok := livekit.ClientInfo_Capability_value[name]; ok {
ci.Capabilities = append(ci.Capabilities, livekit.ClientInfo_Capability(v))
}
}
}
AugmentClientInfo(ci, r)
return ci
}
var (
userAgentParserCache *uaparser.Parser
userAgentParserInit sync.Once
)
func createUserAgentParserWithCustomRules() (*uaparser.Parser, error) {
defaultYaml := uaparser.DefinitionYaml
rules := make(map[string]any)
err := yaml.Unmarshal(defaultYaml, rules)
if err != nil {
return nil, err
}
rules["user_agent_parsers"] = append(rules["user_agent_parsers"].([]any), map[string]any{
"regex": "OBS-Studio\\/([0-9\\.]+)",
"family_replacement": "OBS Studio",
"v1_replacement": "$1",
})
customYaml, err := yaml.Marshal(rules)
if err != nil {
return nil, err
}
return uaparser.NewFromBytes([]byte(customYaml))
}
func getUserAgentParser() *uaparser.Parser {
userAgentParserInit.Do(func() {
if parser, err := createUserAgentParserWithCustomRules(); err != nil {
logger.Warnw("could not create user agent parser with custom rules, using default", err)
userAgentParserCache = uaparser.NewFromSaved()
} else {
userAgentParserCache = parser
}
})
return userAgentParserCache
}
func AugmentClientInfo(ci *livekit.ClientInfo, req *http.Request) {
if ci == nil {
return
}
// get real address (forwarded http header) - check Cloudflare headers first, fall back to X-Forwarded-For
ci.Address = GetClientIP(req)
// attempt to parse types for SDKs that support browser as a platform
if ci.Sdk == livekit.ClientInfo_JS ||
ci.Sdk == livekit.ClientInfo_REACT_NATIVE ||
ci.Sdk == livekit.ClientInfo_FLUTTER ||
ci.Sdk == livekit.ClientInfo_UNITY ||
ci.Sdk == livekit.ClientInfo_UNKNOWN {
client := getUserAgentParser().Parse(req.UserAgent())
if ci.Browser == "" {
ci.Browser = client.UserAgent.Family
ci.BrowserVersion = client.UserAgent.ToVersionString()
}
if ci.Os == "" {
ci.Os = client.Os.Family
ci.OsVersion = client.Os.ToVersionString()
}
if ci.DeviceModel == "" {
model := client.Device.Family
if model != "" && client.Device.Model != "" && model != client.Device.Model {
model += " " + client.Device.Model
}
ci.DeviceModel = model
}
}
}
type ValidateConnectRequestParams struct {
roomName livekit.RoomName
publish string
metadata string
attributes map[string]string
}
type ValidateConnectRequestResult struct {
roomName livekit.RoomName
grants *auth.ClaimGrants
region string
createRoomRequest *livekit.CreateRoomRequest
}
func ValidateConnectRequest(
lgr logger.Logger,
r *http.Request,
limitConfig config.LimitConfig,
params ValidateConnectRequestParams,
router routing.MessageRouter,
roomAllocator RoomAllocator,
) (ValidateConnectRequestResult, int, error) {
var res ValidateConnectRequestResult
// require a claim
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
roomNameInToken, err := EnsureJoinPermission(r.Context())
if err != nil {
return res, http.StatusUnauthorized, err
}
if claims.Identity == "" {
return res, http.StatusBadRequest, ErrIdentityEmpty
}
if !limitConfig.CheckParticipantIdentityLength(claims.Identity) {
return res, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, limitConfig.MaxParticipantIdentityLength)
}
if claims.RoomConfig != nil {
if err := claims.RoomConfig.CheckCredentials(); err != nil {
lgr.Warnw("credentials found in token", nil)
// TODO(dz): in a future version, we'll reject these connections
}
}
res.roomName = params.roomName
if roomNameInToken != "" {
res.roomName = roomNameInToken
}
if res.roomName == "" {
return res, http.StatusBadRequest, ErrNoRoomName
}
if !limitConfig.CheckRoomNameLength(string(res.roomName)) {
return res, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, limitConfig.MaxRoomNameLength)
}
// this is new connection for existing participant - with publish only permissions
if params.publish != "" {
// Make sure grant has GetCanPublish set,
if !claims.Video.GetCanPublish() {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
// Make sure by default subscribe is off
claims.Video.SetCanSubscribe(false)
claims.Identity += "#" + params.publish
}
// room allocator validations
err = roomAllocator.ValidateCreateRoom(r.Context(), res.roomName)
if err != nil {
if errors.Is(err, ErrRoomNotFound) {
return res, http.StatusNotFound, err
} else {
return res, http.StatusInternalServerError, err
}
}
if router, ok := router.(routing.Router); ok {
res.region = router.GetRegion()
if foundNode, err := router.GetNodeForRoom(r.Context(), res.roomName); err == nil {
if selector.LimitsReached(limitConfig, foundNode.Stats) {
return res, http.StatusServiceUnavailable, rtc.ErrLimitExceeded
}
}
}
createRequest := &livekit.CreateRoomRequest{
Name: string(res.roomName),
RoomPreset: claims.RoomPreset,
}
SetRoomConfiguration(createRequest, claims.GetRoomConfiguration())
res.createRoomRequest = createRequest
if len(params.metadata) != 0 {
// Make sure grant has GetCanUpdateOwnMetadata set
if !claims.Video.GetCanUpdateOwnMetadata() {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
claims.Metadata = params.metadata
}
// Add extra attributes to the participant
if len(params.attributes) != 0 {
// Make sure grant has GetCanUpdateOwnMetadata set
if !claims.Video.GetCanUpdateOwnMetadata() {
return res, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
if claims.Attributes == nil {
claims.Attributes = make(map[string]string, len(params.attributes))
}
for k, v := range params.attributes {
if v == "" {
continue // do not allow deleting existing attributes
}
claims.Attributes[k] = v
}
}
res.grants = claims
return res, http.StatusOK, nil
}
func IsRTCPath(path string) bool {
return path == "/rtc" || path == "/rtc/v1"
}
func IsRTCValidatePath(path string) bool {
return path == "/rtc/validate" || path == "/rtc/v1/validate"
}
func IsAgentWorkerPath(path string) bool {
return path == "/agent"
}
func IsAgentPath(path string) bool {
return strings.HasPrefix(path, "/agent")
}
+52
View File
@@ -15,7 +15,10 @@
package service_test
import (
"bytes"
"compress/gzip"
"context"
"net/http"
"testing"
"time"
@@ -75,3 +78,52 @@ func TestIsValidDomain(t *testing.T) {
require.Equal(t, service.IsValidDomain(key), result)
}
}
func compress(t *testing.T, payload []byte) []byte {
t.Helper()
var buf bytes.Buffer
gw, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
require.NoError(t, err)
_, err = gw.Write(payload)
require.NoError(t, err)
require.NoError(t, gw.Close())
return buf.Bytes()
}
func TestDecompressGzip(t *testing.T) {
t.Run("small payload", func(t *testing.T) {
out, err := service.DecompressGzip(compress(t, []byte("hello world")))
require.NoError(t, err)
require.Equal(t, []byte("hello world"), out)
})
t.Run("payload exactly at cap", func(t *testing.T) {
raw := make([]byte, http.DefaultMaxHeaderBytes)
out, err := service.DecompressGzip(compress(t, raw))
require.NoError(t, err)
require.Len(t, out, http.DefaultMaxHeaderBytes)
})
t.Run("payload one byte over capd", func(t *testing.T) {
raw := make([]byte, http.DefaultMaxHeaderBytes+1)
_, err := service.DecompressGzip(compress(t, raw))
require.ErrorIs(t, err, service.ErrGzipTooLarge)
})
t.Run("gzip decompression bomb", func(t *testing.T) {
// 100 MB of zeros
raw := make([]byte, 100<<20)
compressed := compress(t, raw)
require.Less(t, len(compressed), 1<<20,
"sanity: bomb input should compress dramatically")
_, err := service.DecompressGzip(compressed)
require.ErrorIs(t, err, service.ErrGzipTooLarge)
})
t.Run("malformed gzip compression", func(t *testing.T) {
_, err := service.DecompressGzip([]byte("not gzip data"))
require.Error(t, err)
require.Contains(t, err.Error(), "cannot read decompressed")
})
}
+544
View File
@@ -0,0 +1,544 @@
// 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 service
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/pion/webrtc/v4"
"github.com/tomnomnom/linkheader"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/rtc/types"
sutils "github.com/livekit/livekit-server/pkg/utils"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils/guid"
"github.com/livekit/psrpc"
)
const (
cParticipantPath = "/whip/v1"
cParticipantIDPath = "/whip/v1/{participant_id}"
)
type WHIPService struct {
http.Handler
config *config.Config
router routing.Router
roomAllocator RoomAllocator
client rpc.WHIPClient[livekit.NodeID]
topicFormatter rpc.TopicFormatter
participantClient rpc.TypedWHIPParticipantClient
}
func NewWHIPService(
config *config.Config,
router routing.Router,
roomAllocator RoomAllocator,
clientParams rpc.ClientParams,
topicFormatter rpc.TopicFormatter,
participantClient rpc.TypedWHIPParticipantClient,
) (*WHIPService, error) {
client, err := rpc.NewWHIPClient[livekit.NodeID](clientParams.Args())
if err != nil {
return nil, err
}
return &WHIPService{
config: config,
router: router,
roomAllocator: roomAllocator,
client: client,
topicFormatter: topicFormatter,
participantClient: participantClient,
}, nil
}
func (s *WHIPService) SetupRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET "+cParticipantPath, s.handleGet)
mux.HandleFunc("OPTIONS "+cParticipantPath, s.handleOptions)
mux.HandleFunc("POST "+cParticipantPath, s.handleCreate)
mux.HandleFunc("GET "+cParticipantIDPath, s.handleParticipantGet)
mux.HandleFunc("PATCH "+cParticipantIDPath, s.handleParticipantPatch)
mux.HandleFunc("DELETE "+cParticipantIDPath, s.handleParticipantDelete)
}
func (s *WHIPService) handleGet(w http.ResponseWriter, r *http.Request) {
// https:/www.rfc-editor.org/rfc/rfc9725.html#name-http-usage
w.WriteHeader(http.StatusNoContent)
}
func (s *WHIPService) handleOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Methods", "PATCH, OPTIONS, GET, POST, DELETE")
w.Header().Set("Access-Control-Expose-Headers", "*")
w.WriteHeader(http.StatusOK)
// According to https://www.rfc-editor.org/rfc/rfc9725.html#name-stun-turn-server-configurat,
// ICE servers can be returned in OPTIONS response, but not recommended.
//
// Supporting that here is tricky. This would have to get region settings like the
// session CREATE POST request and send a request to get ICE servers from a
// region + media node that is selected. The issue is that a subsequent POST,
// although unlikely, may end up in a different region. Media node in one region and
// TURN in another region, although shuttling media across regions, should still work.
// But, as this is not a recommended way, not supporting it.
}
type createRequest struct {
RoomName livekit.RoomName
ParticipantInit routing.ParticipantInit
ClientIP string
OfferSDP string
SubscribedParticipantTrackNames map[string][]string
FromIngress bool
}
func (s *WHIPService) validateCreate(w http.ResponseWriter, r *http.Request) (*createRequest, int, error) {
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
return nil, http.StatusUnauthorized, rtc.ErrPermissionDenied
}
roomName, err := EnsureJoinPermission(r.Context())
if err != nil {
return nil, http.StatusUnauthorized, err
}
if roomName == "" {
return nil, http.StatusUnauthorized, errors.New("room name cannot be empty")
}
if !s.config.Limit.CheckRoomNameLength(string(roomName)) {
return nil, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, s.config.Limit.MaxRoomNameLength)
}
if claims.Identity == "" {
return nil, http.StatusBadRequest, ErrIdentityEmpty
}
if !s.config.Limit.CheckParticipantIdentityLength(claims.Identity) {
return nil, http.StatusBadRequest, fmt.Errorf("%w: max length %d", ErrParticipantIdentityExceedsLimits, s.config.Limit.MaxParticipantIdentityLength)
}
var clientInfo struct {
ClientIP string `json:"clientIp"`
SubscribedParticipantTrackNames map[string][]string `json:"subscribedParticipantTrackNames"`
}
clientInfoHeader := r.Header.Get("X-LiveKit-ClientInfo")
if clientInfoHeader != "" {
if err := json.NewDecoder(strings.NewReader(clientInfoHeader)).Decode(&clientInfo); err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed json in client info header: %s", err)
}
}
fromIngress := r.Header.Get("X-Livekit-Ingress")
offerSDPBytes, err := io.ReadAll(http.MaxBytesReader(w, r.Body, http.DefaultMaxHeaderBytes))
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
return nil, http.StatusRequestEntityTooLarge, fmt.Errorf("request body exceeds %d bytes", maxErr.Limit)
}
return nil, http.StatusBadRequest, fmt.Errorf("body does not have SDP offer: %s", err)
}
if len(offerSDPBytes) == 0 {
return nil, http.StatusBadRequest, errors.New("body does not have SDP offer")
}
offerSDP := string(offerSDPBytes)
sd := &webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer,
SDP: offerSDP,
}
_, err = sd.Unmarshal()
if err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("malformed SDP offer: %s", err)
}
ci := ParseClientInfo(r)
if ci.Protocol == 0 {
// if no client info available (which will be mostly the case with WHIP clients), at least set protocol
ci.Protocol = types.CurrentProtocol
}
pi := routing.ParticipantInit{
Identity: livekit.ParticipantIdentity(claims.Identity),
Name: livekit.ParticipantName(claims.Name),
AutoSubscribe: true,
Client: ci,
Grants: claims,
CreateRoom: &livekit.CreateRoomRequest{
Name: string(roomName),
RoomPreset: claims.RoomPreset,
},
AdaptiveStream: false,
DisableICELite: true,
}
SetRoomConfiguration(pi.CreateRoom, claims.GetRoomConfiguration())
return &createRequest{
roomName,
pi,
clientInfo.ClientIP,
offerSDP,
clientInfo.SubscribedParticipantTrackNames,
fromIngress != "",
}, http.StatusOK, nil
}
func (s *WHIPService) handleCreate(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-type") != "application/sdp" {
s.handleError("Create", w, r, http.StatusBadRequest, fmt.Errorf("unsupported content-type: %s", r.Header.Get("Content-type")))
return
}
w.Header().Add("Content-type", "application/sdp")
req, status, err := s.validateCreate(w, r)
if err != nil {
s.handleError("Create", w, r, status, err)
return
}
if err := s.roomAllocator.SelectRoomNode(r.Context(), req.RoomName, ""); err != nil {
s.handleError("Create", w, r, http.StatusInternalServerError, err)
return
}
rtcNode, err := s.router.GetNodeForRoom(r.Context(), req.RoomName)
if err != nil {
s.handleError("Create", w, r, http.StatusInternalServerError, err)
return
}
connID := livekit.ConnectionID(guid.New("CO_"))
starSession, err := req.ParticipantInit.ToStartSession(req.RoomName, connID)
if err != nil {
s.handleError("Create", w, r, http.StatusInternalServerError, err)
return
}
subscribedParticipantTracks := map[string]*rpc.WHIPCreateRequest_TrackList{}
for identity, trackNames := range req.SubscribedParticipantTrackNames {
subscribedParticipantTracks[identity] = &rpc.WHIPCreateRequest_TrackList{
TrackNames: trackNames,
}
}
res, err := s.client.Create(r.Context(), livekit.NodeID(rtcNode.Id), &rpc.WHIPCreateRequest{
OfferSdp: req.OfferSDP,
StartSession: starSession,
SubscribedParticipantTracks: subscribedParticipantTracks,
FromIngress: req.FromIngress,
})
if err != nil {
s.handleError("Create", w, r, http.StatusServiceUnavailable, err)
return
}
// created resource sent in Location header:
// https://www.rfc-editor.org/rfc/rfc9725.html#name-ingest-session-setup
// using relative location
w.Header().Add("Location", fmt.Sprintf("%s/%s", cParticipantPath, res.ParticipantId))
// ICE servers as Link header(s):
// https://www.rfc-editor.org/rfc/rfc9725.html#name-stun-turn-server-configurat
var iceServerLinks []*linkheader.Link
for _, iceServer := range res.IceServers {
for _, iceURL := range iceServer.Urls {
iceServerLink := &linkheader.Link{
URL: url.PathEscape(iceURL),
Rel: "ice-server",
Params: map[string]string{},
}
if iceServer.Username != "" {
iceServerLink.Params["username"] = iceServer.Username
}
if iceServer.Credential != "" {
iceServerLink.Params["credential"] = iceServer.Credential
}
iceServerLinks = append(iceServerLinks, iceServerLink)
}
}
for _, iceServerLink := range iceServerLinks {
w.Header().Add("Link", iceServerLink.String())
}
// To support ICE Trickle/Restart, HTTP PATCH should have an ETag
// send ICE session ID (ICE ufrag is used as ID) in ETag header
// https://www.rfc-editor.org/rfc/rfc9725.html#name-http-patch-request-usage
if res.IceSessionId != "" {
w.Header().Add("ETag", res.IceSessionId)
}
// 201 Status Created
w.WriteHeader(http.StatusCreated)
// SDP answer in the response body
w.Write([]byte(res.AnswerSdp))
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Create",
"connID", connID,
"participant", req.ParticipantInit.Identity,
"room", req.RoomName,
"status", http.StatusCreated,
"response", logger.Proto(res),
)
}
func (s *WHIPService) handleParticipantGet(w http.ResponseWriter, r *http.Request) {
// https:/www.rfc-editor.org/rfc/rfc9725.html#name-http-usage
w.WriteHeader(http.StatusNoContent)
}
func (s *WHIPService) iceTrickle(
w http.ResponseWriter,
r *http.Request,
roomName livekit.RoomName,
participantIdentity livekit.ParticipantIdentity,
pID livekit.ParticipantID,
iceSessionID string,
sdpFragment string,
) {
_, err := s.participantClient.ICETrickle(
r.Context(),
s.topicFormatter.ParticipantTopic(r.Context(), roomName, participantIdentity),
&rpc.WHIPParticipantICETrickleRequest{
Room: string(roomName),
ParticipantIdentity: string(participantIdentity),
ParticipantId: string(pID),
IceSessionId: iceSessionID,
SdpFragment: sdpFragment,
},
)
if err != nil {
var pe psrpc.Error
if errors.As(err, &pe) {
switch pe.Code() {
case psrpc.NotFound:
s.handleError("Patch", w, r, http.StatusNotFound, errors.New(pe.Error()))
case psrpc.InvalidArgument:
switch pe.Error() {
case rtc.ErrInvalidSDPFragment.Error(), rtc.ErrMidMismatch.Error(), rtc.ErrICECredentialMismatch.Error():
s.handleError("Patch", w, r, http.StatusBadRequest, errors.New(pe.Error()))
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
} else {
s.handleError("Patch", w, r, http.StatusInternalServerError, nil)
}
return
}
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Patch",
"method", "ice-trickle",
"room", roomName,
"participant", participantIdentity,
"participantID", pID,
"sdpFragment", sdpFragment,
"status", http.StatusNoContent,
)
w.WriteHeader(http.StatusNoContent)
}
func (s *WHIPService) iceRestart(
w http.ResponseWriter,
r *http.Request,
roomName livekit.RoomName,
participantIdentity livekit.ParticipantIdentity,
pID livekit.ParticipantID,
sdpFragment string,
) {
res, err := s.participantClient.ICERestart(
r.Context(),
s.topicFormatter.ParticipantTopic(r.Context(), roomName, participantIdentity),
&rpc.WHIPParticipantICERestartRequest{
Room: string(roomName),
ParticipantIdentity: string(participantIdentity),
ParticipantId: string(pID),
SdpFragment: sdpFragment,
},
)
if err != nil {
var pe psrpc.Error
if errors.As(err, &pe) {
switch pe.Code() {
case psrpc.NotFound:
s.handleError("Patch", w, r, http.StatusNotFound, errors.New(pe.Error()))
case psrpc.InvalidArgument:
switch pe.Error() {
case rtc.ErrInvalidSDPFragment.Error():
s.handleError("Patch", w, r, http.StatusBadRequest, errors.New(pe.Error()))
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
default:
s.handleError("Patch", w, r, http.StatusInternalServerError, errors.New(pe.Error()))
}
} else {
s.handleError("Patch", w, r, http.StatusInternalServerError, nil)
}
return
}
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Patch",
"method", "ice-restart",
"room", roomName,
"participant", participantIdentity,
"participantID", pID,
"sdpFragment", sdpFragment,
"status", http.StatusNoContent,
"res", logger.Proto(res),
)
if res.IceSessionId != "" {
w.Header().Add("ETag", res.IceSessionId)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(res.SdpFragment))
}
func (s *WHIPService) handleParticipantPatch(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Content-type") != "application/trickle-ice-sdpfrag" {
s.handleError("Patch", w, r, http.StatusBadRequest, fmt.Errorf("unsupported content-type: %s", r.Header.Get("Content-type")))
return
}
w.Header().Add("Content-type", "application/trickle-ice-sdpfrag")
// https://www.rfc-editor.org/rfc/rfc9725.html#name-http-patch-request-usage
ifMatch := r.Header.Get("If-Match")
if ifMatch == "" {
s.handleError("Patch", w, r, http.StatusPreconditionRequired, errors.New("missing entity tag"))
return
}
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
s.handleError("Patch", w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return
}
roomName, err := EnsureJoinPermission(r.Context())
if err != nil {
s.handleError("Patch", w, r, http.StatusUnauthorized, err)
return
}
if roomName == "" {
s.handleError("Patch", w, r, http.StatusUnauthorized, errors.New("room name cannot be empty"))
return
}
if claims.Identity == "" {
s.handleError("Patch", w, r, http.StatusUnauthorized, errors.New("participant identity cannot be empty"))
return
}
pID := livekit.ParticipantID(r.PathValue("participant_id"))
if pID == "" {
s.handleError("Patch", w, r, http.StatusBadRequest, errors.New("participant ID cannot be empty"))
return
}
sdpFragmentBytes, err := io.ReadAll(http.MaxBytesReader(w, r.Body, http.DefaultMaxHeaderBytes))
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
s.handleError("Patch", w, r, http.StatusRequestEntityTooLarge, fmt.Errorf("request body exceeds %d bytes", maxErr.Limit))
return
}
s.handleError("Patch", w, r, http.StatusBadRequest, fmt.Errorf("body does not have SDP fragment: %s", err))
return
}
sdpFragment := string(sdpFragmentBytes)
if ifMatch == "*" {
s.iceRestart(w, r, roomName, livekit.ParticipantIdentity(claims.Identity), pID, sdpFragment)
} else {
s.iceTrickle(w, r, roomName, livekit.ParticipantIdentity(claims.Identity), pID, ifMatch, sdpFragment)
}
}
func (s *WHIPService) handleParticipantDelete(w http.ResponseWriter, r *http.Request) {
claims := GetGrants(r.Context())
if claims == nil || claims.Video == nil {
s.handleError("Delete", w, r, http.StatusUnauthorized, rtc.ErrPermissionDenied)
return
}
roomName, err := EnsureJoinPermission(r.Context())
if err != nil {
s.handleError("Delete", w, r, http.StatusUnauthorized, err)
return
}
if roomName == "" {
s.handleError("Delete", w, r, http.StatusUnauthorized, errors.New("room name cannot be empty"))
return
}
if claims.Identity == "" {
s.handleError("Delete", w, r, http.StatusUnauthorized, errors.New("participant identity cannot be empty"))
return
}
_, err = s.participantClient.DeleteSession(
r.Context(),
s.topicFormatter.ParticipantTopic(r.Context(), roomName, livekit.ParticipantIdentity(claims.Identity)),
&rpc.WHIPParticipantDeleteSessionRequest{
Room: string(roomName),
ParticipantIdentity: claims.Identity,
ParticipantId: r.PathValue("participant_id"),
},
)
if err != nil {
s.handleError("Delete", w, r, http.StatusNotFound, err)
return
}
sutils.GetLogger(r.Context()).Infow(
"API WHIP.Delete",
"participant", claims.Identity,
"participantID", r.PathValue("participant_id"),
"room", roomName,
"status", http.StatusOK,
)
w.WriteHeader(http.StatusOK)
}
func (s *WHIPService) handleError(method string, w http.ResponseWriter, r *http.Request, status int, err error) {
sutils.GetLogger(r.Context()).Warnw(
fmt.Sprintf("API WHIP.%s", method), err,
"status", status,
)
w.WriteHeader(status)
json.NewEncoder(w).Encode(struct {
Error string `json:"error"`
}{
Error: err.Error(),
})
}
+55 -21
View File
@@ -22,17 +22,11 @@ import (
"os"
"github.com/google/wire"
"github.com/pion/turn/v4"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gopkg.in/yaml.v3"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/clientconfiguration"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/auth"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
@@ -41,6 +35,13 @@ import (
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/webhook"
"github.com/livekit/psrpc"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/sfu"
"github.com/livekit/livekit-server/pkg/telemetry"
)
func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*LivekitServer, error) {
@@ -51,15 +52,15 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
wire.Bind(new(ServiceStore), new(ObjectStore)),
createKeyProvider,
createWebhookNotifier,
createClientConfiguration,
createForwardStats,
getNodeStatsConfig,
routing.CreateRouter,
getLimitConf,
config.DefaultAPIConfig,
getAPIConf,
wire.Bind(new(routing.MessageRouter), new(routing.Router)),
wire.Bind(new(livekit.RoomService), new(*RoomService)),
telemetry.NewAnalyticsService,
telemetry.NewTelemetryService,
createTelemetryService,
getMessageBus,
NewIOInfoService,
wire.Bind(new(IOClient), new(*IOInfoService)),
@@ -71,15 +72,17 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
getIngressStore,
getIngressConfig,
NewIngressService,
rpc.NewSIPClient,
newSIPClient,
getSIPStore,
getSIPConfig,
NewSIPService,
NewRoomAllocator,
NewRoomService,
NewRTCService,
NewWHIPService,
NewAgentService,
NewAgentDispatchService,
getAgentConfig,
agent.NewAgentClient,
getAgentStore,
getSignalRelayConfig,
@@ -93,6 +96,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
rpc.NewTopicFormatter,
rpc.NewTypedRoomClient,
rpc.NewTypedParticipantClient,
rpc.NewTypedWHIPParticipantClient,
rpc.NewTypedAgentDispatchInternalClient,
NewLocalRoomManager,
NewTURNAuthHandler,
@@ -116,6 +120,7 @@ func InitializeRouter(conf *config.Config, currentNode routing.LocalNode) (routi
getRoomConfig,
routing.NewRoomManagerClient,
rpc.NewKeepalivePubSub,
getNodeStatsConfig,
routing.CreateRouter,
)
@@ -157,15 +162,21 @@ func createKeyProvider(conf *config.Config) (auth.KeyProvider, error) {
func createWebhookNotifier(conf *config.Config, provider auth.KeyProvider) (webhook.QueuedNotifier, error) {
wc := conf.WebHook
if len(wc.URLs) == 0 {
return nil, nil
}
secret := provider.GetSecret(wc.APIKey)
if secret == "" {
if secret == "" && len(wc.URLs) > 0 {
return nil, ErrWebHookMissingAPIKey
}
return webhook.NewDefaultNotifier(wc.APIKey, secret, wc.URLs), nil
return webhook.NewDefaultNotifier(wc, provider)
}
func createTelemetryService(notifier webhook.QueuedNotifier, analytics telemetry.AnalyticsService) telemetry.TelemetryService {
svc := telemetry.NewTelemetryService(notifier, analytics)
if notifier != nil {
notifier.RegisterProcessedHook(svc.Webhook)
}
return svc
}
func createRedisClient(conf *config.Config) (redis.UniversalClient, error) {
@@ -222,6 +233,19 @@ func getIngressConfig(conf *config.Config) *config.IngressConfig {
return &conf.Ingress
}
func newSIPClient(p rpc.ClientParams) (rpc.SIPClient, error) {
// Do not pass parameters directly, as they set timeout that is too short,
// and might set retry policy that is not acceptable for SIP methods.
// Instead, set relevant parameters manually.
return rpc.NewSIPClientWithParams(rpc.ClientParams{
Bus: p.Bus,
ClientOptions: []psrpc.ClientOption{
rpc.WithClientLogger(p.Logger),
otelpsrpc.ClientOptions(otelpsrpc.Config{}),
},
})
}
func getSIPStore(s ObjectStore) SIPStore {
switch store := s.(type) {
case *RedisStore:
@@ -235,10 +259,6 @@ func getSIPConfig(conf *config.Config) *config.SIPConfig {
return &conf.SIP
}
func createClientConfiguration() clientconfiguration.ClientConfigurationManager {
return clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations)
}
func getLimitConf(config *config.Config) config.LimitConfig {
return config.Limit
}
@@ -256,7 +276,9 @@ func getPSRPCConfig(config *config.Config) rpc.PSRPCConfig {
}
func getPSRPCClientParams(config rpc.PSRPCConfig, bus psrpc.MessageBus) rpc.ClientParams {
return rpc.NewClientParams(config, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{})
return rpc.NewClientParams(config, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{},
otelpsrpc.ClientOptions(otelpsrpc.Config{}),
)
}
func createForwardStats(conf *config.Config) *sfu.ForwardStats {
@@ -269,3 +291,15 @@ func createForwardStats(conf *config.Config) *sfu.ForwardStats {
func newInProcessTurnServer(conf *config.Config, authHandler turn.AuthHandler) (*turn.Server, error) {
return NewTurnServer(conf, authHandler, false)
}
func getNodeStatsConfig(config *config.Config) config.NodeStatsConfig {
return config.NodeStats
}
func getAgentConfig(config *config.Config) agent.Config {
return config.Agents
}
func getAPIConf(config *config.Config) config.APIConfig {
return config.API
}
+56 -24
View File
@@ -9,7 +9,6 @@ package service
import (
"fmt"
"github.com/livekit/livekit-server/pkg/agent"
"github.com/livekit/livekit-server/pkg/clientconfiguration"
"github.com/livekit/livekit-server/pkg/config"
"github.com/livekit/livekit-server/pkg/routing"
"github.com/livekit/livekit-server/pkg/sfu"
@@ -22,7 +21,8 @@ import (
"github.com/livekit/protocol/utils"
"github.com/livekit/protocol/webhook"
"github.com/livekit/psrpc"
"github.com/pion/turn/v4"
"github.com/livekit/psrpc/pkg/middleware/otelpsrpc"
"github.com/pion/turn/v5"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"gopkg.in/yaml.v3"
@@ -37,7 +37,7 @@ import (
func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*LivekitServer, error) {
limitConfig := getLimitConf(conf)
apiConfig := config.DefaultAPIConfig()
apiConfig := getAPIConf(conf)
universalClient, err := createRedisClient(conf)
if err != nil {
return nil, err
@@ -60,7 +60,8 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
if err != nil {
return nil, err
}
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub)
nodeStatsConfig := getNodeStatsConfig(conf)
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub, nodeStatsConfig)
objectStore := createStore(universalClient)
roomAllocator, err := NewRoomAllocator(conf, router, objectStore)
if err != nil {
@@ -82,12 +83,12 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
return nil, err
}
analyticsService := telemetry.NewAnalyticsService(conf, currentNode)
telemetryService := telemetry.NewTelemetryService(queuedNotifier, analyticsService)
telemetryService := createTelemetryService(queuedNotifier, analyticsService)
ioInfoService, err := NewIOInfoService(messageBus, egressStore, ingressStore, sipStore, telemetryService)
if err != nil {
return nil, err
}
rtcEgressLauncher := NewEgressLauncher(egressClient, ioInfoService)
rtcEgressLauncher := NewEgressLauncher(egressClient, ioInfoService, objectStore)
topicFormatter := rpc.NewTopicFormatter()
roomClient, err := rpc.NewTypedRoomClient(clientParams)
if err != nil {
@@ -106,7 +107,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
return nil, err
}
agentDispatchService := NewAgentDispatchService(agentDispatchInternalClient, topicFormatter, roomAllocator, router)
egressService := NewEgressService(egressClient, rtcEgressLauncher, objectStore, ioInfoService, roomService)
egressService := NewEgressService(egressClient, rtcEgressLauncher, ioInfoService, roomService)
ingressConfig := getIngressConfig(conf)
ingressClient, err := rpc.NewIngressClient(clientParams)
if err != nil {
@@ -114,18 +115,26 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
}
ingressService := NewIngressService(ingressConfig, nodeID, messageBus, ingressClient, ingressStore, ioInfoService, telemetryService)
sipConfig := getSIPConfig(conf)
sipClient, err := rpc.NewSIPClient(messageBus)
sipClient, err := newSIPClient(clientParams)
if err != nil {
return nil, err
}
sipService := NewSIPService(sipConfig, nodeID, messageBus, sipClient, sipStore, roomService, telemetryService)
rtcService := NewRTCService(conf, roomAllocator, objectStore, router, currentNode, telemetryService)
rtcService := NewRTCService(conf, roomAllocator, router, telemetryService)
whipParticipantClient, err := rpc.NewTypedWHIPParticipantClient(clientParams)
if err != nil {
return nil, err
}
serviceWHIPService, err := NewWHIPService(conf, router, roomAllocator, clientParams, topicFormatter, whipParticipantClient)
if err != nil {
return nil, err
}
agentService, err := NewAgentService(conf, currentNode, messageBus, keyProvider)
if err != nil {
return nil, err
}
clientConfigurationManager := createClientConfiguration()
client, err := agent.NewAgentClient(messageBus)
agentConfig := getAgentConfig(conf)
client, err := agent.NewAgentClient(messageBus, agentConfig)
if err != nil {
return nil, err
}
@@ -133,7 +142,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
timedVersionGenerator := utils.NewDefaultTimedVersionGenerator()
turnAuthHandler := NewTURNAuthHandler(keyProvider)
forwardStats := createForwardStats(conf)
roomManager, err := NewLocalRoomManager(conf, objectStore, currentNode, router, roomAllocator, telemetryService, clientConfigurationManager, client, agentStore, rtcEgressLauncher, timedVersionGenerator, turnAuthHandler, messageBus, forwardStats)
roomManager, err := NewLocalRoomManager(conf, objectStore, currentNode, router, roomAllocator, telemetryService, client, agentStore, rtcEgressLauncher, timedVersionGenerator, turnAuthHandler, messageBus, forwardStats)
if err != nil {
return nil, err
}
@@ -146,7 +155,7 @@ func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*Live
if err != nil {
return nil, err
}
livekitServer, err := NewLivekitServer(conf, roomService, agentDispatchService, egressService, ingressService, sipService, ioInfoService, rtcService, agentService, keyProvider, router, roomManager, signalServer, server, currentNode)
livekitServer, err := NewLivekitServer(conf, roomService, agentDispatchService, egressService, ingressService, sipService, ioInfoService, rtcService, serviceWHIPService, agentService, keyProvider, router, roomManager, signalServer, server, currentNode)
if err != nil {
return nil, err
}
@@ -176,7 +185,8 @@ func InitializeRouter(conf *config.Config, currentNode routing.LocalNode) (routi
if err != nil {
return nil, err
}
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub)
nodeStatsConfig := getNodeStatsConfig(conf)
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub, nodeStatsConfig)
return router, nil
}
@@ -217,15 +227,21 @@ func createKeyProvider(conf *config.Config) (auth.KeyProvider, error) {
func createWebhookNotifier(conf *config.Config, provider auth.KeyProvider) (webhook.QueuedNotifier, error) {
wc := conf.WebHook
if len(wc.URLs) == 0 {
return nil, nil
}
secret := provider.GetSecret(wc.APIKey)
if secret == "" {
if secret == "" && len(wc.URLs) > 0 {
return nil, ErrWebHookMissingAPIKey
}
return webhook.NewDefaultNotifier(wc.APIKey, secret, wc.URLs), nil
return webhook.NewDefaultNotifier(wc, provider)
}
func createTelemetryService(notifier webhook.QueuedNotifier, analytics telemetry.AnalyticsService) telemetry.TelemetryService {
svc := telemetry.NewTelemetryService(notifier, analytics)
if notifier != nil {
notifier.RegisterProcessedHook(svc.Webhook)
}
return svc
}
func createRedisClient(conf *config.Config) (redis.UniversalClient, error) {
@@ -282,6 +298,14 @@ func getIngressConfig(conf *config.Config) *config.IngressConfig {
return &conf.Ingress
}
func newSIPClient(p rpc.ClientParams) (rpc.SIPClient, error) {
return rpc.NewSIPClientWithParams(rpc.ClientParams{
Bus: p.Bus,
ClientOptions: []psrpc.ClientOption{rpc.WithClientLogger(p.Logger), otelpsrpc.ClientOptions(otelpsrpc.Config{})},
})
}
func getSIPStore(s ObjectStore) SIPStore {
switch store := s.(type) {
case *RedisStore:
@@ -295,10 +319,6 @@ func getSIPConfig(conf *config.Config) *config.SIPConfig {
return &conf.SIP
}
func createClientConfiguration() clientconfiguration.ClientConfigurationManager {
return clientconfiguration.NewStaticClientConfigurationManager(clientconfiguration.StaticConfigurations)
}
func getLimitConf(config2 *config.Config) config.LimitConfig {
return config2.Limit
}
@@ -316,7 +336,7 @@ func getPSRPCConfig(config2 *config.Config) rpc.PSRPCConfig {
}
func getPSRPCClientParams(config2 rpc.PSRPCConfig, bus psrpc.MessageBus) rpc.ClientParams {
return rpc.NewClientParams(config2, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{})
return rpc.NewClientParams(config2, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{}, otelpsrpc.ClientOptions(otelpsrpc.Config{}))
}
func createForwardStats(conf *config.Config) *sfu.ForwardStats {
@@ -329,3 +349,15 @@ func createForwardStats(conf *config.Config) *sfu.ForwardStats {
func newInProcessTurnServer(conf *config.Config, authHandler turn.AuthHandler) (*turn.Server, error) {
return NewTurnServer(conf, authHandler, false)
}
func getNodeStatsConfig(config2 *config.Config) config.NodeStatsConfig {
return config2.NodeStats
}
func getAgentConfig(config2 *config.Config) agent.Config {
return config2.Agents
}
func getAPIConf(config2 *config.Config) config.APIConfig {
return config2.API
}
+58 -55
View File
@@ -22,18 +22,19 @@ import (
"time"
"github.com/gorilla/websocket"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/protojson"
"github.com/livekit/livekit-server/pkg/rtc/types"
)
const (
pingFrequency = 10 * time.Second
pingTimeout = 2 * time.Second
pingFrequency = 10 * time.Second
pingTimeout = 2 * time.Second
closeWriteTimeout = 5 * time.Second
)
type WSSignalConnection struct {
@@ -56,75 +57,77 @@ func (c *WSSignalConnection) Close() error {
return c.conn.Close()
}
func (c *WSSignalConnection) CloseWithReason(reason string) error {
msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, reason)
_ = c.conn.WriteControl(websocket.CloseMessage, msg, time.Now().Add(closeWriteTimeout))
return c.conn.Close()
}
func (c *WSSignalConnection) SetReadDeadline(deadline time.Time) error {
return c.conn.SetReadDeadline(deadline)
}
func (c *WSSignalConnection) ReadRequest() (*livekit.SignalRequest, int, error) {
for {
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
msg := &livekit.SignalRequest{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
msg := &livekit.SignalRequest{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
}
func (c *WSSignalConnection) ReadWorkerMessage() (*livekit.WorkerMessage, int, error) {
for {
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
// handle special messages and pass on the rest
messageType, payload, err := c.conn.ReadMessage()
if err != nil {
return nil, 0, err
}
msg := &livekit.WorkerMessage{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
msg := &livekit.WorkerMessage{}
switch messageType {
case websocket.BinaryMessage:
if c.useJSON {
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
// switch to protobuf if client supports it
c.useJSON = false
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
// protobuf encoded
err := proto.Unmarshal(payload, msg)
return msg, len(payload), err
case websocket.TextMessage:
c.mu.Lock()
// json encoded, also write back JSON
c.useJSON = true
c.mu.Unlock()
err := protojson.Unmarshal(payload, msg)
return msg, len(payload), err
default:
logger.Debugw("unsupported message", "message", messageType)
return nil, len(payload), nil
}
}