Files
livekit/pkg/service/roommanager.go
T
Benjamin PrachtandGitHub a877ba2352 Partial support for agent dispatch management (#2872)
- Store agent dispaches independently of room agents on rtc.Room
- Serialize agent dispatches in rtc.Room
- Support for agent dispatch and job serialization in redis

The agent Job object references denormalized Room and ParticipantInfo object. When storing Jobs, this sets the Room to nil, and only stores the Participant identity field. When read back, these fields need to be set to their current value.
2024-07-18 13:36:43 -07:00

929 lines
29 KiB
Go

// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"context"
"fmt"
"os"
"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/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/protocol/utils/must"
"github.com/livekit/psrpc"
"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/rtc"
"github.com/livekit/livekit-server/pkg/rtc/types"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
"github.com/livekit/livekit-server/version"
)
const (
roomPurgeSeconds = 24 * 60 * 60
tokenRefreshInterval = 5 * time.Minute
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
}
// RoomManager manages rooms and its interaction with participants.
// It's responsible for creating, deleting rooms, as well as running sessions for participants
type RoomManager struct {
lock sync.RWMutex
config *config.Config
rtcConfig *rtc.WebRTCConfig
serverInfo *livekit.ServerInfo
currentNode routing.LocalNode
router routing.Router
roomStore ObjectStore
telemetry telemetry.TelemetryService
clientConfManager clientconfiguration.ClientConfigurationManager
agentClient agent.Client
agentStore AgentStore
egressLauncher rtc.EgressLauncher
versionGenerator utils.TimedVersionGenerator
turnAuthHandler *TURNAuthHandler
bus psrpc.MessageBus
rooms map[livekit.RoomName]*rtc.Room
roomServers utils.MultitonService[rpc.RoomTopic]
participantServers utils.MultitonService[rpc.ParticipantTopic]
iceConfigCache *sutils.IceConfigCache[iceConfigCacheKey]
forwardStats *sfu.ForwardStats
}
func NewLocalRoomManager(
conf *config.Config,
roomStore ObjectStore,
currentNode routing.LocalNode,
router routing.Router,
telemetry telemetry.TelemetryService,
clientConfManager clientconfiguration.ClientConfigurationManager,
agentClient agent.Client,
agentStore AgentStore,
egressLauncher rtc.EgressLauncher,
versionGenerator utils.TimedVersionGenerator,
turnAuthHandler *TURNAuthHandler,
bus psrpc.MessageBus,
forwardStats *sfu.ForwardStats,
) (*RoomManager, error) {
rtcConf, err := rtc.NewWebRTCConfig(conf)
if err != nil {
return nil, err
}
return &RoomManager{
config: conf,
rtcConfig: rtcConf,
currentNode: currentNode,
router: router,
roomStore: roomStore,
telemetry: telemetry,
clientConfManager: clientConfManager,
egressLauncher: egressLauncher,
agentClient: agentClient,
agentStore: agentStore,
versionGenerator: versionGenerator,
turnAuthHandler: turnAuthHandler,
bus: bus,
forwardStats: forwardStats,
rooms: make(map[livekit.RoomName]*rtc.Room),
iceConfigCache: sutils.NewIceConfigCache[iceConfigCacheKey](0),
serverInfo: &livekit.ServerInfo{
Edition: livekit.ServerInfo_Standard,
Version: version.Version,
Protocol: types.CurrentProtocol,
AgentProtocol: agent.CurrentProtocol,
Region: conf.Region,
NodeId: currentNode.Id,
},
}, nil
}
func (r *RoomManager) GetRoom(_ context.Context, roomName livekit.RoomName) *rtc.Room {
r.lock.RLock()
defer r.lock.RUnlock()
return r.rooms[roomName]
}
// deleteRoom completely deletes all room information, including active sessions, room store, and routing info
func (r *RoomManager) deleteRoom(ctx context.Context, roomName livekit.RoomName) error {
logger.Infow("deleting room state", "room", roomName)
r.lock.Lock()
delete(r.rooms, roomName)
r.lock.Unlock()
var err, err2 error
wg := sync.WaitGroup{}
wg.Add(2)
// clear routing information
go func() {
defer wg.Done()
err = r.router.ClearRoomState(ctx, roomName)
}()
// also delete room from db
go func() {
defer wg.Done()
err2 = r.roomStore.DeleteRoom(ctx, roomName)
}()
wg.Wait()
if err2 != nil {
err = err2
}
return err
}
// CleanupRooms cleans up after old rooms that have been around for a while
func (r *RoomManager) CleanupRooms() error {
// cleanup rooms that have been left for over a day
ctx := context.Background()
rooms, err := r.roomStore.ListRooms(ctx, nil)
if err != nil {
return err
}
now := time.Now().Unix()
for _, room := range rooms {
if (now - room.CreationTime) > roomPurgeSeconds {
if err := r.deleteRoom(ctx, livekit.RoomName(room.Name)); err != nil {
return err
}
}
}
return nil
}
func (r *RoomManager) CloseIdleRooms() {
r.lock.RLock()
rooms := maps.Values(r.rooms)
r.lock.RUnlock()
for _, room := range rooms {
room.CloseIfEmpty()
}
}
func (r *RoomManager) HasParticipants() bool {
r.lock.RLock()
defer r.lock.RUnlock()
for _, room := range r.rooms {
if len(room.GetParticipants()) != 0 {
return true
}
}
return false
}
func (r *RoomManager) Stop() {
// disconnect all clients
r.lock.RLock()
rooms := maps.Values(r.rooms)
r.lock.RUnlock()
for _, room := range rooms {
room.Close(types.ParticipantCloseReasonRoomManagerStop)
}
r.roomServers.Kill()
r.participantServers.Kill()
if r.rtcConfig != nil {
if r.rtcConfig.UDPMux != nil {
_ = r.rtcConfig.UDPMux.Close()
}
if r.rtcConfig.TCPMuxListener != nil {
_ = r.rtcConfig.TCPMuxListener.Close()
}
}
r.iceConfigCache.Stop()
if r.forwardStats != nil {
r.forwardStats.Stop()
}
}
// StartSession starts WebRTC session when a new participant is connected, takes place on RTC node
func (r *RoomManager) StartSession(
ctx context.Context,
roomName livekit.RoomName,
pi routing.ParticipantInit,
requestSource routing.MessageSource,
responseSink routing.MessageSink,
) error {
sessionStartTime := time.Now()
room, err := r.getOrCreateRoom(ctx, roomName)
if err != nil {
return err
}
defer room.Release()
protoRoom, roomInternal := room.ToProto(), room.Internal()
// only create the room, but don't start a participant session
if pi.Identity == "" {
return nil
}
// should not error out, error is logged in iceServersForParticipant even if it fails
// since this is used for TURN server credentials, we don't want to fail the request even if there's no TURN for the session
apiKey, _, _ := r.getFirstKeyPair()
participant := room.GetParticipant(pi.Identity)
if participant != nil {
// When reconnecting, it means WS has interrupted but underlying peer connection is still ok in this state,
// we'll keep the participant SID, and just swap the sink for the underlying connection
if pi.Reconnect {
if participant.IsClosed() {
// Send leave request if participant is closed, i. e. handle the case of client trying to resume crossing wires with
// server closing the participant due to some irrecoverable condition. Such a condition would have triggered
// a full reconnect when that condition occurred.
//
// It is possible that the client did not get that send request. So, send it again.
logger.Infow("cannot restart a closed participant",
"room", roomName,
"nodeID", r.currentNode.Id,
"participant", pi.Identity,
"reason", pi.ReconnectReason,
)
var leave *livekit.LeaveRequest
pv := types.ProtocolVersion(pi.Client.Protocol)
if pv.SupportsRegionsInLeaveRequest() {
leave = &livekit.LeaveRequest{
Reason: livekit.DisconnectReason_STATE_MISMATCH,
Action: livekit.LeaveRequest_RECONNECT,
}
} else {
leave = &livekit.LeaveRequest{
CanReconnect: true,
Reason: livekit.DisconnectReason_STATE_MISMATCH,
}
}
_ = responseSink.WriteMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
})
return errors.New("could not restart closed participant")
}
participant.GetLogger().Infow("resuming RTC session",
"nodeID", r.currentNode.Id,
"reason", pi.ReconnectReason,
"numParticipants", room.GetParticipantCount(),
)
iceConfig := r.getIceConfig(roomName, participant)
if err = room.ResumeParticipant(
participant,
requestSource,
responseSink,
iceConfig,
r.iceServersForParticipant(
apiKey,
participant,
iceConfig.PreferenceSubscriber == livekit.ICECandidateType_ICT_TLS,
),
pi.ReconnectReason,
); err != nil {
logger.Warnw("could not resume participant", err, "participant", pi.Identity)
return err
}
r.telemetry.ParticipantResumed(ctx, room.ToProto(), participant.ToProto(), livekit.NodeID(r.currentNode.Id), pi.ReconnectReason)
go r.rtcSessionWorker(room, participant, requestSource)
return nil
}
// we need to clean up the existing participant, so a new one can join
participant.GetLogger().Infow("removing duplicate participant")
room.RemoveParticipant(participant.Identity(), participant.ID(), types.ParticipantCloseReasonDuplicateIdentity)
} else if pi.Reconnect {
// send leave request if participant is trying to reconnect without keep subscribe state
// but missing from the room
var leave *livekit.LeaveRequest
pv := types.ProtocolVersion(pi.Client.Protocol)
if pv.SupportsRegionsInLeaveRequest() {
leave = &livekit.LeaveRequest{
Reason: livekit.DisconnectReason_STATE_MISMATCH,
Action: livekit.LeaveRequest_RECONNECT,
}
} else {
leave = &livekit.LeaveRequest{
CanReconnect: true,
Reason: livekit.DisconnectReason_STATE_MISMATCH,
}
}
_ = responseSink.WriteMessage(&livekit.SignalResponse{
Message: &livekit.SignalResponse_Leave{
Leave: leave,
},
})
return errors.New("could not restart participant")
}
logger.Debugw("starting RTC session",
"room", roomName,
"nodeID", r.currentNode.Id,
"participant", pi.Identity,
"clientInfo", logger.Proto(pi.Client),
"reconnect", pi.Reconnect,
"reconnectReason", pi.ReconnectReason,
"adaptiveStream", pi.AdaptiveStream,
"numParticipants", room.GetParticipantCount(),
)
clientConf := r.clientConfManager.GetConfiguration(pi.Client)
pv := types.ProtocolVersion(pi.Client.Protocol)
rtcConf := *r.rtcConfig
rtcConf.SetBufferFactory(room.GetBufferFactory())
if pi.DisableICELite {
rtcConf.SettingEngine.SetLite(false)
}
sid := livekit.ParticipantID(guid.New(utils.ParticipantPrefix))
pLogger := rtc.LoggerWithParticipant(
rtc.LoggerWithRoom(logger.GetLogger(), room.Name(), room.ID()),
pi.Identity,
sid,
false)
// 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
}
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,
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
},
ReconnectOnPublicationError: reconnectOnPublicationError,
ReconnectOnSubscriptionError: reconnectOnSubscriptionError,
ReconnectOnDataChannelError: reconnectOnDataChannelError,
DataChannelMaxBufferedAmount: r.config.RTC.DataChannelMaxBufferedAmount,
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,
})
if err != nil {
return err
}
iceConfig := r.setIceConfig(roomName, participant)
// join room
opts := rtc.ParticipantOptions{
AutoSubscribe: pi.AutoSubscribe,
}
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)
_ = participant.Close(true, types.ParticipantCloseReasonJoinFailed, false)
return err
}
participantTopic := rpc.FormatParticipantTopic(roomName, participant.Identity())
participantServer := must.Get(rpc.NewTypedParticipantServer(r, r.bus))
killParticipantServer := r.participantServers.Replace(participantTopic, participantServer)
if err := participantServer.RegisterAllParticipantTopics(participantTopic); err != nil {
killParticipantServer()
pLogger.Errorw("could not join register participant topic", err)
_ = participant.Close(true, types.ParticipantCloseReasonMessageBusFailed, false)
return err
}
if err = r.roomStore.StoreParticipant(ctx, roomName, participant.ToProto()); err != nil {
pLogger.Errorw("could not store participant", err)
}
persistRoomForParticipantCount := func(proto *livekit.Room) {
if !participant.Hidden() && !room.IsClosed() {
err = r.roomStore.StoreRoom(ctx, proto, room.Internal())
if err != nil {
logger.Errorw("could not store room", err)
}
}
}
// update room store with new numParticipants
persistRoomForParticipantCount(room.ToProto())
clientMeta := &livekit.AnalyticsClientMeta{Region: r.currentNode.Region, Node: r.currentNode.Id}
r.telemetry.ParticipantJoined(ctx, protoRoom, participant.ToProto(), pi.Client, clientMeta, true)
participant.OnClose(func(p types.LocalParticipant) {
killParticipantServer()
if err := r.roomStore.DeleteParticipant(ctx, roomName, p.Identity()); err != nil {
pLogger.Errorw("could not delete participant", err)
}
// update room store with new numParticipants
proto := room.ToProto()
persistRoomForParticipantCount(proto)
r.telemetry.ParticipantLeft(ctx, proto, p.ToProto(), true)
})
participant.OnClaimsChanged(func(participant types.LocalParticipant) {
pLogger.Debugw("refreshing client token after claims change")
if err := r.refreshToken(participant); err != nil {
logger.Errorw("could not refresh token", err)
}
})
participant.OnICEConfigChanged(func(participant types.LocalParticipant, iceConfig *livekit.ICEConfig) {
r.iceConfigCache.Put(iceConfigCacheKey{roomName, participant.Identity()}, iceConfig)
})
go r.rtcSessionWorker(room, participant, requestSource)
return nil
}
// create the actual room object, to be used on RTC node
func (r *RoomManager) getOrCreateRoom(ctx context.Context, roomName livekit.RoomName) (*rtc.Room, error) {
r.lock.RLock()
lastSeenRoom := r.rooms[roomName]
r.lock.RUnlock()
if lastSeenRoom != nil && lastSeenRoom.Hold() {
return lastSeenRoom, nil
}
// create new room, get details first
ri, internal, err := r.roomStore.LoadRoom(ctx, roomName, true)
if err != nil {
return nil, err
}
r.lock.Lock()
currentRoom := r.rooms[roomName]
for currentRoom != lastSeenRoom {
r.lock.Unlock()
if currentRoom != nil && currentRoom.Hold() {
return currentRoom, nil
}
lastSeenRoom = currentRoom
r.lock.Lock()
currentRoom = r.rooms[roomName]
}
// construct ice servers
newRoom := rtc.NewRoom(ri, internal, *r.rtcConfig, r.config.Room, &r.config.Audio, r.serverInfo, r.telemetry, r.agentClient, r.agentStore, r.egressLauncher)
roomTopic := rpc.FormatRoomTopic(roomName)
roomServer := must.Get(rpc.NewTypedRoomServer(r, r.bus))
killRoomServer := r.roomServers.Replace(roomTopic, roomServer)
if err := roomServer.RegisterAllRoomTopics(roomTopic); err != nil {
killRoomServer()
r.lock.Unlock()
return nil, err
}
newRoom.OnClose(func() {
killRoomServer()
roomInfo := newRoom.ToProto()
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.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.OnParticipantChanged(func(p types.LocalParticipant) {
if !p.IsDisconnected() {
if err := r.roomStore.StoreParticipant(ctx, roomName, p.ToProto()); err != nil {
newRoom.Logger.Errorw("could not handle participant change", err)
}
}
})
r.rooms[roomName] = newRoom
r.lock.Unlock()
newRoom.Hold()
r.telemetry.RoomStarted(ctx, newRoom.ToProto())
prometheus.RoomStarted()
return newRoom, nil
}
// 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,
)
defer func() {
pLogger.Debugw("RTC session finishing", "connID", requestSource.ConnectionID())
requestSource.Close()
}()
defer func() {
if r := rtc.Recover(pLogger); r != nil {
os.Exit(1)
}
}()
// send first refresh for cases when client token is close to expiring
_ = r.refreshToken(participant)
tokenTicker := time.NewTicker(tokenRefreshInterval)
defer tokenTicker.Stop()
for {
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():
// In single node mode, the request source is directly tied to the signal message channel
// this means ICE restart isn't possible in single node mode
if obj == nil {
if room.GetParticipantRequestSource(participant.Identity()) == requestSource {
participant.HandleSignalSourceClose()
}
return
}
req := obj.(*livekit.SignalRequest)
if err := rtc.HandleParticipantSignal(room, participant, req, pLogger); err != nil {
// more specific errors are already logged
// treat errors returned as fatal
return
}
}
}
}
type participantReq interface {
GetRoom() string
GetIdentity() string
}
func (r *RoomManager) roomAndParticipantForReq(ctx context.Context, req participantReq) (*rtc.Room, types.LocalParticipant, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.GetRoom()))
if room == nil {
return nil, nil, ErrRoomNotFound
}
participant := room.GetParticipant(livekit.ParticipantIdentity(req.GetIdentity()))
if participant == nil {
return nil, nil, ErrParticipantNotFound
}
return room, participant, nil
}
func (r *RoomManager) RemoveParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.RemoveParticipantResponse, error) {
room, participant, err := r.roomAndParticipantForReq(ctx, req)
if err != nil {
return nil, err
}
participant.GetLogger().Infow("removing participant")
room.RemoveParticipant(livekit.ParticipantIdentity(req.Identity), "", types.ParticipantCloseReasonServiceRequestRemoveParticipant)
return &livekit.RemoveParticipantResponse{}, nil
}
func (r *RoomManager) MutePublishedTrack(ctx context.Context, req *livekit.MuteRoomTrackRequest) (*livekit.MuteRoomTrackResponse, error) {
_, participant, err := r.roomAndParticipantForReq(ctx, req)
if err != nil {
return nil, err
}
participant.GetLogger().Debugw("setting track muted",
"trackID", req.TrackSid, "muted", req.Muted)
if !req.Muted && !r.config.Room.EnableRemoteUnmute {
participant.GetLogger().Errorw("cannot unmute track, remote unmute is disabled", nil)
return nil, ErrRemoteUnmuteNoteEnabled
}
track := participant.SetTrackMuted(livekit.TrackID(req.TrackSid), req.Muted, true)
return &livekit.MuteRoomTrackResponse{Track: track}, nil
}
func (r *RoomManager) UpdateParticipant(ctx context.Context, req *livekit.UpdateParticipantRequest) (*livekit.ParticipantInfo, error) {
_, participant, err := r.roomAndParticipantForReq(ctx, req)
if err != nil {
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 {
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.SetPermission(req.Permission)
}
return participant.ToProto(), nil
}
func (r *RoomManager) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomRequest) (*livekit.DeleteRoomResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
// special case of a non-RTC room e.g. room created but no participants joined
logger.Debugw("Deleting non-rtc room, loading from roomstore")
err := r.roomStore.DeleteRoom(ctx, livekit.RoomName(req.Room))
if err != nil {
logger.Debugw("Error deleting non-rtc room", "err", err)
return nil, err
}
} else {
room.Logger.Infow("deleting room")
room.Close(types.ParticipantCloseReasonServiceRequestDeleteRoom)
}
return &livekit.DeleteRoomResponse{}, nil
}
func (r *RoomManager) UpdateSubscriptions(ctx context.Context, req *livekit.UpdateSubscriptionsRequest) (*livekit.UpdateSubscriptionsResponse, error) {
room, participant, err := r.roomAndParticipantForReq(ctx, req)
if err != nil {
return nil, err
}
participant.GetLogger().Debugw("updating participant subscriptions")
room.UpdateSubscriptions(
participant,
livekit.StringsAsIDs[livekit.TrackID](req.TrackSids),
req.ParticipantTracks,
req.Subscribe,
)
return &livekit.UpdateSubscriptionsResponse{}, nil
}
func (r *RoomManager) SendData(ctx context.Context, req *livekit.SendDataRequest) (*livekit.SendDataResponse, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
room.Logger.Debugw("api send data", "size", len(req.Data))
room.SendDataPacket(&livekit.DataPacket{
Kind: req.Kind,
DestinationIdentities: req.DestinationIdentities,
Value: &livekit.DataPacket_User{
User: &livekit.UserPacket{
Payload: req.Data,
DestinationSids: req.DestinationSids,
DestinationIdentities: req.DestinationIdentities,
Topic: req.Topic,
},
},
}, req.Kind)
return &livekit.SendDataResponse{}, nil
}
func (r *RoomManager) UpdateRoomMetadata(ctx context.Context, req *livekit.UpdateRoomMetadataRequest) (*livekit.Room, error) {
room := r.GetRoom(ctx, livekit.RoomName(req.Room))
if room == nil {
return nil, ErrRoomNotFound
}
room.Logger.Debugw("updating room")
done := room.SetMetadata(req.Metadata)
// wait till the update is applied
<-done
return room.ToProto(), nil
}
func (r *RoomManager) iceServersForParticipant(apiKey string, participant types.LocalParticipant, tlsOnly bool) []*livekit.ICEServer {
var iceServers []*livekit.ICEServer
rtcConf := r.config.RTC
if tlsOnly && r.config.TURN.TLSPort == 0 {
logger.Warnw("tls only enabled but no turn tls config", nil)
tlsOnly = false
}
hasSTUN := false
if r.config.TURN.Enabled {
var urls []string
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))
}
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())
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,
Credential: password,
})
}
}
}
if len(rtcConf.TURNServers) > 0 {
hasSTUN = true
for _, s := range r.config.RTC.TURNServers {
scheme := "turn"
transport := "tcp"
if s.Protocol == "tls" {
scheme = "turns"
} else if s.Protocol == "udp" {
transport = "udp"
}
is := &livekit.ICEServer{
Urls: []string{
fmt.Sprintf("%s:%s:%d?transport=%s", scheme, s.Host, s.Port, transport),
},
Username: s.Username,
Credential: s.Credential,
}
iceServers = append(iceServers, is)
}
}
if len(rtcConf.STUNServers) > 0 {
hasSTUN = true
iceServers = append(iceServers, iceServerForStunServers(r.config.RTC.STUNServers))
}
if !hasSTUN {
iceServers = append(iceServers, iceServerForStunServers(rtcconfig.DefaultStunServers))
}
return iceServers
}
func (r *RoomManager) refreshToken(participant types.LocalParticipant) error {
key, secret, err := r.getFirstKeyPair()
if err != nil {
return err
}
grants := participant.ClaimGrants()
token := auth.NewAccessToken(key, secret)
token.SetName(grants.Name).
SetIdentity(string(participant.Identity())).
SetValidFor(tokenDefaultTTL).
SetMetadata(grants.Metadata).
AddGrant(grants.Video)
jwt, err := token.ToJWT()
if err == nil {
err = participant.SendRefreshToken(jwt)
}
if err != nil {
return err
}
return nil
}
func (r *RoomManager) setIceConfig(roomName livekit.RoomName, participant types.LocalParticipant) *livekit.ICEConfig {
iceConfig := r.getIceConfig(roomName, participant)
participant.SetICEConfig(iceConfig)
return iceConfig
}
func (r *RoomManager) getIceConfig(roomName livekit.RoomName, participant types.LocalParticipant) *livekit.ICEConfig {
return r.iceConfigCache.Get(iceConfigCacheKey{roomName, participant.Identity()})
}
func (r *RoomManager) getFirstKeyPair() (string, string, error) {
for key, secret := range r.config.Keys {
return key, secret, nil
}
return "", "", errors.New("no API keys configured")
}
// ------------------------------------
func iceServerForStunServers(servers []string) *livekit.ICEServer {
iceServer := &livekit.ICEServer{}
for _, stunServer := range servers {
iceServer.Urls = append(iceServer.Urls, fmt.Sprintf("stun:%s", stunServer))
}
return iceServer
}