MatrixRTC/Element Call Setup

This guide assumes that you are using docker compose for deployment.

Tip

You can find help setting up MatrixRTC in our dedicated room - #matrixrtc:continuwuity.org

Instructions

1. Set up your 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. Set up the LiveKit services

Using LiveKit with Matrix requires two services - LiveKit itself, and a brokering 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

# do not create rooms by default
room:
  auto_create: false

Consult config-sample.yaml for all LiveKit options.

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 your lk-jwt-service's address to the foci field of the [global.matrix_rtc] section of your Continuwuity config file.

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:

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

If you configure Continuwuity via environment variables, use the following:

CONTINUWUITY_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
Caddy (on host)
Caddy (in container)
Caddy (via Docker labels)
Traefik (via Docker labels)
nginx
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
}

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. Follow the steps below while checking Docker logs (docker compose logs --follow), in order to help 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.

Then, using that token, fetch the discovery endpoints for MatrixRTC services:

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.

With the same 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

You will see a response as below:

{"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

The lk-jwt-service will, after checking against Continuwuity, answer with a jwt token to create a LiveKit media room:

{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}

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 these logs in real-time.

Common errors in Element Call UI

  • MISSING_MATRIX_RTC_FOCUS/MISSING_MATRIX_RTC_TRANSPORT: LiveKit is missing from Continuwuity's config file
  • "Waiting for media" popup always showing for yourself: a LiveKit URL has been configured in Continuwuity, but your client cannot connect to it for some reason
  • OPEN_ID_ERROR: Your client can reach out to lk-jwt-service, but has problems authenticating with it. In this case, check lk-jwt-service logs for more details

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 in bridge networks to connect to their host's public IP by default. This makes lk-jwt-service fail connecting to livekit or continuwuity on the same host, causing refused and timed out connections to appear in the log entries of 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:

docker run --rm --net container:lk-jwt-service docker.io/curlimages/curl https://livekit.example.com
# --- some errors ---

To resolve this, configure an extra_hosts file mapping LiveKit's and Continuwuity's domain names to Docker's custom host loopback address, represented here by host-gateway:

# in docker-compose.yaml
services:
  lk-jwt-service:
    ...
+    extra_hosts:
+     - "livekit.example.com:host-gateway"
+     - "matrix.example.com:host-gateway"

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.

For more information about the host-gateway parameter, kindly refer to the --add-host documentation for Docker and Podman. Should you have further difficulties, feel free to enquire in the support rooms.

Incorrect IP address for LiveKit

By default, LiveKit automatically finds its public IP address(es), which can be seen in the "Establishing WebRTC connection" section of the connection test page. If these IPs are incorrect, you may want to hardcode your own IP by doing the following:

### in your livekit.yaml ###
rtc:
  # ... other configs here ...
- use_external_ip: true
+ use_external_ip: false
+ node_ip: "1.2.3.4"

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:

Configurations:

Specifications:

Source code:

Other:

Appendix

Additional TURN-over-TLS on 443 configuration

Most of the time, LiveKit does not need TURN to function. However, there are situations where clients may be unable to use nonstandard ports or UDP connections, such as in highly restrictive networks. In such cases, a TURN-over-TLS server on port 443 could relay traffic for the clients.

You can either use LiveKit's built-in TURN server, or integrate LiveKit with coturn.

Using LiveKit's built-in TURN server

First, set up LiveKit's built-in TURN server with its own domain - this example will use livekit-turn.example.com in our example.

## add this to `livekit.yaml` ##
turn:
  enabled: true

  # Note: LiveKit will always advertise its TURN-over-TLS port on 443,
  # so further load balancing is needed to route back from port 443
  # on the host to the port being configured here
  tls_port: 5349

  # Optional: configure an extra UDP port
  # udp_port: 3478

  relay_range_start: 50300
  relay_range_end: 50400

  domain: livekit-turn.example.com

  # replace these with your actual certificate and key files
  cert_file: /path/to/livekit-turn.example.com.crt
  key_file: /path/to/livekit-turn.example.com.key
### add these ports to livekit's docker-compose.yml ###
### if you're using `network_mode: host`, you can skip this part
ports:
  - "127.0.0.1:5349:5349/tcp"
  - "50300-50400:50300-50400/udp"
  # "3478:3478/udp" # (optional UDP port)

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, configure a route from port 443 on the host back to the livekit-turn.example.com service on port 5349. To multiplex the service and LiveKit's WebSocket on the same port, use a layer 4 reverse proxy with SNI routing capabilities, such as Caddy-L4, on the host system.

## 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 {
                    # forward packages to LiveKit's TURN-over-TLS port
                    proxy 127.0.0.1:5349
                    }
                }
            }
            tls
        }
    }
}

# Configuration for the LiveKit modules
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
}
Using an external TURN server (coturn)

First, 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

Then, generate a long random TURN secret for LiveKit, and add it to your coturn config under the static-auth-secret option. coturn allows multiple secrets in its configuration, so set a different one for LiveKit to use.

After that, refer to the following TURN-over-TLS on 443 instructions 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:

### in your livekit.yaml ###
rtc:
  # ... other configs here ...
  turn_servers:
    - host: coturn.example.com
      port: 443
      protocol: tls
      secret: "COTURN_SECRET"

Restart LiveKit, coturn, and Caddy-L4 to apply these changes.

Once the services are configured, run the Testing steps again to check that TURN-over-TLS on 443 is working. There should now be a green tick in the LiveKit connection test page showing "Can connect to TURN" to confirm this is set up successfully.