Compare commits

..

28 Commits

Author SHA1 Message Date
stratself 90b019e36b fix(docs): Remove trailing whitespace 2026-03-07 08:23:57 +00:00
stratself a8872454a9 docs(livekit): various mini-clarifications and edits
* specify that the added ports belong to livekit's container in
  TURN section, and remind firewall rules for them
* prioritize the network_mode: host workaround
* add docker livelogs instructions
* use bash for code blocks instead of console
* some other small fixes
2026-03-06 17:29:43 +00:00
stratself 0ee9b23055 fix(docs): use docker run instead of exec for a livekit troubleshooting command 2026-03-05 12:42:58 +00:00
stratself 7f585a11c6 fix(docs): further apply fixes from feedback for livekit documentation 2026-03-05 03:46:21 +00:00
ginger 0e0bd29408 chore: Trim trailing whitespace
Signed-off-by: Ellis Git <forgejo@mail.ellis.link>
2026-03-04 14:31:48 +00:00
stratself f95419ef59 fix(docs): little nits for livekit's troubleshooting section 2026-03-04 08:54:25 +00:00
stratself 5502e05a59 fix(docs): apply clarity fixes for livekit testing from feedbacks
* clearer wording and ordering on client token versus openid token
* provide outputs for curl examples
2026-03-04 08:48:41 +00:00
stratself 6f100b73ce docs(livekit): new troubleshooting section and other small changes
* add link to matrix-rtc room
* include livekit key-secret pair examples for clarity with livekit.yaml
* troubleshooting: add common EC errors and docker networking subsections
* fix a merge conflict issue
2026-03-03 19:48:40 +00:00
stratself bc13cf0f07 Merge branch 'stratself/docs-livekit-fixes' of https://forgejo.ellis.link/stratself/continuwuity into stratself/docs-livekit-fixes 2026-02-27 19:33:37 +00:00
stratself aa1fcade9d docs: add caveat for deployment with non-federated instances 2026-02-24 14:25:26 +00:00
stratself 38ed6fed50 docs: apply changes from feedback
turn all the things into LiveKit
2026-02-24 14:25:24 +00:00
stratself 44de953c1d docs: add livekit testing instructions against new /get_token endpoint 2026-02-24 14:24:41 +00:00
stratself cee26e6fce docs: add caveat for deployment with non-federated instances 2026-02-24 14:13:29 +00:00
stratself e7ba81f2ed docs: apply changes from feedback
turn all the things into LiveKit
2026-02-24 14:13:29 +00:00
stratself a2105610a6 docs: add livekit testing instructions against new /get_token endpoint 2026-02-24 14:13:29 +00:00
stratself 986db449a1 docs: specify both inbuilt + external options for livekit TURN in calls page 2026-02-24 14:13:29 +00:00
stratself e928aa0bf7 docs: replace personal links and small fixes in docs for Livekit TURN 2026-02-24 14:13:29 +00:00
stratself 940b460d25 docs: move Livekit's inbuilt TURN guide to top
The purpose is to simplify new deployments, which are more likely
to use Livekit-only calls. This also makes docs flow a bit better
2026-02-24 14:13:29 +00:00
stratself a6f6ca7a49 docs: rework Related Documentation section for livekit page
* separate links into categories in order of importance: guides, specs, source codes
* add short description to included community guides
* add Element Call, lk-jwt-service, and the livekit MSCs too
2026-02-24 14:13:29 +00:00
stratself 0325f4d0a5 docs: replaces all instances of matrix-rtc to livekit to match rest of page 2026-02-24 14:13:29 +00:00
stratself 2f1d4ecdbc docs: add caveat for deployment with non-federated instances 2026-02-22 22:18:39 +00:00
stratself 5244c55bf9 docs: apply changes from feedback
turn all the things into LiveKit
2026-02-21 16:53:19 +00:00
stratself dece29f19f docs: add livekit testing instructions against new /get_token endpoint 2026-02-21 04:02:57 +00:00
stratself 38fb955226 docs: specify both inbuilt + external options for livekit TURN in calls page 2026-02-21 03:07:23 +00:00
stratself a462db2f6c docs: replace personal links and small fixes in docs for Livekit TURN 2026-02-21 03:03:30 +00:00
stratself 98a7211015 docs: move Livekit's inbuilt TURN guide to top
The purpose is to simplify new deployments, which are more likely
to use Livekit-only calls. This also makes docs flow a bit better
2026-02-21 02:58:12 +00:00
stratself a2a5658d2c docs: rework Related Documentation section for livekit page
* separate links into categories in order of importance: guides, specs, source codes
* add short description to included community guides
* add Element Call, lk-jwt-service, and the livekit MSCs too
2026-02-21 02:43:05 +00:00
stratself 937d9284d5 docs: replaces all instances of matrix-rtc to livekit to match rest of page 2026-02-21 02:30:39 +00:00
20 changed files with 206 additions and 390 deletions
Generated
+18 -17
View File
@@ -445,14 +445,13 @@ dependencies = [
[[package]]
name = "axum-extra"
version = "0.12.5"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
dependencies = [
"axum",
"axum-core",
"bytes",
"futures-core",
"futures-util",
"headers",
"http",
@@ -460,6 +459,8 @@ dependencies = [
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde_core",
"tower-layer",
"tower-service",
"tracing",
@@ -1221,7 +1222,7 @@ dependencies = [
[[package]]
name = "continuwuity-admin-api"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"ruma-common",
"serde",
@@ -1600,7 +1601,7 @@ dependencies = [
[[package]]
name = "draupnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"ruma-common",
"serde",
@@ -3002,7 +3003,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "meowlnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"ruma-common",
"serde",
@@ -4094,7 +4095,7 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"assign",
"continuwuity-admin-api",
@@ -4117,7 +4118,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"js_int",
"ruma-common",
@@ -4129,7 +4130,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"as_variant",
"assign",
@@ -4152,7 +4153,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -4184,7 +4185,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"as_variant",
"indexmap",
@@ -4209,7 +4210,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"bytes",
"headers",
@@ -4231,7 +4232,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"js_int",
"thiserror 2.0.18",
@@ -4240,7 +4241,7 @@ dependencies = [
[[package]]
name = "ruma-identity-service-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"js_int",
"ruma-common",
@@ -4250,7 +4251,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -4265,7 +4266,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"js_int",
"ruma-common",
@@ -4277,7 +4278,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf#a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
+2 -4
View File
@@ -97,7 +97,7 @@ features = [
]
[workspace.dependencies.axum-extra]
version = "0.12.0"
version = "0.10.1"
default-features = false
features = ["typed-header", "tracing"]
@@ -343,7 +343,7 @@ version = "0.1.2"
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes"
rev = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
rev = "a4b5e89f4951c87e903ef224c2d1c2c4ef1ebeaf"
features = [
"compat",
"rand",
@@ -363,7 +363,6 @@ features = [
"unstable-msc2870",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3814",
"unstable-msc3245",
"unstable-msc3266",
"unstable-msc3381", # polls
@@ -382,7 +381,6 @@ features = [
"unstable-pdu",
"unstable-msc4155",
"unstable-msc4143", # livekit well_known response
"unstable-msc4284"
]
[workspace.dependencies.rust-rocksdb]
-1
View File
@@ -1 +0,0 @@
Added MSC3814 Dehydrated Devices - you can now decrypt messages sent while all devices were logged out.
-1
View File
@@ -1 +0,0 @@
BREAKING: Added an entrypoint to the Docker image. This means you no longer need to specify the binary when running a command using the image. Contributed by @Jade
+2 -8
View File
@@ -180,11 +180,6 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
export RUSTFLAGS="${RUSTFLAGS}"
fi
RUST_PROFILE_DIR="${RUST_PROFILE}"
if [[ "${RUST_PROFILE}" == "dev" ]]; then
RUST_PROFILE_DIR="debug"
fi
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
jq -r ".target_directory"))
mkdir /out/sbin
@@ -196,8 +191,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
for BINARY in "${BINARIES[@]}"; do
echo $BINARY
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY /out/sbin/$BINARY
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY /out/sbin/$BINARY
done
EOF
@@ -281,5 +276,4 @@ ENV LD_LIBRARY_PATH=/usr/lib
# Continuwuity default port
EXPOSE 8008
ENTRYPOINT [ "/sbin/conduwuit" ]
CMD ["/sbin/conduwuit"]
+1 -1
View File
@@ -10,4 +10,4 @@ # Calls
For either one to work correctly, you have to do some additional setup.
- For legacy calls to work, you need to set up a TURN/STUN server. [Read the TURN guide for tips on how to set up coturn](./calls/turn.mdx)
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability, so you might want to configure your TURN server first. [Read the LiveKit guide](./calls/livekit.mdx)
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability - you can setup its inbuilt TURN server, or integrate with an existing one. [Read the LiveKit guide](./calls/livekit.mdx)
+168 -53
View File
@@ -4,6 +4,10 @@ # Matrix RTC/Element Call Setup
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
:::
:::tip
You can find help setting up Matrix RTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
:::
## Instructions
### 1. Domain
@@ -14,17 +18,20 @@ ### 1. Domain
### 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.
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
~$ docker run --rm livekit/livekit-server:latest generate-keys
API Key: APIUxUnMnSkuFWV
API Secret: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
```
:::
```yaml
services:
lk-jwt-service:
@@ -70,6 +77,8 @@ # - "50100-50200:50100-50200/udp"
enable_loopback_candidate: false
keys:
LK_MATRIX_KEY: LK_MATRIX_SECRET
# replace these with your key-secret pair. Example:
# APIUxUnMnSkuFWV: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
```
#### Firewall hints
@@ -95,7 +104,7 @@ ### 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.
By default, all routes should be forwarded to Livekit with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
By default, all routes should be forwarded to LiveKit with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
- `/sfu/get`
- `/healthz`
@@ -104,7 +113,7 @@ ### 4. Configure your Reverse Proxy
<details>
<summary>Example caddy config</summary>
```
matrix-rtc.example.com {
livekit.example.com {
# for lk-jwt-service
@lk-jwt-service path /sfu/get* /healthz* /get_token*
@@ -122,7 +131,7 @@ ### 4. Configure your Reverse Proxy
<summary>Example nginx config</summary>
```
server {
server_name matrix-rtc.example.com;
server_name livekit.example.com;
# for lk-jwt-service
location ~ ^/(sfu/get|healthz|get_token) {
@@ -133,7 +142,7 @@ ### 4. Configure your Reverse Proxy
proxy_buffering off;
}
# for livekit
# for LiveKit
location / {
proxy_pass http://127.0.0.1:7880$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
@@ -173,44 +182,11 @@ ### 6. Start Everything
Start up the services using your usual method - for example `docker compose up -d`.
## Additional Configuration
## Additional TURN configuration
### TURN Integration
### Using LiveKit's built in TURN server
If you've already set up coturn, there may be a port clash between the two services. To fix this, make sure the `min-port` and `max-port` for coturn so it doesn't overlap with LiveKit's range:
```ini
min-port=50201
max-port=65535
```
To improve LiveKit's reliability, you can configure it to use your coturn server.
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 each thing using your TURN server.
Then configure livekit, making sure to replace `COTURN_SECRET`:
```yaml
# livekit.yaml
rtc:
turn_servers:
- host: coturn.ellis.link
port: 3478
protocol: tcp
secret: "COTURN_SECRET"
- host: coturn.ellis.link
port: 5349
protocol: tls # Only if you've set up TLS in your coturn
secret: "COTURN_SECRET"
- host: coturn.ellis.link
port: 3478
protocol: udp
secret: "COTURN_SECRET"
```
## 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.
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:
@@ -221,20 +197,159 @@ ### add this to livekit.yaml ###
udp_port: 3478
relay_range_start: 50300
relay_range_end: 50400
domain: matrix-rtc.example.com
domain: livekit.example.com
```
```yaml
### Add these to docker-compose ###
- "3478:3478/udp"
- "50300-50400:50300-50400/udp"
### 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
```
### Related Documentation
Remember to allow the new `3478/udp` and `50100:50200/udp` ports through your firewall.
- [LiveKit GitHub](https://github.com/livekit/livekit)
- [LiveKit Connection Tester](https://livekit.io/connection-test) - use with the token returned by `/sfu/get` or `/get_token`
- [MatrixRTC proposal](https://half-shot.github.io/msc-crafter/#msc/4143)
- [Synapse documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
- [Community guide](https://tomfos.tr/matrix/livekit/)
- [Community guide](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
### Integration with an external TURN server
If you've already set up coturn, there may be a port clash between the two services. To fix this, make sure coturn's `min-port` and `max-port` do not overlap with LiveKit's range:
```ini
min-port=50201
max-port=65535
```
To improve LiveKit's reliability, you can configure it to use your coturn server.
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 each thing using your TURN server.
Then configure LiveKit, making sure to replace `COTURN_SECRET` with the ones you generated:
```yaml
# livekit.yaml
rtc:
turn_servers:
- host: coturn.example.com
port: 3478
protocol: tcp
secret: "COTURN_SECRET"
- host: coturn.example.com
port: 5349
protocol: tls # Only if you've set up TLS in your coturn
secret: "COTURN_SECRET"
- host: coturn.example.com
port: 3478
protocol: udp
secret: "COTURN_SECRET"
```
## 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).
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, 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
{"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
{"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](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 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:
```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, you can test whether the connection works by cURLing from a sidecar container:
```bash
~$ 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 disabled federation (`enable_registration = 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. You can find a workaround for this limitation [here](https://forgejo.ellis.link/continuwuation/continuwuity/issues/1440).
## Related Documentation
Guides:
- [Element Call self-hosting documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
- [Community guide with overview of LiveKit's mechanisms](https://tomfos.tr/matrix/livekit/)
- [Community guide using systemd](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
Specifications:
- [MatrixRTC proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
- [LiveKit proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
Source code:
- [Element Call](https://github.com/element-hq/element-call)
- [lk-jwt-service](https://github.com/element-hq/lk-jwt-service)
- [LiveKit server](https://github.com/livekit/livekit)
+7 -1
View File
@@ -9,7 +9,7 @@
},
events::{
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
RoomAccountDataEventType,
GlobalAccountDataEventType, RoomAccountDataEventType,
},
serde::Raw,
};
@@ -126,6 +126,12 @@ async fn set_account_data(
)));
}
if event_type_s == GlobalAccountDataEventType::PushRules.to_cow_str() {
return Err!(Request(BadJson(
"This endpoint cannot be used for setting/configuring push rules."
)));
}
let data: serde_json::Value = serde_json::from_str(data.get())
.map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?;
-121
View File
@@ -1,121 +0,0 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, at};
use futures::StreamExt;
use ruma::api::client::dehydrated_device::{
delete_dehydrated_device::unstable as delete_dehydrated_device,
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
put_dehydrated_device::unstable as put_dehydrated_device,
};
use crate::Ruma;
const MAX_BATCH_EVENTS: usize = 50;
/// # `PUT /_matrix/client/../dehydrated_device`
///
/// Creates or overwrites the user's dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn put_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let sender_user = body
.sender_user
.as_deref()
.expect("AccessToken authentication required");
let device_id = body.body.device_id.clone();
services
.users
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response { device_id })
}
/// # `DELETE /_matrix/client/../dehydrated_device`
///
/// Deletes the user's dehydrated device without replacement.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn delete_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device_id = services.users.get_dehydrated_device_id(sender_user).await?;
services.users.remove_device(sender_user, &device_id).await;
Ok(delete_dehydrated_device::Response { device_id })
}
/// # `GET /_matrix/client/../dehydrated_device`
///
/// Gets the user's dehydrated device
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device = services.users.get_dehydrated_device(sender_user).await?;
Ok(get_dehydrated_device::Response {
device_id: device.device_id,
device_data: device.device_data,
})
}
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
///
/// Paginates the events of the dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_events_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.sender_user();
let device_id = &body.body.device_id;
let existing_id = services.users.get_dehydrated_device_id(sender_user).await;
if existing_id.as_ref().is_err()
|| existing_id
.as_ref()
.is_ok_and(|existing_id| existing_id != device_id)
{
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
}
let since: Option<u64> = body
.body
.next_batch
.as_deref()
.map(str::parse)
.transpose()?;
let mut next_batch: Option<u64> = None;
let events = services
.users
.get_to_device_events(sender_user, device_id, since, None)
.take(MAX_BATCH_EVENTS)
.inspect(|&(count, _)| {
next_batch.replace(count);
})
.map(at!(1))
.collect()
.await;
Ok(get_events::Response {
events,
next_batch: next_batch.as_ref().map(ToString::to_string),
})
}
-2
View File
@@ -6,7 +6,6 @@
pub(super) mod backup;
pub(super) mod capabilities;
pub(super) mod context;
pub(super) mod dehydrated_device;
pub(super) mod device;
pub(super) mod directory;
pub(super) mod filter;
@@ -50,7 +49,6 @@
pub(super) use backup::*;
pub(super) use capabilities::*;
pub(super) use context::*;
pub(super) use dehydrated_device::*;
pub(super) use device::*;
pub(super) use directory::*;
pub(super) use filter::*;
+1 -2
View File
@@ -11,7 +11,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Result, at, extract_variant,
Result, extract_variant,
utils::{
ReadyExt, TryFutureExtExt,
stream::{BroadbandExt, Tools, WidebandExt},
@@ -385,7 +385,6 @@ pub(crate) async fn build_sync_events(
last_sync_end_count,
Some(current_count),
)
.map(at!(1))
.collect::<Vec<_>>();
let device_one_time_keys_count = services
-1
View File
@@ -1029,7 +1029,6 @@ async fn collect_to_device(
events: services
.users
.get_to_device_events(sender_user, sender_device, None, Some(next_batch))
.map(at!(1))
.collect()
.await,
})
-1
View File
@@ -50,7 +50,6 @@ pub(crate) async fn get_supported_versions_route(
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
-4
View File
@@ -160,10 +160,6 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::update_device_route)
.ruma_route(&client::delete_device_route)
.ruma_route(&client::delete_devices_route)
.ruma_route(&client::put_dehydrated_device_route)
.ruma_route(&client::delete_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_events_route)
.ruma_route(&client::get_tags_route)
.ruma_route(&client::update_tag_route)
.ruma_route(&client::delete_tag_route)
-4
View File
@@ -362,10 +362,6 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "userid_blurhash",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_dehydrateddevice",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_devicelistversion",
..descriptor::RANDOM_SMALL
@@ -63,9 +63,7 @@ pub(super) async fn fetch_state<Pdu>(
},
| hash_map::Entry::Occupied(_) => {
return Err!(Database(
"State event's type and state_key combination exists multiple times: {}, {}",
pdu.kind(),
state_key
"State event's type and state_key combination exists multiple times.",
));
},
}
@@ -162,9 +162,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
},
| hash_map::Entry::Occupied(_) => {
return Err!(Request(InvalidParam(
"Auth event's type and state_key combination exists multiple times: {}, {}",
auth_event.kind,
auth_event.state_key().unwrap_or("")
"Auth event's type and state_key combination exists multiple times.",
)));
},
}
+2 -2
View File
@@ -10,7 +10,7 @@
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use conduwuit_core::{
Error, Event, Result, at, debug, err, error,
Error, Event, Result, debug, err, error,
result::LogErr,
trace,
utils::{
@@ -175,7 +175,7 @@ async fn handle_response_ok<'a>(
if !new_events.is_empty() {
self.db.mark_as_active(new_events.iter());
let new_events_vec = new_events.into_iter().map(at!(1)).collect();
let new_events_vec = new_events.into_iter().map(|(_, event)| event).collect();
futures.push(self.send_events(dest.clone(), new_events_vec));
} else {
statuses.remove(dest);
-149
View File
@@ -1,149 +0,0 @@
use conduwuit::{Err, Result, implement, trace};
use conduwuit_database::{Deserialized, Json};
use ruma::{
DeviceId, OwnedDeviceId, UserId,
api::client::dehydrated_device::{
DehydratedDeviceData, put_dehydrated_device::unstable::Request,
},
serde::Raw,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DehydratedDevice {
/// Unique ID of the device.
pub device_id: OwnedDeviceId,
/// Contains serialized and encrypted private data.
pub device_data: Raw<DehydratedDeviceData>,
}
/// Creates or recreates the user's dehydrated device.
#[implement(super::Service)]
#[tracing::instrument(
level = "info",
skip_all,
fields(
%user_id,
device_id = %request.device_id,
display_name = ?request.initial_device_display_name,
)
)]
pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> Result {
assert!(
self.exists(user_id).await,
"Tried to create dehydrated device for non-existent user"
);
let existing_id = self.get_dehydrated_device_id(user_id).await;
if existing_id.is_err()
&& self
.get_device_metadata(user_id, &request.device_id)
.await
.is_ok()
{
return Err!("A hydrated device already exists with that ID.");
}
if let Ok(existing_id) = existing_id {
self.remove_device(user_id, &existing_id).await;
}
self.create_device(
user_id,
&request.device_id,
"",
request.initial_device_display_name.clone(),
None,
)
.await?;
trace!(device_data = ?request.device_data);
self.db.userid_dehydrateddevice.raw_put(
user_id,
Json(&DehydratedDevice {
device_id: request.device_id.clone(),
device_data: request.device_data,
}),
);
trace!(device_keys = ?request.device_keys);
self.add_device_keys(user_id, &request.device_id, &request.device_keys)
.await;
trace!(one_time_keys = ?request.one_time_keys);
for (one_time_key_key, one_time_key_value) in &request.one_time_keys {
self.add_one_time_key(user_id, &request.device_id, one_time_key_key, one_time_key_value)
.await?;
}
Ok(())
}
/// Removes a user's dehydrated device.
///
/// Calling this directly will remove the dehydrated data but leak the frontage
/// device. Thus this is called by the regular device interface such that the
/// dehydrated data will not leak instead.
///
/// If device_id is given, the user's dehydrated device must match or this is a
/// no-op, but an Err is still returned to indicate that. Otherwise returns the
/// removed dehydrated device_id.
#[implement(super::Service)]
#[tracing::instrument(
level = "debug",
skip_all,
fields(
%user_id,
device_id = ?maybe_device_id,
)
)]
pub(super) async fn remove_dehydrated_device(
&self,
user_id: &UserId,
maybe_device_id: Option<&DeviceId>,
) -> Result<OwnedDeviceId> {
let Ok(device_id) = self.get_dehydrated_device_id(user_id).await else {
return Err!(Request(NotFound("No dehydrated device for this user.")));
};
if let Some(maybe_device_id) = maybe_device_id {
if maybe_device_id != device_id {
return Err!(Request(NotFound("Not the user's dehydrated device.")));
}
}
self.db.userid_dehydrateddevice.remove(user_id);
Ok(device_id)
}
/// Get the device_id of the user's dehydrated device.
#[implement(super::Service)]
#[tracing::instrument(
level = "debug",
skip_all,
fields(%user_id)
)]
pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result<OwnedDeviceId> {
self.get_dehydrated_device(user_id)
.await
.map(|device| device.device_id)
}
/// Get the dehydrated device private data
#[implement(super::Service)]
#[tracing::instrument(
level = "debug",
skip_all,
fields(%user_id),
ret,
)]
pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
self.db
.userid_dehydrateddevice
.get(user_id)
.await
.deserialized()
}
+3 -12
View File
@@ -1,5 +1,3 @@
pub(super) mod dehydrated_device;
#[cfg(feature = "ldap")]
use std::collections::HashMap;
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
@@ -7,7 +5,7 @@
#[cfg(feature = "ldap")]
use conduwuit::result::LogErr;
use conduwuit::{
Err, Error, Result, Server, debug_warn, err, is_equal_to, trace,
Err, Error, Result, Server, at, debug_warn, err, is_equal_to, trace,
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
};
#[cfg(feature = "ldap")]
@@ -72,7 +70,6 @@ struct Data {
userfilterid_filter: Arc<Map>,
userid_avatarurl: Arc<Map>,
userid_blurhash: Arc<Map>,
userid_dehydrateddevice: Arc<Map>,
userid_devicelistversion: Arc<Map>,
userid_displayname: Arc<Map>,
userid_lastonetimekeyupdate: Arc<Map>,
@@ -113,7 +110,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
userfilterid_filter: args.db["userfilterid_filter"].clone(),
userid_avatarurl: args.db["userid_avatarurl"].clone(),
userid_blurhash: args.db["userid_blurhash"].clone(),
userid_dehydrateddevice: args.db["userid_dehydrateddevice"].clone(),
userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
userid_displayname: args.db["userid_displayname"].clone(),
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
@@ -484,11 +480,6 @@ pub async fn create_device(
/// Removes a device from a user.
pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) {
// Remove dehydrated device if this is the dehydrated device
let _: Result<_> = self
.remove_dehydrated_device(user_id, Some(device_id))
.await;
let userdeviceid = (user_id, device_id);
// Remove tokens
@@ -1012,7 +1003,7 @@ pub fn get_to_device_events<'a>(
device_id: &'a DeviceId,
since: Option<u64>,
to: Option<u64>,
) -> impl Stream<Item = (u64, Raw<AnyToDeviceEvent>)> + Send + 'a {
) -> impl Stream<Item = Raw<AnyToDeviceEvent>> + Send + 'a {
type Key<'a> = (&'a UserId, &'a DeviceId, u64);
let from = (user_id, device_id, since.map_or(0, |since| since.saturating_add(1)));
@@ -1026,7 +1017,7 @@ pub fn get_to_device_events<'a>(
&& device_id == *device_id_
&& to.is_none_or(|to| *count <= to)
})
.map(|((_, _, count), event)| (count, event))
.map(at!(1))
}
pub async fn remove_to_device_events<Until>(