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

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!

Generating the secrets

LiveKit provides a utility to generate secure random keys

~$ docker run --rm livekit/livekit-server:latest generate-keys
API Key:  APIUxUnMnSkuFWV
API Secret:  t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A

Create a docker-compose.yml file as following:

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:

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

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. Clients discover these via the /_matrix/client/v1/rtc/transports endpoint (MSC4143).

[global.matrix_rtc]
foci = [
    { type = "livekit", livekit_service_url = "https://livekit.example.com" },
]

Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.

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
Example caddy config
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
}
Example nginx config
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;
}
Example traefik router
# 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`))

6. Start Everything

Start up the services using your usual method - for example docker compose up -d.

Additional TURN configuration

Using LiveKit's built-in TURN server

LiveKit includes a built-in TURN server which can be used in place of an external option. This TURN server will only work with LiveKit, so you can't use it for legacy Matrix calling or anything else.

If you don't want to set up a separate TURN server, you can enable this with the following changes:

### add this to livekit.yaml ###
turn:
  enabled: true
  udp_port: 3478
  relay_range_start: 50300
  relay_range_end: 50400
  domain: livekit.example.com
### add these to livekit's docker-compose ###
ports:
  - "3478:3478/udp"
  - "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 3478/udp and 50100:50200/udp ports through your firewall.

Integration with an external TURN server

If you've already set up coturn, you can configure Livekit to use it.

Avoid port clashes between the two services

Before continuing, make sure coturn's min-port and max-port do not overlap with LiveKit's port range:

# in your coturn.conf
min-port=50201
max-port=65535

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.

Then configure LiveKit, making sure to replace COTURN_SECRET with the one you generated:

# livekit.yaml
rtc:
  turn_servers:
    - host: coturn.example.com
      port: 3478
      protocol: udp
      secret: "COTURN_SECRET"
    - host: coturn.example.com
      port: 3478
      protocol: tcp
      secret: "COTURN_SECRET"
    - host: coturn.example.com
      port: 5349
      protocol: tls # Only if you have already set up TLS in your coturn
      secret: "COTURN_SECRET"

Restart LiveKit and coturn to apply these changes.

Testing

To test that LiveKit is successfully integrated with Continuwuity, you will need to replicate its Token Exchange Flow.

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.

Then, using that token, request another OpenID token for use with the lk-jwt-service:

~$ curl -X POST -H "Authorization: Bearer <session-access-token>" \
  https://matrix.example.com/_matrix/client/v3/user/@user:example.com/openid/request_token
{"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:

payload.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.

You can then send this payload to the lk-jwt-service:

~$ curl -X POST -d @payload.json https://livekit.example.com/get_token
{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}

The lk-jwt-service will, after checking against Continuwuity, answer with a jwt token to create a LiveKit media room. Use this token to test at the LiveKit Connection Tester. 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 them 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

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.

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 localhost address:

    # 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.

After implementing the changes and restarting your compose, you can test whether the connection works by cURLing from a sidecar container:

~$ docker run --rm --net container:lk-jwt-service docker.io/curlimages/curl https://livekit.example.com
OK

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 via federation paths.

As a workaround, you can enable federation, but forbid all remote servers via the following config parameters:

### in your continuwuity.toml file ###
allow_federation = true
forbidden_remote_server_names = [".*"]

Subscribe to issue !1440 for future updates on this matter.

Guides:

Specifications:

Source code: