POST /internal/call-recordings/{sessionId}/ingest — Финализация записи (jibri → file storage)
Граница интеграции media-стека: jibri (рекордер) по завершении захвата запускает finalize-скрипт, который
стримит байты записи в messenger. messenger выгружает бинарь в file storage, фиксирует запись в
Available и атомарно публикует событие «запись готова». Это финальный шаг пайплайна записи
(recording-pipeline.md).
Граница именования. Внутренний вебхук от media-стека — имена Jitsi/jibri предметны. Наружу
media-стек нейтрален (SFU, ADR-0062).
Почему байты идут через messenger, а не напрямую в file storage. file storage не предоставляет
presigned-PUT, а инфраструктура media-стека не должна знать привязку tenant → bucket/категория/креды
и не имеет прикладного контекста. Ровно messenger владеет связкой FileId ↔ сессия и правом публиковать
«запись готова», поэтому выгрузка проходит через него: jibri стримит сырой файл, messenger кладёт его
в нужную категорию под стабильным FileId и рождает событие. Так секреты хранилища и tenant-раскладка не
утекают в media-плоскость.
Назначение
Приём финализированной записи от jibri: multipart-запрос из файла (байты записи) и meta-json
JibriFinalizeMeta. messenger проверяет подпись и привязку комнаты к сессии, находит активную запись сессии
(call-recording.md), потоково выгружает бинарь в file storage
(memory-bounded), получает FileId и переводит запись Uploading → Available с метаданными, атомарно публикуя
pin.messenger.call-recording-available.v1. При провале захвата/выгрузки — запись переходит в Failed.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | internal (вебхук media-стека) |
| Feature group | recordings (jibri-финализация) |
| Статус | draft |
| Документ | docs/06-services/messenger/api/internal/jibri-recording-ingest.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ExternalSystem — jibri finalize-скрипт |
| Auth policy / Permissions | подписанный вебхук: X-Pin-Signature = HMAC над сырым телом + X-Pin-Timestamp, окно replay ≤ 5 мин, общий секрет; либо mTLS |
| Scope (RBAC) | не применяется (доверенная граница инфраструктуры) |
| Record-level | сессия резолвится по {sessionId} и сверяется с провизированной комнатой (Room.RoomName); несовпадение → deny (fail-closed) |
| Tenant isolation | не применяется строково; DealTenantId записи — scope выгрузки (категория/бакет file storage) |
| Audit | create/update: → Available (FileId + метаданные) — иммутабельный факт готовности записи |
| Trusted boundary | вход подписан/mTLS; полная валидация подписи → привязки комнаты → наличия файла |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST (multipart/form-data) |
| Route | /api/messenger/internal/call-recordings/{sessionId}/ingest |
| Success status | 200 (тело JibriFinalizeAckModel) |
| Idempotency | по sessionId/RecordingId: повтор финализации на терминальной записи (Available/Failed) — no-op ack; version-guard по RecordingStatus |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Multipart: часть file — байты записи; часть meta — json JibriFinalizeMeta.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
X-Pin-Signature | header | string | Да | HMAC над сырым телом; несовпадение → MSG_JITSI_SIGNATURE_INVALID | секрет не логируется |
X-Pin-Timestamp | header | DateTimeOffset | Да | окно replay ≤ 5 мин | — |
sessionId | route | Guid | Да | сессия провизирована (комната сверяется с Room.RoomName); нет → MSG_JITSI_ROOM_UNKNOWN | — |
file | multipart | поток байт | Да (при Outcome = Completed) | непустой; MIME видео/аудио; отсутствует при Completed → MSG_RECORDING_FILE_MISSING | бинарь не логируется |
meta | multipart | JibriFinalizeMeta | Да | см. модель ниже | — |
| Поле | Тип | Обязательность | Валидация / Ограничения | Описание |
|---|
SessionId | Guid | Да | должен совпадать с {sessionId} маршрута | Сессия-владелец записи. |
ExternalRecordingId | string? | Нет | <= 128; корреляция сессии записи в jibri | Пишется в CallRecordingEntity.ExternalRecordingId. |
Outcome | JibriOutcome | Да | Completed / Failed | Итог захвата у рекордера. |
DurationSeconds | int? | Нет | >= 0; обязателен при Completed | Длительность записи. |
StartedAt | DateTimeOffset | Да | — | Начало захвата. |
EndedAt | DateTimeOffset | Да | >= StartedAt | Конец захвата. |
FailureReason | RecordingFailureReason? | Нет | обязателен при Outcome = Failed | Причина сбоя рекордера. |
Enum JibriOutcome
| Значение | Описание |
|---|
Completed | Захват завершён, файл приложен — выгрузка в file storage. |
Failed | Захват не удался — файла нет, запись переходит в Failed. |
Модель ответа JibriFinalizeAckModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
RecordingId | Guid | Да | CallRecordingEntity.Id | — | Запись, к которой отнесён финализ. |
Status | RecordingStatus | Да | CallRecordingEntity.Status | — | Итог: Available (успех) / Failed / текущий терминал при повторе. |
Алгоритм
- Контекст и доступ. Вызывает jibri finalize-скрипт (
ExternalSystem). Проверяется подпись
X-Pin-Signature над сырым телом + свежесть X-Pin-Timestamp; невалидно → MSG_JITSI_SIGNATURE_INVALID.
RECORDINGS_ENABLED включён (иначе финализация не принимается).
- Резолв сессии и привязка комнаты. По
{sessionId} (и совпадению с meta.SessionId) резолвится
сессия-владелец; её провизированная комната сверяется fail-closed — сессия неизвестна/не имеет
провизированной комнаты → MSG_JITSI_ROOM_UNKNOWN.
- Активная запись. Загружается активная (
Pending/Recording/Uploading) запись сессии
(call-recording.md, INV-REC-7); нет → MSG_RECORDING_NOT_FOUND.
Идемпотентность: запись уже терминальна (Available/Failed) → no-op, возвращается текущий Status
(version-guard по RecordingStatus).
- Ветка
Failed. meta.Outcome = Failed → запись → Failed с FailureReason, StoppedAt = EndedAt;
файл не выгружается. Публикуется доменный RecordingFailedEvent (push RecordingState(Failed)) + внутренний
алерт. Ответ — JibriFinalizeAckModel { Status = Failed }.
- Проверка файла. Ветка
Completed без части file (или пустой поток) → MSG_RECORDING_FILE_MISSING.
- Потоковая выгрузка. Запись переводится
→ Uploading (если не была). Бинарь потоково (memory-bounded,
multipart upload) выгружается в file storage: CategoryCode = CALL_RECORDINGS, FileName = call-{sessionId}-{yyyyMMddHHmmss}.{ext} (уникально в категории; ext/content-type — видео/аудио по формату),
scope выгрузки — DealTenantId записи. Сбой/недоступность хранилища → MSG_STORAGE_UNAVAILABLE, запись
остаётся Uploading для ретрая финализации (идемпотентного).
- Фиксация
Available. По ответу file storage получен FileId. Запись Uploading → Available
(INV-REC-2/5): проставляются FileId, FileName, ContentType, SizeBytes, DurationSeconds, Format,
ExternalRecordingId, StartedAt/StoppedAt/AvailableAt (EndedAt из meta) — далее иммутабельны.
В той же транзакции публикуется интеграционное pin.messenger.call-recording-available.v1 (ключ
SessionId, call-recording-available.md) через outbox и
поднимается доменный RecordingAvailableEvent (push RecordingState(Available) без URL). Всё — одно
согласованное изменение (persist-then-push).
- Результат.
JibriFinalizeAckModel { RecordingId, Status = Available } → 200 OK.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | pin.messenger.call-recording-available.v1 (ключ SessionId) — атомарно с → Available (outbox), консюмеры crm/notifications (call-recording-available.md) |
| Realtime push (hub) | RecordingState(Available) (без URL) на успехе; RecordingState(Failed) при Failed |
| External calls | file storage — multipart-выгрузка бинаря (потоковая, memory-bounded); ретраибельна |
| Cache / projections | запись выходит из partial ix_recording_active (стала терминальной) |
| Уведомления | «запись готова» — через консюмер notifications события; внутренний алерт при Failed/MSG_STORAGE_UNAVAILABLE |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
MSG_JITSI_SIGNATURE_INVALID | 401 | подпись не сошлась / timestamp вне окна replay | «Подпись запроса недействительна» | no retry (переподписать) |
MSG_JITSI_ROOM_UNKNOWN | 403 | {sessionId} не резолвится в провизированную сессию/комнату | «Комната неизвестна» | no retry |
MSG_RECORDING_NOT_FOUND | 404 | у сессии нет активной записи | «Запись не найдена» | no retry |
MSG_RECORDING_FILE_MISSING | 400 | Outcome = Completed, но часть file пуста/отсутствует | «Файл записи отсутствует» | no retry (переслать финализ с файлом) |
MSG_STORAGE_UNAVAILABLE | 503 | сбой/недоступность file storage при выгрузке | «Хранилище временно недоступно» | retry (запись остаётся Uploading) |
Повтор финализации на Available/Failed — не ошибка: 200 с текущим Status (идемпотентность по
RecordingId, version-guard по RecordingStatus).
Acceptance Criteria
- Успешная финализация. Дано активная запись сессии (
Pending/Recording/Uploading), валидная подпись,
Outcome = Completed с непустым file. Когда ingest. Тогда бинарь потоково выгружен в file storage
(CategoryCode = CALL_RECORDINGS), запись → Available с FileId и метаданными (иммутабельны, INV-REC-2/5),
в той же транзакции опубликовано pin.messenger.call-recording-available.v1 (ключ SessionId), push
RecordingState(Available) без URL, ответ 200 {Status: Available}.
- Провал захвата. Дано
Outcome = Failed с FailureReason. Когда ingest. Тогда запись → Failed
(StoppedAt = EndedAt), файл не выгружается, поднят RecordingFailedEvent (push RecordingState(Failed))
и внутренний алерт, ответ 200 {Status: Failed}.
- Идемпотентный повтор. Дано запись уже терминальна (
Available/Failed). Когда повторный ingest.
Тогда no-op, 200 с текущим Status (version-guard по RecordingStatus), выгрузка не повторяется.
- Хранилище недоступно. Дано file storage не принял выгрузку. Когда
ingest c Completed. Тогда
MSG_STORAGE_UNAVAILABLE (503), запись остаётся Uploading — финализацию можно идемпотентно повторить.
- Неверная подпись/replay. Дано
X-Pin-Signature не сошлась или X-Pin-Timestamp вне окна ≤ 5 мин.
Когда ingest. Тогда MSG_JITSI_SIGNATURE_INVALID (401), запись не трогается.
- Неизвестная комната. Дано
{sessionId} не резолвится в провизированную сессию/комнату. Когда ingest.
Тогда MSG_JITSI_ROOM_UNKNOWN (403) (fail-closed).
- Completed без файла. Дано
Outcome = Completed, но часть file пуста/отсутствует. Когда ingest.
Тогда MSG_RECORDING_FILE_MISSING (400).
- Нет активной записи. Дано у сессии нет активной записи. Когда
ingest. Тогда MSG_RECORDING_NOT_FOUND
(404).
- Приватность. В лог идут
sessionId/RecordingId/SizeBytes/Format/result; байты записи, URL и
секрет подписи не логируются.
Совместимость и наблюдаемость
- Новые optional-поля
JibriFinalizeMeta — additive; смена схемы имени файла (CategoryCode/FileName) или
ключа события — breaking (согласованный rollout jibri-скрипта + консюмеров). Событие версионируется суффиксом
.v1 (call-recording-available.md).
- Logs:
ingest received/uploaded/available/failed (sessionId, RecordingId, SizeBytes, Format,
result) — без байтов/URL/токенов; metrics: messenger_recording_ingest_total{result},
messenger_recording_upload_bytes (histogram), upload latency p95; traces — сквозной traceparent;
alert на Failed/storage-unavailable; dashboard pin-messenger.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы