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

Медиа-звонки на 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 — информируется.

ФазаBackendInfra/DevOpsFrontendFile storage
Ф0 ИнфраструктураCA/RIA/R (предпосылки)
Ф1 Backend-фундаментA/RCII
Ф2 1:1 медиаA/RCR
Ф3 КонференцииA/RC (presence-модуль)R
Ф4 ЗаписьA/RR (jibri, finalize)CR (приём)
Ф5 HardeningA/RRCC
Ф6 Транскрипция (future)A/RCIC

Дерево зависимостей

Ключ: Ф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), coturn static-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-types video/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_secret messenger↔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-unique ix_conf_active, XOR-CHECK на call_recordings (CallSessionId? XOR ConferenceSessionId? + SessionKind discriminator), индексы 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-авторизация.
  • Расширить RealtimeDispatchKind media-видами + nullable SessionKind-колонка (миграция 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/IOperationHandlerResult); загрузка FindBy/GetById; переходы CallStatus (Ringing→Answered/Declined/Missed/Ended/Failed) через IUnitOfWork.
  • Hub: CallInvite/сигналы; persist-then-push — смена статуса + realtime_outbox в одной tx; realtime-dispatch минтит MediaJoinModel.AccessToken при доставке (не хранит).
  • media-token (area Query) — свежий 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 /callsCallInvite у 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); jicofo conference.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ч, audit ReadSensitive), 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 недоступен на ingestMSG_STORAGE_UNAVAILABLEFailed (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 — ответвление.

Порядок мержа (каждый пункт — отдельная, независимо ревьюируемая порция):

  1. Инфра (Ф0): манифесты Jitsi + prosody-модули + секреты; параллельно PR file-storage-предпосылок.
  2. Миграции + сущности + VO (Ф1) — до любых хендлеров; XOR-CHECK и индексы в одной миграции.
  3. RecordingPolicy + JitsiTokenService + HMAC-фильтр (Ф1) — security-фундамент до эндпоинтов.
  4. api/calls + hub 1:1 + realtime-dispatch media-kinds (Ф2), затем клиент 1:1.
  5. api/conferences + reservation/presence-вебхуки + reaper (Ф3), затем групповой клиент.
  6. jibri + pin_finalize.sh + ingest + recordings API + событие (Ф4), затем retry-джоба.
  7. Наблюдаемость + нагрузка + 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), coturn static-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:

Реаперы/ретраи:

  • conference-room-reaper, recording-finalize-retry — включены, идемпотентны, покрыты метриками.

Runbook:

  • Реакция на «нет jibri», рестарт prosody (комнаты в памяти — клиент реконнектит по REST), ротация секретов, деградация TURN, откат флага.

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

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

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