Перейти к основному содержимому

ADR-0062: Медиа-стек звонков и запись на Jitsi (JVB SFU + coturn + jibri → file storage; JWT из messenger; нативный lib-jitsi-meet)

Статус

Accepted (supersedes отложенный media-пункт ADR-0055 §2.4 — «media-стек выбирается при реализации»)

Дата

2026-07-15

Назначение

Зафиксировать медиа-стек для аудио/видео-звонков и записи в мессенджере PIN: чем обеспечиваются 1:1-звонки и групповые конференции чата сделки, кто выдаёт доступ к комнате, как ведётся запись и куда она попадает. ADR-0055 сознательно отложил этот выбор («звонки — отдельная фаза; media-стек выбирается при реализации») — здесь он делается.

Контекст

  • Отложенное решение. ADR-0055 §2.4 зафиксировал только контракт сигналинга поверх контактов и оставил media-стек (WebRTC/SFU) на этап реализации. Сам мессенджер — собственный (контекст messenger, .NET, PostgreSQL, доставка realtime — SignalR), без внешнего чат-сервера.
  • Целевая нагрузка невысока. ADR-0056: 1–10k одновременно активных, агентов/менеджеров ≤20k. Медиа-решение должно быть простым в эксплуатации и не тянуть за собой инфраструктуру «на вырост».
  • Данные уже готовы к сигналинг-only модели. CallSessionEntity (call-session.md) моделирует 1:1-звонок как запись сигналинга (кто/кому/тип/статус/причина завершения/длительность); в ней нет и не должно быть медиа-данных. Комната описывается owned-VO MediaRoom (RoomName + Domain) — это не медиа и не PII; сами access-токены/SDP/ICE/TURN эфемерны и не персистятся.
  • Появились групповые звонки и запись. Требуются конференции в чате сделки (ConferenceSessionEntity, conference-session.md; участники — CallParticipantEntity, call-participant.md) и запись звонков с сохранением файла (CallRecordingEntity, call-recording.md). Нужен SFU (mesh/P2P на 3+ участниках не тянет) и серверная запись с выгрузкой в файловое хранилище.
  • Кросс-тенантность и допуск по членству. Доступ к звонку/конференции определяется членством в чате/контакте, а не TenantId; выдача доступа к комнате должна выпускаться самим messenger в той же транзакции, что и изменение состояния (persist-then-push, messenger-hub.md).

Решение

Самостоятельно разворачиваемый Jitsi как медиа-стек мессенджера. Компоненты и роли:

  1. JVB (Jitsi VideoBridge) — SFU для группового медиа (конференции чата сделки) и релея 1:1. Публичный медиатрафик — UDP 10000. JVB — узел горизонтального масштабирования по медиа.
  2. coturn — TURN/STUN для прохождения NAT/файрволов; TURN-креды короткоживущие, отдаются клиенту внутри MediaJoinModel.IceServers (эфемерно, не персистятся).
  3. jibri — серверная запись конференций и звонков в файл; по завершении файл выгружается в file storage (filestorage), в CallRecordingEntity фиксируется FileId и метаданные (Format/SizeBytes/DurationSeconds).
  4. JWT выпускает messenger (HS256, общий app_secret с prosody), короткий TTL, минтится в момент доставки realtime-события. Клиент допускается в комнату только по валидному токену — токен несёт room/участника/срок. Токен — Sensitive, никогда не логируется и не хранится.
  5. Нативный клиент lib-jitsi-meet (НЕ IFrame API): фронт подключается к комнате напрямую по MediaJoinModel (RoomName, AccessToken, SignalingUrl wss, MediaDomain, IceServers, ExpiresAt), UI — собственный.
  6. 1:1 и групповые/конференц-звонки — на одном стеке. Для 1:1 CallSessionEntity остаётся сигналинг-моделью (SessionKind.Direct); конференция — ConferenceSessionEntity.
  7. Запись по политике, а не по флагу. Авто-запись определяется типизированной RecordingPolicy.ShouldAutoRecord(chatKind, sessionKind) (не boolean-флаг); ручной старт/стоп — модератором. Запись сопровождается уведомлением участников (RecordingNoticeState).
  8. Жизненный цикл комнаты — через mod_reservations. prosody перед созданием комнаты обращается к messenger webhook’ом (reservation): комната допускается только если её RoomName совпадает с сохранённым MediaRoom.RoomName активной сессии (fail-closed). Присутствие/события комнаты (join/leave/recording) прокидываются кастомным prosody-модулем в messenger.
  9. Интеграция допускается только доверенным вызовом. Внутренние эндпойнты (api/internal/) от медиа-стека аутентифицируются HMAC (X-Pin-Signature + окно защиты от replay) либо mTLS; несовпадение подписи → MSG_JITSI_SIGNATURE_INVALID (401), неизвестная комната → MSG_JITSI_ROOM_UNKNOWN (403).

Готовность записи и завершение конференции публикуются в Kafka: pin.messenger.conference-ended.v1 (key DealId) и pin.messenger.call-recording-available.v1 (key SessionId). 1:1-звонок Kafka не публикует — только realtime через outbox.

Область Действия

ОбластьВходитНе входит
Сервисыmessenger (звонки/конференции/запись, JWT, reservation/ingest), filestorage (хранение записей)crm, catalog, identity — только потребители событий
КонтрактыJWT-claims (room/участник/TTL), reservation-webhook (mod_reservations), POST /internal/jitsi/events, jibri-ingest (POST /internal/call-recordings/{sessionId}/ingest), топики pin.messenger.call-recording-available.v1, pin.messenger.conference-ended.v1; MediaJoinModel как единственный descriptor допускавнешний медиа-протокол — не выставляется наружу
ДанныеConferenceSessionEntity, CallParticipantEntity, CallRecordingEntity, расширенный CallSessionEntity (+SessionKind, ссылка на запись); owned-VO MediaRoomaccess-токены/SDP/ICE/TURN — эфемерны, не персистятся
Эксплуатациядашборды и SLO по JVB / jibri / coturn / prosody / jicofo; публичный UDP 10000; capacity jibri

Варианты

ВариантОписаниеПлюсыМинусыПричина Решения
Self-hosted Jitsi (JVB+coturn+jibri+prosody+jicofo)Свой медиа-стек, JWT/reservation из messenger, jibri-запись → file storageПолный контроль над медиа, допуском и записью; своё хранилище записей (FileId); JWT/reservation «из коробки» через prosody; готовый jibri для записиОперационная нагрузка (5 компонентов, публичный UDP, CPU jibri)Принято — единственный вариант, дающий и SFU, и серверную запись → своё хранилище, и admission через JWT/reservation без «клея»
Managed SaaS (Twilio/Daily/JaaS)Внешний медиа-провайдерНет ops-нагрузки, быстрый стартСтоимость на масштабе, data residency/приватность записей, нет контроля над self-host и хранениемОтклонено: стоимость, резидентность данных, отсутствие контроля над записями
mesh / P2P-onlyПрямые соединения без SFUНет серверного медиа-узлаРазваливается на 3+ участниках; нет серверной записиОтклонено: не тянет групповые конференции и запись
mediasoup / LiveKitGreen-field медиа-серверГибкий SFUНет встроенного jibri-эквивалента (запись → S3/файл) и prosody-admission (JWT/reservation) — больше «клея» под нашу модельОтклонено: пришлось бы дописывать запись и admission руками

Последствия

ТипПоследствиеКомпенсация
АрхитектураПолный контроль над медиа, допуском и собственной записью → file storage (FileId); messenger — единственный источник допуска (JWT + reservation)MediaJoinModel — единственный descriptor; допуск fail-closed по MediaRoom.RoomName
БезопасностьВнутренние эндпойнты открыты медиа-стеку; JWT/TURN-креды чувствительныHMAC (X-Pin-Signature + replay-окно) или mTLS; JWT/токены короткий TTL, Sensitive, не логируются; room-match fail-closed
ДанныеФайлы записей в filestorage; метаданные записи в CallRecordingEntityРетенция RetentionUntil; медиапотоки/токены не персистятся
ЭксплуатацияНовая ops-нагрузка: 5 компонентов, публичный UDP 10000, CPU-нагрузка jibriJVB — узел масштабирования по медиа; jibri — узкое место по ёмкости записи (мониторинг + очередь/лимит одновременных записей)

Затронутые Контракты И Совместимость

КонтрактВерсияСовместимостьМиграция
pin.messenger.conference-ended.v11новый топикadditive
pin.messenger.call-recording-available.v11новый топикadditive
CallSessionEntity (+SessionKind, RecordingStatus?/RecordingId?)расширениеadditive-миграция схемы
MediaJoinModel / MediaTokenModel1новый контракт допускановый
api/internal/jitsi/*, jibri-ingest1внутренний, доверенныйновый

Нефункциональные Требования

ТребованиеЦельПроверка
Задержка входаjoin p95 < 3 c (от запроса допуска до media-соединения)метрика на media-token/join + дашборд JVB
Надёжность записиуспешность записи ≥ 99%доля RecordingStatus=Available от инициированных; алерт по Failed
Безопасностьдопуск только по валидному JWT + room-match; внутренние вызовы подписаныreview + тест fail-closed (MSG_JITSI_ROOM_UNKNOWN/MSG_JITSI_SIGNATURE_INVALID)
Наблюдаемостьдашборды JVB/jibri/coturn/prosody/jicofoметрики компонентов + SLO join/recording

План Отката Или Forward-Fix

СценарийДействиеУсловие Применения
Медиа-стек нестабилен / инцидентFeature flag CALLS_MEDIA_ENABLED = off → деградация до сигналинг-only (503 MSG_CALLS_DISABLED), звонки не поднимаютсяострые проблемы JVB/coturn
Конференции нестабильныCONFERENCE_CALLS_ENABLED = off (503 MSG_CONFERENCES_DISABLED)проблемы групповых звонков
Запись недоступнаRECORDINGS_ENABLED = off; авто-запись через RecordingPolicy не инициируетсянедоступность jibri/хранилища

Изменения В Документации

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • ADR-0055 (собственный мессенджер; §2.4 — media отложен, superseded этим ADR), ADR-0056 (целевая нагрузка / simplicity-first).
  • docs/06-services/messenger/ (детальное ТЗ звонков/конференций/записи), filestorage (хранение файлов записей).