Ми часто стикаємося з задачею: клієнт хоче одночасно транслювати відео на сервер і зберігати локальну копію. Записувати стрім «на льоту» — значить паралельно кодувати один відеопотік у два місця: на сервер по RTMP/SRT та в локальний файл MP4/MOV. Це не просто «зберегти те, що транслюється» — у RTMP та файлового запису різні вимоги до GOP-структури, бітрейту та опорних кадрів. Наша команда пропонує перевірене рішення, яке гарантує синхронність аудіо та відео з дельтою не більше 40 мс. Оцінимо ваш проєкт за 1-2 дні та запропонуємо оптимальну архітектуру. Пишіть — допоможемо уникнути типових помилок.
Ключова складність — апаратний енкодер VideoToolbox на iOS не може обслуговувати двох споживачів одночасно. Якщо підключити до однієї VTCompressionSession два AVAssetWriter, отримаєте помилку -12401. Ми обходимо це, використовуючи fanout через CMSampleBuffer: закодований кадр відправляється і в RTMP-чергу, і в локальний writer. На Android задача вирішується через MediaCodec з повторним використанням буфера. Подробиці — нижче.
Як розділити потік без втрати кадрів?
На 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. Такий підхід у 3 рази стабільніший за послідовний запис.
Чому синхронізація аудіо та відео — головний виклик?
Типова проблема: аудіо в MP4-файлі з'їжджає відносно відео. Причина — AVAudioEngine та AVCaptureVideoDataOutput працюють на різних таймлайнах. CMSampleBuffer з камери використовує kCMClockType_System, аудіобуфери — AVAudioTime з hostTime.
Рішення: нормалізуємо всі часові мітки відносно CACurrentMediaTime() при старті запису, використовуємо його як базовий clock. Для аудіо — AVAudioSourceNode з явним AVAudioTime, для відео — CMSampleBufferGetPresentationTimeStamp мінус зміщення старту.
Дельта між аудіо та відео у файлі не повинна перевищувати 40 ms — це межа сприйняття розсинхрону. Наше рішення у 3 рази стабільніше за наївний підхід з різними таймлайнами, що безпосередньо впливає на економію бюджету при пост-продакшні.
Запис через ReplayKit: коли виправдано?
Якщо стрім йде через ReplayKit (RPBroadcastSampleHandler), запис організовується інакше: обробник отримує RPSampleBufferType.video та RPSampleBufferType.audioApp — їх можна паралельно писати в AVAssetWriter без власного енкодера.
Обмеження: ReplayKit додає затримку 2–5 секунд до захоплення. Для стріму екрана прийнятно, для камерного стріму — ні. Вартість розробки з ReplayKit зазвичай нижча, але якість та затримка гірші за прямий захоплення.
Управління сховищем: практичні поради
Перед стартом запису перевіряємо доступне місце:
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 хвилин — новий файл) знижує ризик втрати даних при краші та спрощує подальше завантаження на сервер.
Таблиця співвідношення бітрейту та розміру файлу
| Бітрейт | Тривалість | Розмір файлу |
|---|---|---|
| 2 Мбіт/с | 1 година | ~900 МБ |
| 4 Мбіт/с | 1 година | ~1.8 ГБ |
| 8 Мбіт/с | 1 година | ~3.6 ГБ |
| Підхід | Затримка | Якість | Складність |
|---|---|---|---|
| Прямий захоплення | <100 мс | Оригінальна | Висока |
| ReplayKit | 2–5 с | Стиснута | Низька |
Як ми реалізуємо паралельний запис: покроково
- Аналіз вимог: бітрейт, роздільна здатність, необхідність синхронізації A/V.
- Вибір стеку: Swift 5.9 + VideoToolbox для iOS, Kotlin + MediaCodec для Android.
- Налаштування
VTCompressionSessionз параметрами для RTMP та локального запису. - Реалізація fanout-черги на двох
DispatchQueue. - Синхронізація аудіо та відео через спільний clock.
- Інтеграція зі сховищем та сегментацією.
- Тестування на реальних пристроях (iPhone 14, Pixel 7, Samsung S23).
Що входить в роботу
- Проєктування архітектури та вибір оптимізованих параметрів кодування.
- Реалізація паралельного запису з fanout та синхронізацією A/V.
- Управління сховищем: перевірка місця, сегментування, автоматичне очищення.
- Інтеграція з вашим бекендом (RTMP-сервер, хмарне сховище).
- Документація з інтеграції та експлуатації.
- Підтримка на етапі тестування та викладання в App Store / Google Play.
За час роботи ми реалізували понад 20 проєктів з мобільним стрімінгом, включаючи записи прямих ефірів для великих медіа. Гарантуємо стабільність запису навіть при нестабільній мережі. Використовуємо сертифіковані підходи (App Store Review Guidelines Section 4.2). Вартість розробки варіюється, але інвестиції окупаються за рахунок зниження витрат на пост-продакшн та повторні стріми.
Терміни та вартість
Базовий паралельний запис (iOS, один потік): 1–1.5 тижні. Повна реалізація з синхронізацією A/V, управлінням сховищем, сегментацією, Android-підтримкою: 3–4 тижні. Вартість розраховується індивідуально. Зв'яжіться з нами, щоб отримати консультацію та оцінку вашого проєкту. Замовте розробку під ключ з індивідуальним підходом.







