diff --git a/.github/workflows/install-test.yaml b/.github/workflows/install-test.yaml index fd203fbe..254e5690 100644 --- a/.github/workflows/install-test.yaml +++ b/.github/workflows/install-test.yaml @@ -23,7 +23,7 @@ on: # workflow tasks jobs: - intall-test: + install-test: runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c24d3661..40c6b7ce 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -16,11 +16,11 @@ before: hooks: - go mod tidy builds: - - id: livekit-cli + - id: lk env: - CGO_ENABLED=0 - main: ./cmd/livekit-cli - binary: livekit-cli + main: ./cmd/lk + binary: lk goarm: - "7" goarch: @@ -42,7 +42,7 @@ archives: release: github: owner: livekit - name: livekit-cli + name: lk draft: true prerelease: auto changelog: diff --git a/Dockerfile b/Dockerfile index 93888c38..4931f5ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,11 +32,11 @@ COPY cmd/ cmd/ COPY pkg/ pkg/ COPY version.go version.go -RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -a -o livekit-cli ./cmd/livekit-cli +RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -a -o lk ./cmd/lk FROM alpine:3.20 -COPY --from=builder /workspace/livekit-cli /livekit-cli +COPY --from=builder /workspace/lk /lk # Run the binary. -ENTRYPOINT ["/livekit-cli"] +ENTRYPOINT ["/lk"] diff --git a/Makefile b/Makefile index 9557d5b1..bde0ace9 100644 --- a/Makefile +++ b/Makefile @@ -5,11 +5,12 @@ GOBIN=$(shell go env GOBIN) endif cli: check_lfs - go build -ldflags "-w -s" -o bin/livekit-cli ./cmd/livekit-cli - GOOS=linux GOARCH=amd64 go build -o bin/livekit-cli-linux ./cmd/livekit-cli + go build -ldflags "-w -s" -o bin/lk ./cmd/lk + GOOS=linux GOARCH=amd64 go build -o bin/lk-linux ./cmd/lk install: cli - cp bin/livekit-cli $(GOBIN)/ + cp bin/lk $(GOBIN)/ + ln -sf $(GOBIN)/lk $(GOBIN)/livekit-cli check_lfs: @{ \ @@ -20,4 +21,4 @@ check_lfs: } fish_autocomplete: cli - ./bin/livekit-cli generate-fish-completion -o autocomplete/fish_autocomplete + ./bin/lk generate-fish-completion -o autocomplete/fish_autocomplete diff --git a/README.md b/README.md index 5e3de993..99b314da 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This package includes command line utilities that interacts with LiveKit. It allows you to: - Create access tokens -- Access LiveKit APIs, create, delete rooms, etc +- Access LiveKit APIs, create, delete rooms, etc. - Join a room as a participant, inspecting in-room events - Start and manage Egress - Perform load testing, efficiently simulating real-world load @@ -49,29 +49,28 @@ make install # Usage -See `livekit-cli --help` for a complete list of subcommands. +See `lk --help` for a complete list of subcommands. ## Set up your project [new] When a default project is set up, you can omit `url`, `api-key`, and `api-secret` when using the CLI. -You could also set up multiple projects, and switch the active project used with the `--project` flag. +You can also set up multiple projects, and temporarily switch the active project used with the `--project` flag, or persistently using `lk project set-default`. ### Adding a project ```shell -livekit-cli project add +lk project add --api-key --api-secret ``` - ### Listing projects ```shell -livekit-cli project list +lk project list ``` ### Switching defaults ```shell -livekit-cli project set-default +lk project set-default ``` ## Publishing to a room @@ -81,11 +80,10 @@ livekit-cli project set-default To publish a demo video as a participant's track, use the following. ```shell -livekit-cli join-room --room yourroom --identity publisher \ - --publish-demo +lk room join --identity publisher --publish-demo ``` -It'll publish the video track with [simulcast](https://blog.livekit.io/an-introduction-to-webrtc-simulcast-6c5f1f6402eb/), at 720p, 360p, and 180p. +This will publish the demo video track with [simulcast](https://blog.livekit.io/an-introduction-to-webrtc-simulcast-6c5f1f6402eb/), at 720p, 360p, and 180p. ### Publish media files @@ -93,13 +91,14 @@ You can publish your own audio/video files. These tracks files need to be encode Refer to [encoding instructions](https://github.com/livekit/server-sdk-go/tree/main#publishing-tracks-to-room) ```shell -livekit-cli join-room --room yourroom --identity publisher \ - --publish path/to/video.ivf \ - --publish path/to/audio.ogg \ - --fps 23.98 +lk room join --identity publisher \ + --publish \ + --publish \ + --fps 23.98 \ + ``` -This will publish the pre-encoded ivf and ogg files to the room, indicating video FPS of 23.98. Note that the FPS only affects the video; it's important to match video framerate with the source to prevent out of sync issues. +This will publish the pre-encoded `.ivf` and `.ogg` files to the room, indicating video FPS of 23.98. Note that the FPS only affects the video; it's important to match video framerate with the source to prevent out of sync issues. Note: For files uploaded via CLI, expect an initial delay before the video becomes visible to the remote viewer. This delay is attributed to the pre-encoded video's fixed keyframe intervals. Video encoded with LiveKit client SDKs do not have this delay. @@ -108,7 +107,7 @@ Note: For files uploaded via CLI, expect an initial delay before the video becom It's possible to publish any source that FFmpeg supports (including live sources such as RTSP) by using it as a transcoder. This is done by running FFmpeg in a separate process, encoding to a Unix socket. (not available on Windows). -`livekit-cli` can then read transcoded data from the socket and publishing them to the room. +`lk` can then read transcoded data from the socket and publishing them to the room. First run FFmpeg like this: @@ -123,25 +122,27 @@ ffmpeg -i \ This transcodes the input into H.264 baseline profile and Opus. -Then, run `livekit-cli` like this: +Then, run `lk` like this: ```shell -livekit-cli join-room --room yourroom --identity bot \ +lk room join --identity bot \ --publish h264:///tmp/myvideo.sock \ - --publish opus:///tmp/myaudio.sock + --publish opus:///tmp/myaudio.sock \ + ```` You should now see both video and audio tracks published to the room. ### Publish from TCP (i.e. gstreamer) -It's possible to publish from video streams coming over a TCP socket. `livekit-cli` can act as a TCP client. For example, with a gstreamer pipeline ending in `! tcpserversink port=16400` and streaming H.264. +It's possible to publish from video streams coming over a TCP socket. `lk` can act as a TCP client. For example, with a gstreamer pipeline ending in `! tcpserversink port=16400` and streaming H.264. -Run `livekit-cli` like this: +Run `lk` like this: ```shell -livekit-cli join-room --room yourroom --identity bot \ - --publish h264:///127.0.0.1:16400 +lk room join --identity bot \ + --publish h264:///127.0.0.1:16400 \ + ``` ### Publish streams from your application @@ -159,17 +160,17 @@ ffplay -i unix:/tmp/myvideo.sock Recording requires [egress service](https://docs.livekit.io/guides/egress/) to be set up first. -Example request.json files are [located here](https://github.com/livekit/livekit-cli/tree/main/cmd/livekit-cli/examples). +Example request.json files are [located here](https://github.com/livekit/livekit-cli/tree/main/cmd/lk/examples). ```shell -# start room composite (recording of room UI) -livekit-cli start-room-composite-egress --request request.json +# Start room composite (recording of room UI) +lk egress start --type room-composite -# start track composite (audio + video) -livekit-cli start-track-composite-egress --request request.json +# Start track composite (audio + video) +lk egress start --type track-composite -# start track egress (single audio or video track) -livekit-cli start-track-egress --request request.json +# Start track egress (single audio or video track) +lk egress start --type track ``` ### Testing egress templates @@ -183,9 +184,9 @@ It'll then open a browser to the template URL, with the correct parameters fille Here's an example: ```shell -livekit-cli test-egress-template \ +lk egress test-template \ --base-url http://localhost:3000 \ - --room --layout --video-publishers 3 + --room test-room --layout speaker --video-publishers 3 ``` This command will launch a browser pointed at `http://localhost:3000`, while simulating 3 publishers publishing to your livekit instance. @@ -194,14 +195,14 @@ This command will launch a browser pointed at `http://localhost:3000`, while sim Load testing utility for LiveKit. This tool is quite versatile and is able to simulate various types of load. -Note: `livekit-load-tester` has been renamed to sub-command `livekit-cli load-test` +Note: `livekit-load-tester` has been renamed to sub-command `lk load-test` ### Quickstart This guide requires a LiveKit server instance to be set up. You can start a load tester with: ```shell -livekit-cli load-test \ +lk load-test \ --room test-room --video-publishers 8 ``` @@ -212,7 +213,7 @@ This simulates 8 video publishers to the room, with no subscribers. Video tracks To test audio capabilities in your app, you can also simulate simultaneous speakers to the room. ```shell -livekit-cli load-test \ +lk load-test \ --room test-room --audio-publishers 5 ``` @@ -224,8 +225,8 @@ In a meeting, typically there's only one active speaker at a time, but this can Generate a token so you can log into the room: ```shell -livekit-cli create-token --join \ - --room test-room --identity user +lk token create --join \ + --room test-room --identity test-user ``` Head over to the [example web client](https://meet.livekit.io/?tab=custom) and paste in the token, you can see the simulated tracks published by the load tester. @@ -264,7 +265,7 @@ of data sent to its subscribers. Use this command to simulate a load test of 5 publishers, and 500 subscribers: ```shell -livekit-cli load-test \ +lk load-test \ --duration 1m \ --video-publishers 5 \ --subscribers 500 @@ -289,16 +290,16 @@ Summary | Tester | Tracks | Bitrate | Latency | Total Dr ### Advanced usage -You could customize various parameters of the test such as +You can customize various parameters of the test such as -- --video-publishers: number of video publishers -- --audio-publishers: number of audio publishers -- --subscribers: number of subscribers -- --video-resolution: publishing video resolution. low, medium, high -- --no-simulcast: disables simulcast -- --num-per-second: number of testers to start each second -- --layout: layout to simulate (speaker, 3x3, 4x4, or 5x5) -- --simulate-speakers: randomly rotate publishers to speak +- `--video-publishers`: number of video publishers +- `--audio-publishers`: number of audio publishers +- `--subscribers`: number of subscribers +- `--video-resolution`: publishing video resolution. low, medium, high +- `--no-simulcast`: disables simulcast +- `--num-per-second`: number of testers to start each second +- `--layout`: layout to simulate (speaker, 3x3, 4x4, or 5x5) +- `--simulate-speakers`: randomly rotate publishers to speak
diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index a4d5466f..4b00936a 100644 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -18,4 +18,4 @@ _cli_bash_autocomplete() { fi } -complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete livekit-cli +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete lk diff --git a/autocomplete/fish_autocomplete b/autocomplete/fish_autocomplete index 71638049..6fd496ec 100644 --- a/autocomplete/fish_autocomplete +++ b/autocomplete/fish_autocomplete @@ -1,6 +1,6 @@ # livekit-cli fish shell completion -function __fish_livekit-cli_no_subcommand --description 'Test if there has been any subcommand yet' +function __fish_lk_no_subcommand --description 'Test if there has been any subcommand yet' for i in (commandline -opc) if contains -- $i create-token create-room list-rooms delete-room update-room-metadata list-participants get-participant remove-participant update-participant mute-track update-subscriptions join-room start-room-composite-egress start-track-composite-egress start-track-egress list-egress update-layout update-stream stop-egress test-egress-template create-ingress update-ingress list-ingress delete-ingress load-test project add list remove set-default help h return 1 @@ -9,263 +9,263 @@ function __fish_livekit-cli_no_subcommand --description 'Test if there has been return 0 end -complete -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -f -l verbose -complete -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -f -l help -s h -d 'show help' -complete -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -f -l version -s v -d 'print the version' -complete -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -f -l help -s h -d 'show help' -complete -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -f -l version -s v -d 'print the version' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'create-token' -d 'creates an access token' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l create -d 'enable token to be used to create rooms' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l list -d 'enable token to be used to list rooms' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l join -d 'enable token to be used to join a room (requires --room and --identity)' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l admin -d 'enable token to be used to manage a room (requires --room)' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l identity -s i -r -d 'unique identity of the participant, used with --join' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l name -s n -r -d 'name of the participant, used with --join. defaults to identity' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l room -s r -r -d 'name of the room to join' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l metadata -r -d 'JSON metadata to encode in the token, will be passed to participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l valid-for -r -d 'amount of time that the token is valid for. i.e. "5m", "1h10m" (s: seconds, m: minutes, h: hours)' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-token' -f -l grant -r -d 'additional VideoGrant fields. It\'ll be merged with other arguments (JSON formatted)' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'create-room' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l name -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l room-egress-file -r -d 'RoomCompositeRequest json file (see examples/room-composite-file.json)' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-room' -f -l track-egress-file -r -d 'AutoTrackEgress json file (see examples/auto-track-egress.json)' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-rooms' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'list-rooms' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-rooms' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-rooms' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-rooms' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-rooms' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-rooms' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'delete-room' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-room' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'update-room-metadata' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-room-metadata' -f -l metadata -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'list-participants' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from list-participants' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'get-participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from get-participant' -f -l identity -r -d 'identity of participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'remove-participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from remove-participant' -f -l identity -r -d 'identity of participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'update-participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l identity -r -d 'identity of participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l metadata -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-participant' -f -l permissions -r -d 'JSON describing participant permissions (existing values for unset fields)' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'mute-track' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l identity -r -d 'identity of participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l track -r -d 'track sid to mute' -complete -c livekit-cli -n '__fish_seen_subcommand_from mute-track' -f -l muted -d 'set to true to mute, false to unmute' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'update-subscriptions' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l identity -r -d 'identity of participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l track -r -d 'track sid to subscribe/unsubscribe' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-subscriptions' -f -l subscribe -d 'set to true to subscribe, otherwise it\'ll unsubscribe' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'join-room' -d 'Joins a room as a participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l identity -r -d 'identity of participant' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l publish-demo -d 'publish demo video as a loop' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l publish -r -d 'files to publish as tracks to room (supports .h264, .ivf, .ogg). can be used multiple times to publish multiple files. can publish from Unix or TCP socket using the format `codec://socket_name` or `codec://host:address` respectively. Valid codecs are h264, vp8, opus' -complete -c livekit-cli -n '__fish_seen_subcommand_from join-room' -f -l fps -r -d 'if video files are published, indicates FPS of video' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'start-room-composite-egress' -d 'Start room composite egress' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l request -r -d 'RoomCompositeEgressRequest as json file (see livekit-cli/examples)' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'start-track-composite-egress' -d 'Start track composite egress' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l request -r -d 'TrackCompositeEgressRequest as json file (see livekit-cli/examples)' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'start-track-egress' -d 'Start track egress' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from start-track-egress' -f -l request -r -d 'TrackEgressRequest as json file (see livekit-cli/examples)' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'list-egress' -d 'List all active egress' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from list-egress' -f -l room -r -d 'limits list to a certain room name' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'update-layout' -d 'Updates layout for a live room composite egress' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l id -r -d 'Egress ID' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-layout' -f -l layout -r -d 'new web layout' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'update-stream' -d 'Adds or removes rtmp output urls from a live stream' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l id -r -d 'Egress ID' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l add-urls -r -d 'urls to add' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-stream' -f -l remove-urls -r -d 'urls to remove' -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'stop-egress' -d 'Stop egress' -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from stop-egress' -f -l id -r -d 'Egress ID' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'test-egress-template' -d 'See what your egress template will look like in a recording' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l base-url -r -d 'base template url' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l layout -r -d 'layout name' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l publishers -r -d 'number of publishers' -complete -c livekit-cli -n '__fish_seen_subcommand_from test-egress-template' -f -l room -r -d 'name of the room' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'create-ingress' -d 'Create an ingress' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from create-ingress' -f -l request -r -d 'CreateIngressRequest as json file (see livekit-cli/examples)' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'update-ingress' -d 'Update an ingress' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from update-ingress' -f -l request -r -d 'UpdateIngressRequest as json file (see livekit-cli/examples)' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'list-ingress' -d 'List all active ingress' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from list-ingress' -f -l room -r -d 'limits list to a certain room name ' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'delete-ingress' -d 'Delete ingress' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from delete-ingress' -f -l id -r -d 'Ingress ID' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'load-test' -d 'Run load tests against LiveKit with simulated publishers & subscribers' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l url -r -d 'url to LiveKit instance' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l project -r -d 'name of a configured project' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l verbose -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l room -r -d 'name of the room (default to random name)' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l duration -r -d 'duration to run, 1m, 1h (by default will run until canceled)' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l video-publishers -s publishers -r -d 'number of participants that would publish video tracks' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l audio-publishers -r -d 'number of participants that would publish audio tracks' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l subscribers -r -d 'number of participants that would subscribe to tracks' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l identity-prefix -r -d 'identity prefix of tester participants (defaults to a random prefix)' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l video-resolution -r -d 'resolution of video to publish. valid values are: high, medium, or low' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l video-codec -r -d 'h264 or vp8, both will be used when unset' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l num-per-second -r -d 'number of testers to start every second' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l layout -r -d 'layout to simulate, choose from speaker, 3x3, 4x4, 5x5' -complete -c livekit-cli -n '__fish_seen_subcommand_from load-test' -f -l no-simulcast -d 'disables simulcast publishing (simulcast is enabled by default)' -complete -c livekit-cli -n '__fish_seen_subcommand_from project' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'project' -d 'subcommand for project management' -complete -c livekit-cli -n '__fish_seen_subcommand_from add' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_seen_subcommand_from project' -a 'add' -d 'add a new project' -complete -c livekit-cli -n '__fish_seen_subcommand_from add' -f -l url -r -d 'URL of the LiveKit server' -complete -c livekit-cli -n '__fish_seen_subcommand_from add' -f -l api-key -r -complete -c livekit-cli -n '__fish_seen_subcommand_from add' -f -l api-secret -r -complete -c livekit-cli -n '__fish_seen_subcommand_from add' -f -l name -r -d 'name given to this project (for later reference).' -complete -c livekit-cli -n '__fish_seen_subcommand_from list' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_seen_subcommand_from project' -a 'list' -d 'list all configured projects' -complete -c livekit-cli -n '__fish_seen_subcommand_from remove' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_seen_subcommand_from project' -a 'remove' -d 'remove an existing project from config' -complete -c livekit-cli -n '__fish_seen_subcommand_from set-default' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_seen_subcommand_from project' -a 'set-default' -d 'set a project as default to use with other commands' -complete -c livekit-cli -n '__fish_seen_subcommand_from help h' -f -l help -s h -d 'show help' -complete -r -c livekit-cli -n '__fish_livekit-cli_no_subcommand' -a 'help h' -d 'Shows a list of commands or help for one command' +complete -c lk -n '__fish_lk_no_subcommand' -f -l verbose +complete -c lk -n '__fish_lk_no_subcommand' -f -l help -s h -d 'show help' +complete -c lk -n '__fish_lk_no_subcommand' -f -l version -s v -d 'print the version' +complete -c lk -n '__fish_lk_no_subcommand' -f -l help -s h -d 'show help' +complete -c lk -n '__fish_lk_no_subcommand' -f -l version -s v -d 'print the version' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'create-token' -d 'creates an access token' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l create -d 'enable token to be used to create rooms' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l list -d 'enable token to be used to list rooms' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l join -d 'enable token to be used to join a room (requires --room and --identity)' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l admin -d 'enable token to be used to manage a room (requires --room)' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l identity -s i -r -d 'unique identity of the participant, used with --join' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l name -s n -r -d 'name of the participant, used with --join. defaults to identity' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l room -s r -r -d 'name of the room to join' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l metadata -r -d 'JSON metadata to encode in the token, will be passed to participant' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l valid-for -r -d 'amount of time that the token is valid for. i.e. "5m", "1h10m" (s: seconds, m: minutes, h: hours)' +complete -c lk -n '__fish_seen_subcommand_from create-token' -f -l grant -r -d 'additional VideoGrant fields. It\'ll be merged with other arguments (JSON formatted)' +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'create-room' +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l name -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l room-egress-file -r -d 'RoomCompositeRequest json file (see cmd/lk/examples/room-composite-file.json)' +complete -c lk -n '__fish_seen_subcommand_from create-room' -f -l track-egress-file -r -d 'AutoTrackEgress json file (see cmd/lk/examples/auto-track-egress.json)' +complete -c lk -n '__fish_seen_subcommand_from list-rooms' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'list-rooms' +complete -c lk -n '__fish_seen_subcommand_from list-rooms' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from list-rooms' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from list-rooms' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from list-rooms' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from list-rooms' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'delete-room' +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from delete-room' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'update-room-metadata' +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from update-room-metadata' -f -l metadata -r +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'list-participants' +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from list-participants' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'get-participant' +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from get-participant' -f -l identity -r -d 'identity of participant' +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'remove-participant' +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from remove-participant' -f -l identity -r -d 'identity of participant' +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'update-participant' +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l identity -r -d 'identity of participant' +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l metadata -r +complete -c lk -n '__fish_seen_subcommand_from update-participant' -f -l permissions -r -d 'JSON describing participant permissions (existing values for unset fields)' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'mute-track' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l identity -r -d 'identity of participant' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l track -r -d 'track sid to mute' +complete -c lk -n '__fish_seen_subcommand_from mute-track' -f -l muted -d 'set to true to mute, false to unmute' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'update-subscriptions' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l identity -r -d 'identity of participant' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l track -r -d 'track sid to subscribe/unsubscribe' +complete -c lk -n '__fish_seen_subcommand_from update-subscriptions' -f -l subscribe -d 'set to true to subscribe, otherwise it\'ll unsubscribe' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'join-room' -d 'Joins a room as a participant' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l identity -r -d 'identity of participant' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l publish-demo -d 'publish demo video as a loop' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l publish -r -d 'files to publish as tracks to room (supports .h264, .ivf, .ogg). can be used multiple times to publish multiple files. can publish from Unix or TCP socket using the format `codec://socket_name` or `codec://host:address` respectively. Valid codecs are h264, vp8, opus' +complete -c lk -n '__fish_seen_subcommand_from join-room' -f -l fps -r -d 'if video files are published, indicates FPS of video' +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'start-room-composite-egress' -d 'Start room composite egress' +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from start-room-composite-egress' -f -l request -r -d 'RoomCompositeEgressRequest as json file (cmd/lk/examples)' +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'start-track-composite-egress' -d 'Start track composite egress' +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from start-track-composite-egress' -f -l request -r -d 'TrackCompositeEgressRequest as json file (see cmd/lk/examples)' +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'start-track-egress' -d 'Start track egress' +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from start-track-egress' -f -l request -r -d 'TrackEgressRequest as json file (see cmd/lk/examples)' +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'list-egress' -d 'List all active egress' +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from list-egress' -f -l room -r -d 'limits list to a certain room name' +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'update-layout' -d 'Updates layout for a live room composite egress' +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l id -r -d 'Egress ID' +complete -c lk -n '__fish_seen_subcommand_from update-layout' -f -l layout -r -d 'new web layout' +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'update-stream' -d 'Adds or removes rtmp output urls from a live stream' +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l id -r -d 'Egress ID' +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l add-urls -r -d 'urls to add' +complete -c lk -n '__fish_seen_subcommand_from update-stream' -f -l remove-urls -r -d 'urls to remove' +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'stop-egress' -d 'Stop egress' +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from stop-egress' -f -l id -r -d 'Egress ID' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'test-egress-template' -d 'See what your egress template will look like in a recording' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l base-url -r -d 'base template url' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l layout -r -d 'layout name' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l publishers -r -d 'number of publishers' +complete -c lk -n '__fish_seen_subcommand_from test-egress-template' -f -l room -r -d 'name of the room' +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'create-ingress' -d 'Create an ingress' +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from create-ingress' -f -l request -r -d 'CreateIngressRequest as json file (see cmd/lk/examples)' +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'update-ingress' -d 'Update an ingress' +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from update-ingress' -f -l request -r -d 'UpdateIngressRequest as json file (see cmd/lk/examples)' +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'list-ingress' -d 'List all active ingress' +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from list-ingress' -f -l room -r -d 'limits list to a certain room name ' +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'delete-ingress' -d 'Delete ingress' +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from delete-ingress' -f -l id -r -d 'Ingress ID' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'load-test' -d 'Run load tests against LiveKit with simulated publishers & subscribers' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l url -r -d 'url to LiveKit instance' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l project -r -d 'name of a configured project' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l verbose +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l room -r -d 'name of the room (default to random name)' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l duration -r -d 'duration to run, 1m, 1h (by default will run until canceled)' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l video-publishers -s publishers -r -d 'number of participants that would publish video tracks' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l audio-publishers -r -d 'number of participants that would publish audio tracks' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l subscribers -r -d 'number of participants that would subscribe to tracks' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l identity-prefix -r -d 'identity prefix of tester participants (defaults to a random prefix)' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l video-resolution -r -d 'resolution of video to publish. valid values are: high, medium, or low' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l video-codec -r -d 'h264 or vp8, both will be used when unset' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l num-per-second -r -d 'number of testers to start every second' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l layout -r -d 'layout to simulate, choose from speaker, 3x3, 4x4, 5x5' +complete -c lk -n '__fish_seen_subcommand_from load-test' -f -l no-simulcast -d 'disables simulcast publishing (simulcast is enabled by default)' +complete -c lk -n '__fish_seen_subcommand_from project' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'project' -d 'subcommand for project management' +complete -c lk -n '__fish_seen_subcommand_from add' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_seen_subcommand_from project' -a 'add' -d 'add a new project' +complete -c lk -n '__fish_seen_subcommand_from add' -f -l url -r -d 'URL of the LiveKit server' +complete -c lk -n '__fish_seen_subcommand_from add' -f -l api-key -r +complete -c lk -n '__fish_seen_subcommand_from add' -f -l api-secret -r +complete -c lk -n '__fish_seen_subcommand_from add' -f -l name -r -d 'name given to this project (for later reference).' +complete -c lk -n '__fish_seen_subcommand_from list' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_seen_subcommand_from project' -a 'list' -d 'list all configured projects' +complete -c lk -n '__fish_seen_subcommand_from remove' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_seen_subcommand_from project' -a 'remove' -d 'remove an existing project from config' +complete -c lk -n '__fish_seen_subcommand_from set-default' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_seen_subcommand_from project' -a 'set-default' -d 'set a project as default to use with other commands' +complete -c lk -n '__fish_seen_subcommand_from help h' -f -l help -s h -d 'show help' +complete -r -c lk -n '__fish_lk_no_subcommand' -a 'help h' -d 'Shows a list of commands or help for one command' diff --git a/autocomplete/zsh_autocomplete b/autocomplete/zsh_autocomplete index 3e32f31f..52897e2a 100644 --- a/autocomplete/zsh_autocomplete +++ b/autocomplete/zsh_autocomplete @@ -18,4 +18,4 @@ _cli_zsh_autocomplete() { fi } -compdef _cli_zsh_autocomplete livekit-cli +compdef _cli_zsh_autocomplete lk diff --git a/cmd/livekit-cli/egress.go b/cmd/livekit-cli/egress.go deleted file mode 100644 index 671619e9..00000000 --- a/cmd/livekit-cli/egress.go +++ /dev/null @@ -1,531 +0,0 @@ -// 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 main - -import ( - "context" - "fmt" - "net/url" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/olekukonko/tablewriter" - "github.com/pkg/browser" - "github.com/urfave/cli/v2" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" - - "github.com/livekit/protocol/egress" - "github.com/livekit/protocol/livekit" - lksdk "github.com/livekit/server-sdk-go/v2" - - "github.com/livekit/livekit-cli/pkg/loadtester" -) - -const egressCategory = "Egress" - -var ( - EgressCommands = []*cli.Command{ - { - Name: "start-room-composite-egress", - Usage: "Start room composite egress", - Before: createEgressClient, - Action: startRoomCompositeEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "request", - Usage: "RoomCompositeEgressRequest as json file (see livekit-cli/examples)", - Required: true, - }, - ), - }, - { - Name: "start-web-egress", - Usage: "Start web egress", - Before: createEgressClient, - Action: startWebEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "request", - Usage: "WebEgressRequest as json file (see livekit-cli/examples)", - Required: true, - }, - ), - }, - { - Name: "start-participant-egress", - Usage: "Start participant egress", - Before: createEgressClient, - Action: startParticipantEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "request", - Usage: "ParticipantEgressRequest as json file (see livekit-cli/examples)", - Required: true, - }, - ), - }, - { - Name: "start-track-composite-egress", - Usage: "Start track composite egress", - Before: createEgressClient, - Action: startTrackCompositeEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "request", - Usage: "TrackCompositeEgressRequest as json file (see livekit-cli/examples)", - Required: true, - }, - ), - }, - { - Name: "start-track-egress", - Usage: "Start track egress", - Before: createEgressClient, - Action: startTrackEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "request", - Usage: "TrackEgressRequest as json file (see livekit-cli/examples)", - Required: true, - }, - ), - }, - { - Name: "list-egress", - Usage: "List all active egress", - Before: createEgressClient, - Action: listEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringSliceFlag{ - Name: "id", - Usage: "list a specific egress id, can be used multiple times", - }, - &cli.StringFlag{ - Name: "room", - Usage: "limits list to a certain room name", - }, - &cli.BoolFlag{ - Name: "active", - Usage: "lists only active egresses", - }, - ), - }, - { - Name: "update-layout", - Usage: "Updates layout for a live room composite egress", - Before: createEgressClient, - Action: updateLayout, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "id", - Usage: "Egress ID", - Required: true, - }, - &cli.StringFlag{ - Name: "layout", - Usage: "new web layout", - Required: true, - }, - ), - }, - { - Name: "update-stream", - Usage: "Adds or removes rtmp output urls from a live stream", - Before: createEgressClient, - Action: updateStream, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "id", - Usage: "Egress ID", - Required: true, - }, - &cli.StringSliceFlag{ - Name: "add-urls", - Usage: "urls to add", - Required: false, - }, - &cli.StringSliceFlag{ - Name: "remove-urls", - Usage: "urls to remove", - Required: false, - }, - ), - }, - { - Name: "stop-egress", - Usage: "Stop egress", - Before: createEgressClient, - Action: stopEgress, - Category: egressCategory, - Flags: withDefaultFlags( - &cli.StringSliceFlag{ - Name: "id", - Usage: "Egress ID to stop, can be specified multiple times", - Required: true, - }, - ), - }, - { - Name: "test-egress-template", - Usage: "See what your egress template will look like in a recording", - Category: egressCategory, - Action: testEgressTemplate, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "base-url (e.g. https://recorder.livekit.io/#)", - Usage: "base template url", - Required: true, - }, - &cli.StringFlag{ - Name: "layout", - Usage: "layout name", - }, - &cli.IntFlag{ - Name: "publishers", - Usage: "number of publishers", - Required: true, - }, - &cli.StringFlag{ - Name: "room", - Usage: "name of the room", - Required: false, - }, - ), - SkipFlagParsing: false, - HideHelp: false, - HideHelpCommand: false, - Hidden: false, - UseShortOptionHandling: false, - HelpName: "", - CustomHelpTemplate: "", - }, - } - - egressClient *lksdk.EgressClient -) - -func createEgressClient(c *cli.Context) error { - pc, err := loadProjectDetails(c) - if err != nil { - return err - } - - egressClient = lksdk.NewEgressClient(pc.URL, pc.APIKey, pc.APISecret, withDefaultClientOpts(pc)...) - return nil -} - -func startRoomCompositeEgress(c *cli.Context) error { - req := &livekit.RoomCompositeEgressRequest{} - if err := unmarshalEgressRequest(c, req); err != nil { - return err - } - - info, err := egressClient.StartRoomCompositeEgress(context.Background(), req) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func startWebEgress(c *cli.Context) error { - req := &livekit.WebEgressRequest{} - if err := unmarshalEgressRequest(c, req); err != nil { - return err - } - - info, err := egressClient.StartWebEgress(context.Background(), req) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func startParticipantEgress(c *cli.Context) error { - req := &livekit.ParticipantEgressRequest{} - if err := unmarshalEgressRequest(c, req); err != nil { - return err - } - - info, err := egressClient.StartParticipantEgress(context.Background(), req) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func startTrackCompositeEgress(c *cli.Context) error { - req := &livekit.TrackCompositeEgressRequest{} - if err := unmarshalEgressRequest(c, req); err != nil { - return err - } - - info, err := egressClient.StartTrackCompositeEgress(context.Background(), req) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func startTrackEgress(c *cli.Context) error { - req := &livekit.TrackEgressRequest{} - if err := unmarshalEgressRequest(c, req); err != nil { - return err - } - - info, err := egressClient.StartTrackEgress(context.Background(), req) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func unmarshalEgressRequest(c *cli.Context, req proto.Message) error { - reqFile := c.String("request") - reqBytes, err := os.ReadFile(reqFile) - if err != nil { - return err - } - if err = protojson.Unmarshal(reqBytes, req); err != nil { - return err - } - - if c.Bool("verbose") { - PrintJSON(req) - } - return nil -} - -func listEgress(c *cli.Context) error { - var items []*livekit.EgressInfo - if c.IsSet("id") { - for _, id := range c.StringSlice("id") { - res, err := egressClient.ListEgress(context.Background(), &livekit.ListEgressRequest{ - EgressId: id, - }) - if err != nil { - return err - } - items = append(items, res.Items...) - } - } else { - res, err := egressClient.ListEgress(context.Background(), &livekit.ListEgressRequest{ - RoomName: c.String("room"), - Active: c.Bool("active"), - }) - if err != nil { - return err - } - items = res.Items - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"EgressID", "Status", "Type", "Source", "Started At", "Error"}) - for _, item := range items { - var startedAt string - if item.StartedAt != 0 { - startedAt = fmt.Sprint(time.Unix(0, item.StartedAt)) - } - var egressType, egressSource string - switch req := item.Request.(type) { - case *livekit.EgressInfo_RoomComposite: - egressType = "room_composite" - egressSource = req.RoomComposite.RoomName - case *livekit.EgressInfo_Web: - egressType = "web" - egressSource = req.Web.Url - case *livekit.EgressInfo_Participant: - egressType = "participant" - egressSource = fmt.Sprintf("%s/%s", req.Participant.RoomName, req.Participant.Identity) - case *livekit.EgressInfo_TrackComposite: - egressType = "track_composite" - trackIDs := make([]string, 0) - if req.TrackComposite.VideoTrackId != "" { - trackIDs = append(trackIDs, req.TrackComposite.VideoTrackId) - } - if req.TrackComposite.AudioTrackId != "" { - trackIDs = append(trackIDs, req.TrackComposite.AudioTrackId) - } - egressSource = fmt.Sprintf("%s/%s", req.TrackComposite.RoomName, strings.Join(trackIDs, ",")) - case *livekit.EgressInfo_Track: - egressType = "track" - egressSource = fmt.Sprintf("%s/%s", req.Track.RoomName, req.Track.TrackId) - } - - table.Append([]string{ - item.EgressId, - item.Status.String(), - egressType, - egressSource, - startedAt, - item.Error, - }) - } - table.Render() - - return nil -} - -func updateLayout(c *cli.Context) error { - info, err := egressClient.UpdateLayout(context.Background(), &livekit.UpdateLayoutRequest{ - EgressId: c.String("id"), - Layout: c.String("layout"), - }) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func updateStream(c *cli.Context) error { - info, err := egressClient.UpdateStream(context.Background(), &livekit.UpdateStreamRequest{ - EgressId: c.String("id"), - AddOutputUrls: c.StringSlice("add-urls"), - RemoveOutputUrls: c.StringSlice("remove-urls"), - }) - if err != nil { - return err - } - - printInfo(info) - return nil -} - -func stopEgress(c *cli.Context) error { - ids := c.StringSlice("id") - var errors []error - for _, id := range ids { - _, err := egressClient.StopEgress(context.Background(), &livekit.StopEgressRequest{ - EgressId: id, - }) - if err != nil { - errors = append(errors, err) - fmt.Println("Error stopping Egress", id, err) - } else { - fmt.Println("Stopping Egress", id) - } - } - if len(errors) != 0 { - return errors[0] - } - return nil -} - -func testEgressTemplate(c *cli.Context) error { - done := make(chan os.Signal, 1) - signal.Notify(done, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - - numPublishers := c.Int("publishers") - rooms := make([]*lksdk.Room, 0, numPublishers) - defer func() { - for _, room := range rooms { - room.Disconnect() - } - }() - - roomName := c.String("room") - if roomName == "" { - roomName = fmt.Sprintf("layout-demo-%v", time.Now().Unix()) - } - - pc, err := loadProjectDetails(c) - if err != nil { - return err - } - - serverURL := pc.URL - apiKey := pc.APIKey - apiSecret := pc.APISecret - - var testers []*loadtester.LoadTester - for i := 0; i < numPublishers; i++ { - lt := loadtester.NewLoadTester(loadtester.TesterParams{ - URL: serverURL, - APIKey: apiKey, - APISecret: apiSecret, - Room: roomName, - IdentityPrefix: "demo-publisher", - Sequence: i, - }) - - err := lt.Start() - if err != nil { - return err - } - - testers = append(testers, lt) - if _, err = lt.PublishSimulcastTrack("demo-video", "high", ""); err != nil { - return err - } - } - - token, err := egress.BuildEgressToken("template_test", apiKey, apiSecret, roomName) - if err != nil { - return err - } - - templateURL := fmt.Sprintf( - "%s/?url=%s&layout=%s&token=%s", - c.String("base-url"), url.QueryEscape(serverURL), c.String("layout"), token, - ) - if err := browser.OpenURL(templateURL); err != nil { - return err - } - - sim := loadtester.NewSpeakerSimulator(loadtester.SpeakerSimulatorParams{ - Testers: testers, - }) - sim.Start() - fmt.Println("simulating speakers...") - - <-done - - sim.Stop() - for _, lt := range testers { - lt.Stop() - } - return nil -} - -func printInfo(info *livekit.EgressInfo) { - if info.Error == "" { - fmt.Printf("EgressID: %v Status: %v\n", info.EgressId, info.Status) - } else { - fmt.Printf("EgressID: %v Error: %v\n", info.EgressId, info.Error) - } -} diff --git a/cmd/livekit-cli/main.go b/cmd/livekit-cli/main.go deleted file mode 100644 index 68f0c6e4..00000000 --- a/cmd/livekit-cli/main.go +++ /dev/null @@ -1,97 +0,0 @@ -// 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 main - -import ( - "fmt" - "os" - - "github.com/urfave/cli/v2" - - livekitcli "github.com/livekit/livekit-cli" - "github.com/livekit/protocol/logger" - lksdk "github.com/livekit/server-sdk-go/v2" -) - -func main() { - app := &cli.App{ - Name: "livekit-cli", - Usage: "CLI client to LiveKit", - Version: livekitcli.Version, - EnableBashCompletion: true, - Suggest: true, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "verbose", - }, - }, - Commands: []*cli.Command{ - { - Name: "generate-fish-completion", - Action: generateFishCompletion, - Hidden: true, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "out", - Aliases: []string{"o"}, - }, - }, - }, - }, - Before: func(c *cli.Context) error { - logConfig := &logger.Config{ - Level: "info", - } - if c.Bool("verbose") { - logConfig.Level = "debug" - } - logger.InitFromConfig(logConfig, "livekit-cli") - lksdk.SetLogger(logger.GetLogger()) - - return nil - }, - } - - app.Commands = append(app.Commands, TokenCommands...) - app.Commands = append(app.Commands, RoomCommands...) - app.Commands = append(app.Commands, JoinCommands...) - app.Commands = append(app.Commands, EgressCommands...) - app.Commands = append(app.Commands, IngressCommands...) - app.Commands = append(app.Commands, LoadTestCommands...) - app.Commands = append(app.Commands, ProjectCommands...) - app.Commands = append(app.Commands, SIPCommands...) - - if err := app.Run(os.Args); err != nil { - fmt.Println(err) - } -} - -func generateFishCompletion(c *cli.Context) error { - fishScript, err := c.App.ToFishCompletion() - if err != nil { - return err - } - - outPath := c.String("out") - if outPath != "" { - if err := os.WriteFile(outPath, []byte(fishScript), 0o644); err != nil { - return err - } - } else { - fmt.Println(fishScript) - } - - return nil -} diff --git a/cmd/livekit-cli/room.go b/cmd/livekit-cli/room.go deleted file mode 100644 index d3d4e249..00000000 --- a/cmd/livekit-cli/room.go +++ /dev/null @@ -1,552 +0,0 @@ -// 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 main - -import ( - "context" - "encoding/json" - "fmt" - "os" - - "github.com/urfave/cli/v2" - "google.golang.org/protobuf/encoding/protojson" - - "github.com/livekit/protocol/livekit" - lksdk "github.com/livekit/server-sdk-go/v2" -) - -const roomCategory = "Room Server API" - -var ( - RoomCommands = []*cli.Command{ - { - Name: "create-room", - Before: createRoomClient, - Action: createRoom, - Category: roomCategory, - Flags: withDefaultFlags( - &cli.StringFlag{ - Name: "name", - Usage: "name of the room", - Required: true, - }, - &cli.StringFlag{ - Name: "room-egress-file", - Usage: "RoomCompositeRequest json file (see examples/room-composite-file.json)", - }, - &cli.StringFlag{ - Name: "participant-egress-file", - Usage: "ParticipantEgress json file (see examples/auto-participant-egress.json)", - }, - &cli.StringFlag{ - Name: "track-egress-file", - Usage: "AutoTrackEgress json file (see examples/auto-track-egress.json)", - }, - &cli.StringFlag{ - Name: "agents-file", - Usage: "Agents configuration json file", - }, - &cli.StringFlag{ - Name: "room-configuration", - Usage: "Name of the room configuration to associate with the created room", - }, - &cli.UintFlag{ - Name: "min-playout-delay", - Usage: "minimum playout delay for video (in ms)", - }, - &cli.UintFlag{ - Name: "max-playout-delay", - Usage: "maximum playout delay for video (in ms)", - }, - &cli.BoolFlag{ - Name: "sync-streams", - Usage: "improve A/V sync by placing them in the same stream. when enabled, transceivers will not be reused", - }, - &cli.UintFlag{ - Name: "empty-timeout", - Usage: "number of seconds to keep the room open before any participant joins", - }, - &cli.UintFlag{ - Name: "departure-timeout", - Usage: "number of seconds to keep the room open after the last participant leaves", - }, - ), - }, - { - Name: "list-rooms", - Before: createRoomClient, - Action: listRooms, - Category: roomCategory, - Flags: withDefaultFlags(), - }, - { - Name: "list-room", - Before: createRoomClient, - Action: listRoom, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - ), - }, - { - Name: "delete-room", - Before: createRoomClient, - Action: deleteRoom, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - ), - }, - { - Name: "update-room-metadata", - Before: createRoomClient, - Action: updateRoomMetadata, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - &cli.StringFlag{ - Name: "metadata", - }, - ), - }, - { - Name: "list-participants", - Before: createRoomClient, - Action: listParticipants, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - ), - }, - { - Name: "get-participant", - Before: createRoomClient, - Action: getParticipant, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - identityFlag, - ), - }, - { - Name: "remove-participant", - Before: createRoomClient, - Action: removeParticipant, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - identityFlag, - ), - }, - { - Name: "update-participant", - Before: createRoomClient, - Action: updateParticipant, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - identityFlag, - &cli.StringFlag{ - Name: "metadata", - }, - &cli.StringFlag{ - Name: "permissions", - Usage: "JSON describing participant permissions (existing values for unset fields)", - }, - ), - }, - { - Name: "mute-track", - Before: createRoomClient, - Action: muteTrack, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - identityFlag, - &cli.StringFlag{ - Name: "track", - Usage: "track sid to mute", - Required: true, - }, - &cli.BoolFlag{ - Name: "muted", - Usage: "set to true to mute, false to unmute", - }, - ), - }, - { - Name: "update-subscriptions", - Before: createRoomClient, - Action: updateSubscriptions, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - identityFlag, - &cli.StringSliceFlag{ - Name: "track", - Usage: "track sid to subscribe/unsubscribe", - Required: true, - }, - &cli.BoolFlag{ - Name: "subscribe", - Usage: "set to true to subscribe, otherwise it'll unsubscribe", - }, - ), - }, - { - Name: "send-data", - Before: createRoomClient, - Action: sendData, - Category: roomCategory, - Flags: withDefaultFlags( - roomFlag, - &cli.StringFlag{ - Name: "data", - Usage: "payload to send to client", - Required: true, - }, - &cli.StringFlag{ - Name: "topic", - Usage: "topic of the message", - }, - &cli.StringSliceFlag{ - Name: "participantID", - Usage: "list of participantID to send the message to", - }, - ), - }, - } - - roomClient *lksdk.RoomServiceClient -) - -func createRoomClient(c *cli.Context) error { - pc, err := loadProjectDetails(c) - if err != nil { - return err - } - - roomClient = lksdk.NewRoomServiceClient(pc.URL, pc.APIKey, pc.APISecret, withDefaultClientOpts(pc)...) - return nil -} - -func createRoom(c *cli.Context) error { - req := &livekit.CreateRoomRequest{ - Name: c.String("name"), - } - - if roomEgressFile := c.String("room-egress-file"); roomEgressFile != "" { - roomEgress := &livekit.RoomCompositeEgressRequest{} - b, err := os.ReadFile(roomEgressFile) - if err != nil { - return err - } - if err = protojson.Unmarshal(b, roomEgress); err != nil { - return err - } - req.Egress = &livekit.RoomEgress{Room: roomEgress} - } - - if participantEgressFile := c.String("participant-egress-file"); participantEgressFile != "" { - participantEgress := &livekit.AutoParticipantEgress{} - b, err := os.ReadFile(participantEgressFile) - if err != nil { - return err - } - if err = protojson.Unmarshal(b, participantEgress); err != nil { - return err - } - if req.Egress == nil { - req.Egress = &livekit.RoomEgress{} - } - req.Egress.Participant = participantEgress - } - - if trackEgressFile := c.String("track-egress-file"); trackEgressFile != "" { - trackEgress := &livekit.AutoTrackEgress{} - b, err := os.ReadFile(trackEgressFile) - if err != nil { - return err - } - if err = protojson.Unmarshal(b, trackEgress); err != nil { - return err - } - if req.Egress == nil { - req.Egress = &livekit.RoomEgress{} - } - req.Egress.Tracks = trackEgress - } - - if agentsFile := c.String("agents-file"); agentsFile != "" { - agent := &livekit.RoomAgent{} - b, err := os.ReadFile(agentsFile) - if err != nil { - return err - } - if err = protojson.Unmarshal(b, agent); err != nil { - return err - } - req.Agent = agent - } - - if roomConfig := c.String("room-configuration"); roomConfig != "" { - req.ConfigName = roomConfig - } - - if c.Uint("min-playout-delay") != 0 { - fmt.Printf("setting min playout delay: %d\n", c.Uint("min-playout-delay")) - req.MinPlayoutDelay = uint32(c.Uint("min-playout-delay")) - } - - if maxPlayoutDelay := c.Uint("max-playout-delay"); maxPlayoutDelay != 0 { - fmt.Printf("setting max playout delay: %d\n", maxPlayoutDelay) - req.MaxPlayoutDelay = uint32(maxPlayoutDelay) - } - - if syncStreams := c.Bool("sync-streams"); syncStreams { - fmt.Printf("setting sync streams: %t\n", syncStreams) - req.SyncStreams = syncStreams - } - - if emptyTimeout := c.Uint("empty-timeout"); emptyTimeout != 0 { - fmt.Printf("setting empty timeout: %d\n", emptyTimeout) - req.EmptyTimeout = uint32(emptyTimeout) - } - - if departureTimeout := c.Uint("departure-timeout"); departureTimeout != 0 { - fmt.Printf("setting departure timeout: %d\n", departureTimeout) - req.DepartureTimeout = uint32(departureTimeout) - } - - room, err := roomClient.CreateRoom(context.Background(), req) - if err != nil { - return err - } - - PrintJSON(room) - return nil -} - -func listRooms(c *cli.Context) error { - res, err := roomClient.ListRooms(context.Background(), &livekit.ListRoomsRequest{}) - if err != nil { - return err - } - if len(res.Rooms) == 0 { - fmt.Println("there are no active rooms") - } - for _, rm := range res.Rooms { - fmt.Printf("%s\t%s\t%d participants\n", rm.Sid, rm.Name, rm.NumParticipants) - } - return nil -} - -func listRoom(c *cli.Context) error { - res, err := roomClient.ListRooms(context.Background(), &livekit.ListRoomsRequest{ - Names: []string{c.String("room")}, - }) - if err != nil { - return err - } - if len(res.Rooms) == 0 { - fmt.Printf("there is no matching room with name: %s\n", c.String("room")) - return nil - } - rm := res.Rooms[0] - PrintJSON(rm) - return nil -} - -func deleteRoom(c *cli.Context) error { - roomId := c.String("room") - _, err := roomClient.DeleteRoom(context.Background(), &livekit.DeleteRoomRequest{ - Room: roomId, - }) - if err != nil { - return err - } - - fmt.Println("deleted room", roomId) - return nil -} - -func updateRoomMetadata(c *cli.Context) error { - roomName := c.String("room") - res, err := roomClient.UpdateRoomMetadata(context.Background(), &livekit.UpdateRoomMetadataRequest{ - Room: roomName, - Metadata: c.String("metadata"), - }) - if err != nil { - return err - } - - fmt.Println("Updated room metadata") - PrintJSON(res) - return nil -} - -func listParticipants(c *cli.Context) error { - roomName := c.String("room") - res, err := roomClient.ListParticipants(context.Background(), &livekit.ListParticipantsRequest{ - Room: roomName, - }) - if err != nil { - return err - } - - for _, p := range res.Participants { - fmt.Printf("%s (%s)\t tracks: %d\n", p.Identity, p.State.String(), len(p.Tracks)) - } - return nil -} - -func getParticipant(c *cli.Context) error { - roomName, identity := participantInfoFromCli(c) - res, err := roomClient.GetParticipant(context.Background(), &livekit.RoomParticipantIdentity{ - Room: roomName, - Identity: identity, - }) - if err != nil { - return err - } - - PrintJSON(res) - - return nil -} - -func updateParticipant(c *cli.Context) error { - roomName, identity := participantInfoFromCli(c) - metadata := c.String("metadata") - permissions := c.String("permissions") - if metadata == "" && permissions == "" { - return fmt.Errorf("either metadata or permissions must be set") - } - - req := &livekit.UpdateParticipantRequest{ - Room: roomName, - Identity: identity, - Metadata: metadata, - } - if permissions != "" { - // load existing participant - participant, err := roomClient.GetParticipant(c.Context, &livekit.RoomParticipantIdentity{ - Room: roomName, - Identity: identity, - }) - if err != nil { - return err - } - - req.Permission = participant.Permission - if req.Permission != nil { - if err = json.Unmarshal([]byte(permissions), req.Permission); err != nil { - return err - } - } - } - - fmt.Println("updating participant...") - PrintJSON(req) - if _, err := roomClient.UpdateParticipant(c.Context, req); err != nil { - return err - } - fmt.Println("participant updated.") - - return nil -} - -func removeParticipant(c *cli.Context) error { - roomName, identity := participantInfoFromCli(c) - _, err := roomClient.RemoveParticipant(context.Background(), &livekit.RoomParticipantIdentity{ - Room: roomName, - Identity: identity, - }) - if err != nil { - return err - } - - fmt.Println("successfully removed participant", identity) - - return nil -} - -func muteTrack(c *cli.Context) error { - roomName, identity := participantInfoFromCli(c) - trackSid := c.String("track") - _, err := roomClient.MutePublishedTrack(context.Background(), &livekit.MuteRoomTrackRequest{ - Room: roomName, - Identity: identity, - TrackSid: trackSid, - Muted: c.Bool("muted"), - }) - if err != nil { - return err - } - - verb := "muted" - if !c.Bool("muted") { - verb = "unmuted" - } - fmt.Println(verb, "track: ", trackSid) - return nil -} - -func updateSubscriptions(c *cli.Context) error { - roomName, identity := participantInfoFromCli(c) - trackSids := c.StringSlice("track") - _, err := roomClient.UpdateSubscriptions(context.Background(), &livekit.UpdateSubscriptionsRequest{ - Room: roomName, - Identity: identity, - TrackSids: trackSids, - Subscribe: c.Bool("subscribe"), - }) - if err != nil { - return err - } - - verb := "subscribed to" - if !c.Bool("subscribe") { - verb = "unsubscribed from" - } - fmt.Println(verb, "tracks: ", trackSids) - return nil -} - -func sendData(c *cli.Context) error { - roomName, _ := participantInfoFromCli(c) - pIDs := c.StringSlice("participantID") - data := c.String("data") - topic := c.String("topic") - req := &livekit.SendDataRequest{ - Room: roomName, - Data: []byte(data), - DestinationSids: pIDs, - } - if topic != "" { - req.Topic = &topic - } - _, err := roomClient.SendData(c.Context, req) - if err != nil { - return err - } - - fmt.Println("successfully sent data to room", roomName) - return nil -} - -func participantInfoFromCli(c *cli.Context) (string, string) { - return c.String("room"), c.String("identity") -} diff --git a/cmd/lk/egress.go b/cmd/lk/egress.go new file mode 100644 index 00000000..bd4b5e76 --- /dev/null +++ b/cmd/lk/egress.go @@ -0,0 +1,783 @@ +// 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 main + +import ( + "context" + "errors" + "fmt" + "net/url" + "os" + "os/signal" + "reflect" + "strings" + "syscall" + "time" + + "github.com/olekukonko/tablewriter" + "github.com/pkg/browser" + "github.com/urfave/cli/v3" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "github.com/livekit/protocol/egress" + "github.com/livekit/protocol/livekit" + lksdk "github.com/livekit/server-sdk-go/v2" + + "github.com/livekit/livekit-cli/pkg/loadtester" +) + +type egressType string + +const ( + EgressTypeRoomComposite egressType = "room-composite" + EgressTypeParticipant egressType = "participant" + EgressTypeTrack egressType = "track" + EgressTypeTrackComposite egressType = "track-composite" + EgressTypeWeb egressType = "web" +) + +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() + ` + +See cmd/livekit-cli/examples` +) + +var ( + EgressCommands = []*cli.Command{ + { + Name: "egress", + Usage: "Record or stream media from LiveKit to elsewhere", + Category: "I/O", + Commands: []*cli.Command{ + { + Name: "start", + Usage: "Start egresses of various types", + Description: egressStartDescription, + Before: createEgressClient, + Action: handleEgressStart, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Usage: "Specify `TYPE` of egress (see above)", + Value: string(EgressTypeRoomComposite), + }, + }, + ArgsUsage: "REQUEST_JSON", + }, + { + Name: "list", + Usage: "List and search active egresses", + Before: createEgressClient, + Action: listEgress, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "id", + Usage: "List a specific egress `ID`, can be used multiple times", + }, + &cli.StringFlag{ + Name: "room", + Usage: "Limits list to a certain room `NAME`", + }, + &cli.BoolFlag{ + Name: "active", + Usage: "Lists only active egresses", + }, + }, + }, + { + Name: "stop", + Usage: "Stop an active egress", + Before: createEgressClient, + Action: stopEgress, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "id", + Usage: "Egress ID to stop, can be specified multiple times", + Required: true, + }, + }, + }, + { + Name: "test-template", + Usage: "See what your egress template will look like in a recording", + Action: testEgressTemplate, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "base-url (e.g. https://recorder.livekit.io/#)", + Usage: "Base template `URL`", + Required: true, + }, + &cli.StringFlag{ + Name: "layout", + Usage: "Layout `TYPE`", + }, + &cli.IntFlag{ + Name: "publishers", + Usage: "`NUMBER` of publishers", + Required: true, + }, + &cli.StringFlag{ + Name: "room", + Usage: "`NAME` of the room", + Required: false, + }, + }, + }, + { + Name: "update-layout", + Usage: "Updates layout for a live room composite egress", + ArgsUsage: "ID", + Before: createEgressClient, + Action: updateLayout, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + Usage: "Egress ID", + Required: true, + }, + &cli.StringFlag{ + Name: "layout", + Usage: "new web layout", + Required: true, + }, + }, + }, + { + Name: "update-stream", + Usage: "Adds or removes RTMP output urls from a live stream", + Before: createEgressClient, + Action: updateStream, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + Usage: "Egress ID", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "add-urls", + Usage: "urls to add", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "remove-urls", + Usage: "urls to remove", + Required: false, + }, + }, + }, + }, + HideHelpCommand: true, + }, + + // Deprecated commands kept for compatibility + { + Hidden: true, // deprectated: use `egress start --room-composite` + Name: "start-room-composite-egress", + Usage: "Start room composite egress", + Before: createEgressClient, + Action: _deprecatedStartRoomCompositeEgress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "request", + Usage: RequestDesc[livekit.RoomCompositeEgressRequest](), + Required: true, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress start --web` + Name: "start-web-egress", + Usage: "Start web egress", + Before: createEgressClient, + Action: _deprecatedStartWebEgress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "request", + Usage: "WebEgressRequest as json file (see cmd/livekit-cli/examples)", + Required: true, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress start --participant` + Name: "start-participant-egress", + Usage: "Start participant egress", + Before: createEgressClient, + Action: _deprecatedStartParticipantEgress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "request", + Usage: "ParticipantEgressRequest as json file (see cmd/livekit-cli/examples)", + Required: true, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress start --track-composite` + Name: "start-track-composite-egress", + Usage: "Start track composite egress", + Before: createEgressClient, + Action: _deprecatedStartTrackCompositeEgress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "request", + Usage: "TrackCompositeEgressRequest as json file (see cmd/livekit-cli/examples)", + Required: true, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress start --track` + Name: "start-track-egress", + Usage: "Start track egress", + Before: createEgressClient, + Action: _deprecatedStartTrackEgress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "request", + Usage: "TrackEgressRequest as json file (see cmd/livekit-cli/examples)", + Required: true, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress list` + Name: "list-egress", + Usage: "List all active egress", + Before: createEgressClient, + Action: listEgress, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "id", + Usage: "list a specific egress id, can be used multiple times", + }, + &cli.StringFlag{ + Name: "room", + Usage: "limits list to a certain room name", + }, + &cli.BoolFlag{ + Name: "active", + Usage: "lists only active egresses", + }, + }, + }, + { + Hidden: true, // deprectated: use `egress update-layout` + Name: "update-layout", + Usage: "Updates layout for a live room composite egress", + Before: createEgressClient, + Action: updateLayout, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + Usage: "Egress ID", + Required: true, + }, + &cli.StringFlag{ + Name: "layout", + Usage: "new web layout", + Required: true, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress update-stream` + Name: "update-stream", + Usage: "Adds or removes rtmp output urls from a live stream", + Before: createEgressClient, + Action: updateStream, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + Usage: "Egress ID", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "add-urls", + Usage: "urls to add", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "remove-urls", + Usage: "urls to remove", + Required: false, + }, + }, + }, + { + Hidden: true, // deprectated: use `egress stop` + Name: "stop-egress", + Usage: "Stop egress", + Before: createEgressClient, + Action: stopEgress, + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "id", + Usage: "Egress ID to stop, can be specified multiple times", + Required: true, + }, + }, + }, + { + Hidden: true, // deprecated: use `egress test-template` + Name: "test-egress-template", + Usage: "See what your egress template will look like in a recording", + Action: testEgressTemplate, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "base-url (e.g. https://recorder.livekit.io/#)", + Usage: "Base template `URL`", + Required: true, + }, + &cli.StringFlag{ + Name: "layout", + Usage: "Layout `TYPE`", + }, + &cli.IntFlag{ + Name: "publishers", + Usage: "`NUMBER` of publishers", + Required: true, + }, + &cli.StringFlag{ + Name: "room", + Usage: "`NAME` of the room", + Required: false, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + HideHelpCommand: false, + UseShortOptionHandling: false, + CustomHelpTemplate: "", + }, + } + + egressClient *lksdk.EgressClient +) + +func createEgressClient(ctx context.Context, c *cli.Command) error { + pc, err := loadProjectDetails(c) + if err != nil { + return err + } + + egressClient = lksdk.NewEgressClient(pc.URL, pc.APIKey, pc.APISecret, withDefaultClientOpts(pc)...) + return nil +} + +func handleEgressStart(ctx context.Context, cmd *cli.Command) error { + switch cmd.String("type") { + case string(EgressTypeRoomComposite): + return startRoomCompositeEgress(ctx, cmd) + case string(EgressTypeWeb): + return startWebEgress(ctx, cmd) + case string(EgressTypeParticipant): + return startParticipantEgress(ctx, cmd) + case string(EgressTypeTrack): + return startTrackEgress(ctx, cmd) + case string(EgressTypeTrackComposite): + return startTrackCompositeEgress(ctx, cmd) + default: + return errors.New("unrecognized egress type " + wrapWith("\"")(cmd.String("type"))) + } +} + +func startRoomCompositeEgress(ctx context.Context, cmd *cli.Command) error { + _ = ctx + req, err := ReadRequestArg[livekit.RoomCompositeEgressRequest](cmd) + if err != nil { + return err + } + + info, err := egressClient.StartRoomCompositeEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func _deprecatedStartRoomCompositeEgress(ctx context.Context, cmd *cli.Command) error { + req := &livekit.RoomCompositeEgressRequest{} + if err := unmarshalEgressRequest(cmd, req); err != nil { + return err + } + + info, err := egressClient.StartRoomCompositeEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func startWebEgress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArg[livekit.WebEgressRequest](cmd) + if err != nil { + return err + } + + info, err := egressClient.StartWebEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func _deprecatedStartWebEgress(ctx context.Context, cmd *cli.Command) error { + req := &livekit.WebEgressRequest{} + if err := unmarshalEgressRequest(cmd, req); err != nil { + return err + } + + info, err := egressClient.StartWebEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func startParticipantEgress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArg[livekit.ParticipantEgressRequest](cmd) + if err != nil { + return err + } + + info, err := egressClient.StartParticipantEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func _deprecatedStartParticipantEgress(ctx context.Context, cmd *cli.Command) error { + req := &livekit.ParticipantEgressRequest{} + if err := unmarshalEgressRequest(cmd, req); err != nil { + return err + } + + info, err := egressClient.StartParticipantEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func startTrackCompositeEgress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArg[livekit.TrackCompositeEgressRequest](cmd) + if err != nil { + return err + } + + info, err := egressClient.StartTrackCompositeEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func _deprecatedStartTrackCompositeEgress(ctx context.Context, cmd *cli.Command) error { + req := &livekit.TrackCompositeEgressRequest{} + if err := unmarshalEgressRequest(cmd, req); err != nil { + return err + } + + info, err := egressClient.StartTrackCompositeEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func startTrackEgress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArg[livekit.TrackEgressRequest](cmd) + if err != nil { + return err + } + + info, err := egressClient.StartTrackEgress(ctx, req) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func _deprecatedStartTrackEgress(ctx context.Context, cmd *cli.Command) error { + req := &livekit.TrackEgressRequest{} + if err := unmarshalEgressRequest(cmd, req); err != nil { + return err + } + + info, err := egressClient.StartTrackEgress(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 { + return err + } + if err = protojson.Unmarshal(reqBytes, req); err != nil { + return err + } + + if cmd.Bool("verbose") { + PrintJSON(req) + } + return nil +} + +func listEgress(ctx context.Context, cmd *cli.Command) error { + var items []*livekit.EgressInfo + if cmd.IsSet("id") { + for _, id := range cmd.StringSlice("id") { + res, err := egressClient.ListEgress(ctx, &livekit.ListEgressRequest{ + EgressId: id, + }) + if err != nil { + return err + } + items = append(items, res.Items...) + } + } else { + res, err := egressClient.ListEgress(ctx, &livekit.ListEgressRequest{ + RoomName: cmd.String("room"), + Active: cmd.Bool("active"), + }) + if err != nil { + return err + } + items = res.Items + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"EgressID", "Status", "Type", "Source", "Started At", "Error"}) + for _, item := range items { + var startedAt string + if item.StartedAt != 0 { + startedAt = fmt.Sprint(time.Unix(0, item.StartedAt)) + } + var egressType, egressSource string + switch req := item.Request.(type) { + case *livekit.EgressInfo_RoomComposite: + egressType = "room_composite" + egressSource = req.RoomComposite.RoomName + case *livekit.EgressInfo_Web: + egressType = "web" + egressSource = req.Web.Url + case *livekit.EgressInfo_Participant: + egressType = "participant" + egressSource = fmt.Sprintf("%s/%s", req.Participant.RoomName, req.Participant.Identity) + case *livekit.EgressInfo_TrackComposite: + egressType = "track_composite" + trackIDs := make([]string, 0) + if req.TrackComposite.VideoTrackId != "" { + trackIDs = append(trackIDs, req.TrackComposite.VideoTrackId) + } + if req.TrackComposite.AudioTrackId != "" { + trackIDs = append(trackIDs, req.TrackComposite.AudioTrackId) + } + egressSource = fmt.Sprintf("%s/%s", req.TrackComposite.RoomName, strings.Join(trackIDs, ",")) + case *livekit.EgressInfo_Track: + egressType = "track" + egressSource = fmt.Sprintf("%s/%s", req.Track.RoomName, req.Track.TrackId) + } + + table.Append([]string{ + item.EgressId, + item.Status.String(), + egressType, + egressSource, + startedAt, + item.Error, + }) + } + table.Render() + + return nil +} + +func updateLayout(ctx context.Context, cmd *cli.Command) error { + egressId := cmd.String("id") + if egressId == "" { + egressId = cmd.Args().First() + } + info, err := egressClient.UpdateLayout(ctx, &livekit.UpdateLayoutRequest{ + EgressId: egressId, + Layout: cmd.String("layout"), + }) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func updateStream(ctx context.Context, cmd *cli.Command) error { + egressId := cmd.String("id") + if egressId == "" { + egressId = cmd.Args().First() + } + info, err := egressClient.UpdateStream(ctx, &livekit.UpdateStreamRequest{ + EgressId: egressId, + AddOutputUrls: cmd.StringSlice("add-urls"), + RemoveOutputUrls: cmd.StringSlice("remove-urls"), + }) + if err != nil { + return err + } + + printInfo(info) + return nil +} + +func stopEgress(ctx context.Context, cmd *cli.Command) error { + ids := cmd.StringSlice("id") + var errors []error + for _, id := range ids { + _, err := egressClient.StopEgress(ctx, &livekit.StopEgressRequest{ + EgressId: id, + }) + if err != nil { + errors = append(errors, err) + fmt.Println("Error stopping Egress", id, err) + } else { + fmt.Println("Stopping Egress", id) + } + } + if len(errors) != 0 { + return errors[0] + } + return nil +} + +func testEgressTemplate(ctx context.Context, cmd *cli.Command) error { + done := make(chan os.Signal, 1) + signal.Notify(done, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + numPublishers := int(cmd.Int("publishers")) + rooms := make([]*lksdk.Room, 0, numPublishers) + defer func() { + for _, room := range rooms { + room.Disconnect() + } + }() + + roomName := cmd.String("room") + if roomName == "" { + roomName = fmt.Sprintf("layout-demo-%v", time.Now().Unix()) + } + + pc, err := loadProjectDetails(cmd) + if err != nil { + return err + } + + serverURL := pc.URL + apiKey := pc.APIKey + apiSecret := pc.APISecret + + var testers []*loadtester.LoadTester + for i := 0; i < numPublishers; i++ { + lt := loadtester.NewLoadTester(loadtester.TesterParams{ + URL: serverURL, + APIKey: apiKey, + APISecret: apiSecret, + Room: roomName, + IdentityPrefix: "demo-publisher", + Sequence: i, + }) + + err := lt.Start() + if err != nil { + return err + } + + testers = append(testers, lt) + if _, err = lt.PublishSimulcastTrack("demo-video", "high", ""); err != nil { + return err + } + } + + token, err := egress.BuildEgressToken("template_test", apiKey, apiSecret, roomName) + if err != nil { + return err + } + + templateURL := fmt.Sprintf( + "%s/?url=%s&layout=%s&token=%s", + cmd.String("base-url"), url.QueryEscape(serverURL), cmd.String("layout"), token, + ) + if err := browser.OpenURL(templateURL); err != nil { + return err + } + + sim := loadtester.NewSpeakerSimulator(loadtester.SpeakerSimulatorParams{ + Testers: testers, + }) + sim.Start() + fmt.Println("simulating speakers...") + + <-done + + sim.Stop() + for _, lt := range testers { + lt.Stop() + } + return nil +} + +func printInfo(info *livekit.EgressInfo) { + if info.Error == "" { + fmt.Printf("EgressID: %v Status: %v\n", info.EgressId, info.Status) + } else { + fmt.Printf("EgressID: %v Error: %v\n", info.EgressId, info.Error) + } +} diff --git a/cmd/livekit-cli/examples/auto-track-egress.json b/cmd/lk/examples/auto-track-egress.json similarity index 100% rename from cmd/livekit-cli/examples/auto-track-egress.json rename to cmd/lk/examples/auto-track-egress.json diff --git a/cmd/livekit-cli/examples/room-composite-file.json b/cmd/lk/examples/room-composite-file.json similarity index 100% rename from cmd/livekit-cli/examples/room-composite-file.json rename to cmd/lk/examples/room-composite-file.json diff --git a/cmd/livekit-cli/examples/room-composite-stream.json b/cmd/lk/examples/room-composite-stream.json similarity index 100% rename from cmd/livekit-cli/examples/room-composite-stream.json rename to cmd/lk/examples/room-composite-stream.json diff --git a/cmd/livekit-cli/examples/rtmp-ingress.json b/cmd/lk/examples/rtmp-ingress.json similarity index 100% rename from cmd/livekit-cli/examples/rtmp-ingress.json rename to cmd/lk/examples/rtmp-ingress.json diff --git a/cmd/livekit-cli/examples/track-composite-file.json b/cmd/lk/examples/track-composite-file.json similarity index 100% rename from cmd/livekit-cli/examples/track-composite-file.json rename to cmd/lk/examples/track-composite-file.json diff --git a/cmd/livekit-cli/examples/track-egress.json b/cmd/lk/examples/track-egress.json similarity index 100% rename from cmd/livekit-cli/examples/track-egress.json rename to cmd/lk/examples/track-egress.json diff --git a/cmd/livekit-cli/examples/web-egress.json b/cmd/lk/examples/web-egress.json similarity index 100% rename from cmd/livekit-cli/examples/web-egress.json rename to cmd/lk/examples/web-egress.json diff --git a/cmd/livekit-cli/ingress.go b/cmd/lk/ingress.go similarity index 54% rename from cmd/livekit-cli/ingress.go rename to cmd/lk/ingress.go index d3324650..b7020366 100644 --- a/cmd/livekit-cli/ingress.go +++ b/cmd/lk/ingress.go @@ -20,8 +20,7 @@ import ( "os" "github.com/olekukonko/tablewriter" - "github.com/urfave/cli/v2" - "google.golang.org/protobuf/encoding/protojson" + "github.com/urfave/cli/v3" "github.com/livekit/protocol/livekit" lksdk "github.com/livekit/server-sdk-go/v2" @@ -32,40 +31,111 @@ const ingressCategory = "Ingress" var ( IngressCommands = []*cli.Command{ { + Name: "ingress", + Usage: "Import outside media sources into a LiveKit room", + Category: "I/O", + Commands: []*cli.Command{ + { + Name: "create", + Usage: "Create an ingress", + UsageText: "lk ingress create [OPTIONS] JSON", + ArgsUsage: "JSON", + Before: createIngressClient, + Action: createIngress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "request", + Usage: "CreateIngressRequest as json file (see cmd/lk/examples)", + TakesFile: true, + }, + }, + }, + { + Name: "update", + Usage: "Update an ingress", + UsageText: "lk ingress update [OPTIONS] JSON", + ArgsUsage: "JSON", + Before: createIngressClient, + Action: updateIngress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "request", + Usage: "UpdateIngressRequest as json file (see cmd/lk/examples)", + TakesFile: true, + }, + }, + }, + { + Name: "list", + Usage: "List all active ingress", + UsageText: "lk ingress list [OPTIONS]", + Before: createIngressClient, + Action: listIngress, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "room", + Usage: "Limits list to a certain room `NAME`", + Required: false, + }, + &cli.StringFlag{ + Name: "id", + Usage: "List a specific ingress `ID`", + Required: false, + }, + }, + }, + { + Name: "delete", + Usage: "Delete an ingress", + UsageText: "lk ingress delete ID", + ArgsUsage: "ID", + Before: createIngressClient, + Action: deleteIngress, + }, + }, + }, + + // Deprecated commands kept for compatibility + { + Hidden: true, // deprecated: use `ingress create` Name: "create-ingress", Usage: "Create an ingress", Before: createIngressClient, Action: createIngress, Category: ingressCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "request", - Usage: "CreateIngressRequest as json file (see livekit-cli/examples)", + Usage: "CreateIngressRequest as json file (see cmd/lk/examples)", Required: true, }, - ), + }, }, { + Hidden: true, // deprecated: use `ingress update` Name: "update-ingress", Usage: "Update an ingress", Before: createIngressClient, Action: updateIngress, Category: ingressCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "request", - Usage: "UpdateIngressRequest as json file (see livekit-cli/examples)", + Usage: "UpdateIngressRequest as json file (see cmd/lk/examples)", Required: true, }, - ), + }, }, { + Hidden: true, // deprecated: use `ingress list` Name: "list-ingress", Usage: "List all active ingress", Before: createIngressClient, Action: listIngress, Category: ingressCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "room", Usage: "limits list to a certain room name ", @@ -76,29 +146,30 @@ var ( Usage: "list a specific ingress id", Required: false, }, - ), + }, }, { + Hidden: true, // deprecated: use `ingress delete` Name: "delete-ingress", Usage: "Delete ingress", Before: createIngressClient, Action: deleteIngress, Category: ingressCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Usage: "Ingress ID", Required: true, }, - ), + }, }, } ingressClient *lksdk.IngressClient ) -func createIngressClient(c *cli.Context) error { - pc, err := loadProjectDetails(c) +func createIngressClient(ctx context.Context, cmd *cli.Command) error { + pc, err := loadProjectDetails(cmd) if err != nil { return err } @@ -107,20 +178,13 @@ func createIngressClient(c *cli.Context) error { return nil } -func createIngress(c *cli.Context) error { - reqFile := c.String("request") - reqBytes, err := os.ReadFile(reqFile) - if err != nil { - return err - } - - req := &livekit.CreateIngressRequest{} - err = protojson.Unmarshal(reqBytes, req) +func createIngress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArgOrFlag[livekit.CreateIngressRequest](cmd) if err != nil { return err } - if c.Bool("verbose") { + if cmd.Bool("verbose") { PrintJSON(req) } @@ -133,20 +197,13 @@ func createIngress(c *cli.Context) error { return nil } -func updateIngress(c *cli.Context) error { - reqFile := c.String("request") - reqBytes, err := os.ReadFile(reqFile) - if err != nil { - return err - } - - req := &livekit.UpdateIngressRequest{} - err = protojson.Unmarshal(reqBytes, req) +func updateIngress(ctx context.Context, cmd *cli.Command) error { + req, err := ReadRequestArgOrFlag[livekit.UpdateIngressRequest](cmd) if err != nil { return err } - if c.Bool("verbose") { + if cmd.Bool("verbose") { PrintJSON(req) } @@ -159,10 +216,10 @@ func updateIngress(c *cli.Context) error { return nil } -func listIngress(c *cli.Context) error { +func listIngress(ctx context.Context, cmd *cli.Command) error { res, err := ingressClient.ListIngress(context.Background(), &livekit.ListIngressRequest{ - RoomName: c.String("room"), - IngressId: c.String("id"), + RoomName: cmd.String("room"), + IngressId: cmd.String("id"), }) if err != nil { return err @@ -193,16 +250,20 @@ func listIngress(c *cli.Context) error { } table.Render() - if c.Bool("verbose") { + if cmd.Bool("verbose") { PrintJSON(res) } return nil } -func deleteIngress(c *cli.Context) error { - info, err := ingressClient.DeleteIngress(context.Background(), &livekit.DeleteIngressRequest{ - IngressId: c.String("id"), +func deleteIngress(ctx context.Context, cmd *cli.Command) error { + id := cmd.String("id") + if id == "" { + id = cmd.Args().First() + } + info, err := ingressClient.DeleteIngress(ctx, &livekit.DeleteIngressRequest{ + IngressId: id, }) if err != nil { return err diff --git a/cmd/livekit-cli/join.go b/cmd/lk/join.go similarity index 91% rename from cmd/livekit-cli/join.go rename to cmd/lk/join.go index fe472db2..cfc761b9 100644 --- a/cmd/livekit-cli/join.go +++ b/cmd/lk/join.go @@ -15,6 +15,7 @@ package main import ( + "context" "fmt" "io" "net" @@ -27,7 +28,7 @@ import ( "github.com/pion/rtcp" "github.com/pion/webrtc/v3" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" provider2 "github.com/livekit/livekit-cli/pkg/provider" "github.com/livekit/protocol/livekit" @@ -38,11 +39,12 @@ import ( var ( JoinCommands = []*cli.Command{ { + Hidden: true, // deprecated: use `lk room join` Name: "join-room", Usage: "Joins a room as a participant", - Action: joinRoom, + Action: _deprecatedJoinRoom, Category: "Simulate", - Flags: withDefaultFlags( + Flags: []cli.Flag{ roomFlag, identityFlag, &cli.BoolFlag{ @@ -50,12 +52,13 @@ var ( Usage: "publish demo video as a loop", }, &cli.StringSliceFlag{ - Name: "publish", - Usage: "files to publish as tracks to room (supports .h264, .ivf, .ogg). " + + Name: "publish", + TakesFile: true, + Usage: "`FILES` to publish as tracks to room (supports .h264, .ivf, .ogg). " + "can be used multiple times to publish multiple files. " + - "can publish from Unix or TCP socket using the format `codec://socket_name` or `codec://host:address` respectively. Valid codecs are h264, vp8, opus", + "can publish from Unix or TCP socket using the format '://' or '://' respectively. Valid codecs are \"h264\", \"vp8\", \"opus\"", }, - &cli.Float64Flag{ + &cli.FloatFlag{ Name: "fps", Usage: "if video files are published, indicates FPS of video", }, @@ -63,15 +66,15 @@ var ( Name: "exit-after-publish", Usage: "when publishing, exit after file or stream is complete", }, - ), + }, }, } ) const mimeDelimiter = "://" -func joinRoom(c *cli.Context) error { - pc, err := loadProjectDetails(c) +func _deprecatedJoinRoom(ctx context.Context, cmd *cli.Command) error { + pc, err := loadProjectDetails(cmd) if err != nil { return err } @@ -144,8 +147,8 @@ func joinRoom(c *cli.Context) error { room, err := lksdk.ConnectToRoom(pc.URL, lksdk.ConnectInfo{ APIKey: pc.APIKey, APISecret: pc.APISecret, - RoomName: c.String("room"), - ParticipantIdentity: c.String("identity"), + RoomName: cmd.String("room"), + ParticipantIdentity: cmd.String("identity"), }, roomCB) if err != nil { return err @@ -156,17 +159,17 @@ func joinRoom(c *cli.Context) error { signal.Notify(done, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - if c.Bool("publish-demo") { + if cmd.Bool("publish-demo") { if err = publishDemo(room); err != nil { return err } } - if c.StringSlice("publish") != nil { - fps := c.Float64("fps") - for _, pub := range c.StringSlice("publish") { + if cmd.StringSlice("publish") != nil { + fps := cmd.Float("fps") + for _, pub := range cmd.StringSlice("publish") { onPublishComplete := func(pub *lksdk.LocalTrackPublication) { - if c.Bool("exit-after-publish") { + if cmd.Bool("exit-after-publish") { close(done) return } diff --git a/cmd/livekit-cli/join_test.go b/cmd/lk/join_test.go similarity index 100% rename from cmd/livekit-cli/join_test.go rename to cmd/lk/join_test.go diff --git a/cmd/livekit-cli/loadtest.go b/cmd/lk/loadtest.go similarity index 55% rename from cmd/livekit-cli/loadtest.go rename to cmd/lk/loadtest.go index 939475c8..bfb74e78 100644 --- a/cmd/livekit-cli/loadtest.go +++ b/cmd/lk/loadtest.go @@ -16,13 +16,10 @@ package main import ( "context" - "os" - "os/signal" - "syscall" "time" "github.com/go-logr/logr" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "github.com/livekit/livekit-cli/pkg/loadtester" "github.com/livekit/protocol/logger" @@ -35,106 +32,98 @@ var LoadTestCommands = []*cli.Command{ Usage: "Run load tests against LiveKit with simulated publishers & subscribers", Category: "Simulate", Action: loadTest, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "room", - Usage: "name of the room (default to random name)", + Usage: "`NAME` of the room (default to random name)", }, &cli.DurationFlag{ Name: "duration", - Usage: "duration to run, 1m, 1h (by default will run until canceled)", + Usage: "`TIME` duration to run, 1m, 1h (by default will run until canceled)", Value: 0, }, &cli.IntFlag{ Name: "video-publishers", Aliases: []string{"publishers"}, - Usage: "number of participants that would publish video tracks", + Usage: "`NUMBER` of participants that would publish video tracks", }, &cli.IntFlag{ Name: "audio-publishers", - Usage: "number of participants that would publish audio tracks", + Usage: "`NUMBER` of participants that would publish audio tracks", }, &cli.IntFlag{ Name: "subscribers", - Usage: "number of participants that would subscribe to tracks", + Usage: "`NUMBER` of participants that would subscribe to tracks", }, &cli.StringFlag{ Name: "identity-prefix", - Usage: "identity prefix of tester participants (defaults to a random prefix)", + Usage: "Identity `PREFIX` of tester participants (defaults to a random prefix)", }, &cli.StringFlag{ Name: "video-resolution", - Usage: "resolution of video to publish. valid values are: high, medium, or low", + Usage: "Resolution `QUALITY` of video to publish (\"high\", \"medium\", or \"low\")", Value: "high", }, &cli.StringFlag{ Name: "video-codec", - Usage: "h264 or vp8, both will be used when unset", + Usage: "`CODEC` \"h264\" or \"vp8\", both will be used when unset", }, - &cli.Float64Flag{ + &cli.FloatFlag{ Name: "num-per-second", - Usage: "number of testers to start every second", + Usage: "`NUMBER` of testers to start every second", Value: 5, }, &cli.StringFlag{ Name: "layout", - Usage: "layout to simulate, choose from speaker, 3x3, 4x4, 5x5", + Usage: "`LAYOUT` to simulate, choose from \"speaker\", \"3x3\", \"4x4\", \"5x5\"", Value: "speaker", }, &cli.BoolFlag{ Name: "no-simulcast", - Usage: "disables simulcast publishing (simulcast is enabled by default)", + Usage: "Disables simulcast publishing (simulcast is enabled by default)", }, &cli.BoolFlag{ Name: "simulate-speakers", - Usage: "fire random speaker events to simulate speaker changes", + Usage: "Fire random speaker events to simulate speaker changes", }, &cli.BoolFlag{ Name: "run-all", - Usage: "runs set list of load test cases", + Usage: "Runs set list of load test cases", Hidden: true, }, - ), + }, }, } -func loadTest(cCtx *cli.Context) error { - pc, err := loadProjectDetails(cCtx) +func loadTest(ctx context.Context, cmd *cli.Command) error { + pc, err := loadProjectDetails(cmd) if err != nil { return err } - if !cCtx.Bool("verbose") { + if !cmd.Bool("verbose") { lksdk.SetLogger(logger.LogRLogger(logr.Discard())) } _ = raiseULimit() - ctx, cancel := context.WithCancel(cCtx.Context) - done := make(chan os.Signal, 1) - signal.Notify(done, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - go func() { - <-done - cancel() - }() - params := loadtester.Params{ - VideoResolution: cCtx.String("video-resolution"), - VideoCodec: cCtx.String("video-codec"), - Duration: cCtx.Duration("duration"), - NumPerSecond: cCtx.Float64("num-per-second"), - Simulcast: !cCtx.Bool("no-simulcast"), - SimulateSpeakers: cCtx.Bool("simulate-speakers"), + VideoResolution: cmd.String("video-resolution"), + VideoCodec: cmd.String("video-codec"), + Duration: cmd.Duration("duration"), + NumPerSecond: cmd.Float("num-per-second"), + Simulcast: !cmd.Bool("no-simulcast"), + SimulateSpeakers: cmd.Bool("simulate-speakers"), TesterParams: loadtester.TesterParams{ URL: pc.URL, APIKey: pc.APIKey, APISecret: pc.APISecret, - Room: cCtx.String("room"), - IdentityPrefix: cCtx.String("identity-prefix"), - Layout: loadtester.LayoutFromString(cCtx.String("layout")), + Room: cmd.String("room"), + IdentityPrefix: cmd.String("identity-prefix"), + Layout: loadtester.LayoutFromString(cmd.String("layout")), }, } - if cCtx.Bool("run-all") { + if cmd.Bool("run-all") { // leave out room name and pub/sub counts if params.Duration == 0 { params.Duration = time.Second * 15 @@ -143,9 +132,9 @@ func loadTest(cCtx *cli.Context) error { return test.RunSuite(ctx) } - params.VideoPublishers = cCtx.Int("video-publishers") - params.AudioPublishers = cCtx.Int("audio-publishers") - params.Subscribers = cCtx.Int("subscribers") + params.VideoPublishers = int(cmd.Int("video-publishers")) + params.AudioPublishers = int(cmd.Int("audio-publishers")) + params.Subscribers = int(cmd.Int("subscribers")) test := loadtester.NewLoadTest(params) return test.Run(ctx) diff --git a/cmd/lk/main.go b/cmd/lk/main.go new file mode 100644 index 00000000..b81bbac8 --- /dev/null +++ b/cmd/lk/main.go @@ -0,0 +1,130 @@ +// 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 main + +import ( + "context" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/urfave/cli/v3" + + livekitcli "github.com/livekit/livekit-cli" + "github.com/livekit/protocol/logger" + lksdk "github.com/livekit/server-sdk-go/v2" +) + +func main() { + app := &cli.Command{ + Name: "lk", + Usage: "CLI client to LiveKit", + Description: "A suite of command line utilities allowing you to access LiveKit APIs services, interact with rooms in realtime, and perform load testing simulations.", + Version: livekitcli.Version, + EnableShellCompletion: true, + Suggest: true, + HideHelpCommand: true, + UseShortOptionHandling: true, + Flags: persistentFlags, + Commands: []*cli.Command{ + { + Name: "generate-fish-completion", + Action: generateFishCompletion, + Hidden: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "out", + Aliases: []string{"o"}, + }, + }, + }, + }, + Before: func(ctx context.Context, cmd *cli.Command) error { + logConfig := &logger.Config{ + Level: "info", + } + if cmd.Bool("verbose") { + logConfig.Level = "debug" + } + logger.InitFromConfig(logConfig, "lk") + lksdk.SetLogger(logger.GetLogger()) + + return nil + }, + } + + app.Commands = append(app.Commands, TokenCommands...) + app.Commands = append(app.Commands, RoomCommands...) + app.Commands = append(app.Commands, JoinCommands...) + app.Commands = append(app.Commands, EgressCommands...) + app.Commands = append(app.Commands, IngressCommands...) + app.Commands = append(app.Commands, LoadTestCommands...) + app.Commands = append(app.Commands, ProjectCommands...) + app.Commands = append(app.Commands, SIPCommands...) + + // Register cleanup hook for SIGINT, SIGTERM, SIGQUIT + ctx, stop := signal.NotifyContext( + context.Background(), + syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, + ) + defer stop() + + // Cleanup on hooked signals, remembering to flush stdout + // before exit to prevent line rag in case of SIGINT + go func() { + <-ctx.Done() + stop() + fmt.Println() + }() + + checkForLegacyName() + + if err := app.Run(ctx, os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + +func checkForLegacyName() { + if !strings.HasSuffix(os.Args[0], "lk") { + fmt.Fprintf( + os.Stderr, + "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DEPRECATION NOTICE ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"+ + "The `livekit-cli` binary has been renamed to `lk`, and some of the options and\n"+ + "commands have changed. Though legacy commands my continue to work, they have\n"+ + "been hidden from the USAGE notes and may be removed in future releases."+ + "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n", + ) + } +} + +func generateFishCompletion(ctx context.Context, cmd *cli.Command) error { + fishScript, err := cmd.ToFishCompletion() + if err != nil { + return err + } + + outPath := cmd.String("out") + if outPath != "" { + if err := os.WriteFile(outPath, []byte(fishScript), 0o644); err != nil { + return err + } + } else { + fmt.Println(fishScript) + } + + return nil +} diff --git a/cmd/livekit-cli/project.go b/cmd/lk/project.go similarity index 75% rename from cmd/livekit-cli/project.go rename to cmd/lk/project.go index 46c4c4d2..f58b0d32 100644 --- a/cmd/livekit-cli/project.go +++ b/cmd/lk/project.go @@ -15,6 +15,7 @@ package main import ( + "context" "errors" "fmt" "net/url" @@ -24,7 +25,7 @@ import ( "github.com/manifoldco/promptui" "github.com/olekukonko/tablewriter" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "github.com/livekit/livekit-cli/pkg/config" ) @@ -33,46 +34,49 @@ var ( ProjectCommands = []*cli.Command{ { Name: "project", - Usage: "subcommand for project management", - Category: "Project Management", + Usage: "Add or remove projects and view existing project properties", + Category: "Core", Before: loadProjectConfig, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { - Name: "add", - Usage: "add a new project", - Action: addProject, + Name: "add", + Usage: "Add a new project", + UsageText: "lk project add PROJECT_NAME", + ArgsUsage: "PROJECT_NAME", + Action: addProject, Flags: []cli.Flag{ &cli.StringFlag{ Name: "url", - Usage: "URL of the LiveKit server", + Usage: "`URL` of the LiveKit server", }, &cli.StringFlag{ - Name: "api-key", + Name: "api-key", + Usage: "Project `KEY`", }, &cli.StringFlag{ - Name: "api-secret", - }, - &cli.StringFlag{ - Name: "name", - Usage: "name given to this project (for later reference).", + Name: "api-secret", + Usage: "Project `SECRET`", }, }, }, { - Name: "list", - Usage: "list all configured projects", - Action: listProjects, + Name: "list", + Usage: "List all configured projects", + UsageText: "lk project list", + Action: listProjects, }, { Name: "remove", - Usage: "remove an existing project from config", - UsageText: "livekit-cli project remove ", + Usage: "Remove an existing project from config", + UsageText: "lk project remove PROJECT_NAME", + ArgsUsage: "PROJECT_NAME", Action: removeProject, }, { Name: "set-default", - Usage: "set a project as default to use with other commands", - UsageText: "livekit-cli project set-default ", + Usage: "Set a project as default to use with other commands", + UsageText: "lk project set-default PROJECT_NAME", + ArgsUsage: "PROJECT_NAME", Action: setDefaultProject, }, }, @@ -84,7 +88,7 @@ var ( nameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`) ) -func loadProjectConfig(c *cli.Context) error { +func loadProjectConfig(ctx context.Context, cmd *cli.Command) error { conf, err := config.LoadOrCreate() if err != nil { return err @@ -102,7 +106,7 @@ func loadProjectConfig(c *cli.Context) error { return nil } -func addProject(c *cli.Context) error { +func addProject(ctx context.Context, cmd *cli.Command) error { p := config.ProjectConfig{} var prompt promptui.Prompt @@ -115,7 +119,7 @@ func addProject(c *cli.Context) error { _, err := url.Parse(val) return err } - if p.URL = c.String("url"); p.URL != "" { + if p.URL = cmd.String("url"); p.URL != "" { if err = validateURL(p.URL); err != nil { return err } @@ -137,7 +141,7 @@ func addProject(c *cli.Context) error { } return nil } - if p.APIKey = c.String("api-key"); p.APIKey != "" { + if p.APIKey = cmd.String("api-key"); p.APIKey != "" { if err = validateKey(p.APIKey); err != nil { return err } @@ -153,7 +157,7 @@ func addProject(c *cli.Context) error { } // API Secret - if p.APISecret = c.String("api-secret"); p.APISecret != "" { + if p.APISecret = cmd.String("api-secret"); p.APISecret != "" { if err = validateKey(p.APISecret); err != nil { return err } @@ -181,7 +185,8 @@ func addProject(c *cli.Context) error { } return nil } - if p.Name = c.String("name"); p.Name != "" { + + if p.Name = cmd.Args().Get(0); p.Name != "" { if err = validateName(p.Name); err != nil { return err } @@ -222,9 +227,9 @@ func addProject(c *cli.Context) error { return nil } -func listProjects(c *cli.Context) error { +func listProjects(ctx context.Context, cmd *cli.Command) error { if len(cliConfig.Projects) == 0 { - fmt.Println("No projects configured, use `livekit-cli project add` to add a new project.") + fmt.Println("No projects configured, use `lk project add` to add a new project.") return nil } @@ -238,12 +243,12 @@ func listProjects(c *cli.Context) error { return nil } -func removeProject(c *cli.Context) error { - if c.NArg() == 0 { - _ = cli.ShowSubcommandHelp(c) +func removeProject(ctx context.Context, cmd *cli.Command) error { + if cmd.NArg() == 0 { + _ = cli.ShowSubcommandHelp(cmd) return errors.New("project name is required") } - name := c.Args().First() + name := cmd.Args().First() var newProjects []config.ProjectConfig for _, p := range cliConfig.Projects { @@ -267,12 +272,12 @@ func removeProject(c *cli.Context) error { return nil } -func setDefaultProject(c *cli.Context) error { - if c.NArg() == 0 { - _ = cli.ShowSubcommandHelp(c) +func setDefaultProject(ctx context.Context, cmd *cli.Command) error { + if cmd.NArg() == 0 { + _ = cli.ShowSubcommandHelp(cmd) return errors.New("project name is required") } - name := c.Args().First() + name := cmd.Args().First() for _, p := range cliConfig.Projects { if p.Name == name { diff --git a/cmd/livekit-cli/proto.go b/cmd/lk/proto.go similarity index 62% rename from cmd/livekit-cli/proto.go rename to cmd/lk/proto.go index 06768092..a62a5518 100644 --- a/cmd/livekit-cli/proto.go +++ b/cmd/lk/proto.go @@ -21,7 +21,7 @@ import ( "reflect" "github.com/olekukonko/tablewriter" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -33,12 +33,36 @@ type protoType[T any] interface { proto.Message } -func ReadRequest[T any, P protoType[T]](c *cli.Context) (*T, error) { - return ReadRequestFile[T, P](c.String(flagRequest)) +func ReadRequest[T any, P protoType[T]](cmd *cli.Command) (*T, error) { + return ReadRequestFileOrLiteral[T, P](cmd.String(flagRequest)) } -func ReadRequestFile[T any, P protoType[T]](path string) (*T, error) { - reqBytes, err := os.ReadFile(path) +func ReadRequestArg[T any, P protoType[T]](cmd *cli.Command) (*T, error) { + reqFile, err := extractArg(cmd) + if err != nil { + return nil, err + } + return ReadRequestFileOrLiteral[T, P](reqFile) +} + +func ReadRequestArgOrFlag[T any, P protoType[T]](cmd *cli.Command) (*T, error) { + reqFile, err := extractArg(cmd) + if err != nil { + return ReadRequest[T, P](cmd) + } + return ReadRequestFileOrLiteral[T, P](reqFile) +} + +func ReadRequestFileOrLiteral[T any, P protoType[T]](pathOrLiteral string) (*T, error) { + var reqBytes []byte + var err error + + // This allows us to read JSON from either CLI arg or FS + if _, err = os.Stat(pathOrLiteral); err != nil { + reqBytes, err = os.ReadFile(pathOrLiteral) + } else { + reqBytes = []byte(pathOrLiteral) + } if err != nil { return nil, err } @@ -61,22 +85,23 @@ func RequestFlag[T any, P protoType[T]]() *cli.StringFlag { func RequestDesc[T any, _ protoType[T]]() string { typ := reflect.TypeFor[T]().Name() - return typ + " as JSON file (see livekit-cli/examples)" + return typ + " as JSON file (see cmd/lk/examples)" } func createAndPrint[T any, P protoType[T], R any]( - c *cli.Context, file string, + ctx context.Context, + cmd *cli.Command, file string, create func(ctx context.Context, p P) (R, error), print func(r R), ) error { - req, err := ReadRequestFile[T, P](file) + req, err := ReadRequestFileOrLiteral[T, P](file) if err != nil { return err } - if c.Bool("verbose") { + if cmd.Bool("verbose") { PrintJSON(req) } - info, err := create(c.Context, req) + info, err := create(ctx, req) if err != nil { return err } @@ -85,18 +110,19 @@ func createAndPrint[T any, P protoType[T], R any]( } func createAndPrintLegacy[T any, P protoType[T], R any]( - c *cli.Context, + ctx context.Context, + cmd *cli.Command, create func(ctx context.Context, p P) (R, error), print func(r R), ) error { - req, err := ReadRequest[T, P](c) + req, err := ReadRequest[T, P](cmd) if err != nil { return err } - if c.Bool("verbose") { + if cmd.Bool("verbose") { PrintJSON(req) } - info, err := create(c.Context, req) + info, err := create(ctx, req) if err != nil { return err } @@ -105,29 +131,30 @@ func createAndPrintLegacy[T any, P protoType[T], R any]( } func createAndPrintReqs[T any, P protoType[T], R any]( - c *cli.Context, + ctx context.Context, + cmd *cli.Command, create func(ctx context.Context, p P) (R, error), print func(r R), ) error { - args := c.Args() + args := cmd.Args() if !args.Present() { return errors.New("at least one JSON request file is required") } for _, file := range args.Slice() { - if err := createAndPrint(c, file, create, print); err != nil { + if err := createAndPrint(ctx, cmd, file, create, print); err != nil { return err } } return nil } -func forEachID(c *cli.Context, fnc func(ctx context.Context, id string) error) error { - args := c.Args() +func forEachID(ctx context.Context, cmd *cli.Command, fnc func(ctx context.Context, id string) error) error { + args := cmd.Args() if !args.Present() { return errors.New("at least one ID is required") } for _, id := range args.Slice() { - if err := fnc(c.Context, id); err != nil { + if err := fnc(ctx, id); err != nil { return err } } @@ -142,11 +169,12 @@ func listAndPrint[ GetItems() []*T }, ]( - c *cli.Context, + ctx context.Context, + cmd *cli.Command, getList func(ctx context.Context, req Req) (Resp, error), req Req, header []string, tableRow func(item *T) []string, ) error { - res, err := getList(c.Context, req) + res, err := getList(ctx, req) if err != nil { return err } @@ -164,7 +192,7 @@ func listAndPrint[ table.Append(row) } table.Render() - if c.Bool("verbose") { + if cmd.Bool("verbose") { PrintJSON(res) } return nil diff --git a/cmd/lk/room.go b/cmd/lk/room.go new file mode 100644 index 00000000..7dcc1bc2 --- /dev/null +++ b/cmd/lk/room.go @@ -0,0 +1,1015 @@ +// 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 main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/livekit/protocol/logger" + "github.com/pion/webrtc/v3" + "github.com/urfave/cli/v3" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/livekit/protocol/livekit" + lksdk "github.com/livekit/server-sdk-go/v2" +) + +var ( + RoomCommands = []*cli.Command{ + { + Name: "room", + Usage: "Create or delete rooms and manage existing room properties", + Category: "Core", + HideHelpCommand: true, + Commands: []*cli.Command{ + { + Name: "create", + Usage: "Create a room", + ArgsUsage: "ROOM_NAME", + Before: createRoomClient, + Action: createRoom, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "room-egress-file", + Usage: "RoomCompositeRequest `JSON` file (see examples/room-composite-file.json)", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "participant-egress-file", + Usage: "ParticipantEgress `JSON` file (see examples/auto-participant-egress.json)", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "track-egress-file", + Usage: "AutoTrackEgress `JSON` file (see examples/auto-track-egress.json)", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "agents-file", + Usage: "Agents configuration `JSON` file", + TakesFile: true, + }, + &cli.StringFlag{ + Name: "room-configuration", + Usage: "`NAME` of the room configuration to associate with the created room", + }, + &cli.UintFlag{ + Name: "min-playout-delay", + Usage: "Minimum playout delay for video (in `MS`)", + }, + &cli.UintFlag{ + Name: "max-playout-delay", + Usage: "Maximum playout delay for video (in `MS`)", + }, + &cli.BoolFlag{ + Name: "sync-streams", + Usage: "Improve A/V sync by placing them in the same stream. when enabled, transceivers will not be reused", + }, + &cli.UintFlag{ + Name: "empty-timeout", + Usage: "Number of `SECS` to keep the room open before any participant joins", + }, + &cli.UintFlag{ + Name: "departure-timeout", + Usage: "Number of `SECS` to keep the room open after the last participant leaves", + }, + }, + }, + { + Name: "list", + Usage: "List or search for active rooms by name", + Before: createRoomClient, + Action: listRooms, + ArgsUsage: "[ROOM_NAME ...]", + }, + { + Name: "update", + Usage: "Modify properties of an active room", + Before: createRoomClient, + Action: updateRoomMetadata, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "metadata", + Required: true, + }, + }, + ArgsUsage: "ROOM_NAME", + }, + { + Name: "delete", + Usage: "Delete a room", + UsageText: "lk room delete [OPTIONS] ROOM_NAME", + Before: createRoomClient, + Action: deleteRoom, + ArgsUsage: "ROOM_NAME_OR_ID", + }, + { + Name: "join", + Usage: "Joins a room as a participant", + UsageText: "lk room join [OPTIONS] ROOM_NAME", + Action: joinRoom, + ArgsUsage: "ROOM_NAME", + Flags: []cli.Flag{ + identityFlag, + &cli.BoolFlag{ + Name: "publish-demo", + Usage: "Publish demo video as a loop", + }, + &cli.StringSliceFlag{ + Name: "publish", + TakesFile: true, + Usage: "`FILES` to publish as tracks to room (supports .h264, .ivf, .ogg). " + + "Can be used multiple times to publish multiple files. " + + "Can publish from Unix or TCP socket using the format '://' or '://' respectively. Valid codecs are \"h264\", \"vp8\", \"opus\"", + }, + &cli.FloatFlag{ + Name: "fps", + Usage: "If video files are published, indicates `FPS` of video", + }, + &cli.BoolFlag{ + Name: "exit-after-publish", + Usage: "When publishing, exit after file or stream is complete", + }, + }, + }, + { + Name: "participants", + Usage: "Manage room participants", + Before: createRoomClient, + Commands: []*cli.Command{ + { + Name: "list", + Usage: "List or search for active rooms by name", + Action: listParticipants, + ArgsUsage: "ROOM_NAME", + }, + { + Name: "get", + Usage: "Fetch metadata of a room participant", + ArgsUsage: "ID", + Before: createRoomClient, + Action: getParticipant, + Flags: []cli.Flag{ + roomFlag, + }, + }, + { + Name: "remove", + Usage: "Remove a participant from a room", + ArgsUsage: "ID", + Before: createRoomClient, + Action: removeParticipant, + Flags: []cli.Flag{ + roomFlag, + }, + }, + { + Name: "update", + Usage: "Change the metadata and permissions for a room participant", + ArgsUsage: "ID", + Before: createRoomClient, + Action: updateParticipant, + Flags: []cli.Flag{ + roomFlag, + &cli.StringFlag{ + Name: "metadata", + Usage: "JSON describing participant metadata (existing values for unset fields)", + }, + &cli.StringFlag{ + Name: "permissions", + Usage: "JSON describing participant permissions (existing values for unset fields)", + }, + }, + }, + }, + }, + { + Name: "mute-track", + Usage: "Mute or unmute a track", + UsageText: "lk room mute-track OPTIONS TRACK_SID", + ArgsUsage: "TRACK_SID", + Before: createRoomClient, + Action: muteTrack, + MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{ + Flags: [][]cli.Flag{ + { + &cli.BoolFlag{ + Name: "m", + Aliases: []string{"mute", "muted"}, + Usage: "Mute the track", + }, + &cli.BoolFlag{ + Name: "u", + Aliases: []string{"unmute"}, + Usage: "Unmute the track", + }, + }, + }, + }}, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + &cli.StringFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "track", + Usage: "Track `SID` to mute", + }, + }, + }, + { + Name: "update-subscriptions", + Usage: "Subscribe or unsubscribe from a track", + UsageText: "lk room update-subscriptions OPTIONS TRACK_SID", + ArgsUsage: "TRACK_SID", + Before: createRoomClient, + Action: updateSubscriptions, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + &cli.StringSliceFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "track", + Usage: "Track `SID` to subscribe/unsubscribe", + }, + &cli.BoolFlag{ + Name: "subscribe", + Usage: "Set to true to subscribe, otherwise it'll unsubscribe", + }, + }, + }, + { + Name: "send-data", + Before: createRoomClient, + Action: sendData, + Usage: "Send arbitrary JSON data to client", + UsageText: "lk room send-data [OPTIONS] DATA", + ArgsUsage: "JSON", + Flags: []cli.Flag{ + roomFlag, + &cli.StringFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "data", + Usage: "`JSON` payload to send to client", + }, + &cli.StringFlag{ + Name: "topic", + Usage: "`TOPIC` of the message", + }, + &cli.StringSliceFlag{ + Hidden: true, // deprecated: use `--participant-ids` + Name: "participantID", + Usage: "list of participantID to send the message to", + }, + &cli.StringSliceFlag{ + Name: "participant-ids", + Usage: "List of participant `ID`s to send the message to", + }, + }, + }, + }, + }, + + // Deprecated commands kept for compatibility + { + Hidden: true, // deprecated: use `room create` + Name: "create-room", + Before: createRoomClient, + Action: createRoom, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "name of the room", + Required: true, + }, + &cli.StringFlag{ + Name: "room-egress-file", + Usage: "RoomCompositeRequest json file (see examples/room-composite-file.json)", + }, + &cli.StringFlag{ + Name: "participant-egress-file", + Usage: "ParticipantEgress json file (see examples/auto-participant-egress.json)", + }, + &cli.StringFlag{ + Name: "track-egress-file", + Usage: "AutoTrackEgress json file (see examples/auto-track-egress.json)", + }, + &cli.StringFlag{ + Name: "agents-file", + Usage: "Agents configuration json file", + }, + &cli.StringFlag{ + Name: "room-configuration", + Usage: "Name of the room configuration to associate with the created room", + }, + &cli.UintFlag{ + Name: "min-playout-delay", + Usage: "minimum playout delay for video (in ms)", + }, + &cli.UintFlag{ + Name: "max-playout-delay", + Usage: "maximum playout delay for video (in ms)", + }, + &cli.BoolFlag{ + Name: "sync-streams", + Usage: "improve A/V sync by placing them in the same stream. when enabled, transceivers will not be reused", + }, + &cli.UintFlag{ + Name: "empty-timeout", + Usage: "number of seconds to keep the room open before any participant joins", + }, + &cli.UintFlag{ + Name: "departure-timeout", + Usage: "number of seconds to keep the room open after the last participant leaves", + }, + }, + }, + { + Hidden: true, // deprecated: use `room list`` + Name: "list-rooms", + Before: createRoomClient, + Action: listRooms, + }, + { + Hidden: true, // deprecated: use `room list` + Name: "list-room", + Before: createRoomClient, + Action: _deprecatedListRoom, + Flags: []cli.Flag{ + roomFlag, + }, + }, + { + Hidden: true, // deprecated: use `room update-metadata` + Name: "update-room-metadata", + Before: createRoomClient, + Action: _deprecatedUpdateRoomMetadata, + Flags: []cli.Flag{ + roomFlag, + &cli.StringFlag{ + Name: "metadata", + }, + }, + }, + { + Hidden: true, // deprecated: use `room participants list` + Name: "list-participants", + Before: createRoomClient, + Action: _deprecatedListParticipants, + Flags: []cli.Flag{ + roomFlag, + }, + }, + { + Hidden: true, // deprecated: use `room participants get` + Name: "get-participant", + Before: createRoomClient, + Action: getParticipant, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + }, + }, + { + Hidden: true, // deprecated: use `room participants remove` + Name: "remove-participant", + Before: createRoomClient, + Action: removeParticipant, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + }, + }, + { + Hidden: true, // deprecated: use `room participants update` + Name: "update-participant", + Before: createRoomClient, + Action: updateParticipant, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + &cli.StringFlag{ + Name: "metadata", + Usage: "`JSON` describing participant metadata", + }, + &cli.StringFlag{ + Name: "permissions", + Usage: "`JSON` describing participant permissions (existing values for unset fields)", + }, + }, + }, + { + Hidden: true, // deprecated: use `room mute-track` + Name: "mute-track", + Usage: "Mute or unmute a track", + UsageText: "lk room mute-track OPTIONS TRACK_SID", + ArgsUsage: "TRACK_SID", + Before: createRoomClient, + Action: muteTrack, + MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{ + Flags: [][]cli.Flag{ + { + &cli.BoolFlag{ + Name: "m", + Aliases: []string{"mute", "muted"}, + Usage: "Mute the track", + }, + &cli.BoolFlag{ + Name: "u", + Aliases: []string{"unmute"}, + Usage: "Unmute the track", + }, + }, + }, + }}, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + &cli.StringFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "track", + Usage: "Track `SID` to mute", + }, + }, + }, + { + Hidden: true, // deprecated: use `room update-subscriptions` + Name: "update-subscriptions", + Usage: "Subscribe or unsubscribe from a track", + UsageText: "lk room update-subscriptions OPTIONS TRACK_SID", + ArgsUsage: "TRACK_SID", + Before: createRoomClient, + Action: updateSubscriptions, + Flags: []cli.Flag{ + roomFlag, + identityFlag, + &cli.StringSliceFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "track", + Usage: "Track `SID` to subscribe/unsubscribe", + }, + &cli.BoolFlag{ + Name: "subscribe", + Usage: "Set to true to subscribe, otherwise it'll unsubscribe", + }, + }, + }, + { + Hidden: true, // deprecated: use `room send-data` + Name: "send-data", + Before: createRoomClient, + Action: sendData, + Usage: "Send arbitrary JSON data to client", + UsageText: "lk room send-data [OPTIONS] DATA", + ArgsUsage: "JSON", + Flags: []cli.Flag{ + roomFlag, + &cli.StringFlag{ + Hidden: true, // deprecated: use ARG0 + Name: "data", + Usage: "`JSON` payload to send to client", + }, + &cli.StringFlag{ + Name: "topic", + Usage: "`TOPIC` of the message", + }, + &cli.StringSliceFlag{ + Hidden: true, // deprecated: use `--participant-ids` + Name: "participantID", + Usage: "list of participantID to send the message to", + }, + &cli.StringSliceFlag{ + Name: "participant-ids", + Usage: "List of participant `ID`s to send the message to", + }, + }, + }, + } + + roomClient *lksdk.RoomServiceClient +) + +func createRoomClient(ctx context.Context, cmd *cli.Command) error { + pc, err := loadProjectDetails(cmd) + if err != nil { + return err + } + + roomClient = lksdk.NewRoomServiceClient(pc.URL, pc.APIKey, pc.APISecret, withDefaultClientOpts(pc)...) + return nil +} + +func createRoom(ctx context.Context, cmd *cli.Command) error { + name, err := extractArg(cmd) + if err != nil { + return err + } + + req := &livekit.CreateRoomRequest{ + Name: name, + } + + if roomEgressFile := cmd.String("room-egress-file"); roomEgressFile != "" { + roomEgress := &livekit.RoomCompositeEgressRequest{} + b, err := os.ReadFile(roomEgressFile) + if err != nil { + return err + } + if err = protojson.Unmarshal(b, roomEgress); err != nil { + return err + } + req.Egress = &livekit.RoomEgress{Room: roomEgress} + } + + if participantEgressFile := cmd.String("participant-egress-file"); participantEgressFile != "" { + participantEgress := &livekit.AutoParticipantEgress{} + b, err := os.ReadFile(participantEgressFile) + if err != nil { + return err + } + if err = protojson.Unmarshal(b, participantEgress); err != nil { + return err + } + if req.Egress == nil { + req.Egress = &livekit.RoomEgress{} + } + req.Egress.Participant = participantEgress + } + + if trackEgressFile := cmd.String("track-egress-file"); trackEgressFile != "" { + trackEgress := &livekit.AutoTrackEgress{} + b, err := os.ReadFile(trackEgressFile) + if err != nil { + return err + } + if err = protojson.Unmarshal(b, trackEgress); err != nil { + return err + } + if req.Egress == nil { + req.Egress = &livekit.RoomEgress{} + } + req.Egress.Tracks = trackEgress + } + + if agentsFile := cmd.String("agents-file"); agentsFile != "" { + agent := &livekit.RoomAgent{} + b, err := os.ReadFile(agentsFile) + if err != nil { + return err + } + if err = protojson.Unmarshal(b, agent); err != nil { + return err + } + req.Agent = agent + } + + if roomConfig := cmd.String("room-configuration"); roomConfig != "" { + req.ConfigName = roomConfig + } + + if cmd.Uint("min-playout-delay") != 0 { + fmt.Printf("setting min playout delay: %d\n", cmd.Uint("min-playout-delay")) + req.MinPlayoutDelay = uint32(cmd.Uint("min-playout-delay")) + } + + if maxPlayoutDelay := cmd.Uint("max-playout-delay"); maxPlayoutDelay != 0 { + fmt.Printf("setting max playout delay: %d\n", maxPlayoutDelay) + req.MaxPlayoutDelay = uint32(maxPlayoutDelay) + } + + if syncStreams := cmd.Bool("sync-streams"); syncStreams { + fmt.Printf("setting sync streams: %t\n", syncStreams) + req.SyncStreams = syncStreams + } + + if emptyTimeout := cmd.Uint("empty-timeout"); emptyTimeout != 0 { + fmt.Printf("setting empty timeout: %d\n", emptyTimeout) + req.EmptyTimeout = uint32(emptyTimeout) + } + + if departureTimeout := cmd.Uint("departure-timeout"); departureTimeout != 0 { + fmt.Printf("setting departure timeout: %d\n", departureTimeout) + req.DepartureTimeout = uint32(departureTimeout) + } + + room, err := roomClient.CreateRoom(context.Background(), req) + if err != nil { + return err + } + + PrintJSON(room) + return nil +} + +func listRooms(ctx context.Context, cmd *cli.Command) error { + names, _ := extractArgs(cmd) + req := livekit.ListRoomsRequest{} + if len(names) > 0 { + req.Names = names + } + + res, err := roomClient.ListRooms(context.Background(), &req) + if err != nil { + return err + } + if len(res.Rooms) == 0 { + if len(names) > 0 { + fmt.Printf( + "there are no rooms matching %s", + strings.Join(mapStrings(names, wrapWith("\"")), ", "), + ) + } else { + fmt.Println("there are no active rooms") + } + } + for _, rm := range res.Rooms { + fmt.Printf("%s\t%s\t%d participants\n", rm.Sid, rm.Name, rm.NumParticipants) + } + return nil +} + +func _deprecatedListRoom(ctx context.Context, cmd *cli.Command) error { + res, err := roomClient.ListRooms(context.Background(), &livekit.ListRoomsRequest{ + Names: []string{cmd.String("room")}, + }) + if err != nil { + return err + } + if len(res.Rooms) == 0 { + fmt.Printf("there is no matching room with name: %s\n", cmd.String("room")) + return nil + } + rm := res.Rooms[0] + PrintJSON(rm) + return nil +} + +func deleteRoom(ctx context.Context, cmd *cli.Command) error { + roomId, err := extractArg(cmd) + if err != nil { + return err + } + + _, err = roomClient.DeleteRoom(context.Background(), &livekit.DeleteRoomRequest{ + Room: roomId, + }) + if err != nil { + return err + } + + fmt.Println("deleted room", roomId) + return nil +} + +func updateRoomMetadata(ctx context.Context, cmd *cli.Command) error { + roomName, _ := extractArg(cmd) + res, err := roomClient.UpdateRoomMetadata(context.Background(), &livekit.UpdateRoomMetadataRequest{ + Room: roomName, + Metadata: cmd.String("metadata"), + }) + if err != nil { + return err + } + + fmt.Println("Updated room metadata") + PrintJSON(res) + return nil +} + +func _deprecatedUpdateRoomMetadata(ctx context.Context, cmd *cli.Command) error { + roomName := cmd.String("room") + res, err := roomClient.UpdateRoomMetadata(context.Background(), &livekit.UpdateRoomMetadataRequest{ + Room: roomName, + Metadata: cmd.String("metadata"), + }) + if err != nil { + return err + } + + fmt.Println("Updated room metadata") + PrintJSON(res) + return nil +} + +func joinRoom(ctx context.Context, cmd *cli.Command) error { + pc, err := loadProjectDetails(cmd) + if err != nil { + return err + } + + done := make(chan os.Signal, 1) + roomCB := &lksdk.RoomCallback{ + ParticipantCallback: lksdk.ParticipantCallback{ + OnDataReceived: func(data []byte, params lksdk.DataReceiveParams) { + identity := params.SenderIdentity + logger.Infow("received data", "data", data, "participant", identity) + }, + OnConnectionQualityChanged: func(update *livekit.ConnectionQualityInfo, p lksdk.Participant) { + logger.Debugw("connection quality changed", "participant", p.Identity(), "quality", update.Quality) + }, + OnTrackSubscribed: func(track *webrtc.TrackRemote, pub *lksdk.RemoteTrackPublication, participant *lksdk.RemoteParticipant) { + logger.Infow("track subscribed", + "kind", pub.Kind(), + "trackID", pub.SID(), + "source", pub.Source(), + "participant", participant.Identity(), + ) + }, + OnTrackUnsubscribed: func(track *webrtc.TrackRemote, pub *lksdk.RemoteTrackPublication, participant *lksdk.RemoteParticipant) { + logger.Infow("track unsubscribed", + "kind", pub.Kind(), + "trackID", pub.SID(), + "source", pub.Source(), + "participant", participant.Identity(), + ) + }, + OnTrackUnpublished: func(pub *lksdk.RemoteTrackPublication, participant *lksdk.RemoteParticipant) { + logger.Infow("track unpublished", + "kind", pub.Kind(), + "trackID", pub.SID(), + "source", pub.Source(), + "participant", participant.Identity(), + ) + }, + OnTrackMuted: func(pub lksdk.TrackPublication, participant lksdk.Participant) { + logger.Infow("track muted", + "kind", pub.Kind(), + "trackID", pub.SID(), + "source", pub.Source(), + "participant", participant.Identity(), + ) + }, + OnTrackUnmuted: func(pub lksdk.TrackPublication, participant lksdk.Participant) { + logger.Infow("track unmuted", + "kind", pub.Kind(), + "trackID", pub.SID(), + "source", pub.Source(), + "participant", participant.Identity(), + ) + }, + }, + OnRoomMetadataChanged: func(metadata string) { + logger.Infow("room metadata changed", "metadata", metadata) + }, + OnReconnecting: func() { + logger.Infow("reconnecting to room") + }, + OnReconnected: func() { + logger.Infow("reconnected to room") + }, + OnDisconnected: func() { + logger.Infow("disconnected from room") + close(done) + }, + } + room, err := lksdk.ConnectToRoom(pc.URL, lksdk.ConnectInfo{ + APIKey: pc.APIKey, + APISecret: pc.APISecret, + RoomName: cmd.String("room"), + ParticipantIdentity: cmd.String("identity"), + }, roomCB) + if err != nil { + return err + } + defer room.Disconnect() + + logger.Infow("connected to room", "room", room.Name()) + + signal.Notify(done, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + if cmd.Bool("publish-demo") { + if err = publishDemo(room); err != nil { + return err + } + } + + if cmd.StringSlice("publish") != nil { + fps := cmd.Float("fps") + for _, pub := range cmd.StringSlice("publish") { + onPublishComplete := func(pub *lksdk.LocalTrackPublication) { + if cmd.Bool("exit-after-publish") { + close(done) + return + } + if pub != nil { + fmt.Printf("finished writing %s\n", pub.Name()) + _ = room.LocalParticipant.UnpublishTrack(pub.SID()) + } + } + if err = handlePublish(room, pub, fps, onPublishComplete); err != nil { + return err + } + } + } + + <-done + return nil +} + +func listParticipants(ctx context.Context, cmd *cli.Command) error { + roomName, err := extractArg(cmd) + if err != nil { + return err + } + + res, err := roomClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{ + Room: roomName, + }) + if err != nil { + return err + } + + for _, p := range res.Participants { + fmt.Printf("%s (%s)\t tracks: %d\n", p.Identity, p.State.String(), len(p.Tracks)) + } + return nil +} + +func _deprecatedListParticipants(ctx context.Context, cmd *cli.Command) error { + roomName := cmd.String("room") + res, err := roomClient.ListParticipants(ctx, &livekit.ListParticipantsRequest{ + Room: roomName, + }) + if err != nil { + return err + } + + for _, p := range res.Participants { + fmt.Printf("%s (%s)\t tracks: %d\n", p.Identity, p.State.String(), len(p.Tracks)) + } + return nil +} + +func getParticipant(ctx context.Context, cmd *cli.Command) error { + _ = ctx + roomName, identity := participantInfoFromArgOrFlags(cmd) + res, err := roomClient.GetParticipant(context.Background(), &livekit.RoomParticipantIdentity{ + Room: roomName, + Identity: identity, + }) + if err != nil { + return err + } + + PrintJSON(res) + + return nil +} + +func updateParticipant(ctx context.Context, cmd *cli.Command) error { + roomName, identity := participantInfoFromArgOrFlags(cmd) + metadata := cmd.String("metadata") + permissions := cmd.String("permissions") + if metadata == "" && permissions == "" { + return fmt.Errorf("either metadata or permissions must be set") + } + + req := &livekit.UpdateParticipantRequest{ + Room: roomName, + Identity: identity, + Metadata: metadata, + } + if permissions != "" { + // load existing participant + participant, err := roomClient.GetParticipant(ctx, &livekit.RoomParticipantIdentity{ + Room: roomName, + Identity: identity, + }) + if err != nil { + return err + } + + req.Permission = participant.Permission + if req.Permission != nil { + if err = json.Unmarshal([]byte(permissions), req.Permission); err != nil { + return err + } + } + } + + fmt.Println("updating participant...") + PrintJSON(req) + if _, err := roomClient.UpdateParticipant(ctx, req); err != nil { + return err + } + fmt.Println("participant updated.") + + return nil +} + +func removeParticipant(ctx context.Context, cmd *cli.Command) error { + _ = ctx + roomName, identity := participantInfoFromArgOrFlags(cmd) + _, err := roomClient.RemoveParticipant(context.Background(), &livekit.RoomParticipantIdentity{ + Room: roomName, + Identity: identity, + }) + if err != nil { + return err + } + + fmt.Println("successfully removed participant", identity) + + return nil +} + +func muteTrack(ctx context.Context, cmd *cli.Command) error { + roomName, identity := participantInfoFromFlags(cmd) + muted := (!cmd.IsSet("m") && !cmd.IsSet("u")) || cmd.Bool("m") || !cmd.Bool("u") + trackSid := cmd.String("track") + if trackSid == "" { + trackSid = cmd.Args().First() + } + _, err := roomClient.MutePublishedTrack(context.Background(), &livekit.MuteRoomTrackRequest{ + Room: roomName, + Identity: identity, + TrackSid: trackSid, + Muted: muted, + }) + if err != nil { + return err + } + + verb := "muted" + if !cmd.Bool("muted") { + verb = "unmuted" + } + fmt.Println(verb, "track: ", trackSid) + return nil +} + +func updateSubscriptions(ctx context.Context, cmd *cli.Command) error { + roomName, identity := participantInfoFromFlags(cmd) + trackSids := cmd.StringSlice("track") + _, err := roomClient.UpdateSubscriptions(context.Background(), &livekit.UpdateSubscriptionsRequest{ + Room: roomName, + Identity: identity, + TrackSids: trackSids, + Subscribe: cmd.Bool("subscribe"), + }) + if err != nil { + return err + } + + verb := "subscribed to" + if !cmd.Bool("subscribe") { + verb = "unsubscribed from" + } + fmt.Println(verb, "tracks: ", trackSids) + return nil +} + +func sendData(ctx context.Context, cmd *cli.Command) error { + roomName, _ := participantInfoFromFlags(cmd) + pIDs := cmd.StringSlice("participantID") + data := cmd.String("data") + if data == "" { + data = cmd.Args().First() + } + topic := cmd.String("topic") + req := &livekit.SendDataRequest{ + Room: roomName, + Data: []byte(data), + DestinationSids: pIDs, + } + if topic != "" { + req.Topic = &topic + } + _, err := roomClient.SendData(ctx, req) + if err != nil { + return err + } + + fmt.Println("successfully sent data to room", roomName) + return nil +} + +func participantInfoFromFlags(c *cli.Command) (string, string) { + return c.String("room"), c.String("identity") +} + +func participantInfoFromArgOrFlags(c *cli.Command) (string, string) { + room := c.String("room") + id := c.String("identity") + if id == "" { + id = c.Args().First() + } + return room, id +} diff --git a/cmd/livekit-cli/sip.go b/cmd/lk/sip.go similarity index 68% rename from cmd/livekit-cli/sip.go rename to cmd/lk/sip.go index 3bc291ac..224bd73f 100644 --- a/cmd/livekit-cli/sip.go +++ b/cmd/lk/sip.go @@ -23,13 +23,13 @@ import ( "github.com/livekit/protocol/livekit" lksdk "github.com/livekit/server-sdk-go/v2" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) //lint:file-ignore SA1019 we still support older APIs for compatibility const ( - sipCategory = "SIP" + sipCategory = "I/O" sipTrunkCategory = "Trunks" sipDispatchCategory = "Dispatch Rules" sipParticipantCategory = "Participants" @@ -39,35 +39,30 @@ var ( SIPCommands = []*cli.Command{ { Name: "sip", - Usage: "SIP management", + Usage: "Manage SIP Trunks, Dispatch Rules, and Participants", Category: sipCategory, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "inbound", Aliases: []string{"in", "inbound-trunk"}, Usage: "Inbound SIP Trunk management", Category: sipTrunkCategory, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "list", - Usage: "List all inbound SIP Trunk", + Usage: "List all inbound SIP Trunks", Action: listSipInboundTrunk, - Flags: withDefaultFlags(), }, { Name: "create", - Usage: "Create a inbound SIP Trunk", + Usage: "Create an inbound SIP Trunk", Action: createSIPInboundTrunk, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: RequestDesc[livekit.CreateSIPInboundTrunkRequest](), }, { Name: "delete", - Usage: "Delete SIP Trunk", + Usage: "Delete a SIP Trunk", Action: deleteSIPTrunk, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: "SIPTrunk ID to delete", }, }, @@ -77,27 +72,22 @@ var ( Aliases: []string{"out", "outbound-trunk"}, Usage: "Outbound SIP Trunk management", Category: sipTrunkCategory, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "list", Usage: "List all outbound SIP Trunk", Action: listSipOutboundTrunk, - Flags: withDefaultFlags(), }, { Name: "create", Usage: "Create a outbound SIP Trunk", Action: createSIPOutboundTrunk, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: RequestDesc[livekit.CreateSIPOutboundTrunkRequest](), }, { Name: "delete", Usage: "Delete SIP Trunk", Action: deleteSIPTrunk, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: "SIPTrunk ID to delete", }, }, @@ -107,27 +97,22 @@ var ( Usage: "SIP Dispatch Rule management", Aliases: []string{"dispatch-rule"}, Category: sipDispatchCategory, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "list", Usage: "List all SIP Dispatch Rule", Action: listSipDispatchRule, - Flags: withDefaultFlags(), }, { Name: "create", Usage: "Create a SIP Dispatch Rule", Action: createSIPDispatchRule, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: RequestDesc[livekit.CreateSIPDispatchRuleRequest](), }, { Name: "delete", Usage: "Delete SIP Dispatch Rule", Action: deleteSIPDispatchRule, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: "SIPTrunk ID to delete", }, }, @@ -136,13 +121,11 @@ var ( Name: "participant", Usage: "SIP Participant management", Category: sipParticipantCategory, - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "create", Usage: "Create a SIP Participant", Action: createSIPParticipant, - Flags: withDefaultFlags(), - Args: true, ArgsUsage: RequestDesc[livekit.CreateSIPParticipantRequest](), }, }, @@ -152,114 +135,112 @@ var ( // Deprecated commands kept for compatibility { - Hidden: true, // deprecated: use "sip trunk create" + Hidden: true, // deprecated: use `sip trunk create` Name: "create-sip-trunk", Usage: "Create a SIP Trunk", Action: createSIPTrunkLegacy, Category: sipCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ //lint:ignore SA1019 we still support it RequestFlag[livekit.CreateSIPTrunkRequest](), - ), + }, }, { - Hidden: true, // deprecated: use "sip trunk list" + Hidden: true, // deprecated: use `sip trunk list` Name: "list-sip-trunk", Usage: "List all SIP trunk", Action: listSipTrunk, Category: sipCategory, - Flags: withDefaultFlags(), }, { - Hidden: true, // deprecated: use "sip trunk delete" + Hidden: true, // deprecated: use `sip trunk delete` Name: "delete-sip-trunk", Usage: "Delete SIP Trunk", Action: deleteSIPTrunkLegacy, Category: sipCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Usage: "SIPTrunk ID", Required: true, }, - ), + }, }, { - Hidden: true, // deprecated: use "sip dispatch create" + Hidden: true, // deprecated: use `sip dispatch create` Name: "create-sip-dispatch-rule", Usage: "Create a SIP Dispatch Rule", Action: createSIPDispatchRuleLegacy, Category: sipCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ RequestFlag[livekit.CreateSIPDispatchRuleRequest](), - ), + }, }, { - Hidden: true, // deprecated: use "sip dispatch list" + Hidden: true, // deprecated: use `sip dispatch list` Name: "list-sip-dispatch-rule", Usage: "List all SIP Dispatch Rule", Action: listSipDispatchRule, Category: sipCategory, - Flags: withDefaultFlags(), }, { - Hidden: true, // deprecated: use "sip dispatch delete" + Hidden: true, // deprecated: use `sip dispatch delete` Name: "delete-sip-dispatch-rule", Usage: "Delete SIP Dispatch Rule", Action: deleteSIPDispatchRuleLegacy, Category: sipCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Usage: "SIPDispatchRule ID", Required: true, }, - ), + }, }, { - Hidden: true, // deprecated: use "sip participant create" + Hidden: true, // deprecated: use `sip participant create` Name: "create-sip-participant", Usage: "Create a SIP Participant", Action: createSIPParticipantLegacy, Category: sipCategory, - Flags: withDefaultFlags( + Flags: []cli.Flag{ RequestFlag[livekit.CreateSIPParticipantRequest](), - ), + }, }, } ) -func createSIPClient(c *cli.Context) (*lksdk.SIPClient, error) { - pc, err := loadProjectDetails(c) +func createSIPClient(cmd *cli.Command) (*lksdk.SIPClient, error) { + pc, err := loadProjectDetails(cmd) if err != nil { return nil, err } return lksdk.NewSIPClient(pc.URL, pc.APIKey, pc.APISecret, withDefaultClientOpts(pc)...), nil } -func createSIPTrunkLegacy(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPTrunkLegacy(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } //lint:ignore SA1019 we still support it - return createAndPrintLegacy(c, cli.CreateSIPTrunk, printSIPTrunkID) + return createAndPrintLegacy(ctx, cmd, cli.CreateSIPTrunk, printSIPTrunkID) } -func createSIPInboundTrunk(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPInboundTrunk(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return createAndPrintReqs(c, cli.CreateSIPInboundTrunk, printSIPInboundTrunkID) + return createAndPrintReqs(ctx, cmd, cli.CreateSIPInboundTrunk, printSIPInboundTrunkID) } -func createSIPOutboundTrunk(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPOutboundTrunk(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return createAndPrintReqs(c, cli.CreateSIPOutboundTrunk, printSIPOutboundTrunkID) + return createAndPrintReqs(ctx, cmd, cli.CreateSIPOutboundTrunk, printSIPOutboundTrunkID) } func userPass(user string, hasPass bool) string { @@ -273,13 +254,13 @@ func userPass(user string, hasPass bool) string { return user + " / " + passStr } -func listSipTrunk(c *cli.Context) error { - cli, err := createSIPClient(c) +func listSipTrunk(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } //lint:ignore SA1019 we still support it - return listAndPrint(c, cli.ListSIPTrunk, &livekit.ListSIPTrunkRequest{}, []string{ + return listAndPrint(ctx, cmd, cli.ListSIPTrunk, &livekit.ListSIPTrunkRequest{}, []string{ "SipTrunkId", "Name", "Kind", "Number", "AllowAddresses", "AllowNumbers", "InboundAuth", "OutboundAddress", "OutboundAuth", @@ -298,12 +279,12 @@ func listSipTrunk(c *cli.Context) error { }) } -func listSipInboundTrunk(c *cli.Context) error { - cli, err := createSIPClient(c) +func listSipInboundTrunk(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return listAndPrint(c, cli.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{}, []string{ + return listAndPrint(ctx, cmd, cli.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{}, []string{ "SipTrunkId", "Name", "Numbers", "AllowedAddresses", "AllowedNumbers", "Authentication", @@ -318,12 +299,12 @@ func listSipInboundTrunk(c *cli.Context) error { }) } -func listSipOutboundTrunk(c *cli.Context) error { - cli, err := createSIPClient(c) +func listSipOutboundTrunk(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return listAndPrint(c, cli.ListSIPOutboundTrunk, &livekit.ListSIPOutboundTrunkRequest{}, []string{ + return listAndPrint(ctx, cmd, cli.ListSIPOutboundTrunk, &livekit.ListSIPOutboundTrunkRequest{}, []string{ "SipTrunkId", "Name", "Address", "Transport", "Numbers", @@ -340,13 +321,13 @@ func listSipOutboundTrunk(c *cli.Context) error { }) } -func deleteSIPTrunk(c *cli.Context) error { - cli, err := createSIPClient(c) +func deleteSIPTrunk(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return forEachID(c, func(ctx context.Context, id string) error { - info, err := cli.DeleteSIPTrunk(c.Context, &livekit.DeleteSIPTrunkRequest{ + return forEachID(ctx, cmd, func(ctx context.Context, id string) error { + info, err := cli.DeleteSIPTrunk(ctx, &livekit.DeleteSIPTrunkRequest{ SipTrunkId: id, }) if err != nil { @@ -357,13 +338,13 @@ func deleteSIPTrunk(c *cli.Context) error { }) } -func deleteSIPTrunkLegacy(c *cli.Context) error { - cli, err := createSIPClient(c) +func deleteSIPTrunkLegacy(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - info, err := cli.DeleteSIPTrunk(c.Context, &livekit.DeleteSIPTrunkRequest{ - SipTrunkId: c.String("id"), + info, err := cli.DeleteSIPTrunk(ctx, &livekit.DeleteSIPTrunkRequest{ + SipTrunkId: cmd.String("id"), }) if err != nil { return err @@ -384,28 +365,28 @@ func printSIPOutboundTrunkID(info *livekit.SIPOutboundTrunkInfo) { fmt.Printf("SIPTrunkID: %v\n", info.GetSipTrunkId()) } -func createSIPDispatchRule(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPDispatchRule(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return createAndPrintReqs(c, cli.CreateSIPDispatchRule, printSIPDispatchRuleID) + return createAndPrintReqs(ctx, cmd, cli.CreateSIPDispatchRule, printSIPDispatchRuleID) } -func createSIPDispatchRuleLegacy(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPDispatchRuleLegacy(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return createAndPrintLegacy(c, cli.CreateSIPDispatchRule, printSIPDispatchRuleID) + return createAndPrintLegacy(ctx, cmd, cli.CreateSIPDispatchRule, printSIPDispatchRuleID) } -func listSipDispatchRule(c *cli.Context) error { - cli, err := createSIPClient(c) +func listSipDispatchRule(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return listAndPrint(c, cli.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{}, []string{ + return listAndPrint(ctx, cmd, cli.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{}, []string{ "SipDispatchRuleId", "Name", "SipTrunks", "Type", "RoomName", "Pin", "HidePhone", "Metadata", }, func(item *livekit.SIPDispatchRuleInfo) []string { var room, typ, pin string @@ -427,13 +408,13 @@ func listSipDispatchRule(c *cli.Context) error { }) } -func deleteSIPDispatchRule(c *cli.Context) error { - cli, err := createSIPClient(c) +func deleteSIPDispatchRule(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return forEachID(c, func(ctx context.Context, id string) error { - info, err := cli.DeleteSIPDispatchRule(c.Context, &livekit.DeleteSIPDispatchRuleRequest{ + return forEachID(ctx, cmd, func(ctx context.Context, id string) error { + info, err := cli.DeleteSIPDispatchRule(ctx, &livekit.DeleteSIPDispatchRuleRequest{ SipDispatchRuleId: id, }) if err != nil { @@ -444,13 +425,13 @@ func deleteSIPDispatchRule(c *cli.Context) error { }) } -func deleteSIPDispatchRuleLegacy(c *cli.Context) error { - cli, err := createSIPClient(c) +func deleteSIPDispatchRuleLegacy(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - info, err := cli.DeleteSIPDispatchRule(c.Context, &livekit.DeleteSIPDispatchRuleRequest{ - SipDispatchRuleId: c.String("id"), + info, err := cli.DeleteSIPDispatchRule(ctx, &livekit.DeleteSIPDispatchRuleRequest{ + SipDispatchRuleId: cmd.String("id"), }) if err != nil { return err @@ -463,12 +444,12 @@ func printSIPDispatchRuleID(info *livekit.SIPDispatchRuleInfo) { fmt.Printf("SIPDispatchRuleID: %v\n", info.SipDispatchRuleId) } -func createSIPParticipant(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPParticipant(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return createAndPrintReqs(c, func(ctx context.Context, req *livekit.CreateSIPParticipantRequest) (*livekit.SIPParticipantInfo, error) { + return createAndPrintReqs(ctx, cmd, func(ctx context.Context, req *livekit.CreateSIPParticipantRequest) (*livekit.SIPParticipantInfo, error) { // CreateSIPParticipant will wait for LiveKit Participant to be created and that can take some time. // Default deadline is too short, thus, we must set a higher deadline for it. ctx, cancel := context.WithTimeout(ctx, 30*time.Second) @@ -478,12 +459,12 @@ func createSIPParticipant(c *cli.Context) error { }, printSIPParticipantInfo) } -func createSIPParticipantLegacy(c *cli.Context) error { - cli, err := createSIPClient(c) +func createSIPParticipantLegacy(ctx context.Context, cmd *cli.Command) error { + cli, err := createSIPClient(cmd) if err != nil { return err } - return createAndPrintLegacy(c, func(ctx context.Context, req *livekit.CreateSIPParticipantRequest) (*livekit.SIPParticipantInfo, error) { + return createAndPrintLegacy(ctx, cmd, func(ctx context.Context, req *livekit.CreateSIPParticipantRequest) (*livekit.SIPParticipantInfo, error) { // CreateSIPParticipant will wait for LiveKit Participant to be created and that can take some time. // Default deadline is too short, thus, we must set a higher deadline for it. ctx, cancel := context.WithTimeout(ctx, 30*time.Second) diff --git a/cmd/livekit-cli/token.go b/cmd/lk/token.go similarity index 54% rename from cmd/livekit-cli/token.go rename to cmd/lk/token.go index a2cc24e4..cc509ef2 100644 --- a/cmd/livekit-cli/token.go +++ b/cmd/lk/token.go @@ -15,12 +15,13 @@ package main import ( + "context" "encoding/json" "errors" "fmt" "time" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" @@ -29,84 +30,162 @@ import ( var ( TokenCommands = []*cli.Command{ { - Name: "create-token", - Usage: "creates an access token", - Action: createToken, - Category: "Token", + Name: "token", + Usage: "Create access tokens with granular capabilities", + Category: "Core", + Before: loadProjectConfig, + Commands: []*cli.Command{ + { + Name: "create", + Usage: "Creates an access token", + Action: createToken, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "create", + Usage: "Token bearer can create rooms", + }, + &cli.BoolFlag{ + Name: "list", + Usage: "Token bearer can list rooms", + }, + &cli.BoolFlag{ + Name: "join", + Usage: "Token bearer can join a room (requires --room and --identity)", + }, + &cli.BoolFlag{ + Name: "admin", + Usage: "Token bearer can manage a room (requires --room)", + }, + &cli.BoolFlag{ + Name: "recorder", + Usage: "Token bearer can record a room (requires --room)", + }, + &cli.BoolFlag{ + Name: "egress", + Usage: "Token bearer can interact with EgressService", + }, + &cli.BoolFlag{ + Name: "ingress", + Usage: "Token bearer can interact with IngressService", + }, + &cli.StringSliceFlag{ + Name: "allow-source", + Usage: "Restric publishing to only `SOURCE` types (e.g. --allow-source camera,microphone), defaults to all", + }, + &cli.BoolFlag{ + Name: "allow-update-metadata", + Usage: "Allow participant to update their own name and metadata from the client side", + }, + &cli.StringFlag{ + Name: "identity", + Aliases: []string{"i"}, + Usage: "Unique `ID` of the participant, used with --join", + }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "`NAME` of the participant, used with --join. defaults to identity", + }, + &cli.StringFlag{ + Name: "room", + Aliases: []string{"r"}, + Usage: "`NAME` of the room to join", + }, + &cli.StringFlag{ + Name: "metadata", + Usage: "`JSON` metadata to encode in the token, will be passed to participant", + }, + &cli.StringFlag{ + Name: "valid-for", + Usage: "`TIME` that the token is valid for, e.g. \"5m\", \"1h10m\" (s: seconds, m: minutes, h: hours)", + Value: "5m", + }, + &cli.StringFlag{ + Name: "grant", + Usage: "Additional `VIDEO_GRANT` fields. It'll be merged with other arguments (JSON formatted)", + }, + }, + }, + }, + }, + + // Deprecated commands kept for compatibility + { + Hidden: true, // deprecated: use `token create` + Name: "create-token", + Usage: "Creates an access token", + Action: createToken, Flags: []cli.Flag{ - apiKeyFlag, - secretFlag, &cli.BoolFlag{ Name: "create", - Usage: "enable token to be used to create rooms", + Usage: "Token bearer can create rooms", }, &cli.BoolFlag{ Name: "list", - Usage: "enable token to be used to list rooms", + Usage: "Token bearer can list rooms", }, &cli.BoolFlag{ Name: "join", - Usage: "enable token to be used to join a room (requires --room and --identity)", + Usage: "Token bearer can join a room (requires --room and --identity)", }, &cli.BoolFlag{ Name: "admin", - Usage: "enable token to be used to manage a room (requires --room)", + Usage: "Token bearer can manage a room (requires --room)", }, &cli.BoolFlag{ Name: "recorder", - Usage: "enable token to be used to record a room (requires --room)", + Usage: "Token bearer can record a room (requires --room)", }, &cli.BoolFlag{ Name: "egress", - Usage: "enable token to interact with EgressService", + Usage: "Token bearer can interact with EgressService", }, &cli.BoolFlag{ Name: "ingress", - Usage: "enable token to interact with IngressService", + Usage: "Token bearer can interact with IngressService", }, &cli.StringSliceFlag{ Name: "allow-source", - Usage: "allow one or more sources to be published (i.e. --allow-source camera,microphone). if left blank, all sources are allowed", + Usage: "Allow one or more `SOURCE`s to be published (i.e. --allow-source camera,microphone). if left blank, all sources are allowed", }, &cli.BoolFlag{ Name: "allow-update-metadata", - Usage: "allow participant to update their own name and metadata from the client side", + Usage: "Allow participant to update their own name and metadata from the client side", }, &cli.StringFlag{ Name: "identity", Aliases: []string{"i"}, - Usage: "unique identity of the participant, used with --join", + Usage: "Unique `ID` of the participant, used with --join", }, &cli.StringFlag{ Name: "name", Aliases: []string{"n"}, - Usage: "name of the participant, used with --join. defaults to identity", + Usage: "`NAME` of the participant, used with --join. defaults to identity", }, &cli.StringFlag{ Name: "room", Aliases: []string{"r"}, - Usage: "name of the room to join", + Usage: "`NAME` of the room to join", }, &cli.StringFlag{ Name: "metadata", - Usage: "JSON metadata to encode in the token, will be passed to participant", + Usage: "`JSON` metadata to encode in the token, will be passed to participant", }, &cli.StringFlag{ Name: "valid-for", - Usage: "amount of time that the token is valid for. i.e. \"5m\", \"1h10m\" (s: seconds, m: minutes, h: hours)", + Usage: "Amount of `TIME` that the token is valid for. i.e. \"5m\", \"1h10m\" (s: seconds, m: minutes, h: hours)", Value: "5m", }, &cli.StringFlag{ Name: "grant", - Usage: "additional VideoGrant fields. It'll be merged with other arguments (JSON formatted)", + Usage: "Additional `VIDEO_GRANT` fields. It'll be merged with other arguments (JSON formatted)", }, - projectFlag, }, }, } ) -func createToken(c *cli.Context) error { +func createToken(ctx context.Context, c *cli.Command) error { p := c.String("identity") // required only for join name := c.String("name") room := c.String("room") diff --git a/cmd/livekit-cli/ulimit_unix.go b/cmd/lk/ulimit_unix.go similarity index 100% rename from cmd/livekit-cli/ulimit_unix.go rename to cmd/lk/ulimit_unix.go diff --git a/cmd/livekit-cli/ulimit_windows.go b/cmd/lk/ulimit_windows.go similarity index 100% rename from cmd/livekit-cli/ulimit_windows.go rename to cmd/lk/ulimit_windows.go diff --git a/cmd/livekit-cli/utils.go b/cmd/lk/utils.go similarity index 67% rename from cmd/livekit-cli/utils.go rename to cmd/lk/utils.go index 09f502cd..1e93ddb2 100644 --- a/cmd/livekit-cli/utils.go +++ b/cmd/lk/utils.go @@ -24,7 +24,7 @@ import ( "github.com/livekit/protocol/utils/interceptors" "github.com/twitchtv/twirp" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "github.com/livekit/livekit-cli/pkg/config" ) @@ -32,56 +32,55 @@ import ( var ( roomFlag = &cli.StringFlag{ Name: "room", - Usage: "name of the room", + Usage: "`NAME` of the room", Required: true, } - urlFlag = &cli.StringFlag{ - Name: "url", - Usage: "url to LiveKit instance", - EnvVars: []string{"LIVEKIT_URL"}, - Value: "http://localhost:7880", - } - apiKeyFlag = &cli.StringFlag{ - Name: "api-key", - EnvVars: []string{"LIVEKIT_API_KEY"}, - } - secretFlag = &cli.StringFlag{ - Name: "api-secret", - EnvVars: []string{"LIVEKIT_API_SECRET"}, - } identityFlag = &cli.StringFlag{ Name: "identity", - Usage: "identity of participant", + Usage: "`ID` of participant", Required: true, } - projectFlag = &cli.StringFlag{ - Name: "project", - Usage: "name of a configured project", - } - verboseFlag = &cli.BoolFlag{ - Name: "verbose", - Required: false, - } - printCurl bool - curlFlag = &cli.BoolFlag{ - Name: "curl", - Usage: "print curl commands for API actions", - Destination: &printCurl, - Required: false, + printCurl bool + persistentFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Usage: "`URL` to LiveKit instance", + Sources: cli.EnvVars("LIVEKIT_URL"), + Value: "http://localhost:7880", + Persistent: true, + }, + &cli.StringFlag{ + Name: "api-key", + Usage: "Your `KEY`", + Sources: cli.EnvVars("LIVEKIT_API_KEY"), + Persistent: true, + }, + &cli.StringFlag{ + Name: "api-secret", + Usage: "Your `SECRET`", + Sources: cli.EnvVars("LIVEKIT_API_SECRET"), + Persistent: true, + }, + &cli.StringFlag{ + Name: "project", + Usage: "`NAME` of a configured project", + Persistent: true, + }, + &cli.BoolFlag{ + Name: "curl", + Usage: "Print curl commands for API actions", + Destination: &printCurl, + Required: false, + Persistent: true, + }, + &cli.BoolFlag{ + Name: "verbose", + Required: false, + Persistent: true, + }, } ) -func withDefaultFlags(flags ...cli.Flag) []cli.Flag { - return append([]cli.Flag{ - urlFlag, - apiKeyFlag, - secretFlag, - projectFlag, - curlFlag, - verboseFlag, - }, flags...) -} - func withDefaultClientOpts(c *config.ProjectConfig) []twirp.ClientOption { var ( opts []twirp.ClientOption @@ -96,6 +95,34 @@ func withDefaultClientOpts(c *config.ProjectConfig) []twirp.ClientOption { return opts } +func extractArg(c *cli.Command) (string, error) { + if !c.Args().Present() { + return "", errors.New("no argument provided") + } + return c.Args().First(), nil +} + +func extractArgs(c *cli.Command) ([]string, error) { + if !c.Args().Present() { + return nil, errors.New("no arguments provided") + } + return c.Args().Slice(), nil +} + +func mapStrings(strs []string, fn func(string) string) []string { + res := make([]string, len(strs)) + for i, str := range strs { + res[i] = fn(str) + } + return res +} + +func wrapWith(wrap string) func(string) string { + return func(str string) string { + return wrap + str + wrap + } +} + func PrintJSON(obj any) { txt, _ := json.MarshalIndent(obj, "", " ") fmt.Println(string(txt)) @@ -123,12 +150,12 @@ var ignoreURL = func(p *loadParams) { // attempt to load connection config, it'll prioritize // 1. command line flags (or env var) // 2. default project config -func loadProjectDetails(c *cli.Context, opts ...loadOption) (*config.ProjectConfig, error) { +func loadProjectDetails(c *cli.Command, opts ...loadOption) (*config.ProjectConfig, error) { p := loadParams{requireURL: true} for _, opt := range opts { opt(&p) } - logDetails := func(c *cli.Context, pc *config.ProjectConfig) { + logDetails := func(c *cli.Command, pc *config.ProjectConfig) { if c.Bool("verbose") { fmt.Printf("URL: %s, api-key: %s, api-secret: %s\n", pc.URL, diff --git a/go.mod b/go.mod index 19f7c018..c1a9e2f2 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 github.com/twitchtv/twirp v8.1.3+incompatible - github.com/urfave/cli/v2 v2.27.2 + github.com/urfave/cli/v3 v3.0.0-alpha9 go.uber.org/atomic v1.11.0 golang.org/x/sync v0.7.0 golang.org/x/time v0.5.0 @@ -36,7 +36,6 @@ require ( github.com/bufbuild/protoyaml-go v0.1.9 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/eapache/channels v1.1.0 // indirect @@ -80,7 +79,6 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect github.com/redis/go-redis/v9 v9.5.3 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect diff --git a/go.sum b/go.sum index 9a733475..c4131ec7 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -166,8 +164,6 @@ github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRci github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -184,8 +180,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= -github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= -github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo= +github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/pkg/loadtester/loadtest.go b/pkg/loadtester/loadtest.go index 4fe895c9..cab0b9aa 100644 --- a/pkg/loadtester/loadtest.go +++ b/pkg/loadtester/loadtest.go @@ -101,6 +101,7 @@ func (t *LoadTest) Run(ctx context.Context) error { for _, name := range names { testerStats := stats[name] summaries[name] = getTesterSummary(testerStats) + fmt.Println(testerStats, summaries[name]) w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) _, _ = fmt.Fprintf(w, "\n%s\t| Track\t| Kind\t| Pkts\t| Bitrate\t| Dropped\n", name) @@ -314,13 +315,13 @@ func (t *LoadTest) run(ctx context.Context, params Params) (map[string]*testerSt return nil }) - if err := ctx.Err(); err != nil { - return nil, err - } - - if err := limiter.Wait(ctx); err != nil { - return nil, err - } + if err := ctx.Err(); err != nil { + return nil, err + } + + if err := limiter.Wait(ctx); err != nil { + return nil, err + } } var speakerSim *SpeakerSimulator diff --git a/version.go b/version.go index 5b5e50b8..8af7d757 100644 --- a/version.go +++ b/version.go @@ -15,5 +15,5 @@ package livekitcli const ( - Version = "1.5.2" + Version = "2.0.0" )