Реалізація мульти-камерного стрімінгу з мобільного пристрою
Ми реалізували кілька проєктів мульти-камерного стрімінгу для 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 тижні через фрагментацію пристроїв. Вартість розраховується індивідуально після аналізу вимог.
Отримайте консультацію щодо вашого проєкту — оцінимо реалізовність та терміни. Зв'яжіться з нами, щоб обговорити деталі. Замовте прорахунок архітектури мульти-камерного стрімінгу.







