Files
livekit/pkg/service/egress.go
T
Milos Pesic b34b047247 Add StopEgress function to the EgressLauncher interface (#4353)
This allows for abstracting away how the stop is implemented - default implementation stays the same - the existing OSS egress launcher just calls the existing Stop method on the client.
2026-03-09 13:17:05 +01:00

374 lines
9.5 KiB
Go

// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"context"
"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) 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
}
if roomName != "" {
room, _, err := s.store.LoadRoom(ctx, livekit.RoomName(roomName), false)
if err != nil {
return nil, err
}
req.RoomId = room.Sid
}
}
info, err := s.client.StartEgress(ctx, "", req)
if err != nil {
return nil, err
}
_, err = s.io.CreateEgress(ctx, info)
if err != nil {
logger.Errorw("failed to create egress", err)
}
return info, nil
}
func (s *egressLauncher) StopEgress(ctx context.Context, req *livekit.StopEgressRequest) (*livekit.EgressInfo, error) {
if s.client == nil {
return nil, ErrEgressNotConnected
}
return s.client.StopEgress(ctx, req.EgressId, req)
}
type LayoutMetadata struct {
Layout string `json:"layout"`
}
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 (s *EgressService) UpdateStream(ctx context.Context, req *livekit.UpdateStreamRequest) (*livekit.EgressInfo, error) {
AppendLogFields(ctx, "egressID", req.EgressId, "addUrls", req.AddOutputUrls, "removeUrls", 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
}