Group voice and video calls with MatrixRTC and LiveKit
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
Every call in the web client now goes through MatrixRTC with a LiveKit
SFU, the model Element Call and Element X use; the legacy 1:1 m.call.*
calls are gone. In a room of two the other person is rung and can answer
or decline; in a bigger room a banner offers to join the call. Calls show
a grid of participant tiles with mute, camera, screen sharing and leave,
and media is end-to-end encrypted in encrypted rooms.
Server:
- MSC4140 delayed events (send/state with a delay, plus the
delayed_events list/restart/cancel/send API), stored and scheduled, so
a member whose client drops off is taken out of the call.
- POST /user/{userId}/openid/request_token, which lk-jwt-service uses to
hand out LiveKit tokens.
- org.matrix.msc4143.rtc_foci in /.well-known/matrix/client, configured
under client_api.rtc.
- Room presets let members send call membership state.
- Migration 0014 for both SQL dialects.
Deploy: LiveKit and lk-jwt-service in compose (calls.yml) and the helm
chart. Docs: docs/calls.md, configuration and deployment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
325431a5be
commit
76dfc98a86
49 files changed
+3849
-432
No files matched your search
@@ -0,0 +1,117 @@
|
||||
# Calls for a local Warp: a LiveKit SFU and the LiveKit JWT service
|
||||
# (lk-jwt-service), layered over docker-compose.yml.
|
||||
#
|
||||
# cd deploy/compose
|
||||
# docker compose -f docker-compose.yml -f calls.yml --profile web up -d --build
|
||||
#
|
||||
# Then open http://localhost:8080 in a few browsers (or private windows),
|
||||
# sign in as different users, and press the voice or video call button in
|
||||
# a room: in a room of two it rings the other person.
|
||||
#
|
||||
# How the pieces find each other:
|
||||
#
|
||||
# browser ── /.well-known/matrix/client ──▶ warp rtc_foci: http://localhost:8070
|
||||
# browser ── openid/request_token ────────▶ warp an OpenID token
|
||||
# browser ── POST /sfu/get ───────────────▶ jwt {url, jwt} for the LiveKit room
|
||||
# jwt ───── /_matrix/federation/v1/openid/userinfo ─▶ warp (https://<server_name>:8448)
|
||||
# browser ── ws://localhost:7880 + UDP 7882 ─▶ livekit
|
||||
#
|
||||
# The JWT service checks a token by asking the user's homeserver over the
|
||||
# federation port, with TLS. To make that work with the default server name
|
||||
# "localhost", warp gets a federation listener on :8448 with a self-signed
|
||||
# certificate, and the JWT service and LiveKit share warp's network
|
||||
# namespace, so that "localhost" means warp for all three. This is a local
|
||||
# setup: in production, give each its own hostname and real certificates
|
||||
# (see docs/calls.md).
|
||||
|
||||
services:
|
||||
# Makes the self-signed certificate for warp's federation listener once.
|
||||
calls-cert:
|
||||
image: alpine:3.21
|
||||
restart: "no"
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- >-
|
||||
test -f /tls/tls.crt ||
|
||||
(apk add --no-cache openssl >/dev/null &&
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 3650
|
||||
-subj "/CN=$${WARP_SERVER_NAME}" -addext "subjectAltName=DNS:$${WARP_SERVER_NAME}"
|
||||
-keyout /tls/tls.key -out /tls/tls.crt &&
|
||||
chmod 644 /tls/tls.key /tls/tls.crt)
|
||||
environment:
|
||||
WARP_SERVER_NAME: ${WARP_SERVER_NAME:-localhost}
|
||||
volumes:
|
||||
- warp-tls:/tls
|
||||
|
||||
warp:
|
||||
depends_on:
|
||||
calls-cert:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
WARP_RTC_LIVEKIT_SERVICE_URL: ${WARP_LIVEKIT_JWT_PUBLIC_URL:-http://localhost:8070}
|
||||
WARP_LISTEN_FEDERATION_ADDRESS: ":8448"
|
||||
WARP_TLS_CERT_FILE: /tls/tls.crt
|
||||
WARP_TLS_KEY_FILE: /tls/tls.key
|
||||
volumes:
|
||||
- warp-tls:/tls:ro
|
||||
# LiveKit and the JWT service live in this container's network
|
||||
# namespace, so their ports are published here.
|
||||
ports:
|
||||
- "8070:8070" # lk-jwt-service
|
||||
- "7880:7880" # LiveKit signalling (WebSocket)
|
||||
- "7881:7881" # LiveKit media over TCP, the fallback
|
||||
- "7882:7882/udp" # LiveKit media over UDP
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.13.7
|
||||
restart: unless-stopped
|
||||
network_mode: service:warp
|
||||
depends_on:
|
||||
- warp
|
||||
environment:
|
||||
# node_ip is what LiveKit tells browsers to send media to. 127.0.0.1
|
||||
# works for browsers on this machine; set LIVEKIT_NODE_IP to the
|
||||
# machine's LAN address to call from other devices.
|
||||
LIVEKIT_CONFIG: |
|
||||
port: 7880
|
||||
bind_addresses: [""]
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
udp_port: 7882
|
||||
use_external_ip: false
|
||||
node_ip: ${LIVEKIT_NODE_IP:-127.0.0.1}
|
||||
room:
|
||||
auto_create: false
|
||||
keys:
|
||||
${LIVEKIT_KEY:-warp}: ${LIVEKIT_SECRET:-warp-local-livekit-secret-change-me-0123456789}
|
||||
logging:
|
||||
level: info
|
||||
|
||||
livekit-jwt:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:0.8.0
|
||||
restart: unless-stopped
|
||||
network_mode: service:warp
|
||||
depends_on:
|
||||
- warp
|
||||
- livekit
|
||||
environment:
|
||||
LIVEKIT_JWT_BIND: ":8070"
|
||||
# The URL handed to browsers, and the one the service creates rooms
|
||||
# through: both reach LiveKit at localhost here.
|
||||
LIVEKIT_URL: ${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
|
||||
LIVEKIT_KEY: ${LIVEKIT_KEY:-warp}
|
||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-warp-local-livekit-secret-change-me-0123456789}
|
||||
# Users of this server may create call rooms on the SFU.
|
||||
LIVEKIT_FULL_ACCESS_HOMESERVERS: ${WARP_SERVER_NAME:-localhost}
|
||||
# warp's federation certificate is self-signed.
|
||||
LIVEKIT_INSECURE_SKIP_VERIFY_TLS: YES_I_KNOW_WHAT_I_AM_DOING
|
||||
|
||||
web:
|
||||
environment:
|
||||
# The browser connects to the JWT service and to LiveKit directly.
|
||||
WARP_WEB_CALL_ORIGINS: >-
|
||||
${WARP_LIVEKIT_JWT_PUBLIC_URL:-http://localhost:8070}
|
||||
${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
|
||||
|
||||
volumes:
|
||||
warp-tls:
|
||||
@@ -12,6 +12,12 @@
|
||||
# http://localhost:8009; with the web profile, the web client is on
|
||||
# http://localhost:8080.
|
||||
#
|
||||
# For voice and video calls (LiveKit and its JWT service) add calls.yml:
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f calls.yml --profile web up -d
|
||||
#
|
||||
# WARP_PORT moves the homeserver off host port 8008.
|
||||
#
|
||||
# Create the first administrator once the server is up:
|
||||
#
|
||||
# docker compose exec warp warp create-account --username admin --admin
|
||||
@@ -63,7 +69,7 @@ services:
|
||||
volumes:
|
||||
- warp-data:/data
|
||||
ports:
|
||||
- "8008:8008"
|
||||
- "${WARP_PORT:-8008}:8008"
|
||||
|
||||
# The admin panel is optional. Start it with the admin profile:
|
||||
# docker compose --profile admin up -d
|
||||
|
||||
@@ -65,3 +65,32 @@ app.kubernetes.io/name: {{ printf "%s-admin" (include "warp.name" .) }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: admin
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
The LiveKit JWT service for calls, with its own name label like the
|
||||
admin panel.
|
||||
*/}}
|
||||
{{- define "warp.calls.fullname" -}}
|
||||
{{- printf "%s-livekit-jwt" (include "warp.fullname" . | trunc 51 | trimSuffix "-") -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "warp.calls.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
|
||||
{{ include "warp.calls.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "warp.calls.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ printf "%s-livekit-jwt" (include "warp.name" .) }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: calls
|
||||
{{- end -}}
|
||||
|
||||
{{/* The JWT service's public URL, advertised to clients: explicit, else its ingress. */}}
|
||||
{{- define "warp.calls.serviceURL" -}}
|
||||
{{- if .Values.calls.livekitServiceURL -}}
|
||||
{{- .Values.calls.livekitServiceURL -}}
|
||||
{{- else if and .Values.calls.jwtService.enabled .Values.calls.jwtService.ingress.enabled -}}
|
||||
{{- printf "https://%s" .Values.calls.jwtService.ingress.host -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -97,6 +97,10 @@ spec:
|
||||
value: {{ .Values.tracing.sampleRatio | quote }}
|
||||
- name: WARP_LOG_FORMAT
|
||||
value: json
|
||||
{{- with (include "warp.calls.serviceURL" .) }}
|
||||
- name: WARP_RTC_LIVEKIT_SERVICE_URL
|
||||
value: {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
{{- if .Values.calls.jwtService.enabled }}
|
||||
{{- $jwt := .Values.calls.jwtService }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "warp.calls.fullname" . }}
|
||||
labels:
|
||||
{{- include "warp.calls.labels" . | nindent 4 }}
|
||||
spec:
|
||||
# The service keeps its state in memory: one replica, unless LIVEKIT_REDIS_URL
|
||||
# is set through extraEnv.
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "warp.calls.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "warp.calls.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: lk-jwt-service
|
||||
image: "{{ $jwt.image.repository }}:{{ $jwt.image.tag }}"
|
||||
imagePullPolicy: {{ $jwt.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: LIVEKIT_JWT_BIND
|
||||
value: ":8080"
|
||||
- name: LIVEKIT_URL
|
||||
value: {{ required "calls.jwtService.livekitURL is required" $jwt.livekitURL | quote }}
|
||||
- name: LIVEKIT_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ required "calls.jwtService.existingSecret is required" $jwt.existingSecret }}
|
||||
key: {{ $jwt.keyKey }}
|
||||
- name: LIVEKIT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ $jwt.existingSecret }}
|
||||
key: {{ $jwt.secretKey }}
|
||||
- name: LIVEKIT_FULL_ACCESS_HOMESERVERS
|
||||
value: {{ $jwt.fullAccessHomeservers | default .Values.serverName | quote }}
|
||||
{{- with $jwt.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml $jwt.resources | nindent 12 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "warp.calls.fullname" . }}
|
||||
labels:
|
||||
{{- include "warp.calls.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ $jwt.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ $jwt.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
selector:
|
||||
{{- include "warp.calls.selectorLabels" . | nindent 4 }}
|
||||
{{- if $jwt.ingress.enabled }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "warp.calls.fullname" . }}
|
||||
labels:
|
||||
{{- include "warp.calls.labels" . | nindent 4 }}
|
||||
{{- with $jwt.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with $jwt.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with $jwt.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: {{ required "calls.jwtService.ingress.host is required" $jwt.ingress.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "warp.calls.fullname" . }}
|
||||
port:
|
||||
name: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -190,3 +190,50 @@ admin:
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
# Calls (MatrixRTC with a LiveKit SFU). Warp only advertises the LiveKit
|
||||
# JWT service and answers its OpenID checks; the media goes through LiveKit.
|
||||
# See docs/calls.md.
|
||||
calls:
|
||||
# The JWT service's public URL, advertised in /.well-known/matrix/client.
|
||||
# Defaults to https://<jwtService.ingress.host> when the chart runs the
|
||||
# service with an ingress; empty and without one, there are no calls.
|
||||
livekitServiceURL: ""
|
||||
|
||||
# lk-jwt-service: hands out LiveKit tokens to users of this server, who
|
||||
# prove who they are with an OpenID token. It checks that token with this
|
||||
# server's federation endpoint at https://<serverName>/.well-known/matrix/server
|
||||
# or <serverName>:8448, so one of the two must reach Warp.
|
||||
jwtService:
|
||||
enabled: false
|
||||
image:
|
||||
repository: ghcr.io/element-hq/lk-jwt-service
|
||||
tag: "0.8.0"
|
||||
pullPolicy: IfNotPresent
|
||||
# The LiveKit SFU's WebSocket URL as browsers reach it, e.g.
|
||||
# wss://livekit.example.org. Run LiveKit itself with its own chart
|
||||
# (https://github.com/livekit/livekit-helm); it needs UDP from clients.
|
||||
livekitURL: wss://livekit.example.org
|
||||
# A secret with the LiveKit API key and secret under these keys.
|
||||
existingSecret: ""
|
||||
keyKey: LIVEKIT_KEY
|
||||
secretKey: LIVEKIT_SECRET
|
||||
# Homeservers whose users may create call rooms on the SFU. Defaults to
|
||||
# serverName.
|
||||
fullAccessHomeservers: ""
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
host: livekit-jwt.example.org
|
||||
tls: []
|
||||
extraEnv: []
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
Reference in new issue
Block a user