From 632099893af9303659ce13772fe6a8a9711e2e0a Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Thu, 26 Mar 2026 09:38:53 +0000 Subject: [PATCH] feat(media): deploy Element Call fork + optimize LiveKit quality - Deploy self-hosted Element Call at call.sunbeam.pt with SSO login - LiveKit: VP9 > AV1 > H.264 codec preferences, Opus stereo - LiveKit: congestion_control.allow_pause=false, larger NACK buffers - LiveKit: resources bumped to 2Gi/4CPU for VP9 SVC - Proxy: add call.* route, TLS cert SAN for call.sunbeam.pt --- base/ingress/pingora-config.yaml | 4 ++ base/media/element-call.yaml | 79 +++++++++++++++++++++++++++ base/media/kustomization.yaml | 1 + base/media/livekit-values.yaml | 56 +++++++++++++++---- overlays/production/cert-manager.yaml | 1 + 5 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 base/media/element-call.yaml diff --git a/base/ingress/pingora-config.yaml b/base/ingress/pingora-config.yaml index 66913f7..b52614d 100644 --- a/base/ingress/pingora-config.yaml +++ b/base/ingress/pingora-config.yaml @@ -348,6 +348,10 @@ data: backend = "http://openbao.data.svc.cluster.local:8200" auth_request = "http://hydra-public.ory.svc.cluster.local:4444/userinfo" + [[routes]] + host_prefix = "call" + backend = "http://element-call.media.svc.cluster.local:80" + # Bare domain (sunbeam.pt) — serves .well-known/matrix delegation only. # The proxy splits on '.', so sunbeam.pt yields prefix "sunbeam". [[routes]] diff --git a/base/media/element-call.yaml b/base/media/element-call.yaml new file mode 100644 index 0000000..c1fadf1 --- /dev/null +++ b/base/media/element-call.yaml @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: element-call-config + namespace: media +data: + config.json: | + { + "default_server_config": { + "m.homeserver": { + "base_url": "https://messages.DOMAIN_SUFFIX", + "server_name": "DOMAIN_SUFFIX" + } + }, + "livekit": { + "livekit_service_url": "https://livekit.DOMAIN_SUFFIX" + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: element-call + namespace: media +spec: + replicas: 1 + selector: + matchLabels: + app: element-call + template: + metadata: + labels: + app: element-call + spec: + containers: + - name: element-call + image: src.sunbeam.pt/studio/element-call:latest + ports: + - containerPort: 8080 + volumeMounts: + - name: config + mountPath: /app/config.json + subPath: config.json + readOnly: true + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 15 + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 10 + resources: + limits: + memory: 64Mi + cpu: 100m + requests: + memory: 32Mi + cpu: 25m + volumes: + - name: config + configMap: + name: element-call-config +--- +apiVersion: v1 +kind: Service +metadata: + name: element-call + namespace: media +spec: + selector: + app: element-call + ports: + - port: 80 + targetPort: 8080 diff --git a/base/media/kustomization.yaml b/base/media/kustomization.yaml index e9e4030..2f6586b 100644 --- a/base/media/kustomization.yaml +++ b/base/media/kustomization.yaml @@ -8,6 +8,7 @@ resources: - vault-secrets.yaml - livekit-alertrules.yaml - lk-jwt-service.yaml + - element-call.yaml # livekit-servicemonitor.yaml disabled — LiveKit runs on hostNetwork and port 6789 # is not reachable from Prometheus due to host firewall. Open port 6789 on the host # or add an iptables rule, then re-enable. diff --git a/base/media/livekit-values.yaml b/base/media/livekit-values.yaml index 6940c90..0c84139 100644 --- a/base/media/livekit-values.yaml +++ b/base/media/livekit-values.yaml @@ -4,16 +4,57 @@ # Reference: https://github.com/livekit/livekit-helm/blob/master/server-sample.yaml livekit: - # LiveKit server config injected as config.yaml port: 7880 log_level: info prometheus_port: 6789 + # ── Codec preferences (order matters — first match wins) ──────────── + room: + enabled_codecs: + - mime: audio/opus + - mime: audio/red + # VP9: ~2x quality-per-bit over H.264, built-in SVC. + - mime: video/vp9 + # AV1: best compression, limited HW decode support still. + - mime: video/av1 + # H.264: broadest HW accel, fallback for constrained clients. + - mime: video/h264 + # VP8: widest compatibility baseline (Element Call default). + - mime: video/vp8 + + # Receiver-side jitter buffer. Tight values for a small team. + playout_delay: + enabled: true + min: 100 + max: 500 + sync_streams: true + empty_timeout: 300 + departure_timeout: 20 + + # ── RTC / transport ───────────────────────────────────────────────── rtc: port_range_start: 49152 port_range_end: 49252 + tcp_port: 7881 use_external_ip: true + allow_tcp_fallback: true + # Request keyframes faster after quality drops. + pli_throttle: + low_quality: 300ms + mid_quality: 500ms + high_quality: 500ms + + # Larger NACK retransmission buffers for loss recovery. + packet_buffer_size_video: 1000 + packet_buffer_size_audio: 500 + + # Never pause video tracks due to congestion — we have the bandwidth. + congestion_control: + enabled: true + allow_pause: false + + # ── TURN ──────────────────────────────────────────────────────────── turn: enabled: true domain: meet.DOMAIN_SUFFIX @@ -24,11 +65,8 @@ livekit: relay_range_end: 23333 redis: - # Valkey is protocol-compatible with Redis; LiveKit sees this as a Redis endpoint address: valkey.data.svc.cluster.local:6379 - # API keys — loaded from K8s Secret managed by VSO (secret/livekit in OpenBao). - # The keys.yaml field contains "devkey: " in YAML format. key_file: keys.yaml storeKeysInSecret: @@ -39,16 +77,14 @@ storeKeysInSecret: # chart template requires livekit.prometheus_port which conflicts with hostNetwork. deployment: - # hostNetwork gives LiveKit direct access to the host network namespace, - # which is the only practical way to expose the 10k-port TURN relay range - # (13333-23333) without listing individual hostPorts in the pod spec. hostNetwork: true resources: limits: - memory: 128Mi + memory: 2048Mi + cpu: 4000m requests: - memory: 64Mi - cpu: 100m + memory: 512Mi + cpu: 500m # Recreate strategy: hostPorts (TURN UDP relay range) block RollingUpdate — # the new pod cannot schedule while the old one still holds the host ports. diff --git a/overlays/production/cert-manager.yaml b/overlays/production/cert-manager.yaml index 71835bb..70519a0 100644 --- a/overlays/production/cert-manager.yaml +++ b/overlays/production/cert-manager.yaml @@ -78,3 +78,4 @@ spec: - search.DOMAIN_SUFFIX - vault.DOMAIN_SUFFIX - find.DOMAIN_SUFFIX + - call.DOMAIN_SUFFIX