Реализация мульти-камерного стриминга с мобильного устройства
Мы реализовали несколько проектов мульти-камерного стриминга для iOS и Android. Одна из ключевых задач — одновременная съёмка с фронтальной и основной камеры. Она стала технически реализуемой на iOS с появлением AVCaptureMultiCamSession в iOS 13. До этого любой «мульти-камерный» стрим был симуляцией: переключение с задержкой или заранее записанный второй источник. Сейчас на iPhone XS и новее можно захватить оба потока одновременно — с ограничениями, о которых ниже. Наш 8-летний опыт в мобильной разработке и более 30 проектов со стримингом позволяют гарантировать стабильное решение под нагрузкой.
Почему мульти-камерный стриминг требует кастомной реализации?
Готовые SDK (например, Wowza GoCoder или HaishinKit) дают базовый мульти-кам, но не оптимизируют тепловой режим и не предлагают гибкого PiP. В нашей реализации на Metal мы управляем каждым этапом: от захвата до композиции. Это позволяет адаптироваться под разные сценарии — от вебинаров до удалённой инспекции. Кастомная архитектура даёт до 40% более стабильный FPS при длительных трансляциях по сравнению с коробочными решениями.
Как проверить поддержку на устройствах?
| Платформа | Условия поддержки | Рекомендованные устройства |
|---|---|---|
| iOS | AVCaptureMultiCamSession.isMultiCamSupported |
iPhone XS, 11, 12, 13, 14, SE (2nd gen+) |
| Android | CameraManager.getCameraCharacteristics + LOGICAL_MULTI_CAMERA |
Samsung Galaxy S22+, Google Pixel 6+, OnePlus 9+ |
На Android поддержка зависит от OEM: например, Xiaomi Redmi Note 10 не отдаёт два потока без ошибок. Тестирование на 5–10 целевых моделях обязательно.
Как обеспечить синхронизацию потоков?
Синхронизация — ключевой момент. Без неё PiP-окно «дёргается» относительно основного видео. Используем AVCaptureDataOutputSynchronizer на iOS и соответствующие timestamp'ы на Android. Это гарантирует рассинхронизацию не более одного кадра (33 мс при 30 fps).
import AVFoundation let backOutput = AVCaptureVideoDataOutput() let frontOutput = AVCaptureVideoDataOutput() let synchronizer = AVCaptureDataOutputSynchronizer(dataOutputs: [backOutput, frontOutput]) synchronizer.setDelegate(self, queue: syncQueue) Архитектурные ограничения, которые нужно знать до начала
AVCaptureMultiCamSession поддерживается не на всех устройствах. Перед инициализацией обязательная проверка:
import AVFoundation let multiCamSession = AVCaptureMultiCamSession() guard AVCaptureMultiCamSession.isMultiCamSupported else { // fallback на AVCaptureSession с одной камерой return } При активном мульти-кам сессии максимальное разрешение каждой камеры снижается — на iPhone 13 Pro можно получить максимум 1920×1440 с основной и 1280×960 с фронтальной одновременно. Попытка выставить 4K на обеих приводит к AVCaptureSessionRuntimeErrorNotification с кодом AVError.outOfMemory. В продакшене — фиксируем 1280×720 для обеих, чего достаточно для стрима.
Тепловой режим. Одновременная работа двух ISP (Image Signal Processor) и GPU-композиция нагревают устройство быстро. На примере iPhone 12 mini: при 30-минутном стриме с двух камер срабатывает thermal throttling, система принудительно снижает framerate до 20fps. Решение — мониторить ProcessInfo.ThermalState и при .serious переключаться на одну камеру или уменьшать битрейт.
Android с Camera2 API: одновременная съёмка поддерживается через CameraManager.getCameraCharacteristics + LOGICAL_MULTI_CAMERA или явное открытие двух физических камер. На практике поддержка зависит от OEM — Samsung Galaxy S22+ отдаёт два потока, бюджетные Xiaomi могут вернуть ERROR_CAMERA_IN_USE. Перед релизом обязательное тестирование на целевом парке устройств.
Почему Metal композиция лучше CIFilter?
CIFilter — удобный, но медленный: каждое применение занимает ~3-5 ms на кадр на iPhone 12. При 30 FPS это 90-150 ms на GPU, что оставляет мало ресурсов для энкодера. Metal-шейдер делает композицию за ~0.5 ms на кадр — в 6-10 раз быстрее. Сравнение в таблице:
| Подход | Время на кадр | FPS при стриме 2 камер |
|---|---|---|
| CIFilter | 3-5 ms | ~20-24 FPS |
| Metal шейдер | 0.5 ms | ~30 FPS |
Metal-композиция в 2 раза производительнее CIFilter при длительных трансляциях.
Как строим пайплайн
На iOS схема: AVCaptureMultiCamSession → два AVCaptureDeviceInput → два AVCaptureVideoDataOutput → Metal-композитор → VideoToolbox-энкодер → RTMP/SRT.
Ключевой момент — композиция. Два видеопотока нельзя напрямую отдать в один энкодер. Нужно смешивать кадры через MTKView или CIFilter. Используем Metal с кастомным шейдером: основная камера занимает full-frame, фронтальная рендерится в PiP-прямоугольник в углу.
import Metal import AVFoundation // Получаем CMSampleBuffer от каждой камеры в разных очередях let backQueue = DispatchQueue(label: "back.camera") let frontQueue = DispatchQueue(label: "front.camera") backOutput.setSampleBufferDelegate(self, queue: backQueue) frontOutput.setSampleBufferDelegate(self, queue: frontQueue) // Синхронизируем через AVCaptureDataOutputSynchronizer let synchronizer = AVCaptureDataOutputSynchronizer( dataOutputs: [backOutput, frontOutput] ) synchronizer.setDelegate(self, queue: syncQueue) AVCaptureDataOutputSynchronizer — обязательный элемент. Без него кадры с двух камер приходят с рассинхронизацией до 33ms (один кадр при 30fps), и в PiP-окне видно «дёрганье» относительно основного потока.
Для SRT-трансляции (как более стабильной альтернативы RTMP на мобиле) — используем libsrt, скомпилированный под iOS/Android, или HaishinKit 2.x с встроенной SRT-поддержкой.
Подробнее о проверке поддержки
- iOS: вызываем
AVCaptureMultiCamSession.isMultiCamSupportedперед созданием сессии. - Android: проверяем
CameraManager.getCameraCharacteristicsналичиеLOGICAL_MULTI_CAMERA. - Тестируем на 5-10 реальных устройствах из целевого парка.
Управление PiP-позицией во время стрима
Позицию PiP-окна делаем перетаскиваемой через UIPanGestureRecognizer с привязкой к углам через snap-анимацию. Координаты сохраняем в UserDefaults — пользователь не должен переставлять каждый раз.
При переключении ориентации Metal-шейдер получает новые координаты PiP автоматически через CADisplayLink, который пересчитывает layout на каждом кадре.
Типичные ошибки
- Не добавлять
AVCaptureMultiCamSessionв фоновый режим (UIBackgroundModes: audio) — при сворачивании приложения iOS убьёт сессию через 30 секунд. - Игнорировать
sessionWasInterruptedпри входящем звонке — нужно приостанавливать стрим и возобновлять вsessionInterruptionEnded. - Использовать
DispatchQueue.mainдля обработкиCMSampleBuffer— декодирование и Metal-рендеринг на главном потоке дропают UI на 8–12ms на каждый кадр.
Что входит в работу
- Архитектурная документация с описанием потоков и тепловых триггеров.
- Исходный код с комментариями на Swift/Kotlin.
- Интеграция с вашим бэкендом (REST/WebSocket/GraphQL).
- Инструкции по публикации в App Store и Google Play.
- 1 месяц поддержки после сдачи проекта.
Гарантируем стабильную работу под нагрузкой и соответствие гайдлайнам Apple и Google.
Сроки и стоимость
iOS-реализация с Metal-композитором, PiP, SRT/RTMP-трансляцией и тестами на тепловой режим: 4–6 недель. Android с Camera2 API — плюс 2–3 недели из-за фрагментации устройств. Стоимость рассчитывается индивидуально после анализа требований.
Получите консультацию по вашему проекту — оценим реализуемость и сроки. Свяжитесь с нами, чтобы обсудить детали. Закажите просчёт архитектуры мульти-камерного стриминга.







