Update readme (#35)

* update readme

* minor tweaks

* fix mkdir

* one more tweak

* local room recording

* update language

* add a line

* typo

* fix display_mock

* rearrange
This commit is contained in:
David Colburn
2021-12-06 17:02:22 -08:00
committed by GitHub
parent 16421bd780
commit a33acbf666
7 changed files with 357 additions and 170 deletions

422
README.md
View File

@@ -5,15 +5,219 @@ Record any website using our recorder, or deploy our service to manage it for yo
## How it works
The recorder launches Chrome and navigates to the supplied url, grabs audio from pulse and video from a virtual frame buffer, and feeds them into GStreamer.
You can write the output as mp4 to a file or upload it to s3, or forward the output to one or multiple rtmp streams.
The recorder launches Chrome and navigates to the supplied url, grabs audio from pulse and video from a virtual
frame buffer, and feeds them into GStreamer. You can write the output as mp4 to a file or upload it to s3, or forward
the output to one or multiple rtmp streams.
## Quick start
Start by creating a `config.yaml`:
```
file_output:
local: true
```
Next, create a `request.json`:
```json
{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"filepath": "/out/demo.mp4"
}
```
Start the recording:
```shell
mkdir -p ~/livekit/recordings
docker run --rm --name quick-demo \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat basic.json)" \
-v ~/livekit/recordings:/out \
livekit/livekit-recorder
```
Then, to stop the recording:
```shell
docker stop quick-demo
```
You should find a `~/livekit/recordings/demo.mp4`.
## Recording LiveKit rooms
If you already have a LiveKit server deployed with SSL, recording a room is simple. If not, skip to the next example.
Update your `config.yaml` with the same key and secret as your deployed server, along with your server websocket address:
```yaml
api_key: <livekit-server-api-key>
api_secret: <livekit-server-api-secret>
ws_url: <livekit-server-ws-url>
```
Create a `room.json`:
```json
{
"template": {
"layout": "speaker-dark",
"room_name": "my-room"
},
"filepath": "out/room.mp4"
}
```
Join the room, either using https://example.livekit.io, or using your own client
Start the recording:
```shell
docker run --rm --name room-demo \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat room.json)" \
-v ~/livekit/recordings:/out \
livekit/livekit-recorder
```
To stop recording, either leave the room, or `docker stop room-demo`. You'll find the file at `~/livekit/recordings/room.mp4`
## Recording local rooms
First, find your IP as seen by docker:
* on linux, this should be `172.17.0.1`
* on mac or windows, run `docker run -it --rm alpine nslookup host.docker.internal` and you should see something like
`Name: host.docker.internal Address: 192.168.65.2`
Update your `config.yaml` with `ws_url` using this IP, along with adding your `api_key` and `api_secret`, and `insecure`:
```yaml
api_key: <livekit-server-api-key>
api_secret: <livekit-server-api-secret>
ws_url: ws://192.168.65.2:7880
insecure: true
```
Create a `room.json`:
```json
{
"template": {
"layout": "speaker-dark",
"room_name": "my-room"
},
"filepath": "out/room.mp4"
}
```
Generate a token for yourself using livekit-server:
```shell
./bin/livekit-server --keys "{api_key}: {api_secret}" create-join-token --room my-room --identity me
```
Start your server using `node-ip` from above:
```shell
./bin/livekit-server --keys "{api_key}: {api_secret}" --node-ip 192.168.65.2 --dev
```
Open https://example.livekit.io, enter the `token` you generated, and connect (keep `ws://localhost:7880` as the LiveKit URL).
Start the recording:
```shell
docker run --rm --network host --name local-demo \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat room.json)" \
-v ~/livekit/recordings:/out \
livekit/livekit-recorder
```
To stop recording, either leave the room, or `docker stop local-demo`. You'll find the file at `~/livekit/recordings/room.mp4`
## Uploading to S3
Update `file_output` in your `config.yaml`:
```yaml
file_output:
s3:
access_key: <s3-access-key>
secret: <s3-secret>
region: <s3-region>
bucket: <s3-bucket>
```
Create a `s3.json`:
```json
{
"template": {
"layout": "speaker-dark",
"room_name": "my-room"
},
"filepath": "path/filename.mp4",
"options": {
"preset": "HD_60"
}
}
```
This time, we've added the `HD_60` preset. This will record at 1280x720, 60fps (the default is 1920x1080, 30fps).
You can find the other presets and options [below](#presets).
Join the room, and start the recording:
```shell
docker run --rm --name s3-demo \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat s3.json)" \
livekit/livekit-recorder
```
End the recording:
```shell
docker stop s3-demo
```
After the recording is stopped, the file will be uploaded to your S3 bucket.
## Rtmp Output
Create a `rtmp.json` (if you have a Twitch account you can fill in your stream key, otherwise replace the rtmp url with your provider):
```json
{
"template": {
"layout": "speaker-dark",
"room_name": "my-room"
},
"rtmp": {
"urls": ["rtmp://live.twitch.tv/app/<stream-key>"]
},
"options": {
"width": "1280",
"height": "720",
"video_bitrate": 2048
}
}
```
This time, we've set custom options to output 720p with a lower bitrate (2048 kbps - the default is 3000 kpbs). If you have sufficient bandwidth, try using `preset: FULL_HD_60` instead for a high quality stream.
Join the room, then start the stream:
```shell
docker run --rm --name rtmp-demo \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat rtmp.json)" \
livekit/livekit-recorder
```
Note: with Twitch, it will take about 25 seconds for them to process before they begin showing the stream.
May be different with other providers.
Stop the stream:
```shell
docker stop rtmp-demo
```
## Config
Both the standalone recorder and recorder service take a yaml config file. If you will be using templates with your recording requests, `ws_url` is required, and to record by room name instead of token, `api_key` and
`api_secret` are also required. When running in service mode, `redis` config is required (with the same db as your LiveKit server), as this is how it receives requests.
Below is a full config, with all optional parameters.
All config options:
* `api_key`, `api_secret`, and `ws_url` are required for recording LiveKit rooms
* `log_level: error` is recommended for production setups. GStreamer logs can be noisy
* `redis` is required for [service mode](#service-mode)
* Only one of `file_output.s3` or `file_output.azblob` should be set.
* If `preset` is set, it will override any other options. Any and all `defaults` can be overridden by a request
All config parameters:
```yaml
api_key: livekit server api key
@@ -21,7 +225,8 @@ api_secret: livekit server api secret
ws_url: livekit server ws url
health_port: http port to serve status (optional)
log_level: valid levels are debug, info, warn, error, fatal, or panic. Defaults to debug
template_address: template url base, can be used to host your own templates. Defaults to https://recorder.livekit.io
template_address: template url base, can be used to host your own templates. Defaults to https://recorder.livekit.io/#
insecure: should only be used for local testing
redis: (service mode only)
address: redis address, including port
username: redis username (optional)
@@ -61,44 +266,36 @@ defaults:
If you don't supply any options with your config defaults or the request, it defaults to FULL_HD_30.
## Request
## Requests
See StartRecordingRequest [here](https://github.com/livekit/protocol/blob/main/livekit_recording.proto#L16).
See [StartRecordingRequest](https://github.com/livekit/protocol/blob/main/livekit_recording.proto#L24).
When using standalone mode, the request can be input as a json file. In service mode, these requests will be made through
the LiveKit server's recording api.
### Input
You can input either a `url` to record from, or choose a `template` and supply a `layout` and `room_name`.
We currently have 4 templates available - grid or speaker, each available in light or dark.
Your config will need your server api key and secret, along with the websocket url.
Check out our [web README](https://github.com/livekit/livekit-recorder/tree/main/web) to learn more or create your own.
### Output
You can either output to a `filepath` or write to one or more `rtmp` `urls`. Depending on your config, the `filepath`
output will either write to a local file or upload to s3 or azure blob.
### Options
You can also override any defaults set in your `config.yaml`.
* Input: either `url` or `template`
* `url`: any url that chrome can connect to for recording
* `template`: `layout` and `room_name` required. `base_url` is optional, used for custom templates
* We currently have 4 templates available; `speaker-light`, `speaker-dark`, `grid-light`, and `grid-dark`. Check out our [web README](https://github.com/livekit/livekit-recorder/tree/main/web) to learn more or create your own.
* Output: either `filepath` or `rtmp`. File output and stream output cannot be mixed
* `filepath`: whether writing to a local file, s3, or azure blob storage, this path will be used. Must end with `.mp4`
* `rtmp`: a list of rtmp urls to stream to
* `options`: will override anything in `config.defaults`. Using `preset` will override all other options
All request options:
```json
{
"url": "<external-website-to-record.com>",
"url": "website-to-record.com",
"template": {
"layout": "<grid|speaker>-<light|dark>",
"room_name": "<room-to-record>",
"base_url": "optional-custom-template-host.com"
"room_name": "my-room",
"base_url": "my-template-host.com"
},
"filepath": "path/recording.mp4",
"filepath": "path/output.mp4",
"rtmp": {
"urls": ["<rtmp://stream-url.com>"]
"urls": ["rtmp://stream-url-1.com", "rtmp://stream-url-2.com"]
},
"options": {
"preset": "FULL_HD_60",
"preset": "FULL_HD_30",
"width": 1920,
"height": 1080,
"depth": 24,
@@ -110,148 +307,103 @@ All request options:
}
```
# Service Mode
## Service Mode
Simply deploy the service, and submit requests through your LiveKit server.
### How it works
The service listens to a redis subscription and waits for the LiveKit server to make a reservation. Once the reservation
is made to ensure availability, the service waits for a StartRecording request from the server before launching the recorder.
The recorder will be stopped by either a `END_RECORDING` signal from the server, or automatically when the last participant leaves if using our templates.
is made to ensure availability, the server sends a StartRecording request to the reserved instance.
A single service instance can record one room at a time.
### Deployment
See guides and deployment docs at https://docs.livekit.io/guides/recording
See guides and deployment docs at https://docs.livekit.io/guides/deploy/recorder.
### Running locally
### Development
If you want to try running against a local livekit server, you'll need to make a couple changes:
This is **not** recommended for a production setup - the following redis changes make your redis server completely
accessible to the internet, and using `--network host` with your docker run command is also not recommended in production.
To run against a local livekit server, you'll need to do the following:
* open `/usr/local/etc/redis.conf` and comment out the line that says `bind 127.0.0.1`
* change `protected-mode yes` to `protected-mode no` in the same file
* add `--network host` to your `docker run` command
* update your redis address from `localhost` to your host ip as docker sees it:
* on linux, this should be `172.17.0.1`
* on mac or windows, run `docker run -it --rm alpine nslookup host.docker.internal` and you should see something like
`Name: host.docker.internal
Address: 192.168.65.2`
* find your IP as seen by docker
* `ws_url` needs to be set using the IP as Docker sees it
* on linux, this should be `172.17.0.1`
* on mac or windows, run `docker run -it --rm alpine nslookup host.docker.internal` and you should see something like
`Name: host.docker.internal
Address: 192.168.65.2`
* update your `redis` and `ws_url` to use this IP instead of `localhost`, and set `insecure` to true in your `config.yaml`
* your livekit-server must be run using `--node-ip` set to the above IP
These changes allow the service to connect to your local redis instance from inside the docker container.
Finally, to build and run:
```bash
```shell
docker build -t livekit-recorder .
docker run --network host \
-e SERVICE_MODE=1 \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
livekit-recorder
```
You can then use our [cli](https://github.com/livekit/livekit-cli) to submit recording requests to your server.
# Examples
## FAQ
Start by filling in a config.yaml:
### It doesn't work at all
```
api_key: <livekit-server-api-key>
api_secret: <livekit-server-api-secret>
ws_url: <livekit-server-ws-url>
file_output:
local: true
```
* Make sure you're using the latest cli, server sdks, livekit-server and livekit-recorder.
This is still in beta, and we still occasionally release breaking changes.
## Basic recording
### I get a `"no recorders available"` error when sending a StartRecording request
basic.json:
```json
{
"template": {
"layout": "speaker-dark",
"room_name": "my-room"
},
"filepath": "/out/demo.mp4"
}
```
```bash
mkdir -p ~/livekit/output
* Your livekit server cannot reach your recorder through redis. Make sure they are both able to reach the same redis db.
### Docker fails to run this on my computer
docker run --rm \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat basic.json)" \
-v ~/livekit/recordings:/out \
livekit/livekit-recorder
```
* The recorder currently only works on `linux` OS with `amd64` architecture. Unfortunately, this means it does not run on the new M1 macbooks.
## Record at 720p, with 2048kbps video bitrate, and upload result to s3
### I'm getting a broken mp4 file
update your config.yaml and replace
```yaml
file_output:
local: true
```
with
```yaml
file_output:
s3:
access_key: <s3-access-key>
secret: <s3-secret>
region: <s3-region>
bucket: <s3-bucket>
```
* GStreamer needs to be properly shut down - if the process is killed, the file will be unusable.
Make sure you're stopping the recording with either a `docker stop` or an `EndRecordingRequest`.
s3.json:
```json
{
"url": "https://www.youtube.com/watch?v=BHACKCNDMW8",
"filepath": "path/filename.mp4",
"options": {
"width": "1280",
"height": "720",
"video_bitrate": 2048
}
}
```
```bash
docker run --name my-recorder --rm \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat s3.json)" \
livekit/livekit-recorder
```
```bash
docker stop my-recorder
```
### Still getting a broken file. How can I debug?
## Stream to Twitch at 1080p, 60fps
* Start with a `url` request recording from, for example, YouTube.
* If the `url` request works, try a `template` request next.
* Join the room yourself by connecting on https://example.livekit.io - recording an empty room will not work.
* If the `template` request doesn't work, the recorder probably isn't connecting to the room.
Using `log_level: debug`, the recorder should print a message that says `launching chrome` along with a url.
Try navigating to this url in your browser to see if it connects to the room.
twitch.json:
```json
{
"template": {
"layout": "speaker-dark",
"room_name": "my-room"
},
"rtmp": {
"urls": ["rtmp://live.twitch.tv/app/<stream-key>"]
},
"options": {
"preset": "FULL_HD_60"
}
}
```
```bash
docker run --rm \
-e LIVEKIT_RECORDER_CONFIG="$(cat config.yaml)" \
-e RECORDING_REQUEST="$(cat twitch.json)" \
livekit/livekit-recorder
```
### My rtmp output is missing video. How can I debug?
* Start with a `url` request recording from, for example, YouTube.
* If the `url` request works, try a `template` request next.
* Join the room yourself by connecting on https://example.livekit.io - recording an empty room will not work.
* Try streaming to a different rtmp endpoint. For testing, we use Twitch and [RTSP Simple Server](https://github.com/aler9/rtsp-simple-server).
## Ending a recording
### I'm seeing GStreamer errors. Is this normal?
Once started, there are a number of ways to end the recording:
* `docker stop <container>`
* if using our templates, the recorder will stop automatically when the last participant leaves
* if using your own webpage, logging `END_RECORDING` to the console
* It's recommended to use `log_level`: `error` in a production setup. This will remove most of the GStreamer logs.
* `Failed to load plugin '/usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstvaapi.so': libva-wayland.so.2: cannot open shared object file: No such file or directory`
* Expected - safe to ignore. This is caused by the wayland plugin being present in the Dockerfile, but its dependencies are not installed. We do not use this plugin - it will be removed from the Dockerfile in the future.
* `WARN flvmux ... Got backwards dts! (0:01:10.379000000 < 0:01:10.457000000)`
* Occurs when streaming to rtmp - safe to ignore. These warnings occur due to live sources being used for the flvmux. The dts difference should be small (under 150ms).
With any of these methods, the recorder will stop gstreamer and finish uploading before shutting down.
### Can I run this without docker?
* It's possible, but not recommended. To do so, you would need gstreamer and all the plugins installed, along with xvfb,
and have a pulseaudio server running.
* Since it records from system audio, each recorder needs to be run on its own machine or VM.
### How do I autoscale?
* Currently, it's not possible to keep X recorders available at all times, although we plan to add that in the future.
* In the meantime, you can autoscale based on CPU - each instance consistently uses 2.5-3 CPU while recording.
* Autoscaling by listening to webhooks and launching a new instance is possible, but not recommended.
Note that each instance requires its own VM, and they are CPU intensive - with 16 cores on the host you should only
expect to be able to run 4 or 5 instances.

View File

@@ -24,6 +24,7 @@ type Config struct {
HealthPort int `yaml:"health_port"`
LogLevel string `yaml:"log_level"`
TemplateAddress string `yaml:"template_address"`
Insecure bool `yaml:"insecure"`
Redis RedisConfig `yaml:"redis"`
FileOutput FileOutput `yaml:"file_output"`
Defaults Defaults `yaml:"defaults"`
@@ -72,7 +73,7 @@ func NewConfig(confString string) (*Config, error) {
// start with defaults
conf := &Config{
LogLevel: "info",
TemplateAddress: "https://recorder.livekit.io",
TemplateAddress: "https://recorder.livekit.io/#",
Defaults: Defaults{
Width: 1920,
Height: 1080,
@@ -125,7 +126,7 @@ func TestConfig() (*Config, error) {
ApiKey: "fakeKey",
ApiSecret: "fakeSecret",
LogLevel: "debug",
TemplateAddress: "https://recorder.livekit.io",
TemplateAddress: "https://recorder.livekit.io/#",
Redis: RedisConfig{
Address: "localhost:6379",
},

View File

@@ -2,6 +2,10 @@
package display
import (
"github.com/livekit/livekit-recorder/pkg/config"
)
type Display struct {
endChan chan struct{}
}
@@ -12,7 +16,7 @@ func New() *Display {
}
}
func (d *Display) Launch(display, url string, width, height, depth int) error {
func (d *Display) Launch(conf *config.Config, url string, width, height, depth int) error {
return nil
}

View File

@@ -13,6 +13,8 @@ import (
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-recorder/pkg/config"
)
const (
@@ -34,11 +36,11 @@ func New() *Display {
}
}
func (d *Display) Launch(display, url string, width, height, depth int) error {
if err := d.launchXvfb(display, width, height, depth); err != nil {
func (d *Display) Launch(conf *config.Config, url string, width, height, depth int) error {
if err := d.launchXvfb(conf.Display, width, height, depth); err != nil {
return err
}
if err := d.launchChrome(display, url, width, height); err != nil {
if err := d.launchChrome(conf, url, width, height); err != nil {
return err
}
return nil
@@ -55,8 +57,8 @@ func (d *Display) launchXvfb(display string, width, height, depth int) error {
return nil
}
func (d *Display) launchChrome(display, url string, width, height int) error {
logger.Debugw("launching chrome")
func (d *Display) launchChrome(conf *config.Config, url string, width, height int) error {
logger.Debugw("launching chrome", "url", url)
opts := []chromedp.ExecAllocatorOption{
chromedp.NoFirstRun,
@@ -95,7 +97,14 @@ func (d *Display) launchChrome(display, url string, width, height int) error {
chromedp.Flag("autoplay-policy", "no-user-gesture-required"),
chromedp.Flag("window-position", "0,0"),
chromedp.Flag("window-size", fmt.Sprintf("%d,%d", width, height)),
chromedp.Flag("display", display),
chromedp.Flag("display", conf.Display),
}
if conf.Insecure {
opts = append(opts,
chromedp.Flag("disable-web-security", true),
chromedp.Flag("allow-running-insecure-content", true),
)
}
allocCtx, _ := chromedp.NewExecAllocator(context.Background(), opts...)

View File

@@ -46,7 +46,7 @@ func NewRecorder(conf *config.Config, recordingID string) *Recorder {
func (r *Recorder) Run() *livekit.RecordingInfo {
r.display = display.New()
options := r.req.Options
err := r.display.Launch(r.conf.Display, r.url, int(options.Width), int(options.Height), int(options.Depth))
err := r.display.Launch(r.conf, r.url, int(options.Width), int(options.Height), int(options.Depth))
if err != nil {
logger.Errorw("error launching display", err)
r.result.Error = err.Error()

View File

@@ -96,7 +96,7 @@ func (r *Recorder) GetInputUrl(req *livekit.StartRecordingRequest) (string, erro
baseUrl = template.BaseUrl
}
return fmt.Sprintf("%s/#/%s?url=%s&token=%s",
return fmt.Sprintf("%s/%s?url=%s&token=%s",
baseUrl, template.Layout, url.QueryEscape(r.conf.WsUrl), token), nil
default:
return "", ErrNoInput

View File

@@ -3,36 +3,57 @@
## Using our templates
We currently have 4 templates available - `speaker-light`, `speaker-dark`, `grid-light`, and `grid-dark`.
The `speaker` templates will show the current active speaker taking up most of the screen, with other participants in the side bar.
The `grid` templates will show a 1x1, 2x2, or 3x3 grid for up to 1, 4, or 9 participants respectively.
Example usage:
```json
{
"api_key": "<key>",
"api_secret": "<secret>",
"input": {
"template": {
"layout": "speaker-dark",
"ws_url": "wss://your-livekit-address.com",
"room_name": "room-to-record"
}
}
}
```
Our templates are deployed at https://recorder.livekit.io.
## Building your own templates
When using this option, you must handle token generation/room connection - the recorder will only open the url and start recording.
When using this option, you must handle room connection - the recorder will only open the url and start recording.
It's easiest to start with a copy of one of our existing templates.
To have the recorder stop when the room closes, the page should send a `console.log('END_RECORDING')`.
For example, our templates do the following:
```js
const onParticipantDisconnected = (room: Room) => {
/* Special rule for recorder */
if (room.participants.size === 0) {
console.log("END_RECORDING")
### Custom template requirements
2. The template **should** use `layout` names as paths (see [src/App.tsx](https://github.com/livekit/livekit-recorder/blob/main/web/src/App.tsx)).
3. The template **should** take `url` and `token` as query parameters. `url` refers to your LiveKit server websocket url (`ws_url` in the recorder's `config.yaml`).
4. The template **should** use the `url` and `token` parameters to connect to your LiveKit room
([src/common.ts:useParams](https://github.com/livekit/livekit-recorder/blob/main/web/src/common.ts#L37)).
5. The template **must** `console.log('START_RECORDING')` to start the recording.
1. If your template does not log `START_RECORDING`, the recording will not start.
6. The template **should** `console.log('END_RECORDING')` to stop the recording.
1. If your template does not log `END_RECORDING`, the recording will need to be stopped manually with either a
`docker stop` if recording locally, or by sending an `EndRecordingRequest` to your LiveKit server.
2. See [src/common.ts:onConnected](https://github.com/livekit/livekit-recorder/blob/main/web/src/common.ts#L13)
for recommended `START_RECORDING` and `END_RECORDING` implementation.
### Using your template
First, you must deploy your template(s).
Once your template is deployed, update your recorder's `config.yaml` and add
```yaml
api_key: your-livekit-server-api-key
api_secret: your-livekit-server-api-secret
ws_url: wss://your-livekit-server-address.com
template_address: https://your-template-address.com/#
```
* Note: the hash is necessary if using hash routing, which is what our templates use. For example, the default
`template_address` is `https://recorder.livekit.io/#`.
* If you want to use both your own templates and LiveKit templates, you can override the template address per
request using the `template.base_url` field.
Send a request to your recorder using
```json
{
"template": {
"layout": "my-layout",
"room_name": "my-room"
}
}
```
The recorder will generate a `token` to join the room, then build the url
`{config.template_address}/{request.layout}?url={encoded config.ws_url}&token={token}`.
If your page doesn't log this message, the recording will need to be stopped by sending a `EndRecordingRequest` to your LiveKit server.
In this example, the generated address would look like
`https://your-template-address.com/#/my-layout?url=wss%3A%2F%2Fyour-livekit-server-address.com&token=...`