На моём сервере теперь работают два сервиса видеозвонков: Jitsi Meet на meet.gluek.info и Bedrud на call.gluek.info. Оба устроены одинаково, по схеме SFU (Selective Forwarding Unit, «избирательный пересыльщик»): каждый участник отправляет своё видео на сервер один раз, а сервер, ничего не перекодируя, раздаёт его остальным, причём каждому в том качестве, которое тому нужно. Разница в том, что вокруг этого медиасервера.
Из чего состоят
| Jitsi Meet (docker-jitsi-meet) | Bedrud + LiveKit | |
|---|---|---|
| Контейнеры | 4: web (nginx), Prosody (XMPP), Jicofo, JVB | 2: Bedrud и LiveKit |
| Медиасервер | JVB, Java | LiveKit, Go |
| Сигнализация | XMPP через Prosody, Jicofo управляет конференциями | HTTP API Bedrud и WebSocket LiveKit |
| Размер образов | ≈ 3,1 ГБ (JVB 1,2 ГБ, Jicofo 1 ГБ, web 570 МБ, Prosody 312 МБ) | ≈ 350 МБ (LiveKit 140 МБ, Bedrud 206 МБ) |
| Память в простое | ≈ 460 МБ (JVB 300, Jicofo 100, Prosody 33, web 11) | ≈ 50–90 МБ (Bedrud 10–50, LiveKit 40) |
| Порты наружу | 443 через прокси + UDP 10000 | 443 через прокси + UDP 7882 и TCP 7881 |
| База данных | отдельной нет (Prosody хранит данные в файлах) | SQLite |
Память я смотрел через docker stats на одном и том же сервере в простое. У JVB по умолчанию ещё и большой лимит: я ограничил контейнер 1,5 ГБ, и Java охотно занимает то, что ей дают.
Важная оговорка: LiveKit у меня уже был для звонков в Matrix, поэтому реальная цена Bedrud для меня — это один контейнер на 50 МБ. Если ставить его с нуля, Bedrud умеет запускать встроенный LiveKit сам, и это всё равно один процесс.
Установка и настройка
Jitsi ставится по официальной инструкции docker-jitsi-meet: скачать релиз, скопировать env.example в .env, сгенерировать пароли скриптом gen-passwords.sh, указать PUBLIC_URL, открыть UDP-порт для JVB. У меня в .env в итоге 15 непустых строк, остальное — значения по умолчанию. Подробная документация, огромное сообщество, ответы на любые вопросы гуглятся.
Проблем с установкой Jitsi у меня не было. Большой плюс — готовые клиенты: в мобильном приложении Jitsi Meet достаточно поменять адрес сервера на свой, и всё работает. К Jitsi привыкли и мои собеседники, и в целом он работает неплохо.
Bedrud — один бинарник или один контейнер с config.yaml на три десятка строк. Но проект молодой (версия 0.13), документация местами расходится с реальностью: образа нет на ghcr, обязательный параметр tokenDuration выглядит необязательным, и без него вход просто не работает. Всё это я разобрал на странице проекта, а по найденным багам написал issue. Самый неприятный для повседневной работы: гости не могут повторно зайти по ссылке в комнату, где звонок уже закончился (подробности ниже). Зато интерфейс администратора, лимиты и пользователи есть из коробки, а в Jitsi для этого пришлось бы настраивать авторизацию в Prosody.
Что умеют
| Jitsi Meet | Bedrud | |
|---|---|---|
| Создать звонок без аккаунта | да, по умолчанию | нет, гости только заходят по ссылке |
| Аккаунты, админка, лимиты | через Prosody/JWT, без веб-админки | встроены: пользователи, passkey, OAuth, лимиты |
| Сквозное шифрование | есть (Insertable Streams) | есть флаг E2E в комнате |
| Комната ожидания | лобби | Gate |
| Запись | через Jibri (ещё контейнер с Chrome) | в разработке |
| Мобильные приложения | Jitsi Meet в Google Play и App Store | обещаны, но не опубликованы: в Google Play страницы нет, в релизах только десктоп (на Mac — неподписанный бинарник в .dmg) |
| Зрелость | с 2013 года, огромное сообщество | 0.x, небольшой проект: последний коммит 26 сентября, приглашение в Discord на сайте недействительно |
Нагрузка на 5, 10 и 20 человек
Как мерил
Участников изображали headless-браузеры Chromium через Playwright в Docker на моём MacBook. Каждый бот заходит в комнату через обычный веб-интерфейс, как живой человек, поэтому сравниваются не только серверы, но и то, как их веб-клиенты просят видео.
Сценарий как на обычной встрече, где камеры у всех одновременно не включены:
- 1 «презентующий»: камера показывает медленно меняющийся слайд 720p (вместо демонстрации экрана);
- 2 «говорящих»: камера 720p с подвижной тестовой картинкой и звук;
- остальные слушают: камера и микрофон выключены.
Боты заходят по одному, после этого 3 минуты идёт звонок, и в это время на сервере каждые 3–5 секунд пишутся docker stats контейнеров и счётчики сетевого интерфейса. В таблице средние значения за эти 3 минуты. CPU указан в процентах одного ядра: на сервере их 5. Трафик — это прирост относительно фона до начала теста. В Jitsi я учитываю все четыре контейнера, в Bedrud — Bedrud и LiveKit.
Результаты
| Jitsi, 5 | Jitsi, 10 | Jitsi, 20 | Bedrud, 5 | Bedrud, 10 | Bedrud, 20 | |
|---|---|---|---|---|---|---|
| CPU сервера, % ядра, среднее | 18 | 26 | 45 | 17 | 26 | 44 |
| CPU сервера, % ядра, пик | 47 | 42 | 64 | 20 | 30 | 83 |
| Память, МБ | 491 | 550 | 586 | 134 | 175 | 248 |
| Отдаёт, Мбит/с | 1,3 | 3,4 | 7,3 | 14,9 | 33,0 | 60,5 |
| Принимает, Мбит/с | 1,0 | 0,6 | 0,5 | 6,4 | 6,5 | 7,0 |
Почти вся нагрузка Jitsi приходится на JVB (18 → 45% ядра, 350 → 440 МБ), а Jicofo, Prosody и web на звонок почти не реагируют. У Bedrud так же: работает LiveKit (17 → 43% ядра, 80 → 140 МБ), а сам Bedrud тратит 0,1–0,2% CPU.
Что из этого следует
- Процессор одинаковый. На одной и той же встрече JVB и LiveKit берут почти одинаковую долю ядра: около 45% на 20 человек. На пятиядерном сервере это не проблема ни для одного из них.
- Память у Bedrud в 2,5–3,5 раза меньше: 134–248 МБ против 490–590 МБ. Разница почти целиком в JVM: JVB с Jicofo занимают 450–540 МБ ещё до того, как начинают что-то пересылать.
- Трафик отличается в 8 раз, но это разница не серверов, а клиентов. Веб-клиент Jitsi в режиме плиток просит у отправителей только нижний слой качества: три камеры вместе присылают на сервер меньше 1 Мбит/с, то есть по 200–300 кбит/с на камеру. Клиент Bedrud (LiveKit) запрашивает видео под размер плитки, а плитки при трёх камерах большие, и каждый отправитель шлёт около 2 Мбит/с. Картинка в Bedrud заметно лучше, но и каналу сервера это обходится дороже: 60 Мбит/с на 20 человек против 7 у Jitsi. Если пересчитать на мегабит пересылаемого видео, LiveKit тратит процессора в разы меньше.
- Оговорка про 20 человек в Bedrud: все 20 ботов жили на одном Mac за DSL на 65 Мбит/с, а сервер отдавал им 60 Мбит/с. Канал был почти забит, а Docker на Mac занимал 4,5 ядра на декодировании видео. Возможно, без этого ограничения трафик Bedrud был бы ещё выше.
- Своп на сервере во время теста не рос (около 2,8 ГБ, как и до теста), так что ни один из серверов не вытеснил другие сервисы.
Чего этот тест не показывает
Это один прогон на каждую точку, с синтетическим видео и с одной машины-клиента. Он хорошо сравнивает два сервера между собой на одной и той же встрече, но не говорит, сколько людей выдержит сервер. Для этого нужно гонять ботов с нескольких машин и с хорошим каналом.
Заодно тест выловил ещё один баг Bedrud: «постоянная» комната выключается после окончания звонка, и гости по старой ссылке в неё больше не попадут, пока хозяин её не пересоздаст (issue #212). Сначала я принял за баг и зависание ботов на «connecting», но его вызвал я сам, когда между прогонами включал комнату прямо в базе, а в LiveKit её к тому моменту уже не было.
Блокировки: какой проще заблокировать
Оба сервиса для браузера выглядят почти одинаково: страница и сигнализация идут по HTTPS на 443-й порт, а звук и видео — по WebRTC, то есть DTLS и SRTP поверх UDP. Сам WebRTC по трафику опознаётся легко: у STUN-пакетов фиксированный «magic cookie», у DTLS характерное рукопожатие. Поэтому если цензор решит резать WebRTC целиком, пострадают оба, и любой другой сервис звонков в браузере тоже. Содержимое при этом зашифровано, а адрес сервера (meet.gluek.info или call.gluek.info) виден только в SNI при подключении к сайту.
Различаются они тем, что будет, если заблокировать конкретный порт или UDP вообще:
- Jitsi шлёт медиа через один порт UDP 10000. Это порт JVB по умолчанию, и правило «резать UDP 10000» пишется в одну строку. Поэтому после публикации поста я подключил Jitsi к TURN-серверу coturn, который уже стоял на сервере для другого сервиса. Если UDP не проходит, клиент идёт через TURN по TCP 3478, а если закрыт и он, то через TURN по TLS на 443-м порту (
turn.gluek.info). - LiveKit принимает медиа на UDP 7882, а если UDP не проходит, клиент сам переключается на TCP 7881 (ICE-TCP). Качество по TCP хуже, но звонок состоится. Порты тоже стандартные для LiveKit, так что опознать его по ним так же легко.
- Сигнализация у Jitsi идёт через XMPP по WebSocket, у Bedrud — через WebSocket LiveKit. Обе спрятаны внутри TLS на 443-м порту и снаружи от обычного HTTPS не отличаются.
- IP-адрес у обоих один и тот же — адрес моего VPS. Если заблокируют его, перестанет работать всё сразу, включая сайт.
Итого: теперь у обоих есть путь, который переживает блокировку всего, кроме 443-го порта. TURN по TLS на 443-м порту снаружи выглядит как обычное HTTPS-соединение с turn.gluek.info, и отличить его можно разве что по характеру трафика. Порт 443 на сервере занят Caddy, поэтому я поставил сборку Caddy с модулем layer4: она смотрит на SNI в начале TLS-соединения и отдаёт соединения с turn.gluek.info в coturn, а всё остальное — сайтам. Тот же TURN я отдал и LiveKit: если не проходят ни UDP 7882, ни TCP 7881, клиенты Bedrud и Element Call тоже уходят на turn.gluek.info:443. Путь через TLS я проверил тестовыми соединениями, а настоящий звонок из сети, где открыт только 443-й порт, ещё не пробовал.
Итог
Если LiveKit на сервере уже есть, Bedrud почти бесплатен: два контейнера, в 2,5–3,5 раза меньше памяти, чем у Jitsi, при том же процессоре и с более качественной картинкой. Но проект сырой: за один вечер я нашёл четыре бага (ссылка на комнату, например, живёт ровно один звонок), а мобильных клиентов, которые обещает сайт, пока нет.
Я оставляю оба. Никогда не знаешь, какой из них в нужный момент сработает лучше: однажды Jitsi не запустился на холодную и подвёл меня, а второй сервис на том же сервере стоит почти ничего.