Files
continuwuity/docs/calls/livekit.mdx
T

545 lines
19 KiB
Plaintext

# Matrix RTC/Element Call Setup
:::info
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
:::
:::tip
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
:::
## Instructions
### 1. Domain
LiveKit should live on its own domain or subdomain. In this guide we use `livekit.example.com` - this should be replaced with a domain you control.
Make sure the DNS record for the (sub)domain you plan to use is pointed to your server.
### 2. Services
Using LiveKit with Matrix requires two services - LiveKit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
You must generate a key and secret to allow the Matrix service to authenticate with LiveKit. `LK_MATRIX_KEY` should be around 20 random characters, and `LK_MATRIX_SECRET` should be around 64. Remember to replace these with the actual values!
:::tip Generating the secrets
LiveKit provides a utility to generate secure random keys
```bash
docker run --rm livekit/livekit-server:latest generate-keys
# API Key: APIUxUnMnSkuFWV
# API Secret: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
```
:::
Create a `docker-compose.yml` file as following:
```yaml
services:
lk-jwt-service:
image: ghcr.io/element-hq/lk-jwt-service:latest
container_name: lk-jwt-service
environment:
- LIVEKIT_JWT_BIND=:8081
- LIVEKIT_URL=wss://livekit.example.com # your LiveKit domain
- LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com # your server_name
# Replace these with the generated values as above
- LIVEKIT_KEY=LK_MATRIX_KEY # APIUxUnMnSkuFWV
- LIVEKIT_SECRET=LK_MATRIX_SECRET # t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
restart: unless-stopped
ports:
- "8081:8081"
livekit:
image: livekit/livekit-server:latest
container_name: livekit
command: --config /etc/livekit.yaml
restart: unless-stopped
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
network_mode: "host" # /!\ LiveKit binds to all addresses by default.
# Make sure port 7880 is blocked by your firewall to prevent access bypassing your reverse proxy
# Alternatively, uncomment the lines below and comment `network_mode: "host"` above to specify port mappings.
# ports:
# - "127.0.0.1:7880:7880/tcp"
# - "7881:7881/tcp"
# - "50100-50200:50100-50200/udp"
```
Next, we need to configure LiveKit. In the same directory, create `livekit.yaml` with the following content - remembering to replace `LK_MATRIX_KEY` and `LK_MATRIX_SECRET` with the values you generated:
```yaml
port: 7880
bind_addresses:
- ""
rtc:
tcp_port: 7881
port_range_start: 50100
port_range_end: 50200
use_external_ip: true
enable_loopback_candidate: false
keys:
LK_MATRIX_KEY: LK_MATRIX_SECRET
# replace these with your key-secret pair. Example:
# APIUxUnMnSkuFWV: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
```
Consult [`config-sample.yaml`][config-sample-yaml] for all LiveKit options.
[livekit-config-sample-yaml]: https://github.com/livekit/livekit/blob/master/config-sample.yaml
#### Firewall hints
You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firewall. If you use UFW, the commands are: `ufw allow 7881/tcp` and `ufw allow 50100:50200/udp`.
### 3. Telling clients where to find LiveKit
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to the `[global.matrix_rtc]` config section using the `foci` option.
The variable should be a list of servers serving as MatrixRTC endpoints. Replace the URL with the address you are deploying your instance of lk-jwt-service to:
```toml
[global.matrix_rtc]
foci = [
{ type = "livekit", livekit_service_url = "https://livekit.example.com" },
]
```
This will expose LiveKit information on the following endpoints for clients to discover:
- `/_matrix/client/unstable/org.matrix.msc4143/rtc/transports` (MSC4143 unstable, behind auth)
- `/.well-known/matrix/client` (fallback, not behind auth. Only enabled if `[global.well_known].client` is set)
### 4. Configure your Reverse Proxy
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
All paths should be forwarded to LiveKit by default, with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
- `/sfu/get`
- `/healthz`
- `/get_token`
<details>
<summary>Example caddy config</summary>
```
livekit.example.com {
# for lk-jwt-service
@lk-jwt-service path /sfu/get* /healthz* /get_token*
route @lk-jwt-service {
reverse_proxy 127.0.0.1:8081
}
# for livekit
reverse_proxy 127.0.0.1:7880
}
```
</details>
<details>
<summary>Example nginx config</summary>
```
server {
server_name livekit.example.com;
# for lk-jwt-service
location ~ ^/(sfu/get|healthz|get_token) {
proxy_pass http://127.0.0.1:8081$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_buffering off;
}
# for LiveKit
location / {
proxy_pass http://127.0.0.1:7880$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_buffering off;
# websocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
```
Note that for websockets to work, you need to have this somewhere outside your server block:
```
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
```
</details>
<details>
<summary>Example traefik router</summary>
```
# on LiveKit itself
traefik.http.routers.livekit.rule=Host(`livekit.example.com`)
# on the JWT service
traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))
```
</details>
<details>
<summary>Example docker compose file with caddy-docker-proxy labels</summary>
```yaml
# This setup assumes all containers share the same bridge network
services:
lk-jwt-service:
image: ghcr.io/element-hq/lk-jwt-service:latest
container_name: lk-jwt-service
# lk-jwt-service environment config here..
labels:
caddy: livekit.example.com
caddy.@lk-jwt-service.path: "/sfu/get* /healthz* /get_token*"
caddy.reverse_proxy: "@lk-jwt-service {{upstreams 8081}}"
livekit:
image: livekit/livekit-server:latest
container_name: livekit
command: --config /etc/livekit.yaml
restart: unless-stopped
labels:
caddy: livekit.example.com
caddy.reverse_proxy: "{{upstreams 7880}}"
volumes:
- ./livekit.yaml:/etc/livekit.yaml:ro
ports:
- "127.0.0.1:7880:7880/tcp"
- "7881:7881/tcp"
- "50100-50200:50100-50200/udp"
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
ports:
- 80:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=caddy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
restart: unless-stopped
labels:
# If you already configured `[global.well_known]` with Continuwuity,
# comment out the *_respond labels and add this line
# caddy.reverse_proxy: /.well-known/matrix/* homeserver:8008
caddy.1_respond: /.well-known/matrix/server {"m.server":"matrix.example.com:443"}
caddy.2_respond: /.well-known/matrix/client {"m.server":{"base_url":"https://matrix.example.com"},"m.homeserver":{"base_url":"https://matrix.example.com"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit.example.com"}]}
# If you are having problems with continuwuity serving headers uncomment
# the header section below.
# caddy: example.com
# caddy.0_header: "*"
# caddy.0_header.Access-Control-Allow-Origin: "*"
# caddy.0_header.Access-Control-Allow-Methods: "GET, POST, OPTIONS"
# caddy.0_header.Access-Control-Allow-Headers: "Authorization"
# caddy.0_header.Content-Type: "application/json"
homeserver:
image: forgejo.ellis.link/continuwuation/continuwuity:latest
restart: unless-stopped
# add additional environment, volume, and network config here...
labels:
caddy: matrix.example.com
caddy.reverse_proxy: "{{upstreams 8008}}"
```
</details>
### 6. Start Everything
Start up the services using your usual method - for example `docker compose up -d`.
## Testing
To test that LiveKit is successfully integrated with Continuwuity, you will need to replicate its [Token Exchange Flow](https://github.com/element-hq/lk-jwt-service#%EF%B8%8F-how-it-works--token-exchange-flow). Follow the steps below while checking Docker logs (`docker-compose logs --follow`), in order to help [troubleshooting](#troubleshooting) any issues.
First, you will need an access token for your current login session. These can be found in your client's settings or obtained via [this website](https://timedout.uk/mxtoken.html).
Then, using that token, fetch the discovery endpoints for MatrixRTC services:
```bash
curl -H "Authorization: Bearer <session-access-token>" \
https://matrix.example.com/_matrix/client/unstable/org.matrix.msc4143/rtc/transports
```
In the output, you should see the LiveKit URL matching the one [configured above](#3-telling-clients-where-to-find-livekit).
With the same token, request another OpenID token for use with the lk-jwt-service:
```bash
curl -X POST -H "Authorization: Bearer <session-access-token>" \
https://matrix.example.com/_matrix/client/v3/user/@user:example.com/openid/request_token
```
You will see a response as below:
```json
{"access_token":"<openid_access_token>","token_type":"Bearer","matrix_server_name":"example.com","expires_in":3600}
```
Next, create a `payload.json` file with the following content:
<details>
<summary>`payload.json`</summary>
```json
{
"room_id": "abc",
"slot_id": "xyz",
"openid_token": {
"matrix_server_name": "example.com",
"access_token": "<openid_access_token>",
"token_type": "Bearer"
},
"member": {
"id": "xyz",
"claimed_device_id": "DEVICEID",
"claimed_user_id": "@user:example.com"
}
}
```
Replace `matrix_server_name` and `claimed_user_id` with your information, and `<openid_access_token>` with the one you got from the previous step. Other values can be left as-is.
</details>
You can then send this payload to the lk-jwt-service:
```bash
curl -X POST -d @payload.json https://livekit.example.com/get_token
```
The lk-jwt-service will, after checking against Continuwuity, answer with a `jwt` token to create a LiveKit media room:
```json
{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}
```
Use this token to test at the [LiveKit Connection Tester](https://livekit.io/connection-test). If everything works there, then you have set up LiveKit successfully!
## Troubleshooting
To debug any issues, you can place a call or redo the Testing instructions, and check the container logs for any specific errors. Use `docker-compose logs --follow` to follow these logs in real-time.
### Common errors in Element Call UI
- `MISSING_MATRIX_RTC_FOCUS`: LiveKit is missing from Continuwuity's config file
- "Waiting for media" popup always showing: a LiveKit URL has been configured in Continuwuity, but your client cannot connect to it for some reason
For browser-based clients, you can also inspect connections using DevTools' Networking tab, to see which requests are erroring out.
### Docker loopback networking issues
Some distros do not allow Docker containers to connect to its host's public IP by default. This would cause `lk-jwt-service` to fail connecting to `livekit` or `continuwuity` on the same host. As a result, you would see connection refused/connection timeouts log entries in the JWT service, even when `LIVEKIT_URL` has been configured correctly.
You can also test that this is the case by cURLing from a sidecar container:
```bash
docker run --rm --net container:lk-jwt-service docker.io/curlimages/curl https://livekit.example.com
# --- some errors ---
```
To alleviate this, you can try one of the following workarounds:
- Use `network_mode: host` for the `lk-jwt-service` container (instead of the default bridge networking).
- Add an `extra_hosts` file mapping livekit's (and continuwuity's) domain name to a locally-reachable address:
```diff
# in docker-compose.yaml
services:
lk-jwt-service:
...
+ extra_hosts:
+ - "livekit.example.com:127.0.0.1"
+ - "matrix.example.com:127.0.0.1"
```
- (**untested, use at your own risk**) Implement an iptables workaround as shown [here](https://forums.docker.com/t/unable-to-connect-to-host-service-from-inside-docker-container/145749/6).
After implementing the changes and restarting your compose, `lk-jwt-service` should now connect to your other services. The sidecar container test above should now return an `OK` from LiveKit.
### Workaround for non-federating servers
When deploying on servers with federation disabled (`allow_federation = false`), LiveKit will fail as it can't fetch the required [OpenID endpoint](https://spec.matrix.org/v1.17/server-server-api/#get_matrixfederationv1openiduserinfo) via federation paths.
As a workaround, you can enable federation, but forbid all remote servers via the following config parameters:
```toml
### in your continuwuity.toml file ###
allow_federation = true
forbidden_remote_server_names = [".*"]
```
Subscribe to issue [!1440](https://forgejo.ellis.link/continuwuation/continuwuity/issues/1440) for future updates on this matter.
## Related Documentation
Guides:
- [Element Call self-hosting documentation from element-hq][element-call-selfhosting]
- [Community guide with overview of LiveKit's mechanisms][tom-livekit-guide]
- [Community guide using systemd][kimiblock-livekit-guide]
[element-call-selfhosting]: https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md
[tom-livekit-guide]: https://tomfos.tr/matrix/livekit/
[kimiblock-livekit-guide]: https://blog.kimiblock.top/2024/12/24/hosting-element-call/
Configurations:
- [Livekit's `config-sample.yaml`][livekit-config-sample-yaml] - LiveKit configuration file with full options
Specifications:
- [MSC4143 - MatrixRTC proposal][MSC4143]
- [MSC4195 - LiveKit proposal][MSC4195]
[MSC4143]: https://github.com/matrix-org/matrix-spec-proposals/pull/4143
[MSC4195]: https://github.com/matrix-org/matrix-spec-proposals/pull/4195
Source code:
- [Element Call][element-call-github]
- [lk-jwt-service][lk-jwt-service-github]
- [LiveKit server][livekit-server-github]
[element-call-github]: https://github.com/element-hq/element-call
[lk-jwt-service-github]: https://github.com/element-hq/lk-jwt-service
[livekit-server-github]: https://github.com/livekit/livekit
Other:
- [Matrix VOIP and LiveKit][sspaeth-matrix-voip] - Community member deep dive on current VOIP solutions on Matrix
[sspaeth-matrix-voip]: https://sspaeth.de/2026/04/matrix-voip-and-livekit/
## Appendix
### Additional TURNS-over-443 configuration
In most situations, LiveKit [does not need TURN][sspaeth-matrix-voip-turn] to function. However, when clients are in very restrictive networks where UDP traffic and non-standard ports are disallowed, a TURN-over-TLS server on port `:443` could be employed to relay traffic.
[sspaeth-matrix-voip-turn]: https://sspaeth.de/2026/04/matrix-voip-and-livekit/#turn
You can either use LiveKit's built-in TURN server, or integrate LiveKit with [coturn](./turn).
<details>
<summary>Using LiveKit's built-in TURN server</summary>
First, set up LiveKit's built-in TURN server with its own domain - we'll use `livekit-turn.example.com` in our example.
```yaml
## add this to `livekit.yaml` ##
turn:
enabled: true
tls_port: 5349
relay_range_start: 50300
relay_range_end: 50400
domain: livekit-turn.example.com
# replace these with your actual cert/key files
cert_file: /path/to/livekit-turn.example.com.crt
key_file: /path/to/livekit-turn.example.com.key
```
```yaml
### add these to livekit's docker-compose ###
ports:
- "127.0.0.1:5349:5349/tcp"
- "50300-50400:50300-50400/udp"
### if you're using `network_mode: host`, you can skip this part
```
Recreate the LiveKit container (with `docker-compose up -d livekit`) to apply these changes. Remember to allow the new `50300:50400/udp` ports through your firewall.
Then, we will configure a route from port 443 of the host back to our `livekit-turn.example.com` service on port 5349. To both **multiplex** this and LiveKit's websocket on the same port, we will use a layer-4 reverse proxy with **SNI routing** capabilities, such as [caddy-l4][caddy-l4].
```
## in your Caddyfile ##
{
servers {
listener_wrappers {
# intercept packets meant for the TURN domain first
# before forwarding other packets to "normal" HTTP listeners
layer4 {
@turn {
tls {
sni livekit-turn.example.com
}
}
route @turn {
proxy {
upstream 127.0.0.1:5349 # forward to normal TURNS port
}
}
}
tls
}
}
}
# livekit stuff
https://livekit.example.com {
@lk-jwt-service path /healthz /get_token /sfu/get
route @lk-jwt-service {
reverse_proxy 127.0.0.1:8081
}
reverse_proxy http://127.0.0.1:7880
}
```
[caddy-l4]: https://github.com/mholt/caddy-l4
</details>
<details>
<summary>Using an external TURN server</summary>
Before continuing, make sure coturn's `min-port` and `max-port` do not overlap with LiveKit's port range:
```ini
# in your coturn.conf
min-port=50201
max-port=65535
```
Then, generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want, so set a different one for LiveKit to use.
After that, refer to the following [**TURN instructions**](./turn#turns-over-443) to set up coturn with TLS, as well as multiplexing with LiveKit's websocket on port 443.
Then configure LiveKit, making sure to replace `COTURN_SECRET` with the one you generated:
```yaml
# livekit.yaml
rtc:
turn_servers:
- host: coturn.example.com
port: 443
protocol: tls
secret: "COTURN_SECRET"
```
Restart LiveKit, coturn, and Caddy-l4 to apply these changes.
</details>
After finishing configuration, you can run the Testing steps again to check that TURN-over-TLS is working. In the LiveKit connection test page, there should be a green tick saying "Can connect to TURN".