Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик)
Мастер-документ доставки media-стека звонков messenger от инфраструктуры Jitsi до записи и hardening.
Сводит воедино домен/API/события (контракт), интеграционный гид Jitsi (эта папка) и внешние предпосылки
(file storage) в последовательный план по фазам, из которого команда планирует спринты. Контракт и
конкретика Jitsi живут в отдельных документах — здесь порядок, зависимости, критерии готовности и
ответственность.
Назначение и как читать
| Роль | Что берёт отсюда |
|---|---|
| Аналитик / тимлид | Дерево зависимостей, фазы с DoR/DoD, RACI, критический путь и порядок мержа — для декомпозиции на спринты и приёмки. |
| Разработчик (backend) | Порядок реализации сущностей → 1:1 → конференций → записи; ссылки на backend-implementation.md и контрактные доки. |
| Infra / DevOps | Фаза Ф0 (Jitsi-компоненты, секреты, флаги) — блокирующая предпосылка для всего остального. |
| Frontend | Нативный клиент lib-jitsi-meet по фазам 1:1 → конференции (jitsi-client.md). |
Читать сверху вниз: сначала «Участники и потоки», затем «Дерево зависимостей», затем фазы Ф0→Ф6 по порядку. Каждая фаза самодостаточна: Цель → DoR (предпосылки входа) → Задачи (чеклист) → Артефакты → DoD (критерии готовности) → Зависимости → Риски. Что и где реализуется по коду — backend-implementation.md; приёмочные сценарии — testing-and-acceptance.md; предпосылки хранилища — filestorage-prerequisites.md.
Участники и потоки работ
Поставка идёт четырьмя параллельными, но зависимыми потоками:
| Поток | Зона ответственности | Ключевые артефакты |
|---|---|---|
Backend (messenger .NET) | Сущности/EF/миграции, CQRS-хендлеры calls/conferences/recordings, internal-вебхуки, JWT-минтинг, realtime-dispatch, реаперы | backend-implementation.md, домен/API/события |
| Infra / DevOps | Развёртывание prosody/jicofo/JVB/jibri/coturn, секреты (vault), feature-флаги, PIN-модули prosody, дашборды/алерты | jitsi-deployment.md, jitsi-modifications.md |
Frontend (front-end Next.js) | Нативный клиент lib-jitsi-meet, мост в SignalR, UI звонка/конференции/индикатора записи | jitsi-client.md |
| File storage (внешний сервис) | Категория CALL_RECORDINGS, видео content-types, приём multipart-стрима | filestorage-prerequisites.md |
RACI по фазам
R — исполнитель, A — владелец результата, C — консультируется, I — информируется.
| Фаза | Backend | Infra/DevOps | Frontend | File storage |
|---|---|---|---|---|
| Ф0 Инфраструктура | C | A/R | I | A/R (предпосылки) |
| Ф1 Backend-фундамент | A/R | C | I | I |
| Ф2 1:1 медиа | A/R | C | R | — |
| Ф3 Конференции | A/R | C (presence-модуль) | R | — |
| Ф4 Запись | A/R | R (jibri, finalize) | C | R (приём) |
| Ф5 Hardening | A/R | R | C | C |
| Ф6 Транскрипция (future) | A/R | C | I | C |
Дерево зависимостей
Ключ: Ф0 разблокирует всё; Ф1 — общий фундамент 1:1 и конференций; конференции (Ф3) переиспользуют media-token и dispatch из 1:1 (Ф2); запись (Ф4) требует и конференций (Ф3), и file-storage-предпосылок из Ф0; Ф5 идёт поверх всего доставленного функционала; Ф6 — необязательный шов.
Фазы поставки
Ф0 — Инфраструктура
Цель. Поднять саморазмещённый Jitsi (сигналинг + медиа + TURN) под целевую нагрузку и подготовить блокирующие предпосылки file storage — так, чтобы к комнате можно было подключиться нативным клиентом со smoke-JWT.
Предпосылки (DoR). Утверждён ADR-0062;
доступ к vault; k8s-кластер; Nomad-референс thirdparty/jitsi/infra-provisioning/nomad/* под рукой.
Задачи.
- Helm/k8s-манифесты prosody/jicofo/JVB/coturn (jibri — в Ф4), перенос env из Nomad-референса (jitsi-deployment.md, таблица env).
- Секреты в vault:
JWT_APP_SECRET(HS256, общий messenger↔prosody), coturnstatic-auth-secret. - Prosody-модули PIN:
mod_auth_token/mod_token_affiliation(роль модератора),mod_reservations(admission),mod_pin_muc_presence(presence) — инвентарь jitsi-modifications.md. - Ingress:
wss://<MediaDomain>/xmpp-websocket, публичный UDP 10000 (JVB), coturn 443. - Feature-флаги заведены (выключены):
CALLS_MEDIA_ENABLED,CONFERENCE_CALLS_ENABLED,RECORDINGS_ENABLED. - File storage предпосылки (параллельно, блокирует Ф4): категория
CALL_RECORDINGS, видео content-typesvideo/mp4/video/webm/video/x-matroska, схемаFileName = call-{sessionId}-{ts}— filestorage-prerequisites.md.
Артефакты. Работающий namespace Jitsi; манифесты в репо инфры; секреты в vault; чеклист env.
DoD. Smoke-join: клиентом lib-jitsi-meet со статически выпущенным JWT удаётся войти в тестовую комнату,
установить медиа между двумя peer'ами; prosody принимает подпись JWT_APP_SECRET; /metrics JVB/jicofo
отдаются; file-storage категория CALL_RECORDINGS создана и принимает тестовый видео-upload.
Зависимости. Нет (корень дерева).
Риски + митигации.
- UDP 10000 не проброшен / за NAT → coturn обязателен как relay; проверить TURN-путь отдельно.
- Рассинхрон
JWT_APP_ID/app_secretmessenger↔prosody → единый источник в vault, один ключ на оба. - File-storage-предпосылки затягиваются → вынести в самое начало Ф0, они не зависят от Jitsi и блокируют только Ф4.
Ф1 — Backend-фундамент
Цель. Персистентная и security-основа: агрегаты, EF-маппинг, миграции, политика записи, минтинг JWT и защита internal-эндпоинтов — без пользовательского функционала звонков.
Предпосылки (DoR). Ф0 DoD (есть prosody с JWT_APP_SECRET); контрактные доки домена приняты
(call-session.md, conference-session.md,
call-participant.md, call-recording.md).
Задачи.
- Сущности
CallSessionEntity,ConferenceSessionEntity,CallParticipantEntity(owned-child),CallRecordingEntityкакAggregateRoot<Guid>/Entity<Guid>; enum'ы с.HasConversion<string>(). - Owned VO
MediaRoom{RoomName,Domain}черезConfigureOwnedTypes. - EF-миграции: таблицы
messenger.conference_sessions,messenger.call_participants,messenger.call_recordings; partial-uniqueix_conf_active, XOR-CHECK наcall_recordings(CallSessionId?XORConferenceSessionId?+SessionKinddiscriminator), индексыix_conf_last_activity/ix_conf_deal_id. -
RecordingPolicy.ShouldAutoRecord(chatKind, sessionKind)— типизированное решение (не флаг). -
JitsiTokenService— минтинг HS256 JWT (room-scoped, claims per jitsi-jwt.md); TURN-креды изstatic-auth-secret. Токен не хранится/не логируется. - HMAC-фильтр internal-эндпоинтов:
X-Pin-Signature+ timestamp replay-окно (или mTLS), fail-closed сверкаRoomName; ExternalSystem-авторизация. - Расширить
RealtimeDispatchKindmedia-видами + nullableSessionKind-колонка (миграцияrealtime_outbox).
Артефакты. Миграции; RecordingPolicy; JitsiTokenService; HMAC-фильтр; юнит-тесты policy/token/фильтра.
Детали кода — backend-implementation.md.
DoD. Миграции применяются на чистой БД и откатываются; XOR-CHECK отвергает запись с двумя FK и запись без
FK; JitsiTokenService выдаёт JWT, который принимает prosody из Ф0 (интеграционная проверка подписи); HMAC-фильтр
отвергает запрос без/с протухшей подписью и с несовпадающим RoomName.
Зависимости. Ф0.
Риски + митигации.
- XOR-инвариант обходится через приложение → закрепить CHECK на уровне БД, не только в домене.
- Дрейф claim-структуры от того, что читает prosody → интеграционный тест «минт → prosody принимает» в CI.
Ф2 — 1:1 медиа
Цель. Рабочий звонок один-на-один: инициация, ответ/отклонение/завершение, вход в комнату через
MediaJoinModel, медиа p2p между двумя участниками.
Предпосылки (DoR). Ф1 DoD; CALLS_MEDIA_ENABLED доступен для включения в dev.
Задачи.
- Эволюция
api/calls: убрать SDP/ICE из контрактов (медиа — вне бэкенда), выдаватьMediaJoinModel. Роуты:POST /calls,POST /calls/{id}/{answer,decline,end},GET /calls,POST /calls/{id}/media-token. - CQRS-хендлеры (
IOperationRequest/IOperationHandler→Result); загрузкаFindBy/GetById; переходыCallStatus(Ringing→Answered/Declined/Missed/Ended/Failed) черезIUnitOfWork. - Hub:
CallInvite/сигналы; persist-then-push — смена статуса +realtime_outboxв одной tx; realtime-dispatch минтитMediaJoinModel.AccessTokenпри доставке (не хранит). -
media-token(areaQuery) — свежийMediaJoinModelдля reconnect/refresh доexp. - Нативный клиент 1:1 на
lib-jitsi-meetсp2p.enabled(JVB подключается при escalation); мост событий в SignalR (jitsi-client.md). - realtime-dispatch: media-kinds в
RealtimeDispatchKind.
Артефакты. api/calls/*; hub-контракты 1:1; клиентский модуль звонка; e2e-сценарий 1:1.
DoD. Два пользователя проходят цикл POST /calls → CallInvite у callee → answer → медиа установлено
(p2p) → end; decline/Missed-таймаут работают; AccessToken/TURN-Credential отсутствуют в БД и логах;
media-token выдаёт валидный свежий дескриптор.
Зависимости. Ф1.
Риски + митигации.
- Гонки статусов (double-answer/decline) → оптимистическая блокировка + guard переходов в домене.
- Токен протухает во время звонка → клиент рефрешит через
media-tokenзаранее (доexp).
Ф3 — Конференции
Цель. Групповой звонок в чате сделки: старт, динамический ростер, авторитетный серверный presence, авто-завершение по опустошению.
Предпосылки (DoR). Ф2 DoD (переиспользуются dispatch/media-token/hub-паттерны); prosody-модули
mod_reservations + mod_pin_muc_presence из Ф0 работают.
Задачи.
-
api/conferences:POST /chats/{chatId}/conferences,POST /conferences/{id}/{join,leave,end,media-token},GET /conferences/{id},GET /chats/{chatId}/conferences. Гейт доступа = членство в Deal-чате (INV-CHAT-4). - Ростер
CallParticipantEntity:Invited/Joined/Left/Declined; пересчётActiveParticipantCount/PeakParticipantCount; ролиModerator/Speaker, передача модератора при выходе (INV-CONF-6). - Reservation-вебхук:
POST /internal/jitsi/reservations(+DELETE /{roomName}) — admission, fail-closed сверка имени,Provisioning → Active. - Presence-вебхук:
POST /internal/jitsi/events(join/leave/heartbeat) — авторитет ростера (INV-CPART-5, при расхождении побеждает серверное событие). - Background
conference-room-reaper: закрытие осиротевших/зависших комнат (EmptyTimeout/Timeout), идемпотентно поLastActivityAt(../background/conference-room-reaper.md). - Групповой клиент/виджет: ростер, screen-share (
ScreenShareState), индикатор модератора.
Артефакты. api/conferences/*; internal reservation/presence-эндпоинты; reaper; групповой UI-виджет.
DoD. 3+ участника входят/выходят, ростер сходится с серверным presence; admission fail-closed отвергает
неизвестную комнату (MSG_JITSI_ROOM_UNKNOWN); выход последнего → Ended (LastParticipantLeft); reaper
закрывает пустую комнату после grace; передача модератора при выходе текущего.
Зависимости. Ф1, Ф2.
Риски + митигации.
- Потеря presence-события → «призрачный» участник → reaper по
LastActivityAt+ heartbeat; серверное событие авторитетно. - Двойной старт конференции на чат → partial-unique
ix_conf_active+ command-guard (INV-CONF-2).
Ф4 — Запись
Цель. Сквозная авто-запись по политике: захват jibri → финализация → выгрузка в file storage →
Available → Kafka-событие; выдача записи по запросу.
Предпосылки (DoR). Ф3 DoD; file-storage-предпосылки из Ф0 выполнены
(filestorage-prerequisites.md); RECORDINGS_ENABLED доступен.
Задачи.
- Развернуть jibri (диск
/mnt/recordings,SYS_ADMIN, headless Chrome); jicofoconference.enable-moderator-checks=true, brewery-jid. -
pin_finalize.sh(заменаjitsi_uploader.sh,JIBRI_FINALIZE_RECORDING_SCRIPT_PATH): читаетmetadata.json(session_id,pin.*), стримит multipart в ingest, удаляет директорию только при 2xx (recording-pipeline.md). - Ingest-эндпоинт
POST /internal/call-recordings/{sessionId}/ingest: memory-bounded streaming в file storage (IFilesService.UploadFile/POST /api/files/upload-file/form);Recording → Uploading → Available(FileId+SizeBytes+DurationSeconds+Format) атомарно с публикацией Kafka. - Событие
pin.messenger.call-recording-available.v1(ключSessionId) через транзакционный outbox. -
RecordingPolicyавто-старт:CallRecordingEntityвPending, индикаторNoticeState → AnnouncedдоRecording(INV-REC-4). - Recordings API:
GET /calls/{id}/recordings,/conferences/{id}/recordings,/recordings/{id},/recordings/{id}/download(CDN presigned GET ~1ч, auditReadSensitive),POST /conferences/{id}/recording/stop. - Background
recording-finalize-retry: записи, застрявшие вUploading/Pending→ ретрай/Failed.
Артефакты. jibri-манифест; pin_finalize.sh; ingest-эндпоинт; recordings API; retry-джоба;
Kafka-контракт call-recording-available.md.
DoD. Конференция сделки авто-пишется: индикатор объявлен до старта; после завершения файл появляется в
file storage, запись → Available, call-recording-available.v1 опубликовано с ключом SessionId;
download выдаёт presigned-GET; при 2xx локальная директория удалена, при не-2xx — сохранена и запись вне
Available; retry-джоба поднимает застрявшую выгрузку.
Зависимости. Ф3; file-storage-предпосылки (Ф0).
Риски + митигации.
- Нет свободного jibri при запросе записи → алерт
MSG_RECORDING_PROVISION_FAILED; политика не роняет сам звонок (запись опциональна). - Буферизация большого файла в память → строго streamed multipart в скрипте, ingest и выгрузке.
- File storage недоступен на ingest →
MSG_STORAGE_UNAVAILABLE→Failed (UploadFailed), retry-джоба.
Ф5 — Hardening
Цель. Довести до продакшн-готовности: наблюдаемость, нагрузочная проверка, безопасность, реаперы/ретраи.
Предпосылки (DoR). Ф2–Ф4 DoD (весь функционал доставлен на dev/stage).
Задачи.
- Метрики: join p95 (invite→media), доля успешных записей, длительности сессий, presence-lag; дашборды
JVB/jicofo/coturn/jibri (
/metrics). - Нагрузочная проверка в рамках ADR-0056 (1–10k активных, конференции редки): число параллельных конференций/записей, авто-масштаб JVB/jibri.
- Безопасность: room-scoped короткоживущий JWT, fail-closed admission, отсутствие токенов в логах/трейсах/событиях, HMAC replay-окно, ротация секретов (dual-accept).
- Реаперы/ретраи под нагрузкой:
conference-room-reaper,recording-finalize-retry— идемпотентность и идёмпотентные метрики. - Алерты: нет свободного jibri, всплеск
Failed-записей, деградация join p95, protухание presence.
Артефакты. Дашборды; отчёт нагрузочного теста; security-чеклист; runbook.
DoD. Дашборды показывают live-метрики; нагрузочный профиль целевого envelope проходит без деградации; security-чеклист пройден (нет утечки токенов, fail-closed подтверждён); алерты срабатывают на инъецированных отказах.
Зависимости. Ф2, Ф3, Ф4.
Риски + митигации.
- Скрытая утечка токена в трейсе → явный тест на отсутствие
AccessToken/Credentialв логах/событиях. - Реапер закрывает живую комнату → grace + heartbeat + идемпотентность (обновляет только
Provisioning/Active).
Ф6 — Транскрипция / конспект (future)
Цель. Необязательный шов STT/конспект поверх готовой записи — не блокирует MVP.
Предпосылки (DoR). Ф4 DoD (есть call-recording-available.v1 + файл в file storage).
Задачи.
- Консюмер
call-recording-available.v1→ пайплайн STT →CallMeetingInsightEntity(конспект/таймкоды). - jigasi/multitrack-audio при необходимости (
.mka) — transcription-roadmap.md.
Артефакты. Роадмап-документ; консюмер-заготовка.
DoD. Шов подтверждён (событие доступно и потребляемо); функционал — вне MVP.
Зависимости. Ф4.
Риски + митигации. Преждевременное усложнение → держать за отдельным флагом, не тянуть в критический путь.
Критический путь и порядок мержа
Критический путь: Ф0 (Jitsi + file-storage prereqs) → Ф1 (фундамент) → Ф2 (1:1) → Ф3 (конференции) → Ф4 (запись) → Ф5 (hardening). Ф6 — ответвление.
Порядок мержа (каждый пункт — отдельная, независимо ревьюируемая порция):
- Инфра (Ф0): манифесты Jitsi + prosody-модули + секреты; параллельно PR file-storage-предпосылок.
- Миграции + сущности + VO (Ф1) — до любых хендлеров; XOR-CHECK и индексы в одной миграции.
RecordingPolicy+JitsiTokenService+ HMAC-фильтр (Ф1) — security-фундамент до эндпоинтов.api/calls+ hub 1:1 + realtime-dispatch media-kinds (Ф2), затем клиент 1:1.api/conferences+ reservation/presence-вебхуки + reaper (Ф3), затем групповой клиент.- jibri +
pin_finalize.sh+ ingest + recordings API + событие (Ф4), затем retry-джоба. - Наблюдаемость + нагрузка + security-полировка (Ф5).
Флаги удерживают незавершённое: CALLS_MEDIA_ENABLED включается по завершении Ф2, CONFERENCE_CALLS_ENABLED
— Ф3, RECORDINGS_ENABLED — Ф4. Мерж возможен раньше включения флага.
Master sequence — 1:1 звонок (end-to-end)
Master sequence — конференция (start → join → auto-record → finalize → available)
Продакшн-чеклист
Feature-флаги (включать по завершении фазы):
-
CALLS_MEDIA_ENABLED(после Ф2),CONFERENCE_CALLS_ENABLED(после Ф3),RECORDINGS_ENABLED(после Ф4).
Секреты (vault):
-
JWT_APP_SECRET(общий messenger↔prosody, HS256), coturnstatic-auth-secret, HMAC-секрет internal-вебхуков; политика ротации с dual-accept окном.
Дашборды:
- Join p95 (invite→media), доля успешных записей, активные конференции/записи, presence-lag; панели
JVB/jicofo/coturn/jibri
/metrics.
Алерты:
- Нет свободного jibri (
MSG_RECORDING_PROVISION_FAILED); всплескFailed-записей; деградация join p95; протухание presence / зависшие комнаты; file storage недоступен на ingest.
Инфраструктура:
- Публичный UDP 10000 (JVB), coturn 443,
wss://<MediaDomain>/xmpp-websocket; persistent volume/mnt/recordingsдля jibri;JIBRI_USAGE_TIMEOUTавто-shutdown.
File storage:
- Категория
CALL_RECORDINGS, видео content-types,FileName = call-{sessionId}-{ts}(filestorage-prerequisites.md).
Реаперы/ретраи:
-
conference-room-reaper,recording-finalize-retry— включены, идемпотентны, покрыты метриками.
Runbook:
- Реакция на «нет jibri», рестарт prosody (комнаты в памяти — клиент реконнектит по REST), ротация секретов, деградация TURN, откат флага.
Обратные ссылки
Автоссылки: где используется этот документ.
- calls (3): Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Backend-реализация медиа-звонков: спека → код (messenger), Jitsi-звонки: план верификации и приёмки
Связанные документы
- Реализация backend: backend-implementation.md; предпосылки хранилища: filestorage-prerequisites.md; приёмка/тесты: testing-and-acceptance.md.
- Интеграционный гид: README.md, jitsi-deployment.md, jitsi-jwt.md, jitsi-rooms.md, jitsi-client.md, recording-pipeline.md, jitsi-modifications.md, transcription-roadmap.md.
- Контракт: домен call-session.md, conference-session.md, call-participant.md, call-recording.md; API calls, conferences, recordings, internal, hub; события conference-ended.md, call-recording-available.md.
- Решение: ADR-0062, ADR-0056 simplicity-first.