element-hq / element-meta

Shared/meta documentation and project artefacts for Element clients
75 stars 12 forks source link

Self-hosted call.element support (like it's done for jitsi) #2371

Open alexander-potemkin opened 8 months ago

alexander-potemkin commented 8 months ago

Your use case

What would you like to do?

I would like to configure my own element-call support on the server side, so that all of the clients would pick it up instead of call.element.io

Why would you like to do it?

To facilitate self-hosted calls.

How would you like to achieve it?

Ideally, that element call service would be populated through the server; but .well-known approach, like it's done for Jitsi, works too.

Have you considered any alternatives?

Yes. The alternative - it's to rebuild and repackage and publish all of the Element clients. Doable, but feels a bit unreasonable.

Additional context

https://github.com/element-hq/element-call/issues/2228 https://github.com/element-hq/element-desktop/issues/1566

alexander-potemkin commented 6 months ago

Bumping this up...

rajil commented 5 months ago

I scraped at the documentation to get element call server going. I have tested this to call between ElementX android <> ElementX android; ElementX android <> Element Web (nightly).

Here are my instructions. If you find something needs to change do point that out.

Proxy setup

DNS names

I am assuming that the domain name is mydomain.com. Make sure the following dns names have public ip/FQDN:

Nginx proxy config

The following assumes that the elementcall docker container is running at 192.168.1.2. The following config forwards the call to element call/livekit running via docker.

server {
    server_name call.mydomain.com;

     ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    ssl_dhparam /etc/ssl/dhparam.pem;

    listen 443 ssl http2;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    ssl_protocols TLSv1.2;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_prefer_server_ciphers on;

    proxy_buffer_size       128k;
    proxy_buffers           4 256k;
    proxy_busy_buffers_size 256k;

    location / {
        proxy_pass http://192.168.1.2:8093;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    server_name sfu.mydomain.com;

    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    ssl_dhparam /etc/ssl/dhparam.pem;

    listen 443 ssl http2;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    ssl_protocols TLSv1.2;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_prefer_server_ciphers on;

    proxy_buffer_size       128k;
    proxy_buffers           4 256k;
    proxy_busy_buffers_size 256k;

    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_comp_level 2;
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg #image/gif image/png application/wasm;
    gzip_vary off;
    gzip_disable "MSIE [1-6]\.";
    error_page 405 =200 $uri;

    default_type application/wasm;

    location / {
       proxy_pass http://192.168.1.2:7880;
       proxy_http_version 1.1;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection $connection_upgrade;

       proxy_set_header Host $host;
       proxy_set_header X-Forwarded-Server $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    server_name sfu-jwt.mydomain.com;

     ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    ssl_dhparam /etc/ssl/dhparam.pem;

    listen 443 ssl http2;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    ssl_protocols TLSv1.2;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_prefer_server_ciphers on;

    proxy_buffer_size       128k;
    proxy_buffers           4 256k;
    proxy_busy_buffers_size 256k;

    location / {
        proxy_pass http://192.168.1.2:8881;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Docker setup

Element call

Use the following docker compose:

$ cat docker-compose.yaml 
networks:
  lkbackend:

services:
  element-call:
    image: ghcr.io/element-hq/element-call:latest
    container_name: element-call
    hostname: element-call
    ports:
      - 8093:8080
    volumes:
      - /home/ubuntu/dockerdata/volumes/elementcall/config.json:/app/config.json
    restart: unless-stopped
    networks:
      - lkbackend

  jwt-service:
    image: ghcr.io/element-hq/lk-jwt-service:latest-ci
    container_name: lk-jwt-service
    hostname: lk-jwt-service
    ports:
      - 8881:8080
    environment:
      - LIVEKIT_SECRET=somestrongstring
      - LIVEKIT_URL=wss://sfu.mydomain.com:443
      - LIVEKIT_KEY=devkey
    deploy:
      restart_policy:
        condition: on-failure
    networks:
      - lkbackend

  livekit:
    image: livekit/livekit-server:latest
    command: --dev --config /etc/livekit.yaml
    restart: unless-stopped
    volumes:
      - /home/ubuntu/dockerdata/volumes/elementcall/backend/livekit.yaml:/etc/livekit.yaml
    network_mode: "host"

  redis:
    image: redis:6-alpine
    command: redis-server /etc/redis.conf
    ports:
      - 6379:6379
    volumes:
      - /home/ubuntu/dockerdata/volumes/elementcall/backend/redis.conf:/etc/redis.conf
    networks:
      - lkbackend

Here are the supporting files: config.json

cat  /home/ubuntu/dockerdata/volumes/elementcall/config.json
{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.mydomain.com",
      "server_name": "mydomain.com"
    }
  },
  "livekit": {
     "livekit_service_url": "https://sfu-jwt.mydomain.com"
  }
}

livekit.yaml

$ cat /home/ubuntu/dockerdata/volumes/elementcall/backend/livekit.yaml 
port: 7880
bind_addresses:
  - "0.0.0.0"
rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: false

turn:
  enabled: false
  domain: localhost
  cert_file: ""
  key_file: ""
  tls_port: 5349
  udp_port: 443
  external_tls: true
keys:
  devkey: "somestrongstring"
logging:

redis.conf

$ cat /home/ubuntu/dockerdata/volumes/elementcall/backend/redis.conf
bind 0.0.0.0
protected-mode yes
port 6379
timeout 0
tcp-keepalive 300

Synapse change

set the following in homeserver.yaml

listeners:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true

    resources:
      - names: [client, federation,openid]
        compress: false

#https://github.com/element-hq/element-call/issues/2005
serve_server_wellknown: true

Firewall/Router port forwards

For Livekit

From https://docs.livekit.io/home/self-hosting/ports-firewall/ Forward UDP ports 50100:50200 to docker instance 192.168.1.2 Forward TCP port 7881 to docker instance 192.168.1.2

For Element Call

Forward TCP port 443 to Nginx server

alexander-potemkin commented 5 months ago

@rajil , thanks. My question was not about setting up Element call - it's done, no problems - a bit different from your approach, but I believe, it's a matter of taste.

My question is about making Mobile & Desktop apps to use self-hosted version, because for now they use call.element.io without any possibility to change that in the settings. I'm sorry if my original post was misleading.

rajil commented 5 months ago

@alexander-potemkin The nightly version of ElementX Android already allows changing from call.element.io. And so does the nightly version of Element Desktop.

alexander-potemkin commented 5 months ago

@rajil , my hopes were for Element "Classic"... Anyway, thanks for letting know! If that's not too much to ask, could you please, let me know how it's handled? Does it follow .well-known or DNS directives? Or it relies on end user to apply the changes manually?

rajil commented 5 months ago

Element Classic is no longer being developed AFAIK. I had to apply the change manually in the EX client settings (not sure if there is a .well-known for it).

alexander-potemkin commented 5 months ago

Thank you. My question remains then:

rajil commented 5 months ago

It seems that .well-known support is on the way

alexander-potemkin commented 5 months ago

It seems that .well-known support is on the way

As it's a commit into element-call, it's the only service that is affected; if they push it to JS SDK - it might affect Desktop clients, but not mobiles. Unless I'm missing something.

rajil commented 5 months ago

Mobile is also supported.

alexander-potemkin commented 5 months ago

Mobile is also supported.

Will see!

jacotec commented 3 months ago

I scraped at the documentation to get element call server going. I have tested this to call between ElementX android <> ElementX android; ElementX android <> Element Web (nightly).

Here are my instructions. If you find something needs to change do point that out.

I simply want to send you a big THANK YOU! Due to the current lack of docs I'd never be able to set up EC today without your post. Awesome starting point!

jacotec commented 3 months ago

Adding some comments after I did the described setup:

If the Element-Call-VM is behind a firewall like pfSense and is not directly exposed to the internet, you need to change the line in livekit.yaml to:

use_external_ip: true

With this setting, livekit will detect the external IP address via STUN. Having this set to "false" won't work in this scenario.

Furthermore, some .well-known's must be changed / added:

.well-known/matrix/client must be extended with this:

  "org.matrix.msc4143.rtc_foci": [
    {
      "type": "livekit",
      "livekit_service_url": "https://sfu-jwt.mydomain.com"
    }
  ]

.well-known/element/element.json must be introduced, so Element-X gets the changed server:

{"call":{"widget_url":"https://call.mydomain.com"}}

The Element-Web config.json at the server which serves Element-Web (Usually the same machine which runs Synapse) must be extended by these:

   "features": {
        "feature_video_rooms": true,
    "feature_new_room_decoration_ui": true,
    "feature_group_calls": true,
    "feature_element_call_video_rooms": true,
        [...]
    },
    "element_call": {
        "url": "https://call.mydomain.com",
        "participant_limit": 8,
        "brand": "Element Call",
    "use_exclusively": true
    },

Element-Desktop will, for now, not get the information from the .well-known/element/element.json. I hope they will implement this in the future. To make Element-Desktop work with Element-Call by default, you need to copy the config.json which you use for your Element-Web into each Desktop installation. The locations are described here).

jacotec commented 3 months ago

I found that Element-Call as for now is using VP8 as codec. This really kills battery at Apple devices (iPhone/iPad) which are getting very hot during calls. VP8 is not a hardware-supported codec here, so all must be done in software.

I've filed an issue here, but meanwhile you can force livekit to use H.264 which solves all these problems:

In /opt/element-call/volumes/elementcall/backend/livekit.yaml add this section:

room:
  enabled_codecs:
    - mime: video/h264
    - mime: audio/opus

This will force livekit to use H.264 and all is back to normal in the Apple world, iPhones not getting hot anymore, battery use for 15 minutes video call on an iPhone 14 is down to 3% now.

benradey commented 3 weeks ago

I got all this working (thank you to the amazing devs and the helpful community members who helped me along my way) and wanted to post my setup as a bit of extra reference material, in addition to @rajil 's great starting point above.

What really made the setup "click" for me was this realization: your goal is to get each service individually exposed to the internet. That's it. You set a few key values for the sake of Element X knowing where each service lives, but the services do not directly communicate with each other. I was initially making things a lot harder for myself because I was trying to set up fancy container networking, under the false assumption that the services were interdependent. They are not; they are discrete, independent services that are for the most part blissfully ignorant of each other. Any configuration values that refer to another service are ONLY to tell Element X where that service lives.

Without further ado, here are the configuration file changes that I made:

homeserver.yml (Synapse)

experimental_features:
  msc3266_enabled: true
max_event_delay_duration: 24h

config.json (Element Call)

{
  "default_server_config": {
    "m.homeserver": {
      "base_url": "https://matrix.YOUR_DOMAIN.COM",
      "server_name": "matrix.YOUR_DOMAIN.COM"
    }
  },
  "livekit": {
    "livekit_service_url": "https://livekit-jwt.YOUR_DOMAIN.COM"
  },
  "features": {
    "feature_use_device_session_member_events": true
  },
  "eula": "https://static.element.io/legal/online-EULA.pdf"
}

livekit.yaml (Livekit)

port: 7880
bind_addresses:
  - "0.0.0.0"
rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: false
turn:
  enabled: false
  domain: localhost
  cert_file: ""
  key_file: ""
  tls_port: 5349
  udp_port: 443
  external_tls: true
keys:
  devkey: SOMETHING_SECRET_YOU_SHOULD_GENERATE

Nginx config (Element Call, Livekit, and Livekit-JWT)

server {
    server_name call.YOUR_DOMAIN.COM;

    http2 on;
    listen 443 ssl;

    location / {
        proxy_pass http://localhost:35363;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    ssl_certificate /etc/letsencrypt/live/call.YOUR_DOMAIN.COM/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/call.YOUR_DOMAIN.COM/privkey.pem; # managed by Certbot
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    server_name livekit.YOUR_DOMAIN.COM;

    http2 on;
    listen 443 ssl;

    default_type application/wasm;

    location / {
       proxy_pass http://localhost:7880;
       proxy_http_version 1.1;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection $connection_upgrade;

       proxy_set_header Host $host;
       proxy_set_header X-Forwarded-Server $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;
    }

    ssl_certificate /etc/letsencrypt/live/livekit.YOUR_DOMAIN.COM/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/livekit.YOUR_DOMAIN.COM/privkey.pem; # managed by Certbot
}

server {
    server_name livekit-jwt.YOUR_DOMAIN.COM;

    http2 on;
    listen 443 ssl;

    location / {
        proxy_pass http://localhost:54835;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    ssl_certificate /etc/letsencrypt/live/livekit-jwt.YOUR_DOMAIN.COM/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/livekit-jwt.YOUR_DOMAIN.COM/privkey.pem; # managed by Certbot
}

Nginx config (Synapse)

I won't paste the entire file here, but the only changes relevant are for the .well-known endpoints. Here's a snippet:

    location /.well-known/matrix/ {
        add_header Content-Type application/json;
        add_header Access-Control-Allow-Origin *;

        location = /.well-known/matrix/server {
            return 200 '{"m.server": "matrix.YOUR_DOMAIN.COM:443"}';
        }

        location = /.well-known/matrix/client {
            return 200 '{"m.homeserver":{"base_url":"https://matrix.YOUR_DOMAIN.COM"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit-jwt.YOUR_DOMAIN.COM"}]}';
        }
    }

    location /.well-known/element/ {
    add_header Content-Type application/json;
    add_header Access-Control-Allow-Origin *;
        location = /.well-known/element/element.json {
            return 200 '{"call":{"widget_url":"https://call.YOUR_DOMAIN.COM"}}';
        }
    }

And now here are the commands that I use to run the actual containers. I'm in an environment where I use podman instead of docker, but the concepts should translate 1:1. You may wish to tweak the -d --rm options; I used them to translate the commands into a system service via podman generate systemd.

podman run -d --rm --name elementCall-livekit \
  -p 7880-7881:7880-7881/tcp \
  -p 50100-50200:50100-50200/udp \
  -v /wherever/livekit.yaml:/etc/livekit.yaml \
  --user 1000 \
  livekit/livekit-server:latest --dev --config /etc/livekit.yaml

podman run -d --rm --name elementCall-lk-jwt \
  -e LIVEKIT_URL=wss://livekit.YOUR_DOMAIN.COM \
  -e LIVEKIT_KEY=devkey \
  -e LIVEKIT_SECRET=THE_SAME_GENERATED_SECRET_THING_FROM_EARLIER \
  -p 54835:8080/tcp \
  --user 1000 \
  ghcr.io/element-hq/lk-jwt-service:latest-ci

podman run -d --rm --name elementCall-ui \
  -p 35363:8080/tcp \
  -v /wherever/config.json:/app/config.json \
  --user 1000 \
  ghcr.io/element-hq/element-call:latest-ci

With the services running, now you need to set up some port forwards on your router. Port 7881 TCP and port range 50100-50200 UDP, to whichever machine is hosting your services.

That's it. Notably, I did NOT need to include a redis image as part of this setup. This works for me. If I made any mistakes, I welcome feedback.

At this point you should be able to restart your phone or log out / log in to Element X and have it routing calls through your self-hosted instance! :tada:

heikomat commented 3 weeks ago

Having setup-docs is nice and all, but the original issue still persists, at least for element desktop.

While Element X now uses the .well-known/element/element.json, element web (and therefore element desktop) do not. It is currently (without manually modifing the config of the client) not possible for someone to install element-desktop, join my matrix instance and have the client use my element-call instance.

To workaround this i currently host a configured element-web instance and users are encouraged to use that web-version instead. I would however prefer to be able to use element desktop because it better integrates into the OS.

alexander-potemkin commented 3 weeks ago

I would however prefer to be able to use element desktop because it better integrates into the OS.

... and because it's the only app (across mobiles and web) that offers search functionality.

escix commented 2 weeks ago

Your use case

What would you like to do?

Inform others how I selfhosted element-call on FreeBSD

Why would you like to do it?

Helping others and learn from others if I did something wrong

How would you like to achieve it?

By posting the below instructions:

Caddyfile:

(matrix-well-known-header) {
    # Headers
    header Access-Control-Allow-Origin "*"
    header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"
    header Content-Type "application/json"
}

(logging) {
        log {
                level DEBUG
                output file /var/log/caddy/caddy.log {
                        roll_size 15mb
                        roll_keep 20
                }
        }
}

(errors) {
        handle_errors {
#                root * /DATA/worldweb/error_pages
                rewrite * /{http.error.status_code}.html
                file_server
        }
}

(server_add) {
        bind PUBLIC_IPV4
        bind PUBLIC_IPV6
}

turnserver.mydomain.com {
        import logging
        import errors
        import server_add

 }

mydomain.com {
        import logging
        import errors
        import server_add

        header /.well-known/matrix/* Content-Type application/json
        header /.well-known/matrix/* Access-Control-Allow-Origin *

        respond /.well-known/matrix/server {"m.server":"mydomain.com:443"}
        respond /.well-known/matrix/client {"m.homeserver":{"base_url":"https://mydomain.com"},"org.matrix.msc3575.proxy":{"url":"https://mydomain.com/sliding-sync"},"org.matrix.msc4143.rtc_foci":{"type":"livekit","livekit_service_url":"https://livekit-jwt.mydomain.com"}}
        respond /.well-known/element/element.json {"call":{"widget_url":"https://call.mydomain.com"}}

        handle /_matrix/client/unstable/org.matrix.msc3575/sync* {
        reverse_proxy unix//var/run/synapse/main_public.sock

        }
        reverse_proxy /_matrix/* unix//var/run/synapse/main_public.sock
        reverse_proxy /_synapse/client/* unix//var/run/synapse/main_public.sock
        reverse_proxy /_synapse/admin/* unix//var/run/synapse/main_public.sock

        handle_path /sliding-sync/* {
                reverse_proxy unix//var/run/synapse/main_public.sock

        }

    }

call.mydomain.com {
        import logging
        import server_add
        root * /opt/sliding_sync/element-call/dist
        file_server
        try_files {path} /index.html
}

livekit.mydomain.com {
        import logging
        import server_add
        reverse_proxy IP_OF_LIVEKIT:7880
}

livekit-jwt.mydomain.com {
        import logging
        import server_add
        reverse_proxy IP_OF_LK:8080
}

homeser.yaml:

experimental_features:
  msc3266_enabled: true

max_event_delay_duration: 24h

dits/config.json:

{
  "default_server_config":{
    "m.homeserver":{
      "base_url":"https://mydomain.com",
      "server_name":"mydomain.com"
    }
  },
  "livekit": {
    "livekit_service_url":"https://livekit-jwt.mydomain.com"
  },
  "element_call": {
     "url": "https://call.mydomain.com",
     "participant_limit": 8,
     "brand": "Element Call",
     "use_exclusively": true
    },
  "features":{
    "feature_video_rooms": true,
    "feature_new_room_decoration_ui": true,
    "feature_group_calls": true,
    "feature_element_call_video_rooms": true,
    "feature_use_device_session_member_events":true
  }
}

livekit.yaml:

port: 7880
bind_addresses:
  - "0.0.0.0"
log_level: info
rtc:
  tcp_port: 7881
  port_range_start: 50000
  port_range_end: 60000
  # use_external_ip should be set to true for most cloud environments where
  # the host has a public IP address, but is not exposed to the process.
  # LiveKit will attempt to use STUN to discover the true IP, and advertise
  # that IP with its clients
  use_external_ip: true

  turn_servers:
  - host: turnserver.mydomain.com
    username: "Username"
    credential: "password"
    port: 3478
    protocol: tcp
  - host: turnserver.mydomain.com
    username: "Username"
    credential: "password"
    port: 5349
    protocol: tls
  - host: turnserver.mydomain.com
    username: "Username"
    credential: "password"
    port: 3478
    protocol: udp

turn:
  enabled: false

redis:

keys:
  devkey: secretcodefordevkey

room:
  enabled_codecs:
    - mime: video/h264
    - mime: audio/opus

Build element-call:

git clone https://github.com/element-hq/element-call.git
cd element-call
yarn
yarn build

Build livekit:

git clone https://github.com/livekit/livekit
cd livekit
./bootstrap.sh
mage
/opt/sliding_sync/livekit/livekit/bin/livekit-server --config /opt/sliding_sync/element-call/backend/livekit.yaml &

clone and run lk-jwt-service:

cd /opt/sliding_sync/lk-jwt-service/lk-jwt-service && HOMESERVER_URL="https://mydomain.com" LIVEKIT_URL="wss://livekit.mydomain.com" LIVEKIT_KEY=devkey LIVEKIT_SECRET=secretcodefordevkey go run *.go &

Have you considered any alternatives?

No response

Additional context

No response