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

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.

Metadata

ПолеЗначение
Сервис/контекстmessenger
API areainternal (вебхук media-стека)
Feature grouprecordings (jibri-финализация)
Статусdraft
Документdocs/06-services/messenger/api/internal/jibri-recording-ingest.md

Актор и доступ

ПроверкаЗначение
Actor typeExternalSystem — 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)
Auditcreate/update: → Available (FileId + метаданные) — иммутабельный факт готовности записи
Trusted boundaryвход подписан/mTLS; полная валидация подписи → привязки комнаты → наличия файла

HTTP-контракт

ПолеЗначение
MethodPOST (multipart/form-data)
Route/api/messenger/internal/call-recordings/{sessionId}/ingest
Success status200 (тело JibriFinalizeAckModel)
Idempotencyпо sessionId/RecordingId: повтор финализации на терминальной записи (Available/Failed) — no-op ack; version-guard по RecordingStatus
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

Multipart: часть file — байты записи; часть meta — json JibriFinalizeMeta.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
X-Pin-SignatureheaderstringДаHMAC над сырым телом; несовпадение → MSG_JITSI_SIGNATURE_INVALIDсекрет не логируется
X-Pin-TimestampheaderDateTimeOffsetДаокно replay ≤ 5 мин
sessionIdrouteGuidДасессия провизирована (комната сверяется с Room.RoomName); нет → MSG_JITSI_ROOM_UNKNOWN
filemultipartпоток байтДа (при Outcome = Completed)непустой; MIME видео/аудио; отсутствует при CompletedMSG_RECORDING_FILE_MISSINGбинарь не логируется
metamultipartJibriFinalizeMetaДасм. модель ниже

JibriFinalizeMeta

ПолеТипОбязательностьВалидация / ОграниченияОписание
SessionIdGuidДадолжен совпадать с {sessionId} маршрутаСессия-владелец записи.
ExternalRecordingIdstring?Нет<= 128; корреляция сессии записи в jibriПишется в CallRecordingEntity.ExternalRecordingId.
OutcomeJibriOutcomeДаCompleted / FailedИтог захвата у рекордера.
DurationSecondsint?Нет>= 0; обязателен при CompletedДлительность записи.
StartedAtDateTimeOffsetДаНачало захвата.
EndedAtDateTimeOffsetДа>= StartedAtКонец захвата.
FailureReasonRecordingFailureReason?Нетобязателен при Outcome = FailedПричина сбоя рекордера.

Enum JibriOutcome

ЗначениеОписание
CompletedЗахват завершён, файл приложен — выгрузка в file storage.
FailedЗахват не удался — файла нет, запись переходит в Failed.

Модель ответа JibriFinalizeAckModel

ПолеТипОбязательностьИсточникMaskingОписание
RecordingIdGuidДаCallRecordingEntity.IdЗапись, к которой отнесён финализ.
StatusRecordingStatusДаCallRecordingEntity.StatusИтог: Available (успех) / Failed / текущий терминал при повторе.

Алгоритм

  1. Контекст и доступ. Вызывает jibri finalize-скрипт (ExternalSystem). Проверяется подпись X-Pin-Signature над сырым телом + свежесть X-Pin-Timestamp; невалидно → MSG_JITSI_SIGNATURE_INVALID. RECORDINGS_ENABLED включён (иначе финализация не принимается).
  2. Резолв сессии и привязка комнаты. По {sessionId} (и совпадению с meta.SessionId) резолвится сессия-владелец; её провизированная комната сверяется fail-closed — сессия неизвестна/не имеет провизированной комнаты → MSG_JITSI_ROOM_UNKNOWN.
  3. Активная запись. Загружается активная (Pending/Recording/Uploading) запись сессии (call-recording.md, INV-REC-7); нет → MSG_RECORDING_NOT_FOUND. Идемпотентность: запись уже терминальна (Available/Failed) → no-op, возвращается текущий Status (version-guard по RecordingStatus).
  4. Ветка Failed. meta.Outcome = Failed → запись → Failed с FailureReason, StoppedAt = EndedAt; файл не выгружается. Публикуется доменный RecordingFailedEvent (push RecordingState(Failed)) + внутренний алерт. Ответ — JibriFinalizeAckModel { Status = Failed }.
  5. Проверка файла. Ветка Completed без части file (или пустой поток) → MSG_RECORDING_FILE_MISSING.
  6. Потоковая выгрузка. Запись переводится → Uploading (если не была). Бинарь потоково (memory-bounded, multipart upload) выгружается в file storage: CategoryCode = CALL_RECORDINGS, FileName = call-{sessionId}-{yyyyMMddHHmmss}.{ext} (уникально в категории; ext/content-type — видео/аудио по формату), scope выгрузки — DealTenantId записи. Сбой/недоступность хранилища → MSG_STORAGE_UNAVAILABLE, запись остаётся Uploading для ретрая финализации (идемпотентного).
  7. Фиксация 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).
  8. Результат. JibriFinalizeAckModel { RecordingId, Status = Available }200 OK.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventspin.messenger.call-recording-available.v1 (ключ SessionId) — атомарно с → Available (outbox), консюмеры crm/notifications (call-recording-available.md)
Realtime push (hub)RecordingState(Available) (без URL) на успехе; RecordingState(Failed) при Failed
External callsfile storage — multipart-выгрузка бинаря (потоковая, memory-bounded); ретраибельна
Cache / projectionsзапись выходит из partial ix_recording_active (стала терминальной)
Уведомления«запись готова» — через консюмер notifications события; внутренний алерт при Failed/MSG_STORAGE_UNAVAILABLE

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
MSG_JITSI_SIGNATURE_INVALID401подпись не сошлась / timestamp вне окна replay«Подпись запроса недействительна»no retry (переподписать)
MSG_JITSI_ROOM_UNKNOWN403{sessionId} не резолвится в провизированную сессию/комнату«Комната неизвестна»no retry
MSG_RECORDING_NOT_FOUND404у сессии нет активной записи«Запись не найдена»no retry
MSG_RECORDING_FILE_MISSING400Outcome = Completed, но часть file пуста/отсутствует«Файл записи отсутствует»no retry (переслать финализ с файлом)
MSG_STORAGE_UNAVAILABLE503сбой/недоступность 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.

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

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

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