snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
This commit is contained in:
131
pkg/service/agent_dispatch_service.go
Normal file
131
pkg/service/agent_dispatch_service.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// 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"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"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 {
|
||||
limitConf config.LimitConfig
|
||||
agentDispatchClient rpc.TypedAgentDispatchInternalClient
|
||||
topicFormatter rpc.TopicFormatter
|
||||
roomAllocator RoomAllocator
|
||||
router routing.MessageRouter
|
||||
}
|
||||
|
||||
func NewAgentDispatchService(
|
||||
limitConf config.LimitConfig,
|
||||
agentDispatchClient rpc.TypedAgentDispatchInternalClient,
|
||||
topicFormatter rpc.TopicFormatter,
|
||||
roomAllocator RoomAllocator,
|
||||
router routing.MessageRouter,
|
||||
) *AgentDispatchService {
|
||||
return &AgentDispatchService{
|
||||
limitConf: limitConf,
|
||||
agentDispatchClient: agentDispatchClient,
|
||||
topicFormatter: topicFormatter,
|
||||
roomAllocator: roomAllocator,
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
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.limitConf.CheckMetadataSize(req.Metadata) {
|
||||
return nil, ErrMetadataExceedsLimits
|
||||
}
|
||||
if !ag.limitConf.CheckAttributesSize(req.Attributes) {
|
||||
return nil, ErrAttributeExceedsLimits
|
||||
}
|
||||
|
||||
if ag.roomAllocator.AutoCreateEnabled(ctx) {
|
||||
err := ag.roomAllocator.SelectRoomNode(ctx, livekit.RoomName(req.Room), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = ag.router.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: req.Room})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dispatch := &livekit.AgentDispatch{
|
||||
Id: DeterministicID(guid.AgentDispatchPrefix, RequestID(ctx)),
|
||||
AgentName: req.AgentName,
|
||||
Room: req.Room,
|
||||
Metadata: req.Metadata,
|
||||
RestartPolicy: req.RestartPolicy,
|
||||
Deployment: req.Deployment,
|
||||
Attributes: req.Attributes,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
return ag.agentDispatchClient.DeleteDispatch(ctx, ag.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
580
pkg/service/agentservice.go
Normal file
580
pkg/service/agentservice.go
Normal file
@@ -0,0 +1,580 @@
|
||||
// Copyright 2024 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"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"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/rtc"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/version"
|
||||
"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/psrpc"
|
||||
)
|
||||
|
||||
type AgentSocketUpgrader struct {
|
||||
websocket.Upgrader
|
||||
}
|
||||
|
||||
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
|
||||
u.CheckOrigin = func(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// reject non websocket requests
|
||||
if !websocket.IsWebSocketUpgrade(r) {
|
||||
w.WriteHeader(404)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if pv, err := strconv.Atoi(r.FormValue("protocol")); err == nil {
|
||||
registration.Protocol = agent.WorkerProtocolVersion(pv)
|
||||
}
|
||||
|
||||
return conn, registration, true
|
||||
}
|
||||
|
||||
func DispatchAgentWorkerSignal(c agent.SignalConn, h agent.WorkerSignalHandler, l logger.Logger) bool {
|
||||
req, _, err := c.ReadWorkerMessage()
|
||||
if err != nil {
|
||||
if IsWebSocketCloseError(err) {
|
||||
l.Debugw("worker closed WS connection", "wsError", err)
|
||||
} else {
|
||||
l.Errorw("error reading from websocket", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if err := agent.DispatchWorkerSignal(req, h); err != nil {
|
||||
l.Warnw("unable to handle worker signal", err, "req", logger.Proto(req))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
for !wr.Registered() {
|
||||
if ok = DispatchAgentWorkerSignal(c, wr, l); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := c.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
}
|
||||
return wr.Registration(), true
|
||||
}
|
||||
|
||||
type AgentService struct {
|
||||
upgrader AgentSocketUpgrader
|
||||
|
||||
signalMessageSizeLimit int64
|
||||
|
||||
*AgentHandler
|
||||
}
|
||||
|
||||
type AgentHandler struct {
|
||||
agentServer rpc.AgentInternalServer
|
||||
mu sync.Mutex
|
||||
logger logger.Logger
|
||||
|
||||
serverInfo *livekit.ServerInfo
|
||||
workers map[string]*agent.Worker
|
||||
jobToWorker map[livekit.JobID]*agent.Worker
|
||||
keyProvider auth.KeyProvider
|
||||
targetLoad float32
|
||||
|
||||
namespaceWorkers map[workerKey][]*agent.Worker
|
||||
roomKeyCount int
|
||||
publisherKeyCount int
|
||||
participantKeyCount int
|
||||
namespaces []string // namespaces deprecated
|
||||
agentNames []string
|
||||
|
||||
roomTopic string
|
||||
publisherTopic string
|
||||
participantTopic string
|
||||
}
|
||||
|
||||
type workerKey struct {
|
||||
agentName string
|
||||
namespace string
|
||||
jobType livekit.JobType
|
||||
deployment string
|
||||
}
|
||||
|
||||
func NewAgentService(
|
||||
conf *config.Config,
|
||||
currentNode routing.LocalNode,
|
||||
bus psrpc.MessageBus,
|
||||
keyProvider auth.KeyProvider,
|
||||
) (*AgentService, error) {
|
||||
s := &AgentService{
|
||||
signalMessageSizeLimit: conf.Limit.AgentSignalMessageSizeLimit,
|
||||
}
|
||||
|
||||
serverInfo := &livekit.ServerInfo{
|
||||
Edition: livekit.ServerInfo_Standard,
|
||||
Version: version.Version,
|
||||
Protocol: types.CurrentProtocol,
|
||||
AgentProtocol: agent.CurrentProtocol,
|
||||
Region: conf.Region,
|
||||
NodeId: string(currentNode.NodeID()),
|
||||
}
|
||||
|
||||
agentServer, err := rpc.NewAgentInternalServer(s, bus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.AgentHandler = NewAgentHandler(
|
||||
agentServer,
|
||||
keyProvider,
|
||||
logger.GetLogger(),
|
||||
serverInfo,
|
||||
conf.Agents.TargetLoad,
|
||||
agent.RoomAgentTopic,
|
||||
agent.PublisherAgentTopic,
|
||||
agent.ParticipantAgentTopic,
|
||||
)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *AgentService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if conn, registration, ok := s.upgrader.Upgrade(w, r, nil); ok {
|
||||
// bound the size of a single signalling frame so an oversized message is
|
||||
// rejected by the transport before being fully buffered in memory. This
|
||||
// limits the compressed bytes read off the wire; the decompressed size is
|
||||
// bounded separately in WSSignalConnection.
|
||||
if s.signalMessageSizeLimit > 0 {
|
||||
conn.SetReadLimit(s.signalMessageSizeLimit)
|
||||
}
|
||||
sigConn := NewWSSignalConnection(conn, s.signalMessageSizeLimit)
|
||||
defer sigConn.Close()
|
||||
|
||||
s.HandleConnection(r.Context(), sigConn, registration)
|
||||
}
|
||||
}
|
||||
|
||||
func NewAgentHandler(
|
||||
agentServer rpc.AgentInternalServer,
|
||||
keyProvider auth.KeyProvider,
|
||||
logger logger.Logger,
|
||||
serverInfo *livekit.ServerInfo,
|
||||
targetLoad float32,
|
||||
roomTopic string,
|
||||
publisherTopic string,
|
||||
participantTopic string,
|
||||
) *AgentHandler {
|
||||
return &AgentHandler{
|
||||
agentServer: agentServer,
|
||||
logger: logger.WithComponent("agents"),
|
||||
workers: make(map[string]*agent.Worker),
|
||||
jobToWorker: make(map[livekit.JobID]*agent.Worker),
|
||||
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, registration agent.WorkerRegistration) {
|
||||
registration, ok := HandshakeAgentWorker(conn, h.serverInfo, registration, h.logger)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := GetAPIKey(ctx)
|
||||
apiSecret := h.keyProvider.GetSecret(apiKey)
|
||||
|
||||
worker := agent.NewWorker(registration, apiKey, apiSecret, conn, h.logger)
|
||||
h.registerWorker(worker)
|
||||
|
||||
handlerWorker := &agentHandlerWorker{h, worker}
|
||||
for ok := true; ok; {
|
||||
ok = DispatchAgentWorkerSignal(conn, handlerWorker, worker.Logger())
|
||||
}
|
||||
|
||||
h.deregisterWorker(worker)
|
||||
worker.Close()
|
||||
}
|
||||
|
||||
func (h *AgentHandler) registerWorker(w *agent.Worker) {
|
||||
h.mu.Lock()
|
||||
|
||||
h.workers[w.ID] = w
|
||||
|
||||
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, w.Deployment)
|
||||
var typeTopic string
|
||||
switch w.JobType {
|
||||
case livekit.JobType_JT_ROOM:
|
||||
typeTopic = h.roomTopic
|
||||
case livekit.JobType_JT_PUBLISHER:
|
||||
typeTopic = h.publisherTopic
|
||||
case livekit.JobType_JT_PARTICIPANT:
|
||||
typeTopic = h.participantTopic
|
||||
}
|
||||
|
||||
err := h.agentServer.RegisterJobRequestTopic(nameTopic, typeTopic)
|
||||
if err != nil {
|
||||
h.mu.Unlock()
|
||||
|
||||
w.Logger().Errorw("failed to register job request topic", err)
|
||||
w.Close()
|
||||
return
|
||||
}
|
||||
|
||||
switch w.JobType {
|
||||
case livekit.JobType_JT_ROOM:
|
||||
h.roomKeyCount++
|
||||
case livekit.JobType_JT_PUBLISHER:
|
||||
h.publisherKeyCount++
|
||||
case livekit.JobType_JT_PARTICIPANT:
|
||||
h.participantKeyCount++
|
||||
}
|
||||
|
||||
h.namespaces = append(h.namespaces, w.Namespace)
|
||||
sort.Strings(h.namespaces)
|
||||
h.agentNames = append(h.agentNames, w.AgentName)
|
||||
sort.Strings(h.agentNames)
|
||||
}
|
||||
|
||||
h.namespaceWorkers[key] = append(workers, w)
|
||||
h.mu.Unlock()
|
||||
|
||||
h.logger.Infow("worker registered",
|
||||
"namespace", w.Namespace,
|
||||
"jobType", w.JobType,
|
||||
"agentName", w.AgentName,
|
||||
"workerID", w.ID,
|
||||
)
|
||||
if created {
|
||||
err := h.agentServer.PublishWorkerRegistered(context.Background(), agent.DefaultHandlerNamespace, &emptypb.Empty{})
|
||||
// TODO: when this happens, should we disconnect the worker so it'll retry?
|
||||
if err != nil {
|
||||
w.Logger().Errorw("failed to publish worker registered", err, "namespace", w.Namespace, "jobType", w.JobType, "agentName", w.AgentName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AgentHandler) deregisterWorker(w *agent.Worker) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
delete(h.workers, w.ID)
|
||||
|
||||
key := workerKey{w.AgentName, w.Namespace, w.JobType, w.Deployment}
|
||||
|
||||
workers, ok := h.namespaceWorkers[key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
index := slices.Index(workers, w)
|
||||
if index == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(workers) > 1 {
|
||||
h.namespaceWorkers[key] = slices.Delete(workers, index, index+1)
|
||||
} else {
|
||||
h.logger.Infow("last worker deregistered",
|
||||
"namespace", w.Namespace,
|
||||
"jobType", w.JobType,
|
||||
"agentName", w.AgentName,
|
||||
"workerID", w.ID,
|
||||
)
|
||||
delete(h.namespaceWorkers, key)
|
||||
|
||||
topic := agent.GetAgentTopic(w.AgentName, w.Namespace, w.Deployment)
|
||||
|
||||
switch w.JobType {
|
||||
case livekit.JobType_JT_ROOM:
|
||||
h.roomKeyCount--
|
||||
h.agentServer.DeregisterJobRequestTopic(topic, h.roomTopic)
|
||||
case livekit.JobType_JT_PUBLISHER:
|
||||
h.publisherKeyCount--
|
||||
h.agentServer.DeregisterJobRequestTopic(topic, h.publisherTopic)
|
||||
case livekit.JobType_JT_PARTICIPANT:
|
||||
h.participantKeyCount--
|
||||
h.agentServer.DeregisterJobRequestTopic(topic, h.participantTopic)
|
||||
}
|
||||
|
||||
// agentNames and namespaces contains repeated entries for each agentNames/namespaces combinations
|
||||
if i := slices.Index(h.namespaces, w.Namespace); i != -1 {
|
||||
h.namespaces = slices.Delete(h.namespaces, i, i+1)
|
||||
}
|
||||
if i := slices.Index(h.agentNames, w.AgentName); i != -1 {
|
||||
h.agentNames = slices.Delete(h.agentNames, i, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
jobs := w.RunningJobs()
|
||||
for jobID := range jobs {
|
||||
h.deregisterJob(jobID)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AgentHandler) deregisterJob(jobID livekit.JobID) {
|
||||
h.agentServer.DeregisterJobTerminateTopic(string(jobID))
|
||||
|
||||
delete(h.jobToWorker, jobID)
|
||||
|
||||
// TODO update dispatch state
|
||||
}
|
||||
|
||||
func (h *AgentHandler) JobRequest(ctx context.Context, job *livekit.Job) (*rpc.JobRequestResponse, error) {
|
||||
logger := h.logger.WithUnlikelyValues(
|
||||
"jobID", job.Id,
|
||||
"namespace", job.Namespace,
|
||||
"agentName", job.AgentName,
|
||||
"jobType", job.Type.String(),
|
||||
)
|
||||
if job.Room != nil {
|
||||
logger = logger.WithValues("room", job.Room.Name, "roomID", job.Room.Sid)
|
||||
}
|
||||
if job.Participant != nil {
|
||||
logger = logger.WithValues("participant", job.Participant.Identity)
|
||||
}
|
||||
|
||||
key := workerKey{job.AgentName, job.Namespace, job.Type, job.Deployment}
|
||||
attempted := make(map[*agent.Worker]struct{})
|
||||
for {
|
||||
selected, err := h.selectWorkerWeightedByLoad(key, attempted)
|
||||
if err != nil {
|
||||
logger.Warnw("no worker available to handle job", err)
|
||||
return nil, psrpc.NewError(psrpc.ResourceExhausted, err)
|
||||
}
|
||||
|
||||
logger := logger.WithValues("workerID", selected.ID)
|
||||
attempted[selected] = struct{}{}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AgentHandler) JobRequestAffinity(ctx context.Context, job *livekit.Job) float32 {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var affinity float32
|
||||
for _, w := range h.workers {
|
||||
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, h.targetLoad-w.Load())
|
||||
}
|
||||
}
|
||||
|
||||
return affinity
|
||||
}
|
||||
|
||||
func (h *AgentHandler) JobTerminate(ctx context.Context, req *rpc.JobTerminateRequest) (*rpc.JobTerminateResponse, error) {
|
||||
h.mu.Lock()
|
||||
w := h.jobToWorker[livekit.JobID(req.JobId)]
|
||||
h.mu.Unlock()
|
||||
|
||||
if w == nil {
|
||||
return nil, psrpc.NewErrorf(psrpc.NotFound, "no worker for jobID")
|
||||
}
|
||||
|
||||
state, err := w.TerminateJob(livekit.JobID(req.JobId), req.Reason)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &rpc.JobTerminateResponse{
|
||||
State: state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *AgentHandler) CheckEnabled(ctx context.Context, req *rpc.CheckEnabledRequest) (*rpc.CheckEnabledResponse, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// This doesn't return the full agentName -> namespace mapping, which can cause some unnecessary RPC.
|
||||
// namespaces are however deprecated.
|
||||
return &rpc.CheckEnabledResponse{
|
||||
Namespaces: slices.Compact(slices.Clone(h.namespaces)),
|
||||
AgentNames: slices.Compact(slices.Clone(h.agentNames)),
|
||||
RoomEnabled: h.roomKeyCount != 0,
|
||||
PublisherEnabled: h.publisherKeyCount != 0,
|
||||
ParticipantEnabled: h.participantKeyCount != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *AgentHandler) DrainConnections(interval time.Duration, force bool) {
|
||||
// Snapshot workers and release the lock before Close. Worker.Close closes the
|
||||
// signal connection, which unblocks HandleConnection's read loop so it can call
|
||||
// deregisterWorker — that needs h.mu. Holding the lock across Close deadlocks drain.
|
||||
h.mu.Lock()
|
||||
workers := make([]*agent.Worker, 0, len(h.workers))
|
||||
for _, w := range h.workers {
|
||||
workers = append(workers, w)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if !force {
|
||||
// jitter drain start
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(interval))))
|
||||
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
|
||||
for _, w := range workers {
|
||||
w.Close()
|
||||
<-t.C
|
||||
}
|
||||
} else {
|
||||
// drain as quickly as possible when forced
|
||||
for _, w := range workers {
|
||||
w.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AgentHandler) selectWorkerWeightedByLoad(key workerKey, ignore map[*agent.Worker]struct{}) (*agent.Worker, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
workers, ok := h.namespaceWorkers[key]
|
||||
if !ok {
|
||||
return nil, errors.New("no workers available")
|
||||
}
|
||||
|
||||
normalizedLoads := make(map[*agent.Worker]float32)
|
||||
var availableSum float32
|
||||
for _, w := range workers {
|
||||
if _, ok := ignore[w]; !ok && w.Status() == livekit.WorkerStatus_WS_AVAILABLE {
|
||||
normalizedLoads[w] = max(0, 1-w.Load())
|
||||
availableSum += normalizedLoads[w]
|
||||
}
|
||||
}
|
||||
|
||||
if availableSum == 0 {
|
||||
return nil, errors.New("no workers with sufficient capacity")
|
||||
}
|
||||
|
||||
currentSum := rand.Float32() * availableSum
|
||||
for w, load := range normalizedLoads {
|
||||
if currentSum -= load; currentSum <= 0 {
|
||||
return w, nil
|
||||
}
|
||||
}
|
||||
return workers[0], nil
|
||||
}
|
||||
|
||||
var _ agent.WorkerSignalHandler = (*agentHandlerWorker)(nil)
|
||||
|
||||
type agentHandlerWorker struct {
|
||||
h *AgentHandler
|
||||
*agent.Worker
|
||||
}
|
||||
|
||||
func (w *agentHandlerWorker) HandleUpdateJob(update *livekit.UpdateJobStatus) error {
|
||||
if err := w.Worker.HandleUpdateJob(update); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if agent.JobStatusIsEnded(update.Status) {
|
||||
w.h.mu.Lock()
|
||||
w.h.deregisterJob(livekit.JobID(update.JobId))
|
||||
w.h.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
261
pkg/service/auth.go
Normal file
261
pkg/service/auth.go
Normal file
@@ -0,0 +1,261 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/twitchtv/twirp"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
const (
|
||||
authorizationHeader = "Authorization"
|
||||
bearerPrefix = "Bearer "
|
||||
accessTokenParam = "access_token"
|
||||
)
|
||||
|
||||
type grantsKey struct{}
|
||||
|
||||
type grantsValue struct {
|
||||
claims *auth.ClaimGrants
|
||||
apiKey string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
ErrPermissionDenied = errors.New("permissions denied")
|
||||
ErrMissingAuthorization = errors.New("invalid authorization header. Must start with " + bearerPrefix)
|
||||
ErrInvalidAuthorizationToken = errors.New("invalid authorization token")
|
||||
ErrInvalidAPIKey = errors.New("invalid API key")
|
||||
)
|
||||
|
||||
// authentication middleware
|
||||
type APIKeyAuthMiddleware struct {
|
||||
provider auth.KeyProvider
|
||||
}
|
||||
|
||||
func NewAPIKeyAuthMiddleware(provider auth.KeyProvider) *APIKeyAuthMiddleware {
|
||||
return &APIKeyAuthMiddleware{
|
||||
provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if r.URL != nil && (r.URL.Path == "/rtc/validate" || r.URL.Path == "/rtc/v1/validate") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
authHeader := r.Header.Get(authorizationHeader)
|
||||
var authToken string
|
||||
|
||||
if authHeader != "" {
|
||||
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
||||
HandleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
|
||||
return
|
||||
}
|
||||
|
||||
authToken = authHeader[len(bearerPrefix):]
|
||||
} else {
|
||||
// attempt to find from request header
|
||||
authToken = r.FormValue(accessTokenParam)
|
||||
}
|
||||
|
||||
if authToken != "" {
|
||||
v, err := auth.ParseAPIToken(authToken)
|
||||
if err != nil {
|
||||
HandleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
|
||||
return
|
||||
}
|
||||
|
||||
secret := m.provider.GetSecret(v.APIKey())
|
||||
if secret == "" {
|
||||
HandleError(w, r, http.StatusUnauthorized, ErrInvalidAPIKey, "apiKey", v.APIKey())
|
||||
return
|
||||
}
|
||||
|
||||
claims, grants, err := v.Verify(secret)
|
||||
if err != nil {
|
||||
HandleError(w, r, http.StatusUnauthorized, fmt.Errorf("%w: %s", ErrInvalidAuthorizationToken, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var expiresAt time.Time
|
||||
if claims != nil && claims.ExpiresAt != nil {
|
||||
expiresAt = claims.ExpiresAt.Time
|
||||
}
|
||||
|
||||
// set grants in context
|
||||
ctx := r.Context()
|
||||
r = r.WithContext(context.WithValue(ctx, grantsKey{}, &grantsValue{
|
||||
claims: grants,
|
||||
apiKey: v.APIKey(),
|
||||
expiresAt: expiresAt,
|
||||
}))
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func WithAPIKey(ctx context.Context, grants *auth.ClaimGrants, apiKey string) context.Context {
|
||||
return context.WithValue(ctx, grantsKey{}, &grantsValue{
|
||||
claims: grants,
|
||||
apiKey: apiKey,
|
||||
})
|
||||
}
|
||||
|
||||
func GetGrants(ctx context.Context) *auth.ClaimGrants {
|
||||
val := ctx.Value(grantsKey{})
|
||||
v, ok := val.(*grantsValue)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return v.claims
|
||||
}
|
||||
|
||||
func GetTokenExpiresAt(ctx context.Context) time.Time {
|
||||
val := ctx.Value(grantsKey{})
|
||||
v, ok := val.(*grantsValue)
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
}
|
||||
return v.expiresAt
|
||||
}
|
||||
|
||||
func GetAPIKey(ctx context.Context) string {
|
||||
val := ctx.Value(grantsKey{})
|
||||
v, ok := val.(*grantsValue)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return v.apiKey
|
||||
}
|
||||
|
||||
func WithGrants(ctx context.Context, grants *auth.ClaimGrants, apiKey string) context.Context {
|
||||
return WithGrantsExpiry(ctx, grants, apiKey, time.Time{})
|
||||
}
|
||||
|
||||
func WithGrantsExpiry(ctx context.Context, grants *auth.ClaimGrants, apiKey string, expiresAt time.Time) context.Context {
|
||||
return context.WithValue(ctx, grantsKey{}, &grantsValue{
|
||||
claims: grants,
|
||||
apiKey: apiKey,
|
||||
expiresAt: expiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
func SetAuthorizationToken(r *http.Request, token string) {
|
||||
r.Header.Set(authorizationHeader, bearerPrefix+token)
|
||||
}
|
||||
|
||||
func EnsureJoinPermission(ctx context.Context) (name livekit.RoomName, err error) {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.Video == nil {
|
||||
err = ErrPermissionDenied
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Video.RoomJoin {
|
||||
name = livekit.RoomName(claims.Video.Room)
|
||||
} else {
|
||||
err = ErrPermissionDenied
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func EnsureAdminPermission(ctx context.Context, room livekit.RoomName) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.Video == nil {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !claims.Video.RoomAdmin || room != livekit.RoomName(claims.Video.Room) {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureCreatePermission(ctx context.Context) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.Video == nil || !claims.Video.RoomCreate {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureListPermission(ctx context.Context) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.Video == nil || !claims.Video.RoomList {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureRecordPermission(ctx context.Context) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.Video == nil || !claims.Video.RoomRecord {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureIngressAdminPermission(ctx context.Context) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.Video == nil || !claims.Video.IngressAdmin {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureSIPAdminPermission(ctx context.Context) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.SIP == nil || !claims.SIP.Admin {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureSIPCallPermission(ctx context.Context) error {
|
||||
claims := GetGrants(ctx)
|
||||
if claims == nil || claims.SIP == nil || !claims.SIP.Call {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
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())
|
||||
}
|
||||
74
pkg/service/auth_test.go
Normal file
74
pkg/service/auth_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/auth/authfakes"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
func TestAuthMiddleware(t *testing.T) {
|
||||
api := "APIabcdefg"
|
||||
secret := "somesecretencodedinbase62extendto32bytes"
|
||||
provider := &authfakes.FakeKeyProvider{}
|
||||
provider.GetSecretReturns(secret)
|
||||
|
||||
m := service.NewAPIKeyAuthMiddleware(provider)
|
||||
var grants *auth.ClaimGrants
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
grants = service.GetGrants(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
orig := &auth.VideoGrant{Room: "abcdefg", RoomJoin: true}
|
||||
// ensure that the original claim could be retrieved
|
||||
at := auth.NewAccessToken(api, secret).
|
||||
AddGrant(orig)
|
||||
token, err := at.ToJWT()
|
||||
require.NoError(t, err)
|
||||
|
||||
r := &http.Request{Header: http.Header{}}
|
||||
w := httptest.NewRecorder()
|
||||
service.SetAuthorizationToken(r, token)
|
||||
m.ServeHTTP(w, r, handler)
|
||||
|
||||
require.NotNil(t, grants)
|
||||
require.EqualValues(t, orig, grants.Video)
|
||||
|
||||
// no authorization == no claims
|
||||
grants = nil
|
||||
w = httptest.NewRecorder()
|
||||
r = &http.Request{Header: http.Header{}}
|
||||
m.ServeHTTP(w, r, handler)
|
||||
require.Nil(t, grants)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// incorrect authorization: error
|
||||
grants = nil
|
||||
w = httptest.NewRecorder()
|
||||
r = &http.Request{Header: http.Header{}}
|
||||
service.SetAuthorizationToken(r, "invalid token")
|
||||
m.ServeHTTP(w, r, handler)
|
||||
require.Nil(t, grants)
|
||||
require.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
31
pkg/service/basic_auth.go
Normal file
31
pkg/service/basic_auth.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GenBasicAuthMiddleware(username string, password string) (func(http.ResponseWriter, *http.Request, http.HandlerFunc) ) {
|
||||
return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
given_username, given_password, ok := r.BasicAuth()
|
||||
unauthorized := func() {
|
||||
rw.Header().Set("WWW-Authenticate", "Basic realm=\"Protected Area\"")
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
if !ok {
|
||||
unauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if given_username != username {
|
||||
unauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if given_password != password {
|
||||
unauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
next(rw, r)
|
||||
}
|
||||
}
|
||||
33
pkg/service/clients.go
Normal file
33
pkg/service/clients.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
)
|
||||
|
||||
//counterfeiter:generate . IOClient
|
||||
type IOClient interface {
|
||||
CreateEgress(ctx context.Context, info *livekit.EgressInfo) (*emptypb.Empty, error)
|
||||
GetEgress(ctx context.Context, req *rpc.GetEgressRequest) (*livekit.EgressInfo, error)
|
||||
ListEgress(ctx context.Context, req *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error)
|
||||
CreateIngress(ctx context.Context, req *livekit.IngressInfo) (*rpc.CreateIngressResponse, error)
|
||||
UpdateIngressState(ctx context.Context, req *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error)
|
||||
}
|
||||
109
pkg/service/docker_test.go
Normal file
109
pkg/service/docker_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright 2024 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
mobyclient "github.com/moby/moby/client"
|
||||
"github.com/ory/dockertest/v4"
|
||||
)
|
||||
|
||||
var Docker dockertest.ClosablePool
|
||||
|
||||
// go test -docker=false ./pkg/service skips the tests that need a docker
|
||||
// daemon, for a checkout without one. Running them is the default: a run that
|
||||
// quietly covers less than the last one is worse than a run that stops, so a
|
||||
// daemon that should be there and is not still fails the whole package.
|
||||
var useDocker = flag.Bool("docker", true, "run the tests that need a docker daemon")
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// m.Run would parse them, but the flag is read before that
|
||||
flag.Parse()
|
||||
|
||||
if *useDocker {
|
||||
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(ctx, mobyclient.PingOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to Docker: %s", err)
|
||||
}
|
||||
Docker = pool
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func requireDocker(t testing.TB) {
|
||||
t.Helper()
|
||||
|
||||
if !*useDocker {
|
||||
t.Skip("this test needs a docker daemon, and -docker=false says there is none")
|
||||
}
|
||||
}
|
||||
|
||||
func waitTCPPort(t testing.TB, addr string) {
|
||||
if err := Docker.Retry(t.Context(), 30*time.Second, func() error {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
return err
|
||||
}
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var redisLast atomic.Uint32
|
||||
|
||||
func runRedis(t testing.TB) string {
|
||||
requireDocker(t)
|
||||
|
||||
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() {
|
||||
// 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)
|
||||
|
||||
t.Log("Redis running on", addr)
|
||||
return addr
|
||||
}
|
||||
410
pkg/service/egress.go
Normal file
410
pkg/service/egress.go
Normal file
@@ -0,0 +1,410 @@
|
||||
// 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"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/twitchtv/twirp"
|
||||
|
||||
"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 {
|
||||
launcher rtc.EgressLauncher
|
||||
client rpc.EgressClient
|
||||
io IOClient
|
||||
roomService livekit.RoomService
|
||||
}
|
||||
|
||||
type egressLauncher struct {
|
||||
client rpc.EgressClient
|
||||
io IOClient
|
||||
store ServiceStore
|
||||
}
|
||||
|
||||
func NewEgressService(
|
||||
client rpc.EgressClient,
|
||||
launcher rtc.EgressLauncher,
|
||||
io IOClient,
|
||||
rs livekit.RoomService,
|
||||
) *EgressService {
|
||||
return &EgressService{
|
||||
client: client,
|
||||
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) StartEgress(ctx context.Context, req *livekit.StartEgressRequest) (*livekit.EgressInfo, error) {
|
||||
sourceType, outputType := egress.GetTypes(&livekit.EgressInfo_Egress{Egress: req})
|
||||
fields := []any{
|
||||
"room", req.RoomName,
|
||||
"sourceType", sourceType,
|
||||
"outputType", outputType,
|
||||
}
|
||||
defer func() {
|
||||
AppendLogFields(ctx, fields...)
|
||||
}()
|
||||
|
||||
egressID, idFromCtx := EgressID(ctx)
|
||||
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
|
||||
EgressId: egressID,
|
||||
Request: &rpc.StartEgressRequest_Egress{
|
||||
Egress: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !idFromCtx {
|
||||
fields = append(fields, "egressID", ei.EgressId)
|
||||
}
|
||||
return ei, err
|
||||
}
|
||||
|
||||
func (s *EgressService) StartRoomCompositeEgress(ctx context.Context, req *livekit.RoomCompositeEgressRequest) (*livekit.EgressInfo, error) {
|
||||
fields := []any{
|
||||
"room", req.RoomName,
|
||||
"baseUrl", req.CustomBaseUrl,
|
||||
"outputType", egress.GetOutputType(req),
|
||||
}
|
||||
defer func() {
|
||||
AppendLogFields(ctx, fields...)
|
||||
}()
|
||||
|
||||
egressID, idFromCtx := EgressID(ctx)
|
||||
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
|
||||
EgressId: egressID,
|
||||
Request: &rpc.StartEgressRequest_RoomComposite{
|
||||
RoomComposite: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 := []any{
|
||||
"url", req.Url,
|
||||
"outputType", egress.GetOutputType(req),
|
||||
}
|
||||
defer func() {
|
||||
AppendLogFields(ctx, fields...)
|
||||
}()
|
||||
egressID, idFromCtx := EgressID(ctx)
|
||||
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
|
||||
EgressId: egressID,
|
||||
Request: &rpc.StartEgressRequest_Web{
|
||||
Web: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 := []any{
|
||||
"room", req.RoomName,
|
||||
"identity", req.Identity,
|
||||
"outputType", egress.GetOutputType(req),
|
||||
}
|
||||
defer func() {
|
||||
AppendLogFields(ctx, fields...)
|
||||
}()
|
||||
egressID, idFromCtx := EgressID(ctx)
|
||||
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
|
||||
EgressId: egressID,
|
||||
Request: &rpc.StartEgressRequest_Participant{
|
||||
Participant: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 := []any{
|
||||
"room", req.RoomName,
|
||||
"audioTrackID", req.AudioTrackId,
|
||||
"videoTrackID", req.VideoTrackId,
|
||||
"outputType", egress.GetOutputType(req),
|
||||
}
|
||||
defer func() {
|
||||
AppendLogFields(ctx, fields...)
|
||||
}()
|
||||
egressID, idFromCtx := EgressID(ctx)
|
||||
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
|
||||
EgressId: egressID,
|
||||
Request: &rpc.StartEgressRequest_TrackComposite{
|
||||
TrackComposite: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 := []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...)
|
||||
}()
|
||||
egressID, idFromCtx := EgressID(ctx)
|
||||
ei, err := s.startEgress(ctx, &rpc.StartEgressRequest{
|
||||
EgressId: egressID,
|
||||
Request: &rpc.StartEgressRequest_Track{
|
||||
Track: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !idFromCtx {
|
||||
fields = append(fields, "egressID", ei.EgressId)
|
||||
}
|
||||
return ei, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
case *rpc.StartEgressRequest_Egress:
|
||||
roomName = v.Egress.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
|
||||
}
|
||||
|
||||
type LayoutMetadata struct {
|
||||
Layout string `json:"layout"`
|
||||
}
|
||||
|
||||
func (s *EgressService) UpdateLayout(ctx context.Context, req *livekit.UpdateLayoutRequest) (*livekit.EgressInfo, error) {
|
||||
AppendLogFields(ctx, "egressID", req.EgressId, "layout", req.Layout)
|
||||
if err := EnsureRecordPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
info, err := s.io.GetEgress(ctx, &rpc.GetEgressRequest{EgressId: req.EgressId})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(&LayoutMetadata{Layout: req.Layout})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grants := GetGrants(ctx)
|
||||
grants.Video.Room = info.RoomName
|
||||
grants.Video.RoomAdmin = true
|
||||
|
||||
_, err = s.roomService.UpdateParticipant(ctx, &livekit.UpdateParticipantRequest{
|
||||
Room: info.RoomName,
|
||||
Identity: info.EgressId,
|
||||
Metadata: string(metadata),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func redactedStreamUrls(urls []string) []string {
|
||||
redacted := make([]string, len(urls))
|
||||
for i, u := range urls {
|
||||
redacted[i], _ = utils.RedactStreamKey(u)
|
||||
}
|
||||
return redacted
|
||||
}
|
||||
|
||||
func (s *EgressService) UpdateStream(ctx context.Context, req *livekit.UpdateStreamRequest) (*livekit.EgressInfo, error) {
|
||||
AppendLogFields(ctx, "egressID", req.EgressId, "addUrls", redactedStreamUrls(req.AddOutputUrls), "removeUrls", redactedStreamUrls(req.RemoveOutputUrls))
|
||||
if err := EnsureRecordPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
if s.client == nil {
|
||||
return nil, ErrEgressNotConnected
|
||||
}
|
||||
|
||||
info, err := s.client.UpdateStream(ctx, req.EgressId, req)
|
||||
if err != nil {
|
||||
var loadErr error
|
||||
info, loadErr = s.io.GetEgress(ctx, &rpc.GetEgressRequest{EgressId: req.EgressId})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
switch info.Status {
|
||||
case livekit.EgressStatus_EGRESS_STARTING,
|
||||
livekit.EgressStatus_EGRESS_ACTIVE:
|
||||
return nil, err
|
||||
default:
|
||||
return nil, twirp.NewError(twirp.FailedPrecondition,
|
||||
fmt.Sprintf("egress with status %s cannot be updated", info.Status.String()))
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *EgressService) ListEgress(ctx context.Context, req *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error) {
|
||||
if req.RoomName != "" {
|
||||
AppendLogFields(ctx, "room", req.RoomName)
|
||||
}
|
||||
if err := EnsureRecordPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
return s.io.ListEgress(ctx, req)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
info, err = s.launcher.StopEgress(ctx, req)
|
||||
if err != nil {
|
||||
var loadErr error
|
||||
info, loadErr = s.io.GetEgress(ctx, &rpc.GetEgressRequest{EgressId: req.EgressId})
|
||||
if loadErr != nil {
|
||||
return nil, loadErr
|
||||
}
|
||||
|
||||
switch info.Status {
|
||||
case livekit.EgressStatus_EGRESS_STARTING,
|
||||
livekit.EgressStatus_EGRESS_ACTIVE:
|
||||
return nil, err
|
||||
default:
|
||||
return nil, twirp.NewError(twirp.FailedPrecondition,
|
||||
fmt.Sprintf("egress with status %s cannot be stopped", info.Status.String()))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
44
pkg/service/egressid.go
Normal file
44
pkg/service/egressid.go
Normal 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
|
||||
}
|
||||
52
pkg/service/errors.go
Normal file
52
pkg/service/errors.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 (
|
||||
"github.com/livekit/psrpc"
|
||||
)
|
||||
|
||||
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")
|
||||
ErrRoomLockFailed = psrpc.NewErrorf(psrpc.Internal, "could not lock room")
|
||||
ErrRoomUnlockFailed = psrpc.NewErrorf(psrpc.Internal, "could not unlock room, lock token does not match")
|
||||
ErrRemoteUnmuteNoteEnabled = psrpc.NewErrorf(psrpc.FailedPrecondition, "remote unmute not enabled")
|
||||
ErrTrackNotFound = psrpc.NewErrorf(psrpc.NotFound, "track is not found")
|
||||
ErrWebHookMissingAPIKey = psrpc.NewErrorf(psrpc.InvalidArgument, "api_key is required to use webhooks")
|
||||
ErrSIPNotConnected = psrpc.NewErrorf(psrpc.Internal, "sip not connected (redis required)")
|
||||
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")
|
||||
)
|
||||
420
pkg/service/ingress.go
Normal file
420
pkg/service/ingress.go
Normal file
@@ -0,0 +1,420 @@
|
||||
// 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"
|
||||
"net/url"
|
||||
|
||||
"github.com/livekit/protocol/ingress"
|
||||
"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/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
)
|
||||
|
||||
//counterfeiter:generate . IngressLauncher
|
||||
type IngressLauncher interface {
|
||||
LaunchPullIngress(ctx context.Context, info *livekit.IngressInfo) (*livekit.IngressInfo, error)
|
||||
}
|
||||
|
||||
type IngressService struct {
|
||||
conf *config.IngressConfig
|
||||
nodeID livekit.NodeID
|
||||
bus psrpc.MessageBus
|
||||
psrpcClient rpc.IngressClient
|
||||
store IngressStore
|
||||
io IOClient
|
||||
telemetry telemetry.TelemetryService
|
||||
launcher IngressLauncher
|
||||
}
|
||||
|
||||
func NewIngressServiceWithIngressLauncher(
|
||||
conf *config.IngressConfig,
|
||||
nodeID livekit.NodeID,
|
||||
bus psrpc.MessageBus,
|
||||
psrpcClient rpc.IngressClient,
|
||||
store IngressStore,
|
||||
io IOClient,
|
||||
ts telemetry.TelemetryService,
|
||||
launcher IngressLauncher,
|
||||
) *IngressService {
|
||||
|
||||
return &IngressService{
|
||||
conf: conf,
|
||||
nodeID: nodeID,
|
||||
bus: bus,
|
||||
psrpcClient: psrpcClient,
|
||||
store: store,
|
||||
io: io,
|
||||
telemetry: ts,
|
||||
launcher: launcher,
|
||||
}
|
||||
}
|
||||
|
||||
func NewIngressService(
|
||||
conf *config.IngressConfig,
|
||||
nodeID livekit.NodeID,
|
||||
bus psrpc.MessageBus,
|
||||
psrpcClient rpc.IngressClient,
|
||||
store IngressStore,
|
||||
io IOClient,
|
||||
ts telemetry.TelemetryService,
|
||||
) *IngressService {
|
||||
s := NewIngressServiceWithIngressLauncher(conf, nodeID, bus, psrpcClient, store, io, ts, nil)
|
||||
|
||||
s.launcher = s
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *IngressService) CreateIngress(ctx context.Context, req *livekit.CreateIngressRequest) (*livekit.IngressInfo, error) {
|
||||
fields := []any{
|
||||
"inputType", req.InputType,
|
||||
"name", req.Name,
|
||||
}
|
||||
if req.RoomName != "" {
|
||||
fields = append(fields, "room", req.RoomName, "identity", req.ParticipantIdentity)
|
||||
}
|
||||
defer func() {
|
||||
AppendLogFields(ctx, fields...)
|
||||
}()
|
||||
|
||||
var url string
|
||||
switch req.InputType {
|
||||
case livekit.IngressInput_RTMP_INPUT:
|
||||
url = s.conf.RTMPBaseURL
|
||||
case livekit.IngressInput_WHIP_INPUT:
|
||||
url = s.conf.WHIPBaseURL
|
||||
case livekit.IngressInput_URL_INPUT:
|
||||
default:
|
||||
return nil, ingress.ErrInvalidIngressType
|
||||
}
|
||||
|
||||
ig, err := s.CreateIngressWithUrl(ctx, url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields = append(fields, "ingressID", ig.IngressId)
|
||||
|
||||
return ig, nil
|
||||
}
|
||||
|
||||
func (s *IngressService) CreateIngressWithUrl(ctx context.Context, urlStr string, req *livekit.CreateIngressRequest) (*livekit.IngressInfo, error) {
|
||||
err := EnsureIngressAdminPermission(ctx)
|
||||
if err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrIngressNotConnected
|
||||
}
|
||||
|
||||
if req.InputType == livekit.IngressInput_URL_INPUT {
|
||||
if req.Url == "" {
|
||||
return nil, ingress.ErrInvalidIngress("missing URL parameter")
|
||||
}
|
||||
urlObj, err := url.Parse(req.Url)
|
||||
if err != nil {
|
||||
return nil, psrpc.NewError(psrpc.InvalidArgument, err)
|
||||
}
|
||||
switch urlObj.Scheme {
|
||||
case "http", "https", "srt":
|
||||
case "udp":
|
||||
if !s.conf.EnableUDPURLPull {
|
||||
return nil, ingress.ErrInvalidIngress("udp url pull is not enabled")
|
||||
}
|
||||
default:
|
||||
return nil, ingress.ErrInvalidIngress(fmt.Sprintf("invalid url scheme %s", urlObj.Scheme))
|
||||
}
|
||||
// Marshall the URL again for sanitization
|
||||
urlStr = urlObj.String()
|
||||
}
|
||||
|
||||
reqID := RequestID(ctx)
|
||||
var sk string
|
||||
if req.InputType != livekit.IngressInput_URL_INPUT {
|
||||
sk = guid.New("")
|
||||
}
|
||||
|
||||
info := &livekit.IngressInfo{
|
||||
IngressId: DeterministicID(utils.IngressPrefix, reqID),
|
||||
Name: req.Name,
|
||||
StreamKey: sk,
|
||||
Url: urlStr,
|
||||
InputType: req.InputType,
|
||||
Audio: req.Audio,
|
||||
Video: req.Video,
|
||||
EnableTranscoding: req.EnableTranscoding,
|
||||
RoomName: req.RoomName,
|
||||
ParticipantIdentity: req.ParticipantIdentity,
|
||||
ParticipantName: req.ParticipantName,
|
||||
ParticipantMetadata: req.ParticipantMetadata,
|
||||
State: &livekit.IngressState{},
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
|
||||
switch req.InputType {
|
||||
case livekit.IngressInput_RTMP_INPUT,
|
||||
livekit.IngressInput_WHIP_INPUT:
|
||||
info.Reusable = true
|
||||
if err := ingress.ValidateForSerialization(info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case livekit.IngressInput_URL_INPUT:
|
||||
if err := ingress.Validate(info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, ingress.ErrInvalidIngressType
|
||||
}
|
||||
|
||||
updateEnableTranscoding(info)
|
||||
|
||||
if req.InputType == livekit.IngressInput_URL_INPUT {
|
||||
retInfo, err := s.launcher.LaunchPullIngress(ctx, info)
|
||||
if retInfo != nil {
|
||||
info = retInfo
|
||||
} else {
|
||||
info.State.Status = livekit.IngressState_ENDPOINT_ERROR
|
||||
info.State.Error = err.Error()
|
||||
}
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
// The Ingress instance will create the ingress object when handling the URL pull ingress
|
||||
} else {
|
||||
var resp *rpc.CreateIngressResponse
|
||||
resp, err = s.io.CreateIngress(ctx, info)
|
||||
switch err {
|
||||
case nil:
|
||||
if resp.GetInfo() != nil {
|
||||
info = resp.GetInfo()
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (s *IngressService) LaunchPullIngress(ctx context.Context, info *livekit.IngressInfo) (*livekit.IngressInfo, error) {
|
||||
req := &rpc.StartIngressRequest{
|
||||
Info: info,
|
||||
}
|
||||
|
||||
return s.psrpcClient.StartIngress(ctx, req)
|
||||
}
|
||||
|
||||
func updateEnableTranscoding(info *livekit.IngressInfo) {
|
||||
// Set BypassTranscoding as well for backward compatibility
|
||||
if info.EnableTranscoding != nil {
|
||||
info.BypassTranscoding = !*info.EnableTranscoding
|
||||
return
|
||||
}
|
||||
|
||||
switch info.InputType {
|
||||
case livekit.IngressInput_WHIP_INPUT:
|
||||
f := false
|
||||
info.EnableTranscoding = &f
|
||||
info.BypassTranscoding = true
|
||||
default:
|
||||
t := true
|
||||
info.EnableTranscoding = &t
|
||||
}
|
||||
}
|
||||
|
||||
func updateInfoUsingRequest(req *livekit.UpdateIngressRequest, info *livekit.IngressInfo) error {
|
||||
if req.Name != "" {
|
||||
info.Name = req.Name
|
||||
}
|
||||
if req.RoomName != "" {
|
||||
info.RoomName = req.RoomName
|
||||
}
|
||||
if req.ParticipantIdentity != "" {
|
||||
info.ParticipantIdentity = req.ParticipantIdentity
|
||||
}
|
||||
if req.ParticipantName != "" {
|
||||
info.ParticipantName = req.ParticipantName
|
||||
}
|
||||
if req.EnableTranscoding != nil {
|
||||
info.EnableTranscoding = req.EnableTranscoding
|
||||
}
|
||||
|
||||
if req.ParticipantMetadata != "" {
|
||||
info.ParticipantMetadata = req.ParticipantMetadata
|
||||
}
|
||||
if req.Audio != nil {
|
||||
info.Audio = req.Audio
|
||||
}
|
||||
if req.Video != nil {
|
||||
info.Video = req.Video
|
||||
}
|
||||
|
||||
if req.Enabled != nil {
|
||||
info.Enabled = req.Enabled
|
||||
}
|
||||
|
||||
if err := ingress.ValidateForSerialization(info); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateEnableTranscoding(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IngressService) UpdateIngress(ctx context.Context, req *livekit.UpdateIngressRequest) (*livekit.IngressInfo, error) {
|
||||
fields := []any{
|
||||
"ingress", req.IngressId,
|
||||
"name", req.Name,
|
||||
}
|
||||
if req.RoomName != "" {
|
||||
fields = append(fields, "room", req.RoomName, "identity", req.ParticipantIdentity)
|
||||
}
|
||||
AppendLogFields(ctx, fields...)
|
||||
err := EnsureIngressAdminPermission(ctx)
|
||||
if err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
if s.psrpcClient == nil {
|
||||
return nil, ErrIngressNotConnected
|
||||
}
|
||||
|
||||
info, err := s.store.LoadIngress(ctx, req.IngressId)
|
||||
if err != nil {
|
||||
logger.Errorw("could not load ingress info", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !info.Reusable {
|
||||
logger.Infow("ingress update attempted on non reusable ingress", "ingressID", info.IngressId)
|
||||
return info, ErrIngressNonReusable
|
||||
}
|
||||
|
||||
switch info.State.Status {
|
||||
case livekit.IngressState_ENDPOINT_ERROR:
|
||||
info.State.Status = livekit.IngressState_ENDPOINT_INACTIVE
|
||||
_, err = s.io.UpdateIngressState(ctx, &rpc.UpdateIngressStateRequest{
|
||||
IngressId: req.IngressId,
|
||||
State: info.State,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warnw("could not store ingress state", err)
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case livekit.IngressState_ENDPOINT_INACTIVE:
|
||||
err = updateInfoUsingRequest(req, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case livekit.IngressState_ENDPOINT_BUFFERING,
|
||||
livekit.IngressState_ENDPOINT_PUBLISHING:
|
||||
err := updateInfoUsingRequest(req, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do not store the returned state as the ingress service will do it
|
||||
if _, err = s.psrpcClient.UpdateIngress(ctx, req.IngressId, req); err != nil {
|
||||
logger.Warnw("could not update active ingress", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.store.UpdateIngress(ctx, info)
|
||||
if err != nil {
|
||||
logger.Errorw("could not update ingress info", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *IngressService) ListIngress(ctx context.Context, req *livekit.ListIngressRequest) (*livekit.ListIngressResponse, error) {
|
||||
AppendLogFields(ctx, "room", req.RoomName)
|
||||
err := EnsureIngressAdminPermission(ctx)
|
||||
if err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrIngressNotConnected
|
||||
}
|
||||
|
||||
var infos []*livekit.IngressInfo
|
||||
if req.IngressId != "" {
|
||||
info, err := s.store.LoadIngress(ctx, req.IngressId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos = []*livekit.IngressInfo{info}
|
||||
} else {
|
||||
infos, err = s.store.ListIngress(ctx, livekit.RoomName(req.RoomName))
|
||||
if err != nil {
|
||||
logger.Errorw("could not list ingress info", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &livekit.ListIngressResponse{Items: infos}, nil
|
||||
}
|
||||
|
||||
func (s *IngressService) DeleteIngress(ctx context.Context, req *livekit.DeleteIngressRequest) (*livekit.IngressInfo, error) {
|
||||
AppendLogFields(ctx, "ingressID", req.IngressId)
|
||||
if err := EnsureIngressAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
if s.psrpcClient == nil {
|
||||
return nil, ErrIngressNotConnected
|
||||
}
|
||||
|
||||
info, err := s.store.LoadIngress(ctx, req.IngressId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch info.State.Status {
|
||||
case livekit.IngressState_ENDPOINT_BUFFERING,
|
||||
livekit.IngressState_ENDPOINT_PUBLISHING:
|
||||
if _, err = s.psrpcClient.DeleteIngress(ctx, req.IngressId, req); err != nil {
|
||||
logger.Warnw("could not stop active ingress", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.store.DeleteIngress(ctx, info)
|
||||
if err != nil {
|
||||
logger.Errorw("could not delete ingress info", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.State.Status = livekit.IngressState_ENDPOINT_INACTIVE
|
||||
|
||||
s.telemetry.IngressDeleted(ctx, info)
|
||||
|
||||
return info, nil
|
||||
}
|
||||
96
pkg/service/ingress_test.go
Normal file
96
pkg/service/ingress_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/livekit-server/pkg/service/servicefakes"
|
||||
)
|
||||
|
||||
func TestCreateURLPullIngressScheme(t *testing.T) {
|
||||
newService := func(enableUDP bool) (*service.IngressService, *servicefakes.FakeIngressLauncher) {
|
||||
launcher := &servicefakes.FakeIngressLauncher{}
|
||||
launcher.LaunchPullIngressCalls(func(_ context.Context, info *livekit.IngressInfo) (*livekit.IngressInfo, error) {
|
||||
return info, nil
|
||||
})
|
||||
|
||||
svc := service.NewIngressServiceWithIngressLauncher(
|
||||
&config.IngressConfig{EnableUDPURLPull: enableUDP},
|
||||
"nodeID",
|
||||
nil,
|
||||
nil,
|
||||
&servicefakes.FakeIngressStore{},
|
||||
nil,
|
||||
nil,
|
||||
launcher,
|
||||
)
|
||||
return svc, launcher
|
||||
}
|
||||
|
||||
adminCtx := func() context.Context {
|
||||
return service.WithGrants(context.Background(), &auth.ClaimGrants{Video: &auth.VideoGrant{IngressAdmin: true}}, "")
|
||||
}
|
||||
|
||||
createReq := func(url string) *livekit.CreateIngressRequest {
|
||||
return &livekit.CreateIngressRequest{
|
||||
InputType: livekit.IngressInput_URL_INPUT,
|
||||
Url: url,
|
||||
RoomName: "testroom",
|
||||
ParticipantIdentity: "ingress",
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("udp rejected when disabled", func(t *testing.T) {
|
||||
svc, launcher := newService(false)
|
||||
|
||||
_, err := svc.CreateIngress(adminCtx(), createReq("udp://1.2.3.4:1234"))
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "udp url pull is not enabled")
|
||||
require.Zero(t, launcher.LaunchPullIngressCallCount())
|
||||
})
|
||||
|
||||
t.Run("udp accepted when enabled", func(t *testing.T) {
|
||||
svc, launcher := newService(true)
|
||||
|
||||
info, err := svc.CreateIngress(adminCtx(), createReq("udp://1.2.3.4:1234"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "udp://1.2.3.4:1234", info.Url)
|
||||
require.Equal(t, 1, launcher.LaunchPullIngressCallCount())
|
||||
})
|
||||
|
||||
t.Run("other schemes unaffected by the udp option", func(t *testing.T) {
|
||||
for _, url := range []string{"http://example.com/live", "https://example.com/live", "srt://1.2.3.4:1234"} {
|
||||
svc, _ := newService(false)
|
||||
|
||||
info, err := svc.CreateIngress(adminCtx(), createReq(url))
|
||||
require.NoError(t, err, url)
|
||||
require.Equal(t, url, info.Url)
|
||||
}
|
||||
|
||||
svc, _ := newService(true)
|
||||
_, err := svc.CreateIngress(adminCtx(), createReq("rtsp://1.2.3.4/live"))
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid url scheme rtsp")
|
||||
})
|
||||
}
|
||||
115
pkg/service/interfaces.go
Normal file
115
pkg/service/interfaces.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
|
||||
|
||||
// encapsulates CRUD operations for room settings
|
||||
//
|
||||
//counterfeiter:generate . ObjectStore
|
||||
type ObjectStore interface {
|
||||
ServiceStore
|
||||
OSSServiceStore
|
||||
|
||||
// enable locking on a specific room to prevent race
|
||||
// returns a (lock uuid, error)
|
||||
LockRoom(ctx context.Context, roomName livekit.RoomName, duration time.Duration) (string, error)
|
||||
UnlockRoom(ctx context.Context, roomName livekit.RoomName, uid string) error
|
||||
|
||||
StoreRoom(ctx context.Context, room *livekit.Room, internal *livekit.RoomInternal) error
|
||||
|
||||
StoreParticipant(ctx context.Context, roomName livekit.RoomName, participant *livekit.ParticipantInfo) error
|
||||
DeleteParticipant(ctx context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate . ServiceStore
|
||||
type ServiceStore interface {
|
||||
LoadRoom(ctx context.Context, roomName livekit.RoomName, includeInternal bool) (*livekit.Room, *livekit.RoomInternal, 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)
|
||||
}
|
||||
|
||||
//counterfeiter:generate . EgressStore
|
||||
type EgressStore interface {
|
||||
StoreEgress(ctx context.Context, info *livekit.EgressInfo) error
|
||||
LoadEgress(ctx context.Context, egressID string) (*livekit.EgressInfo, error)
|
||||
ListEgress(ctx context.Context, roomName livekit.RoomName, active bool) ([]*livekit.EgressInfo, error)
|
||||
UpdateEgress(ctx context.Context, info *livekit.EgressInfo) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate . IngressStore
|
||||
type IngressStore interface {
|
||||
StoreIngress(ctx context.Context, info *livekit.IngressInfo) error
|
||||
LoadIngress(ctx context.Context, ingressID string) (*livekit.IngressInfo, error)
|
||||
LoadIngressFromStreamKey(ctx context.Context, streamKey string) (*livekit.IngressInfo, error)
|
||||
ListIngress(ctx context.Context, roomName livekit.RoomName) ([]*livekit.IngressInfo, error)
|
||||
UpdateIngress(ctx context.Context, info *livekit.IngressInfo) error
|
||||
UpdateIngressState(ctx context.Context, ingressId string, state *livekit.IngressState) error
|
||||
DeleteIngress(ctx context.Context, info *livekit.IngressInfo) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate . RoomAllocator
|
||||
type RoomAllocator interface {
|
||||
AutoCreateEnabled(ctx context.Context) bool
|
||||
SelectRoomNode(ctx context.Context, roomName livekit.RoomName, nodeID livekit.NodeID) error
|
||||
CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest, isExplicit bool) (*livekit.Room, *livekit.RoomInternal, bool, error)
|
||||
ValidateCreateRoom(ctx context.Context, roomName livekit.RoomName) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate . SIPStore
|
||||
type SIPStore interface {
|
||||
StoreSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error
|
||||
StoreSIPInboundTrunk(ctx context.Context, info *livekit.SIPInboundTrunkInfo) error
|
||||
StoreSIPOutboundTrunk(ctx context.Context, info *livekit.SIPOutboundTrunkInfo) error
|
||||
LoadSIPTrunk(ctx context.Context, sipTrunkID string) (*livekit.SIPTrunkInfo, error)
|
||||
LoadSIPInboundTrunk(ctx context.Context, sipTrunkID string) (*livekit.SIPInboundTrunkInfo, error)
|
||||
LoadSIPOutboundTrunk(ctx context.Context, sipTrunkID string) (*livekit.SIPOutboundTrunkInfo, error)
|
||||
ListSIPTrunk(ctx context.Context, opts *livekit.ListSIPTrunkRequest) (*livekit.ListSIPTrunkResponse, error)
|
||||
ListSIPInboundTrunk(ctx context.Context, opts *livekit.ListSIPInboundTrunkRequest) (*livekit.ListSIPInboundTrunkResponse, error)
|
||||
ListSIPOutboundTrunk(ctx context.Context, opts *livekit.ListSIPOutboundTrunkRequest) (*livekit.ListSIPOutboundTrunkResponse, error)
|
||||
DeleteSIPTrunk(ctx context.Context, sipTrunkID string) error
|
||||
|
||||
StoreSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error
|
||||
LoadSIPDispatchRule(ctx context.Context, sipDispatchRuleID string) (*livekit.SIPDispatchRuleInfo, error)
|
||||
ListSIPDispatchRule(ctx context.Context, opts *livekit.ListSIPDispatchRuleRequest) (*livekit.ListSIPDispatchRuleResponse, error)
|
||||
DeleteSIPDispatchRule(ctx context.Context, sipDispatchRuleID string) error
|
||||
}
|
||||
|
||||
//counterfeiter:generate . AgentStore
|
||||
type AgentStore interface {
|
||||
StoreAgentDispatch(ctx context.Context, dispatch *livekit.AgentDispatch) error
|
||||
DeleteAgentDispatch(ctx context.Context, dispatch *livekit.AgentDispatch) error
|
||||
ListAgentDispatches(ctx context.Context, roomName livekit.RoomName) ([]*livekit.AgentDispatch, error)
|
||||
|
||||
StoreAgentJob(ctx context.Context, job *livekit.Job) error
|
||||
DeleteAgentJob(ctx context.Context, job *livekit.Job) error
|
||||
}
|
||||
194
pkg/service/ioservice.go
Normal file
194
pkg/service/ioservice.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// 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"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"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"
|
||||
)
|
||||
|
||||
type IOInfoService struct {
|
||||
ioServer rpc.IOInfoServer
|
||||
|
||||
es EgressStore
|
||||
is IngressStore
|
||||
ss SIPStore
|
||||
telemetry telemetry.TelemetryService
|
||||
|
||||
shutdown chan struct{}
|
||||
}
|
||||
|
||||
func NewIOInfoService(
|
||||
bus psrpc.MessageBus,
|
||||
es EgressStore,
|
||||
is IngressStore,
|
||||
ss SIPStore,
|
||||
ts telemetry.TelemetryService,
|
||||
) (*IOInfoService, error) {
|
||||
s := &IOInfoService{
|
||||
es: es,
|
||||
is: is,
|
||||
ss: ss,
|
||||
telemetry: ts,
|
||||
shutdown: make(chan struct{}),
|
||||
}
|
||||
|
||||
if bus != nil {
|
||||
ioServer, err := rpc.NewIOInfoServer(s, bus,
|
||||
otelpsrpc.ServerOptions(otelpsrpc.Config{}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.ioServer = ioServer
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *IOInfoService) Start() error {
|
||||
if s.es != nil {
|
||||
rs := s.es.(*RedisStore)
|
||||
err := rs.Start()
|
||||
if err != nil {
|
||||
logger.Errorw("failed to start redis egress worker", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IOInfoService) Stop() {
|
||||
close(s.shutdown)
|
||||
|
||||
if s.ioServer != nil {
|
||||
s.ioServer.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
err := s.es.StoreEgress(ctx, info)
|
||||
if err != nil {
|
||||
logger.Errorw("could not update egress", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.telemetry.EgressStarted(ctx, info)
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
case livekit.EgressStatus_EGRESS_ACTIVE,
|
||||
livekit.EgressStatus_EGRESS_ENDING:
|
||||
s.telemetry.EgressUpdated(ctx, info)
|
||||
|
||||
case livekit.EgressStatus_EGRESS_COMPLETE,
|
||||
livekit.EgressStatus_EGRESS_FAILED,
|
||||
livekit.EgressStatus_EGRESS_ABORTED,
|
||||
livekit.EgressStatus_EGRESS_LIMIT_REACHED:
|
||||
s.telemetry.EgressEnded(ctx, info)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorw("could not update egress", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
logger.Errorw("failed to load egress", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &livekit.ListEgressResponse{Items: []*livekit.EgressInfo{info}}, nil
|
||||
}
|
||||
|
||||
items, err := s.es.ListEgress(ctx, livekit.RoomName(req.RoomName), req.Active)
|
||||
if err != nil {
|
||||
logger.Errorw("failed to list egress", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &livekit.ListEgressResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *IOInfoService) UpdateMetrics(ctx context.Context, req *rpc.UpdateMetricsRequest) (*emptypb.Empty, error) {
|
||||
logger.Infow("received egress metrics",
|
||||
"egressID", req.Info.EgressId,
|
||||
"avgCpu", req.AvgCpuUsage,
|
||||
"maxCpu", req.MaxCpuUsage,
|
||||
)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *IOInfoService) UpdateSIPCallState(ctx context.Context, req *rpc.UpdateSIPCallStateRequest) (*emptypb.Empty, error) {
|
||||
// TODO: placeholder
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *IOInfoService) RecordCallContext(context.Context, *rpc.RecordCallContextRequest) (*emptypb.Empty, error) {
|
||||
// TODO: placeholder
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
117
pkg/service/ioservice_ingress.go
Normal file
117
pkg/service/ioservice_ingress.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2024 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"
|
||||
"errors"
|
||||
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
)
|
||||
|
||||
func (s *IOInfoService) CreateIngress(ctx context.Context, info *livekit.IngressInfo) (*rpc.CreateIngressResponse, error) {
|
||||
if s.is == nil {
|
||||
return nil, ErrIngressNotConnected
|
||||
}
|
||||
|
||||
err := s.is.StoreIngress(ctx, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.telemetry.IngressCreated(ctx, info)
|
||||
|
||||
return &rpc.CreateIngressResponse{Info: info}, nil
|
||||
}
|
||||
|
||||
func (s *IOInfoService) GetIngressInfo(ctx context.Context, req *rpc.GetIngressInfoRequest) (*rpc.GetIngressInfoResponse, error) {
|
||||
info, err := s.loadIngressFromInfoRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &rpc.GetIngressInfoResponse{Info: info}, nil
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
info, err = s.is.LoadIngressFromStreamKey(context.Background(), req.StreamKey)
|
||||
} else {
|
||||
err = errors.New("request needs to specify either IngressId or StreamKey")
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err = s.is.UpdateIngressState(ctx, req.IngressId, req.State); err != nil {
|
||||
logger.Errorw("could not update ingress", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info.State.Status != req.State.Status {
|
||||
info.State = req.State
|
||||
|
||||
switch req.State.Status {
|
||||
case livekit.IngressState_ENDPOINT_ERROR,
|
||||
livekit.IngressState_ENDPOINT_INACTIVE,
|
||||
livekit.IngressState_ENDPOINT_COMPLETE:
|
||||
s.telemetry.IngressEnded(ctx, info)
|
||||
|
||||
if req.State.Error != "" {
|
||||
logger.Infow("ingress failed", "error", req.State.Error, "ingressID", req.IngressId)
|
||||
} else {
|
||||
logger.Infow("ingress ended", "ingressID", req.IngressId)
|
||||
}
|
||||
|
||||
case livekit.IngressState_ENDPOINT_PUBLISHING:
|
||||
s.telemetry.IngressStarted(ctx, info)
|
||||
|
||||
logger.Infow("ingress started", "ingressID", req.IngressId)
|
||||
|
||||
case livekit.IngressState_ENDPOINT_BUFFERING:
|
||||
s.telemetry.IngressUpdated(ctx, info)
|
||||
|
||||
logger.Infow("ingress buffering", "ingressID", req.IngressId)
|
||||
}
|
||||
} else {
|
||||
// Status didn't change, send Updated event
|
||||
info.State = req.State
|
||||
|
||||
s.telemetry.IngressUpdated(ctx, info)
|
||||
|
||||
logger.Infow("ingress state updated", "ingressID", req.IngressId, "status", info.State.Status)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
154
pkg/service/ioservice_sip.go
Normal file
154
pkg/service/ioservice_sip.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"github.com/dennwc/iters"
|
||||
"github.com/twitchtv/twirp"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/sip"
|
||||
)
|
||||
|
||||
// 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 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}), call)
|
||||
if err == nil {
|
||||
return tr, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
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] {
|
||||
it := livekit.ListPageIter(s.ss.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{
|
||||
Numbers: []string{called},
|
||||
})
|
||||
return iters.PagesAsIter(ctx, it)
|
||||
}
|
||||
|
||||
// matchSIPDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched.
|
||||
// Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs).
|
||||
func (s *IOInfoService) matchSIPDispatchRule(ctx context.Context, trunk *livekit.SIPInboundTrunkInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) {
|
||||
if s.ss == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
var trunkID string
|
||||
if trunk != nil {
|
||||
trunkID = trunk.SipTrunkId
|
||||
}
|
||||
// Trunk can still be nil here in case none matched or were defined.
|
||||
// This is still fine, but only in case we'll match exactly one wildcard dispatch rule.
|
||||
it := s.SelectSIPDispatchRule(ctx, trunkID)
|
||||
return sip.MatchDispatchRuleIter(trunk, it, req)
|
||||
}
|
||||
|
||||
func (s *IOInfoService) SelectSIPDispatchRule(ctx context.Context, trunkID string) iters.Iter[*livekit.SIPDispatchRuleInfo] {
|
||||
var trunkIDs []string
|
||||
if trunkID != "" {
|
||||
trunkIDs = []string{trunkID}
|
||||
}
|
||||
it := livekit.ListPageIter(s.ss.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{
|
||||
TrunkIds: trunkIDs,
|
||||
})
|
||||
return iters.PagesAsIter(ctx, it)
|
||||
}
|
||||
|
||||
func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
|
||||
call := req.SIPCall()
|
||||
log := logger.GetLogger()
|
||||
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
|
||||
}
|
||||
_, 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, call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trunkID := ""
|
||||
if trunk != nil {
|
||||
trunkID = trunk.SipTrunkId
|
||||
}
|
||||
log = log.WithValues("sipTrunk", trunkID)
|
||||
if trunk != nil {
|
||||
log.Debugw("SIP trunk matched")
|
||||
} else {
|
||||
log.Debugw("No SIP trunk matched")
|
||||
}
|
||||
best, err := s.matchSIPDispatchRule(ctx, trunk, req)
|
||||
if err != nil {
|
||||
if e := (*sip.ErrNoDispatchMatched)(nil); errors.As(err, &e) {
|
||||
return &rpc.EvaluateSIPDispatchRulesResponse{
|
||||
SipTrunkId: trunkID,
|
||||
Result: rpc.SIPDispatchResult_DROP,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
log.Debugw("SIP dispatch rule matched", "sipRule", best.SipDispatchRuleId)
|
||||
resp, err := sip.EvaluateDispatchRule("", trunk, best, req)
|
||||
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", 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
|
||||
}
|
||||
_, 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, "", call)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if trunk == nil {
|
||||
log.Debugw("No SIP trunk matched for auth", "sipTrunk", "")
|
||||
return &rpc.GetSIPTrunkAuthenticationResponse{}, nil
|
||||
}
|
||||
log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId)
|
||||
return sip.InboundTrunkAuthPrompt(trunk)
|
||||
}
|
||||
122
pkg/service/ioservice_sip_test.go
Normal file
122
pkg/service/ioservice_sip_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright 2024 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/dennwc/iters"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/psrpc"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func ioStoreDocker(t testing.TB) (*service.IOInfoService, *service.RedisStore) {
|
||||
r := redisClientDocker(t)
|
||||
bus := psrpc.NewRedisMessageBus(r)
|
||||
rs := service.NewRedisStore(r)
|
||||
io, err := service.NewIOInfoService(bus, rs, rs, rs, nil)
|
||||
require.NoError(t, err)
|
||||
return io, rs
|
||||
}
|
||||
|
||||
func TestSIPTrunkSelect(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, rs := ioStoreDocker(t)
|
||||
|
||||
for _, tr := range []*livekit.SIPInboundTrunkInfo{
|
||||
{SipTrunkId: "any", Numbers: nil},
|
||||
{SipTrunkId: "B", Numbers: []string{"B1", "B2"}},
|
||||
{SipTrunkId: "BC", Numbers: []string{"B1", "C1"}},
|
||||
} {
|
||||
err := rs.StoreSIPInboundTrunk(ctx, tr)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, tr := range []*livekit.SIPTrunkInfo{
|
||||
{SipTrunkId: "old-any", OutboundNumber: ""},
|
||||
{SipTrunkId: "old-A", OutboundNumber: "A"},
|
||||
} {
|
||||
err := rs.StoreSIPTrunk(ctx, tr)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
number string
|
||||
exp []string
|
||||
}{
|
||||
{"A", []string{"old-A", "old-any", "any"}},
|
||||
{"B1", []string{"B", "BC", "old-any", "any"}},
|
||||
{"B2", []string{"B", "old-any", "any"}},
|
||||
{"C1", []string{"BC", "old-any", "any"}},
|
||||
{"wrong", []string{"old-any", "any"}},
|
||||
} {
|
||||
t.Run(c.number, func(t *testing.T) {
|
||||
it := s.SelectSIPInboundTrunk(ctx, c.number)
|
||||
defer it.Close()
|
||||
list, err := iters.All(it)
|
||||
require.NoError(t, err)
|
||||
var ids []string
|
||||
for _, v := range list {
|
||||
ids = append(ids, v.SipTrunkId)
|
||||
}
|
||||
slices.Sort(c.exp)
|
||||
slices.Sort(ids)
|
||||
require.Equal(t, c.exp, ids)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSIPRuleSelect(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s, rs := ioStoreDocker(t)
|
||||
|
||||
for _, r := range []*livekit.SIPDispatchRuleInfo{
|
||||
{SipDispatchRuleId: "any", TrunkIds: nil},
|
||||
{SipDispatchRuleId: "B", TrunkIds: []string{"B1", "B2"}},
|
||||
{SipDispatchRuleId: "BC", TrunkIds: []string{"B1", "C1"}},
|
||||
} {
|
||||
err := rs.StoreSIPDispatchRule(ctx, r)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, c := range []struct {
|
||||
trunk string
|
||||
exp []string
|
||||
}{
|
||||
{"A", []string{"any"}},
|
||||
{"B1", []string{"B", "BC", "any"}},
|
||||
{"B2", []string{"B", "any"}},
|
||||
{"C1", []string{"BC", "any"}},
|
||||
{"wrong", []string{"any"}},
|
||||
} {
|
||||
t.Run(c.trunk, func(t *testing.T) {
|
||||
it := s.SelectSIPDispatchRule(ctx, c.trunk)
|
||||
defer it.Close()
|
||||
list, err := iters.All(it)
|
||||
require.NoError(t, err)
|
||||
var ids []string
|
||||
for _, v := range list {
|
||||
ids = append(ids, v.SipDispatchRuleId)
|
||||
}
|
||||
slices.Sort(c.exp)
|
||||
slices.Sort(ids)
|
||||
require.Equal(t, c.exp, ids)
|
||||
})
|
||||
}
|
||||
}
|
||||
298
pkg/service/localstore.go
Normal file
298
pkg/service/localstore.go
Normal file
@@ -0,0 +1,298 @@
|
||||
// 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"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thoas/go-funk"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils"
|
||||
)
|
||||
|
||||
var _ OSSServiceStore = (*LocalStore)(nil)
|
||||
|
||||
// encapsulates CRUD operations for room settings
|
||||
type LocalStore struct {
|
||||
// map of roomName => room
|
||||
rooms map[livekit.RoomName]*livekit.Room
|
||||
roomInternal map[livekit.RoomName]*livekit.RoomInternal
|
||||
// map of roomName => { identity: participant }
|
||||
participants map[livekit.RoomName]map[livekit.ParticipantIdentity]*livekit.ParticipantInfo
|
||||
|
||||
agentDispatches map[livekit.RoomName]map[string]*livekit.AgentDispatch
|
||||
agentJobs map[livekit.RoomName]map[string]*livekit.Job
|
||||
|
||||
lock sync.RWMutex
|
||||
globalLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewLocalStore() *LocalStore {
|
||||
return &LocalStore{
|
||||
rooms: make(map[livekit.RoomName]*livekit.Room),
|
||||
roomInternal: make(map[livekit.RoomName]*livekit.RoomInternal),
|
||||
participants: make(map[livekit.RoomName]map[livekit.ParticipantIdentity]*livekit.ParticipantInfo),
|
||||
agentDispatches: make(map[livekit.RoomName]map[string]*livekit.AgentDispatch),
|
||||
agentJobs: make(map[livekit.RoomName]map[string]*livekit.Job),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalStore) StoreRoom(_ context.Context, room *livekit.Room, internal *livekit.RoomInternal) error {
|
||||
if room.CreationTime == 0 {
|
||||
now := time.Now()
|
||||
room.CreationTime = now.Unix()
|
||||
room.CreationTimeMs = now.UnixMilli()
|
||||
}
|
||||
roomName := livekit.RoomName(room.Name)
|
||||
|
||||
s.lock.Lock()
|
||||
s.rooms[roomName] = room
|
||||
s.roomInternal[roomName] = internal
|
||||
s.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) LoadRoom(_ context.Context, roomName livekit.RoomName, includeInternal bool) (*livekit.Room, *livekit.RoomInternal, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
room := s.rooms[roomName]
|
||||
if room == nil {
|
||||
return nil, nil, ErrRoomNotFound
|
||||
}
|
||||
|
||||
var internal *livekit.RoomInternal
|
||||
if includeInternal {
|
||||
internal = s.roomInternal[roomName]
|
||||
}
|
||||
|
||||
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()
|
||||
rooms := make([]*livekit.Room, 0, len(s.rooms))
|
||||
for _, r := range s.rooms {
|
||||
if roomNames == nil || funk.Contains(roomNames, livekit.RoomName(r.Name)) {
|
||||
rooms = append(rooms, r)
|
||||
}
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) DeleteRoom(ctx context.Context, roomName livekit.RoomName) error {
|
||||
room, _, err := s.LoadRoom(ctx, roomName, false)
|
||||
if err == ErrRoomNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
delete(s.participants, livekit.RoomName(room.Name))
|
||||
delete(s.rooms, livekit.RoomName(room.Name))
|
||||
delete(s.roomInternal, livekit.RoomName(room.Name))
|
||||
delete(s.agentDispatches, livekit.RoomName(room.Name))
|
||||
delete(s.agentJobs, livekit.RoomName(room.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) LockRoom(_ context.Context, _ livekit.RoomName, _ time.Duration) (string, error) {
|
||||
// local rooms lock & unlock globally
|
||||
s.globalLock.Lock()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) UnlockRoom(_ context.Context, _ livekit.RoomName, _ string) error {
|
||||
s.globalLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) StoreParticipant(_ context.Context, roomName livekit.RoomName, participant *livekit.ParticipantInfo) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
roomParticipants := s.participants[roomName]
|
||||
if roomParticipants == nil {
|
||||
roomParticipants = make(map[livekit.ParticipantIdentity]*livekit.ParticipantInfo)
|
||||
s.participants[roomName] = roomParticipants
|
||||
}
|
||||
roomParticipants[livekit.ParticipantIdentity(participant.Identity)] = participant
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) LoadParticipant(_ context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
roomParticipants := s.participants[roomName]
|
||||
if roomParticipants == nil {
|
||||
return nil, ErrParticipantNotFound
|
||||
}
|
||||
participant := roomParticipants[identity]
|
||||
if participant == nil {
|
||||
return nil, ErrParticipantNotFound
|
||||
}
|
||||
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()
|
||||
|
||||
roomParticipants := s.participants[roomName]
|
||||
if roomParticipants == nil {
|
||||
// empty array
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
items := make([]*livekit.ParticipantInfo, 0, len(roomParticipants))
|
||||
for _, p := range roomParticipants {
|
||||
items = append(items, p)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) DeleteParticipant(_ context.Context, roomName livekit.RoomName, identity livekit.ParticipantIdentity) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
roomParticipants := s.participants[roomName]
|
||||
if roomParticipants != nil {
|
||||
delete(roomParticipants, identity)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) StoreAgentDispatch(ctx context.Context, dispatch *livekit.AgentDispatch) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
clone := utils.CloneProto(dispatch)
|
||||
if clone.State != nil {
|
||||
clone.State.Jobs = nil
|
||||
}
|
||||
|
||||
roomDispatches := s.agentDispatches[livekit.RoomName(dispatch.Room)]
|
||||
if roomDispatches == nil {
|
||||
roomDispatches = make(map[string]*livekit.AgentDispatch)
|
||||
s.agentDispatches[livekit.RoomName(dispatch.Room)] = roomDispatches
|
||||
}
|
||||
|
||||
roomDispatches[clone.Id] = clone
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) DeleteAgentDispatch(ctx context.Context, dispatch *livekit.AgentDispatch) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
roomDispatches := s.agentDispatches[livekit.RoomName(dispatch.Room)]
|
||||
if roomDispatches != nil {
|
||||
delete(roomDispatches, dispatch.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) ListAgentDispatches(ctx context.Context, roomName livekit.RoomName) ([]*livekit.AgentDispatch, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
agentDispatches := s.agentDispatches[roomName]
|
||||
if agentDispatches == nil {
|
||||
return nil, nil
|
||||
}
|
||||
agentJobs := s.agentJobs[roomName]
|
||||
|
||||
var js []*livekit.Job
|
||||
for _, j := range agentJobs {
|
||||
js = append(js, utils.CloneProto(j))
|
||||
}
|
||||
var ds []*livekit.AgentDispatch
|
||||
|
||||
m := make(map[string]*livekit.AgentDispatch)
|
||||
for _, d := range agentDispatches {
|
||||
clone := utils.CloneProto(d)
|
||||
m[d.Id] = clone
|
||||
ds = append(ds, clone)
|
||||
}
|
||||
|
||||
for _, j := range js {
|
||||
d := m[j.DispatchId]
|
||||
if d != nil {
|
||||
d.State.Jobs = append(d.State.Jobs, utils.CloneProto(j))
|
||||
}
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) StoreAgentJob(ctx context.Context, job *livekit.Job) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
clone := utils.CloneProto(job)
|
||||
clone.Room = nil
|
||||
if clone.Participant != nil {
|
||||
clone.Participant = &livekit.ParticipantInfo{
|
||||
Identity: clone.Participant.Identity,
|
||||
}
|
||||
}
|
||||
|
||||
roomJobs := s.agentJobs[livekit.RoomName(job.Room.Name)]
|
||||
if roomJobs == nil {
|
||||
roomJobs = make(map[string]*livekit.Job)
|
||||
s.agentJobs[livekit.RoomName(job.Room.Name)] = roomJobs
|
||||
}
|
||||
roomJobs[clone.Id] = clone
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStore) DeleteAgentJob(ctx context.Context, job *livekit.Job) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
roomJobs := s.agentJobs[livekit.RoomName(job.Room.Name)]
|
||||
if roomJobs != nil {
|
||||
delete(roomJobs, job.Id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1095
pkg/service/redisstore.go
Normal file
1095
pkg/service/redisstore.go
Normal file
File diff suppressed because it is too large
Load Diff
254
pkg/service/redisstore_sip.go
Normal file
254
pkg/service/redisstore_sip.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
const (
|
||||
SIPTrunkKey = "sip_trunk"
|
||||
SIPInboundTrunkKey = "sip_inbound_trunk"
|
||||
SIPOutboundTrunkKey = "sip_outbound_trunk"
|
||||
SIPDispatchRuleKey = "sip_dispatch_rule"
|
||||
)
|
||||
|
||||
func (s *RedisStore) StoreSIPTrunk(ctx context.Context, info *livekit.SIPTrunkInfo) error {
|
||||
return redisStoreOne(s.ctx, s, SIPTrunkKey, info.SipTrunkId, info)
|
||||
}
|
||||
|
||||
func (s *RedisStore) StoreSIPInboundTrunk(ctx context.Context, info *livekit.SIPInboundTrunkInfo) error {
|
||||
return redisStoreOne(s.ctx, s, SIPInboundTrunkKey, info.SipTrunkId, info)
|
||||
}
|
||||
|
||||
func (s *RedisStore) StoreSIPOutboundTrunk(ctx context.Context, info *livekit.SIPOutboundTrunkInfo) error {
|
||||
return redisStoreOne(s.ctx, s, SIPOutboundTrunkKey, info.SipTrunkId, info)
|
||||
}
|
||||
|
||||
func (s *RedisStore) loadSIPLegacyTrunk(ctx context.Context, id string) (*livekit.SIPTrunkInfo, error) {
|
||||
return redisLoadOne[livekit.SIPTrunkInfo](ctx, s, SIPTrunkKey, id, ErrSIPTrunkNotFound)
|
||||
}
|
||||
|
||||
func (s *RedisStore) loadSIPInboundTrunk(ctx context.Context, id string) (*livekit.SIPInboundTrunkInfo, error) {
|
||||
return redisLoadOne[livekit.SIPInboundTrunkInfo](ctx, s, SIPInboundTrunkKey, id, ErrSIPTrunkNotFound)
|
||||
}
|
||||
|
||||
func (s *RedisStore) loadSIPOutboundTrunk(ctx context.Context, id string) (*livekit.SIPOutboundTrunkInfo, error) {
|
||||
return redisLoadOne[livekit.SIPOutboundTrunkInfo](ctx, s, SIPOutboundTrunkKey, id, ErrSIPTrunkNotFound)
|
||||
}
|
||||
|
||||
func (s *RedisStore) LoadSIPTrunk(ctx context.Context, id string) (*livekit.SIPTrunkInfo, error) {
|
||||
tr, err := s.loadSIPLegacyTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return tr, nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
in, err := s.loadSIPInboundTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return in.AsTrunkInfo(), nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
out, err := s.loadSIPOutboundTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return out.AsTrunkInfo(), nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrSIPTrunkNotFound
|
||||
}
|
||||
|
||||
func (s *RedisStore) LoadSIPInboundTrunk(ctx context.Context, id string) (*livekit.SIPInboundTrunkInfo, error) {
|
||||
in, err := s.loadSIPInboundTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return in, nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
tr, err := s.loadSIPLegacyTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return tr.AsInbound(), nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrSIPTrunkNotFound
|
||||
}
|
||||
|
||||
func (s *RedisStore) LoadSIPOutboundTrunk(ctx context.Context, id string) (*livekit.SIPOutboundTrunkInfo, error) {
|
||||
in, err := s.loadSIPOutboundTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return in, nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
tr, err := s.loadSIPLegacyTrunk(ctx, id)
|
||||
if err == nil {
|
||||
return tr.AsOutbound(), nil
|
||||
} else if err != ErrSIPTrunkNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return nil, ErrSIPTrunkNotFound
|
||||
}
|
||||
|
||||
func (s *RedisStore) DeleteSIPTrunk(ctx context.Context, id string) error {
|
||||
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) {
|
||||
return redisIterPage[livekit.SIPTrunkInfo](ctx, s, SIPTrunkKey, page)
|
||||
}
|
||||
|
||||
func (s *RedisStore) listSIPInboundTrunk(ctx context.Context, page *livekit.Pagination) ([]*livekit.SIPInboundTrunkInfo, error) {
|
||||
return redisIterPage[livekit.SIPInboundTrunkInfo](ctx, s, SIPInboundTrunkKey, page)
|
||||
}
|
||||
|
||||
func (s *RedisStore) listSIPOutboundTrunk(ctx context.Context, page *livekit.Pagination) ([]*livekit.SIPOutboundTrunkInfo, error) {
|
||||
return redisIterPage[livekit.SIPOutboundTrunkInfo](ctx, s, SIPOutboundTrunkKey, page)
|
||||
}
|
||||
|
||||
func (s *RedisStore) listSIPDispatchRule(ctx context.Context, page *livekit.Pagination) ([]*livekit.SIPDispatchRuleInfo, error) {
|
||||
return redisIterPage[livekit.SIPDispatchRuleInfo](ctx, s, SIPDispatchRuleKey, page)
|
||||
}
|
||||
|
||||
func (s *RedisStore) ListSIPTrunk(ctx context.Context, req *livekit.ListSIPTrunkRequest) (*livekit.ListSIPTrunkResponse, error) {
|
||||
var items []*livekit.SIPTrunkInfo
|
||||
old, err := s.listSIPLegacyTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range old {
|
||||
v := t
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
in, err := s.listSIPInboundTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range in {
|
||||
v := t.AsTrunkInfo()
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
out, err := s.listSIPOutboundTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range out {
|
||||
v := t.AsTrunkInfo()
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
items = sortPage(items, req.Page)
|
||||
return &livekit.ListSIPTrunkResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *RedisStore) ListSIPInboundTrunk(ctx context.Context, req *livekit.ListSIPInboundTrunkRequest) (*livekit.ListSIPInboundTrunkResponse, error) {
|
||||
var items []*livekit.SIPInboundTrunkInfo
|
||||
in, err := s.listSIPInboundTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range in {
|
||||
v := t
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
old, err := s.listSIPLegacyTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range old {
|
||||
v := t.AsInbound()
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
items = sortPage(items, req.Page)
|
||||
return &livekit.ListSIPInboundTrunkResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *RedisStore) ListSIPOutboundTrunk(ctx context.Context, req *livekit.ListSIPOutboundTrunkRequest) (*livekit.ListSIPOutboundTrunkResponse, error) {
|
||||
var items []*livekit.SIPOutboundTrunkInfo
|
||||
out, err := s.listSIPOutboundTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range out {
|
||||
v := t
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
old, err := s.listSIPLegacyTrunk(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range old {
|
||||
v := t.AsOutbound()
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
items = sortPage(items, req.Page)
|
||||
return &livekit.ListSIPOutboundTrunkResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *RedisStore) StoreSIPDispatchRule(ctx context.Context, info *livekit.SIPDispatchRuleInfo) error {
|
||||
return redisStoreOne(ctx, s, SIPDispatchRuleKey, info.SipDispatchRuleId, info)
|
||||
}
|
||||
|
||||
func (s *RedisStore) LoadSIPDispatchRule(ctx context.Context, sipDispatchRuleId string) (*livekit.SIPDispatchRuleInfo, error) {
|
||||
return redisLoadOne[livekit.SIPDispatchRuleInfo](ctx, s, SIPDispatchRuleKey, sipDispatchRuleId, ErrSIPDispatchRuleNotFound)
|
||||
}
|
||||
|
||||
func (s *RedisStore) DeleteSIPDispatchRule(ctx context.Context, sipDispatchRuleId string) error {
|
||||
return s.rc.HDel(s.ctx, SIPDispatchRuleKey, sipDispatchRuleId).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) ListSIPDispatchRule(ctx context.Context, req *livekit.ListSIPDispatchRuleRequest) (*livekit.ListSIPDispatchRuleResponse, error) {
|
||||
var items []*livekit.SIPDispatchRuleInfo
|
||||
out, err := s.listSIPDispatchRule(ctx, req.Page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range out {
|
||||
v := t
|
||||
if req.Filter(v) && req.Page.Filter(v) {
|
||||
items = append(items, v)
|
||||
}
|
||||
}
|
||||
items = sortPage(items, req.Page)
|
||||
return &livekit.ListSIPDispatchRuleResponse{Items: items}, nil
|
||||
}
|
||||
357
pkg/service/redisstore_sip_test.go
Normal file
357
pkg/service/redisstore_sip_test.go
Normal file
@@ -0,0 +1,357 @@
|
||||
// Copyright 2024 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dennwc/iters"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
func TestSIPStoreDispatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStoreDocker(t)
|
||||
|
||||
id := guid.New(utils.SIPDispatchRulePrefix)
|
||||
|
||||
// No dispatch rules initially.
|
||||
list, err := rs.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
|
||||
// Loading non-existent dispatch should return proper not found error.
|
||||
got, err := rs.LoadSIPDispatchRule(ctx, id)
|
||||
require.Equal(t, service.ErrSIPDispatchRuleNotFound, err)
|
||||
require.Nil(t, got)
|
||||
|
||||
// Creation without ID should fail.
|
||||
rule := &livekit.SIPDispatchRuleInfo{
|
||||
TrunkIds: []string{"trunk"},
|
||||
Rule: &livekit.SIPDispatchRule{Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{
|
||||
DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{
|
||||
RoomName: "room",
|
||||
Pin: "1234",
|
||||
},
|
||||
}},
|
||||
}
|
||||
err = rs.StoreSIPDispatchRule(ctx, rule)
|
||||
require.Error(t, err)
|
||||
|
||||
// Creation
|
||||
rule.SipDispatchRuleId = id
|
||||
err = rs.StoreSIPDispatchRule(ctx, rule)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Loading
|
||||
got, err = rs.LoadSIPDispatchRule(ctx, id)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(rule, got))
|
||||
|
||||
// Listing
|
||||
list, err = rs.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.True(t, proto.Equal(rule, list.Items[0]))
|
||||
|
||||
// Deletion. Should not return error if not exists.
|
||||
err = rs.DeleteSIPDispatchRule(ctx, id)
|
||||
require.NoError(t, err)
|
||||
err = rs.DeleteSIPDispatchRule(ctx, id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that it's deleted.
|
||||
list, err = rs.ListSIPDispatchRule(ctx, &livekit.ListSIPDispatchRuleRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
|
||||
got, err = rs.LoadSIPDispatchRule(ctx, id)
|
||||
require.Equal(t, service.ErrSIPDispatchRuleNotFound, err)
|
||||
require.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestSIPStoreTrunk(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStoreDocker(t)
|
||||
|
||||
oldID := guid.New(utils.SIPTrunkPrefix)
|
||||
inID := guid.New(utils.SIPTrunkPrefix)
|
||||
outID := guid.New(utils.SIPTrunkPrefix)
|
||||
|
||||
// No trunks initially. Check legacy, inbound, outbound.
|
||||
// Loading non-existent trunk should return proper not found error.
|
||||
oldList, err := rs.ListSIPTrunk(ctx, &livekit.ListSIPTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, oldList.Items)
|
||||
|
||||
old, err := rs.LoadSIPTrunk(ctx, oldID)
|
||||
require.Equal(t, service.ErrSIPTrunkNotFound, err)
|
||||
require.Nil(t, old)
|
||||
|
||||
inList, err := rs.ListSIPInboundTrunk(ctx, &livekit.ListSIPInboundTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, inList.Items)
|
||||
|
||||
in, err := rs.LoadSIPInboundTrunk(ctx, oldID)
|
||||
require.Equal(t, service.ErrSIPTrunkNotFound, err)
|
||||
require.Nil(t, in)
|
||||
|
||||
outList, err := rs.ListSIPOutboundTrunk(ctx, &livekit.ListSIPOutboundTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, outList.Items)
|
||||
|
||||
out, err := rs.LoadSIPOutboundTrunk(ctx, oldID)
|
||||
require.Equal(t, service.ErrSIPTrunkNotFound, err)
|
||||
require.Nil(t, out)
|
||||
|
||||
// Creation without ID should fail.
|
||||
oldT := &livekit.SIPTrunkInfo{
|
||||
Name: "Legacy",
|
||||
}
|
||||
err = rs.StoreSIPTrunk(ctx, oldT)
|
||||
require.Error(t, err)
|
||||
|
||||
inT := &livekit.SIPInboundTrunkInfo{
|
||||
Name: "Inbound",
|
||||
}
|
||||
err = rs.StoreSIPInboundTrunk(ctx, inT)
|
||||
require.Error(t, err)
|
||||
|
||||
outT := &livekit.SIPOutboundTrunkInfo{
|
||||
Name: "Outbound",
|
||||
}
|
||||
err = rs.StoreSIPOutboundTrunk(ctx, outT)
|
||||
require.Error(t, err)
|
||||
|
||||
// Creation
|
||||
oldT.SipTrunkId = oldID
|
||||
err = rs.StoreSIPTrunk(ctx, oldT)
|
||||
require.NoError(t, err)
|
||||
|
||||
inT.SipTrunkId = inID
|
||||
err = rs.StoreSIPInboundTrunk(ctx, inT)
|
||||
require.NoError(t, err)
|
||||
|
||||
outT.SipTrunkId = outID
|
||||
err = rs.StoreSIPOutboundTrunk(ctx, outT)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Loading (with matching kind)
|
||||
oldT2, err := rs.LoadSIPTrunk(ctx, oldID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(oldT, oldT2))
|
||||
|
||||
inT2, err := rs.LoadSIPInboundTrunk(ctx, inID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(inT, inT2))
|
||||
|
||||
outT2, err := rs.LoadSIPOutboundTrunk(ctx, outID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(outT, outT2))
|
||||
|
||||
// Loading (compat)
|
||||
oldT2, err = rs.LoadSIPTrunk(ctx, inID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(inT.AsTrunkInfo(), oldT2))
|
||||
|
||||
oldT2, err = rs.LoadSIPTrunk(ctx, outID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(outT.AsTrunkInfo(), oldT2))
|
||||
|
||||
inT2, err = rs.LoadSIPInboundTrunk(ctx, oldID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(oldT.AsInbound(), inT2))
|
||||
|
||||
outT2, err = rs.LoadSIPOutboundTrunk(ctx, oldID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(oldT.AsOutbound(), outT2))
|
||||
|
||||
// Listing (always shows legacy + new)
|
||||
listOld, err := rs.ListSIPTrunk(ctx, &livekit.ListSIPTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listOld.Items, 3)
|
||||
slices.SortFunc(listOld.Items, func(a, b *livekit.SIPTrunkInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
require.True(t, proto.Equal(inT.AsTrunkInfo(), listOld.Items[0]))
|
||||
require.True(t, proto.Equal(oldT, listOld.Items[1]))
|
||||
require.True(t, proto.Equal(outT.AsTrunkInfo(), listOld.Items[2]))
|
||||
|
||||
listIn, err := rs.ListSIPInboundTrunk(ctx, &livekit.ListSIPInboundTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listIn.Items, 2)
|
||||
slices.SortFunc(listIn.Items, func(a, b *livekit.SIPInboundTrunkInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
require.True(t, proto.Equal(inT, listIn.Items[0]))
|
||||
require.True(t, proto.Equal(oldT.AsInbound(), listIn.Items[1]))
|
||||
|
||||
listOut, err := rs.ListSIPOutboundTrunk(ctx, &livekit.ListSIPOutboundTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, listOut.Items, 2)
|
||||
slices.SortFunc(listOut.Items, func(a, b *livekit.SIPOutboundTrunkInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
require.True(t, proto.Equal(oldT.AsOutbound(), listOut.Items[0]))
|
||||
require.True(t, proto.Equal(outT, listOut.Items[1]))
|
||||
|
||||
// Deletion. Should not return error if not exists.
|
||||
err = rs.DeleteSIPTrunk(ctx, oldID)
|
||||
require.NoError(t, err)
|
||||
err = rs.DeleteSIPTrunk(ctx, oldID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Other objects are still there.
|
||||
inT2, err = rs.LoadSIPInboundTrunk(ctx, inID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(inT, inT2))
|
||||
|
||||
outT2, err = rs.LoadSIPOutboundTrunk(ctx, outID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, proto.Equal(outT, outT2))
|
||||
|
||||
// Delete the rest
|
||||
err = rs.DeleteSIPTrunk(ctx, inID)
|
||||
require.NoError(t, err)
|
||||
err = rs.DeleteSIPTrunk(ctx, outID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check everything is deleted.
|
||||
oldList, err = rs.ListSIPTrunk(ctx, &livekit.ListSIPTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, oldList.Items)
|
||||
|
||||
inList, err = rs.ListSIPInboundTrunk(ctx, &livekit.ListSIPInboundTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, inList.Items)
|
||||
|
||||
outList, err = rs.ListSIPOutboundTrunk(ctx, &livekit.ListSIPOutboundTrunkRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, outList.Items)
|
||||
|
||||
old, err = rs.LoadSIPTrunk(ctx, oldID)
|
||||
require.Equal(t, service.ErrSIPTrunkNotFound, err)
|
||||
require.Nil(t, old)
|
||||
|
||||
in, err = rs.LoadSIPInboundTrunk(ctx, oldID)
|
||||
require.Equal(t, service.ErrSIPTrunkNotFound, err)
|
||||
require.Nil(t, in)
|
||||
|
||||
out, err = rs.LoadSIPOutboundTrunk(ctx, oldID)
|
||||
require.Equal(t, service.ErrSIPTrunkNotFound, err)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
|
||||
func TestSIPTrunkList(t *testing.T) {
|
||||
s := redisStoreDocker(t)
|
||||
|
||||
testIter(t, func(ctx context.Context, id string) error {
|
||||
if strings.HasSuffix(id, "0") {
|
||||
return s.StoreSIPTrunk(ctx, &livekit.SIPTrunkInfo{
|
||||
SipTrunkId: id,
|
||||
OutboundNumber: id,
|
||||
})
|
||||
}
|
||||
return s.StoreSIPInboundTrunk(ctx, &livekit.SIPInboundTrunkInfo{
|
||||
SipTrunkId: id,
|
||||
Numbers: []string{id},
|
||||
})
|
||||
}, func(ctx context.Context, page *livekit.Pagination, ids []string) iters.PageIter[*livekit.SIPInboundTrunkInfo] {
|
||||
return livekit.ListPageIter(s.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{
|
||||
TrunkIds: ids, Page: page,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSIPRuleList(t *testing.T) {
|
||||
s := redisStoreDocker(t)
|
||||
|
||||
testIter(t, func(ctx context.Context, id string) error {
|
||||
return s.StoreSIPDispatchRule(ctx, &livekit.SIPDispatchRuleInfo{
|
||||
SipDispatchRuleId: id,
|
||||
TrunkIds: []string{id},
|
||||
})
|
||||
}, func(ctx context.Context, page *livekit.Pagination, ids []string) iters.PageIter[*livekit.SIPDispatchRuleInfo] {
|
||||
return livekit.ListPageIter(s.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{
|
||||
DispatchRuleIds: ids, Page: page,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type listItem interface {
|
||||
ID() string
|
||||
}
|
||||
|
||||
func allIDs[T listItem](t testing.TB, it iters.PageIter[T]) []string {
|
||||
defer it.Close()
|
||||
got, err := iters.AllPages(context.Background(), iters.MapPage(it, func(ctx context.Context, v T) (string, error) {
|
||||
return v.ID(), nil
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
return got
|
||||
}
|
||||
|
||||
func testIter[T listItem](
|
||||
t *testing.T,
|
||||
create func(ctx context.Context, id string) error,
|
||||
list func(ctx context.Context, page *livekit.Pagination, ids []string) iters.PageIter[T],
|
||||
) {
|
||||
ctx := context.Background()
|
||||
var all []string
|
||||
for i := range 250 {
|
||||
id := fmt.Sprintf("%05d", i)
|
||||
all = append(all, id)
|
||||
err := create(ctx, id)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// List everything with pagination disabled (legacy)
|
||||
it := list(ctx, nil, nil)
|
||||
got := allIDs(t, it)
|
||||
require.Equal(t, all, got)
|
||||
|
||||
// List with pagination enabled
|
||||
it = list(ctx, &livekit.Pagination{Limit: 10}, nil)
|
||||
got = allIDs(t, it)
|
||||
require.Equal(t, all, got)
|
||||
|
||||
// List with pagination enabled, custom ID
|
||||
it = list(ctx, &livekit.Pagination{Limit: 10, AfterId: all[55]}, nil)
|
||||
got = allIDs(t, it)
|
||||
require.Equal(t, all[56:], got)
|
||||
|
||||
// List fixed IDs
|
||||
it = list(ctx, &livekit.Pagination{Limit: 10, AfterId: all[5]}, []string{
|
||||
all[10],
|
||||
all[3],
|
||||
"invalid",
|
||||
all[8],
|
||||
})
|
||||
got = allIDs(t, it)
|
||||
require.Equal(t, []string{
|
||||
all[8],
|
||||
all[10],
|
||||
}, got)
|
||||
}
|
||||
391
pkg/service/redisstore_test.go
Normal file
391
pkg/service/redisstore_test.go
Normal file
@@ -0,0 +1,391 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/atomic"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/protocol/ingress"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
func redisStoreDocker(t testing.TB) *service.RedisStore {
|
||||
return service.NewRedisStore(redisClientDocker(t))
|
||||
}
|
||||
|
||||
func redisStore(t testing.TB) *service.RedisStore {
|
||||
return service.NewRedisStore(redisClient(t))
|
||||
}
|
||||
|
||||
func TestRoomInternal(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStore(t)
|
||||
|
||||
room := &livekit.Room{
|
||||
Sid: "123",
|
||||
Name: "test_room",
|
||||
}
|
||||
internal := &livekit.RoomInternal{
|
||||
TrackEgress: &livekit.AutoTrackEgress{Filepath: "egress"},
|
||||
}
|
||||
|
||||
require.NoError(t, rs.StoreRoom(ctx, room, internal))
|
||||
actualRoom, actualInternal, err := rs.LoadRoom(ctx, livekit.RoomName(room.Name), true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, room.Sid, actualRoom.Sid)
|
||||
require.Equal(t, internal.TrackEgress.Filepath, actualInternal.TrackEgress.Filepath)
|
||||
|
||||
// remove internal
|
||||
require.NoError(t, rs.StoreRoom(ctx, room, nil))
|
||||
_, actualInternal, err = rs.LoadRoom(ctx, livekit.RoomName(room.Name), true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, actualInternal)
|
||||
|
||||
// clean up
|
||||
require.NoError(t, rs.DeleteRoom(ctx, "test_room"))
|
||||
}
|
||||
|
||||
func TestParticipantPersistence(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStore(t)
|
||||
|
||||
roomName := livekit.RoomName("room1")
|
||||
_ = rs.DeleteRoom(ctx, roomName)
|
||||
|
||||
p := &livekit.ParticipantInfo{
|
||||
Sid: "PA_test",
|
||||
Identity: "test",
|
||||
State: livekit.ParticipantInfo_ACTIVE,
|
||||
Tracks: []*livekit.TrackInfo{
|
||||
{
|
||||
Sid: "track1",
|
||||
Type: livekit.TrackType_AUDIO,
|
||||
Name: "audio",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// create the participant
|
||||
require.NoError(t, rs.StoreParticipant(ctx, roomName, p))
|
||||
|
||||
// result should match
|
||||
pGet, err := rs.LoadParticipant(ctx, roomName, livekit.ParticipantIdentity(p.Identity))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, p.Identity, pGet.Identity)
|
||||
require.Equal(t, len(p.Tracks), len(pGet.Tracks))
|
||||
require.Equal(t, p.Tracks[0].Sid, pGet.Tracks[0].Sid)
|
||||
|
||||
// list should return one participant
|
||||
participants, err := rs.ListParticipants(ctx, roomName)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, participants, 1)
|
||||
|
||||
// deleting participant should return to normal
|
||||
require.NoError(t, rs.DeleteParticipant(ctx, roomName, livekit.ParticipantIdentity(p.Identity)))
|
||||
|
||||
participants, err = rs.ListParticipants(ctx, roomName)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, participants, 0)
|
||||
|
||||
// shouldn't be able to get it
|
||||
_, err = rs.LoadParticipant(ctx, roomName, livekit.ParticipantIdentity(p.Identity))
|
||||
require.Equal(t, err, service.ErrParticipantNotFound)
|
||||
}
|
||||
|
||||
func TestRoomLock(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStore(t)
|
||||
lockInterval := 5 * time.Millisecond
|
||||
roomName := livekit.RoomName("myroom")
|
||||
|
||||
t.Run("normal locking", func(t *testing.T) {
|
||||
token, err := rs.LockRoom(ctx, roomName, lockInterval)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
require.NoError(t, rs.UnlockRoom(ctx, roomName, token))
|
||||
})
|
||||
|
||||
t.Run("waits before acquiring lock", func(t *testing.T) {
|
||||
token, err := rs.LockRoom(ctx, roomName, lockInterval)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, token)
|
||||
unlocked := atomic.NewUint32(0)
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
// attempt to lock again
|
||||
defer wg.Done()
|
||||
token2, err := rs.LockRoom(ctx, roomName, lockInterval)
|
||||
require.NoError(t, err)
|
||||
defer rs.UnlockRoom(ctx, roomName, token2)
|
||||
require.Equal(t, uint32(1), unlocked.Load())
|
||||
}()
|
||||
|
||||
// release after 2 ms
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
unlocked.Store(1)
|
||||
_ = rs.UnlockRoom(ctx, roomName, token)
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
t.Run("lock expires", func(t *testing.T) {
|
||||
token, err := rs.LockRoom(ctx, roomName, lockInterval)
|
||||
require.NoError(t, err)
|
||||
defer rs.UnlockRoom(ctx, roomName, token)
|
||||
|
||||
time.Sleep(lockInterval + time.Millisecond)
|
||||
token2, err := rs.LockRoom(ctx, roomName, lockInterval)
|
||||
require.NoError(t, err)
|
||||
_ = rs.UnlockRoom(ctx, roomName, token2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEgressStore(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStore(t)
|
||||
|
||||
roomName := "egress-test"
|
||||
|
||||
// store egress info
|
||||
info := &livekit.EgressInfo{
|
||||
EgressId: guid.New(utils.EgressPrefix),
|
||||
RoomId: guid.New(utils.RoomPrefix),
|
||||
RoomName: roomName,
|
||||
Status: livekit.EgressStatus_EGRESS_STARTING,
|
||||
Request: &livekit.EgressInfo_RoomComposite{
|
||||
RoomComposite: &livekit.RoomCompositeEgressRequest{
|
||||
RoomName: roomName,
|
||||
Layout: "speaker-dark",
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rs.StoreEgress(ctx, info))
|
||||
|
||||
// load
|
||||
res, err := rs.LoadEgress(ctx, info.EgressId)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, res.EgressId, info.EgressId)
|
||||
|
||||
// store another
|
||||
info2 := &livekit.EgressInfo{
|
||||
EgressId: guid.New(utils.EgressPrefix),
|
||||
RoomId: guid.New(utils.RoomPrefix),
|
||||
RoomName: "another-egress-test",
|
||||
Status: livekit.EgressStatus_EGRESS_STARTING,
|
||||
Request: &livekit.EgressInfo_RoomComposite{
|
||||
RoomComposite: &livekit.RoomCompositeEgressRequest{
|
||||
RoomName: "another-egress-test",
|
||||
Layout: "speaker-dark",
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, rs.StoreEgress(ctx, info2))
|
||||
|
||||
// update
|
||||
info2.Status = livekit.EgressStatus_EGRESS_COMPLETE
|
||||
info2.EndedAt = time.Now().Add(-24 * time.Hour).UnixNano()
|
||||
require.NoError(t, rs.UpdateEgress(ctx, info))
|
||||
|
||||
// list
|
||||
list, err := rs.ListEgress(ctx, "", false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 2)
|
||||
|
||||
// list by room
|
||||
list, err = rs.ListEgress(ctx, livekit.RoomName(roomName), false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 1)
|
||||
|
||||
// update
|
||||
info.Status = livekit.EgressStatus_EGRESS_COMPLETE
|
||||
info.EndedAt = time.Now().Add(-24 * time.Hour).UnixNano()
|
||||
require.NoError(t, rs.UpdateEgress(ctx, info))
|
||||
|
||||
// clean
|
||||
require.NoError(t, rs.CleanEndedEgress())
|
||||
|
||||
// list
|
||||
list, err = rs.ListEgress(ctx, livekit.RoomName(roomName), false)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 0)
|
||||
}
|
||||
|
||||
func TestIngressStore(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStore(t)
|
||||
|
||||
info := &livekit.IngressInfo{
|
||||
IngressId: "ingressId",
|
||||
StreamKey: "streamKey",
|
||||
State: &livekit.IngressState{
|
||||
StartedAt: 2,
|
||||
},
|
||||
}
|
||||
|
||||
err := rs.StoreIngress(ctx, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = rs.UpdateIngressState(ctx, info.IngressId, info.State)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
rs.DeleteIngress(ctx, info)
|
||||
})
|
||||
|
||||
pulledInfo, err := rs.LoadIngress(ctx, "ingressId")
|
||||
require.NoError(t, err)
|
||||
compareIngressInfo(t, pulledInfo, info)
|
||||
|
||||
infos, err := rs.ListIngress(ctx, "room")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(infos))
|
||||
|
||||
info.RoomName = "room"
|
||||
err = rs.UpdateIngress(ctx, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
infos, err = rs.ListIngress(ctx, "room")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(infos))
|
||||
compareIngressInfo(t, infos[0], info)
|
||||
|
||||
info.RoomName = ""
|
||||
err = rs.UpdateIngress(ctx, info)
|
||||
require.NoError(t, err)
|
||||
|
||||
infos, err = rs.ListIngress(ctx, "room")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(infos))
|
||||
|
||||
info.State.StartedAt = 1
|
||||
err = rs.UpdateIngressState(ctx, info.IngressId, info.State)
|
||||
require.Equal(t, ingress.ErrIngressOutOfDate, err)
|
||||
|
||||
info.State.StartedAt = 3
|
||||
err = rs.UpdateIngressState(ctx, info.IngressId, info.State)
|
||||
require.NoError(t, err)
|
||||
|
||||
infos, err = rs.ListIngress(ctx, "")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(infos))
|
||||
require.Equal(t, "", infos[0].RoomName)
|
||||
}
|
||||
|
||||
func TestAgentStore(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rs := redisStore(t)
|
||||
|
||||
ad := &livekit.AgentDispatch{
|
||||
Id: "dispatch_id",
|
||||
AgentName: "agent_name",
|
||||
Metadata: "metadata",
|
||||
Room: "room_name",
|
||||
State: &livekit.AgentDispatchState{
|
||||
CreatedAt: 1,
|
||||
DeletedAt: 2,
|
||||
Jobs: []*livekit.Job{
|
||||
&livekit.Job{
|
||||
Id: "job_id",
|
||||
DispatchId: "dispatch_id",
|
||||
Type: livekit.JobType_JT_PUBLISHER,
|
||||
Room: &livekit.Room{
|
||||
Name: "room_name",
|
||||
},
|
||||
Participant: &livekit.ParticipantInfo{
|
||||
Identity: "identity",
|
||||
Name: "name",
|
||||
},
|
||||
Namespace: "ns",
|
||||
Metadata: "metadata",
|
||||
AgentName: "agent_name",
|
||||
State: &livekit.JobState{
|
||||
Status: livekit.JobStatus_JS_RUNNING,
|
||||
StartedAt: 3,
|
||||
EndedAt: 4,
|
||||
Error: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := rs.StoreAgentDispatch(ctx, ad)
|
||||
require.NoError(t, err)
|
||||
|
||||
rd, err := rs.ListAgentDispatches(ctx, "not_a_room")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(rd))
|
||||
|
||||
rd, err = rs.ListAgentDispatches(ctx, "room_name")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(rd))
|
||||
|
||||
expected := utils.CloneProto(ad)
|
||||
expected.State.Jobs = nil
|
||||
require.True(t, proto.Equal(expected, rd[0]))
|
||||
|
||||
err = rs.StoreAgentJob(ctx, ad.State.Jobs[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
rd, err = rs.ListAgentDispatches(ctx, "room_name")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(rd))
|
||||
|
||||
expected = utils.CloneProto(ad)
|
||||
expected.State.Jobs[0].Room = nil
|
||||
expected.State.Jobs[0].Participant = &livekit.ParticipantInfo{
|
||||
Identity: "identity",
|
||||
}
|
||||
require.True(t, proto.Equal(expected, rd[0]))
|
||||
|
||||
err = rs.DeleteAgentJob(ctx, ad.State.Jobs[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
rd, err = rs.ListAgentDispatches(ctx, "room_name")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(rd))
|
||||
|
||||
expected = utils.CloneProto(ad)
|
||||
expected.State.Jobs = nil
|
||||
require.True(t, proto.Equal(expected, rd[0]))
|
||||
|
||||
err = rs.DeleteAgentDispatch(ctx, ad)
|
||||
require.NoError(t, err)
|
||||
|
||||
rd, err = rs.ListAgentDispatches(ctx, "room_name")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(rd))
|
||||
}
|
||||
|
||||
func compareIngressInfo(t *testing.T, expected, v *livekit.IngressInfo) {
|
||||
require.Equal(t, expected.IngressId, v.IngressId)
|
||||
require.Equal(t, expected.StreamKey, v.StreamKey)
|
||||
require.Equal(t, expected.RoomName, v.RoomName)
|
||||
}
|
||||
53
pkg/service/requestid.go
Normal file
53
pkg/service/requestid.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
)
|
||||
|
||||
// RequestIDAttribute is the participant attribute stamped with the request id
|
||||
// for calls that join a room before dialing. A
|
||||
// retry finds the existing participant with a matching value and skips the dial.
|
||||
const RequestIDAttribute = "lk.request_id"
|
||||
|
||||
type requestIDKey struct{}
|
||||
|
||||
// WithRequestID stores the client idempotency id on the context and propagates
|
||||
// it to downstream services via outgoing metadata. A no-op when id is empty.
|
||||
func WithRequestID(ctx context.Context, id string) context.Context {
|
||||
if id == "" {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, requestIDKey{}, id)
|
||||
}
|
||||
|
||||
func RequestID(ctx context.Context) string {
|
||||
if id, ok := ctx.Value(requestIDKey{}).(string); ok && id != "" {
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DeterministicID derives a stable resource id from the request id — retries of the same logical call
|
||||
// yield the same id and dedup at the store. With no request id it falls back to a random id.
|
||||
func DeterministicID(prefix, requestID string) string {
|
||||
if requestID == "" {
|
||||
return guid.New(prefix)
|
||||
}
|
||||
return guid.Hash(prefix, []byte(requestID))
|
||||
}
|
||||
70
pkg/service/requestid_test.go
Normal file
70
pkg/service/requestid_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeterministicID(t *testing.T) {
|
||||
const prefix = "EG_"
|
||||
|
||||
// Same request id -> same id: this is what makes an SDK-retried create dedup.
|
||||
a := DeterministicID(prefix, "req_123")
|
||||
if b := DeterministicID(prefix, "req_123"); a != b {
|
||||
t.Fatalf("not stable for same request id: %q != %q", a, b)
|
||||
}
|
||||
if !strings.HasPrefix(a, prefix) {
|
||||
t.Fatalf("id %q missing prefix %q", a, prefix)
|
||||
}
|
||||
|
||||
// Different request ids -> different ids.
|
||||
if c := DeterministicID(prefix, "req_456"); c == a {
|
||||
t.Fatalf("different request ids produced the same id: %q", c)
|
||||
}
|
||||
|
||||
// Different prefixes -> different ids (so e.g. a room and identity derived
|
||||
// from the same request id don't collide).
|
||||
if d := DeterministicID("IN_", "req_123"); d == a {
|
||||
t.Fatalf("different prefixes produced the same id: %q", d)
|
||||
}
|
||||
|
||||
// Empty request id -> random (each call differs), still prefixed. Preserves
|
||||
// non-idempotent behavior when the client sent no request id.
|
||||
r1 := DeterministicID(prefix, "")
|
||||
r2 := DeterministicID(prefix, "")
|
||||
if r1 == r2 {
|
||||
t.Fatalf("empty request id should produce random ids, got equal: %q", r1)
|
||||
}
|
||||
if !strings.HasPrefix(r1, prefix) {
|
||||
t.Fatalf("random id %q missing prefix %q", r1, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestID(t *testing.T) {
|
||||
if got := RequestID(context.Background()); got != "" {
|
||||
t.Fatalf("RequestID on bare context = %q, want empty", got)
|
||||
}
|
||||
ctx := WithRequestID(context.Background(), "req_abc")
|
||||
if got := RequestID(ctx); got != "req_abc" {
|
||||
t.Fatalf("RequestID = %q, want req_abc", got)
|
||||
}
|
||||
// An empty id is a no-op (the client sent none).
|
||||
if got := RequestID(WithRequestID(context.Background(), "")); got != "" {
|
||||
t.Fatalf("WithRequestID(\"\") should be a no-op, got %q", got)
|
||||
}
|
||||
}
|
||||
135
pkg/service/requestlimit_test.go
Normal file
135
pkg/service/requestlimit_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2024 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_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
// readAllHandler mimics the way a Twirp handler consumes the whole request body
|
||||
// before doing anything else. It records how much it managed to read and whether
|
||||
// the read failed (e.g. because the body limit was exceeded).
|
||||
type readAllHandler struct {
|
||||
bytesRead int
|
||||
readErr error
|
||||
called bool
|
||||
}
|
||||
|
||||
func (h *readAllHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.called = true
|
||||
if r.Body == nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
b, err := io.ReadAll(r.Body)
|
||||
h.bytesRead = len(b)
|
||||
h.readErr = err
|
||||
if err != nil {
|
||||
// a real decoder surfaces this as a 4xx/5xx; emulate that
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func TestRequestBodyLimiter(t *testing.T) {
|
||||
const limit = 1024
|
||||
|
||||
t.Run("rejects oversized declared Content-Length before decoding", func(t *testing.T) {
|
||||
l := service.NewRequestBodyLimiter(limit)
|
||||
handler := &readAllHandler{}
|
||||
|
||||
body := strings.NewReader(strings.Repeat("a", limit*4))
|
||||
r := httptest.NewRequest(http.MethodPost, "/twirp/livekit.Egress/StartRoomCompositeEgress", body)
|
||||
require.EqualValues(t, limit*4, r.ContentLength)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
l.ServeHTTP(w, r, handler.ServeHTTP)
|
||||
|
||||
require.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
|
||||
// the body must never be handed to the decoder
|
||||
require.False(t, handler.called)
|
||||
})
|
||||
|
||||
t.Run("bounds body when Content-Length is absent/dishonest", func(t *testing.T) {
|
||||
l := service.NewRequestBodyLimiter(limit)
|
||||
handler := &readAllHandler{}
|
||||
|
||||
body := strings.NewReader(strings.Repeat("a", limit*4))
|
||||
r := httptest.NewRequest(http.MethodPost, "/twirp/livekit.Egress/StartRoomCompositeEgress", body)
|
||||
// simulate chunked encoding / unknown length
|
||||
r.ContentLength = -1
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
l.ServeHTTP(w, r, handler.ServeHTTP)
|
||||
|
||||
// the decoder was invoked but could not read more than the limit
|
||||
require.True(t, handler.called)
|
||||
require.Error(t, handler.readErr)
|
||||
require.LessOrEqual(t, handler.bytesRead, limit)
|
||||
})
|
||||
|
||||
t.Run("allows request within limit", func(t *testing.T) {
|
||||
l := service.NewRequestBodyLimiter(limit)
|
||||
handler := &readAllHandler{}
|
||||
|
||||
payload := strings.Repeat("a", limit/2)
|
||||
r := httptest.NewRequest(http.MethodPost, "/twirp/livekit.Egress/StartRoomCompositeEgress", strings.NewReader(payload))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
l.ServeHTTP(w, r, handler.ServeHTTP)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.True(t, handler.called)
|
||||
require.NoError(t, handler.readErr)
|
||||
require.Equal(t, len(payload), handler.bytesRead)
|
||||
})
|
||||
|
||||
t.Run("disabled when limit is non-positive", func(t *testing.T) {
|
||||
l := service.NewRequestBodyLimiter(0)
|
||||
handler := &readAllHandler{}
|
||||
|
||||
payload := strings.Repeat("a", limit*8)
|
||||
r := httptest.NewRequest(http.MethodPost, "/twirp/livekit.Egress/StartRoomCompositeEgress", strings.NewReader(payload))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
l.ServeHTTP(w, r, handler.ServeHTTP)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.NoError(t, handler.readErr)
|
||||
require.Equal(t, len(payload), handler.bytesRead)
|
||||
})
|
||||
|
||||
t.Run("passes through nil body", func(t *testing.T) {
|
||||
l := service.NewRequestBodyLimiter(limit)
|
||||
handler := &readAllHandler{}
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.Body = nil
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
l.ServeHTTP(w, r, handler.ServeHTTP)
|
||||
|
||||
require.True(t, handler.called)
|
||||
})
|
||||
}
|
||||
248
pkg/service/roomallocator.go
Normal file
248
pkg/service/roomallocator.go
Normal file
@@ -0,0 +1,248 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
"github.com/livekit/psrpc"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/routing/selector"
|
||||
)
|
||||
|
||||
type StandardRoomAllocator struct {
|
||||
config *config.Config
|
||||
router routing.Router
|
||||
selector selector.NodeSelector
|
||||
roomStore ObjectStore
|
||||
}
|
||||
|
||||
func NewRoomAllocator(conf *config.Config, router routing.Router, rs ObjectStore) (RoomAllocator, error) {
|
||||
ns, err := selector.CreateNodeSelector(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StandardRoomAllocator{
|
||||
config: conf,
|
||||
router: router,
|
||||
selector: ns,
|
||||
roomStore: rs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *StandardRoomAllocator) AutoCreateEnabled(context.Context) bool {
|
||||
return r.config.Room.AutoCreate
|
||||
}
|
||||
|
||||
// CreateRoom creates a new room from a request and allocates it to a node to handle
|
||||
// it'll also monitor its state, and cleans it up when appropriate
|
||||
func (r *StandardRoomAllocator) CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest, isExplicit bool) (*livekit.Room, *livekit.RoomInternal, bool, error) {
|
||||
token, err := r.roomStore.LockRoom(ctx, livekit.RoomName(req.Name), 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
defer func() {
|
||||
_ = r.roomStore.UnlockRoom(ctx, livekit.RoomName(req.Name), token)
|
||||
}()
|
||||
|
||||
// find existing room and update it
|
||||
var created bool
|
||||
rm, internal, err := r.roomStore.LoadRoom(ctx, livekit.RoomName(req.Name), true)
|
||||
if errors.Is(err, ErrRoomNotFound) {
|
||||
created = true
|
||||
now := time.Now()
|
||||
rm = &livekit.Room{
|
||||
Sid: guid.New(utils.RoomPrefix),
|
||||
Name: req.Name,
|
||||
CreationTime: now.Unix(),
|
||||
CreationTimeMs: now.UnixMilli(),
|
||||
TurnPassword: utils.RandomSecret(),
|
||||
}
|
||||
internal = &livekit.RoomInternal{}
|
||||
applyDefaultRoomConfig(rm, internal, &r.config.Room)
|
||||
} else if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
req, err = r.applyNamedRoomConfiguration(req)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
if req.EmptyTimeout > 0 {
|
||||
rm.EmptyTimeout = req.EmptyTimeout
|
||||
}
|
||||
if req.DepartureTimeout > 0 {
|
||||
rm.DepartureTimeout = req.DepartureTimeout
|
||||
}
|
||||
if req.MaxParticipants > 0 {
|
||||
rm.MaxParticipants = req.MaxParticipants
|
||||
}
|
||||
if req.Metadata != "" {
|
||||
rm.Metadata = req.Metadata
|
||||
}
|
||||
if req.Egress != nil {
|
||||
if req.Egress.Participant != nil {
|
||||
internal.ParticipantEgress = req.Egress.Participant
|
||||
}
|
||||
if req.Egress.Tracks != nil {
|
||||
internal.TrackEgress = req.Egress.Tracks
|
||||
}
|
||||
}
|
||||
if req.Agents != nil {
|
||||
internal.AgentDispatches = req.Agents
|
||||
}
|
||||
if req.MinPlayoutDelay > 0 || req.MaxPlayoutDelay > 0 {
|
||||
internal.PlayoutDelay = &livekit.PlayoutDelay{
|
||||
Enabled: true,
|
||||
Min: req.MinPlayoutDelay,
|
||||
Max: req.MaxPlayoutDelay,
|
||||
}
|
||||
}
|
||||
if req.SyncStreams {
|
||||
internal.SyncStreams = true
|
||||
}
|
||||
|
||||
if err = r.roomStore.StoreRoom(ctx, rm, internal); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
return rm, internal, created, nil
|
||||
}
|
||||
|
||||
func (r *StandardRoomAllocator) SelectRoomNode(ctx context.Context, roomName livekit.RoomName, nodeID livekit.NodeID) error {
|
||||
// check if room already assigned
|
||||
existing, err := r.router.GetNodeForRoom(ctx, roomName)
|
||||
if !errors.Is(err, routing.ErrNotFound) && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if already assigned and still available, keep it on that node
|
||||
if err == nil && selector.IsAvailable(existing) {
|
||||
// if node hosting the room is full, deny entry
|
||||
if selector.LimitsReached(r.config.Limit, existing.Stats) {
|
||||
return routing.ErrNodeLimitReached
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// select a new node
|
||||
if nodeID == "" {
|
||||
nodes, err := r.router.ListNodes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node, err := r.selector.SelectNode(nodes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodeID = livekit.NodeID(node.Id)
|
||||
}
|
||||
|
||||
logger.Infow("selected node for room", "room", roomName, "selectedNodeID", nodeID)
|
||||
err = r.router.SetNodeForRoom(ctx, roomName, nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 && EnsureCreatePermission(ctx) != nil {
|
||||
_, _, err := r.roomStore.LoadRoom(ctx, roomName, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyDefaultRoomConfig(room *livekit.Room, internal *livekit.RoomInternal, conf *config.RoomConfig) {
|
||||
room.EmptyTimeout = conf.EmptyTimeout
|
||||
room.DepartureTimeout = conf.DepartureTimeout
|
||||
room.MaxParticipants = conf.MaxParticipants
|
||||
for _, codec := range conf.EnabledCodecs {
|
||||
room.EnabledCodecs = append(room.EnabledCodecs, &livekit.Codec{
|
||||
Mime: codec.Mime,
|
||||
FmtpLine: codec.FmtpLine,
|
||||
})
|
||||
}
|
||||
internal.PlayoutDelay = &livekit.PlayoutDelay{
|
||||
Enabled: conf.PlayoutDelay.Enabled,
|
||||
Min: uint32(conf.PlayoutDelay.Min),
|
||||
Max: uint32(conf.PlayoutDelay.Max),
|
||||
}
|
||||
internal.SyncStreams = conf.SyncStreams
|
||||
}
|
||||
|
||||
func (r *StandardRoomAllocator) applyNamedRoomConfiguration(req *livekit.CreateRoomRequest) (*livekit.CreateRoomRequest, error) {
|
||||
if req.RoomPreset == "" {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
conf, ok := r.config.Room.RoomConfigurations[req.RoomPreset]
|
||||
if !ok {
|
||||
return req, psrpc.NewErrorf(psrpc.InvalidArgument, "unknown room configuration in create room request")
|
||||
}
|
||||
|
||||
clone := utils.CloneProto(req)
|
||||
|
||||
if clone.EmptyTimeout == 0 {
|
||||
clone.EmptyTimeout = conf.EmptyTimeout
|
||||
}
|
||||
if clone.DepartureTimeout == 0 {
|
||||
clone.DepartureTimeout = conf.DepartureTimeout
|
||||
}
|
||||
if clone.MaxParticipants == 0 {
|
||||
clone.MaxParticipants = conf.MaxParticipants
|
||||
}
|
||||
if clone.Egress == nil {
|
||||
clone.Egress = utils.CloneProto(conf.Egress)
|
||||
}
|
||||
if clone.Agents == nil {
|
||||
clone.Agents = make([]*livekit.RoomAgentDispatch, 0, len(conf.Agents))
|
||||
for _, agent := range conf.Agents {
|
||||
clone.Agents = append(clone.Agents, utils.CloneProto(agent))
|
||||
}
|
||||
}
|
||||
if clone.MinPlayoutDelay == 0 {
|
||||
clone.MinPlayoutDelay = conf.MinPlayoutDelay
|
||||
}
|
||||
if clone.MaxPlayoutDelay == 0 {
|
||||
clone.MaxPlayoutDelay = conf.MaxPlayoutDelay
|
||||
}
|
||||
if !clone.SyncStreams {
|
||||
clone.SyncStreams = conf.SyncStreams
|
||||
}
|
||||
if clone.Metadata == "" {
|
||||
clone.Metadata = conf.Metadata
|
||||
}
|
||||
|
||||
return clone, nil
|
||||
}
|
||||
98
pkg/service/roomallocator_test.go
Normal file
98
pkg/service/roomallocator_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/routing/routingfakes"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/livekit-server/pkg/service/servicefakes"
|
||||
)
|
||||
|
||||
func TestCreateRoom(t *testing.T) {
|
||||
t.Run("ensure default room settings are applied", func(t *testing.T) {
|
||||
conf, err := config.NewConfig("", true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
node, err := routing.NewLocalNode(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
ra, conf := newTestRoomAllocator(t, conf, node.Clone())
|
||||
|
||||
room, _, _, err := ra.CreateRoom(context.Background(), &livekit.CreateRoomRequest{Name: "myroom"}, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, conf.Room.EmptyTimeout, room.EmptyTimeout)
|
||||
require.Equal(t, conf.Room.DepartureTimeout, room.DepartureTimeout)
|
||||
require.NotEmpty(t, room.EnabledCodecs)
|
||||
})
|
||||
}
|
||||
|
||||
func SelectRoomNode(t *testing.T) {
|
||||
t.Run("reject new participants when track limit has been reached", func(t *testing.T) {
|
||||
conf, err := config.NewConfig("", true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
conf.Limit.NumTracks = 10
|
||||
|
||||
node, err := routing.NewLocalNode(conf)
|
||||
require.NoError(t, err)
|
||||
node.SetStats(&livekit.NodeStats{
|
||||
NumTracksIn: 100,
|
||||
NumTracksOut: 100,
|
||||
})
|
||||
|
||||
ra, _ := newTestRoomAllocator(t, conf, node.Clone())
|
||||
|
||||
err = ra.SelectRoomNode(context.Background(), "low-limit-room", "")
|
||||
require.ErrorIs(t, err, routing.ErrNodeLimitReached)
|
||||
})
|
||||
|
||||
t.Run("reject new participants when bandwidth limit has been reached", func(t *testing.T) {
|
||||
conf, err := config.NewConfig("", true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
conf.Limit.BytesPerSec = 100
|
||||
|
||||
node, err := routing.NewLocalNode(conf)
|
||||
require.NoError(t, err)
|
||||
node.SetStats(&livekit.NodeStats{
|
||||
BytesInPerSec: 1000,
|
||||
BytesOutPerSec: 1000,
|
||||
})
|
||||
|
||||
ra, _ := newTestRoomAllocator(t, conf, node.Clone())
|
||||
|
||||
err = ra.SelectRoomNode(context.Background(), "low-limit-room", "")
|
||||
require.ErrorIs(t, err, routing.ErrNodeLimitReached)
|
||||
})
|
||||
}
|
||||
|
||||
func newTestRoomAllocator(t *testing.T, conf *config.Config, node *livekit.Node) (service.RoomAllocator, *config.Config) {
|
||||
store := &servicefakes.FakeObjectStore{}
|
||||
store.LoadRoomReturns(nil, nil, service.ErrRoomNotFound)
|
||||
router := &routingfakes.FakeRouter{}
|
||||
|
||||
router.GetNodeForRoomReturns(node, nil)
|
||||
|
||||
ra, err := service.NewRoomAllocator(conf, router, store)
|
||||
require.NoError(t, err)
|
||||
return ra, conf
|
||||
}
|
||||
1246
pkg/service/roommanager.go
Normal file
1246
pkg/service/roommanager.go
Normal file
File diff suppressed because it is too large
Load Diff
375
pkg/service/roommanager_service.go
Normal file
375
pkg/service/roommanager_service.go
Normal file
@@ -0,0 +1,375 @@
|
||||
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"
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/livekit-server/pkg/sfu/rtpstats"
|
||||
"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"
|
||||
)
|
||||
|
||||
// whipSessionNotifyInterval is a var (rather than a const) so tests can shorten it.
|
||||
var 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) || errors.Is(err, ErrParticipantNotFound) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s whipService) sendConnectionNotify(ctx context.Context, participant types.Participant) error {
|
||||
if participant.IsClosed() {
|
||||
return ErrParticipantNotFound
|
||||
}
|
||||
|
||||
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) {
|
||||
var video *livekit.InputVideoState
|
||||
var audio *livekit.InputAudioState
|
||||
|
||||
for _, t := range participant.GetPublishedTracks() {
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ti := t.ToProto()
|
||||
if ti == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case livekit.TrackType_VIDEO:
|
||||
if video != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
video = &livekit.InputVideoState{
|
||||
MimeType: ti.MimeType,
|
||||
Width: ti.Width,
|
||||
Height: ti.Height,
|
||||
AverageBitrate: trackAverageBitrate(t),
|
||||
}
|
||||
|
||||
case livekit.TrackType_AUDIO:
|
||||
if audio != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
channels := uint32(1)
|
||||
if ti.Stereo {
|
||||
channels = 2
|
||||
}
|
||||
|
||||
audio = &livekit.InputAudioState{
|
||||
MimeType: ti.MimeType,
|
||||
Channels: channels,
|
||||
AverageBitrate: trackAverageBitrate(t),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return video, audio
|
||||
}
|
||||
|
||||
func trackAverageBitrate(t types.MediaTrack) uint32 {
|
||||
var allStats []*livekit.RTPStats
|
||||
for _, r := range t.Receivers() {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if s := r.GetTrackStats(); s != nil {
|
||||
allStats = append(allStats, s)
|
||||
}
|
||||
}
|
||||
|
||||
agg := rtpstats.AggregateRTPStats(allStats)
|
||||
if agg == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint32(agg.Bitrate)
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
type whipParticipantService struct {
|
||||
*RoomManager
|
||||
}
|
||||
|
||||
func getWHIPParticipant(
|
||||
room *rtc.Room,
|
||||
identity livekit.ParticipantIdentity,
|
||||
participantID livekit.ParticipantID,
|
||||
) types.LocalParticipant {
|
||||
lp := room.GetParticipant(identity)
|
||||
if lp == nil || lp.ID() != participantID {
|
||||
return nil
|
||||
}
|
||||
return lp
|
||||
}
|
||||
|
||||
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 := getWHIPParticipant(room, livekit.ParticipantIdentity(req.GetParticipantIdentity()), livekit.ParticipantID(req.GetParticipantId()))
|
||||
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 := getWHIPParticipant(room, livekit.ParticipantIdentity(req.GetParticipantIdentity()), livekit.ParticipantID(req.GetParticipantId()))
|
||||
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 := getWHIPParticipant(room, livekit.ParticipantIdentity(req.GetParticipantIdentity()), livekit.ParticipantID(req.GetParticipantId()))
|
||||
if lp != nil {
|
||||
room.RemoveParticipant(
|
||||
lp.Identity(),
|
||||
lp.ID(),
|
||||
types.ParticipantCloseReasonClientRequestLeave,
|
||||
)
|
||||
lp.AddOnClose(types.ParticipantCloseKeyWHIP, nil)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
125
pkg/service/roommanager_service_test.go
Normal file
125
pkg/service/roommanager_service_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/atomic"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/psrpc"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
|
||||
)
|
||||
|
||||
// fakeIngressHandlerClient records WHIPRTCConnectionNotify calls. It embeds the
|
||||
// interface so only the method under test needs to be implemented; any other
|
||||
// call would panic (and we assert none happen).
|
||||
type fakeIngressHandlerClient struct {
|
||||
rpc.IngressHandlerClient
|
||||
notifyCount atomic.Int32
|
||||
}
|
||||
|
||||
func (f *fakeIngressHandlerClient) WHIPRTCConnectionNotify(
|
||||
_ context.Context,
|
||||
_ string,
|
||||
_ *rpc.WHIPRTCConnectionNotifyRequest,
|
||||
_ ...psrpc.RequestOption,
|
||||
) (*emptypb.Empty, error) {
|
||||
f.notifyCount.Inc()
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// TestWhipNotifySessionStopsWhenParticipantLeaves verifies the notifier loop
|
||||
// terminates once the WHIP participant leaves the room (i.e. IsClosed becomes
|
||||
// true), and stops issuing further connection notifications.
|
||||
func TestWhipNotifySessionStopsWhenParticipantLeaves(t *testing.T) {
|
||||
origInterval := whipSessionNotifyInterval
|
||||
whipSessionNotifyInterval = 5 * time.Millisecond
|
||||
t.Cleanup(func() { whipSessionNotifyInterval = origInterval })
|
||||
|
||||
var closed atomic.Bool
|
||||
participant := &typesfakes.FakeParticipant{}
|
||||
participant.IsClosedStub = func() bool { return closed.Load() }
|
||||
participant.IDReturns(livekit.ParticipantID("PA_test"))
|
||||
participant.ToProtoReturns(&livekit.ParticipantInfo{})
|
||||
|
||||
cli := &fakeIngressHandlerClient{}
|
||||
s := whipService{ingressRpcCli: cli}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- s.notifySession(context.Background(), participant)
|
||||
}()
|
||||
|
||||
// while the participant is connected the loop should keep notifying
|
||||
require.Eventually(t, func() bool {
|
||||
return cli.notifyCount.Load() > 0
|
||||
}, time.Second, time.Millisecond, "expected notifications while participant is connected")
|
||||
|
||||
// the participant leaves the room
|
||||
closed.Store(true)
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("notifySession did not stop after the participant left the room")
|
||||
}
|
||||
|
||||
// no further notifications should be attempted after it stops
|
||||
countAtStop := cli.notifyCount.Load()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.Equal(t, countAtStop, cli.notifyCount.Load(), "should not notify after the participant left")
|
||||
}
|
||||
|
||||
// TestWhipNotifySessionStopsOnContextCancel verifies the loop exits when the
|
||||
// aliveCtx (cancelled from the participant's OnClose callback) is done.
|
||||
func TestWhipNotifySessionStopsOnContextCancel(t *testing.T) {
|
||||
origInterval := whipSessionNotifyInterval
|
||||
whipSessionNotifyInterval = 5 * time.Millisecond
|
||||
t.Cleanup(func() { whipSessionNotifyInterval = origInterval })
|
||||
|
||||
participant := &typesfakes.FakeParticipant{}
|
||||
participant.IsClosedReturns(false)
|
||||
participant.IDReturns(livekit.ParticipantID("PA_test"))
|
||||
participant.ToProtoReturns(&livekit.ParticipantInfo{})
|
||||
|
||||
cli := &fakeIngressHandlerClient{}
|
||||
s := whipService{ingressRpcCli: cli}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- s.notifySession(ctx, participant)
|
||||
}()
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("notifySession did not stop after context was cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWhipSendConnectionNotifySkipsClosedParticipant verifies the guard that
|
||||
// short-circuits the RPC (and drives loop termination) for a closed participant.
|
||||
func TestWhipSendConnectionNotifySkipsClosedParticipant(t *testing.T) {
|
||||
participant := &typesfakes.FakeParticipant{}
|
||||
participant.IsClosedReturns(true)
|
||||
participant.IDReturns(livekit.ParticipantID("PA_test"))
|
||||
participant.ToProtoReturns(&livekit.ParticipantInfo{})
|
||||
|
||||
cli := &fakeIngressHandlerClient{}
|
||||
s := whipService{ingressRpcCli: cli}
|
||||
|
||||
err := s.sendConnectionNotify(context.Background(), participant)
|
||||
require.ErrorIs(t, err, ErrParticipantNotFound)
|
||||
require.Zero(t, cli.notifyCount.Load(), "should not issue an RPC for a closed participant")
|
||||
}
|
||||
412
pkg/service/roomservice.go
Normal file
412
pkg/service/roomservice.go
Normal file
@@ -0,0 +1,412 @@
|
||||
// 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"
|
||||
"strconv"
|
||||
|
||||
"github.com/twitchtv/twirp"
|
||||
|
||||
"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/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/psrpc"
|
||||
)
|
||||
|
||||
type RoomService struct {
|
||||
limitConf config.LimitConfig
|
||||
apiConf config.APIConfig
|
||||
router routing.MessageRouter
|
||||
roomAllocator RoomAllocator
|
||||
roomStore ServiceStore
|
||||
egressLauncher rtc.EgressLauncher
|
||||
topicFormatter rpc.TopicFormatter
|
||||
roomClient rpc.TypedRoomClient
|
||||
participantClient rpc.TypedParticipantClient
|
||||
|
||||
rpc.UnimplementedRoomServer
|
||||
rpc.UnimplementedParticipantServer
|
||||
}
|
||||
|
||||
func NewRoomService(
|
||||
limitConf config.LimitConfig,
|
||||
apiConf config.APIConfig,
|
||||
router routing.MessageRouter,
|
||||
roomAllocator RoomAllocator,
|
||||
serviceStore ServiceStore,
|
||||
egressLauncher rtc.EgressLauncher,
|
||||
topicFormatter rpc.TopicFormatter,
|
||||
roomClient rpc.TypedRoomClient,
|
||||
participantClient rpc.TypedParticipantClient,
|
||||
) (svc *RoomService, err error) {
|
||||
svc = &RoomService{
|
||||
limitConf: limitConf,
|
||||
apiConf: apiConf,
|
||||
router: router,
|
||||
roomAllocator: roomAllocator,
|
||||
roomStore: serviceStore,
|
||||
egressLauncher: egressLauncher,
|
||||
topicFormatter: topicFormatter,
|
||||
roomClient: roomClient,
|
||||
participantClient: participantClient,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *RoomService) CreateRoom(ctx context.Context, req *livekit.CreateRoomRequest) (*livekit.Room, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
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 {
|
||||
return nil, ErrEgressNotConnected
|
||||
}
|
||||
|
||||
if !s.limitConf.CheckRoomNameLength(req.Name) {
|
||||
return nil, fmt.Errorf("%w: max length %d", ErrRoomNameExceedsLimits, s.limitConf.MaxRoomNameLength)
|
||||
}
|
||||
|
||||
if !s.limitConf.CheckMetadataSize(req.Metadata) {
|
||||
return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize)))
|
||||
}
|
||||
|
||||
for _, ad := range req.Agents {
|
||||
if !s.limitConf.CheckMetadataSize(ad.Metadata) {
|
||||
return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize)))
|
||||
}
|
||||
if !s.limitConf.CheckAttributesSize(ad.Attributes) {
|
||||
return nil, twirp.InvalidArgumentError(ErrAttributeExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxAttributesSize)))
|
||||
}
|
||||
}
|
||||
|
||||
err := s.roomAllocator.SelectRoomNode(ctx, livekit.RoomName(req.Name), livekit.NodeID(req.NodeId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
room, err := s.router.CreateRoom(ctx, req)
|
||||
RecordResponse(ctx, room)
|
||||
return room, err
|
||||
}
|
||||
|
||||
func (s *RoomService) ListRooms(ctx context.Context, req *livekit.ListRoomsRequest) (*livekit.ListRoomsResponse, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Names)
|
||||
err := EnsureListPermission(ctx)
|
||||
if err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
var names []livekit.RoomName
|
||||
if len(req.Names) > 0 {
|
||||
names = livekit.StringsAsIDs[livekit.RoomName](req.Names)
|
||||
}
|
||||
rooms, err := s.roomStore.ListRooms(ctx, names)
|
||||
if err != nil {
|
||||
// TODO: translate error codes to Twirp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &livekit.ListRoomsResponse{
|
||||
Rooms: rooms,
|
||||
}
|
||||
RecordResponse(ctx, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *RoomService) DeleteRoom(ctx context.Context, req *livekit.DeleteRoomRequest) (*livekit.DeleteRoomResponse, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Room)
|
||||
if err := EnsureCreatePermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
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
|
||||
room, err := s.router.CreateRoom(ctx, &livekit.CreateRoomRequest{Name: req.Room})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = s.roomClient.DeleteRoom(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if os, ok := s.roomStore.(OSSServiceStore); ok {
|
||||
err = os.DeleteRoom(ctx, livekit.RoomName(req.Room))
|
||||
}
|
||||
res := &livekit.DeleteRoomResponse{}
|
||||
RecordResponse(ctx, room)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *RoomService) ListParticipants(ctx context.Context, req *livekit.ListParticipantsRequest) (res *livekit.ListParticipantsResponse, err error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Room)
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
RecordResponse(ctx, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
RecordResponse(ctx, participant)
|
||||
return participant, nil
|
||||
}
|
||||
|
||||
func (s *RoomService) RemoveParticipant(ctx context.Context, req *livekit.RoomParticipantIdentity) (*livekit.RemoveParticipantResponse, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity)
|
||||
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
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.RemoveParticipant(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
|
||||
RecordResponse(ctx, res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *RoomService) MutePublishedTrack(ctx context.Context, req *livekit.MuteRoomTrackRequest) (*livekit.MuteRoomTrackResponse, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity, "trackID", req.TrackSid, "muted", req.Muted)
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
res, err := s.participantClient.MutePublishedTrack(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
|
||||
RecordResponse(ctx, res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *RoomService) UpdateParticipant(ctx context.Context, req *livekit.UpdateParticipantRequest) (*livekit.ParticipantInfo, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity)
|
||||
|
||||
if !s.limitConf.CheckParticipantNameLength(req.Name) {
|
||||
return nil, twirp.InvalidArgumentError(ErrNameExceedsLimits.Error(), strconv.Itoa(s.limitConf.MaxParticipantNameLength))
|
||||
}
|
||||
|
||||
if !s.limitConf.CheckMetadataSize(req.Metadata) {
|
||||
return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize)))
|
||||
}
|
||||
|
||||
if !s.limitConf.CheckAttributesSize(req.Attributes) {
|
||||
return nil, twirp.InvalidArgumentError(ErrAttributeExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxAttributesSize)))
|
||||
}
|
||||
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *RoomService) UpdateSubscriptions(ctx context.Context, req *livekit.UpdateSubscriptionsRequest) (*livekit.UpdateSubscriptionsResponse, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
trackSIDs := append(make([]string, 0), req.TrackSids...)
|
||||
for _, pt := range req.ParticipantTracks {
|
||||
trackSIDs = append(trackSIDs, pt.TrackSids...)
|
||||
}
|
||||
AppendLogFields(ctx, "room", req.Room, "participant", req.Identity, "trackID", trackSIDs)
|
||||
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
res, err := s.participantClient.UpdateSubscriptions(ctx, s.topicFormatter.ParticipantTopic(ctx, livekit.RoomName(req.Room), livekit.ParticipantIdentity(req.Identity)), req)
|
||||
RecordResponse(ctx, res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *RoomService) SendData(ctx context.Context, req *livekit.SendDataRequest) (*livekit.SendDataResponse, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
roomName := livekit.RoomName(req.Room)
|
||||
AppendLogFields(ctx, "room", roomName, "size", len(req.Data))
|
||||
if err := EnsureAdminPermission(ctx, roomName); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
// nonce is either absent or 128-bit UUID
|
||||
if len(req.Nonce) != 0 && len(req.Nonce) != 16 {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, fmt.Sprintf("nonce should be 16-bytes or not present, got: %d bytes", len(req.Nonce)))
|
||||
}
|
||||
|
||||
res, err := s.roomClient.SendData(ctx, s.topicFormatter.RoomTopic(ctx, livekit.RoomName(req.Room)), req)
|
||||
RecordResponse(ctx, res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *RoomService) UpdateRoomMetadata(ctx context.Context, req *livekit.UpdateRoomMetadataRequest) (*livekit.Room, error) {
|
||||
RecordRequest(ctx, req)
|
||||
|
||||
AppendLogFields(ctx, "room", req.Room, "size", len(req.Metadata))
|
||||
if !s.limitConf.CheckMetadataSize(req.Metadata) {
|
||||
return nil, twirp.InvalidArgumentError(ErrMetadataExceedsLimits.Error(), strconv.Itoa(int(s.limitConf.MaxMetadataSize)))
|
||||
}
|
||||
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.Room)); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
RecordResponse(ctx, room)
|
||||
return room, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if req.Room == req.DestinationRoom {
|
||||
return nil, twirp.InvalidArgumentError(ErrDestinationSameAsSourceRoom.Error(), "")
|
||||
}
|
||||
|
||||
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 (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)
|
||||
}
|
||||
|
||||
if req.Room == req.DestinationRoom {
|
||||
return nil, twirp.InvalidArgumentError(ErrDestinationSameAsSourceRoom.Error(), "")
|
||||
}
|
||||
|
||||
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 (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
|
||||
}
|
||||
|
||||
res, err := s.participantClient.PerformRpc(ctx, s.topicFormatter.ParticipantTopic(ctx, roomName, livekit.ParticipantIdentity(req.DestinationIdentity)), req)
|
||||
RecordResponse(ctx, res)
|
||||
return res, err
|
||||
}
|
||||
209
pkg/service/roomservice_test.go
Normal file
209
pkg/service/roomservice_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/twitchtv/twirp"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/rpc/rpcfakes"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing/routingfakes"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/livekit-server/pkg/service/servicefakes"
|
||||
)
|
||||
|
||||
func TestDeleteRoom(t *testing.T) {
|
||||
t.Run("missing permissions", func(t *testing.T) {
|
||||
svc := newTestRoomService(config.LimitConfig{})
|
||||
grant := &auth.ClaimGrants{
|
||||
Video: &auth.VideoGrant{},
|
||||
}
|
||||
ctx := service.WithGrants(context.Background(), grant, "")
|
||||
_, err := svc.DeleteRoom(ctx, &livekit.DeleteRoomRequest{
|
||||
Room: "testroom",
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetaDataLimits(t *testing.T) {
|
||||
adminCtx := func() context.Context {
|
||||
return service.WithGrants(context.Background(), &auth.ClaimGrants{Video: &auth.VideoGrant{}}, "")
|
||||
}
|
||||
createCtx := func() context.Context {
|
||||
return service.WithGrants(context.Background(), &auth.ClaimGrants{Video: &auth.VideoGrant{RoomCreate: true}}, "")
|
||||
}
|
||||
requireInvalidArg := func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
terr, ok := err.(twirp.Error)
|
||||
require.True(t, ok, "expected twirp error, got %T (%v)", err, err)
|
||||
require.Equal(t, twirp.InvalidArgument, terr.Code())
|
||||
}
|
||||
|
||||
t.Run("metadata exceeds limit", func(t *testing.T) {
|
||||
svc := newTestRoomService(config.LimitConfig{MaxMetadataSize: 5})
|
||||
|
||||
_, err := svc.UpdateParticipant(adminCtx(), &livekit.UpdateParticipantRequest{
|
||||
Room: "testroom",
|
||||
Identity: "123",
|
||||
Metadata: "abcdefg",
|
||||
})
|
||||
requireInvalidArg(t, err)
|
||||
|
||||
_, err = svc.UpdateRoomMetadata(adminCtx(), &livekit.UpdateRoomMetadataRequest{
|
||||
Room: "testroom",
|
||||
Metadata: "abcdefg",
|
||||
})
|
||||
requireInvalidArg(t, err)
|
||||
|
||||
_, err = svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{
|
||||
Name: "testroom",
|
||||
Metadata: "abcdefg",
|
||||
})
|
||||
requireInvalidArg(t, err)
|
||||
|
||||
_, err = svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{
|
||||
Name: "testroom",
|
||||
Agents: []*livekit.RoomAgentDispatch{
|
||||
{AgentName: "bot", Metadata: "abcdefg"},
|
||||
},
|
||||
})
|
||||
requireInvalidArg(t, err)
|
||||
})
|
||||
|
||||
t.Run("embedded agent dispatch in CreateRoom exceeds attributes limit", func(t *testing.T) {
|
||||
svc := newTestRoomService(config.LimitConfig{MaxAttributesSize: 5})
|
||||
_, err := svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{
|
||||
Name: "testroom",
|
||||
Agents: []*livekit.RoomAgentDispatch{
|
||||
{AgentName: "bot", Attributes: map[string]string{"key": "abcdefg"}},
|
||||
},
|
||||
})
|
||||
requireInvalidArg(t, err)
|
||||
})
|
||||
|
||||
notExceedsLimitsSvc := map[string]*TestRoomService{
|
||||
"metadata exceeds limits": newTestRoomService(config.LimitConfig{
|
||||
MaxMetadataSize: 5,
|
||||
MaxAttributesSize: 5,
|
||||
}),
|
||||
"metadata no limits": newTestRoomService(config.LimitConfig{}),
|
||||
}
|
||||
|
||||
for n, svc := range notExceedsLimitsSvc {
|
||||
t.Run(n, func(t *testing.T) {
|
||||
_, err := svc.UpdateParticipant(adminCtx(), &livekit.UpdateParticipantRequest{
|
||||
Room: "testroom",
|
||||
Identity: "123",
|
||||
Metadata: "abc",
|
||||
})
|
||||
terr, ok := err.(twirp.Error)
|
||||
require.True(t, ok)
|
||||
require.NotEqual(t, twirp.InvalidArgument, terr.Code())
|
||||
|
||||
_, err = svc.UpdateRoomMetadata(adminCtx(), &livekit.UpdateRoomMetadataRequest{
|
||||
Room: "testroom",
|
||||
Metadata: "abc",
|
||||
})
|
||||
terr, ok = err.(twirp.Error)
|
||||
require.True(t, ok)
|
||||
require.NotEqual(t, twirp.InvalidArgument, terr.Code())
|
||||
|
||||
_, err = svc.CreateRoom(createCtx(), &livekit.CreateRoomRequest{
|
||||
Name: "testroom",
|
||||
Metadata: "abc",
|
||||
Agents: []*livekit.RoomAgentDispatch{
|
||||
{AgentName: "bot", Metadata: "abc", Attributes: map[string]string{"k": "v"}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
terr, ok = err.(twirp.Error)
|
||||
require.True(t, ok)
|
||||
require.NotEqual(t, twirp.InvalidArgument, terr.Code())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentDispatchMetadataLimits(t *testing.T) {
|
||||
ctx := service.WithGrants(context.Background(), &auth.ClaimGrants{
|
||||
Video: &auth.VideoGrant{Room: "testroom", RoomAdmin: true},
|
||||
}, "")
|
||||
|
||||
t.Run("metadata exceeds limits", func(t *testing.T) {
|
||||
svc := newTestAgentDispatchService(config.LimitConfig{MaxMetadataSize: 5})
|
||||
_, err := svc.CreateDispatch(ctx, &livekit.CreateAgentDispatchRequest{
|
||||
Room: "testroom",
|
||||
Metadata: "abcdefg",
|
||||
})
|
||||
require.ErrorIs(t, err, service.ErrMetadataExceedsLimits)
|
||||
})
|
||||
|
||||
t.Run("attributes exceeds limits", func(t *testing.T) {
|
||||
svc := newTestAgentDispatchService(config.LimitConfig{MaxAttributesSize: 5})
|
||||
_, err := svc.CreateDispatch(ctx, &livekit.CreateAgentDispatchRequest{
|
||||
Room: "testroom",
|
||||
Attributes: map[string]string{"key": "abcdefg"},
|
||||
})
|
||||
require.ErrorIs(t, err, service.ErrAttributeExceedsLimits)
|
||||
})
|
||||
}
|
||||
|
||||
func newTestAgentDispatchService(limitConf config.LimitConfig) *service.AgentDispatchService {
|
||||
allocator := &servicefakes.FakeRoomAllocator{}
|
||||
allocator.AutoCreateEnabledReturns(false)
|
||||
return service.NewAgentDispatchService(limitConf, nil, rpc.NewTopicFormatter(), allocator, &routingfakes.FakeRouter{})
|
||||
}
|
||||
|
||||
func newTestRoomService(limitConf config.LimitConfig) *TestRoomService {
|
||||
router := &routingfakes.FakeRouter{}
|
||||
allocator := &servicefakes.FakeRoomAllocator{}
|
||||
store := &servicefakes.FakeServiceStore{}
|
||||
svc, err := service.NewRoomService(
|
||||
limitConf,
|
||||
config.APIConfig{ExecutionTimeout: 2},
|
||||
router,
|
||||
allocator,
|
||||
store,
|
||||
nil,
|
||||
rpc.NewTopicFormatter(),
|
||||
&rpcfakes.FakeTypedRoomClient{},
|
||||
&rpcfakes.FakeTypedParticipantClient{},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &TestRoomService{
|
||||
RoomService: *svc,
|
||||
router: router,
|
||||
allocator: allocator,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
type TestRoomService struct {
|
||||
service.RoomService
|
||||
router *routingfakes.FakeRouter
|
||||
allocator *servicefakes.FakeRoomAllocator
|
||||
store *servicefakes.FakeServiceStore
|
||||
}
|
||||
797
pkg/service/rtcservice.go
Normal file
797
pkg/service/rtcservice.go
Normal file
@@ -0,0 +1,797 @@
|
||||
// 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"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"go.uber.org/atomic"
|
||||
"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/rtc"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// how long the response source is drained after the request direction is gone,
|
||||
// applies only when the source is not closed by the relay, i. e. when there is
|
||||
// no way to tell that everything pending has been read
|
||||
responseFlushTimeout = 250 * time.Millisecond
|
||||
|
||||
// how long the response pump is given to stop, a bit more than the drain deadline
|
||||
// so that it can finish the write it is in when that deadline expires
|
||||
responsePumpDoneTimeout = 2 * responseFlushTimeout
|
||||
)
|
||||
|
||||
type RTCService struct {
|
||||
router routing.MessageRouter
|
||||
roomAllocator RoomAllocator
|
||||
upgrader websocket.Upgrader
|
||||
config *config.Config
|
||||
isDev bool
|
||||
limits config.LimitConfig
|
||||
telemetry telemetry.TelemetryService
|
||||
|
||||
mu sync.Mutex
|
||||
connections map[*websocket.Conn]struct{}
|
||||
}
|
||||
|
||||
func NewRTCService(
|
||||
conf *config.Config,
|
||||
ra RoomAllocator,
|
||||
router routing.MessageRouter,
|
||||
telemetry telemetry.TelemetryService,
|
||||
) *RTCService {
|
||||
s := &RTCService{
|
||||
router: router,
|
||||
roomAllocator: ra,
|
||||
config: conf,
|
||||
isDev: conf.Development,
|
||||
limits: conf.Limit,
|
||||
telemetry: telemetry,
|
||||
connections: map[*websocket.Conn]struct{}{},
|
||||
}
|
||||
|
||||
s.upgrader = websocket.Upgrader{
|
||||
EnableCompression: true,
|
||||
|
||||
// allow connections from any origin, since script may be hosted anywhere
|
||||
// security is enforced by access tokens
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *RTCService) SetupRoutes(mux *http.ServeMux) {
|
||||
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) 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)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte("success"))
|
||||
}
|
||||
|
||||
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 {
|
||||
HandleError(w, r, code, err)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte("success"))
|
||||
}
|
||||
|
||||
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,
|
||||
TokenExpiresAt: res.tokenExpiresAt,
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
prometheus.IncrementParticipantJoinValidationFail(1)
|
||||
resolveLogger(true)
|
||||
HandleError(w, r, code, err, getLoggerFields()...)
|
||||
return
|
||||
}
|
||||
|
||||
participantIdentity = pi.Identity
|
||||
if pi.ID != "" {
|
||||
pID = pi.ID
|
||||
}
|
||||
pLogger.Debugw("join request validated", append(getLoggerFields(), "participantInit", &pi)...)
|
||||
|
||||
// give it a few attempts to start session
|
||||
var cr connectionResult
|
||||
var initialResponse *livekit.SignalResponse
|
||||
for attempt := 0; attempt < s.config.SignalRelay.ConnectAttempts; attempt++ {
|
||||
connectionTimeout := time.Duration(3+attempt) * time.Second
|
||||
ctx := utils.ContextWithAttempt(r.Context(), attempt)
|
||||
cr, initialResponse, err = s.startConnection(ctx, roomName, pi, connectionTimeout)
|
||||
if err == nil || errors.Is(err, context.Canceled) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
prometheus.IncrementParticipantJoinFail(1)
|
||||
status := http.StatusInternalServerError
|
||||
var psrpcErr psrpc.Error
|
||||
if errors.As(err, &psrpcErr) {
|
||||
status = psrpcErr.ToHttp()
|
||||
}
|
||||
resolveLogger(true)
|
||||
HandleError(w, r, status, err, getLoggerFields()...)
|
||||
return
|
||||
}
|
||||
|
||||
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 := rtc.NewBytesSignalStats(r.Context(), s.telemetry)
|
||||
if join := initialResponse.GetJoin(); join != nil {
|
||||
signalStats.ResolveRoom(join.GetRoom())
|
||||
signalStats.ResolveParticipant(join.GetParticipant())
|
||||
}
|
||||
if pi.Reconnect && pi.ID != "" {
|
||||
signalStats.ResolveParticipant(&livekit.ParticipantInfo{
|
||||
Sid: string(pi.ID),
|
||||
Identity: string(pi.Identity),
|
||||
})
|
||||
}
|
||||
|
||||
closedByClient := atomic.NewBool(false)
|
||||
done := make(chan struct{})
|
||||
// closed by the response pump when it has stopped writing to the web socket
|
||||
responsePumpDone := make(chan struct{})
|
||||
responsePumpStarted := false
|
||||
var sigConn *WSSignalConnection
|
||||
// function exits when websocket terminates, it'll close the event reading off of request sink and response source as well
|
||||
defer func() {
|
||||
resolveLogger(true)
|
||||
pLogger.Debugw("finishing WS connection", "closedByClient", closedByClient.Load())
|
||||
|
||||
// signal the response pump before anything else so that it can flush responses
|
||||
// the participant queued on its way out, a leave request sent just before the
|
||||
// signalling connection was closed (on migration for example) is dropped otherwise
|
||||
close(done)
|
||||
cr.RequestSink.Close()
|
||||
if responsePumpStarted {
|
||||
select {
|
||||
case <-responsePumpDone:
|
||||
case <-time.After(responsePumpDoneTimeout):
|
||||
pLogger.Debugw("timed out waiting for response pump to finish")
|
||||
}
|
||||
}
|
||||
cr.ResponseSource.Close()
|
||||
|
||||
// close the web socket on all paths, even when the response pump is wedged
|
||||
// writing to an unresponsive client
|
||||
if sigConn != nil {
|
||||
sigConn.CloseWithReason("")
|
||||
}
|
||||
|
||||
signalStats.Stop()
|
||||
}()
|
||||
|
||||
// upgrade only once the basics are good to go
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
prometheus.IncrementParticipantJoinUpgradeFail(1)
|
||||
resolveLogger(true)
|
||||
HandleError(w, r, http.StatusInternalServerError, err, getLoggerFields()...)
|
||||
return
|
||||
}
|
||||
// bound the size of a single signalling frame so an oversized message is
|
||||
// rejected by the transport before being fully buffered in memory. This limits
|
||||
// the compressed bytes read off the wire; the decompressed size is bounded
|
||||
// separately in WSSignalConnection below.
|
||||
if s.limits.SignalMessageSizeLimit > 0 {
|
||||
conn.SetReadLimit(s.limits.SignalMessageSizeLimit)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.connections[conn] = struct{}{}
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.connections, conn)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// websocket established
|
||||
sigConn = NewWSSignalConnection(conn, s.limits.SignalMessageSizeLimit)
|
||||
pLogger.Debugw("sending initial response", "response", logger.Proto(initialResponse))
|
||||
count, err := sigConn.WriteResponse(initialResponse)
|
||||
if err != nil {
|
||||
prometheus.IncrementParticipantJoinWriteInitialResponseFail(1)
|
||||
resolveLogger(true)
|
||||
pLogger.Warnw("could not write initial response", err)
|
||||
return
|
||||
}
|
||||
signalStats.AddBytes(uint64(count), true)
|
||||
|
||||
prometheus.IncrementParticipantJoin(1)
|
||||
joinDuration = time.Since(startedAt)
|
||||
prometheus.RecordSessionJoinLatency(int(pi.Client.GetProtocol()), joinDuration)
|
||||
|
||||
pLogger.Debugw(
|
||||
"new client WS connected",
|
||||
"reconnect", pi.Reconnect,
|
||||
"reconnectReason", pi.ReconnectReason,
|
||||
"adaptiveStream", pi.AdaptiveStream,
|
||||
"selectedNodeID", cr.NodeID,
|
||||
"nodeSelectionReason", cr.NodeSelectionReason,
|
||||
)
|
||||
|
||||
// writes one response from the response source to the web socket,
|
||||
// returns false if the response pump should stop
|
||||
writeResponse := func(msg proto.Message) bool {
|
||||
res, ok := msg.(*livekit.SignalResponse)
|
||||
if !ok {
|
||||
pLogger.Errorw(
|
||||
"unexpected message type", nil,
|
||||
"type", fmt.Sprintf("%T", msg),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
switch m := res.Message.(type) {
|
||||
case *livekit.SignalResponse_Offer:
|
||||
pLogger.Debugw("sending offer", "offer", logger.Proto(res))
|
||||
|
||||
case *livekit.SignalResponse_Answer:
|
||||
pLogger.Debugw("sending answer", "answer", logger.Proto(res))
|
||||
|
||||
case *livekit.SignalResponse_Join:
|
||||
pLogger.Debugw("sending join", "join", logger.Proto(res))
|
||||
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", logger.Proto(res))
|
||||
signalStats.ResolveRoom(m.RoomUpdate.GetRoom())
|
||||
|
||||
case *livekit.SignalResponse_Update:
|
||||
pLogger.Debugw("sending participant update", "participantUpdate", logger.Proto(res))
|
||||
|
||||
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", logger.Proto(res))
|
||||
|
||||
default:
|
||||
pLogger.Debugw("sending signal response", "response", logger.Proto(res))
|
||||
}
|
||||
|
||||
if count, err := sigConn.WriteResponse(res); err != nil {
|
||||
pLogger.Warnw("error writing to websocket", err)
|
||||
return false
|
||||
} else {
|
||||
signalStats.AddBytes(uint64(count), true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// handle responses
|
||||
responsePumpStarted = true
|
||||
go func() {
|
||||
defer func() {
|
||||
close(responsePumpDone)
|
||||
|
||||
// when the source is terminated, this means Participant.Close had been called and RTC connection is done
|
||||
// we would terminate the signal connection as well
|
||||
sigConn.CloseWithReason("")
|
||||
}()
|
||||
defer func() {
|
||||
if r := rtc.Recover(pLogger); r != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
// the request direction is gone, flush what the participant queued on its
|
||||
// way out unless the client is the one that went away
|
||||
if !closedByClient.Load() {
|
||||
if !drainMessageSource(cr.ResponseSource, responseFlushTimeout, writeResponse) {
|
||||
pLogger.Debugw("could not drain response source fully")
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
case msg := <-cr.ResponseSource.ReadChan():
|
||||
if msg == nil {
|
||||
resolveLogger(true)
|
||||
pLogger.Debugw("nothing to read from response source")
|
||||
return
|
||||
}
|
||||
if !writeResponse(msg) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// handle incoming requests from websocket
|
||||
for {
|
||||
req, count, err := sigConn.ReadRequest()
|
||||
if err != nil {
|
||||
if IsWebSocketCloseError(err) {
|
||||
closedByClient.Store(true)
|
||||
} else {
|
||||
pLogger.Errorw("error reading from websocket", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
signalStats.AddBytes(uint64(count), false)
|
||||
|
||||
switch m := req.Message.(type) {
|
||||
case *livekit.SignalRequest_Ping:
|
||||
count, perr := sigConn.WriteResponse(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Pong{
|
||||
//
|
||||
// Although this field is int64, some clients (like JS) cause overflow if nanosecond granularity is used.
|
||||
// So. use UnixMillis().
|
||||
//
|
||||
Pong: time.Now().UnixMilli(),
|
||||
},
|
||||
})
|
||||
if perr == nil {
|
||||
signalStats.AddBytes(uint64(count), true)
|
||||
}
|
||||
case *livekit.SignalRequest_PingReq:
|
||||
count, perr := sigConn.WriteResponse(&livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_PongResp{
|
||||
PongResp: &livekit.Pong{
|
||||
LastPingTimestamp: m.PingReq.Timestamp,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if perr == nil {
|
||||
signalStats.AddBytes(uint64(count), true)
|
||||
}
|
||||
}
|
||||
|
||||
switch req.Message.(type) {
|
||||
case *livekit.SignalRequest_Offer:
|
||||
pLogger.Debugw("received offer", "offer", logger.Proto(req))
|
||||
case *livekit.SignalRequest_Answer:
|
||||
pLogger.Debugw("received answer", "answer", logger.Proto(req))
|
||||
default:
|
||||
pLogger.Debugw("received signal request", "request", logger.Proto(req))
|
||||
}
|
||||
|
||||
if err := cr.RequestSink.WriteMessage(req); err != nil {
|
||||
pLogger.Warnw("error writing to request sink", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drainMessageSource writes messages that are still queued in source using write.
|
||||
// It is used when tearing a signalling connection down, responses the participant queued
|
||||
// on its way out, a leave request on migration for example, would be dropped otherwise.
|
||||
//
|
||||
// The producer writes all pending messages into the source before closing it, so draining
|
||||
// till the source is closed is a complete flush. The deadline is a backstop for the cases
|
||||
// where the source is not closed, i. e. when there is no way to tell that everything
|
||||
// pending has been read. Returns true if the source was drained fully.
|
||||
func drainMessageSource(source routing.MessageSource, timeout time.Duration, write func(proto.Message) bool) bool {
|
||||
deadline := time.NewTimer(timeout)
|
||||
defer deadline.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-source.ReadChan():
|
||||
if msg == nil {
|
||||
return true
|
||||
}
|
||||
if !write(msg) {
|
||||
return false
|
||||
}
|
||||
|
||||
case <-deadline.C:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RTCService) DrainConnections(interval time.Duration, force bool) {
|
||||
s.mu.Lock()
|
||||
conns := maps.Clone(s.connections)
|
||||
s.mu.Unlock()
|
||||
|
||||
if !force {
|
||||
// jitter drain start
|
||||
time.Sleep(time.Duration(rand.Int63n(int64(interval))))
|
||||
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
|
||||
for c := range conns {
|
||||
_ = c.Close()
|
||||
<-t.C
|
||||
}
|
||||
} else {
|
||||
// drain as quickly as possible when forced
|
||||
for c := range conns {
|
||||
_ = c.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type connectionResult struct {
|
||||
routing.StartParticipantSignalResults
|
||||
Room *livekit.Room
|
||||
}
|
||||
|
||||
func (s *RTCService) startConnection(
|
||||
ctx context.Context,
|
||||
roomName livekit.RoomName,
|
||||
pi routing.ParticipantInit,
|
||||
timeout time.Duration,
|
||||
) (connectionResult, *livekit.SignalResponse, error) {
|
||||
var cr connectionResult
|
||||
var err error
|
||||
|
||||
if err := s.roomAllocator.SelectRoomNode(ctx, roomName, ""); err != nil {
|
||||
return cr, nil, err
|
||||
}
|
||||
|
||||
// this needs to be started first *before* using router functions on this node
|
||||
cr.StartParticipantSignalResults, err = s.router.StartParticipantSignal(ctx, roomName, pi)
|
||||
if err != nil {
|
||||
return cr, nil, err
|
||||
}
|
||||
|
||||
// wait for the first message before upgrading to websocket. If no one is
|
||||
// responding to our connection attempt, we should terminate the connection
|
||||
// instead of waiting forever on the WebSocket
|
||||
initialResponse, err := readInitialResponse(cr.ResponseSource, timeout)
|
||||
if err != nil {
|
||||
// close the connection to avoid leaking
|
||||
cr.RequestSink.Close()
|
||||
cr.ResponseSource.Close()
|
||||
return cr, nil, err
|
||||
}
|
||||
|
||||
return cr, initialResponse, nil
|
||||
}
|
||||
|
||||
func readInitialResponse(source routing.MessageSource, timeout time.Duration) (*livekit.SignalResponse, error) {
|
||||
responseTimer := time.NewTimer(timeout)
|
||||
defer responseTimer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-responseTimer.C:
|
||||
return nil, errors.New("timed out while waiting for signal response")
|
||||
case msg := <-source.ReadChan():
|
||||
if msg == nil {
|
||||
return nil, errors.New("connection closed by media")
|
||||
}
|
||||
res, ok := msg.(*livekit.SignalResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected message type: %T", msg)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
109
pkg/service/rtcservice_test.go
Normal file
109
pkg/service/rtcservice_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
)
|
||||
|
||||
func leaveResponse(action livekit.LeaveRequest_Action) *livekit.SignalResponse {
|
||||
return &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Leave{
|
||||
Leave: &livekit.LeaveRequest{
|
||||
Action: action,
|
||||
Reason: livekit.DisconnectReason_MIGRATION,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainMessageSource(t *testing.T) {
|
||||
collect := func(msgs *[]proto.Message) func(proto.Message) bool {
|
||||
return func(msg proto.Message) bool {
|
||||
*msgs = append(*msgs, msg)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("drains queued messages of a closed source", func(t *testing.T) {
|
||||
source := routing.NewDefaultMessageChannel("CO_test")
|
||||
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME)))
|
||||
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RECONNECT)))
|
||||
source.Close()
|
||||
|
||||
var got []proto.Message
|
||||
require.True(t, drainMessageSource(source, time.Second, collect(&got)))
|
||||
require.Len(t, got, 2)
|
||||
require.Equal(
|
||||
t,
|
||||
livekit.LeaveRequest_RESUME,
|
||||
got[0].(*livekit.SignalResponse).GetLeave().GetAction(),
|
||||
)
|
||||
require.Equal(
|
||||
t,
|
||||
livekit.LeaveRequest_RECONNECT,
|
||||
got[1].(*livekit.SignalResponse).GetLeave().GetAction(),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("drains messages written while draining", func(t *testing.T) {
|
||||
source := routing.NewDefaultMessageChannel("CO_test")
|
||||
// mimics the relay pushing a message that was still in flight when the
|
||||
// request direction went away, and closing the source right after
|
||||
go func() {
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
_ = source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME))
|
||||
source.Close()
|
||||
}()
|
||||
|
||||
var got []proto.Message
|
||||
require.True(t, drainMessageSource(source, time.Second, collect(&got)))
|
||||
require.Len(t, got, 1)
|
||||
})
|
||||
|
||||
t.Run("gives up on the deadline when the source stays open", func(t *testing.T) {
|
||||
source := routing.NewDefaultMessageChannel("CO_test")
|
||||
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME)))
|
||||
|
||||
var got []proto.Message
|
||||
start := time.Now()
|
||||
require.False(t, drainMessageSource(source, 50*time.Millisecond, collect(&got)))
|
||||
require.GreaterOrEqual(t, time.Since(start), 50*time.Millisecond)
|
||||
// what was queued is still flushed
|
||||
require.Len(t, got, 1)
|
||||
})
|
||||
|
||||
t.Run("stops when the write fails", func(t *testing.T) {
|
||||
source := routing.NewDefaultMessageChannel("CO_test")
|
||||
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RESUME)))
|
||||
require.NoError(t, source.WriteMessage(leaveResponse(livekit.LeaveRequest_RECONNECT)))
|
||||
source.Close()
|
||||
|
||||
var got []proto.Message
|
||||
require.False(t, drainMessageSource(source, time.Second, func(msg proto.Message) bool {
|
||||
got = append(got, msg)
|
||||
return false
|
||||
}))
|
||||
require.Len(t, got, 1)
|
||||
})
|
||||
}
|
||||
450
pkg/service/server.go
Normal file
450
pkg/service/server.go
Normal file
@@ -0,0 +1,450 @@
|
||||
// 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"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
httppprof "net/http/pprof"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pion/turn/v5"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/cors"
|
||||
"github.com/twitchtv/twirp"
|
||||
"github.com/urfave/negroni/v3"
|
||||
"go.uber.org/atomic"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/utils/xtwirp"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/version"
|
||||
)
|
||||
|
||||
type LivekitServer struct {
|
||||
config *config.Config
|
||||
ioService *IOInfoService
|
||||
rtcService *RTCService
|
||||
whipService *WHIPService
|
||||
agentService *AgentService
|
||||
httpServer *http.Server
|
||||
promServer *http.Server
|
||||
debugServer *http.Server
|
||||
router routing.Router
|
||||
roomManager *RoomManager
|
||||
signalServer *SignalServer
|
||||
turnServer *turn.Server
|
||||
currentNode routing.LocalNode
|
||||
running atomic.Bool
|
||||
doneChan chan struct{}
|
||||
closedChan chan struct{}
|
||||
}
|
||||
|
||||
func NewLivekitServer(conf *config.Config,
|
||||
roomService livekit.RoomService,
|
||||
agentDispatchService *AgentDispatchService,
|
||||
egressService *EgressService,
|
||||
ingressService *IngressService,
|
||||
sipService *SIPService,
|
||||
ioService *IOInfoService,
|
||||
rtcService *RTCService,
|
||||
whipService *WHIPService,
|
||||
agentService *AgentService,
|
||||
keyProvider auth.KeyProvider,
|
||||
router routing.Router,
|
||||
roomManager *RoomManager,
|
||||
signalServer *SignalServer,
|
||||
turnServer *turn.Server,
|
||||
currentNode routing.LocalNode,
|
||||
) (s *LivekitServer, err error) {
|
||||
s = &LivekitServer{
|
||||
config: conf,
|
||||
ioService: ioService,
|
||||
rtcService: rtcService,
|
||||
whipService: whipService,
|
||||
agentService: agentService,
|
||||
router: router,
|
||||
roomManager: roomManager,
|
||||
signalServer: signalServer,
|
||||
// turn server starts automatically
|
||||
turnServer: turnServer,
|
||||
currentNode: currentNode,
|
||||
closedChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
middlewares := []negroni.Handler{
|
||||
// always first
|
||||
negroni.NewRecovery(),
|
||||
// CORS is allowed, we rely on token authentication to prevent improper use
|
||||
cors.New(cors.Options{
|
||||
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,
|
||||
}),
|
||||
negroni.HandlerFunc(RemoveDoubleSlashes),
|
||||
// limit request body size so large messages cannot exhaust memory
|
||||
NewRequestBodyLimiter(conf.Limit.MaxAPIRequestBodySize),
|
||||
}
|
||||
if keyProvider != nil {
|
||||
middlewares = append(middlewares, NewAPIKeyAuthMiddleware(keyProvider))
|
||||
}
|
||||
|
||||
serverOptions := []any{
|
||||
twirp.WithServerHooks(twirp.ChainHooks(
|
||||
TwirpLogger(),
|
||||
TwirpEgressID(),
|
||||
TwirpRequestStatusReporter(),
|
||||
)),
|
||||
}
|
||||
for _, opt := range xtwirp.DefaultServerOptions() {
|
||||
serverOptions = append(serverOptions, opt)
|
||||
}
|
||||
roomServer := livekit.NewRoomServiceServer(roomService, serverOptions...)
|
||||
agentDispatchServer := livekit.NewAgentDispatchServiceServer(agentDispatchService, serverOptions...)
|
||||
egressServer := livekit.NewEgressServer(egressService, serverOptions...)
|
||||
ingressServer := livekit.NewIngressServer(ingressService, serverOptions...)
|
||||
sipServer := livekit.NewSIPServer(sipService, serverOptions...)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
if conf.Development {
|
||||
// pprof handlers are registered onto DefaultServeMux
|
||||
mux = http.DefaultServeMux
|
||||
mux.HandleFunc("/debug/goroutine", s.debugGoroutines)
|
||||
mux.HandleFunc("/debug/rooms", s.debugInfo)
|
||||
}
|
||||
|
||||
xtwirp.RegisterServer(mux, roomServer)
|
||||
xtwirp.RegisterServer(mux, agentDispatchServer)
|
||||
xtwirp.RegisterServer(mux, egressServer)
|
||||
xtwirp.RegisterServer(mux, ingressServer)
|
||||
xtwirp.RegisterServer(mux, sipServer)
|
||||
rtcService.SetupRoutes(mux)
|
||||
whipService.SetupRoutes(mux)
|
||||
mux.Handle("/agent", agentService)
|
||||
mux.HandleFunc("/", s.defaultHandler)
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: configureMiddlewares(mux, middlewares...),
|
||||
}
|
||||
|
||||
if conf.PrometheusPort > 0 {
|
||||
logger.Warnw("prometheus_port is deprecated, please switch to prometheus.port instead", nil)
|
||||
conf.Prometheus.Port = conf.PrometheusPort
|
||||
}
|
||||
|
||||
if conf.Prometheus.Port > 0 {
|
||||
promHandler := promhttp.Handler()
|
||||
if conf.Prometheus.Username != "" && conf.Prometheus.Password != "" {
|
||||
protectedHandler := negroni.New()
|
||||
protectedHandler.Use(negroni.HandlerFunc(GenBasicAuthMiddleware(conf.Prometheus.Username, conf.Prometheus.Password)))
|
||||
protectedHandler.UseHandler(promHandler)
|
||||
promHandler = protectedHandler
|
||||
} else if conf.Prometheus.Username != "" || conf.Prometheus.Password != "" {
|
||||
logger.Warnw("prometheus username or password is set but not both, set both or nothing for unauthenticated access", nil)
|
||||
err = errors.New("prometheus username or password is set but not both, set both or nothing for unauthenticated access")
|
||||
return
|
||||
}
|
||||
s.promServer = &http.Server{
|
||||
Handler: promHandler,
|
||||
}
|
||||
}
|
||||
|
||||
if conf.DebugHandler.Port > 0 {
|
||||
debugMux := http.NewServeMux()
|
||||
debugMux.HandleFunc("/debug/pprof/", httppprof.Index)
|
||||
debugMux.HandleFunc("/debug/pprof/cmdline", httppprof.Cmdline)
|
||||
debugMux.HandleFunc("/debug/pprof/profile", httppprof.Profile)
|
||||
debugMux.HandleFunc("/debug/pprof/symbol", httppprof.Symbol)
|
||||
debugMux.HandleFunc("/debug/pprof/trace", httppprof.Trace)
|
||||
debugMux.HandleFunc("/debug/goroutine", s.debugGoroutines)
|
||||
debugMux.HandleFunc("/debug/rooms", s.debugInfo)
|
||||
s.debugServer = &http.Server{
|
||||
Handler: http.Handler(debugMux),
|
||||
}
|
||||
}
|
||||
|
||||
if err = router.RemoveDeadNodes(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *LivekitServer) Node() *livekit.Node {
|
||||
return s.currentNode.Clone()
|
||||
}
|
||||
|
||||
func (s *LivekitServer) HTTPPort() int {
|
||||
return int(s.config.Port)
|
||||
}
|
||||
|
||||
func (s *LivekitServer) IsRunning() bool {
|
||||
return s.running.Load()
|
||||
}
|
||||
|
||||
func (s *LivekitServer) Start() error {
|
||||
if s.running.Load() {
|
||||
return errors.New("already running")
|
||||
}
|
||||
s.doneChan = make(chan struct{})
|
||||
|
||||
if err := s.router.RegisterNode(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := s.router.UnregisterNode(); err != nil {
|
||||
logger.Errorw("could not unregister node", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := s.router.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.ioService.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addresses := s.config.BindAddresses
|
||||
if addresses == nil {
|
||||
addresses = []string{""}
|
||||
}
|
||||
|
||||
// ensure we could listen
|
||||
listeners := make([]net.Listener, 0)
|
||||
promListeners := make([]net.Listener, 0)
|
||||
debugListeners := make([]net.Listener, 0)
|
||||
for _, addr := range addresses {
|
||||
ln, err := net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(int(s.config.Port))))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listeners = append(listeners, ln)
|
||||
|
||||
if s.promServer != nil {
|
||||
ln, err = net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(int(s.config.Prometheus.Port))))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
promListeners = append(promListeners, ln)
|
||||
}
|
||||
|
||||
if s.debugServer != nil {
|
||||
ln, err = net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(int(s.config.DebugHandler.Port))))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
debugListeners = append(debugListeners, ln)
|
||||
}
|
||||
}
|
||||
|
||||
values := []any{
|
||||
"portHttp", s.config.Port,
|
||||
"nodeID", s.currentNode.NodeID(),
|
||||
"nodeIP", s.currentNode.NodeIP(),
|
||||
"version", version.Version,
|
||||
}
|
||||
if s.config.BindAddresses != nil {
|
||||
values = append(values, "bindAddresses", s.config.BindAddresses)
|
||||
}
|
||||
if s.config.RTC.TCPPort != 0 {
|
||||
values = append(values, "rtc.portTCP", s.config.RTC.TCPPort)
|
||||
}
|
||||
if !s.config.RTC.ForceTCP && s.config.RTC.UDPPort.Valid() {
|
||||
values = append(values, "rtc.portUDP", s.config.RTC.UDPPort)
|
||||
} else {
|
||||
values = append(values,
|
||||
"rtc.portICERange", []uint32{s.config.RTC.ICEPortRangeStart, s.config.RTC.ICEPortRangeEnd},
|
||||
)
|
||||
}
|
||||
if s.config.Prometheus.Port != 0 {
|
||||
values = append(values, "portPrometheus", s.config.Prometheus.Port)
|
||||
}
|
||||
if s.config.DebugHandler.Port != 0 {
|
||||
values = append(values, "portDebugHandler", s.config.DebugHandler.Port)
|
||||
}
|
||||
if s.config.Region != "" {
|
||||
values = append(values, "region", s.config.Region)
|
||||
}
|
||||
logger.Infow("starting LiveKit server", values...)
|
||||
if runtime.GOOS == "windows" {
|
||||
logger.Infow("Windows detected, capacity management is unavailable")
|
||||
}
|
||||
|
||||
for _, promLn := range promListeners {
|
||||
go s.promServer.Serve(promLn)
|
||||
}
|
||||
|
||||
for _, debugLn := range debugListeners {
|
||||
go s.debugServer.Serve(debugLn)
|
||||
}
|
||||
|
||||
if err := s.signalServer.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpGroup := &errgroup.Group{}
|
||||
for _, ln := range listeners {
|
||||
l := ln
|
||||
httpGroup.Go(func() error {
|
||||
return s.httpServer.Serve(l)
|
||||
})
|
||||
}
|
||||
go func() {
|
||||
if err := httpGroup.Wait(); err != http.ErrServerClosed {
|
||||
logger.Errorw("could not start server", err)
|
||||
s.Stop(true)
|
||||
}
|
||||
}()
|
||||
|
||||
go s.backgroundWorker()
|
||||
|
||||
// give time for Serve goroutine to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
s.running.Store(true)
|
||||
|
||||
<-s.doneChan
|
||||
|
||||
// wait for shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
_ = s.httpServer.Shutdown(ctx)
|
||||
if s.debugServer != nil {
|
||||
_ = s.debugServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
if s.turnServer != nil {
|
||||
_ = s.turnServer.Close()
|
||||
}
|
||||
|
||||
s.roomManager.Stop()
|
||||
s.signalServer.Stop()
|
||||
s.ioService.Stop()
|
||||
|
||||
close(s.closedChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LivekitServer) Stop(force bool) {
|
||||
// wait for all participants to exit
|
||||
s.router.Drain()
|
||||
partTicker := time.NewTicker(5 * time.Second)
|
||||
waitingForParticipants := !force && s.roomManager.HasParticipants()
|
||||
for waitingForParticipants {
|
||||
<-partTicker.C
|
||||
logger.Infow("waiting for participants to exit")
|
||||
waitingForParticipants = s.roomManager.HasParticipants()
|
||||
}
|
||||
partTicker.Stop()
|
||||
|
||||
if !s.running.Swap(false) {
|
||||
return
|
||||
}
|
||||
|
||||
s.router.Stop()
|
||||
close(s.doneChan)
|
||||
|
||||
// wait for fully closed
|
||||
<-s.closedChan
|
||||
}
|
||||
|
||||
func (s *LivekitServer) RoomManager() *RoomManager {
|
||||
return s.roomManager
|
||||
}
|
||||
|
||||
func (s *LivekitServer) debugGoroutines(w http.ResponseWriter, _ *http.Request) {
|
||||
_ = pprof.Lookup("goroutine").WriteTo(w, 2)
|
||||
}
|
||||
|
||||
func (s *LivekitServer) debugInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
s.roomManager.lock.RLock()
|
||||
info := make([]map[string]any, 0, len(s.roomManager.rooms))
|
||||
for _, room := range s.roomManager.rooms {
|
||||
info = append(info, room.DebugInfo())
|
||||
}
|
||||
s.roomManager.lock.RUnlock()
|
||||
|
||||
b, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
} else {
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LivekitServer) defaultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
s.healthCheck(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LivekitServer) healthCheck(w http.ResponseWriter, _ *http.Request) {
|
||||
var updatedAt time.Time
|
||||
if s.Node().Stats != nil {
|
||||
updatedAt = time.Unix(s.Node().Stats.UpdatedAt, 0)
|
||||
}
|
||||
if time.Since(updatedAt) > 4*time.Second {
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
_, _ = fmt.Fprintf(w, "Not Ready\nNode Updated At %s", updatedAt)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
// worker to perform periodic tasks per node
|
||||
func (s *LivekitServer) backgroundWorker() {
|
||||
roomTicker := time.NewTicker(1 * time.Second)
|
||||
defer roomTicker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-s.doneChan:
|
||||
return
|
||||
case <-roomTicker.C:
|
||||
s.roomManager.CloseIdleRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func configureMiddlewares(handler http.Handler, middlewares ...negroni.Handler) *negroni.Negroni {
|
||||
n := negroni.New()
|
||||
for _, m := range middlewares {
|
||||
n.Use(m)
|
||||
}
|
||||
n.UseHandler(handler)
|
||||
return n
|
||||
}
|
||||
414
pkg/service/servicefakes/fake_agent_store.go
Normal file
414
pkg/service/servicefakes/fake_agent_store.go
Normal file
@@ -0,0 +1,414 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeAgentStore struct {
|
||||
DeleteAgentDispatchStub func(context.Context, *livekit.AgentDispatch) error
|
||||
deleteAgentDispatchMutex sync.RWMutex
|
||||
deleteAgentDispatchArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.AgentDispatch
|
||||
}
|
||||
deleteAgentDispatchReturns struct {
|
||||
result1 error
|
||||
}
|
||||
deleteAgentDispatchReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
DeleteAgentJobStub func(context.Context, *livekit.Job) error
|
||||
deleteAgentJobMutex sync.RWMutex
|
||||
deleteAgentJobArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.Job
|
||||
}
|
||||
deleteAgentJobReturns struct {
|
||||
result1 error
|
||||
}
|
||||
deleteAgentJobReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
ListAgentDispatchesStub func(context.Context, livekit.RoomName) ([]*livekit.AgentDispatch, error)
|
||||
listAgentDispatchesMutex sync.RWMutex
|
||||
listAgentDispatchesArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
}
|
||||
listAgentDispatchesReturns struct {
|
||||
result1 []*livekit.AgentDispatch
|
||||
result2 error
|
||||
}
|
||||
listAgentDispatchesReturnsOnCall map[int]struct {
|
||||
result1 []*livekit.AgentDispatch
|
||||
result2 error
|
||||
}
|
||||
StoreAgentDispatchStub func(context.Context, *livekit.AgentDispatch) error
|
||||
storeAgentDispatchMutex sync.RWMutex
|
||||
storeAgentDispatchArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.AgentDispatch
|
||||
}
|
||||
storeAgentDispatchReturns struct {
|
||||
result1 error
|
||||
}
|
||||
storeAgentDispatchReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
StoreAgentJobStub func(context.Context, *livekit.Job) error
|
||||
storeAgentJobMutex sync.RWMutex
|
||||
storeAgentJobArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.Job
|
||||
}
|
||||
storeAgentJobReturns struct {
|
||||
result1 error
|
||||
}
|
||||
storeAgentJobReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentDispatch(arg1 context.Context, arg2 *livekit.AgentDispatch) error {
|
||||
fake.deleteAgentDispatchMutex.Lock()
|
||||
ret, specificReturn := fake.deleteAgentDispatchReturnsOnCall[len(fake.deleteAgentDispatchArgsForCall)]
|
||||
fake.deleteAgentDispatchArgsForCall = append(fake.deleteAgentDispatchArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.AgentDispatch
|
||||
}{arg1, arg2})
|
||||
stub := fake.DeleteAgentDispatchStub
|
||||
fakeReturns := fake.deleteAgentDispatchReturns
|
||||
fake.recordInvocation("DeleteAgentDispatch", []interface{}{arg1, arg2})
|
||||
fake.deleteAgentDispatchMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentDispatchCallCount() int {
|
||||
fake.deleteAgentDispatchMutex.RLock()
|
||||
defer fake.deleteAgentDispatchMutex.RUnlock()
|
||||
return len(fake.deleteAgentDispatchArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentDispatchCalls(stub func(context.Context, *livekit.AgentDispatch) error) {
|
||||
fake.deleteAgentDispatchMutex.Lock()
|
||||
defer fake.deleteAgentDispatchMutex.Unlock()
|
||||
fake.DeleteAgentDispatchStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentDispatchArgsForCall(i int) (context.Context, *livekit.AgentDispatch) {
|
||||
fake.deleteAgentDispatchMutex.RLock()
|
||||
defer fake.deleteAgentDispatchMutex.RUnlock()
|
||||
argsForCall := fake.deleteAgentDispatchArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentDispatchReturns(result1 error) {
|
||||
fake.deleteAgentDispatchMutex.Lock()
|
||||
defer fake.deleteAgentDispatchMutex.Unlock()
|
||||
fake.DeleteAgentDispatchStub = nil
|
||||
fake.deleteAgentDispatchReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentDispatchReturnsOnCall(i int, result1 error) {
|
||||
fake.deleteAgentDispatchMutex.Lock()
|
||||
defer fake.deleteAgentDispatchMutex.Unlock()
|
||||
fake.DeleteAgentDispatchStub = nil
|
||||
if fake.deleteAgentDispatchReturnsOnCall == nil {
|
||||
fake.deleteAgentDispatchReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.deleteAgentDispatchReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentJob(arg1 context.Context, arg2 *livekit.Job) error {
|
||||
fake.deleteAgentJobMutex.Lock()
|
||||
ret, specificReturn := fake.deleteAgentJobReturnsOnCall[len(fake.deleteAgentJobArgsForCall)]
|
||||
fake.deleteAgentJobArgsForCall = append(fake.deleteAgentJobArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.Job
|
||||
}{arg1, arg2})
|
||||
stub := fake.DeleteAgentJobStub
|
||||
fakeReturns := fake.deleteAgentJobReturns
|
||||
fake.recordInvocation("DeleteAgentJob", []interface{}{arg1, arg2})
|
||||
fake.deleteAgentJobMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentJobCallCount() int {
|
||||
fake.deleteAgentJobMutex.RLock()
|
||||
defer fake.deleteAgentJobMutex.RUnlock()
|
||||
return len(fake.deleteAgentJobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentJobCalls(stub func(context.Context, *livekit.Job) error) {
|
||||
fake.deleteAgentJobMutex.Lock()
|
||||
defer fake.deleteAgentJobMutex.Unlock()
|
||||
fake.DeleteAgentJobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentJobArgsForCall(i int) (context.Context, *livekit.Job) {
|
||||
fake.deleteAgentJobMutex.RLock()
|
||||
defer fake.deleteAgentJobMutex.RUnlock()
|
||||
argsForCall := fake.deleteAgentJobArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentJobReturns(result1 error) {
|
||||
fake.deleteAgentJobMutex.Lock()
|
||||
defer fake.deleteAgentJobMutex.Unlock()
|
||||
fake.DeleteAgentJobStub = nil
|
||||
fake.deleteAgentJobReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) DeleteAgentJobReturnsOnCall(i int, result1 error) {
|
||||
fake.deleteAgentJobMutex.Lock()
|
||||
defer fake.deleteAgentJobMutex.Unlock()
|
||||
fake.DeleteAgentJobStub = nil
|
||||
if fake.deleteAgentJobReturnsOnCall == nil {
|
||||
fake.deleteAgentJobReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.deleteAgentJobReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) ListAgentDispatches(arg1 context.Context, arg2 livekit.RoomName) ([]*livekit.AgentDispatch, error) {
|
||||
fake.listAgentDispatchesMutex.Lock()
|
||||
ret, specificReturn := fake.listAgentDispatchesReturnsOnCall[len(fake.listAgentDispatchesArgsForCall)]
|
||||
fake.listAgentDispatchesArgsForCall = append(fake.listAgentDispatchesArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
}{arg1, arg2})
|
||||
stub := fake.ListAgentDispatchesStub
|
||||
fakeReturns := fake.listAgentDispatchesReturns
|
||||
fake.recordInvocation("ListAgentDispatches", []interface{}{arg1, arg2})
|
||||
fake.listAgentDispatchesMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) ListAgentDispatchesCallCount() int {
|
||||
fake.listAgentDispatchesMutex.RLock()
|
||||
defer fake.listAgentDispatchesMutex.RUnlock()
|
||||
return len(fake.listAgentDispatchesArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) ListAgentDispatchesCalls(stub func(context.Context, livekit.RoomName) ([]*livekit.AgentDispatch, error)) {
|
||||
fake.listAgentDispatchesMutex.Lock()
|
||||
defer fake.listAgentDispatchesMutex.Unlock()
|
||||
fake.ListAgentDispatchesStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) ListAgentDispatchesArgsForCall(i int) (context.Context, livekit.RoomName) {
|
||||
fake.listAgentDispatchesMutex.RLock()
|
||||
defer fake.listAgentDispatchesMutex.RUnlock()
|
||||
argsForCall := fake.listAgentDispatchesArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) ListAgentDispatchesReturns(result1 []*livekit.AgentDispatch, result2 error) {
|
||||
fake.listAgentDispatchesMutex.Lock()
|
||||
defer fake.listAgentDispatchesMutex.Unlock()
|
||||
fake.ListAgentDispatchesStub = nil
|
||||
fake.listAgentDispatchesReturns = struct {
|
||||
result1 []*livekit.AgentDispatch
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) ListAgentDispatchesReturnsOnCall(i int, result1 []*livekit.AgentDispatch, result2 error) {
|
||||
fake.listAgentDispatchesMutex.Lock()
|
||||
defer fake.listAgentDispatchesMutex.Unlock()
|
||||
fake.ListAgentDispatchesStub = nil
|
||||
if fake.listAgentDispatchesReturnsOnCall == nil {
|
||||
fake.listAgentDispatchesReturnsOnCall = make(map[int]struct {
|
||||
result1 []*livekit.AgentDispatch
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.listAgentDispatchesReturnsOnCall[i] = struct {
|
||||
result1 []*livekit.AgentDispatch
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentDispatch(arg1 context.Context, arg2 *livekit.AgentDispatch) error {
|
||||
fake.storeAgentDispatchMutex.Lock()
|
||||
ret, specificReturn := fake.storeAgentDispatchReturnsOnCall[len(fake.storeAgentDispatchArgsForCall)]
|
||||
fake.storeAgentDispatchArgsForCall = append(fake.storeAgentDispatchArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.AgentDispatch
|
||||
}{arg1, arg2})
|
||||
stub := fake.StoreAgentDispatchStub
|
||||
fakeReturns := fake.storeAgentDispatchReturns
|
||||
fake.recordInvocation("StoreAgentDispatch", []interface{}{arg1, arg2})
|
||||
fake.storeAgentDispatchMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentDispatchCallCount() int {
|
||||
fake.storeAgentDispatchMutex.RLock()
|
||||
defer fake.storeAgentDispatchMutex.RUnlock()
|
||||
return len(fake.storeAgentDispatchArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentDispatchCalls(stub func(context.Context, *livekit.AgentDispatch) error) {
|
||||
fake.storeAgentDispatchMutex.Lock()
|
||||
defer fake.storeAgentDispatchMutex.Unlock()
|
||||
fake.StoreAgentDispatchStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentDispatchArgsForCall(i int) (context.Context, *livekit.AgentDispatch) {
|
||||
fake.storeAgentDispatchMutex.RLock()
|
||||
defer fake.storeAgentDispatchMutex.RUnlock()
|
||||
argsForCall := fake.storeAgentDispatchArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentDispatchReturns(result1 error) {
|
||||
fake.storeAgentDispatchMutex.Lock()
|
||||
defer fake.storeAgentDispatchMutex.Unlock()
|
||||
fake.StoreAgentDispatchStub = nil
|
||||
fake.storeAgentDispatchReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentDispatchReturnsOnCall(i int, result1 error) {
|
||||
fake.storeAgentDispatchMutex.Lock()
|
||||
defer fake.storeAgentDispatchMutex.Unlock()
|
||||
fake.StoreAgentDispatchStub = nil
|
||||
if fake.storeAgentDispatchReturnsOnCall == nil {
|
||||
fake.storeAgentDispatchReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.storeAgentDispatchReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentJob(arg1 context.Context, arg2 *livekit.Job) error {
|
||||
fake.storeAgentJobMutex.Lock()
|
||||
ret, specificReturn := fake.storeAgentJobReturnsOnCall[len(fake.storeAgentJobArgsForCall)]
|
||||
fake.storeAgentJobArgsForCall = append(fake.storeAgentJobArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.Job
|
||||
}{arg1, arg2})
|
||||
stub := fake.StoreAgentJobStub
|
||||
fakeReturns := fake.storeAgentJobReturns
|
||||
fake.recordInvocation("StoreAgentJob", []interface{}{arg1, arg2})
|
||||
fake.storeAgentJobMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentJobCallCount() int {
|
||||
fake.storeAgentJobMutex.RLock()
|
||||
defer fake.storeAgentJobMutex.RUnlock()
|
||||
return len(fake.storeAgentJobArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentJobCalls(stub func(context.Context, *livekit.Job) error) {
|
||||
fake.storeAgentJobMutex.Lock()
|
||||
defer fake.storeAgentJobMutex.Unlock()
|
||||
fake.StoreAgentJobStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentJobArgsForCall(i int) (context.Context, *livekit.Job) {
|
||||
fake.storeAgentJobMutex.RLock()
|
||||
defer fake.storeAgentJobMutex.RUnlock()
|
||||
argsForCall := fake.storeAgentJobArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentJobReturns(result1 error) {
|
||||
fake.storeAgentJobMutex.Lock()
|
||||
defer fake.storeAgentJobMutex.Unlock()
|
||||
fake.StoreAgentJobStub = nil
|
||||
fake.storeAgentJobReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) StoreAgentJobReturnsOnCall(i int, result1 error) {
|
||||
fake.storeAgentJobMutex.Lock()
|
||||
defer fake.storeAgentJobMutex.Unlock()
|
||||
fake.StoreAgentJobStub = nil
|
||||
if fake.storeAgentJobReturnsOnCall == nil {
|
||||
fake.storeAgentJobReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.storeAgentJobReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeAgentStore) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.AgentStore = new(FakeAgentStore)
|
||||
347
pkg/service/servicefakes/fake_egress_store.go
Normal file
347
pkg/service/servicefakes/fake_egress_store.go
Normal file
@@ -0,0 +1,347 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeEgressStore struct {
|
||||
ListEgressStub func(context.Context, livekit.RoomName, bool) ([]*livekit.EgressInfo, error)
|
||||
listEgressMutex sync.RWMutex
|
||||
listEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 bool
|
||||
}
|
||||
listEgressReturns struct {
|
||||
result1 []*livekit.EgressInfo
|
||||
result2 error
|
||||
}
|
||||
listEgressReturnsOnCall map[int]struct {
|
||||
result1 []*livekit.EgressInfo
|
||||
result2 error
|
||||
}
|
||||
LoadEgressStub func(context.Context, string) (*livekit.EgressInfo, error)
|
||||
loadEgressMutex sync.RWMutex
|
||||
loadEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
loadEgressReturns struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}
|
||||
loadEgressReturnsOnCall map[int]struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}
|
||||
StoreEgressStub func(context.Context, *livekit.EgressInfo) error
|
||||
storeEgressMutex sync.RWMutex
|
||||
storeEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.EgressInfo
|
||||
}
|
||||
storeEgressReturns struct {
|
||||
result1 error
|
||||
}
|
||||
storeEgressReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
UpdateEgressStub func(context.Context, *livekit.EgressInfo) error
|
||||
updateEgressMutex sync.RWMutex
|
||||
updateEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.EgressInfo
|
||||
}
|
||||
updateEgressReturns struct {
|
||||
result1 error
|
||||
}
|
||||
updateEgressReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) ListEgress(arg1 context.Context, arg2 livekit.RoomName, arg3 bool) ([]*livekit.EgressInfo, error) {
|
||||
fake.listEgressMutex.Lock()
|
||||
ret, specificReturn := fake.listEgressReturnsOnCall[len(fake.listEgressArgsForCall)]
|
||||
fake.listEgressArgsForCall = append(fake.listEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 bool
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.ListEgressStub
|
||||
fakeReturns := fake.listEgressReturns
|
||||
fake.recordInvocation("ListEgress", []interface{}{arg1, arg2, arg3})
|
||||
fake.listEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) ListEgressCallCount() int {
|
||||
fake.listEgressMutex.RLock()
|
||||
defer fake.listEgressMutex.RUnlock()
|
||||
return len(fake.listEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) ListEgressCalls(stub func(context.Context, livekit.RoomName, bool) ([]*livekit.EgressInfo, error)) {
|
||||
fake.listEgressMutex.Lock()
|
||||
defer fake.listEgressMutex.Unlock()
|
||||
fake.ListEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) ListEgressArgsForCall(i int) (context.Context, livekit.RoomName, bool) {
|
||||
fake.listEgressMutex.RLock()
|
||||
defer fake.listEgressMutex.RUnlock()
|
||||
argsForCall := fake.listEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) ListEgressReturns(result1 []*livekit.EgressInfo, result2 error) {
|
||||
fake.listEgressMutex.Lock()
|
||||
defer fake.listEgressMutex.Unlock()
|
||||
fake.ListEgressStub = nil
|
||||
fake.listEgressReturns = struct {
|
||||
result1 []*livekit.EgressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) ListEgressReturnsOnCall(i int, result1 []*livekit.EgressInfo, result2 error) {
|
||||
fake.listEgressMutex.Lock()
|
||||
defer fake.listEgressMutex.Unlock()
|
||||
fake.ListEgressStub = nil
|
||||
if fake.listEgressReturnsOnCall == nil {
|
||||
fake.listEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 []*livekit.EgressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.listEgressReturnsOnCall[i] = struct {
|
||||
result1 []*livekit.EgressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) LoadEgress(arg1 context.Context, arg2 string) (*livekit.EgressInfo, error) {
|
||||
fake.loadEgressMutex.Lock()
|
||||
ret, specificReturn := fake.loadEgressReturnsOnCall[len(fake.loadEgressArgsForCall)]
|
||||
fake.loadEgressArgsForCall = append(fake.loadEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.LoadEgressStub
|
||||
fakeReturns := fake.loadEgressReturns
|
||||
fake.recordInvocation("LoadEgress", []interface{}{arg1, arg2})
|
||||
fake.loadEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) LoadEgressCallCount() int {
|
||||
fake.loadEgressMutex.RLock()
|
||||
defer fake.loadEgressMutex.RUnlock()
|
||||
return len(fake.loadEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) LoadEgressCalls(stub func(context.Context, string) (*livekit.EgressInfo, error)) {
|
||||
fake.loadEgressMutex.Lock()
|
||||
defer fake.loadEgressMutex.Unlock()
|
||||
fake.LoadEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) LoadEgressArgsForCall(i int) (context.Context, string) {
|
||||
fake.loadEgressMutex.RLock()
|
||||
defer fake.loadEgressMutex.RUnlock()
|
||||
argsForCall := fake.loadEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) LoadEgressReturns(result1 *livekit.EgressInfo, result2 error) {
|
||||
fake.loadEgressMutex.Lock()
|
||||
defer fake.loadEgressMutex.Unlock()
|
||||
fake.LoadEgressStub = nil
|
||||
fake.loadEgressReturns = struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) LoadEgressReturnsOnCall(i int, result1 *livekit.EgressInfo, result2 error) {
|
||||
fake.loadEgressMutex.Lock()
|
||||
defer fake.loadEgressMutex.Unlock()
|
||||
fake.LoadEgressStub = nil
|
||||
if fake.loadEgressReturnsOnCall == nil {
|
||||
fake.loadEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.loadEgressReturnsOnCall[i] = struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) StoreEgress(arg1 context.Context, arg2 *livekit.EgressInfo) error {
|
||||
fake.storeEgressMutex.Lock()
|
||||
ret, specificReturn := fake.storeEgressReturnsOnCall[len(fake.storeEgressArgsForCall)]
|
||||
fake.storeEgressArgsForCall = append(fake.storeEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.EgressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.StoreEgressStub
|
||||
fakeReturns := fake.storeEgressReturns
|
||||
fake.recordInvocation("StoreEgress", []interface{}{arg1, arg2})
|
||||
fake.storeEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) StoreEgressCallCount() int {
|
||||
fake.storeEgressMutex.RLock()
|
||||
defer fake.storeEgressMutex.RUnlock()
|
||||
return len(fake.storeEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) StoreEgressCalls(stub func(context.Context, *livekit.EgressInfo) error) {
|
||||
fake.storeEgressMutex.Lock()
|
||||
defer fake.storeEgressMutex.Unlock()
|
||||
fake.StoreEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) StoreEgressArgsForCall(i int) (context.Context, *livekit.EgressInfo) {
|
||||
fake.storeEgressMutex.RLock()
|
||||
defer fake.storeEgressMutex.RUnlock()
|
||||
argsForCall := fake.storeEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) StoreEgressReturns(result1 error) {
|
||||
fake.storeEgressMutex.Lock()
|
||||
defer fake.storeEgressMutex.Unlock()
|
||||
fake.StoreEgressStub = nil
|
||||
fake.storeEgressReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) StoreEgressReturnsOnCall(i int, result1 error) {
|
||||
fake.storeEgressMutex.Lock()
|
||||
defer fake.storeEgressMutex.Unlock()
|
||||
fake.StoreEgressStub = nil
|
||||
if fake.storeEgressReturnsOnCall == nil {
|
||||
fake.storeEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.storeEgressReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) UpdateEgress(arg1 context.Context, arg2 *livekit.EgressInfo) error {
|
||||
fake.updateEgressMutex.Lock()
|
||||
ret, specificReturn := fake.updateEgressReturnsOnCall[len(fake.updateEgressArgsForCall)]
|
||||
fake.updateEgressArgsForCall = append(fake.updateEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.EgressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.UpdateEgressStub
|
||||
fakeReturns := fake.updateEgressReturns
|
||||
fake.recordInvocation("UpdateEgress", []interface{}{arg1, arg2})
|
||||
fake.updateEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) UpdateEgressCallCount() int {
|
||||
fake.updateEgressMutex.RLock()
|
||||
defer fake.updateEgressMutex.RUnlock()
|
||||
return len(fake.updateEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) UpdateEgressCalls(stub func(context.Context, *livekit.EgressInfo) error) {
|
||||
fake.updateEgressMutex.Lock()
|
||||
defer fake.updateEgressMutex.Unlock()
|
||||
fake.UpdateEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) UpdateEgressArgsForCall(i int) (context.Context, *livekit.EgressInfo) {
|
||||
fake.updateEgressMutex.RLock()
|
||||
defer fake.updateEgressMutex.RUnlock()
|
||||
argsForCall := fake.updateEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) UpdateEgressReturns(result1 error) {
|
||||
fake.updateEgressMutex.Lock()
|
||||
defer fake.updateEgressMutex.Unlock()
|
||||
fake.UpdateEgressStub = nil
|
||||
fake.updateEgressReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) UpdateEgressReturnsOnCall(i int, result1 error) {
|
||||
fake.updateEgressMutex.Lock()
|
||||
defer fake.updateEgressMutex.Unlock()
|
||||
fake.UpdateEgressStub = nil
|
||||
if fake.updateEgressReturnsOnCall == nil {
|
||||
fake.updateEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.updateEgressReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeEgressStore) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.EgressStore = new(FakeEgressStore)
|
||||
118
pkg/service/servicefakes/fake_ingress_launcher.go
Normal file
118
pkg/service/servicefakes/fake_ingress_launcher.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeIngressLauncher struct {
|
||||
LaunchPullIngressStub func(context.Context, *livekit.IngressInfo) (*livekit.IngressInfo, error)
|
||||
launchPullIngressMutex sync.RWMutex
|
||||
launchPullIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}
|
||||
launchPullIngressReturns struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
launchPullIngressReturnsOnCall map[int]struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) LaunchPullIngress(arg1 context.Context, arg2 *livekit.IngressInfo) (*livekit.IngressInfo, error) {
|
||||
fake.launchPullIngressMutex.Lock()
|
||||
ret, specificReturn := fake.launchPullIngressReturnsOnCall[len(fake.launchPullIngressArgsForCall)]
|
||||
fake.launchPullIngressArgsForCall = append(fake.launchPullIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.LaunchPullIngressStub
|
||||
fakeReturns := fake.launchPullIngressReturns
|
||||
fake.recordInvocation("LaunchPullIngress", []interface{}{arg1, arg2})
|
||||
fake.launchPullIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) LaunchPullIngressCallCount() int {
|
||||
fake.launchPullIngressMutex.RLock()
|
||||
defer fake.launchPullIngressMutex.RUnlock()
|
||||
return len(fake.launchPullIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) LaunchPullIngressCalls(stub func(context.Context, *livekit.IngressInfo) (*livekit.IngressInfo, error)) {
|
||||
fake.launchPullIngressMutex.Lock()
|
||||
defer fake.launchPullIngressMutex.Unlock()
|
||||
fake.LaunchPullIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) LaunchPullIngressArgsForCall(i int) (context.Context, *livekit.IngressInfo) {
|
||||
fake.launchPullIngressMutex.RLock()
|
||||
defer fake.launchPullIngressMutex.RUnlock()
|
||||
argsForCall := fake.launchPullIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) LaunchPullIngressReturns(result1 *livekit.IngressInfo, result2 error) {
|
||||
fake.launchPullIngressMutex.Lock()
|
||||
defer fake.launchPullIngressMutex.Unlock()
|
||||
fake.LaunchPullIngressStub = nil
|
||||
fake.launchPullIngressReturns = struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) LaunchPullIngressReturnsOnCall(i int, result1 *livekit.IngressInfo, result2 error) {
|
||||
fake.launchPullIngressMutex.Lock()
|
||||
defer fake.launchPullIngressMutex.Unlock()
|
||||
fake.LaunchPullIngressStub = nil
|
||||
if fake.launchPullIngressReturnsOnCall == nil {
|
||||
fake.launchPullIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.launchPullIngressReturnsOnCall[i] = struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeIngressLauncher) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.IngressLauncher = new(FakeIngressLauncher)
|
||||
574
pkg/service/servicefakes/fake_ingress_store.go
Normal file
574
pkg/service/servicefakes/fake_ingress_store.go
Normal file
@@ -0,0 +1,574 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeIngressStore struct {
|
||||
DeleteIngressStub func(context.Context, *livekit.IngressInfo) error
|
||||
deleteIngressMutex sync.RWMutex
|
||||
deleteIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}
|
||||
deleteIngressReturns struct {
|
||||
result1 error
|
||||
}
|
||||
deleteIngressReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
ListIngressStub func(context.Context, livekit.RoomName) ([]*livekit.IngressInfo, error)
|
||||
listIngressMutex sync.RWMutex
|
||||
listIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
}
|
||||
listIngressReturns struct {
|
||||
result1 []*livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
listIngressReturnsOnCall map[int]struct {
|
||||
result1 []*livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
LoadIngressStub func(context.Context, string) (*livekit.IngressInfo, error)
|
||||
loadIngressMutex sync.RWMutex
|
||||
loadIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
loadIngressReturns struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
loadIngressReturnsOnCall map[int]struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
LoadIngressFromStreamKeyStub func(context.Context, string) (*livekit.IngressInfo, error)
|
||||
loadIngressFromStreamKeyMutex sync.RWMutex
|
||||
loadIngressFromStreamKeyArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
loadIngressFromStreamKeyReturns struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
loadIngressFromStreamKeyReturnsOnCall map[int]struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}
|
||||
StoreIngressStub func(context.Context, *livekit.IngressInfo) error
|
||||
storeIngressMutex sync.RWMutex
|
||||
storeIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}
|
||||
storeIngressReturns struct {
|
||||
result1 error
|
||||
}
|
||||
storeIngressReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
UpdateIngressStub func(context.Context, *livekit.IngressInfo) error
|
||||
updateIngressMutex sync.RWMutex
|
||||
updateIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}
|
||||
updateIngressReturns struct {
|
||||
result1 error
|
||||
}
|
||||
updateIngressReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
UpdateIngressStateStub func(context.Context, string, *livekit.IngressState) error
|
||||
updateIngressStateMutex sync.RWMutex
|
||||
updateIngressStateArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
arg3 *livekit.IngressState
|
||||
}
|
||||
updateIngressStateReturns struct {
|
||||
result1 error
|
||||
}
|
||||
updateIngressStateReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) DeleteIngress(arg1 context.Context, arg2 *livekit.IngressInfo) error {
|
||||
fake.deleteIngressMutex.Lock()
|
||||
ret, specificReturn := fake.deleteIngressReturnsOnCall[len(fake.deleteIngressArgsForCall)]
|
||||
fake.deleteIngressArgsForCall = append(fake.deleteIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.DeleteIngressStub
|
||||
fakeReturns := fake.deleteIngressReturns
|
||||
fake.recordInvocation("DeleteIngress", []interface{}{arg1, arg2})
|
||||
fake.deleteIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) DeleteIngressCallCount() int {
|
||||
fake.deleteIngressMutex.RLock()
|
||||
defer fake.deleteIngressMutex.RUnlock()
|
||||
return len(fake.deleteIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) DeleteIngressCalls(stub func(context.Context, *livekit.IngressInfo) error) {
|
||||
fake.deleteIngressMutex.Lock()
|
||||
defer fake.deleteIngressMutex.Unlock()
|
||||
fake.DeleteIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) DeleteIngressArgsForCall(i int) (context.Context, *livekit.IngressInfo) {
|
||||
fake.deleteIngressMutex.RLock()
|
||||
defer fake.deleteIngressMutex.RUnlock()
|
||||
argsForCall := fake.deleteIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) DeleteIngressReturns(result1 error) {
|
||||
fake.deleteIngressMutex.Lock()
|
||||
defer fake.deleteIngressMutex.Unlock()
|
||||
fake.DeleteIngressStub = nil
|
||||
fake.deleteIngressReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) DeleteIngressReturnsOnCall(i int, result1 error) {
|
||||
fake.deleteIngressMutex.Lock()
|
||||
defer fake.deleteIngressMutex.Unlock()
|
||||
fake.DeleteIngressStub = nil
|
||||
if fake.deleteIngressReturnsOnCall == nil {
|
||||
fake.deleteIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.deleteIngressReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) ListIngress(arg1 context.Context, arg2 livekit.RoomName) ([]*livekit.IngressInfo, error) {
|
||||
fake.listIngressMutex.Lock()
|
||||
ret, specificReturn := fake.listIngressReturnsOnCall[len(fake.listIngressArgsForCall)]
|
||||
fake.listIngressArgsForCall = append(fake.listIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
}{arg1, arg2})
|
||||
stub := fake.ListIngressStub
|
||||
fakeReturns := fake.listIngressReturns
|
||||
fake.recordInvocation("ListIngress", []interface{}{arg1, arg2})
|
||||
fake.listIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) ListIngressCallCount() int {
|
||||
fake.listIngressMutex.RLock()
|
||||
defer fake.listIngressMutex.RUnlock()
|
||||
return len(fake.listIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) ListIngressCalls(stub func(context.Context, livekit.RoomName) ([]*livekit.IngressInfo, error)) {
|
||||
fake.listIngressMutex.Lock()
|
||||
defer fake.listIngressMutex.Unlock()
|
||||
fake.ListIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) ListIngressArgsForCall(i int) (context.Context, livekit.RoomName) {
|
||||
fake.listIngressMutex.RLock()
|
||||
defer fake.listIngressMutex.RUnlock()
|
||||
argsForCall := fake.listIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) ListIngressReturns(result1 []*livekit.IngressInfo, result2 error) {
|
||||
fake.listIngressMutex.Lock()
|
||||
defer fake.listIngressMutex.Unlock()
|
||||
fake.ListIngressStub = nil
|
||||
fake.listIngressReturns = struct {
|
||||
result1 []*livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) ListIngressReturnsOnCall(i int, result1 []*livekit.IngressInfo, result2 error) {
|
||||
fake.listIngressMutex.Lock()
|
||||
defer fake.listIngressMutex.Unlock()
|
||||
fake.ListIngressStub = nil
|
||||
if fake.listIngressReturnsOnCall == nil {
|
||||
fake.listIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 []*livekit.IngressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.listIngressReturnsOnCall[i] = struct {
|
||||
result1 []*livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngress(arg1 context.Context, arg2 string) (*livekit.IngressInfo, error) {
|
||||
fake.loadIngressMutex.Lock()
|
||||
ret, specificReturn := fake.loadIngressReturnsOnCall[len(fake.loadIngressArgsForCall)]
|
||||
fake.loadIngressArgsForCall = append(fake.loadIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.LoadIngressStub
|
||||
fakeReturns := fake.loadIngressReturns
|
||||
fake.recordInvocation("LoadIngress", []interface{}{arg1, arg2})
|
||||
fake.loadIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressCallCount() int {
|
||||
fake.loadIngressMutex.RLock()
|
||||
defer fake.loadIngressMutex.RUnlock()
|
||||
return len(fake.loadIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressCalls(stub func(context.Context, string) (*livekit.IngressInfo, error)) {
|
||||
fake.loadIngressMutex.Lock()
|
||||
defer fake.loadIngressMutex.Unlock()
|
||||
fake.LoadIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressArgsForCall(i int) (context.Context, string) {
|
||||
fake.loadIngressMutex.RLock()
|
||||
defer fake.loadIngressMutex.RUnlock()
|
||||
argsForCall := fake.loadIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressReturns(result1 *livekit.IngressInfo, result2 error) {
|
||||
fake.loadIngressMutex.Lock()
|
||||
defer fake.loadIngressMutex.Unlock()
|
||||
fake.LoadIngressStub = nil
|
||||
fake.loadIngressReturns = struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressReturnsOnCall(i int, result1 *livekit.IngressInfo, result2 error) {
|
||||
fake.loadIngressMutex.Lock()
|
||||
defer fake.loadIngressMutex.Unlock()
|
||||
fake.LoadIngressStub = nil
|
||||
if fake.loadIngressReturnsOnCall == nil {
|
||||
fake.loadIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.loadIngressReturnsOnCall[i] = struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressFromStreamKey(arg1 context.Context, arg2 string) (*livekit.IngressInfo, error) {
|
||||
fake.loadIngressFromStreamKeyMutex.Lock()
|
||||
ret, specificReturn := fake.loadIngressFromStreamKeyReturnsOnCall[len(fake.loadIngressFromStreamKeyArgsForCall)]
|
||||
fake.loadIngressFromStreamKeyArgsForCall = append(fake.loadIngressFromStreamKeyArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.LoadIngressFromStreamKeyStub
|
||||
fakeReturns := fake.loadIngressFromStreamKeyReturns
|
||||
fake.recordInvocation("LoadIngressFromStreamKey", []interface{}{arg1, arg2})
|
||||
fake.loadIngressFromStreamKeyMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressFromStreamKeyCallCount() int {
|
||||
fake.loadIngressFromStreamKeyMutex.RLock()
|
||||
defer fake.loadIngressFromStreamKeyMutex.RUnlock()
|
||||
return len(fake.loadIngressFromStreamKeyArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressFromStreamKeyCalls(stub func(context.Context, string) (*livekit.IngressInfo, error)) {
|
||||
fake.loadIngressFromStreamKeyMutex.Lock()
|
||||
defer fake.loadIngressFromStreamKeyMutex.Unlock()
|
||||
fake.LoadIngressFromStreamKeyStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressFromStreamKeyArgsForCall(i int) (context.Context, string) {
|
||||
fake.loadIngressFromStreamKeyMutex.RLock()
|
||||
defer fake.loadIngressFromStreamKeyMutex.RUnlock()
|
||||
argsForCall := fake.loadIngressFromStreamKeyArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressFromStreamKeyReturns(result1 *livekit.IngressInfo, result2 error) {
|
||||
fake.loadIngressFromStreamKeyMutex.Lock()
|
||||
defer fake.loadIngressFromStreamKeyMutex.Unlock()
|
||||
fake.LoadIngressFromStreamKeyStub = nil
|
||||
fake.loadIngressFromStreamKeyReturns = struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) LoadIngressFromStreamKeyReturnsOnCall(i int, result1 *livekit.IngressInfo, result2 error) {
|
||||
fake.loadIngressFromStreamKeyMutex.Lock()
|
||||
defer fake.loadIngressFromStreamKeyMutex.Unlock()
|
||||
fake.LoadIngressFromStreamKeyStub = nil
|
||||
if fake.loadIngressFromStreamKeyReturnsOnCall == nil {
|
||||
fake.loadIngressFromStreamKeyReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.loadIngressFromStreamKeyReturnsOnCall[i] = struct {
|
||||
result1 *livekit.IngressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) StoreIngress(arg1 context.Context, arg2 *livekit.IngressInfo) error {
|
||||
fake.storeIngressMutex.Lock()
|
||||
ret, specificReturn := fake.storeIngressReturnsOnCall[len(fake.storeIngressArgsForCall)]
|
||||
fake.storeIngressArgsForCall = append(fake.storeIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.StoreIngressStub
|
||||
fakeReturns := fake.storeIngressReturns
|
||||
fake.recordInvocation("StoreIngress", []interface{}{arg1, arg2})
|
||||
fake.storeIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) StoreIngressCallCount() int {
|
||||
fake.storeIngressMutex.RLock()
|
||||
defer fake.storeIngressMutex.RUnlock()
|
||||
return len(fake.storeIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) StoreIngressCalls(stub func(context.Context, *livekit.IngressInfo) error) {
|
||||
fake.storeIngressMutex.Lock()
|
||||
defer fake.storeIngressMutex.Unlock()
|
||||
fake.StoreIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) StoreIngressArgsForCall(i int) (context.Context, *livekit.IngressInfo) {
|
||||
fake.storeIngressMutex.RLock()
|
||||
defer fake.storeIngressMutex.RUnlock()
|
||||
argsForCall := fake.storeIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) StoreIngressReturns(result1 error) {
|
||||
fake.storeIngressMutex.Lock()
|
||||
defer fake.storeIngressMutex.Unlock()
|
||||
fake.StoreIngressStub = nil
|
||||
fake.storeIngressReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) StoreIngressReturnsOnCall(i int, result1 error) {
|
||||
fake.storeIngressMutex.Lock()
|
||||
defer fake.storeIngressMutex.Unlock()
|
||||
fake.StoreIngressStub = nil
|
||||
if fake.storeIngressReturnsOnCall == nil {
|
||||
fake.storeIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.storeIngressReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngress(arg1 context.Context, arg2 *livekit.IngressInfo) error {
|
||||
fake.updateIngressMutex.Lock()
|
||||
ret, specificReturn := fake.updateIngressReturnsOnCall[len(fake.updateIngressArgsForCall)]
|
||||
fake.updateIngressArgsForCall = append(fake.updateIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.UpdateIngressStub
|
||||
fakeReturns := fake.updateIngressReturns
|
||||
fake.recordInvocation("UpdateIngress", []interface{}{arg1, arg2})
|
||||
fake.updateIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressCallCount() int {
|
||||
fake.updateIngressMutex.RLock()
|
||||
defer fake.updateIngressMutex.RUnlock()
|
||||
return len(fake.updateIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressCalls(stub func(context.Context, *livekit.IngressInfo) error) {
|
||||
fake.updateIngressMutex.Lock()
|
||||
defer fake.updateIngressMutex.Unlock()
|
||||
fake.UpdateIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressArgsForCall(i int) (context.Context, *livekit.IngressInfo) {
|
||||
fake.updateIngressMutex.RLock()
|
||||
defer fake.updateIngressMutex.RUnlock()
|
||||
argsForCall := fake.updateIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressReturns(result1 error) {
|
||||
fake.updateIngressMutex.Lock()
|
||||
defer fake.updateIngressMutex.Unlock()
|
||||
fake.UpdateIngressStub = nil
|
||||
fake.updateIngressReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressReturnsOnCall(i int, result1 error) {
|
||||
fake.updateIngressMutex.Lock()
|
||||
defer fake.updateIngressMutex.Unlock()
|
||||
fake.UpdateIngressStub = nil
|
||||
if fake.updateIngressReturnsOnCall == nil {
|
||||
fake.updateIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.updateIngressReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressState(arg1 context.Context, arg2 string, arg3 *livekit.IngressState) error {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
ret, specificReturn := fake.updateIngressStateReturnsOnCall[len(fake.updateIngressStateArgsForCall)]
|
||||
fake.updateIngressStateArgsForCall = append(fake.updateIngressStateArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
arg3 *livekit.IngressState
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.UpdateIngressStateStub
|
||||
fakeReturns := fake.updateIngressStateReturns
|
||||
fake.recordInvocation("UpdateIngressState", []interface{}{arg1, arg2, arg3})
|
||||
fake.updateIngressStateMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressStateCallCount() int {
|
||||
fake.updateIngressStateMutex.RLock()
|
||||
defer fake.updateIngressStateMutex.RUnlock()
|
||||
return len(fake.updateIngressStateArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressStateCalls(stub func(context.Context, string, *livekit.IngressState) error) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
defer fake.updateIngressStateMutex.Unlock()
|
||||
fake.UpdateIngressStateStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressStateArgsForCall(i int) (context.Context, string, *livekit.IngressState) {
|
||||
fake.updateIngressStateMutex.RLock()
|
||||
defer fake.updateIngressStateMutex.RUnlock()
|
||||
argsForCall := fake.updateIngressStateArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressStateReturns(result1 error) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
defer fake.updateIngressStateMutex.Unlock()
|
||||
fake.UpdateIngressStateStub = nil
|
||||
fake.updateIngressStateReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) UpdateIngressStateReturnsOnCall(i int, result1 error) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
defer fake.updateIngressStateMutex.Unlock()
|
||||
fake.UpdateIngressStateStub = nil
|
||||
if fake.updateIngressStateReturnsOnCall == nil {
|
||||
fake.updateIngressStateReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.updateIngressStateReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeIngressStore) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.IngressStore = new(FakeIngressStore)
|
||||
436
pkg/service/servicefakes/fake_ioclient.go
Normal file
436
pkg/service/servicefakes/fake_ioclient.go
Normal file
@@ -0,0 +1,436 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type FakeIOClient struct {
|
||||
CreateEgressStub func(context.Context, *livekit.EgressInfo) (*emptypb.Empty, error)
|
||||
createEgressMutex sync.RWMutex
|
||||
createEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.EgressInfo
|
||||
}
|
||||
createEgressReturns struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}
|
||||
createEgressReturnsOnCall map[int]struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}
|
||||
CreateIngressStub func(context.Context, *livekit.IngressInfo) (*rpc.CreateIngressResponse, error)
|
||||
createIngressMutex sync.RWMutex
|
||||
createIngressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}
|
||||
createIngressReturns struct {
|
||||
result1 *rpc.CreateIngressResponse
|
||||
result2 error
|
||||
}
|
||||
createIngressReturnsOnCall map[int]struct {
|
||||
result1 *rpc.CreateIngressResponse
|
||||
result2 error
|
||||
}
|
||||
GetEgressStub func(context.Context, *rpc.GetEgressRequest) (*livekit.EgressInfo, error)
|
||||
getEgressMutex sync.RWMutex
|
||||
getEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *rpc.GetEgressRequest
|
||||
}
|
||||
getEgressReturns struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}
|
||||
getEgressReturnsOnCall map[int]struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}
|
||||
ListEgressStub func(context.Context, *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error)
|
||||
listEgressMutex sync.RWMutex
|
||||
listEgressArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.ListEgressRequest
|
||||
}
|
||||
listEgressReturns struct {
|
||||
result1 *livekit.ListEgressResponse
|
||||
result2 error
|
||||
}
|
||||
listEgressReturnsOnCall map[int]struct {
|
||||
result1 *livekit.ListEgressResponse
|
||||
result2 error
|
||||
}
|
||||
UpdateIngressStateStub func(context.Context, *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error)
|
||||
updateIngressStateMutex sync.RWMutex
|
||||
updateIngressStateArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *rpc.UpdateIngressStateRequest
|
||||
}
|
||||
updateIngressStateReturns struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}
|
||||
updateIngressStateReturnsOnCall map[int]struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateEgress(arg1 context.Context, arg2 *livekit.EgressInfo) (*emptypb.Empty, error) {
|
||||
fake.createEgressMutex.Lock()
|
||||
ret, specificReturn := fake.createEgressReturnsOnCall[len(fake.createEgressArgsForCall)]
|
||||
fake.createEgressArgsForCall = append(fake.createEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.EgressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.CreateEgressStub
|
||||
fakeReturns := fake.createEgressReturns
|
||||
fake.recordInvocation("CreateEgress", []interface{}{arg1, arg2})
|
||||
fake.createEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateEgressCallCount() int {
|
||||
fake.createEgressMutex.RLock()
|
||||
defer fake.createEgressMutex.RUnlock()
|
||||
return len(fake.createEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateEgressCalls(stub func(context.Context, *livekit.EgressInfo) (*emptypb.Empty, error)) {
|
||||
fake.createEgressMutex.Lock()
|
||||
defer fake.createEgressMutex.Unlock()
|
||||
fake.CreateEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateEgressArgsForCall(i int) (context.Context, *livekit.EgressInfo) {
|
||||
fake.createEgressMutex.RLock()
|
||||
defer fake.createEgressMutex.RUnlock()
|
||||
argsForCall := fake.createEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateEgressReturns(result1 *emptypb.Empty, result2 error) {
|
||||
fake.createEgressMutex.Lock()
|
||||
defer fake.createEgressMutex.Unlock()
|
||||
fake.CreateEgressStub = nil
|
||||
fake.createEgressReturns = struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateEgressReturnsOnCall(i int, result1 *emptypb.Empty, result2 error) {
|
||||
fake.createEgressMutex.Lock()
|
||||
defer fake.createEgressMutex.Unlock()
|
||||
fake.CreateEgressStub = nil
|
||||
if fake.createEgressReturnsOnCall == nil {
|
||||
fake.createEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.createEgressReturnsOnCall[i] = struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateIngress(arg1 context.Context, arg2 *livekit.IngressInfo) (*rpc.CreateIngressResponse, error) {
|
||||
fake.createIngressMutex.Lock()
|
||||
ret, specificReturn := fake.createIngressReturnsOnCall[len(fake.createIngressArgsForCall)]
|
||||
fake.createIngressArgsForCall = append(fake.createIngressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.IngressInfo
|
||||
}{arg1, arg2})
|
||||
stub := fake.CreateIngressStub
|
||||
fakeReturns := fake.createIngressReturns
|
||||
fake.recordInvocation("CreateIngress", []interface{}{arg1, arg2})
|
||||
fake.createIngressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateIngressCallCount() int {
|
||||
fake.createIngressMutex.RLock()
|
||||
defer fake.createIngressMutex.RUnlock()
|
||||
return len(fake.createIngressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateIngressCalls(stub func(context.Context, *livekit.IngressInfo) (*rpc.CreateIngressResponse, error)) {
|
||||
fake.createIngressMutex.Lock()
|
||||
defer fake.createIngressMutex.Unlock()
|
||||
fake.CreateIngressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateIngressArgsForCall(i int) (context.Context, *livekit.IngressInfo) {
|
||||
fake.createIngressMutex.RLock()
|
||||
defer fake.createIngressMutex.RUnlock()
|
||||
argsForCall := fake.createIngressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateIngressReturns(result1 *rpc.CreateIngressResponse, result2 error) {
|
||||
fake.createIngressMutex.Lock()
|
||||
defer fake.createIngressMutex.Unlock()
|
||||
fake.CreateIngressStub = nil
|
||||
fake.createIngressReturns = struct {
|
||||
result1 *rpc.CreateIngressResponse
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) CreateIngressReturnsOnCall(i int, result1 *rpc.CreateIngressResponse, result2 error) {
|
||||
fake.createIngressMutex.Lock()
|
||||
defer fake.createIngressMutex.Unlock()
|
||||
fake.CreateIngressStub = nil
|
||||
if fake.createIngressReturnsOnCall == nil {
|
||||
fake.createIngressReturnsOnCall = make(map[int]struct {
|
||||
result1 *rpc.CreateIngressResponse
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.createIngressReturnsOnCall[i] = struct {
|
||||
result1 *rpc.CreateIngressResponse
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) GetEgress(arg1 context.Context, arg2 *rpc.GetEgressRequest) (*livekit.EgressInfo, error) {
|
||||
fake.getEgressMutex.Lock()
|
||||
ret, specificReturn := fake.getEgressReturnsOnCall[len(fake.getEgressArgsForCall)]
|
||||
fake.getEgressArgsForCall = append(fake.getEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *rpc.GetEgressRequest
|
||||
}{arg1, arg2})
|
||||
stub := fake.GetEgressStub
|
||||
fakeReturns := fake.getEgressReturns
|
||||
fake.recordInvocation("GetEgress", []interface{}{arg1, arg2})
|
||||
fake.getEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) GetEgressCallCount() int {
|
||||
fake.getEgressMutex.RLock()
|
||||
defer fake.getEgressMutex.RUnlock()
|
||||
return len(fake.getEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) GetEgressCalls(stub func(context.Context, *rpc.GetEgressRequest) (*livekit.EgressInfo, error)) {
|
||||
fake.getEgressMutex.Lock()
|
||||
defer fake.getEgressMutex.Unlock()
|
||||
fake.GetEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) GetEgressArgsForCall(i int) (context.Context, *rpc.GetEgressRequest) {
|
||||
fake.getEgressMutex.RLock()
|
||||
defer fake.getEgressMutex.RUnlock()
|
||||
argsForCall := fake.getEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) GetEgressReturns(result1 *livekit.EgressInfo, result2 error) {
|
||||
fake.getEgressMutex.Lock()
|
||||
defer fake.getEgressMutex.Unlock()
|
||||
fake.GetEgressStub = nil
|
||||
fake.getEgressReturns = struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) GetEgressReturnsOnCall(i int, result1 *livekit.EgressInfo, result2 error) {
|
||||
fake.getEgressMutex.Lock()
|
||||
defer fake.getEgressMutex.Unlock()
|
||||
fake.GetEgressStub = nil
|
||||
if fake.getEgressReturnsOnCall == nil {
|
||||
fake.getEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.getEgressReturnsOnCall[i] = struct {
|
||||
result1 *livekit.EgressInfo
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) ListEgress(arg1 context.Context, arg2 *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error) {
|
||||
fake.listEgressMutex.Lock()
|
||||
ret, specificReturn := fake.listEgressReturnsOnCall[len(fake.listEgressArgsForCall)]
|
||||
fake.listEgressArgsForCall = append(fake.listEgressArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.ListEgressRequest
|
||||
}{arg1, arg2})
|
||||
stub := fake.ListEgressStub
|
||||
fakeReturns := fake.listEgressReturns
|
||||
fake.recordInvocation("ListEgress", []interface{}{arg1, arg2})
|
||||
fake.listEgressMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) ListEgressCallCount() int {
|
||||
fake.listEgressMutex.RLock()
|
||||
defer fake.listEgressMutex.RUnlock()
|
||||
return len(fake.listEgressArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) ListEgressCalls(stub func(context.Context, *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error)) {
|
||||
fake.listEgressMutex.Lock()
|
||||
defer fake.listEgressMutex.Unlock()
|
||||
fake.ListEgressStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) ListEgressArgsForCall(i int) (context.Context, *livekit.ListEgressRequest) {
|
||||
fake.listEgressMutex.RLock()
|
||||
defer fake.listEgressMutex.RUnlock()
|
||||
argsForCall := fake.listEgressArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) ListEgressReturns(result1 *livekit.ListEgressResponse, result2 error) {
|
||||
fake.listEgressMutex.Lock()
|
||||
defer fake.listEgressMutex.Unlock()
|
||||
fake.ListEgressStub = nil
|
||||
fake.listEgressReturns = struct {
|
||||
result1 *livekit.ListEgressResponse
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) ListEgressReturnsOnCall(i int, result1 *livekit.ListEgressResponse, result2 error) {
|
||||
fake.listEgressMutex.Lock()
|
||||
defer fake.listEgressMutex.Unlock()
|
||||
fake.ListEgressStub = nil
|
||||
if fake.listEgressReturnsOnCall == nil {
|
||||
fake.listEgressReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.ListEgressResponse
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.listEgressReturnsOnCall[i] = struct {
|
||||
result1 *livekit.ListEgressResponse
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) UpdateIngressState(arg1 context.Context, arg2 *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
ret, specificReturn := fake.updateIngressStateReturnsOnCall[len(fake.updateIngressStateArgsForCall)]
|
||||
fake.updateIngressStateArgsForCall = append(fake.updateIngressStateArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *rpc.UpdateIngressStateRequest
|
||||
}{arg1, arg2})
|
||||
stub := fake.UpdateIngressStateStub
|
||||
fakeReturns := fake.updateIngressStateReturns
|
||||
fake.recordInvocation("UpdateIngressState", []interface{}{arg1, arg2})
|
||||
fake.updateIngressStateMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) UpdateIngressStateCallCount() int {
|
||||
fake.updateIngressStateMutex.RLock()
|
||||
defer fake.updateIngressStateMutex.RUnlock()
|
||||
return len(fake.updateIngressStateArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) UpdateIngressStateCalls(stub func(context.Context, *rpc.UpdateIngressStateRequest) (*emptypb.Empty, error)) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
defer fake.updateIngressStateMutex.Unlock()
|
||||
fake.UpdateIngressStateStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) UpdateIngressStateArgsForCall(i int) (context.Context, *rpc.UpdateIngressStateRequest) {
|
||||
fake.updateIngressStateMutex.RLock()
|
||||
defer fake.updateIngressStateMutex.RUnlock()
|
||||
argsForCall := fake.updateIngressStateArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) UpdateIngressStateReturns(result1 *emptypb.Empty, result2 error) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
defer fake.updateIngressStateMutex.Unlock()
|
||||
fake.UpdateIngressStateStub = nil
|
||||
fake.updateIngressStateReturns = struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) UpdateIngressStateReturnsOnCall(i int, result1 *emptypb.Empty, result2 error) {
|
||||
fake.updateIngressStateMutex.Lock()
|
||||
defer fake.updateIngressStateMutex.Unlock()
|
||||
fake.UpdateIngressStateStub = nil
|
||||
if fake.updateIngressStateReturnsOnCall == nil {
|
||||
fake.updateIngressStateReturnsOnCall = make(map[int]struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.updateIngressStateReturnsOnCall[i] = struct {
|
||||
result1 *emptypb.Empty
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeIOClient) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.IOClient = new(FakeIOClient)
|
||||
989
pkg/service/servicefakes/fake_object_store.go
Normal file
989
pkg/service/servicefakes/fake_object_store.go
Normal file
@@ -0,0 +1,989 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeObjectStore struct {
|
||||
DeleteParticipantStub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) error
|
||||
deleteParticipantMutex sync.RWMutex
|
||||
deleteParticipantArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 livekit.ParticipantIdentity
|
||||
}
|
||||
deleteParticipantReturns struct {
|
||||
result1 error
|
||||
}
|
||||
deleteParticipantReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
arg1 context.Context
|
||||
arg2 []livekit.RoomName
|
||||
}
|
||||
listRoomsReturns struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
}
|
||||
listRoomsReturnsOnCall map[int]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 {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 bool
|
||||
}
|
||||
loadRoomReturns struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}
|
||||
loadRoomReturnsOnCall map[int]struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}
|
||||
LockRoomStub func(context.Context, livekit.RoomName, time.Duration) (string, error)
|
||||
lockRoomMutex sync.RWMutex
|
||||
lockRoomArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 time.Duration
|
||||
}
|
||||
lockRoomReturns struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
lockRoomReturnsOnCall map[int]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 {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 *livekit.ParticipantInfo
|
||||
}
|
||||
storeParticipantReturns struct {
|
||||
result1 error
|
||||
}
|
||||
storeParticipantReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
StoreRoomStub func(context.Context, *livekit.Room, *livekit.RoomInternal) error
|
||||
storeRoomMutex sync.RWMutex
|
||||
storeRoomArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.Room
|
||||
arg3 *livekit.RoomInternal
|
||||
}
|
||||
storeRoomReturns struct {
|
||||
result1 error
|
||||
}
|
||||
storeRoomReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
UnlockRoomStub func(context.Context, livekit.RoomName, string) error
|
||||
unlockRoomMutex sync.RWMutex
|
||||
unlockRoomArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 string
|
||||
}
|
||||
unlockRoomReturns struct {
|
||||
result1 error
|
||||
}
|
||||
unlockRoomReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteParticipant(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.ParticipantIdentity) error {
|
||||
fake.deleteParticipantMutex.Lock()
|
||||
ret, specificReturn := fake.deleteParticipantReturnsOnCall[len(fake.deleteParticipantArgsForCall)]
|
||||
fake.deleteParticipantArgsForCall = append(fake.deleteParticipantArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 livekit.ParticipantIdentity
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.DeleteParticipantStub
|
||||
fakeReturns := fake.deleteParticipantReturns
|
||||
fake.recordInvocation("DeleteParticipant", []interface{}{arg1, arg2, arg3})
|
||||
fake.deleteParticipantMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteParticipantCallCount() int {
|
||||
fake.deleteParticipantMutex.RLock()
|
||||
defer fake.deleteParticipantMutex.RUnlock()
|
||||
return len(fake.deleteParticipantArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteParticipantCalls(stub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) error) {
|
||||
fake.deleteParticipantMutex.Lock()
|
||||
defer fake.deleteParticipantMutex.Unlock()
|
||||
fake.DeleteParticipantStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteParticipantArgsForCall(i int) (context.Context, livekit.RoomName, livekit.ParticipantIdentity) {
|
||||
fake.deleteParticipantMutex.RLock()
|
||||
defer fake.deleteParticipantMutex.RUnlock()
|
||||
argsForCall := fake.deleteParticipantArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteParticipantReturns(result1 error) {
|
||||
fake.deleteParticipantMutex.Lock()
|
||||
defer fake.deleteParticipantMutex.Unlock()
|
||||
fake.DeleteParticipantStub = nil
|
||||
fake.deleteParticipantReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteParticipantReturnsOnCall(i int, result1 error) {
|
||||
fake.deleteParticipantMutex.Lock()
|
||||
defer fake.deleteParticipantMutex.Unlock()
|
||||
fake.DeleteParticipantStub = nil
|
||||
if fake.deleteParticipantReturnsOnCall == nil {
|
||||
fake.deleteParticipantReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.deleteParticipantReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) 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 *FakeObjectStore) DeleteRoomCallCount() int {
|
||||
fake.deleteRoomMutex.RLock()
|
||||
defer fake.deleteRoomMutex.RUnlock()
|
||||
return len(fake.deleteRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) DeleteRoomCalls(stub func(context.Context, livekit.RoomName) error) {
|
||||
fake.deleteRoomMutex.Lock()
|
||||
defer fake.deleteRoomMutex.Unlock()
|
||||
fake.DeleteRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) 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 *FakeObjectStore) DeleteRoomReturns(result1 error) {
|
||||
fake.deleteRoomMutex.Lock()
|
||||
defer fake.deleteRoomMutex.Unlock()
|
||||
fake.DeleteRoomStub = nil
|
||||
fake.deleteRoomReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) 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 *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)]
|
||||
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 *FakeObjectStore) ListParticipantsCallCount() int {
|
||||
fake.listParticipantsMutex.RLock()
|
||||
defer fake.listParticipantsMutex.RUnlock()
|
||||
return len(fake.listParticipantsArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) ListParticipantsCalls(stub func(context.Context, livekit.RoomName) ([]*livekit.ParticipantInfo, error)) {
|
||||
fake.listParticipantsMutex.Lock()
|
||||
defer fake.listParticipantsMutex.Unlock()
|
||||
fake.ListParticipantsStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) 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 *FakeObjectStore) 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 *FakeObjectStore) 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 *FakeObjectStore) ListRooms(arg1 context.Context, arg2 []livekit.RoomName) ([]*livekit.Room, error) {
|
||||
var arg2Copy []livekit.RoomName
|
||||
if arg2 != nil {
|
||||
arg2Copy = make([]livekit.RoomName, len(arg2))
|
||||
copy(arg2Copy, arg2)
|
||||
}
|
||||
fake.listRoomsMutex.Lock()
|
||||
ret, specificReturn := fake.listRoomsReturnsOnCall[len(fake.listRoomsArgsForCall)]
|
||||
fake.listRoomsArgsForCall = append(fake.listRoomsArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 []livekit.RoomName
|
||||
}{arg1, arg2Copy})
|
||||
stub := fake.ListRoomsStub
|
||||
fakeReturns := fake.listRoomsReturns
|
||||
fake.recordInvocation("ListRooms", []interface{}{arg1, arg2Copy})
|
||||
fake.listRoomsMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) ListRoomsCallCount() int {
|
||||
fake.listRoomsMutex.RLock()
|
||||
defer fake.listRoomsMutex.RUnlock()
|
||||
return len(fake.listRoomsArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) ListRoomsCalls(stub func(context.Context, []livekit.RoomName) ([]*livekit.Room, error)) {
|
||||
fake.listRoomsMutex.Lock()
|
||||
defer fake.listRoomsMutex.Unlock()
|
||||
fake.ListRoomsStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) ListRoomsArgsForCall(i int) (context.Context, []livekit.RoomName) {
|
||||
fake.listRoomsMutex.RLock()
|
||||
defer fake.listRoomsMutex.RUnlock()
|
||||
argsForCall := fake.listRoomsArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) ListRoomsReturns(result1 []*livekit.Room, result2 error) {
|
||||
fake.listRoomsMutex.Lock()
|
||||
defer fake.listRoomsMutex.Unlock()
|
||||
fake.ListRoomsStub = nil
|
||||
fake.listRoomsReturns = struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) ListRoomsReturnsOnCall(i int, result1 []*livekit.Room, result2 error) {
|
||||
fake.listRoomsMutex.Lock()
|
||||
defer fake.listRoomsMutex.Unlock()
|
||||
fake.ListRoomsStub = nil
|
||||
if fake.listRoomsReturnsOnCall == nil {
|
||||
fake.listRoomsReturnsOnCall = make(map[int]struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.listRoomsReturnsOnCall[i] = struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) 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 *FakeObjectStore) LoadParticipantCallCount() int {
|
||||
fake.loadParticipantMutex.RLock()
|
||||
defer fake.loadParticipantMutex.RUnlock()
|
||||
return len(fake.loadParticipantArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LoadParticipantCalls(stub func(context.Context, livekit.RoomName, livekit.ParticipantIdentity) (*livekit.ParticipantInfo, error)) {
|
||||
fake.loadParticipantMutex.Lock()
|
||||
defer fake.loadParticipantMutex.Unlock()
|
||||
fake.LoadParticipantStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) 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 *FakeObjectStore) 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 *FakeObjectStore) 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 *FakeObjectStore) LoadRoom(arg1 context.Context, arg2 livekit.RoomName, arg3 bool) (*livekit.Room, *livekit.RoomInternal, error) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
ret, specificReturn := fake.loadRoomReturnsOnCall[len(fake.loadRoomArgsForCall)]
|
||||
fake.loadRoomArgsForCall = append(fake.loadRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 bool
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.LoadRoomStub
|
||||
fakeReturns := fake.loadRoomReturns
|
||||
fake.recordInvocation("LoadRoom", []interface{}{arg1, arg2, arg3})
|
||||
fake.loadRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2, ret.result3
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LoadRoomCallCount() int {
|
||||
fake.loadRoomMutex.RLock()
|
||||
defer fake.loadRoomMutex.RUnlock()
|
||||
return len(fake.loadRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LoadRoomCalls(stub func(context.Context, livekit.RoomName, bool) (*livekit.Room, *livekit.RoomInternal, error)) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
defer fake.loadRoomMutex.Unlock()
|
||||
fake.LoadRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LoadRoomArgsForCall(i int) (context.Context, livekit.RoomName, bool) {
|
||||
fake.loadRoomMutex.RLock()
|
||||
defer fake.loadRoomMutex.RUnlock()
|
||||
argsForCall := fake.loadRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LoadRoomReturns(result1 *livekit.Room, result2 *livekit.RoomInternal, result3 error) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
defer fake.loadRoomMutex.Unlock()
|
||||
fake.LoadRoomStub = nil
|
||||
fake.loadRoomReturns = struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}{result1, result2, result3}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LoadRoomReturnsOnCall(i int, result1 *livekit.Room, result2 *livekit.RoomInternal, result3 error) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
defer fake.loadRoomMutex.Unlock()
|
||||
fake.LoadRoomStub = nil
|
||||
if fake.loadRoomReturnsOnCall == nil {
|
||||
fake.loadRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
})
|
||||
}
|
||||
fake.loadRoomReturnsOnCall[i] = struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}{result1, result2, result3}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LockRoom(arg1 context.Context, arg2 livekit.RoomName, arg3 time.Duration) (string, error) {
|
||||
fake.lockRoomMutex.Lock()
|
||||
ret, specificReturn := fake.lockRoomReturnsOnCall[len(fake.lockRoomArgsForCall)]
|
||||
fake.lockRoomArgsForCall = append(fake.lockRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 time.Duration
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.LockRoomStub
|
||||
fakeReturns := fake.lockRoomReturns
|
||||
fake.recordInvocation("LockRoom", []interface{}{arg1, arg2, arg3})
|
||||
fake.lockRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LockRoomCallCount() int {
|
||||
fake.lockRoomMutex.RLock()
|
||||
defer fake.lockRoomMutex.RUnlock()
|
||||
return len(fake.lockRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LockRoomCalls(stub func(context.Context, livekit.RoomName, time.Duration) (string, error)) {
|
||||
fake.lockRoomMutex.Lock()
|
||||
defer fake.lockRoomMutex.Unlock()
|
||||
fake.LockRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LockRoomArgsForCall(i int) (context.Context, livekit.RoomName, time.Duration) {
|
||||
fake.lockRoomMutex.RLock()
|
||||
defer fake.lockRoomMutex.RUnlock()
|
||||
argsForCall := fake.lockRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LockRoomReturns(result1 string, result2 error) {
|
||||
fake.lockRoomMutex.Lock()
|
||||
defer fake.lockRoomMutex.Unlock()
|
||||
fake.LockRoomStub = nil
|
||||
fake.lockRoomReturns = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) LockRoomReturnsOnCall(i int, result1 string, result2 error) {
|
||||
fake.lockRoomMutex.Lock()
|
||||
defer fake.lockRoomMutex.Unlock()
|
||||
fake.LockRoomStub = nil
|
||||
if fake.lockRoomReturnsOnCall == nil {
|
||||
fake.lockRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.lockRoomReturnsOnCall[i] = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{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)]
|
||||
fake.storeParticipantArgsForCall = append(fake.storeParticipantArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 *livekit.ParticipantInfo
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.StoreParticipantStub
|
||||
fakeReturns := fake.storeParticipantReturns
|
||||
fake.recordInvocation("StoreParticipant", []interface{}{arg1, arg2, arg3})
|
||||
fake.storeParticipantMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreParticipantCallCount() int {
|
||||
fake.storeParticipantMutex.RLock()
|
||||
defer fake.storeParticipantMutex.RUnlock()
|
||||
return len(fake.storeParticipantArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreParticipantCalls(stub func(context.Context, livekit.RoomName, *livekit.ParticipantInfo) error) {
|
||||
fake.storeParticipantMutex.Lock()
|
||||
defer fake.storeParticipantMutex.Unlock()
|
||||
fake.StoreParticipantStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreParticipantArgsForCall(i int) (context.Context, livekit.RoomName, *livekit.ParticipantInfo) {
|
||||
fake.storeParticipantMutex.RLock()
|
||||
defer fake.storeParticipantMutex.RUnlock()
|
||||
argsForCall := fake.storeParticipantArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreParticipantReturns(result1 error) {
|
||||
fake.storeParticipantMutex.Lock()
|
||||
defer fake.storeParticipantMutex.Unlock()
|
||||
fake.StoreParticipantStub = nil
|
||||
fake.storeParticipantReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreParticipantReturnsOnCall(i int, result1 error) {
|
||||
fake.storeParticipantMutex.Lock()
|
||||
defer fake.storeParticipantMutex.Unlock()
|
||||
fake.StoreParticipantStub = nil
|
||||
if fake.storeParticipantReturnsOnCall == nil {
|
||||
fake.storeParticipantReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.storeParticipantReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreRoom(arg1 context.Context, arg2 *livekit.Room, arg3 *livekit.RoomInternal) error {
|
||||
fake.storeRoomMutex.Lock()
|
||||
ret, specificReturn := fake.storeRoomReturnsOnCall[len(fake.storeRoomArgsForCall)]
|
||||
fake.storeRoomArgsForCall = append(fake.storeRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.Room
|
||||
arg3 *livekit.RoomInternal
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.StoreRoomStub
|
||||
fakeReturns := fake.storeRoomReturns
|
||||
fake.recordInvocation("StoreRoom", []interface{}{arg1, arg2, arg3})
|
||||
fake.storeRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreRoomCallCount() int {
|
||||
fake.storeRoomMutex.RLock()
|
||||
defer fake.storeRoomMutex.RUnlock()
|
||||
return len(fake.storeRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreRoomCalls(stub func(context.Context, *livekit.Room, *livekit.RoomInternal) error) {
|
||||
fake.storeRoomMutex.Lock()
|
||||
defer fake.storeRoomMutex.Unlock()
|
||||
fake.StoreRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreRoomArgsForCall(i int) (context.Context, *livekit.Room, *livekit.RoomInternal) {
|
||||
fake.storeRoomMutex.RLock()
|
||||
defer fake.storeRoomMutex.RUnlock()
|
||||
argsForCall := fake.storeRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreRoomReturns(result1 error) {
|
||||
fake.storeRoomMutex.Lock()
|
||||
defer fake.storeRoomMutex.Unlock()
|
||||
fake.StoreRoomStub = nil
|
||||
fake.storeRoomReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) StoreRoomReturnsOnCall(i int, result1 error) {
|
||||
fake.storeRoomMutex.Lock()
|
||||
defer fake.storeRoomMutex.Unlock()
|
||||
fake.StoreRoomStub = nil
|
||||
if fake.storeRoomReturnsOnCall == nil {
|
||||
fake.storeRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.storeRoomReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) UnlockRoom(arg1 context.Context, arg2 livekit.RoomName, arg3 string) error {
|
||||
fake.unlockRoomMutex.Lock()
|
||||
ret, specificReturn := fake.unlockRoomReturnsOnCall[len(fake.unlockRoomArgsForCall)]
|
||||
fake.unlockRoomArgsForCall = append(fake.unlockRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 string
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.UnlockRoomStub
|
||||
fakeReturns := fake.unlockRoomReturns
|
||||
fake.recordInvocation("UnlockRoom", []interface{}{arg1, arg2, arg3})
|
||||
fake.unlockRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) UnlockRoomCallCount() int {
|
||||
fake.unlockRoomMutex.RLock()
|
||||
defer fake.unlockRoomMutex.RUnlock()
|
||||
return len(fake.unlockRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) UnlockRoomCalls(stub func(context.Context, livekit.RoomName, string) error) {
|
||||
fake.unlockRoomMutex.Lock()
|
||||
defer fake.unlockRoomMutex.Unlock()
|
||||
fake.UnlockRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) UnlockRoomArgsForCall(i int) (context.Context, livekit.RoomName, string) {
|
||||
fake.unlockRoomMutex.RLock()
|
||||
defer fake.unlockRoomMutex.RUnlock()
|
||||
argsForCall := fake.unlockRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) UnlockRoomReturns(result1 error) {
|
||||
fake.unlockRoomMutex.Lock()
|
||||
defer fake.unlockRoomMutex.Unlock()
|
||||
fake.UnlockRoomStub = nil
|
||||
fake.unlockRoomReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) UnlockRoomReturnsOnCall(i int, result1 error) {
|
||||
fake.unlockRoomMutex.Lock()
|
||||
defer fake.unlockRoomMutex.Unlock()
|
||||
fake.UnlockRoomStub = nil
|
||||
if fake.unlockRoomReturnsOnCall == nil {
|
||||
fake.unlockRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.unlockRoomReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeObjectStore) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.ObjectStore = new(FakeObjectStore)
|
||||
352
pkg/service/servicefakes/fake_room_allocator.go
Normal file
352
pkg/service/servicefakes/fake_room_allocator.go
Normal file
@@ -0,0 +1,352 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeRoomAllocator struct {
|
||||
AutoCreateEnabledStub func(context.Context) bool
|
||||
autoCreateEnabledMutex sync.RWMutex
|
||||
autoCreateEnabledArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
}
|
||||
autoCreateEnabledReturns struct {
|
||||
result1 bool
|
||||
}
|
||||
autoCreateEnabledReturnsOnCall map[int]struct {
|
||||
result1 bool
|
||||
}
|
||||
CreateRoomStub func(context.Context, *livekit.CreateRoomRequest, bool) (*livekit.Room, *livekit.RoomInternal, bool, error)
|
||||
createRoomMutex sync.RWMutex
|
||||
createRoomArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.CreateRoomRequest
|
||||
arg3 bool
|
||||
}
|
||||
createRoomReturns struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 bool
|
||||
result4 error
|
||||
}
|
||||
createRoomReturnsOnCall map[int]struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 bool
|
||||
result4 error
|
||||
}
|
||||
SelectRoomNodeStub func(context.Context, livekit.RoomName, livekit.NodeID) error
|
||||
selectRoomNodeMutex sync.RWMutex
|
||||
selectRoomNodeArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 livekit.NodeID
|
||||
}
|
||||
selectRoomNodeReturns struct {
|
||||
result1 error
|
||||
}
|
||||
selectRoomNodeReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
ValidateCreateRoomStub func(context.Context, livekit.RoomName) error
|
||||
validateCreateRoomMutex sync.RWMutex
|
||||
validateCreateRoomArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
}
|
||||
validateCreateRoomReturns struct {
|
||||
result1 error
|
||||
}
|
||||
validateCreateRoomReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) AutoCreateEnabled(arg1 context.Context) bool {
|
||||
fake.autoCreateEnabledMutex.Lock()
|
||||
ret, specificReturn := fake.autoCreateEnabledReturnsOnCall[len(fake.autoCreateEnabledArgsForCall)]
|
||||
fake.autoCreateEnabledArgsForCall = append(fake.autoCreateEnabledArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
}{arg1})
|
||||
stub := fake.AutoCreateEnabledStub
|
||||
fakeReturns := fake.autoCreateEnabledReturns
|
||||
fake.recordInvocation("AutoCreateEnabled", []interface{}{arg1})
|
||||
fake.autoCreateEnabledMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) AutoCreateEnabledCallCount() int {
|
||||
fake.autoCreateEnabledMutex.RLock()
|
||||
defer fake.autoCreateEnabledMutex.RUnlock()
|
||||
return len(fake.autoCreateEnabledArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) AutoCreateEnabledCalls(stub func(context.Context) bool) {
|
||||
fake.autoCreateEnabledMutex.Lock()
|
||||
defer fake.autoCreateEnabledMutex.Unlock()
|
||||
fake.AutoCreateEnabledStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) AutoCreateEnabledArgsForCall(i int) context.Context {
|
||||
fake.autoCreateEnabledMutex.RLock()
|
||||
defer fake.autoCreateEnabledMutex.RUnlock()
|
||||
argsForCall := fake.autoCreateEnabledArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) AutoCreateEnabledReturns(result1 bool) {
|
||||
fake.autoCreateEnabledMutex.Lock()
|
||||
defer fake.autoCreateEnabledMutex.Unlock()
|
||||
fake.AutoCreateEnabledStub = nil
|
||||
fake.autoCreateEnabledReturns = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) AutoCreateEnabledReturnsOnCall(i int, result1 bool) {
|
||||
fake.autoCreateEnabledMutex.Lock()
|
||||
defer fake.autoCreateEnabledMutex.Unlock()
|
||||
fake.AutoCreateEnabledStub = nil
|
||||
if fake.autoCreateEnabledReturnsOnCall == nil {
|
||||
fake.autoCreateEnabledReturnsOnCall = make(map[int]struct {
|
||||
result1 bool
|
||||
})
|
||||
}
|
||||
fake.autoCreateEnabledReturnsOnCall[i] = struct {
|
||||
result1 bool
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) CreateRoom(arg1 context.Context, arg2 *livekit.CreateRoomRequest, arg3 bool) (*livekit.Room, *livekit.RoomInternal, bool, error) {
|
||||
fake.createRoomMutex.Lock()
|
||||
ret, specificReturn := fake.createRoomReturnsOnCall[len(fake.createRoomArgsForCall)]
|
||||
fake.createRoomArgsForCall = append(fake.createRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 *livekit.CreateRoomRequest
|
||||
arg3 bool
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.CreateRoomStub
|
||||
fakeReturns := fake.createRoomReturns
|
||||
fake.recordInvocation("CreateRoom", []interface{}{arg1, arg2, arg3})
|
||||
fake.createRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2, ret.result3, ret.result4
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3, fakeReturns.result4
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) CreateRoomCallCount() int {
|
||||
fake.createRoomMutex.RLock()
|
||||
defer fake.createRoomMutex.RUnlock()
|
||||
return len(fake.createRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) CreateRoomCalls(stub func(context.Context, *livekit.CreateRoomRequest, bool) (*livekit.Room, *livekit.RoomInternal, bool, error)) {
|
||||
fake.createRoomMutex.Lock()
|
||||
defer fake.createRoomMutex.Unlock()
|
||||
fake.CreateRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) CreateRoomArgsForCall(i int) (context.Context, *livekit.CreateRoomRequest, bool) {
|
||||
fake.createRoomMutex.RLock()
|
||||
defer fake.createRoomMutex.RUnlock()
|
||||
argsForCall := fake.createRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) CreateRoomReturns(result1 *livekit.Room, result2 *livekit.RoomInternal, result3 bool, result4 error) {
|
||||
fake.createRoomMutex.Lock()
|
||||
defer fake.createRoomMutex.Unlock()
|
||||
fake.CreateRoomStub = nil
|
||||
fake.createRoomReturns = struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 bool
|
||||
result4 error
|
||||
}{result1, result2, result3, result4}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) CreateRoomReturnsOnCall(i int, result1 *livekit.Room, result2 *livekit.RoomInternal, result3 bool, result4 error) {
|
||||
fake.createRoomMutex.Lock()
|
||||
defer fake.createRoomMutex.Unlock()
|
||||
fake.CreateRoomStub = nil
|
||||
if fake.createRoomReturnsOnCall == nil {
|
||||
fake.createRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 bool
|
||||
result4 error
|
||||
})
|
||||
}
|
||||
fake.createRoomReturnsOnCall[i] = struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 bool
|
||||
result4 error
|
||||
}{result1, result2, result3, result4}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) SelectRoomNode(arg1 context.Context, arg2 livekit.RoomName, arg3 livekit.NodeID) error {
|
||||
fake.selectRoomNodeMutex.Lock()
|
||||
ret, specificReturn := fake.selectRoomNodeReturnsOnCall[len(fake.selectRoomNodeArgsForCall)]
|
||||
fake.selectRoomNodeArgsForCall = append(fake.selectRoomNodeArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 livekit.NodeID
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.SelectRoomNodeStub
|
||||
fakeReturns := fake.selectRoomNodeReturns
|
||||
fake.recordInvocation("SelectRoomNode", []interface{}{arg1, arg2, arg3})
|
||||
fake.selectRoomNodeMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) SelectRoomNodeCallCount() int {
|
||||
fake.selectRoomNodeMutex.RLock()
|
||||
defer fake.selectRoomNodeMutex.RUnlock()
|
||||
return len(fake.selectRoomNodeArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) SelectRoomNodeCalls(stub func(context.Context, livekit.RoomName, livekit.NodeID) error) {
|
||||
fake.selectRoomNodeMutex.Lock()
|
||||
defer fake.selectRoomNodeMutex.Unlock()
|
||||
fake.SelectRoomNodeStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) SelectRoomNodeArgsForCall(i int) (context.Context, livekit.RoomName, livekit.NodeID) {
|
||||
fake.selectRoomNodeMutex.RLock()
|
||||
defer fake.selectRoomNodeMutex.RUnlock()
|
||||
argsForCall := fake.selectRoomNodeArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) SelectRoomNodeReturns(result1 error) {
|
||||
fake.selectRoomNodeMutex.Lock()
|
||||
defer fake.selectRoomNodeMutex.Unlock()
|
||||
fake.SelectRoomNodeStub = nil
|
||||
fake.selectRoomNodeReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) SelectRoomNodeReturnsOnCall(i int, result1 error) {
|
||||
fake.selectRoomNodeMutex.Lock()
|
||||
defer fake.selectRoomNodeMutex.Unlock()
|
||||
fake.SelectRoomNodeStub = nil
|
||||
if fake.selectRoomNodeReturnsOnCall == nil {
|
||||
fake.selectRoomNodeReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.selectRoomNodeReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) ValidateCreateRoom(arg1 context.Context, arg2 livekit.RoomName) error {
|
||||
fake.validateCreateRoomMutex.Lock()
|
||||
ret, specificReturn := fake.validateCreateRoomReturnsOnCall[len(fake.validateCreateRoomArgsForCall)]
|
||||
fake.validateCreateRoomArgsForCall = append(fake.validateCreateRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
}{arg1, arg2})
|
||||
stub := fake.ValidateCreateRoomStub
|
||||
fakeReturns := fake.validateCreateRoomReturns
|
||||
fake.recordInvocation("ValidateCreateRoom", []interface{}{arg1, arg2})
|
||||
fake.validateCreateRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) ValidateCreateRoomCallCount() int {
|
||||
fake.validateCreateRoomMutex.RLock()
|
||||
defer fake.validateCreateRoomMutex.RUnlock()
|
||||
return len(fake.validateCreateRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) ValidateCreateRoomCalls(stub func(context.Context, livekit.RoomName) error) {
|
||||
fake.validateCreateRoomMutex.Lock()
|
||||
defer fake.validateCreateRoomMutex.Unlock()
|
||||
fake.ValidateCreateRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) ValidateCreateRoomArgsForCall(i int) (context.Context, livekit.RoomName) {
|
||||
fake.validateCreateRoomMutex.RLock()
|
||||
defer fake.validateCreateRoomMutex.RUnlock()
|
||||
argsForCall := fake.validateCreateRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) ValidateCreateRoomReturns(result1 error) {
|
||||
fake.validateCreateRoomMutex.Lock()
|
||||
defer fake.validateCreateRoomMutex.Unlock()
|
||||
fake.ValidateCreateRoomStub = nil
|
||||
fake.validateCreateRoomReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) ValidateCreateRoomReturnsOnCall(i int, result1 error) {
|
||||
fake.validateCreateRoomMutex.Lock()
|
||||
defer fake.validateCreateRoomMutex.Unlock()
|
||||
fake.ValidateCreateRoomStub = nil
|
||||
if fake.validateCreateRoomReturnsOnCall == nil {
|
||||
fake.validateCreateRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.validateCreateRoomReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeRoomAllocator) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.RoomAllocator = new(FakeRoomAllocator)
|
||||
288
pkg/service/servicefakes/fake_service_store.go
Normal file
288
pkg/service/servicefakes/fake_service_store.go
Normal file
@@ -0,0 +1,288 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type FakeServiceStore struct {
|
||||
ListRoomsStub func(context.Context, []livekit.RoomName) ([]*livekit.Room, error)
|
||||
listRoomsMutex sync.RWMutex
|
||||
listRoomsArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 []livekit.RoomName
|
||||
}
|
||||
listRoomsReturns struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
}
|
||||
listRoomsReturnsOnCall map[int]struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
}
|
||||
LoadRoomStub func(context.Context, livekit.RoomName, bool) (*livekit.Room, *livekit.RoomInternal, error)
|
||||
loadRoomMutex sync.RWMutex
|
||||
loadRoomArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 bool
|
||||
}
|
||||
loadRoomReturns struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}
|
||||
loadRoomReturnsOnCall map[int]struct {
|
||||
result1 *livekit.Room
|
||||
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) ListRooms(arg1 context.Context, arg2 []livekit.RoomName) ([]*livekit.Room, error) {
|
||||
var arg2Copy []livekit.RoomName
|
||||
if arg2 != nil {
|
||||
arg2Copy = make([]livekit.RoomName, len(arg2))
|
||||
copy(arg2Copy, arg2)
|
||||
}
|
||||
fake.listRoomsMutex.Lock()
|
||||
ret, specificReturn := fake.listRoomsReturnsOnCall[len(fake.listRoomsArgsForCall)]
|
||||
fake.listRoomsArgsForCall = append(fake.listRoomsArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 []livekit.RoomName
|
||||
}{arg1, arg2Copy})
|
||||
stub := fake.ListRoomsStub
|
||||
fakeReturns := fake.listRoomsReturns
|
||||
fake.recordInvocation("ListRooms", []interface{}{arg1, arg2Copy})
|
||||
fake.listRoomsMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) ListRoomsCallCount() int {
|
||||
fake.listRoomsMutex.RLock()
|
||||
defer fake.listRoomsMutex.RUnlock()
|
||||
return len(fake.listRoomsArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) ListRoomsCalls(stub func(context.Context, []livekit.RoomName) ([]*livekit.Room, error)) {
|
||||
fake.listRoomsMutex.Lock()
|
||||
defer fake.listRoomsMutex.Unlock()
|
||||
fake.ListRoomsStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) ListRoomsArgsForCall(i int) (context.Context, []livekit.RoomName) {
|
||||
fake.listRoomsMutex.RLock()
|
||||
defer fake.listRoomsMutex.RUnlock()
|
||||
argsForCall := fake.listRoomsArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) ListRoomsReturns(result1 []*livekit.Room, result2 error) {
|
||||
fake.listRoomsMutex.Lock()
|
||||
defer fake.listRoomsMutex.Unlock()
|
||||
fake.ListRoomsStub = nil
|
||||
fake.listRoomsReturns = struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) ListRoomsReturnsOnCall(i int, result1 []*livekit.Room, result2 error) {
|
||||
fake.listRoomsMutex.Lock()
|
||||
defer fake.listRoomsMutex.Unlock()
|
||||
fake.ListRoomsStub = nil
|
||||
if fake.listRoomsReturnsOnCall == nil {
|
||||
fake.listRoomsReturnsOnCall = make(map[int]struct {
|
||||
result1 []*livekit.Room
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.listRoomsReturnsOnCall[i] = struct {
|
||||
result1 []*livekit.Room
|
||||
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)]
|
||||
fake.loadRoomArgsForCall = append(fake.loadRoomArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 livekit.RoomName
|
||||
arg3 bool
|
||||
}{arg1, arg2, arg3})
|
||||
stub := fake.LoadRoomStub
|
||||
fakeReturns := fake.loadRoomReturns
|
||||
fake.recordInvocation("LoadRoom", []interface{}{arg1, arg2, arg3})
|
||||
fake.loadRoomMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2, ret.result3
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) LoadRoomCallCount() int {
|
||||
fake.loadRoomMutex.RLock()
|
||||
defer fake.loadRoomMutex.RUnlock()
|
||||
return len(fake.loadRoomArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) LoadRoomCalls(stub func(context.Context, livekit.RoomName, bool) (*livekit.Room, *livekit.RoomInternal, error)) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
defer fake.loadRoomMutex.Unlock()
|
||||
fake.LoadRoomStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) LoadRoomArgsForCall(i int) (context.Context, livekit.RoomName, bool) {
|
||||
fake.loadRoomMutex.RLock()
|
||||
defer fake.loadRoomMutex.RUnlock()
|
||||
argsForCall := fake.loadRoomArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) LoadRoomReturns(result1 *livekit.Room, result2 *livekit.RoomInternal, result3 error) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
defer fake.loadRoomMutex.Unlock()
|
||||
fake.LoadRoomStub = nil
|
||||
fake.loadRoomReturns = struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}{result1, result2, result3}
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) LoadRoomReturnsOnCall(i int, result1 *livekit.Room, result2 *livekit.RoomInternal, result3 error) {
|
||||
fake.loadRoomMutex.Lock()
|
||||
defer fake.loadRoomMutex.Unlock()
|
||||
fake.LoadRoomStub = nil
|
||||
if fake.loadRoomReturnsOnCall == nil {
|
||||
fake.loadRoomReturnsOnCall = make(map[int]struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
})
|
||||
}
|
||||
fake.loadRoomReturnsOnCall[i] = struct {
|
||||
result1 *livekit.Room
|
||||
result2 *livekit.RoomInternal
|
||||
result3 error
|
||||
}{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()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeServiceStore) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.ServiceStore = new(FakeServiceStore)
|
||||
193
pkg/service/servicefakes/fake_session_handler.go
Normal file
193
pkg/service/servicefakes/fake_session_handler.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Code generated by counterfeiter. DO NOT EDIT.
|
||||
package servicefakes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
type FakeSessionHandler struct {
|
||||
HandleSessionStub func(context.Context, routing.ParticipantInit, livekit.ConnectionID, routing.MessageSource, routing.MessageSink) error
|
||||
handleSessionMutex sync.RWMutex
|
||||
handleSessionArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 routing.ParticipantInit
|
||||
arg3 livekit.ConnectionID
|
||||
arg4 routing.MessageSource
|
||||
arg5 routing.MessageSink
|
||||
}
|
||||
handleSessionReturns struct {
|
||||
result1 error
|
||||
}
|
||||
handleSessionReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
LoggerStub func(context.Context) logger.Logger
|
||||
loggerMutex sync.RWMutex
|
||||
loggerArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
}
|
||||
loggerReturns struct {
|
||||
result1 logger.Logger
|
||||
}
|
||||
loggerReturnsOnCall map[int]struct {
|
||||
result1 logger.Logger
|
||||
}
|
||||
invocations map[string][][]interface{}
|
||||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) HandleSession(arg1 context.Context, arg2 routing.ParticipantInit, arg3 livekit.ConnectionID, arg4 routing.MessageSource, arg5 routing.MessageSink) error {
|
||||
fake.handleSessionMutex.Lock()
|
||||
ret, specificReturn := fake.handleSessionReturnsOnCall[len(fake.handleSessionArgsForCall)]
|
||||
fake.handleSessionArgsForCall = append(fake.handleSessionArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 routing.ParticipantInit
|
||||
arg3 livekit.ConnectionID
|
||||
arg4 routing.MessageSource
|
||||
arg5 routing.MessageSink
|
||||
}{arg1, arg2, arg3, arg4, arg5})
|
||||
stub := fake.HandleSessionStub
|
||||
fakeReturns := fake.handleSessionReturns
|
||||
fake.recordInvocation("HandleSession", []interface{}{arg1, arg2, arg3, arg4, arg5})
|
||||
fake.handleSessionMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2, arg3, arg4, arg5)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) HandleSessionCallCount() int {
|
||||
fake.handleSessionMutex.RLock()
|
||||
defer fake.handleSessionMutex.RUnlock()
|
||||
return len(fake.handleSessionArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) HandleSessionCalls(stub func(context.Context, routing.ParticipantInit, livekit.ConnectionID, routing.MessageSource, routing.MessageSink) error) {
|
||||
fake.handleSessionMutex.Lock()
|
||||
defer fake.handleSessionMutex.Unlock()
|
||||
fake.HandleSessionStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) HandleSessionArgsForCall(i int) (context.Context, routing.ParticipantInit, livekit.ConnectionID, routing.MessageSource, routing.MessageSink) {
|
||||
fake.handleSessionMutex.RLock()
|
||||
defer fake.handleSessionMutex.RUnlock()
|
||||
argsForCall := fake.handleSessionArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) HandleSessionReturns(result1 error) {
|
||||
fake.handleSessionMutex.Lock()
|
||||
defer fake.handleSessionMutex.Unlock()
|
||||
fake.HandleSessionStub = nil
|
||||
fake.handleSessionReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) HandleSessionReturnsOnCall(i int, result1 error) {
|
||||
fake.handleSessionMutex.Lock()
|
||||
defer fake.handleSessionMutex.Unlock()
|
||||
fake.HandleSessionStub = nil
|
||||
if fake.handleSessionReturnsOnCall == nil {
|
||||
fake.handleSessionReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.handleSessionReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) Logger(arg1 context.Context) logger.Logger {
|
||||
fake.loggerMutex.Lock()
|
||||
ret, specificReturn := fake.loggerReturnsOnCall[len(fake.loggerArgsForCall)]
|
||||
fake.loggerArgsForCall = append(fake.loggerArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
}{arg1})
|
||||
stub := fake.LoggerStub
|
||||
fakeReturns := fake.loggerReturns
|
||||
fake.recordInvocation("Logger", []interface{}{arg1})
|
||||
fake.loggerMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) LoggerCallCount() int {
|
||||
fake.loggerMutex.RLock()
|
||||
defer fake.loggerMutex.RUnlock()
|
||||
return len(fake.loggerArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) LoggerCalls(stub func(context.Context) logger.Logger) {
|
||||
fake.loggerMutex.Lock()
|
||||
defer fake.loggerMutex.Unlock()
|
||||
fake.LoggerStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) LoggerArgsForCall(i int) context.Context {
|
||||
fake.loggerMutex.RLock()
|
||||
defer fake.loggerMutex.RUnlock()
|
||||
argsForCall := fake.loggerArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) LoggerReturns(result1 logger.Logger) {
|
||||
fake.loggerMutex.Lock()
|
||||
defer fake.loggerMutex.Unlock()
|
||||
fake.LoggerStub = nil
|
||||
fake.loggerReturns = struct {
|
||||
result1 logger.Logger
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) LoggerReturnsOnCall(i int, result1 logger.Logger) {
|
||||
fake.loggerMutex.Lock()
|
||||
defer fake.loggerMutex.Unlock()
|
||||
fake.LoggerStub = nil
|
||||
if fake.loggerReturnsOnCall == nil {
|
||||
fake.loggerReturnsOnCall = make(map[int]struct {
|
||||
result1 logger.Logger
|
||||
})
|
||||
}
|
||||
fake.loggerReturnsOnCall[i] = struct {
|
||||
result1 logger.Logger
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
for key, value := range fake.invocations {
|
||||
copiedInvocations[key] = value
|
||||
}
|
||||
return copiedInvocations
|
||||
}
|
||||
|
||||
func (fake *FakeSessionHandler) recordInvocation(key string, args []interface{}) {
|
||||
fake.invocationsMutex.Lock()
|
||||
defer fake.invocationsMutex.Unlock()
|
||||
if fake.invocations == nil {
|
||||
fake.invocations = map[string][][]interface{}{}
|
||||
}
|
||||
if fake.invocations[key] == nil {
|
||||
fake.invocations[key] = [][]interface{}{}
|
||||
}
|
||||
fake.invocations[key] = append(fake.invocations[key], args)
|
||||
}
|
||||
|
||||
var _ service.SessionHandler = new(FakeSessionHandler)
|
||||
1115
pkg/service/servicefakes/fake_sipstore.go
Normal file
1115
pkg/service/servicefakes/fake_sipstore.go
Normal file
File diff suppressed because it is too large
Load Diff
222
pkg/service/signal.go
Normal file
222
pkg/service/signal.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"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"
|
||||
"github.com/livekit/psrpc"
|
||||
"github.com/livekit/psrpc/pkg/metadata"
|
||||
"github.com/livekit/psrpc/pkg/middleware"
|
||||
)
|
||||
|
||||
//counterfeiter:generate . SessionHandler
|
||||
type SessionHandler interface {
|
||||
Logger(ctx context.Context) logger.Logger
|
||||
|
||||
HandleSession(
|
||||
ctx context.Context,
|
||||
pi routing.ParticipantInit,
|
||||
connectionID livekit.ConnectionID,
|
||||
requestSource routing.MessageSource,
|
||||
responseSink routing.MessageSink,
|
||||
) error
|
||||
}
|
||||
|
||||
type SignalServer struct {
|
||||
server rpc.TypedSignalServer
|
||||
nodeID livekit.NodeID
|
||||
}
|
||||
|
||||
func NewSignalServer(
|
||||
nodeID livekit.NodeID,
|
||||
region string,
|
||||
bus psrpc.MessageBus,
|
||||
config config.SignalRelayConfig,
|
||||
sessionHandler SessionHandler,
|
||||
) (*SignalServer, error) {
|
||||
s, err := rpc.NewTypedSignalServer(
|
||||
nodeID,
|
||||
&signalService{region, sessionHandler, config},
|
||||
bus,
|
||||
middleware.WithServerMetrics(rpc.PSRPCMetricsObserver{}),
|
||||
psrpc.WithServerChannelSize(config.StreamBufferSize),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SignalServer{s, nodeID}, nil
|
||||
}
|
||||
|
||||
func NewDefaultSignalServer(
|
||||
currentNode routing.LocalNode,
|
||||
bus psrpc.MessageBus,
|
||||
config config.SignalRelayConfig,
|
||||
router routing.Router,
|
||||
roomManager *RoomManager,
|
||||
) (r *SignalServer, err error) {
|
||||
return NewSignalServer(currentNode.NodeID(), currentNode.Region(), bus, config, &defaultSessionHandler{currentNode, router, roomManager})
|
||||
}
|
||||
|
||||
type defaultSessionHandler struct {
|
||||
currentNode routing.LocalNode
|
||||
router routing.Router
|
||||
roomManager *RoomManager
|
||||
}
|
||||
|
||||
func (s *defaultSessionHandler) Logger(ctx context.Context) logger.Logger {
|
||||
return utils.GetLogger(ctx)
|
||||
}
|
||||
|
||||
func (s *defaultSessionHandler) HandleSession(
|
||||
ctx context.Context,
|
||||
pi routing.ParticipantInit,
|
||||
connectionID livekit.ConnectionID,
|
||||
requestSource routing.MessageSource,
|
||||
responseSink routing.MessageSink,
|
||||
) error {
|
||||
prometheus.IncrementParticipantRtcInit(1)
|
||||
|
||||
rtcNode, err := s.router.GetNodeForRoom(ctx, livekit.RoomName(pi.CreateRoom.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if livekit.NodeID(rtcNode.Id) != s.currentNode.NodeID() {
|
||||
err = routing.ErrIncorrectRTCNode
|
||||
logger.Errorw("called participant on incorrect node", err,
|
||||
"rtcNode", rtcNode,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return s.roomManager.StartSession(ctx, pi, requestSource, responseSink, false)
|
||||
}
|
||||
|
||||
func (s *SignalServer) Start() error {
|
||||
logger.Debugw("starting relay signal server", "topic", s.nodeID)
|
||||
return s.server.RegisterAllNodeTopics(s.nodeID)
|
||||
}
|
||||
|
||||
func (s *SignalServer) Stop() {
|
||||
s.server.Kill()
|
||||
}
|
||||
|
||||
type signalService struct {
|
||||
region string
|
||||
sessionHandler SessionHandler
|
||||
config config.SignalRelayConfig
|
||||
}
|
||||
|
||||
func (r *signalService) RelaySignal(stream psrpc.ServerStream[*rpc.RelaySignalResponse, *rpc.RelaySignalRequest]) (err error) {
|
||||
var req *rpc.RelaySignalRequest
|
||||
var ok bool
|
||||
select {
|
||||
case req, ok = <-stream.Channel():
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
case <-stream.Context().Done():
|
||||
return stream.Context().Err()
|
||||
case <-time.After(r.config.RetryTimeout):
|
||||
return errors.New("timeout waiting for start session")
|
||||
}
|
||||
|
||||
ss := req.StartSession
|
||||
if ss == nil {
|
||||
return errors.New("expected start session message")
|
||||
}
|
||||
|
||||
pi, err := routing.ParticipantInitFromStartSession(ss, r.region)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read participant from session")
|
||||
}
|
||||
|
||||
l := r.sessionHandler.Logger(stream.Context()).WithValues(
|
||||
"room", ss.RoomName,
|
||||
"participant", ss.Identity,
|
||||
"connID", ss.ConnectionId,
|
||||
)
|
||||
|
||||
stream.Hijack()
|
||||
sink := routing.NewSignalMessageSink(routing.SignalSinkParams[*rpc.RelaySignalResponse, *rpc.RelaySignalRequest]{
|
||||
Logger: l,
|
||||
Stream: stream,
|
||||
Config: r.config,
|
||||
Writer: signalResponseMessageWriter{},
|
||||
ConnectionID: livekit.ConnectionID(ss.ConnectionId),
|
||||
})
|
||||
reqChan := routing.NewDefaultMessageChannel(livekit.ConnectionID(ss.ConnectionId))
|
||||
|
||||
go func() {
|
||||
err := routing.CopySignalStreamToMessageChannel[*rpc.RelaySignalResponse, *rpc.RelaySignalRequest](
|
||||
stream,
|
||||
reqChan,
|
||||
signalRequestMessageReader{},
|
||||
r.config,
|
||||
prometheus.RecordSignalRequestSuccess,
|
||||
prometheus.RecordSignalRequestFailure,
|
||||
)
|
||||
l.Debugw("signal stream closed", "error", err)
|
||||
|
||||
reqChan.Close()
|
||||
}()
|
||||
|
||||
// copy the context to prevent a race between the session handler closing
|
||||
// and the delivery of any parting messages from the client. take care to
|
||||
// copy the incoming rpc headers to avoid dropping any session vars.
|
||||
ctx := metadata.NewContextWithIncomingHeader(context.Background(), metadata.IncomingHeader(stream.Context()))
|
||||
err = r.sessionHandler.HandleSession(ctx, *pi, livekit.ConnectionID(ss.ConnectionId), reqChan, sink)
|
||||
if err != nil {
|
||||
sink.Close()
|
||||
l.Errorw("could not handle new participant", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type signalResponseMessageWriter struct{}
|
||||
|
||||
func (e signalResponseMessageWriter) Write(seq uint64, close bool, msgs []proto.Message) *rpc.RelaySignalResponse {
|
||||
r := &rpc.RelaySignalResponse{
|
||||
Seq: seq,
|
||||
Responses: make([]*livekit.SignalResponse, 0, len(msgs)),
|
||||
Close: close,
|
||||
}
|
||||
for _, m := range msgs {
|
||||
r.Responses = append(r.Responses, m.(*livekit.SignalResponse))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type signalRequestMessageReader struct{}
|
||||
|
||||
func (e signalRequestMessageReader) Read(rm *rpc.RelaySignalRequest) ([]proto.Message, error) {
|
||||
msgs := make([]proto.Message, 0, len(rm.Requests))
|
||||
for _, m := range rm.Requests {
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
156
pkg/service/signal_test.go
Normal file
156
pkg/service/signal_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/routing"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
"github.com/livekit/livekit-server/pkg/service/servicefakes"
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.Init("node", livekit.NodeType_CONTROLLER)
|
||||
}
|
||||
|
||||
func TestSignal(t *testing.T) {
|
||||
cfg := config.SignalRelayConfig{
|
||||
RetryTimeout: 30 * time.Second,
|
||||
MinRetryInterval: 500 * time.Millisecond,
|
||||
MaxRetryInterval: 5 * time.Second,
|
||||
StreamBufferSize: 1000,
|
||||
}
|
||||
|
||||
t.Run("messages are delivered", func(t *testing.T) {
|
||||
bus := psrpc.NewLocalMessageBus()
|
||||
|
||||
reqMessageIn := &livekit.SignalRequest{
|
||||
Message: &livekit.SignalRequest_Ping{Ping: 123},
|
||||
}
|
||||
resMessageIn := &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Pong{Pong: 321},
|
||||
}
|
||||
|
||||
var reqMessageOut proto.Message
|
||||
var resErr error
|
||||
done := make(chan struct{})
|
||||
|
||||
client, err := routing.NewSignalClient(livekit.NodeID("node0"), bus, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := &servicefakes.FakeSessionHandler{
|
||||
LoggerStub: func(context.Context) logger.Logger { return logger.GetLogger() },
|
||||
HandleSessionStub: func(
|
||||
ctx context.Context,
|
||||
pi routing.ParticipantInit,
|
||||
connectionID livekit.ConnectionID,
|
||||
requestSource routing.MessageSource,
|
||||
responseSink routing.MessageSink,
|
||||
) error {
|
||||
go func() {
|
||||
reqMessageOut = <-requestSource.ReadChan()
|
||||
resErr = responseSink.WriteMessage(resMessageIn)
|
||||
responseSink.Close()
|
||||
close(done)
|
||||
}()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
server, err := service.NewSignalServer(livekit.NodeID("node1"), "region", bus, cfg, handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, reqSink, resSource, err := client.StartParticipantSignal(
|
||||
context.Background(),
|
||||
livekit.RoomName("room1"),
|
||||
routing.ParticipantInit{},
|
||||
livekit.NodeID("node1"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = reqSink.WriteMessage(reqMessageIn)
|
||||
require.NoError(t, err)
|
||||
|
||||
<-done
|
||||
require.True(t, proto.Equal(reqMessageIn, reqMessageOut), "req message should match %s %s", protojson.Format(reqMessageIn), protojson.Format(reqMessageOut))
|
||||
require.NoError(t, resErr)
|
||||
|
||||
resMessageOut := <-resSource.ReadChan()
|
||||
require.True(t, proto.Equal(resMessageIn, resMessageOut), "res message should match %s %s", protojson.Format(resMessageIn), protojson.Format(resMessageOut))
|
||||
})
|
||||
|
||||
t.Run("messages are delivered when session handler fails", func(t *testing.T) {
|
||||
bus := psrpc.NewLocalMessageBus()
|
||||
|
||||
resMessageIn := &livekit.SignalResponse{
|
||||
Message: &livekit.SignalResponse_Pong{Pong: 321},
|
||||
}
|
||||
|
||||
var resErr error
|
||||
done := make(chan struct{})
|
||||
|
||||
client, err := routing.NewSignalClient(livekit.NodeID("node0"), bus, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := &servicefakes.FakeSessionHandler{
|
||||
LoggerStub: func(context.Context) logger.Logger { return logger.GetLogger() },
|
||||
HandleSessionStub: func(
|
||||
ctx context.Context,
|
||||
pi routing.ParticipantInit,
|
||||
connectionID livekit.ConnectionID,
|
||||
requestSource routing.MessageSource,
|
||||
responseSink routing.MessageSink,
|
||||
) error {
|
||||
defer close(done)
|
||||
resErr = responseSink.WriteMessage(resMessageIn)
|
||||
return errors.New("start session failed")
|
||||
},
|
||||
}
|
||||
server, err := service.NewSignalServer(livekit.NodeID("node1"), "region", bus, cfg, handler)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, resSource, err := client.StartParticipantSignal(
|
||||
context.Background(),
|
||||
livekit.RoomName("room1"),
|
||||
routing.ParticipantInit{},
|
||||
livekit.NodeID("node1"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
<-done
|
||||
require.NoError(t, resErr)
|
||||
|
||||
resMessageOut := <-resSource.ReadChan()
|
||||
require.True(t, proto.Equal(resMessageIn, resMessageOut), "res message should match %s %s", protojson.Format(resMessageIn), protojson.Format(resMessageOut))
|
||||
})
|
||||
}
|
||||
800
pkg/service/sip.go
Normal file
800
pkg/service/sip.go
Normal file
@@ -0,0 +1,800 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/dennwc/iters"
|
||||
"github.com/twitchtv/twirp"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/sip"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/utils/guid"
|
||||
"github.com/livekit/psrpc"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
)
|
||||
|
||||
type SIPService struct {
|
||||
conf *config.SIPConfig
|
||||
nodeID livekit.NodeID
|
||||
bus psrpc.MessageBus
|
||||
psrpcClient rpc.SIPClient
|
||||
store SIPStore
|
||||
roomService livekit.RoomService
|
||||
}
|
||||
|
||||
func NewSIPService(
|
||||
conf *config.SIPConfig,
|
||||
nodeID livekit.NodeID,
|
||||
bus psrpc.MessageBus,
|
||||
psrpcClient rpc.SIPClient,
|
||||
store SIPStore,
|
||||
rs livekit.RoomService,
|
||||
ts telemetry.TelemetryService,
|
||||
) *SIPService {
|
||||
return &SIPService{
|
||||
conf: conf,
|
||||
nodeID: nodeID,
|
||||
bus: bus,
|
||||
psrpcClient: psrpcClient,
|
||||
store: store,
|
||||
roomService: rs,
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a new UnlikelyLogger, populated with values from the request context
|
||||
// (e.g. service, method, etc.).
|
||||
func unlikelyLoggerFromCtx(ctx context.Context) logger.UnlikelyLogger {
|
||||
var logFields []any
|
||||
logCtx, ok := ctx.Value(twirpLoggerKey{}).(*twirpLogger)
|
||||
if ok {
|
||||
logFields = logCtx.fields
|
||||
}
|
||||
return logger.GetLogger().WithUnlikelyValues(logFields...)
|
||||
}
|
||||
|
||||
func (s *SIPService) CreateSIPTrunk(ctx context.Context, req *livekit.CreateSIPTrunkRequest) (*livekit.SIPTrunkInfo, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if len(req.InboundNumbersRegex) != 0 {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "Trunks with InboundNumbersRegex are deprecated. Use InboundNumbers instead.")
|
||||
}
|
||||
|
||||
// Keep ID empty, so that validation can print "<new>" instead of a non-existent ID in the error.
|
||||
info := &livekit.SIPTrunkInfo{
|
||||
InboundAddresses: req.InboundAddresses,
|
||||
OutboundAddress: req.OutboundAddress,
|
||||
OutboundNumber: req.OutboundNumber,
|
||||
InboundNumbers: req.InboundNumbers,
|
||||
InboundUsername: req.InboundUsername,
|
||||
InboundPassword: req.InboundPassword,
|
||||
OutboundUsername: req.OutboundUsername,
|
||||
OutboundPassword: req.OutboundPassword,
|
||||
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())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer it.Close()
|
||||
if err = sip.ValidateTrunksIter(it); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we can generate ID and store.
|
||||
info.SipTrunkId = guid.New(utils.SIPTrunkPrefix)
|
||||
if err := s.store.StoreSIPTrunk(ctx, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) CreateSIPInboundTrunk(ctx context.Context, req *livekit.CreateSIPInboundTrunkRequest) (*livekit.SIPInboundTrunkInfo, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if err := req.ValidateResult().LogUnlikelySoftErrors(unlikelyLoggerFromCtx(ctx)); err != nil {
|
||||
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
|
||||
}
|
||||
|
||||
info := req.Trunk
|
||||
if info.SipTrunkId != "" {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID must be empty")
|
||||
}
|
||||
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.
|
||||
|
||||
// Validate all trunks including the new one first.
|
||||
it, err := ListSIPInboundTrunk(ctx, s.store, &livekit.ListSIPInboundTrunkRequest{
|
||||
Numbers: req.GetTrunk().GetNumbers(),
|
||||
}, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer it.Close()
|
||||
if err = sip.ValidateTrunksIter(it); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we can generate ID and store.
|
||||
info.SipTrunkId = guid.New(utils.SIPTrunkPrefix)
|
||||
if err := s.store.StoreSIPInboundTrunk(ctx, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) CreateSIPOutboundTrunk(ctx context.Context, req *livekit.CreateSIPOutboundTrunkRequest) (*livekit.SIPOutboundTrunkInfo, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if err := req.ValidateResult().LogUnlikelySoftErrors(unlikelyLoggerFromCtx(ctx)); err != nil {
|
||||
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
|
||||
}
|
||||
|
||||
info := req.Trunk
|
||||
if info.SipTrunkId != "" {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID must be empty")
|
||||
}
|
||||
AppendLogFields(ctx, "trunk", logger.Proto(info))
|
||||
|
||||
// No additional validation needed for outbound.
|
||||
info.SipTrunkId = guid.New(utils.SIPTrunkPrefix)
|
||||
if err := s.store.StoreSIPOutboundTrunk(ctx, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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.ValidateResult().LogUnlikelySoftErrors(unlikelyLoggerFromCtx(ctx)); 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:
|
||||
var result livekit.ValidationResult
|
||||
info, result = a.ApplyResult(info)
|
||||
if err := result.LogUnlikelySoftErrors(unlikelyLoggerFromCtx(ctx)); 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.ValidateResult().LogUnlikelySoftErrors(unlikelyLoggerFromCtx(ctx)); 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:
|
||||
var result livekit.ValidationResult
|
||||
info, result = a.ApplyResult(info)
|
||||
if err := result.LogUnlikelySoftErrors(unlikelyLoggerFromCtx(ctx)); 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)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if req.SipTrunkId == "" {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID is required")
|
||||
}
|
||||
AppendLogFields(ctx, "trunkID", req.SipTrunkId)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return &livekit.GetSIPInboundTrunkResponse{Trunk: trunk}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) GetSIPOutboundTrunk(ctx context.Context, req *livekit.GetSIPOutboundTrunkRequest) (*livekit.GetSIPOutboundTrunkResponse, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if req.SipTrunkId == "" {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID is required")
|
||||
}
|
||||
AppendLogFields(ctx, "trunkID", req.SipTrunkId)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return &livekit.GetSIPOutboundTrunkResponse{Trunk: trunk}, nil
|
||||
}
|
||||
|
||||
// deprecated: ListSIPTrunk will be removed in the future
|
||||
func (s *SIPService) ListSIPTrunk(ctx context.Context, req *livekit.ListSIPTrunkRequest) (*livekit.ListSIPTrunkResponse, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
it := livekit.ListPageIter(s.store.ListSIPTrunk, req)
|
||||
defer it.Close()
|
||||
|
||||
items, err := iters.AllPages(ctx, it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &livekit.ListSIPTrunkResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func ListSIPInboundTrunk(ctx context.Context, s SIPStore, req *livekit.ListSIPInboundTrunkRequest, add ...*livekit.SIPInboundTrunkInfo) (iters.Iter[*livekit.SIPInboundTrunkInfo], error) {
|
||||
if s == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
pages := livekit.ListPageIter(s.ListSIPInboundTrunk, req)
|
||||
it := iters.PagesAsIter(ctx, pages)
|
||||
if len(add) != 0 {
|
||||
it = iters.MultiIter(true, it, iters.Slice(add))
|
||||
}
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func ListSIPOutboundTrunk(ctx context.Context, s SIPStore, req *livekit.ListSIPOutboundTrunkRequest, add ...*livekit.SIPOutboundTrunkInfo) (iters.Iter[*livekit.SIPOutboundTrunkInfo], error) {
|
||||
if s == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
pages := livekit.ListPageIter(s.ListSIPOutboundTrunk, req)
|
||||
it := iters.PagesAsIter(ctx, pages)
|
||||
if len(add) != 0 {
|
||||
it = iters.MultiIter(true, it, iters.Slice(add))
|
||||
}
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) ListSIPInboundTrunk(ctx context.Context, req *livekit.ListSIPInboundTrunkRequest) (*livekit.ListSIPInboundTrunkResponse, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
it, err := ListSIPInboundTrunk(ctx, s.store, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
items, err := iters.All(it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &livekit.ListSIPInboundTrunkResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) ListSIPOutboundTrunk(ctx context.Context, req *livekit.ListSIPOutboundTrunkRequest) (*livekit.ListSIPOutboundTrunkResponse, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
it, err := ListSIPOutboundTrunk(ctx, s.store, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
items, err := iters.All(it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &livekit.ListSIPOutboundTrunkResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) DeleteSIPTrunk(ctx context.Context, req *livekit.DeleteSIPTrunkRequest) (*livekit.SIPTrunkInfo, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if req.SipTrunkId == "" {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "trunk ID is required")
|
||||
}
|
||||
|
||||
AppendLogFields(ctx, "trunkID", req.SipTrunkId)
|
||||
if err := s.store.DeleteSIPTrunk(ctx, req.SipTrunkId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &livekit.SIPTrunkInfo{SipTrunkId: req.SipTrunkId}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) CreateSIPDispatchRule(ctx context.Context, req *livekit.CreateSIPDispatchRuleRequest) (*livekit.SIPDispatchRuleInfo, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
AppendLogFields(ctx,
|
||||
"request", logger.Proto(req),
|
||||
"trunkID", req.TrunkIds,
|
||||
)
|
||||
// Keep ID empty, so that validation can print "<new>" instead of a non-existent ID in the error.
|
||||
info := req.DispatchRuleInfo()
|
||||
info.SipDispatchRuleId = ""
|
||||
|
||||
// Validate all rules including the new one first.
|
||||
it, err := ListSIPDispatchRule(ctx, s.store, &livekit.ListSIPDispatchRuleRequest{
|
||||
TrunkIds: req.TrunkIds,
|
||||
}, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer it.Close()
|
||||
if _, err = sip.ValidateDispatchRulesIter(it); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now we can generate ID and store.
|
||||
info.SipDispatchRuleId = guid.New(utils.SIPDispatchRulePrefix)
|
||||
if err := s.store.StoreSIPDispatchRule(ctx, info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
pages := livekit.ListPageIter(s.ListSIPDispatchRule, req)
|
||||
it := iters.PagesAsIter(ctx, pages)
|
||||
if len(add) != 0 {
|
||||
it = iters.MultiIter(true, it, iters.Slice(add))
|
||||
}
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) ListSIPDispatchRule(ctx context.Context, req *livekit.ListSIPDispatchRuleRequest) (*livekit.ListSIPDispatchRuleResponse, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
it, err := ListSIPDispatchRule(ctx, s.store, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
items, err := iters.All(it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &livekit.ListSIPDispatchRuleResponse{Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) DeleteSIPDispatchRule(ctx context.Context, req *livekit.DeleteSIPDispatchRuleRequest) (*livekit.SIPDispatchRuleInfo, error) {
|
||||
if err := EnsureSIPAdminPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
if req.SipDispatchRuleId == "" {
|
||||
return nil, twirp.NewError(twirp.InvalidArgument, "dispatch rule ID is required")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err = s.store.DeleteSIPDispatchRule(ctx, info.SipDispatchRuleId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) CreateSIPParticipant(ctx context.Context, req *livekit.CreateSIPParticipantRequest) (*livekit.SIPParticipantInfo, error) {
|
||||
unlikelyLogger := logger.GetLogger().WithUnlikelyValues(
|
||||
"room", req.RoomName,
|
||||
"sipTrunk", req.SipTrunkId,
|
||||
"toUser", req.SipCallTo,
|
||||
"participant", req.ParticipantIdentity,
|
||||
)
|
||||
AppendLogFields(ctx,
|
||||
"room", req.RoomName,
|
||||
"participant", req.ParticipantIdentity,
|
||||
"toUser", req.SipCallTo,
|
||||
"trunkID", req.SipTrunkId,
|
||||
)
|
||||
ireq, err := s.CreateSIPParticipantRequest(ctx, req, "", "", "", "")
|
||||
if err != nil {
|
||||
unlikelyLogger.Errorw("cannot create sip participant request", err)
|
||||
return nil, wrapSIPContextError(err)
|
||||
}
|
||||
unlikelyLogger = unlikelyLogger.WithValues(
|
||||
"callID", ireq.SipCallId,
|
||||
"fromUser", ireq.Number,
|
||||
"toHost", ireq.Address,
|
||||
)
|
||||
AppendLogFields(ctx,
|
||||
"callID", ireq.SipCallId,
|
||||
"fromUser", ireq.Number,
|
||||
"toHost", ireq.Address,
|
||||
)
|
||||
|
||||
// CreateSIPParticipant will wait for LiveKit Participant to be created and that can take some time.
|
||||
// Thus, we must set a higher deadline for it, if it's not set already.
|
||||
timeout := 30 * time.Second
|
||||
if req.WaitUntilAnswered {
|
||||
timeout = 80 * time.Second
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeout = time.Until(deadline)
|
||||
} else {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
resp, err := s.psrpcClient.CreateSIPParticipant(ctx, "", ireq, psrpc.WithRequestTimeout(timeout))
|
||||
if err != nil {
|
||||
unlikelyLogger.Errorw("cannot create sip participant", err)
|
||||
return nil, wrapSIPContextError(err)
|
||||
}
|
||||
return &livekit.SIPParticipantInfo{
|
||||
ParticipantId: resp.ParticipantId,
|
||||
ParticipantIdentity: resp.ParticipantIdentity,
|
||||
RoomName: req.RoomName,
|
||||
SipCallId: ireq.SipCallId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) CreateSIPParticipantRequest(ctx context.Context, req *livekit.CreateSIPParticipantRequest, projectID, host, wsUrl, token string) (*rpc.InternalCreateSIPParticipantRequest, error) {
|
||||
if err := EnsureSIPCallPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if s.store == nil {
|
||||
return nil, ErrSIPNotConnected
|
||||
}
|
||||
req.Upgrade()
|
||||
callID := sip.NewCallID()
|
||||
log := logger.GetLogger().WithUnlikelyValues(
|
||||
"callID", callID,
|
||||
"room", req.RoomName,
|
||||
"sipTrunk", req.SipTrunkId,
|
||||
"toUser", req.SipCallTo,
|
||||
"projectID", projectID,
|
||||
)
|
||||
if err := req.ValidateResult().LogUnlikelySoftErrors(log); err != nil {
|
||||
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), 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
|
||||
}
|
||||
internalReq, result := rpc.NewCreateSIPParticipantRequestResult(projectID, callID, host, wsUrl, token, req, trunk)
|
||||
if err := result.LogUnlikelySoftErrors(log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return internalReq, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) TransferSIPParticipant(ctx context.Context, req *livekit.TransferSIPParticipantRequest) (*livekit.TransferSIPParticipantResponse, error) {
|
||||
AppendLogFields(ctx,
|
||||
"room", req.RoomName,
|
||||
"participant", req.ParticipantIdentity,
|
||||
"transferTo", req.TransferTo,
|
||||
"playDialtone", req.PlayDialtone,
|
||||
)
|
||||
log := unlikelyLoggerFromCtx(ctx)
|
||||
|
||||
ireq, err := s.transferSIPParticipantRequest(ctx, req, log)
|
||||
if err != nil {
|
||||
log.Errorw("cannot create transfer sip participant request", 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 = min(timeout, time.Until(deadline))
|
||||
} else {
|
||||
var cancel func()
|
||||
ctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// Send the resolved timeout so the SIP node uses our deadline, not its
|
||||
// own default (which could outlive us).
|
||||
ireq.RingingTimeout = durationpb.New(timeout)
|
||||
|
||||
iresp, err := s.psrpcClient.TransferSIPParticipant(ctx, ireq.SipCallId, ireq, psrpc.WithRequestTimeout(timeout))
|
||||
if err != nil {
|
||||
log.Errorw("cannot transfer sip participant", err)
|
||||
return nil, wrapSIPContextError(err)
|
||||
}
|
||||
|
||||
return &livekit.TransferSIPParticipantResponse{
|
||||
TransferId: iresp.GetTransferId(),
|
||||
Status: iresp.GetStatus(),
|
||||
Reason: iresp.GetReason(),
|
||||
SipStatus: iresp.GetSipStatus(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SIPService) transferSIPParticipantRequest(ctx context.Context, req *livekit.TransferSIPParticipantRequest, log logger.UnlikelyLogger) (*rpc.InternalTransferSIPParticipantRequest, error) {
|
||||
if req.RoomName == "" {
|
||||
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "Missing room name")
|
||||
}
|
||||
|
||||
if req.ParticipantIdentity == "" {
|
||||
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "Missing participant identity")
|
||||
}
|
||||
|
||||
if err := EnsureSIPCallPermission(ctx); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
if err := EnsureAdminPermission(ctx, livekit.RoomName(req.RoomName)); err != nil {
|
||||
return nil, twirpAuthError(err)
|
||||
}
|
||||
|
||||
if err := req.ValidateResult().LogUnlikelySoftErrors(log); err != nil {
|
||||
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
|
||||
}
|
||||
|
||||
resp, err := s.roomService.GetParticipant(ctx, &livekit.RoomParticipantIdentity{
|
||||
Room: req.RoomName,
|
||||
Identity: req.ParticipantIdentity,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
callID, ok := resp.Attributes[livekit.AttrSIPCallID]
|
||||
if !ok {
|
||||
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "no SIP session associated with participant")
|
||||
}
|
||||
|
||||
return &rpc.InternalTransferSIPParticipantRequest{
|
||||
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
|
||||
}
|
||||
361
pkg/service/turn.go
Normal file
361
pkg/service/turn.go
Normal file
@@ -0,0 +1,361 @@
|
||||
// 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 (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jxskiss/base62"
|
||||
"github.com/pion/stun/v3"
|
||||
"github.com/pion/turn/v5"
|
||||
"github.com/pires/go-proxyproto"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
"github.com/livekit/protocol/logger/pionlogger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry"
|
||||
"github.com/livekit/livekit-server/pkg/telemetry/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
LivekitRealm = "livekit"
|
||||
|
||||
allocateRetries = 50
|
||||
)
|
||||
|
||||
var ErrExpired = errors.New("expired")
|
||||
|
||||
// parsePeerCIDRs compiles a list of CIDR strings, failing with a field-specific
|
||||
// error on any invalid entry so a malformed peer policy is never silently ignored.
|
||||
func parsePeerCIDRs(field string, cidrs []string) ([]*net.IPNet, error) {
|
||||
parsed := make([]*net.IPNet, 0, len(cidrs))
|
||||
for _, cidr := range cidrs {
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid CIDR %q in %s: %w", cidr, field, err)
|
||||
}
|
||||
parsed = append(parsed, ipnet)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone bool) (*turn.Server, error) {
|
||||
turnConf := conf.TURN
|
||||
if !turnConf.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if turnConf.TLSPort <= 0 && turnConf.UDPPort <= 0 {
|
||||
return nil, errors.New("invalid TURN ports")
|
||||
} else if turnConf.TLSPort > 0 {
|
||||
if turnConf.Domain == "" {
|
||||
return nil, errors.New("TURN domain required")
|
||||
}
|
||||
|
||||
if !IsValidDomain(turnConf.Domain) {
|
||||
return nil, errors.New("TURN domain is not correct")
|
||||
}
|
||||
}
|
||||
|
||||
// parse peer CIDR policies once at startup so a malformed entry fails loudly
|
||||
// instead of being silently skipped on every permission decision (fail-open)
|
||||
allowRestrictedPeerCIDRs, err := parsePeerCIDRs("turn.allow_restricted_peer_cidrs", turnConf.AllowRestrictedPeerCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
denyPeerCIDRs, err := parsePeerCIDRs("turn.deny_peer_cidrs", turnConf.DenyPeerCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverConfig := turn.ServerConfig{
|
||||
Realm: LivekitRealm,
|
||||
AuthHandler: authHandler,
|
||||
LoggerFactory: pionlogger.NewLoggerFactory(logger.GetLogger()),
|
||||
}
|
||||
|
||||
// cap concurrent relay allocations per participant so one credential cannot
|
||||
// exhaust the shared relay-port range (a value <= 0 disables the quota)
|
||||
if turnConf.PerUserRelayAllocationLimit > 0 {
|
||||
quota := newTURNAllocationQuota(turnConf.PerUserRelayAllocationLimit)
|
||||
serverConfig.QuotaHandler = quota.Allow
|
||||
serverConfig.EventHandler = quota.eventHandler()
|
||||
}
|
||||
|
||||
var logValues []any
|
||||
logValues = append(logValues, "turn.relay_range_start", turnConf.RelayPortRangeStart)
|
||||
logValues = append(logValues, "turn.relay_range_end", turnConf.RelayPortRangeEnd)
|
||||
logValues = append(logValues, "turn.per_user_relay_allocation_limit", turnConf.PerUserRelayAllocationLimit)
|
||||
|
||||
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 {
|
||||
relayAddrGen = telemetry.NewRelayAddressGenerator(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 _, ipnet := range allowRestrictedPeerCIDRs {
|
||||
if ipnet.Contains(peerIP) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false
|
||||
}
|
||||
|
||||
// if allowed, check deny list for overrides
|
||||
}
|
||||
|
||||
for _, ipnet := range denyPeerCIDRs {
|
||||
if ipnet.Contains(peerIP) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if turnConf.TLSPort > 0 {
|
||||
listener, err := newTURNTCPListener(turnConf, net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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, "turn.proxyProtocol", turnConf.ProxyProtocol)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infow("Starting TURN server", logValues...)
|
||||
return turn.NewServer(serverConfig)
|
||||
}
|
||||
|
||||
// newTURNTCPListener returns the TCP listener for TURN/TLS. The PROXY protocol
|
||||
// header, when enabled, is read before TLS so the client address is known to
|
||||
// the TLS layer and to TURN regardless of who terminates TLS.
|
||||
func newTURNTCPListener(turnConf config.TURNConfig, address string) (net.Listener, error) {
|
||||
var tlsConfig *tls.Config
|
||||
if !turnConf.ExternalTLS {
|
||||
cert, err := tls.LoadX509KeyPair(turnConf.CertFile, turnConf.KeyFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "TURN tls cert required")
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
|
||||
var proxyPolicy proxyproto.ConnPolicyFunc
|
||||
if turnConf.ProxyProtocol {
|
||||
trusted, err := parsePeerCIDRs("turn.proxy_protocol_trusted_cidrs", turnConf.ProxyProtocolTrustedCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(trusted) == 0 {
|
||||
return nil, errors.New("turn.proxy_protocol requires at least one entry in turn.proxy_protocol_trusted_cidrs")
|
||||
}
|
||||
proxyPolicy = proxyProtocolPolicy(trusted)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not listen on TURN TCP port")
|
||||
}
|
||||
if proxyPolicy != nil {
|
||||
listener = &proxyproto.Listener{Listener: listener, ConnPolicy: proxyPolicy}
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
listener = tls.NewListener(listener, tlsConfig)
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
// proxyProtocolPolicy requires the PROXY header from trusted proxies and closes
|
||||
// every other connection, so the header cannot be forged by a direct client.
|
||||
func proxyProtocolPolicy(trusted []*net.IPNet) proxyproto.ConnPolicyFunc {
|
||||
return func(opts proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
|
||||
tcpAddr, ok := opts.Upstream.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return proxyproto.REJECT, fmt.Errorf("%w: unexpected address %v", proxyproto.ErrInvalidUpstream, opts.Upstream)
|
||||
}
|
||||
for _, ipnet := range trusted {
|
||||
if ipnet.Contains(tcpAddr.IP) {
|
||||
return proxyproto.REQUIRE, nil
|
||||
}
|
||||
}
|
||||
// wrapping ErrInvalidUpstream closes this connection and keeps the listener accepting
|
||||
return proxyproto.REJECT, fmt.Errorf("%w: %s is not a trusted proxy", proxyproto.ErrInvalidUpstream, tcpAddr.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func getTURNAuthHandlerFunc(handler *TURNAuthHandler) turn.AuthHandler {
|
||||
return handler.HandleAuth
|
||||
}
|
||||
|
||||
type TURNAuthHandler struct {
|
||||
keyProvider auth.KeyProvider
|
||||
}
|
||||
|
||||
func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler {
|
||||
return &TURNAuthHandler{
|
||||
keyProvider: keyProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) (string, int64) {
|
||||
// clamp defensively: non-positive TTLs fall back to the default and overflowing ones are capped
|
||||
ttlSeconds, _ = config.ClampTURNTTLSeconds(ttlSeconds)
|
||||
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) 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|%d", secret, pID, expiry)
|
||||
|
||||
sum := sha256.Sum256([]byte(keyInput))
|
||||
return base62.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
parts := strings.Split(string(decoded), "|")
|
||||
if len(parts) != 3 {
|
||||
return "", nil, false
|
||||
}
|
||||
expiry, err := strconv.ParseInt(parts[2], 10, 64)
|
||||
if err != nil {
|
||||
return "", nil, false
|
||||
}
|
||||
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
|
||||
}
|
||||
416
pkg/service/turn_test.go
Normal file
416
pkg/service/turn_test.go
Normal file
@@ -0,0 +1,416 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/config"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
expiry := time.Now().Add(-time.Minute).Unix()
|
||||
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, expiry))
|
||||
_, _, 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")
|
||||
|
||||
expiry := time.Now().Add(-time.Minute).Unix()
|
||||
username := base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", turnTestAPIKey, pID, expiry))
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func TestParsePeerCIDRs(t *testing.T) {
|
||||
t.Run("valid entries are compiled", func(t *testing.T) {
|
||||
nets, err := parsePeerCIDRs("turn.deny_peer_cidrs", []string{"203.0.113.0/24", "10.0.0.0/8"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, nets, 2)
|
||||
require.True(t, nets[0].Contains(net.ParseIP("203.0.113.5")))
|
||||
require.False(t, nets[0].Contains(net.ParseIP("203.0.114.5")))
|
||||
})
|
||||
|
||||
t.Run("empty list is fine", func(t *testing.T) {
|
||||
nets, err := parsePeerCIDRs("turn.deny_peer_cidrs", nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, nets)
|
||||
})
|
||||
|
||||
t.Run("invalid entry is rejected with field context", func(t *testing.T) {
|
||||
_, err := parsePeerCIDRs("turn.deny_peer_cidrs", []string{"203.0.113.0/33"})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "turn.deny_peer_cidrs")
|
||||
require.Contains(t, err.Error(), "203.0.113.0/33")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewTurnServer_InvalidPeerCIDRFailsStartup(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
mutID func(c *config.Config)
|
||||
field string
|
||||
}{
|
||||
{
|
||||
name: "invalid deny cidr",
|
||||
mutID: func(c *config.Config) { c.TURN.DenyPeerCIDRs = []string{"203.0.113.0/33"} },
|
||||
field: "turn.deny_peer_cidrs",
|
||||
},
|
||||
{
|
||||
name: "invalid allow cidr",
|
||||
mutID: func(c *config.Config) { c.TURN.AllowRestrictedPeerCIDRs = []string{"not-a-cidr"} },
|
||||
field: "turn.allow_restricted_peer_cidrs",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
conf := &config.Config{}
|
||||
conf.TURN.Enabled = true
|
||||
conf.TURN.UDPPort = 3478
|
||||
tc.mutID(conf)
|
||||
|
||||
_, err := NewTurnServer(conf, nil, false)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.field)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTURNAuthHandler_CreateUsername_TTLClamped(t *testing.T) {
|
||||
h := newTestTurnAuthHandler()
|
||||
pID := livekit.ParticipantID("PA_ttl_clamp")
|
||||
|
||||
// An overflowing TTL must not wrap into a past expiry; it clamps to the max.
|
||||
_, overflowExpiry := h.CreateUsername(turnTestAPIKey, pID, 1<<62+1)
|
||||
require.Greater(t, overflowExpiry, time.Now().Unix())
|
||||
require.LessOrEqual(t, overflowExpiry, time.Now().Unix()+int64(config.TURNMaxTTLSeconds)+1)
|
||||
|
||||
// A non-positive TTL falls back to the default rather than producing a past/wrapped expiry.
|
||||
_, negativeExpiry := h.CreateUsername(turnTestAPIKey, pID, -1<<40)
|
||||
require.InDelta(t, time.Now().Unix()+int64(config.DefaultTURNTTLSeconds), negativeExpiry, 2)
|
||||
}
|
||||
|
||||
func proxyProtocolTURNConfig(trustedCIDRs ...string) config.TURNConfig {
|
||||
return config.TURNConfig{ExternalTLS: true, ProxyProtocol: true, ProxyProtocolTrustedCIDRs: trustedCIDRs}
|
||||
}
|
||||
|
||||
func TestNewTURNTCPListener_ProxyProtocol(t *testing.T) {
|
||||
listener, err := newTURNTCPListener(proxyProtocolTURNConfig("127.0.0.0/8"), "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan net.Addr, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
accepted <- nil
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
// the PROXY header is consumed lazily, on the first read
|
||||
buf := make([]byte, 1)
|
||||
_, _ = conn.Read(buf)
|
||||
accepted <- conn.RemoteAddr()
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.Write([]byte("PROXY TCP4 203.0.113.9 127.0.0.1 40123 443\r\nx"))
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case addr := <-accepted:
|
||||
require.NotNil(t, addr)
|
||||
require.Equal(t, "203.0.113.9:40123", addr.String())
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for the accepted connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTURNTCPListener_ProxyProtocolRejectsBareConnection(t *testing.T) {
|
||||
listener, err := newTURNTCPListener(proxyProtocolTURNConfig("127.0.0.0/8"), "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
result <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 1)
|
||||
_, err = conn.Read(buf)
|
||||
result <- err
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.Write([]byte("not a proxy header\r\n"))
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case err := <-result:
|
||||
require.Error(t, err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for the rejected connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTURNTCPListener_WithoutProxyProtocol(t *testing.T) {
|
||||
listener, err := newTURNTCPListener(config.TURNConfig{ExternalTLS: true}, "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan net.Addr, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
accepted <- nil
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
accepted <- conn.RemoteAddr()
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
select {
|
||||
case addr := <-accepted:
|
||||
require.NotNil(t, addr)
|
||||
require.Equal(t, conn.LocalAddr().String(), addr.String())
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for the accepted connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTURNTCPListener_ProxyProtocolClosesUntrustedProxy(t *testing.T) {
|
||||
listener, err := newTURNTCPListener(proxyProtocolTURNConfig("203.0.113.0/24"), "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan struct{}, 1)
|
||||
go func() {
|
||||
if conn, err := listener.Accept(); err == nil {
|
||||
conn.Close()
|
||||
accepted <- struct{}{}
|
||||
}
|
||||
}()
|
||||
|
||||
conn, err := net.Dial("tcp", listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, _ = conn.Write([]byte("PROXY TCP4 203.0.113.9 127.0.0.1 40123 443\r\nx"))
|
||||
|
||||
require.NoError(t, conn.SetReadDeadline(time.Now().Add(5*time.Second)))
|
||||
_, err = conn.Read(make([]byte, 1))
|
||||
require.Error(t, err, "the listener should have closed the connection")
|
||||
|
||||
select {
|
||||
case <-accepted:
|
||||
t.Fatal("connection from an untrusted proxy must not be accepted")
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTURNTCPListener_ProxyProtocolRequiresTrustedCIDRs(t *testing.T) {
|
||||
_, err := newTURNTCPListener(proxyProtocolTURNConfig(), "127.0.0.1:0")
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = newTURNTCPListener(proxyProtocolTURNConfig("not-a-cidr"), "127.0.0.1:0")
|
||||
require.Error(t, err)
|
||||
}
|
||||
187
pkg/service/turnquota.go
Normal file
187
pkg/service/turnquota.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// 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 (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pion/turn/v5"
|
||||
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
// defaultTURNReservationTTL bounds how long a reserved-but-unconfirmed
|
||||
// allocation slot may live. Pion calls the QuotaHandler and then synchronously
|
||||
// creates the allocation, so a healthy reservation is confirmed within
|
||||
// microseconds; this generous window only matters if allocation creation fails
|
||||
// after the quota check (see turnAllocationQuota).
|
||||
const defaultTURNReservationTTL = 30 * time.Second
|
||||
|
||||
// turnAllocationQuota enforces a per-user cap on concurrent relay allocations on
|
||||
// the embedded TURN server. Without it, a single authenticated participant can
|
||||
// reuse its credential across many client 5-tuples and open one relay socket/port
|
||||
// per request, exhausting the shared relay-port range for every other participant.
|
||||
//
|
||||
// The quota is keyed by the user ID returned from TURNAuthHandler.HandleAuth,
|
||||
// which is the stable participant ID embedded in the signed TURN username. A
|
||||
// participant cannot forge a different ID without the API secret, so this bounds
|
||||
// the total relay footprint of any one participant.
|
||||
//
|
||||
// Slots are reserved in Allow (which Pion calls before creating an allocation)
|
||||
// and released in OnDeleted, all under a single lock, so a burst of concurrent
|
||||
// Allocate requests cannot race past the limit. Reservations are keyed by the
|
||||
// client source address so that Allocate retransmissions from the same 5-tuple
|
||||
// are idempotent rather than double-counted.
|
||||
//
|
||||
// A reservation starts out pending and is confirmed in OnCreated. Pion emits no
|
||||
// event when an Allocate passes the quota check but then fails to create a relay
|
||||
// (e.g. the relay-port range is exhausted), so each pending reservation carries a
|
||||
// reclaim timer that frees the slot after reservationTTL. This keeps a failed
|
||||
// attempt from occupying a slot forever, which would otherwise let a participant
|
||||
// lock itself out and grow the tracking map without bound.
|
||||
type turnAllocationQuota struct {
|
||||
limit int
|
||||
reservationTTL time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
users map[string]map[string]*turnAllocationSlot // userID -> source-address key -> slot
|
||||
}
|
||||
|
||||
type turnAllocationSlot struct {
|
||||
confirmed bool
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
func newTURNAllocationQuota(limit int) *turnAllocationQuota {
|
||||
return &turnAllocationQuota{
|
||||
limit: limit,
|
||||
reservationTTL: defaultTURNReservationTTL,
|
||||
users: make(map[string]map[string]*turnAllocationSlot),
|
||||
}
|
||||
}
|
||||
|
||||
func srcAddrKey(srcAddr net.Addr) string {
|
||||
if srcAddr == nil {
|
||||
return ""
|
||||
}
|
||||
// include the network so an ip:port reused across UDP and TCP relays is not
|
||||
// collapsed into a single slot
|
||||
return srcAddr.Network() + "|" + srcAddr.String()
|
||||
}
|
||||
|
||||
// Allow implements turn.QuotaHandler. It returns true if the allocation may
|
||||
// proceed, reserving a slot for the user, and false (Pion replies 486 Allocation
|
||||
// Quota Reached) once the user is at its limit.
|
||||
func (q *turnAllocationQuota) Allow(userID, _ string, srcAddr net.Addr) bool {
|
||||
key := srcAddrKey(srcAddr)
|
||||
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
slots := q.users[userID]
|
||||
if _, ok := slots[key]; ok {
|
||||
// already reserved for this 5-tuple (retransmit / create race) - idempotent
|
||||
return true
|
||||
}
|
||||
if len(slots) >= q.limit {
|
||||
logger.Infow("TURN allocation quota reached",
|
||||
"participantID", userID,
|
||||
"limit", q.limit,
|
||||
)
|
||||
return false
|
||||
}
|
||||
if slots == nil {
|
||||
slots = make(map[string]*turnAllocationSlot)
|
||||
q.users[userID] = slots
|
||||
}
|
||||
// reserve a pending slot; reclaim it if the allocation is never created so a
|
||||
// failed attempt (Pion emits no created/deleted event) cannot hold it forever
|
||||
slot := &turnAllocationSlot{}
|
||||
slot.timer = time.AfterFunc(q.reservationTTL, func() {
|
||||
q.reclaimPending(userID, key, slot)
|
||||
})
|
||||
slots[key] = slot
|
||||
return true
|
||||
}
|
||||
|
||||
// OnCreated confirms a reservation once the relay allocation actually exists,
|
||||
// cancelling its reclaim timer so a live allocation is never evicted early.
|
||||
func (q *turnAllocationQuota) OnCreated(srcAddr, _ net.Addr, _, userID, _ string, _ net.Addr, _ int) {
|
||||
key := srcAddrKey(srcAddr)
|
||||
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
slot := q.users[userID][key]
|
||||
if slot == nil {
|
||||
return
|
||||
}
|
||||
slot.confirmed = true
|
||||
if slot.timer != nil {
|
||||
slot.timer.Stop()
|
||||
slot.timer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// OnDeleted releases the slot reserved in Allow so the user regains capacity once
|
||||
// an allocation ends.
|
||||
func (q *turnAllocationQuota) OnDeleted(srcAddr, _ net.Addr, _, userID, _ string) {
|
||||
key := srcAddrKey(srcAddr)
|
||||
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
q.removeLocked(userID, key)
|
||||
}
|
||||
|
||||
// reclaimPending drops a reservation that was never confirmed, freeing a slot
|
||||
// left behind by an Allocate that passed the quota but failed to create a relay.
|
||||
// It is identity-aware: a timer that fires after its slot was already released
|
||||
// must not evict a replacement reservation created for the same userID+key.
|
||||
func (q *turnAllocationQuota) reclaimPending(userID, key string, slot *turnAllocationSlot) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
if q.users[userID][key] != slot || slot.confirmed {
|
||||
return
|
||||
}
|
||||
q.removeLocked(userID, key)
|
||||
}
|
||||
|
||||
func (q *turnAllocationQuota) removeLocked(userID, key string) {
|
||||
slots := q.users[userID]
|
||||
slot := slots[key]
|
||||
if slot == nil {
|
||||
return
|
||||
}
|
||||
if slot.timer != nil {
|
||||
slot.timer.Stop()
|
||||
}
|
||||
delete(slots, key)
|
||||
if len(slots) == 0 {
|
||||
delete(q.users, userID)
|
||||
}
|
||||
}
|
||||
|
||||
// eventHandler returns the Pion EventHandler wired to confirm and release quota
|
||||
// slots as allocations are created and torn down.
|
||||
func (q *turnAllocationQuota) eventHandler() turn.EventHandler {
|
||||
return turn.EventHandler{
|
||||
OnAllocationCreated: q.OnCreated,
|
||||
OnAllocationDeleted: q.OnDeleted,
|
||||
}
|
||||
}
|
||||
203
pkg/service/turnquota_test.go
Normal file
203
pkg/service/turnquota_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// 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"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func udpAddr(t *testing.T, port int) net.Addr {
|
||||
t.Helper()
|
||||
return &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: port}
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_LimitsPerUser(t *testing.T) {
|
||||
q := newTURNAllocationQuota(4)
|
||||
|
||||
// four distinct source 5-tuples for the same participant are allowed
|
||||
for i := 0; i < 4; i++ {
|
||||
require.True(t, q.Allow("participantA", LivekitRealm, udpAddr(t, 5000+i)), "allocation %d", i)
|
||||
}
|
||||
// the fifth is rejected (Pion returns 486 Allocation Quota Reached)
|
||||
require.False(t, q.Allow("participantA", LivekitRealm, udpAddr(t, 6000)))
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_IsolatedPerUser(t *testing.T) {
|
||||
q := newTURNAllocationQuota(2)
|
||||
|
||||
require.True(t, q.Allow("A", LivekitRealm, udpAddr(t, 5000)))
|
||||
require.True(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)))
|
||||
require.False(t, q.Allow("A", LivekitRealm, udpAddr(t, 5002)))
|
||||
|
||||
// a different participant has its own budget
|
||||
require.True(t, q.Allow("B", LivekitRealm, udpAddr(t, 5000)))
|
||||
require.True(t, q.Allow("B", LivekitRealm, udpAddr(t, 5001)))
|
||||
require.False(t, q.Allow("B", LivekitRealm, udpAddr(t, 5002)))
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_ReleaseFreesSlot(t *testing.T) {
|
||||
q := newTURNAllocationQuota(1)
|
||||
|
||||
addr := udpAddr(t, 5000)
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
require.False(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)))
|
||||
|
||||
// once the allocation is deleted the slot is reclaimed
|
||||
q.OnDeleted(addr, nil, "udp", "A", LivekitRealm)
|
||||
require.True(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)))
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_RetransmitIsIdempotent(t *testing.T) {
|
||||
q := newTURNAllocationQuota(1)
|
||||
|
||||
addr := udpAddr(t, 5000)
|
||||
// same 5-tuple retried multiple times must consume only one slot
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
|
||||
// a different 5-tuple is still over quota
|
||||
require.False(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)))
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_ForgetsUserWhenEmpty(t *testing.T) {
|
||||
q := newTURNAllocationQuota(1)
|
||||
|
||||
addr := udpAddr(t, 5000)
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
q.OnDeleted(addr, nil, "udp", "A", LivekitRealm)
|
||||
|
||||
q.mu.Lock()
|
||||
_, tracked := q.users["A"]
|
||||
q.mu.Unlock()
|
||||
require.False(t, tracked, "user with no active allocations should not be retained")
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_UnconfirmedReservationReclaimed(t *testing.T) {
|
||||
// an Allocate that passes the quota but never produces an allocation (e.g.
|
||||
// relay ports exhausted) must not hold its slot forever
|
||||
q := newTURNAllocationQuota(1)
|
||||
q.reservationTTL = 10 * time.Millisecond
|
||||
|
||||
require.True(t, q.Allow("A", LivekitRealm, udpAddr(t, 5000)))
|
||||
require.False(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)), "at quota while reservation pending")
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return q.Allow("A", LivekitRealm, udpAddr(t, 5002))
|
||||
}, time.Second, 5*time.Millisecond, "slot should be reclaimed after TTL")
|
||||
|
||||
// the reclaimed user should not linger once fully drained
|
||||
q.OnDeleted(udpAddr(t, 5002), nil, "udp", "A", LivekitRealm)
|
||||
q.mu.Lock()
|
||||
_, tracked := q.users["A"]
|
||||
q.mu.Unlock()
|
||||
require.False(t, tracked)
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_ConfirmedReservationSurvivesTTL(t *testing.T) {
|
||||
// a confirmed (live) allocation must never be reclaimed by the pending timer
|
||||
q := newTURNAllocationQuota(1)
|
||||
q.reservationTTL = 10 * time.Millisecond
|
||||
|
||||
addr := udpAddr(t, 5000)
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
q.OnCreated(addr, nil, "udp", "A", LivekitRealm, nil, 0)
|
||||
|
||||
time.Sleep(30 * time.Millisecond) // past the TTL
|
||||
|
||||
require.False(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)), "confirmed allocation must still hold its slot")
|
||||
|
||||
q.OnDeleted(addr, nil, "udp", "A", LivekitRealm)
|
||||
require.True(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)))
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_StaleReclaimDoesNotEvictReplacement(t *testing.T) {
|
||||
// a reclaim timer that fires after its slot was released must not delete a
|
||||
// fresh reservation created for the same participant + client address
|
||||
q := newTURNAllocationQuota(1)
|
||||
q.reservationTTL = time.Hour // keep real timers from firing during the test
|
||||
|
||||
addr := udpAddr(t, 5000)
|
||||
key := srcAddrKey(addr)
|
||||
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
q.mu.Lock()
|
||||
stale := q.users["A"][key]
|
||||
q.mu.Unlock()
|
||||
|
||||
// the original allocation is torn down, then the same 5-tuple is reused and
|
||||
// its replacement allocation is confirmed
|
||||
q.OnDeleted(addr, nil, "udp", "A", LivekitRealm)
|
||||
require.True(t, q.Allow("A", LivekitRealm, addr))
|
||||
q.OnCreated(addr, nil, "udp", "A", LivekitRealm, nil, 0)
|
||||
|
||||
// the original slot's timer fires late: it must be a no-op
|
||||
q.reclaimPending("A", key, stale)
|
||||
|
||||
q.mu.Lock()
|
||||
_, live := q.users["A"][key]
|
||||
q.mu.Unlock()
|
||||
require.True(t, live, "replacement slot must survive a stale reclaim")
|
||||
require.False(t, q.Allow("A", LivekitRealm, udpAddr(t, 5001)), "replacement must still count against quota")
|
||||
}
|
||||
|
||||
// TestTURNAllocationQuota_ConcurrentAllocatesRespectLimit is the core security
|
||||
// property: a burst of concurrent Allocate requests reusing one credential must
|
||||
// not be able to race past the limit.
|
||||
func TestTURNAllocationQuota_ConcurrentAllocatesRespectLimit(t *testing.T) {
|
||||
const limit = 4
|
||||
q := newTURNAllocationQuota(limit)
|
||||
|
||||
var granted atomic.Int32
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 200; i++ {
|
||||
wg.Add(1)
|
||||
go func(port int) {
|
||||
defer wg.Done()
|
||||
if q.Allow("attacker", LivekitRealm, udpAddr(t, port)) {
|
||||
granted.Add(1)
|
||||
}
|
||||
}(7000 + i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
require.Equal(t, int32(limit), granted.Load(), "concurrent allocations must not exceed the quota")
|
||||
}
|
||||
|
||||
func TestTURNAllocationQuota_SrcAddrKeyDistinguishesNetwork(t *testing.T) {
|
||||
udp := &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 5000}
|
||||
tcp := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 5000}
|
||||
require.NotEqual(t, srcAddrKey(udp), srcAddrKey(tcp))
|
||||
require.Equal(t, "", srcAddrKey(nil))
|
||||
}
|
||||
|
||||
func BenchmarkTURNAllocationQuota_Allow(b *testing.B) {
|
||||
q := newTURNAllocationQuota(1000000)
|
||||
addrs := make([]net.Addr, 0, 64)
|
||||
for i := 0; i < 64; i++ {
|
||||
addrs = append(addrs, &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 5000 + i})
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
q.Allow(fmt.Sprintf("u%d", i%128), LivekitRealm, addrs[i%len(addrs)])
|
||||
}
|
||||
}
|
||||
442
pkg/service/twirp.go
Normal file
442
pkg/service/twirp.go
Normal file
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* Copyright 2022 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"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/twitchtv/twirp"
|
||||
"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"
|
||||
)
|
||||
|
||||
type twirpRequestFields struct {
|
||||
service string
|
||||
method string
|
||||
error twirp.Error
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
type twirpLoggerKey struct{}
|
||||
|
||||
// logging handling inspired by https://github.com/bakins/twirpzap
|
||||
// License: Apache-2.0
|
||||
func TwirpLogger() *twirp.ServerHooks {
|
||||
loggerPool := &sync.Pool{
|
||||
New: func() any {
|
||||
return &twirpLogger{
|
||||
fieldsOrig: make([]any, 0, 30),
|
||||
}
|
||||
},
|
||||
}
|
||||
return &twirp.ServerHooks{
|
||||
RequestReceived: func(ctx context.Context) (context.Context, error) {
|
||||
return loggerRequestReceived(ctx, loggerPool)
|
||||
},
|
||||
RequestRouted: loggerRequestRouted,
|
||||
Error: loggerErrorReceived,
|
||||
ResponseSent: func(ctx context.Context) {
|
||||
loggerResponseSent(ctx, loggerPool)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type twirpLogger struct {
|
||||
twirpRequestFields
|
||||
|
||||
fieldsOrig []any
|
||||
fields []any
|
||||
startedAt time.Time
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
r.fields = append(r.fields, fields...)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if svc, ok := twirp.ServiceName(ctx); ok {
|
||||
r.service = svc
|
||||
r.fields = append(r.fields, "service", svc)
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, twirpLoggerKey{}, r), nil
|
||||
}
|
||||
|
||||
func loggerRequestRouted(ctx context.Context) (context.Context, error) {
|
||||
if meth, ok := twirp.MethodName(ctx); ok {
|
||||
l, ok := ctx.Value(twirpLoggerKey{}).(*twirpLogger)
|
||||
if !ok || l == nil {
|
||||
return ctx, nil
|
||||
}
|
||||
l.method = meth
|
||||
l.fields = append(l.fields, "method", meth)
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func loggerResponseSent(ctx context.Context, twirpLoggerPool *sync.Pool) {
|
||||
r, ok := ctx.Value(twirpLoggerKey{}).(*twirpLogger)
|
||||
if !ok || r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if r.error != nil {
|
||||
r.fields = append(r.fields, "error", r.error.Msg())
|
||||
r.fields = append(r.fields, "code", r.error.Code())
|
||||
}
|
||||
|
||||
statusFamily, _ := twirpResponseStatus(ctx, r.error)
|
||||
serviceMethod := "API " + r.service + "." + r.method
|
||||
prometheus.RecordTwirpRequestLatency(r.service, r.method, duration, statusFamily)
|
||||
utils.GetLogger(ctx).WithComponent(utils.ComponentAPI).Infow(serviceMethod, r.fields...)
|
||||
|
||||
// reset fields and return to pool
|
||||
r.reset()
|
||||
twirpLoggerPool.Put(r)
|
||||
}
|
||||
|
||||
func loggerErrorReceived(ctx context.Context, e twirp.Error) context.Context {
|
||||
r, ok := ctx.Value(twirpLoggerKey{}).(*twirpLogger)
|
||||
if !ok || r == nil {
|
||||
return ctx
|
||||
}
|
||||
|
||||
r.error = e
|
||||
return ctx
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
type statusReporterKey struct{}
|
||||
|
||||
func TwirpRequestStatusReporter() *twirp.ServerHooks {
|
||||
return &twirp.ServerHooks{
|
||||
RequestReceived: statusReporterRequestReceived,
|
||||
RequestRouted: statusReporterRequestRouted,
|
||||
Error: statusReporterErrorReceived,
|
||||
ResponseSent: statusReporterResponseSent,
|
||||
}
|
||||
}
|
||||
|
||||
func statusReporterRequestReceived(ctx context.Context) (context.Context, error) {
|
||||
r := &twirpRequestFields{}
|
||||
|
||||
if svc, ok := twirp.ServiceName(ctx); ok {
|
||||
r.service = svc
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, statusReporterKey{}, r), nil
|
||||
}
|
||||
|
||||
func statusReporterRequestRouted(ctx context.Context) (context.Context, error) {
|
||||
if meth, ok := twirp.MethodName(ctx); ok {
|
||||
l, ok := ctx.Value(statusReporterKey{}).(*twirpRequestFields)
|
||||
if !ok || l == nil {
|
||||
return ctx, nil
|
||||
}
|
||||
l.method = meth
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func twirpResponseStatus(ctx context.Context, err twirp.Error) (statusFamily string, code twirp.ErrorCode) {
|
||||
if statusCode, ok := twirp.StatusCode(ctx); ok {
|
||||
statusFamily = httpStatusFamily(statusCode)
|
||||
}
|
||||
if err != nil {
|
||||
code = err.Code()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func httpStatusFamily(statusCode string) string {
|
||||
var statusFamily string
|
||||
if status, err := strconv.Atoi(statusCode); err == nil {
|
||||
switch {
|
||||
case status >= 400 && status <= 499:
|
||||
statusFamily = "4xx"
|
||||
case status >= 500 && status <= 599:
|
||||
statusFamily = "5xx"
|
||||
default:
|
||||
statusFamily = statusCode
|
||||
}
|
||||
}
|
||||
return statusFamily
|
||||
}
|
||||
|
||||
func statusReporterResponseSent(ctx context.Context) {
|
||||
r, ok := ctx.Value(statusReporterKey{}).(*twirpRequestFields)
|
||||
if !ok || r == nil {
|
||||
return
|
||||
}
|
||||
|
||||
statusFamily, code := twirpResponseStatus(ctx, r.error)
|
||||
prometheus.RecordTwirpRequestStatus(r.service, r.method, statusFamily, code)
|
||||
}
|
||||
|
||||
func statusReporterErrorReceived(ctx context.Context, e twirp.Error) context.Context {
|
||||
r, ok := ctx.Value(statusReporterKey{}).(*twirpRequestFields)
|
||||
if !ok || r == nil {
|
||||
return ctx
|
||||
}
|
||||
|
||||
r.error = e
|
||||
return ctx
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
type twirpTelemetryKey struct{}
|
||||
|
||||
func TwirpTelemetry(
|
||||
nodeID livekit.NodeID,
|
||||
getProjectID func(ctx context.Context) string,
|
||||
telemetry telemetry.TelemetryService,
|
||||
) *twirp.ServerHooks {
|
||||
return &twirp.ServerHooks{
|
||||
RequestReceived: telemetryRequestReceived,
|
||||
Error: telemetryErrorReceived,
|
||||
ResponseSent: func(ctx context.Context) {
|
||||
telemetryResponseSent(ctx, nodeID, getProjectID, telemetry)
|
||||
},
|
||||
RequestRouted: telemetryRequestRouted,
|
||||
}
|
||||
}
|
||||
|
||||
func RecordRequest(ctx context.Context, request proto.Message) {
|
||||
if request == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a, ok := ctx.Value(twirpTelemetryKey{}).(*livekit.APICallInfo)
|
||||
if !ok || a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// capture request and extract common fields to top level as appropriate
|
||||
switch msg := request.(type) {
|
||||
case *livekit.CreateRoomRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_CreateRoomRequest{
|
||||
CreateRoomRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetName()
|
||||
|
||||
case *livekit.ListRoomsRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_ListRoomsRequest{
|
||||
ListRoomsRequest: msg,
|
||||
},
|
||||
}
|
||||
|
||||
case *livekit.DeleteRoomRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_DeleteRoomRequest{
|
||||
DeleteRoomRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
|
||||
case *livekit.ListParticipantsRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_ListParticipantsRequest{
|
||||
ListParticipantsRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
|
||||
case *livekit.RoomParticipantIdentity:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_RoomParticipantIdentity{
|
||||
RoomParticipantIdentity: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
a.ParticipantIdentity = msg.GetIdentity()
|
||||
|
||||
case *livekit.MuteRoomTrackRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_MuteRoomTrackRequest{
|
||||
MuteRoomTrackRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
a.ParticipantIdentity = msg.GetIdentity()
|
||||
a.TrackId = msg.GetTrackSid()
|
||||
|
||||
case *livekit.UpdateParticipantRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_UpdateParticipantRequest{
|
||||
UpdateParticipantRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
a.ParticipantIdentity = msg.GetIdentity()
|
||||
|
||||
case *livekit.UpdateSubscriptionsRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_UpdateSubscriptionsRequest{
|
||||
UpdateSubscriptionsRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
a.ParticipantIdentity = msg.GetIdentity()
|
||||
|
||||
case *livekit.SendDataRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_SendDataRequest{
|
||||
SendDataRequest: msg,
|
||||
},
|
||||
}
|
||||
a.RoomName = msg.GetRoom()
|
||||
|
||||
case *livekit.UpdateRoomMetadataRequest:
|
||||
a.Request = &livekit.APICallRequest{
|
||||
Message: &livekit.APICallRequest_UpdateRoomMetadataRequest{
|
||||
UpdateRoomMetadataRequest: msg,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RecordResponse(ctx context.Context, response proto.Message) {
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a, ok := ctx.Value(twirpTelemetryKey{}).(*livekit.APICallInfo)
|
||||
if !ok || a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// extract common fields to top level as appropriate
|
||||
switch msg := response.(type) {
|
||||
case *livekit.Room:
|
||||
a.RoomId = msg.GetSid()
|
||||
|
||||
case *livekit.ParticipantInfo:
|
||||
a.ParticipantId = msg.GetSid()
|
||||
}
|
||||
}
|
||||
|
||||
func telemetryRequestReceived(ctx context.Context) (context.Context, error) {
|
||||
a := &livekit.APICallInfo{}
|
||||
a.StartedAt = timestamppb.Now()
|
||||
|
||||
if svc, ok := twirp.ServiceName(ctx); ok {
|
||||
a.Service = svc
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, twirpTelemetryKey{}, a), nil
|
||||
}
|
||||
|
||||
func telemetryRequestRouted(ctx context.Context) (context.Context, error) {
|
||||
if meth, ok := twirp.MethodName(ctx); ok {
|
||||
a, ok := ctx.Value(twirpTelemetryKey{}).(*livekit.APICallInfo)
|
||||
if !ok || a == nil {
|
||||
return ctx, nil
|
||||
}
|
||||
a.Method = meth
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func telemetryResponseSent(
|
||||
ctx context.Context,
|
||||
nodeID livekit.NodeID,
|
||||
getProjectID func(ctx context.Context) string,
|
||||
telemetry telemetry.TelemetryService,
|
||||
) {
|
||||
a, ok := ctx.Value(twirpTelemetryKey{}).(*livekit.APICallInfo)
|
||||
if !ok || a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if getProjectID != nil {
|
||||
a.ProjectId = getProjectID(ctx)
|
||||
}
|
||||
a.NodeId = string(nodeID)
|
||||
if statusCode, ok := twirp.StatusCode(ctx); ok {
|
||||
if status, err := strconv.ParseInt(statusCode, 10, 32); err == nil {
|
||||
a.Status = int32(status)
|
||||
}
|
||||
}
|
||||
a.DurationNs = time.Since(a.StartedAt.AsTime()).Nanoseconds()
|
||||
if telemetry != nil {
|
||||
telemetry.APICall(ctx, a)
|
||||
}
|
||||
}
|
||||
|
||||
func telemetryErrorReceived(ctx context.Context, e twirp.Error) context.Context {
|
||||
a, ok := ctx.Value(twirpTelemetryKey{}).(*livekit.APICallInfo)
|
||||
if !ok || a == nil {
|
||||
return ctx
|
||||
}
|
||||
|
||||
a.TwirpErrorCode = string(e.Code())
|
||||
a.TwirpErrorMessage = e.Msg()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
34
pkg/service/twirp_test.go
Normal file
34
pkg/service/twirp_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2024 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 (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/twitchtv/twirp"
|
||||
)
|
||||
|
||||
func TestConvertErrToTwirp(t *testing.T) {
|
||||
t.Run("handles not found", func(t *testing.T) {
|
||||
err := ErrRoomNotFound
|
||||
var tErr twirp.Error
|
||||
require.True(t, errors.As(err, &tErr))
|
||||
require.Equal(t, twirp.NotFound, tErr.Code())
|
||||
})
|
||||
}
|
||||
459
pkg/service/utils.go
Normal file
459
pkg/service/utils.go
Normal file
@@ -0,0 +1,459 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGzipReadFailed = errors.New("cannot read decompressed data")
|
||||
ErrGzipTooLarge = errors.New("decompressed data too large")
|
||||
ErrRequestBodyTooLarge = errors.New("request body 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) {
|
||||
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"
|
||||
}
|
||||
|
||||
func RemoveDoubleSlashes(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if strings.HasPrefix(r.URL.Path, "//") {
|
||||
r.URL.Path = r.URL.Path[1:]
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
// RequestBodyLimiter bounds the size of an incoming HTTP request body so that
|
||||
// large messages cannot exhaust memory. The Twirp handlers decode the whole
|
||||
// body before any grant check runs, so the limit is applied here, up front.
|
||||
//
|
||||
// It does not decode the body itself: a request whose Content-Length exceeds
|
||||
// the limit is rejected with 413, and the body is wrapped with
|
||||
// http.MaxBytesReader so a missing or dishonest Content-Length is still caught
|
||||
// by the downstream decoder.
|
||||
type RequestBodyLimiter struct {
|
||||
maxBytes int64
|
||||
}
|
||||
|
||||
func NewRequestBodyLimiter(maxBytes int64) *RequestBodyLimiter {
|
||||
return &RequestBodyLimiter{maxBytes: maxBytes}
|
||||
}
|
||||
|
||||
func (l *RequestBodyLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if l.maxBytes <= 0 || r.Body == nil {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// reject early when the declared size already exceeds the limit
|
||||
if r.ContentLength > l.maxBytes {
|
||||
HandleError(w, r, http.StatusRequestEntityTooLarge, ErrRequestBodyTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
// bound the read in case Content-Length is missing or wrong
|
||||
r.Body = http.MaxBytesReader(w, r.Body, l.maxBytes)
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
func IsValidDomain(domain string) bool {
|
||||
domainRegexp := regexp.MustCompile(`^(?i)[a-z0-9-]+(\.[a-z0-9-]+)+\.?$`)
|
||||
return domainRegexp.MatchString(domain)
|
||||
}
|
||||
|
||||
func GetClientIP(r *http.Request) string {
|
||||
// CF proxy typically is first thing the user reaches
|
||||
if ip := r.Header.Get("CF-Connecting-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
return ip
|
||||
}
|
||||
|
||||
func SetRoomConfiguration(createRequest *livekit.CreateRoomRequest, conf *livekit.RoomConfiguration) {
|
||||
if conf == nil {
|
||||
return
|
||||
}
|
||||
createRequest.Agents = conf.Agents
|
||||
createRequest.Egress = conf.Egress
|
||||
createRequest.EmptyTimeout = conf.EmptyTimeout
|
||||
createRequest.DepartureTimeout = conf.DepartureTimeout
|
||||
createRequest.MaxParticipants = conf.MaxParticipants
|
||||
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
|
||||
tokenExpiresAt time.Time
|
||||
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
|
||||
res.tokenExpiresAt = GetTokenExpiresAt(r.Context())
|
||||
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")
|
||||
}
|
||||
129
pkg/service/utils_test.go
Normal file
129
pkg/service/utils_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package service_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
func redisClientDocker(t testing.TB) *redis.Client {
|
||||
addr := runRedis(t)
|
||||
cli := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := cli.Ping(ctx).Err(); err != nil {
|
||||
_ = cli.Close()
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = cli.Close()
|
||||
})
|
||||
return cli
|
||||
}
|
||||
|
||||
func redisClient(t testing.TB) *redis.Client {
|
||||
cli := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := cli.Ping(ctx).Err()
|
||||
if err == nil {
|
||||
t.Cleanup(func() {
|
||||
_ = cli.Close()
|
||||
})
|
||||
return cli
|
||||
}
|
||||
_ = cli.Close()
|
||||
t.Logf("local redis not available: %v", err)
|
||||
|
||||
t.Logf("starting redis in docker")
|
||||
return redisClientDocker(t)
|
||||
}
|
||||
|
||||
func TestIsValidDomain(t *testing.T) {
|
||||
list := map[string]bool{
|
||||
"turn.myhost.com": true,
|
||||
"turn.google.com": true,
|
||||
"https://host.com": false,
|
||||
"turn://host.com": false,
|
||||
}
|
||||
for key, result := range list {
|
||||
service.IsValidDomain(key)
|
||||
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")
|
||||
})
|
||||
}
|
||||
543
pkg/service/whipservice.go
Normal file
543
pkg/service/whipservice.go
Normal file
@@ -0,0 +1,543 @@
|
||||
// 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"
|
||||
"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: 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(),
|
||||
})
|
||||
}
|
||||
306
pkg/service/wire.go
Normal file
306
pkg/service/wire.go
Normal file
@@ -0,0 +1,306 @@
|
||||
// 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.
|
||||
|
||||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/pion/turn/v5"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
redisLiveKit "github.com/livekit/protocol/redis"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/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) {
|
||||
wire.Build(
|
||||
getNodeID,
|
||||
createRedisClient,
|
||||
createStore,
|
||||
wire.Bind(new(ServiceStore), new(ObjectStore)),
|
||||
createKeyProvider,
|
||||
createWebhookNotifier,
|
||||
createForwardStats,
|
||||
getNodeStatsConfig,
|
||||
routing.CreateRouter,
|
||||
getLimitConf,
|
||||
getAPIConf,
|
||||
wire.Bind(new(routing.MessageRouter), new(routing.Router)),
|
||||
wire.Bind(new(livekit.RoomService), new(*RoomService)),
|
||||
telemetry.NewAnalyticsService,
|
||||
createTelemetryService,
|
||||
getMessageBus,
|
||||
NewIOInfoService,
|
||||
wire.Bind(new(IOClient), new(*IOInfoService)),
|
||||
rpc.NewEgressClient,
|
||||
rpc.NewIngressClient,
|
||||
getEgressStore,
|
||||
NewEgressLauncher,
|
||||
NewEgressService,
|
||||
getIngressStore,
|
||||
getIngressConfig,
|
||||
NewIngressService,
|
||||
newSIPClient,
|
||||
getSIPStore,
|
||||
getSIPConfig,
|
||||
NewSIPService,
|
||||
NewRoomAllocator,
|
||||
NewRoomService,
|
||||
NewRTCService,
|
||||
NewWHIPService,
|
||||
NewAgentService,
|
||||
NewAgentDispatchService,
|
||||
getAgentConfig,
|
||||
agent.NewAgentClient,
|
||||
getAgentStore,
|
||||
getSignalRelayConfig,
|
||||
NewDefaultSignalServer,
|
||||
routing.NewSignalClient,
|
||||
getRoomConfig,
|
||||
routing.NewRoomManagerClient,
|
||||
rpc.NewKeepalivePubSub,
|
||||
getPSRPCConfig,
|
||||
getPSRPCClientParams,
|
||||
rpc.NewTopicFormatter,
|
||||
rpc.NewTypedRoomClient,
|
||||
rpc.NewTypedParticipantClient,
|
||||
rpc.NewTypedWHIPParticipantClient,
|
||||
rpc.NewTypedAgentDispatchInternalClient,
|
||||
NewLocalRoomManager,
|
||||
NewTURNAuthHandler,
|
||||
getTURNAuthHandlerFunc,
|
||||
newInProcessTurnServer,
|
||||
utils.NewDefaultTimedVersionGenerator,
|
||||
NewLivekitServer,
|
||||
)
|
||||
return &LivekitServer{}, nil
|
||||
}
|
||||
|
||||
func InitializeRouter(conf *config.Config, currentNode routing.LocalNode) (routing.Router, error) {
|
||||
wire.Build(
|
||||
createRedisClient,
|
||||
getNodeID,
|
||||
getMessageBus,
|
||||
getSignalRelayConfig,
|
||||
getPSRPCConfig,
|
||||
getPSRPCClientParams,
|
||||
routing.NewSignalClient,
|
||||
getRoomConfig,
|
||||
routing.NewRoomManagerClient,
|
||||
rpc.NewKeepalivePubSub,
|
||||
getNodeStatsConfig,
|
||||
routing.CreateRouter,
|
||||
)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getNodeID(currentNode routing.LocalNode) livekit.NodeID {
|
||||
return currentNode.NodeID()
|
||||
}
|
||||
|
||||
func createKeyProvider(conf *config.Config) (auth.KeyProvider, error) {
|
||||
// prefer keyfile if set
|
||||
if conf.KeyFile != "" {
|
||||
var otherFilter os.FileMode = 0007
|
||||
if st, err := os.Stat(conf.KeyFile); err != nil {
|
||||
return nil, err
|
||||
} else if st.Mode().Perm()&otherFilter != 0000 {
|
||||
return nil, fmt.Errorf("key file others permissions must be set to 0")
|
||||
}
|
||||
f, err := os.Open(conf.KeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
decoder := yaml.NewDecoder(f)
|
||||
if err = decoder.Decode(conf.Keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(conf.Keys) == 0 {
|
||||
return nil, errors.New("one of key-file or keys must be provided in order to support a secure installation")
|
||||
}
|
||||
|
||||
return auth.NewFileBasedKeyProviderFromMap(conf.Keys), nil
|
||||
}
|
||||
|
||||
func createWebhookNotifier(conf *config.Config, provider auth.KeyProvider) (webhook.QueuedNotifier, error) {
|
||||
wc := conf.WebHook
|
||||
|
||||
secret := provider.GetSecret(wc.APIKey)
|
||||
if secret == "" && len(wc.URLs) > 0 {
|
||||
return nil, ErrWebHookMissingAPIKey
|
||||
}
|
||||
|
||||
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) {
|
||||
if !conf.Redis.IsConfigured() {
|
||||
return nil, nil
|
||||
}
|
||||
return redisLiveKit.GetRedisClient(&conf.Redis)
|
||||
}
|
||||
|
||||
func createStore(rc redis.UniversalClient) ObjectStore {
|
||||
if rc != nil {
|
||||
return NewRedisStore(rc)
|
||||
}
|
||||
return NewLocalStore()
|
||||
}
|
||||
|
||||
func getMessageBus(rc redis.UniversalClient, psrpcConf rpc.PSRPCConfig) psrpc.MessageBus {
|
||||
opts := psrpcConf.BusOptions()
|
||||
if rc == nil {
|
||||
return psrpc.NewLocalMessageBus(opts...)
|
||||
}
|
||||
return psrpc.NewRedisMessageBus(rc, opts...)
|
||||
}
|
||||
|
||||
func getEgressStore(s ObjectStore) EgressStore {
|
||||
switch store := s.(type) {
|
||||
case *RedisStore:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getIngressStore(s ObjectStore) IngressStore {
|
||||
switch store := s.(type) {
|
||||
case *RedisStore:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAgentStore(s ObjectStore) AgentStore {
|
||||
switch store := s.(type) {
|
||||
case *RedisStore:
|
||||
return store
|
||||
case *LocalStore:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getSIPConfig(conf *config.Config) *config.SIPConfig {
|
||||
return &conf.SIP
|
||||
}
|
||||
|
||||
func getLimitConf(config *config.Config) config.LimitConfig {
|
||||
return config.Limit
|
||||
}
|
||||
|
||||
func getRoomConfig(config *config.Config) config.RoomConfig {
|
||||
return config.Room
|
||||
}
|
||||
|
||||
func getSignalRelayConfig(config *config.Config) config.SignalRelayConfig {
|
||||
return config.SignalRelay
|
||||
}
|
||||
|
||||
func getPSRPCConfig(config *config.Config) rpc.PSRPCConfig {
|
||||
return config.PSRPC
|
||||
}
|
||||
|
||||
func getPSRPCClientParams(config rpc.PSRPCConfig, bus psrpc.MessageBus) rpc.ClientParams {
|
||||
return rpc.NewClientParams(config, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{},
|
||||
otelpsrpc.ClientOptions(otelpsrpc.Config{}),
|
||||
)
|
||||
}
|
||||
|
||||
func createForwardStats(conf *config.Config) *sfu.ForwardStats {
|
||||
if conf.RTC.ForwardStats.SummaryInterval == 0 || conf.RTC.ForwardStats.ReportInterval == 0 || conf.RTC.ForwardStats.ReportWindow == 0 {
|
||||
return nil
|
||||
}
|
||||
return sfu.NewForwardStats(conf.RTC.ForwardStats.SummaryInterval, conf.RTC.ForwardStats.ReportInterval, conf.RTC.ForwardStats.ReportWindow)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
360
pkg/service/wire_gen.go
Normal file
360
pkg/service/wire_gen.go
Normal file
@@ -0,0 +1,360 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
redis2 "github.com/livekit/protocol/redis"
|
||||
"github.com/livekit/protocol/rpc"
|
||||
"github.com/livekit/protocol/utils"
|
||||
"github.com/livekit/protocol/webhook"
|
||||
"github.com/livekit/psrpc"
|
||||
"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"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
func InitializeServer(conf *config.Config, currentNode routing.LocalNode) (*LivekitServer, error) {
|
||||
limitConfig := getLimitConf(conf)
|
||||
apiConfig := getAPIConf(conf)
|
||||
universalClient, err := createRedisClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeID := getNodeID(currentNode)
|
||||
psrpcConfig := getPSRPCConfig(conf)
|
||||
v := getMessageBus(universalClient, psrpcConfig)
|
||||
signalRelayConfig := getSignalRelayConfig(conf)
|
||||
signalClient, err := routing.NewSignalClient(nodeID, v, signalRelayConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientParams := getPSRPCClientParams(psrpcConfig, v)
|
||||
roomConfig := getRoomConfig(conf)
|
||||
roomManagerClient, err := routing.NewRoomManagerClient(clientParams, roomConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keepalivePubSub, err := rpc.NewKeepalivePubSub(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeStatsConfig := getNodeStatsConfig(conf)
|
||||
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub, nodeStatsConfig)
|
||||
objectStore := createStore(universalClient)
|
||||
roomAllocator, err := NewRoomAllocator(conf, router, objectStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
egressClient, err := rpc.NewEgressClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
egressStore := getEgressStore(objectStore)
|
||||
ingressStore := getIngressStore(objectStore)
|
||||
sipStore := getSIPStore(objectStore)
|
||||
keyProvider, err := createKeyProvider(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queuedNotifier, err := createWebhookNotifier(conf, keyProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
analyticsService := telemetry.NewAnalyticsService(conf, currentNode)
|
||||
telemetryService := createTelemetryService(queuedNotifier, analyticsService)
|
||||
ioInfoService, err := NewIOInfoService(v, egressStore, ingressStore, sipStore, telemetryService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rtcEgressLauncher := NewEgressLauncher(egressClient, ioInfoService, objectStore)
|
||||
topicFormatter := rpc.NewTopicFormatter()
|
||||
v2, err := rpc.NewTypedRoomClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v3, err := rpc.NewTypedParticipantClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roomService, err := NewRoomService(limitConfig, apiConfig, router, roomAllocator, objectStore, rtcEgressLauncher, topicFormatter, v2, v3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v4, err := rpc.NewTypedAgentDispatchInternalClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agentDispatchService := NewAgentDispatchService(limitConfig, v4, topicFormatter, roomAllocator, router)
|
||||
egressService := NewEgressService(egressClient, rtcEgressLauncher, ioInfoService, roomService)
|
||||
ingressConfig := getIngressConfig(conf)
|
||||
ingressClient, err := rpc.NewIngressClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ingressService := NewIngressService(ingressConfig, nodeID, v, ingressClient, ingressStore, ioInfoService, telemetryService)
|
||||
sipConfig := getSIPConfig(conf)
|
||||
sipClient, err := newSIPClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sipService := NewSIPService(sipConfig, nodeID, v, sipClient, sipStore, roomService, telemetryService)
|
||||
rtcService := NewRTCService(conf, roomAllocator, router, telemetryService)
|
||||
v5, err := rpc.NewTypedWHIPParticipantClient(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceWHIPService, err := NewWHIPService(conf, router, roomAllocator, clientParams, topicFormatter, v5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agentService, err := NewAgentService(conf, currentNode, v, keyProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agentConfig := getAgentConfig(conf)
|
||||
client, err := agent.NewAgentClient(v, agentConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agentStore := getAgentStore(objectStore)
|
||||
timedVersionGenerator := utils.NewDefaultTimedVersionGenerator()
|
||||
turnAuthHandler := NewTURNAuthHandler(keyProvider)
|
||||
forwardStats := createForwardStats(conf)
|
||||
roomManager, err := NewLocalRoomManager(conf, objectStore, currentNode, router, roomAllocator, telemetryService, client, agentStore, rtcEgressLauncher, timedVersionGenerator, turnAuthHandler, v, forwardStats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signalServer, err := NewDefaultSignalServer(currentNode, v, signalRelayConfig, router, roomManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v6 := getTURNAuthHandlerFunc(turnAuthHandler)
|
||||
server, err := newInProcessTurnServer(conf, v6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
return livekitServer, nil
|
||||
}
|
||||
|
||||
func InitializeRouter(conf *config.Config, currentNode routing.LocalNode) (routing.Router, error) {
|
||||
universalClient, err := createRedisClient(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeID := getNodeID(currentNode)
|
||||
psrpcConfig := getPSRPCConfig(conf)
|
||||
v := getMessageBus(universalClient, psrpcConfig)
|
||||
signalRelayConfig := getSignalRelayConfig(conf)
|
||||
signalClient, err := routing.NewSignalClient(nodeID, v, signalRelayConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientParams := getPSRPCClientParams(psrpcConfig, v)
|
||||
roomConfig := getRoomConfig(conf)
|
||||
roomManagerClient, err := routing.NewRoomManagerClient(clientParams, roomConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keepalivePubSub, err := rpc.NewKeepalivePubSub(clientParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeStatsConfig := getNodeStatsConfig(conf)
|
||||
router := routing.CreateRouter(universalClient, currentNode, signalClient, roomManagerClient, keepalivePubSub, nodeStatsConfig)
|
||||
return router, nil
|
||||
}
|
||||
|
||||
// wire.go:
|
||||
|
||||
func getNodeID(currentNode routing.LocalNode) livekit.NodeID {
|
||||
return currentNode.NodeID()
|
||||
}
|
||||
|
||||
func createKeyProvider(conf *config.Config) (auth.KeyProvider, error) {
|
||||
|
||||
if conf.KeyFile != "" {
|
||||
var otherFilter os.FileMode = 0007
|
||||
if st, err := os.Stat(conf.KeyFile); err != nil {
|
||||
return nil, err
|
||||
} else if st.Mode().Perm()&otherFilter != 0000 {
|
||||
return nil, fmt.Errorf("key file others permissions must be set to 0")
|
||||
}
|
||||
f, err := os.Open(conf.KeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
decoder := yaml.NewDecoder(f)
|
||||
if err = decoder.Decode(conf.Keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(conf.Keys) == 0 {
|
||||
return nil, errors.New("one of key-file or keys must be provided in order to support a secure installation")
|
||||
}
|
||||
|
||||
return auth.NewFileBasedKeyProviderFromMap(conf.Keys), nil
|
||||
}
|
||||
|
||||
func createWebhookNotifier(conf *config.Config, provider auth.KeyProvider) (webhook.QueuedNotifier, error) {
|
||||
wc := conf.WebHook
|
||||
|
||||
secret := provider.GetSecret(wc.APIKey)
|
||||
if secret == "" && len(wc.URLs) > 0 {
|
||||
return nil, ErrWebHookMissingAPIKey
|
||||
}
|
||||
|
||||
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) {
|
||||
if !conf.Redis.IsConfigured() {
|
||||
return nil, nil
|
||||
}
|
||||
return redis2.GetRedisClient(&conf.Redis)
|
||||
}
|
||||
|
||||
func createStore(rc redis.UniversalClient) ObjectStore {
|
||||
if rc != nil {
|
||||
return NewRedisStore(rc)
|
||||
}
|
||||
return NewLocalStore()
|
||||
}
|
||||
|
||||
func getMessageBus(rc redis.UniversalClient, psrpcConf rpc.PSRPCConfig) psrpc.MessageBus {
|
||||
opts := psrpcConf.BusOptions()
|
||||
if rc == nil {
|
||||
return psrpc.NewLocalMessageBus(opts...)
|
||||
}
|
||||
return psrpc.NewRedisMessageBus(rc, opts...)
|
||||
}
|
||||
|
||||
func getEgressStore(s ObjectStore) EgressStore {
|
||||
switch store := s.(type) {
|
||||
case *RedisStore:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getIngressStore(s ObjectStore) IngressStore {
|
||||
switch store := s.(type) {
|
||||
case *RedisStore:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getAgentStore(s ObjectStore) AgentStore {
|
||||
switch store := s.(type) {
|
||||
case *RedisStore:
|
||||
return store
|
||||
case *LocalStore:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
return store
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getSIPConfig(conf *config.Config) *config.SIPConfig {
|
||||
return &conf.SIP
|
||||
}
|
||||
|
||||
func getLimitConf(config2 *config.Config) config.LimitConfig {
|
||||
return config2.Limit
|
||||
}
|
||||
|
||||
func getRoomConfig(config2 *config.Config) config.RoomConfig {
|
||||
return config2.Room
|
||||
}
|
||||
|
||||
func getSignalRelayConfig(config2 *config.Config) config.SignalRelayConfig {
|
||||
return config2.SignalRelay
|
||||
}
|
||||
|
||||
func getPSRPCConfig(config2 *config.Config) rpc.PSRPCConfig {
|
||||
return config2.PSRPC
|
||||
}
|
||||
|
||||
func getPSRPCClientParams(config2 rpc.PSRPCConfig, bus psrpc.MessageBus) rpc.ClientParams {
|
||||
return rpc.NewClientParams(config2, bus, logger.GetLogger(), rpc.PSRPCMetricsObserver{}, otelpsrpc.ClientOptions(otelpsrpc.Config{}))
|
||||
}
|
||||
|
||||
func createForwardStats(conf *config.Config) *sfu.ForwardStats {
|
||||
if conf.RTC.ForwardStats.SummaryInterval == 0 || conf.RTC.ForwardStats.ReportInterval == 0 || conf.RTC.ForwardStats.ReportWindow == 0 {
|
||||
return nil
|
||||
}
|
||||
return sfu.NewForwardStats(conf.RTC.ForwardStats.SummaryInterval, conf.RTC.ForwardStats.ReportInterval, conf.RTC.ForwardStats.ReportWindow)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
249
pkg/service/wsprotocol.go
Normal file
249
pkg/service/wsprotocol.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/frostbyte73/core"
|
||||
"github.com/gorilla/websocket"
|
||||
"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
|
||||
closeWriteTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type WSSignalConnection struct {
|
||||
conn types.WebsocketClient
|
||||
mu sync.Mutex
|
||||
useJSON bool
|
||||
|
||||
// maximum size (in bytes) of a single decompressed message; 0 disables the limit
|
||||
messageSizeLimit int64
|
||||
|
||||
closed core.Fuse
|
||||
}
|
||||
|
||||
func NewWSSignalConnection(conn types.WebsocketClient, messageSizeLimit int64) *WSSignalConnection {
|
||||
wsc := &WSSignalConnection{
|
||||
conn: conn,
|
||||
mu: sync.Mutex{},
|
||||
useJSON: false,
|
||||
messageSizeLimit: messageSizeLimit,
|
||||
}
|
||||
go wsc.pingWorker()
|
||||
return wsc
|
||||
}
|
||||
|
||||
// readMessage reads a single websocket message, bounding the size of the
|
||||
// decompressed payload. The transport-level read limit (SetReadLimit) only
|
||||
// accounts for the compressed bytes read off the wire, so a small compressed
|
||||
// frame can still expand into a much larger buffer once inflated. Reading through
|
||||
// an io.LimitReader caps the decompressed size at the same limit.
|
||||
func (c *WSSignalConnection) readMessage() (int, []byte, error) {
|
||||
messageType, r, err := c.conn.NextReader()
|
||||
if err != nil {
|
||||
return messageType, nil, err
|
||||
}
|
||||
|
||||
if c.messageSizeLimit > 0 {
|
||||
// read one byte past the limit so an exactly-at-limit message is accepted
|
||||
// while anything larger is detected and rejected
|
||||
limited := io.LimitReader(r, c.messageSizeLimit+1)
|
||||
payload, err := io.ReadAll(limited)
|
||||
if err != nil {
|
||||
return messageType, nil, err
|
||||
}
|
||||
if int64(len(payload)) > c.messageSizeLimit {
|
||||
return messageType, nil, fmt.Errorf("signal message exceeds size limit of %d bytes", c.messageSizeLimit)
|
||||
}
|
||||
return messageType, payload, nil
|
||||
}
|
||||
|
||||
payload, err := io.ReadAll(r)
|
||||
return messageType, payload, err
|
||||
}
|
||||
|
||||
func (c *WSSignalConnection) Close() error {
|
||||
c.closed.Break()
|
||||
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *WSSignalConnection) CloseWithReason(reason string) error {
|
||||
c.closed.Break()
|
||||
|
||||
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) {
|
||||
// handle special messages and pass on the rest
|
||||
messageType, payload, err := c.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:
|
||||
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) {
|
||||
// handle special messages and pass on the rest
|
||||
messageType, payload, err := c.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:
|
||||
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) WriteResponse(msg *livekit.SignalResponse) (int, error) {
|
||||
var msgType int
|
||||
var payload []byte
|
||||
var err error
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.useJSON {
|
||||
msgType = websocket.TextMessage
|
||||
payload, err = protojson.Marshal(msg)
|
||||
} else {
|
||||
msgType = websocket.BinaryMessage
|
||||
payload, err = proto.Marshal(msg)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(payload), c.conn.WriteMessage(msgType, payload)
|
||||
}
|
||||
|
||||
func (c *WSSignalConnection) WriteServerMessage(msg *livekit.ServerMessage) (int, error) {
|
||||
var msgType int
|
||||
var payload []byte
|
||||
var err error
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.useJSON {
|
||||
msgType = websocket.TextMessage
|
||||
payload, err = protojson.Marshal(msg)
|
||||
} else {
|
||||
msgType = websocket.BinaryMessage
|
||||
payload, err = proto.Marshal(msg)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(payload), c.conn.WriteMessage(msgType, payload)
|
||||
}
|
||||
|
||||
func (c *WSSignalConnection) pingWorker() {
|
||||
ticker := time.NewTicker(pingFrequency)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.closed.Watch():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
err := c.conn.WriteControl(websocket.PingMessage, []byte(""), time.Now().Add(pingTimeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsWebSocketCloseError checks that error is normal/expected closure
|
||||
func IsWebSocketCloseError(err error) bool {
|
||||
return errors.Is(err, io.EOF) ||
|
||||
strings.HasSuffix(err.Error(), "use of closed network connection") ||
|
||||
strings.HasSuffix(err.Error(), "connection reset by peer") ||
|
||||
websocket.IsCloseError(
|
||||
err,
|
||||
websocket.CloseAbnormalClosure,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseNormalClosure,
|
||||
websocket.CloseNoStatusReceived,
|
||||
)
|
||||
}
|
||||
74
pkg/service/wsprotocol_test.go
Normal file
74
pkg/service/wsprotocol_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types/typesfakes"
|
||||
"github.com/livekit/livekit-server/pkg/service"
|
||||
)
|
||||
|
||||
// TestWSSignalConnectionMessageSizeLimit exercises the decompressed-size bound in
|
||||
// WSSignalConnection. NextReader returns the (already decompressed) message
|
||||
// stream, so a reader larger than the limit stands in for a small compressed
|
||||
// frame that expands past the limit once inflated.
|
||||
func TestWSSignalConnectionMessageSizeLimit(t *testing.T) {
|
||||
const limit = 1024
|
||||
|
||||
t.Run("rejects message larger than limit", func(t *testing.T) {
|
||||
fake := &typesfakes.FakeWebsocketClient{}
|
||||
fake.NextReaderReturns(websocket.BinaryMessage, bytes.NewReader(make([]byte, limit+1)), nil)
|
||||
|
||||
c := service.NewWSSignalConnection(fake, limit)
|
||||
_, _, err := c.ReadRequest()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds size limit")
|
||||
})
|
||||
|
||||
t.Run("accepts message within limit", func(t *testing.T) {
|
||||
payload, err := proto.Marshal(&livekit.SignalRequest{})
|
||||
require.NoError(t, err)
|
||||
require.LessOrEqual(t, len(payload), limit)
|
||||
|
||||
fake := &typesfakes.FakeWebsocketClient{}
|
||||
fake.NextReaderReturns(websocket.BinaryMessage, bytes.NewReader(payload), nil)
|
||||
|
||||
c := service.NewWSSignalConnection(fake, limit)
|
||||
msg, _, err := c.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, msg)
|
||||
})
|
||||
|
||||
t.Run("limit of zero reads unbounded payload", func(t *testing.T) {
|
||||
// a payload well beyond any typical limit is read in full when disabled
|
||||
payload, err := proto.Marshal(&livekit.SignalRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
fake := &typesfakes.FakeWebsocketClient{}
|
||||
fake.NextReaderReturns(websocket.BinaryMessage, bytes.NewReader(payload), nil)
|
||||
|
||||
c := service.NewWSSignalConnection(fake, 0)
|
||||
_, _, err = c.ReadRequest()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user