Реалізація записи прямої трансляції в мобільному додатку
Записувати стрім «на льоту» — значит паралельно кодувати один відео потік у два місця: на сервер (RTMP/SRT) і в локальний файл (MP4/MOV). Це не просто «зберегти те, що транслюється» — у RTMP й файловій записі різні вимоги до GOP-структури, бітрейту й опорних кадрів.
Проблема розділення потоків
На iOS наївний підхід — запустити другий AVAssetWriter паралельно з енкодером трансляції. Не працює: VideoToolbox-сесію (VTCompressionSession) не можна використовувати одночасно у двох consumer-ах. При спробі отримаєте -12401 kVTVideoEncoderNotAvailableNowErr.
Правильний підхід: один VTCompressionSession → закодовані CMSampleBuffer → fanout до двох writer-ів. Після отримання кодованого буфера в VTCompressionOutputCallback пишемо його й у RTMP-чергу, й у AVAssetWriter.
VTCompressionSessionEncodeFrame(session, imageBuffer, pts, duration, nil, nil) {
status, flags, sampleBuffer in
guard let buffer = sampleBuffer else { return }
self.rtmpQueue.enqueue(buffer) // → стрім
self.fileWriterInput.append(buffer) // → локальний файл
}
Обидва виклики не повинні бути синхронними на одному потоці — якщо RTMP-черга заблокована (мережа упала), fileWriterInput.append не повинен чекати. Використовуємо дві незалежні DispatchQueue.
Синхронізація аудіо й відео у записі
Типова проблема: аудіо в MP4-файлі з'їжджає відносно відео. Причина — AVAudioEngine й AVCaptureVideoDataOutput працюють на різних таймлайнах. CMSampleBuffer з камери використовує kCMClockType_System, аудіобуфери — AVAudioTime з hostTime.
Рішення: нормалізуємо всі часові мітки відносно CACurrentMediaTime() при старті записи, використовуємо його як базовий clock. Для аудіо — AVAudioSourceNode з явним AVAudioTime, для відео — CMSampleBufferGetPresentationTimeStamp мінус зміщення старту.
Дельта між аудіо й відео у файлі не повинна перевищувати 40ms — це межа сприйняття рассинхрону.
Запис через ReplayKit (альтернативний сценарій)
Якщо стрім іде через ReplayKit (RPBroadcastSampleHandler), запис організується інакше: обробник отримує RPSampleBufferType.video й RPSampleBufferType.audioApp — їх можна паралельно писати в AVAssetWriter без власного енкодера.
Обмеження: ReplayKit додає затримку 2–5 секунд до захисту. Для стрима екрана приймемо, для камерного стрима — ні.
Управління хранилищем
Перед стартом записи перевіряємо доступне місце:
let attrs = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
let freeSpace = attrs[.systemFreeSize] as? Int64 ?? 0
let estimatedSize = Int64(bitrate / 8) * expectedDurationSeconds
guard freeSpace > estimatedSize * 2 else { /* попередження */ }
Коефіцієнт 2 — запас під часові файли AVAssetWriter й OS. При 4 Мбіт/с година стрима займає ~1.8 ГБ.
Сегментна запис (кожні 30 хвилин — новий файл) знижує ризик втрати даних при краху й спрощує подальшу загрузку на сервер.
Терміни
Базова паралельна запис (iOS, один потік): 1–1.5 тижні. Повна реалізація з синхронізацією A/V, управлінням хранилищем, сегментацією, Android-підтримкою: 3–4 тижні. Вартість розраховується індивідуально.







