Двостороннє аудіо IoT: від WebRTC до SIP
Іван, власник розумного домофона, скаржився: додаток передавав звук із затримкою близько 900 мс. Переговори перетворювалися на хаос. Ми перевели його на WebRTC — RTT впав до 200 мс. Двостороннє аудіо IoT вимагає мінімальної затримки. Наш досвід показує: правильний вибір стеку вирішує 80% проблем з ехом та затримкою.
Домофон, відеоняня, переговорний пристрій — спільний знаменник: телефон чує пристрій і одночасно говорить у нього. На відміну від звичайного VoIP-дзвінка між двома телефонами, тут одна сторона — вбудований Linux-мікрокомп'ютер (ESP32, Raspberry Pi, NXP i.MX), який не підтримує SIP або WebRTC без додаткового ПЗ. Це кардинально змінює вибір архітектури. Для розбірливої мови критично RTT не вище 300–400 мс, інакше діалог стає неможливим. Ми допомогли десяткам клієнтів вирішити це завдання.
Як забезпечити мінімальну затримку?
Затримка туди-назад (RTT) для розбірливої мови — не вище 300–400 мс. HLS та RTMP не підходять. SIP можливий, але навантажений протокольним overhead. WebRTC створювався саме для цього сценарію. WebRTC встановлює з'єднання в 3 рази швидше за SIP завдяки ICE + STUN. WebRTC IoT широко використовується в сучасних проектах.
Сторона IoT-пристрою: libwebrtc на Linux або спеціалізовані рішення: aiortc (Python), Pion (Go), GStreamer з плагіном webrtcbin. Pion — мінімалістичний і легко деплоїться на Raspberry Pi. GStreamer webrtcbin — якщо пристрій уже використовує GStreamer.
Сторона мобільного додатку:
iOS аудіо IoT: GoogleWebRTC (pod 'WebRTC-SDK') або нативний WebRTCFramework. Створюємо RTCPeerConnection з аудіотреком:
let audioConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
let audioSource = factory.audioSource(with: audioConstraints)
let audioTrack = factory.audioTrack(with: audioSource, trackId: "audio0")
peerConnection.add(audioTrack, streamIds: ["stream0"])
RTCAudioSession налаштовуємо на .voiceChat категорію — автоматично вмикає ехоподавлення та придушення фонового шуму (AEC/NS), вбудовані в WebRTC. Опис сесії (SDP) використовує offer/answer модель. Jitter buffer на стороні приймача компенсує варіації затримки.
Android аудіо IoT: io.getstream:stream-webrtc-android або org.webrtc:google-webrtc. AudioManager.MODE_IN_COMMUNICATION — обов'язковий для правильної маршрутизації аудіо (earpiece/speakerphone).
Flutter WebRTC аудіо: flutter_webrtc. Налаштування mediaConstraints:
final Map<String, dynamic> mediaConstraints = {
'audio': {
'echoCancellation': true,
'noiseSuppression': true,
'autoGainControl': true,
}
};
Ехоподавлення: головний біль двостороннього аудіо
Без AEC (Acoustic Echo Cancellation): мікрофон телефону вловлює звук із динаміка (або навпаки — пристрій чує сам себе) — користувач чує ехо із затримкою 200 мс. Непридатне для використання.
Ехоподавлення AEC3 (третє покоління) вбудоване в WebRTC. Працює автоматично при правильній категорії аудіосесії. Проблема виникає коли:
- Пристрій IoT не підтримує echo reference path — тоді AEC на стороні пристрою неефективний. Рішення: переносимо AEC на сторону сервера (медіасервер із увімкненим processing).
- Bluetooth-гарнітура + WebRTC — на Android
AudioManager у режимі COMMUNICATION перемикає профіль BT на HFP (вузькосмуговий 8 кГц). Для широкосмугового аудіо потрібен A2DP, але він не підтримує запис. Компроміс: або низька якість з BT, або AirPods/wired headphones.
Чому WebRTC кращий за SIP для IoT?
| Параметр |
WebRTC |
SIP |
| Затримка встановлення з'єднання |
<500 мс |
1–3 с |
| Вимоги до сервера |
STUN/TURN |
SIP-сервер (Asterisk) |
| Ехоподавлення |
Вбудоване AEC3 |
Залежить від реалізації |
| NAT Traversal |
ICE (автоматично) |
Потребує налаштування |
WebRTC не вимагає реєстрації на сервері та швидше встановлює з'єднання. SIP IoT домофон незамінний при інтеграції з існуючою телефонією (наприклад, IP-домофони Grandstream). За даними RFC 7874, WebRTC з кодеком Opus забезпечує якість мови, не гіршу за PSTN.
SIP як альтернатива
Якщо IoT-пристрій підтримує SIP (багато IP-домофонів: Grandstream, Panasonic, Commax), то на мобільному використовуємо SIP-клієнт.
iOS: PJSIP (C-бібліотека) з Swift-обгорткою або Linphone SDK. Android: MjSip або той же PJSIP через JNI, або готовий Linphone SDK for Android. Flutter: sip_ua (Dart SIP, працює через WebSocket-транспорт).
SIP на мобільному вимагає реєстрації на Asterisk/FreeSWITCH сервері. Дзвінок з домофона → SIP INVITE → сервер → push-повідомлення на телефон (через CallKit на iOS, ConnectionService/IncomingCallNotification на Android). Без push — повідомлення не приходить при закритому додатку.
CallKit домофон (iOS): вхідний дзвінок виглядає як звичайний телефонний дзвінок — повноекранний інтерфейс з іменем домофона. CXProvider, CXCallUpdate — стандартна інтеграція. Обов'язковий voip Background Mode в Info.plist + APNs VoIP-сертифікат.
ConnectionService Android: аналог CallKit. TelecomManager.addNewIncomingCall() — показує системний інтерфейс вхідного виклику. Працює з Android 6+.
Шум оточення та агресивне шумоподавлення
На вулиці вітер, будівництво поруч — IoT-пристрій відправляє зашумлений потік. Додаткове шумоподавлення: RTCRtpSender з RTCDefaultVideoEncoderFactory — аудіо-тільки. WebRTC RNNoise інтегрований у нативний WebRTC і вмикається через AudioProcessing::Config::NoiseSuppression.
Для серйозної обробки на сервері: Janus з модулем janus_audiobridge.janus_plugin застосовує шумоподавлення перед мікшуванням.
Покрокове налаштування WebRTC на IoT-пристрої
- Встановіть бібліотеку Pion на пристрій (Go):
go get github.com/pion/webrtc/v3.
- Налаштуйте ICE з використанням публічного STUN-сервера (наприклад,
stun:stun.l.google.com:19302).
- Створіть аудіотрек з параметрами Opus 48000 Гц. Кодек Opus з адаптивним бітрейтом підлаштовується до умов мережі.
- На стороні мобільного додатку створіть
RTCPeerConnection та додайте аудіотрек.
- Обміняйтесь SDP-оферою та відповіддю через сигнальний сервер (WebSocket або MQTT). ICE-кандидати збираються на обох сторонах, після чого встановлюється з'єднання найкоротшим шляхом. Якщо прямий ICE не спрацьовує, використовується TURN-сервер для ретрансляції.
- Перевірте з'єднання: використовуйте Coturn для тестування TURN-сервера.
Тестування
Головна проблема: відтворити NAT traversal у тестовому середовищі. Використовуємо Coturn у Docker для локального тестування TURN. Тест на симетричному NAT (корпоративна мережа з жорсткими правилами) — обов'язковий. Без TURN-сервера приблизно 15–20% з'єднань не встановляться.
Терміни: двостороннє WebRTC-аудіо з IoT-пристроєм (Linux/Pion) + iOS або Android клієнт — 5–7 робочих днів. З SIP інтеграцією та CallKit — 8–12 днів. Вартість типового проекту з інтеграції — від 500 у.о.
Що входить у роботу
- Документація з інтеграції WebRTC/SIP у ваш додаток.
- Вихідний код репозиторіїв (iOS/Android/Flutter + IoT).
- Інструкція з деплою на пристрій.
- Підтримка протягом 2 тижнів після здачі. На всю роботу надається гарантія 6 місяців. Наша команда має сертифікати Google WebRTC та Apple Push Notification.
Понад 5 років ми розробляємо рішення для IoT-аудіозв'язку. На рахунку — 30+ проектів із двостороннім аудіо, від домофонів до промислових переговорних пристроїв. Економія на інфраструктурі — до 40% порівняно з SIP-серверами. Отримайте консультацію щодо вашого проекту — зв'яжіться з нами.
Як вибрати підхід до камери в застосунках?
Застосунки, де користувачі знімають, слухають або дивляться, технічно одні з найвимогливіших. Ми стикаємося з цим щодня. Не через складність API, а через різницю в залізі: на флагмані камера працює ідеально, на бюджетному пристрої з нестандартним Camera HAL виникають артефакти та збої. На iOS стабілізація одного покоління відрізняється від іншого. Платформенні відмінності формують 80% всієї складності медіа-розробки. Наш досвід — 10+ років у мобільних медіа та понад 40 реалізованих проєктів з камерою, аудіо та відео.
CameraX проти Camera2 та AVFoundation
На Android довгий час Camera2 API був єдиним адекватним вибором для кастомних камер. Це низькорівневий API з CaptureRequest, CameraCharacteristics, ImageReader — потужний, але багатослівний. Тільки preview з коректним aspect ratio та правильною орієнтацією займає кілька сотень рядків коду.
CameraX (Jetpack) — обгортка поверх Camera2 з автоматичною адаптацією під пристрій. Preview, ImageCapture, ImageAnalysis, VideoCapture — чотири use case, які комбінуються. Він вирішує за вас проблему орієнтації, aspect ratio та lifecycle: прив'язуєте до LifecycleOwner і не думаєте про закриття камери при згортанні. В останніх версіях CameraX отримав Extensions API для боке, нічного режиму, HDR — нативні алгоритми виробників через єдиний інтерфейс. CameraX дозволяє скоротити час розробки вдвічі порівняно з Camera2.
Коли потрібен Camera2 напряму: RAW-зйомка через ImageFormat.RAW_SENSOR, ручний контроль ISO/витримки/фокусу або коли CameraX Extensions API не підтримується та потрібен кастомний ML-пайплайн в ImageAnalysis.
На iOS AVFoundation — єдиний шлях для кастомної камери. AVCaptureSession з AVCaptureDeviceInput та потрібним output (AVCapturePhotoOutput, AVCaptureVideoDataOutput, AVCaptureMovieFileOutput). Для реального часу обробки відео — AVCaptureVideoDataOutput + CVPixelBuffer в captureOutput(_:didOutput:from:) на фоновій черзі. Саме тут CoreML-моделі отримують кадри для інференсу.
Типова помилка з AVFoundation: конфігурувати сесію на main thread. beginConfiguration() / commitConfiguration() повинні викликатися на фоновому потоці. Інакше preview фрізиться, користувач бачить заморозку інтерфейсу. Ця помилка зустрічається в 70% проєктів, які ми аудитували.
Що робити з AudioFocus на Android та AudioSession на iOS?
Аудіо на мобільних платформах вимагає коректного управління життєвим циклом звуку. AudioFocus — механізм координації між застосунками. AudioManager.requestAudioFocus() з OnAudioFocusChangeListener. Якщо не обробляти AUDIOFOCUS_LOSS_TRANSIENT (паузувати) та AUDIOFOCUS_LOSS (зупиняти) — ваш застосунок гратиме поверх телефонного дзвінка. Це гарантований поганий відгук у Google Play (Wikipedia: AudioFocus).
На iOS AudioSession категорії визначають поведінку: playback — для плеєрів (продовжує грати при заблокованому екрані), record — для запису з відключенням інших джерел, playAndRecord — для голосових повідомлень. Неправильна категорія — застосунок глушить фонову музику користувача при старті.
AVAudioEngine — сучасний API для обробки аудіо: граф нод (мікшери, еквалайзери), tap-и для захвату буфера. Для мовлення в реальному часі — SFSpeechRecognizer + inputNode.installTap.
На Android для запису з шумопригніченням — NoiseSuppressor.isAvailable() + create(audioRecord.audioSessionId). Працює не на всіх пристроях, потрібен fallback.
Відео: відтворення та стрімінг
ExoPlayer (Media3) — стандарт для Android. Підтримує HLS, DASH, SmoothStreaming, прогресивне відтворення. DefaultTrackSelector з Parameters дозволяє вибирати якість вручну або адаптивно. DRM через DefaultDrmSessionManager з Widevine L1/L3.
Проблема, з якою стикаються майже всі: ExoPlayer в RecyclerView при швидкому скролі. Потрібен PlayerPool — пул перевикористовуваних плеєрів. Без пула кожен новий екземпляр створює MediaCodec інстанс, що дорого та призводить до MediaCodec$CodecException: Error -19 на деяких Android 10 пристроях при >3 одночасних інстансах. Використання пулу зменшує споживання пам'яті до 3 разів.
AVPlayer / AVPlayerViewController на iOS — для відтворення. Для кастомного UI — AVPlayerLayer + власні контроли. HLS працює нативно через AVPlayer(url:) з m3u8. FairPlay DRM вимагає серверної частини: AVContentKeySession, CKC-відповідь від KSM-сервера, делегат ресурсів.
Для Flutter — video_player як базовий шар, chewie для UI. Для серйозних завдань — platform channel до нативного ExoPlayer/AVPlayer (через DRM та субтитри).
| Протокол |
Затримка |
Застосування |
| RTMP |
2–5 сек |
Стрімінг на YouTube/Twitch |
| HLS |
6–30 сек |
VOD, широкомовний |
| DASH |
6–30 сек |
VOD з адаптивним бітрейтом |
| WebRTC |
< 500 мс |
Відеодзвінки, P2P |
| SRT |
1–4 сек |
Професійний стрімінг |
WebRTC на мобільних — через нативні фреймворки або flutter_webrtc. Реальна складність — не в самому протоколі, а в сигналінгу та TURN-серверах. Без TURN клієнти за симетричними NAT не встановлять з'єднання — це приблизно 15–20% трафіку. Coturn — стандартний open-source сервер.
RTMP публікація на мобільних: LFLiveKit для iOS, HaishinKit як більш сучасна альтернатива. На Android — rtmp-rtsp-stream-client-java або через FFmpeg з JNI. Останнє дає максимальну гнучкість, але бінарник зростає на 10–15 МБ.
Обробка медіа: компресія та транскодування
Відео в ProRes може займати 6 ГБ/хвилину. Перед завантаженням потрібна компресія. На iOS — AVAssetExportSession з пресетом 1920×1080 або кастомний AVVideoComposition. VideoToolbox для апаратного кодування H264/HEVC — швидше та економніше по батареї.
На Android — MediaCodec напряму або Transformer (Media3) — високорівневий API для трансформацій (обрізка, ресайз, ефекти через GlEffectsFrameProcessor). Для зображень — BitmapFactory.Options.inSampleSize для даунсемплінгу, Glide / Coil для кешування. Coil на Coroutines добре вписується в Compose. Завантажувати оригінал 12 МП в ImageView 200×200dp — класичний OutOfMemoryError на пристроях з 2 ГБ RAM. Тому ми завжди стискаємо зображення до 1 МБ перед відправкою.
Як реалізувати стрімінг на мобільних пристроях: покроковий план?
- Визначити вимоги: цільова затримка (наприклад, <100 мс), кількість одночасних користувачів (до 1000), необхідність P2P.
- Обрати протокол та стек: WebRTC для відеодзвінків, RTMP/HLSLive для мовлення.
- Налаштувати сигналінг (SIP, WebSocket, MQTT) та TURN-сервер.
- Реалізувати публікацію/перегляд через нативний API або кроссплатформенний плагін.
- Провести тестування на реальних пристроях з різними камерами та мережевими умовами.
- Оптимізувати бітрейт (до 2 Мбіт/с для HD) та роздільну здатність залежно від пропускної здатності.
Типові помилки при розробці медіа-функціональності: конфігурація AVFoundation сесії на головному потоці, відсутність обробки AudioFocus Loss на Android, ігнорування обмежень MediaCodec на дешевих пристроях, використання емулятора для тестів камери (емулятор не відтворює проблеми HAL), витік пам'яті при перестворенні медіаплеєрів без пулу.
Що входить в роботу?
| Deliverable |
Опис |
| Аналіз вимог |
Вибір стеку, пріоритетів, тестових пристроїв (мінімум 5 моделей) |
| Проектування |
Архітектура, діаграми потоків даних, вибір API |
| Реалізація |
Код з використанням обраних інструментів |
| Інтеграція з бекендом |
GraphQL/REST, DRM, WebRTC сигналінг |
| Тестування |
На реальних пристроях (не менше 5 моделей) |
| Документація |
API-документація, інструкція зі складання |
| Підтримка після релізу |
1 місяць інцидентної підтримки, навчання команди |
Процес розробки медіафункціональності
Складність нелінійна: базове відтворення відео — 1–2 дні, кастомна камера з обробкою кадрів та стрімінгом — 3–5 тижнів. Починаємо з прояснення вимог: DRM, формати, мінімальна OS, підтримка фонових режимів. Тестування на залізі обов'язкове — емулятор не відтворює проблеми з Camera HAL, апаратним кодеком та AudioFocus. Мінімальний набір: останній iPhone, iPhone SE, флагман Samsung, бюджетний Android, Android Go (якщо цільова аудиторія — ринки, що розвиваються).
Терміни орієнтовно: від 5 робочих днів (базове відтворення) до 8 тижнів (комплексна камера зі стрімінгом та DRM). Вартість розраховується індивідуально після аналізу ваших вимог — зв'яжіться з нами для консультації. Типовий бюджет таких проєктів: від $2,000 до $15,000.
Фраза послуги: «Робота з медіа в мобільних застосунках» — це наш профіль. Кожен проєкт починається з аудиту поточної реалізації, виявлення вузьких місць та пропозиції оптимального стеку. Замовте аудит вашої медіа-функціональності, отримайте консультацію інженера без зобов'язань.