diff --git a/cmd/lk/egress.go b/cmd/lk/egress.go index 313d7b284..9cf490e41 100644 --- a/cmd/lk/egress.go +++ b/cmd/lk/egress.go @@ -50,20 +50,18 @@ const ( ) var ( - egressStartDescription = `Initiates a new egress of the chosen TYPE: - - "room-composite" composes multiple participant tracks into a single output stream - - "participant" captures a single participant - - "track" captures a single track without transcoding - - "track-composite" captures an audio and a video track - - "web" captures any website, with a lifecycle detached from LiveKit rooms - -REQUEST_JSON is one of: - - ` + reflect.TypeFor[livekit.RoomCompositeEgressRequest]().Name() + ` - - ` + reflect.TypeFor[livekit.ParticipantEgressRequest]().Name() + ` - - ` + reflect.TypeFor[livekit.TrackEgressRequest]().Name() + ` - - ` + reflect.TypeFor[livekit.TrackCompositeEgressRequest]().Name() + ` - - ` + reflect.TypeFor[livekit.WebEgressRequest]().Name() + ` - + egressStartDescription = `Initiates a new egress. + +REQUEST_JSON is a ` + reflect.TypeFor[livekit.StartEgressRequest]().Name() + `, whose source is a +layout template, a web page, or media tracks from a room. + +TYPE selects a deprecated per-type request instead: + - "room-composite" composes multiple participant tracks into a single output stream, as ` + reflect.TypeFor[livekit.RoomCompositeEgressRequest]().Name() + ` + - "participant" captures a single participant, as ` + reflect.TypeFor[livekit.ParticipantEgressRequest]().Name() + ` + - "track" captures a single track without transcoding, as ` + reflect.TypeFor[livekit.TrackEgressRequest]().Name() + ` + - "track-composite" captures an audio and a video track, as ` + reflect.TypeFor[livekit.TrackCompositeEgressRequest]().Name() + ` + - "web" captures any website, with a lifecycle detached from LiveKit rooms, as ` + reflect.TypeFor[livekit.WebEgressRequest]().Name() + ` + See cmd/livekit-cli/examples` ) @@ -83,7 +81,6 @@ var ( &cli.StringFlag{ Name: "type", Usage: "Specify `TYPE` of egress (see above)", - Value: string(EgressTypeRoomComposite), }, }, ArgsUsage: "REQUEST_JSON", @@ -397,6 +394,8 @@ func createEgressClient(ctx context.Context, cmd *cli.Command) (context.Context, func handleEgressStart(ctx context.Context, cmd *cli.Command) error { switch cmd.String("type") { + case "": + return startEgress(ctx, cmd) case string(EgressTypeRoomComposite): return startRoomCompositeEgress(ctx, cmd) case string(EgressTypeWeb): @@ -563,6 +562,21 @@ func _deprecatedStartTrackEgress(ctx context.Context, cmd *cli.Command) error { return nil } +func startEgress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArg[livekit.StartEgressRequest](cmd) + if err != nil { + return err + } + + info, err := egressClient.StartEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + func unmarshalEgressRequest(cmd *cli.Command, req proto.Message) error { reqBytes, err := os.ReadFile(cmd.String("request")) if err != nil { diff --git a/cmd/lk/egress_test.go b/cmd/lk/egress_test.go index d606027c8..61bd36448 100644 --- a/cmd/lk/egress_test.go +++ b/cmd/lk/egress_test.go @@ -30,12 +30,13 @@ import ( lksdk "github.com/livekit/server-sdk-go/v2" ) -// fakeEgressService implements livekit.Egress. Only ListEgress is exercised -// by the egress list tests; the other RPCs return empty results. +// fakeEgressService implements livekit.Egress. Only ListEgress and StartEgress +// are exercised by the egress tests; the other RPCs return empty results. type fakeEgressService struct { listRequests []*livekit.ListEgressRequest listResponses []*livekit.ListEgressResponse listErr error + startRequests []*livekit.StartEgressRequest } 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 return f.listResponses[idx], nil } -func (f *fakeEgressService) StartEgress(_ context.Context, _ *livekit.StartEgressRequest) (*livekit.EgressInfo, error) { - return nil, nil +func (f *fakeEgressService) StartEgress(_ context.Context, req *livekit.StartEgressRequest) (*livekit.EgressInfo, error) { + f.startRequests = append(f.startRequests, req) + return &livekit.EgressInfo{EgressId: "EG_test"}, nil } func (f *fakeEgressService) StartRoomCompositeEgress(_ context.Context, _ *livekit.RoomCompositeEgressRequest) (*livekit.EgressInfo, error) { return nil, nil @@ -427,3 +429,49 @@ func TestListEgress_JSONOrdering_ByID(t *testing.T) { assert.Equal(t, []string{"EG_C", "EG_A", "EG_B"}, extractEgressIDs(t, buf.Bytes())) } + +func buildEgressStartCommand(t *testing.T, egressType, request string) *cli.Command { + t.Helper() + var captured *cli.Command + app := &cli.Command{ + Name: "test", + Flags: []cli.Flag{&cli.StringFlag{Name: "type"}}, + Action: func(_ context.Context, cmd *cli.Command) error { + captured = cmd + return nil + }, + } + + args := []string{"test"} + if egressType != "" { + args = append(args, "--type", egressType) + } + args = append(args, request) + + require.NoError(t, app.Run(context.Background(), args)) + require.NotNil(t, captured) + return captured +} + +func TestEgressStart_Media(t *testing.T) { + svc := &fakeEgressService{} + setupFakeEgressClient(t, svc) + + cmd := buildEgressStartCommand(t, "", `{ + "room_name": "my-room", + "media": { "video_track_id": "TR_XXXXXXXXXXXX" }, + "outputs": [{ "file": { "filepath": "my-track.mp4" } }] + }`) + require.NoError(t, handleEgressStart(context.Background(), cmd)) + + require.Len(t, svc.startRequests, 1) + req := svc.startRequests[0] + assert.Equal(t, "my-room", req.GetRoomName()) + assert.Equal(t, "TR_XXXXXXXXXXXX", req.GetMedia().GetVideoTrackId()) + require.Len(t, req.GetOutputs(), 1) + assert.Equal(t, "my-track.mp4", req.GetOutputs()[0].GetFile().GetFilepath()) +} + +func TestEgressStart_UnknownType(t *testing.T) { + require.Error(t, handleEgressStart(context.Background(), buildEgressStartCommand(t, "nonsense", "{}"))) +} diff --git a/cmd/lk/examples/media-egress-v2.json b/cmd/lk/examples/media-egress-v2.json new file mode 100644 index 000000000..4c4f554b7 --- /dev/null +++ b/cmd/lk/examples/media-egress-v2.json @@ -0,0 +1,13 @@ +{ + "room_name": "my-room", + "media": { + "video_track_id": "TR_XXXXXXXXXXXX" + }, + "outputs": [ + { + "file": { + "filepath": "my-track.mp4" + } + } + ] +} diff --git a/cmd/lk/examples/template-egress-v2.json b/cmd/lk/examples/template-egress-v2.json new file mode 100644 index 000000000..efafeca48 --- /dev/null +++ b/cmd/lk/examples/template-egress-v2.json @@ -0,0 +1,21 @@ +{ + "room_name": "my-room", + "template": { + "layout": "speaker-dark" + }, + "outputs": [ + { + "file": { + "filepath": "livekit-demo/my-room-test.mp4" + } + } + ], + "storage": { + "s3": { + "access_key": "aws-access-key", + "secret": "aws-access-secret", + "region": "aws-region", + "bucket": "my-bucket" + } + } +} diff --git a/cmd/lk/examples/web-egress-v2.json b/cmd/lk/examples/web-egress-v2.json new file mode 100644 index 000000000..37fcc880d --- /dev/null +++ b/cmd/lk/examples/web-egress-v2.json @@ -0,0 +1,20 @@ +{ + "web": { + "url": "https://docs.livekit.io/server/egress" + }, + "outputs": [ + { + "file": { + "filepath": "livekit-demo/my-web-test.mp4" + } + } + ], + "storage": { + "s3": { + "access_key": "aws-access-key", + "secret": "aws-access-secret", + "region": "aws-region", + "bucket": "my-bucket" + } + } +}