Skip to content

Commit 63ab90f

Browse files
authored
feat(egress): support v2 StartEgress
lk egress start REQUEST_JSON with no --type now takes a livekit.StartEgressRequest and calls EgressClient.StartEgress. The five per-type values (room-composite, participant, track, track-composite, web) remain as explicit legacy opt-ins. Adds cmd/lk/examples/{media,template,web}-egress-v2.json.
1 parent 3a3142b commit 63ab90f

5 files changed

Lines changed: 135 additions & 19 deletions

File tree

cmd/lk/egress.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,18 @@ const (
5050
)
5151

5252
var (
53-
egressStartDescription = `Initiates a new egress of the chosen TYPE:
54-
- "room-composite" composes multiple participant tracks into a single output stream
55-
- "participant" captures a single participant
56-
- "track" captures a single track without transcoding
57-
- "track-composite" captures an audio and a video track
58-
- "web" captures any website, with a lifecycle detached from LiveKit rooms
59-
60-
REQUEST_JSON is one of:
61-
- ` + reflect.TypeFor[livekit.RoomCompositeEgressRequest]().Name() + `
62-
- ` + reflect.TypeFor[livekit.ParticipantEgressRequest]().Name() + `
63-
- ` + reflect.TypeFor[livekit.TrackEgressRequest]().Name() + `
64-
- ` + reflect.TypeFor[livekit.TrackCompositeEgressRequest]().Name() + `
65-
- ` + reflect.TypeFor[livekit.WebEgressRequest]().Name() + `
66-
53+
egressStartDescription = `Initiates a new egress.
54+
55+
REQUEST_JSON is a ` + reflect.TypeFor[livekit.StartEgressRequest]().Name() + `, whose source is a
56+
layout template, a web page, or media tracks from a room.
57+
58+
TYPE selects a deprecated per-type request instead:
59+
- "room-composite" composes multiple participant tracks into a single output stream, as ` + reflect.TypeFor[livekit.RoomCompositeEgressRequest]().Name() + `
60+
- "participant" captures a single participant, as ` + reflect.TypeFor[livekit.ParticipantEgressRequest]().Name() + `
61+
- "track" captures a single track without transcoding, as ` + reflect.TypeFor[livekit.TrackEgressRequest]().Name() + `
62+
- "track-composite" captures an audio and a video track, as ` + reflect.TypeFor[livekit.TrackCompositeEgressRequest]().Name() + `
63+
- "web" captures any website, with a lifecycle detached from LiveKit rooms, as ` + reflect.TypeFor[livekit.WebEgressRequest]().Name() + `
64+
6765
See cmd/livekit-cli/examples`
6866
)
6967

@@ -83,7 +81,6 @@ var (
8381
&cli.StringFlag{
8482
Name: "type",
8583
Usage: "Specify `TYPE` of egress (see above)",
86-
Value: string(EgressTypeRoomComposite),
8784
},
8885
},
8986
ArgsUsage: "REQUEST_JSON",
@@ -397,6 +394,8 @@ func createEgressClient(ctx context.Context, cmd *cli.Command) (context.Context,
397394

398395
func handleEgressStart(ctx context.Context, cmd *cli.Command) error {
399396
switch cmd.String("type") {
397+
case "":
398+
return startEgress(ctx, cmd)
400399
case string(EgressTypeRoomComposite):
401400
return startRoomCompositeEgress(ctx, cmd)
402401
case string(EgressTypeWeb):
@@ -563,6 +562,21 @@ func _deprecatedStartTrackEgress(ctx context.Context, cmd *cli.Command) error {
563562
return nil
564563
}
565564

565+
func startEgress(ctx context.Context, cmd *cli.Command) error {
566+
req, err := ReadRequestArg[livekit.StartEgressRequest](cmd)
567+
if err != nil {
568+
return err
569+
}
570+
571+
info, err := egressClient.StartEgress(ctx, req)
572+
if err != nil {
573+
return err
574+
}
575+
576+
printInfo(info)
577+
return nil
578+
}
579+
566580
func unmarshalEgressRequest(cmd *cli.Command, req proto.Message) error {
567581
reqBytes, err := os.ReadFile(cmd.String("request"))
568582
if err != nil {

cmd/lk/egress_test.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ import (
3030
lksdk "github.com/livekit/server-sdk-go/v2"
3131
)
3232

33-
// fakeEgressService implements livekit.Egress. Only ListEgress is exercised
34-
// by the egress list tests; the other RPCs return empty results.
33+
// fakeEgressService implements livekit.Egress. Only ListEgress and StartEgress
34+
// are exercised by the egress tests; the other RPCs return empty results.
3535
type fakeEgressService struct {
3636
listRequests []*livekit.ListEgressRequest
3737
listResponses []*livekit.ListEgressResponse
3838
listErr error
39+
startRequests []*livekit.StartEgressRequest
3940
}
4041

4142
func (f *fakeEgressService) ListEgress(_ context.Context, req *livekit.ListEgressRequest) (*livekit.ListEgressResponse, error) {
@@ -50,8 +51,9 @@ func (f *fakeEgressService) ListEgress(_ context.Context, req *livekit.ListEgres
5051
return f.listResponses[idx], nil
5152
}
5253

53-
func (f *fakeEgressService) StartEgress(_ context.Context, _ *livekit.StartEgressRequest) (*livekit.EgressInfo, error) {
54-
return nil, nil
54+
func (f *fakeEgressService) StartEgress(_ context.Context, req *livekit.StartEgressRequest) (*livekit.EgressInfo, error) {
55+
f.startRequests = append(f.startRequests, req)
56+
return &livekit.EgressInfo{EgressId: "EG_test"}, nil
5557
}
5658
func (f *fakeEgressService) StartRoomCompositeEgress(_ context.Context, _ *livekit.RoomCompositeEgressRequest) (*livekit.EgressInfo, error) {
5759
return nil, nil
@@ -427,3 +429,49 @@ func TestListEgress_JSONOrdering_ByID(t *testing.T) {
427429

428430
assert.Equal(t, []string{"EG_C", "EG_A", "EG_B"}, extractEgressIDs(t, buf.Bytes()))
429431
}
432+
433+
func buildEgressStartCommand(t *testing.T, egressType, request string) *cli.Command {
434+
t.Helper()
435+
var captured *cli.Command
436+
app := &cli.Command{
437+
Name: "test",
438+
Flags: []cli.Flag{&cli.StringFlag{Name: "type"}},
439+
Action: func(_ context.Context, cmd *cli.Command) error {
440+
captured = cmd
441+
return nil
442+
},
443+
}
444+
445+
args := []string{"test"}
446+
if egressType != "" {
447+
args = append(args, "--type", egressType)
448+
}
449+
args = append(args, request)
450+
451+
require.NoError(t, app.Run(context.Background(), args))
452+
require.NotNil(t, captured)
453+
return captured
454+
}
455+
456+
func TestEgressStart_Media(t *testing.T) {
457+
svc := &fakeEgressService{}
458+
setupFakeEgressClient(t, svc)
459+
460+
cmd := buildEgressStartCommand(t, "", `{
461+
"room_name": "my-room",
462+
"media": { "video_track_id": "TR_XXXXXXXXXXXX" },
463+
"outputs": [{ "file": { "filepath": "my-track.mp4" } }]
464+
}`)
465+
require.NoError(t, handleEgressStart(context.Background(), cmd))
466+
467+
require.Len(t, svc.startRequests, 1)
468+
req := svc.startRequests[0]
469+
assert.Equal(t, "my-room", req.GetRoomName())
470+
assert.Equal(t, "TR_XXXXXXXXXXXX", req.GetMedia().GetVideoTrackId())
471+
require.Len(t, req.GetOutputs(), 1)
472+
assert.Equal(t, "my-track.mp4", req.GetOutputs()[0].GetFile().GetFilepath())
473+
}
474+
475+
func TestEgressStart_UnknownType(t *testing.T) {
476+
require.Error(t, handleEgressStart(context.Background(), buildEgressStartCommand(t, "nonsense", "{}")))
477+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"room_name": "my-room",
3+
"media": {
4+
"video_track_id": "TR_XXXXXXXXXXXX"
5+
},
6+
"outputs": [
7+
{
8+
"file": {
9+
"filepath": "my-track.mp4"
10+
}
11+
}
12+
]
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"room_name": "my-room",
3+
"template": {
4+
"layout": "speaker-dark"
5+
},
6+
"outputs": [
7+
{
8+
"file": {
9+
"filepath": "livekit-demo/my-room-test.mp4"
10+
}
11+
}
12+
],
13+
"storage": {
14+
"s3": {
15+
"access_key": "aws-access-key",
16+
"secret": "aws-access-secret",
17+
"region": "aws-region",
18+
"bucket": "my-bucket"
19+
}
20+
}
21+
}

cmd/lk/examples/web-egress-v2.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"web": {
3+
"url": "https://docs.livekit.io/server/egress"
4+
},
5+
"outputs": [
6+
{
7+
"file": {
8+
"filepath": "livekit-demo/my-web-test.mp4"
9+
}
10+
}
11+
],
12+
"storage": {
13+
"s3": {
14+
"access_key": "aws-access-key",
15+
"secret": "aws-access-secret",
16+
"region": "aws-region",
17+
"bucket": "my-bucket"
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)