Files
livekit/pkg/service/roommanager_test.go
Martin Njalale 2fab31691b service: start room composite egress configured in a room preset (#4926)
* service: start room composite egress configured in a room preset

StandardRoomAllocator.CreateRoom merged a room preset into a copy of the
request and discarded it. RoomInternal has no field for room composite
egress, and getOrCreateRoom started it from the caller's original request,
so a room composite egress from a preset was never started.

RoomAllocator.CreateRoom now also returns the request with the preset
applied, and getOrCreateRoom starts the egress from it.

* service: return ErrEgressNotConnected instead of calling a nil egress launcher

getOrCreateRoom started a room composite egress without checking that an
egress launcher exists. RoomService.CreateRoom rejects egress on the request
when there is none, but not egress from a room preset or a token's roomConfig.
2026-09-30 13:25:24 +02:00

182 lines
5.4 KiB
Go

// 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"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/mediatransportutil/pkg/rtcconfig"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils"
"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/routingfakes"
"github.com/livekit/livekit-server/pkg/rtc"
"github.com/livekit/livekit-server/pkg/service"
"github.com/livekit/livekit-server/pkg/telemetry/telemetryfakes"
)
// fakeEgressLauncher records the egress start requests it receives.
type fakeEgressLauncher struct {
mu sync.Mutex
started []*rpc.StartEgressRequest
}
func (f *fakeEgressLauncher) StartEgress(_ context.Context, req *rpc.StartEgressRequest) (*livekit.EgressInfo, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.started = append(f.started, req)
return &livekit.EgressInfo{}, nil
}
func (f *fakeEgressLauncher) StopEgress(context.Context, *livekit.StopEgressRequest) (*livekit.EgressInfo, error) {
return &livekit.EgressInfo{}, nil
}
func (f *fakeEgressLauncher) roomCompositeRoomNames() []string {
f.mu.Lock()
defer f.mu.Unlock()
var names []string
for _, req := range f.started {
if rc := req.GetRoomComposite(); rc != nil {
names = append(names, rc.RoomName)
}
}
return names
}
func newTestRoomManager(t *testing.T, conf *config.Config, egressLauncher rtc.EgressLauncher) *service.RoomManager {
// no fixed RTC ports, so that the test does not bind the defaults
conf.RTC.TCPPort = 0
conf.RTC.UDPPort = rtcconfig.PortRange{}
node, err := routing.NewLocalNode(conf)
require.NoError(t, err)
router := &routingfakes.FakeRouter{}
router.GetNodeForRoomReturns(node.Clone(), nil)
store := service.NewLocalStore()
ra, err := service.NewRoomAllocator(conf, router, store)
require.NoError(t, err)
rm, err := service.NewLocalRoomManager(
conf,
store,
node,
router,
ra,
&telemetryfakes.FakeTelemetryService{},
nil,
store,
egressLauncher,
utils.NewDefaultTimedVersionGenerator(),
nil,
psrpc.NewLocalMessageBus(),
nil,
)
require.NoError(t, err)
t.Cleanup(rm.Stop)
return rm
}
func TestRoomCompositeEgressOnCreateRoom(t *testing.T) {
t.Run("from request", func(t *testing.T) {
conf, err := config.NewConfig("", true, nil, nil)
require.NoError(t, err)
launcher := &fakeEgressLauncher{}
rm := newTestRoomManager(t, conf, launcher)
_, err = rm.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
Name: "room-a",
Egress: &livekit.RoomEgress{
Room: &livekit.RoomCompositeEgressRequest{
Layout: "grid",
FileOutputs: []*livekit.EncodedFileOutput{{Filepath: "recordings/{room_name}.mp4"}},
},
},
})
require.NoError(t, err)
require.Equal(t, []string{"room-a"}, launcher.roomCompositeRoomNames())
})
t.Run("from room preset", func(t *testing.T) {
conf, err := config.NewConfig(roomPresetConfig, true, nil, nil)
require.NoError(t, err)
launcher := &fakeEgressLauncher{}
rm := newTestRoomManager(t, conf, launcher)
for _, name := range []string{"room-a", "room-b"} {
_, err = rm.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
Name: name,
RoomPreset: "record",
})
require.NoError(t, err)
}
// each room records itself, and the preset is left as configured
require.Equal(t, []string{"room-a", "room-b"}, launcher.roomCompositeRoomNames())
require.Empty(t, conf.Room.RoomConfigurations["record"].GetEgress().GetRoom().GetRoomName())
})
t.Run("from room preset when a session starts", func(t *testing.T) {
conf, err := config.NewConfig(roomPresetConfig, true, nil, nil)
require.NoError(t, err)
launcher := &fakeEgressLauncher{}
rm := newTestRoomManager(t, conf, launcher)
// the join and WHIP paths create the room through StartSession, with the
// token's room preset; without an identity only the room is created
err = rm.StartSession(context.Background(), routing.ParticipantInit{
CreateRoom: &livekit.CreateRoomRequest{
Name: "room-a",
RoomPreset: "record",
},
}, nil, nil, false)
require.NoError(t, err)
require.Equal(t, []string{"room-a"}, launcher.roomCompositeRoomNames())
})
t.Run("from room preset without an egress launcher", func(t *testing.T) {
conf, err := config.NewConfig(roomPresetConfig, true, nil, nil)
require.NoError(t, err)
rm := newTestRoomManager(t, conf, nil)
_, err = rm.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
Name: "room-a",
RoomPreset: "record",
})
require.ErrorIs(t, err, service.ErrEgressNotConnected)
})
}
const roomPresetConfig = `
room:
room_configurations:
record:
egress:
room:
layout: grid
file_outputs:
- filepath: recordings/{room_name}.mp4
`