Видеозвонки в браузере на Bedrud: комната по ссылке, гости без регистрации. Сайт обещает ещё клиенты для Android, iOS и десктопа, но на деле их почти нет: страница в Google Play не открывается, Homebrew-тапа не существует, а в релизах на GitHub есть только десктопный клиент, причём для Mac это .dmg с голым неподписанным исполняемым файлом вместо приложения. Исходники Android- и iOS-клиентов в репозитории лежат, но готовых сборок нет. Главная особенность моей установки в том, что Bedrud не поднимает свой медиасервер, а использует LiveKit на rtc.gluek.info, который уже стоял для звонков в Matrix.
Зачем, если есть Jitsi
На meet.gluek.info у меня с июня работает Jitsi Meet, и он никуда не делся. Но это четыре контейнера (веб, XMPP-сервер Prosody, Jicofo и видеомост JVB на Java), которые в простое занимают около 460 МБ памяти, а образы весят больше 3 ГБ. На сервере, где уже работают несколько десятков контейнеров и память на счету, это заметно.
Когда для Element Call пришлось поднять LiveKit, оказалось, что на сервере теперь есть второй, куда более лёгкий медиасервер. LiveKit — это SFU на Go, тот же класс, что и JVB, только без XMPP вокруг. Оставалось найти к нему веб-интерфейс «комната по ссылке», и Bedrud подошёл: он и так построен на LiveKit и умеет работать с внешним сервером.
Сравнение по ресурсам и настройке — в отдельном посте Bedrud vs Jitsi.
Как это устроено
браузер ──https──▶ Caddy ──▶ bedrud:8090 (интерфейс, API, комнаты, пользователи)
│
└──wss/udp──▶ rtc.gluek.info ──▶ livekit:7880 (медиа: UDP 7882 / TCP 7881)
▲ │
создание комнат, токены ┘ └─ вебхуки ──▶ bedrud и lk-jwt (Matrix)
- Bedrud отвечает за всё, кроме медиа: пользователи, комнаты, чат, права. Браузеру он выдаёт токен LiveKit и адрес
wss://rtc.gluek.info, а дальше звук и видео идут напрямую в LiveKit - LiveKit один на всех: его же использует lk-jwt-service для звонков в Element X. Новых портов открывать не пришлось, всё медиа идёт через тот же UDP 7882
- В простое Bedrud занимает около 10 МБ памяти, после звонков около 50 МБ. LiveKit — 40–80 МБ, и эту память я уже тратил ради Matrix
Установка
Bedrud ставится одним бинарником или скриптом curl | bash, но у меня всё в Docker за общим Caddy, поэтому я собрал установку руками.
Образ
В документации указан образ ghcr.io/themadorg/bedrud, но на момент установки скачать его без авторизации нельзя (unauthorized). Docker-образы лежат в релизах на GitHub архивами, их нужно загрузить самому:
B=https://github.com/themadorg/bedrud/releases/download/v0.13.0
curl -fsSLO $B/SHA256SUMS
curl -fsSLO $B/bedrud-docker-alpine-amd64.tar.gz
grep alpine-amd64 SHA256SUMS && sha256sum bedrud-docker-alpine-amd64.tar.gz
docker load -i bedrud-docker-alpine-amd64.tar.gz # ghcr.io/themadorg/bedrud:0.13.0-alpine
Обновление — то же самое с новой версией и сменой тега в compose.
Конфиг
Главное — livekit.external: true: тогда Bedrud не запускает встроенный LiveKit и не проксирует его через себя.
server:
port: "8090"
domain: "call.gluek.info"
behindProxy: true
trustedProxies: ["172.18.0.0/16"] # Docker-сеть, где живёт Caddy
database:
type: "sqlite"
path: "/var/lib/bedrud/bedrud.db"
livekit:
external: true
host: "wss://rtc.gluek.info" # куда подключаются браузеры
internalHost: "http://livekit:7880" # API LiveKit внутри Docker-сети
apiKey: "lk-gluek" # тот же ключ, что у lk-jwt (см. ниже)
apiSecret: "…"
auth:
jwtSecret: "…" # openssl rand -hex 32
sessionSecret: "…"
frontendURL: "https://call.gluek.info"
tokenDuration: 24 # обязательно, см. подводные камни
cors:
allowedOrigins: "https://call.gluek.info"
Compose — один сервис в общей сети с Caddy и LiveKit, без опубликованных портов:
services:
bedrud:
image: ghcr.io/themadorg/bedrud:0.13.0-alpine
container_name: bedrud
command: ["run", "--config", "/etc/bedrud/config.yaml"]
mem_limit: 256m
restart: unless-stopped
volumes:
- ./config.yaml:/etc/bedrud/config.yaml:ro
- ./data:/var/lib/bedrud
networks: [caddy]
networks:
caddy:
external: true # общая сеть с Caddy и LiveKit
LiveKit: общий ключ и второй вебхук
Bedrud узнаёт от LiveKit, что участник вышел или комната закрылась, через вебхуки. В конфиге LiveKit достаточно добавить второй адрес:
webhook:
api_key: lk-gluek
urls:
- http://lk-jwt:8080/sfu_webhook # Matrix
- http://bedrud:8090/api/livekit/webhook # Bedrud
Тонкость в том, что у LiveKit один webhook.api_key на все адреса: каждый вебхук подписывается этим ключом. А Bedrud проверяет подпись своим apiKey. Поэтому завести Bedrud отдельную пару ключей не получится: ему нужен тот же ключ, что и lk-jwt. На своём сервере это не страшно, но Bedrud при этом может управлять и комнатами Element Call.
Комнату в LiveKit Bedrud создаёт сам через API, поэтому room.auto_create: false, который стоит в LiveKit ради Element Call (чтобы комнаты на моём SFU не могли создавать пользователи Matrix с других серверов), ему не мешает.
Caddy
call.gluek.info {
reverse_proxy bedrud:8090
}
После этого нужно зарегистрироваться (первый аккаунт я сделал через passkey) и выдать себе права админа:
docker exec bedrud bedrud --config /etc/bedrud/config.yaml user promote --email you@example.com
Подводные камни
tokenDuration без значения по умолчанию
Если не указать auth.tokenDuration, он равен нулю, и каждый токен входа истекает в момент выдачи (exp == iat). Вход проходит, но на любое действие интерфейс отвечает «Invalid token», через несколько секунд выкидывает на страницу входа, а при закрытой регистрации ещё и пишет, что регистрация выключена. Подпись токена при этом верная, так что искать проблему в секретах или прокси бесполезно. В примере конфига в документации tokenDuration: 24 есть, но выглядит как необязательный. Написал об этом issue #210.
Вебхуки, которые «не проходят подпись»
В логе Bedrud на каждый вход в комнату и публикацию трека пишется LiveKit webhook: invalid signature. На самом деле подпись в порядке: Bedrud разбирает JSON от LiveKit обычным encoding/json, а тот не понимает enum-поля, которые LiveKit присылает строками. В результате событие о выходе участника теряется, и он числится в комнате, пока её не закроют. От внешнего LiveKit это не зависит, встроенный присылает то же самое. Issue #209.
Ссылка на комнату живёт один звонок
Когда из комнаты выходит последний участник, LiveKit примерно через 20 секунд удаляет её у себя и присылает событие room_finished, а Bedrud по нему помечает комнату неактивной. Гость, открывший ту же ссылку позже, увидит «room is no longer active». Хозяин комнаты при входе получит предложение пересоздать её с тем же адресом. Но пересозданная комната становится приватной, и гостей в неё опять не пустит, пока в настройках не включить Public.
Галочка «постоянная комната» от этого не спасает: планировщик Bedrud постоянные комнаты не трогает, а вот обработчик room_finished выключает их так же, как и остальные. Описал это в issue #212.
Поначалу я решил, что дело в auto_create: false у LiveKit, и даже включил его. Но оказалось, что зависание на «connecting» я вызвал сам во время нагрузочного теста, когда вручную включил комнату в базе, а в LiveKit её уже не было. В обычной работе Bedrud создаёт комнату в LiveKit сам, так что auto_create можно оставить выключенным.
Единственный реальный случай, когда это мешает, — перезапуск LiveKit (например, при обновлении) посреди звонка. LiveKit держит комнаты в памяти и забывает их, не отправив room_finished. Участники не могут переподключиться, пока планировщик Bedrud примерно через минуту не погасит комнату, после чего её придётся пересоздать. Так что LiveKit лучше обновлять, когда никто не звонит.
«TLS off» в админке
На странице Overview горит красный бейдж «TLS off». Он показывает, терминирует ли HTTPS сам процесс Bedrud, а у меня этим занимается Caddy. Можно игнорировать.
Сертификат и Let’s Encrypt
Блок в Caddy я добавил раньше, чем DNS-запись, а в ту же ночь у Let’s Encrypt ещё и был инцидент с валидацией. Сертификат в итоге выдал ZeroSSL: Caddy переключается на него сам, если в глобальных опциях указан email. Без него Caddy ходит только в Let’s Encrypt (а при ошибках — в его staging). Вывод на будущее: сначала DNS, потом Caddy, и email в глобальном блоке лучше держать всегда.
Настройки
Регистрация открыта для всех, но с ограничениями. Гости без аккаунта могут только заходить в комнаты по ссылке, создавать комнаты им нельзя: это зашито в коде, настройки нет. Почту Bedrud не подтверждает, потому что SMTP пока не настроен.
Лимиты задаются через CLI и хранятся в базе, а не в config.yaml:
B="docker exec bedrud bedrud --config /etc/bedrud/config.yaml"
$B settings set maxRoomsPerUser 3 # активных комнат на пользователя (админа не касается)
$B settings set maxParticipantsLimit 20 # участников в комнате
$B settings set maxUploadBytesPerUser 52428800 # 50 МБ файлов в чате на пользователя
$B settings set globalDiskThresholdBytes 2147483648 # 2 ГБ на все загрузки
Последний лимит нужен потому, что файлы из чата удаляются только вместе с комнатой, когда её завершают или удаляют. Если все просто вышли, комната становится неактивной, но файлы остаются навсегда. Настройка chatMessageTTLHours в админке есть, но в версии 0.13.0 ни на что не влияет. Предложил это доделать в issue #211.
При создании комнаты можно включить сквозное шифрование (E2E) и комнату ожидания (Gate), где гостей впускает хозяин.
Итог
Bedrud — молодой проект (версия 0.13, на момент установки 9 звёзд на GitHub), так что Jitsi я не выключаю. Но как второй вариант для звонков он почти ничего не стоит: при уже работающем LiveKit это один контейнер на 50 МБ и пара строк в конфигах.