Мы часто сталкиваемся с задачей: клиент хочет одновременно транслировать видео на сервер и сохранять локальную копию. Записывать стрим «на лету» — значит параллельно кодировать один видеопоток в два места: на сервер по 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 минус смещение старта.
Дельта между аудио и видео в файле не должна превышать 40ms — это граница восприятия рассинхрона. Наше решение в 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 недели. Стоимость рассчитывается индивидуально. Свяжитесь с нами, чтобы получить консультацию и оценку вашего проекта. Закажите разработку под ключ с индивидуальным подходом.







