Peer-to-Peer дзвінки в мобільних застосунках: досвід інтеграції та про що не пишуть в документації
Усім привіт! Мене звати Іван, я Head of Mobile в Jiji, українській продуктовій IT-компанії, яка створює продукти електронної комерції в Африці. Вже близько 20 років займаюся розробкою, з них 15 — для iOS та Android. Нещодавно я завершив інтеграцію P2P-дзвінків у мобільний застосунок. Це була одна з найскладніших задач за всю мою кар’єру, тому хочу поділитися своїм досвідом. Насправді я б хотів повернутися в минуле та сам прочитати таку статтю — можливо, тоді я впорався б із цим завданням у 10 разів швидше.
У цій статті розглянемо типові сценарії використання P2P-дзвінків у мобільних та десктопних застосунках, зробимо детальний огляд усіх пов’язаних технологій. Потім розберемо етапи реалізації на Android та iOS, а також проблеми, з якими я зіткнувся.
Основні властивості P2P-дзвінків
Для чого взагалі потрібні P2P-дзвінки у застосунках, якщо це можна зробити через GSM-оператора? Такі дзвінки мають певні особливості:
- P2P-зʼєднання. Peer-to-peer передбачає, що дані (наприклад, відео або аудіо) передаються безпосередньо між пристроями без посередника. Це зменшує затримку та навантаження на сервер, але вимагає обходу NAT і Firewall, про що ми поговоримо пізніше.
- Медіатреки. Медіастрими прямують від одного пристрою до іншого найкоротшим шляхом через мінімальну кількість проміжних вузлів. Їх можна динамічно додавати або видаляти, наприклад, додати потік із камери в будь-який момент. Ви можете це бачити в Zoom, де можна вмикати та вимикати мікрофон, змінювати камеру або ділитися екраном.
- Бітрейт і кодеки. У P2P-дзвінках можна налаштовувати FPS (кількість кадрів на секунду) для відео залежно від якості з’єднання. Це може відбуватися динамічно: при погіршенні зв’язку FPS може падати, але загальна якість спілкування залишатиметься на прийнятному рівні.
- Шифрування. Можливість додати власне шифрування. Якщо для бізнесу важлива конфіденційність, це є вагомою перевагою.
- Мультиплатформність. P2P-дзвінки працюють у всіх сучасних браузерах, мобільних та десктопних застосунках, тому проблем із сумісністю не виникає. Доступна повна кастомізація інтерфейсу. Можна інтегрувати дзвінок як частину іншого сценарію в застосунку.
- Ціна. Інтернет дешевший за звичайні дзвінки, що дозволяє користувачам телефонувати безкоштовно через Wi-Fi. Це особливо відчутно при міжнародних дзвінках.
Типові застосунки, де доцільно впроваджувати P2P-дзвінки:
- Месенджери. Всі сучасні месенджери використовують P2P, оскільки це зменшує затримку та навантаження на сервери (WhatsApp, Telegram, Viber, Signal).
- Застосунки для знайомств. Користувачі можуть спілкуватися онлайн перед реальною зустріччю (Tinder, Bumble, Badoo).
- Сервіси онлайн-навчання та тренувань. Мала затримка робить розмову більш живою, що важливо для вивчення мов (Preply, Cambly, Duolingo Events).
- Онлайн-маркетплейси. Використовуються для дзвінків між продавцем та покупцем, щоб уточнити деталі або продемонструвати товар у реальному часі (Jiji, Avito).
- Ігрові застосунки для командних ігор. Тут може застосовуватися P2P або гібрид MCU (Discord, online-games).
- Медичні застосунки для консультацій з лікарем, де важлива безпека та конфіденційність.
- Корпоративні інструменти. P2P підходить для невеликих групових дзвінків (до чотирьох співрозмовників). Якщо учасників більше, краще використовувати гібрид P2P+SFO (Zoom, Microsoft Teams).
Різниця у вартості інтернету та мобільних дзвінків в країнах Африки стала основною причиною, чому ми в Jiji вирішили додати P2P-дзвінки. Ми помітили, що продавці й покупці часто переходили із нашого чату у сторонні месенджери, такі як WhatsApp. Проте за межами застосунку ми не можемо захистити користувачів від шахрайства. Також їм зручно зберігати історію дзвінків, прив’язану до оголошень, а не до невідомих контактів. Крім того, небагато класифайдів мають функціонал P2P-дзвінків, тому це дає нам конкурентну перевагу.
Real-Time Media Stack
Коли ми говоримо про P2P-дзвінки, в назві вже ховаються як мінімум дві непов’язані технології: P2P-з’єднання та відеостримінг. Якщо ви почнете досліджувати, як реалізований цей функціонал у великих продуктах, щоразу відповіддю буде... WebRTC.
Це безкоштовний проєкт з відкритим вихідним кодом, який підтримується світовими IT-гігантами, такими як Google, Microsoft та Apple. Фактично, він є синонімом P2P відео- та голосових дзвінків.

Крім базових P2P-з’єднань та стримінгу, WebRTC надає безліч інших функцій:
- API для доступу до камери та мікрофона на всіх платформах, а також для демонстрації екрана;
- узгодження роботи різних драйверів, моделей дозволів та кодеків на кожній операційній системі;
- підтримка кодування та декодування, а також фрагментування стримів;
- автоматичний вибір кодека залежно від пристрою;
- апаратне прискорення: реалізовані такі функції, як Echo Cancellation, Auto Gain Control, Noise Suppression;
- передача даних з низькою затримкою;
- Jitter Buffer — механізм, який синхронізує аудіо та відеопотоки, що надходять з різною затримкою, забезпечуючи стабільний звук та зображення;
- обхід NAT та Firewall через алгоритм ICE (Interactive Connectivity Establishment);
- власний протокол для обміну параметрами між пристроями та вбудоване шифрування стримів за допомогою SRA-TP.
Отже, WebRTC — потужна бібліотека. Якби ви захотіли реалізувати цей функціонал власним рішенням, знадобилося б залучення команди розробників усіх профілів — бекенд, спеціалісти з безпеки, обробки відео та медіа, спеціалісти з P2P та обходу NAT, близько двох років роботи.
Здавалося б, тоді можна обирати WebRTC та на цьому завершувати статтю. Але, на жаль, не все так просто.

Для інтеграції WebRTC нам знадобиться сім основних кроків:

Крок 1. SDK для Android та iOS
Сам WebRTC зараз розвивається у вебі та поширюється у вигляді відкритого коду на C++. Донедавна існували офіційні SDK для Android та iOS, але зараз їх видалено з репозиторіїв. На щастя, мобільна спільнота створила власні неофіційні SDK, які підтримуються та оновлюються. Ви можете завантажити скомпільовані бінарники у вигляді динамічної бібліотеки. Після цього її можна інтегрувати в Android-застосунок через JNI або в iOS-застосунок через Bridging Header.
Можу порекомендувати дві неофіційні SDK, які підтримують останні версії WebRTC та перевірені на практиці.
- webrtc.googlesource.com
- Community-driven Android SDK (gradle)
- Community-driven iOS SDK (SPM, CocoaPods)
Крок 2. Розуміння STUN та TURN
Наступний крок — робота з базовим класом з’єднання: на Android це Peer Connection, на iOS — RTC Connection. Цей клас відповідає за встановлення P2P-зв’язку і виконує основну роботу під час ініціалізації та подальшої взаємодії між клієнтами.

Щоб організувати P2P-з’єднання між користувачами з різних локальних мереж, обом сторонам потрібні публічні адреси одна одної. У межах однієї локальної мережі все просто: запит іде на локальний IP, одержувач бачить джерело пакета і може відповісти. У реальних сценаріях користувачі зазвичай у різних мережах і не мають публічних IP-адрес; застосунок також не може самостійно визначити власні публічні IP і порт.
Причина — робота NAT (Network Address Translation). Це механізм в маршрутизаторі, що дозволяє кільком пристроям використовувати одну зовнішню IP-адресу для доступу до інтернету. Під час виходу в інтернет для кожного вихідного з’єднання роутер тимчасово виділяє публічний порт і створює запис у таблиці відповідностей. Коли надходить відповідь із мережі, він за цим записом пересилає пакети на потрібний внутрішній хост.

Щоб дізнатися публічні IP та порти обох пірів, використовується сервіс STUN (Session Traversal Utilities for NAT). Це проста технологія, яка повідомляє клієнтам їхні зовнішню адресу і порт; вона задокументована (є RFC) і доступна на багатьох безплатних серверах, зокрема від Google. За принципом роботи це схоже на сайти, що показують ваш публічний IP, тільки ще повертається і порт. На практиці STUN працює приблизно у 80% P2P-з’єднань — за умови, що NAT трансформує внутрішні адреси у зовнішні передбачувано.
Якщо ж використовується симетричний NAT або увімкнено простий Firewall, що блокує вхідні пакети на «незнайомі» порти, STUN не допоможе. У таких мережевих умовах роутер може призначати різні публічні порти для різних напрямків трафіку, тому навіть знаючи свій «поточний» публічний порт, клієнт не зможе надійно приймати вхідні пакети.
У таких випадках єдиним варіантом стає ретрансляція всього трафіку через TURN-сервер. Ця технологія працює як проксі для медіа. Коли двоє клієнтів не можуть встановити пряме з’єднання через симетричний NAT або Firewall, вони обидва підключаються до TURN-сервера, який отримує аудіо- та відеопакети від одного користувача і пересилає їх іншому. Таким чином, весь медіатрафік іде через TURN-сервер, що гарантує зв’язок у будь-яких мережевих умовах, але збільшує затримку та вартість, оскільки сервер стає транзитним вузлом. Орієнтовно на TURN припадає близько 20% трафіку.

Ідея обходити NAT є цілком легальною, хоча у
Утім P2P існував ще до торентів (зокрема у VoIP-дзвінках). Окрім STUN і TURN, застосовували й інші підходи (наприклад, Hole Punching чи HTTP-тунелювання), але WebRTC стандартизував найочевидніші механізми, об’єднавши їх під назвою ICE: клієнт послідовно перебирає всі можливі шляхи з’єднання — від локальної мережі, через STUN і, за потреби, через TURN. Тому STUN/TURN-сервіси з’явилися в багатьох хмарних провайдерів.
STUN-сервери зазвичай безкоштовні, тоді як у TURN-серверах є оплата за трафік.
STUN (безкоштовні):
- stun:stun.l.google.com:19302
- stun:stun.l.google.com:5349
- stun:stun1.l.google.com:3478
- stun:stun1.l.google.com:5349
- stun:stun2.l.google.com:19302
- stun:stun2.l.google.com:5349
- stun:stun3.l.google.com:3478
- stun:stun3.l.google.com:5349
TURN (безкоштовні для тесту):
TURN cloud провайдеру ($0.01-0.05 / GB):
Крок 3. Сигналінг та API
Щоб P2P-з’єднання встановилось, клієнти повинні обмінятися публічними IP-адресами та списками підтримуваних кодеків. Ця інформація генерується WebRTC у спеціальний рядок, що називається SDP (Session Description Protocol). Для встановлення з’єднання, один застосунок повинен передати свій SDP іншому і навпаки. Тоді P2P-з’єднання може бути встановлене.
WebRTC назвав цей процес сигналінгом, а самі сигнали — offer та answer. Користувач, що дзвонить, відправляє offer, а той, хто відповідає — answer. Ці сигнали містять всю необхідну інформацію. Звісно, для передачі цих даних нам потрібен бекенд, оскільки P2P-з’єднання ще не встановлене. Найпростіше реалізувати обмін сигналами між клієнтами через вебсокети. Типовий алгоритм обміну сигналами виглядає так:

Базовий набір сигналів для WebRTC-з’єднання: Offer, Answer, IceCandidate. Для зручності та швидкого старту бекенд може реалізувати передачу будь-яких даних разом з сигналами. При цьому нові сигнали ви зможете додавати на клієнті без змін у бекенді, і у вас буде єдиний API для первинного обміну даними між клієнтами.

Окрім offer, answer нам потрібні будуть сигнали життєвого циклу дзвінка: початок виклику, встановлення дзвінка, додзвін, прийняття, відхилення, завершення. Усе це — ваша зона відповідальності та реалізується відповідно до бізнес-логіки.
Типовий перелік сигналів саме для дзвінка p2p може виглядати так:

Також зручно додати до API сигнал про зміну стану медіа, наприклад, коли віддалений користувач вимкнув мікрофон або відео. WebRTC при м’юті продовжує тримати стрим із нульовими FPS/бітрейтом, тож інша сторона не дізнається про зміну автоматично. Щоб коректно оновлювати інтерфейс (іконки mute, стан відео), стан треків потрібно передавати сигналами.
Крім того, WebRTC SDK надає повну можливість створювати відеотреки з камери та аудіотреки з мікрофона, а також легко додавати та видаляти їх з поточного з’єднання.

Крок 4. Робота з медіапотоками та архітектура застосунку
Для виведення відео WebRTC SDK надає готові view-елементи для відображення як локального відео з камери, так і відео, що надходить від іншого користувача. Ви можете вимикати будь-який трек. У межах камери також є можливість перемикатися між передньою та задньою без створення нового відеотреку — це відбувається плавно, без бліків.

Аудіотрек, що надходить від іншого користувача, автоматично програється. Єдиним додатковим функціоналом може бути можливість перемкнути виведення звуку з динаміка на спікерфон.
Крок 5. Архітектура та сервіси
Для реалізації P2P-дзвінків потрібні такі компоненти: WebRTC-клієнт із SDK, який утримує Peer/RTC connection і медіатреки; сигналінг-клієнт, що через наше API приймає та надсилає сигнали; менеджер дзвінка, який зберігає поточний стан, обробляє всі сигнали з бекенду й за бізнес-логікою ініціює з’єднання.
Зв’язок із UI має бути слабким: користувач може згорнути екран дзвінка, тож сервіси поточного дзвінка не повинні залежати від конкретних екранів і мають переживати життєвий цикл Activity/ViewController. Має бути можливість повернутися до активного дзвінка й відновити стан UI з усіма відеотреками та «перемичками» юзерів.

Крок 6. Механіка вхідного дзвінка
Як надіслати сигнал про дзвінок: варіантів небагато — постійне з’єднання через WebSocket в апці або пуші (Firebase Messaging / Apple Push Notification Service).
У Firebase Messaging рекомендується вказувати високий пріоритет. Ви можете використовувати системні нотифікації або створити власні, додавши кнопки «прийняти» та «відхилити».
Щоб користувач міг згорнути застосунок, а відеострим з камери й запис аудіо не перервалися, на Android потрібно стартувати foreground-сервіс з типом phoneCall, який показує нотифікацію про активний дзвінок. Без нього «зйомка» з камери та запис аудіо припиняться, щойно застосунок піде у бекграунд. На iOS використовують PushKit для UI вхідного дзвінка і вмикають необхідні Background modes в налаштуваннях застосунку (аудіо, Picture-in-Picture, VoIP push notifications).
Крок 7. Перевірка флоу для Caller \ Callee
Так виглядає фінальний алгоритм для дзвінка:

- Локальні треки: створити локальні аудіо- та відеотреки можна ще до того, як відбудеться з’єднання, щоб, наприклад, відобразити свій стрим з камери під час додзвону.
- Сигналінг: один клієнт відправляє offer, інший очікує його, а потім відправляє answer.
- ICE-кандидати: після обміну offer та answer WebRTC починає шукати пари IP-адрес і портів, використовуючи алгоритм ICE. Він може знайти кілька десятків кандидатів (різні комбінації з STUN і TURN-серверами). Усіх знайдених кандидатів потрібно передати іншому користувачеві. Оскільки майже всі кандидати будуть знайдені майже одночасно, бажано накопичувати їх і відправляти раз на 500 мс одним пакетом.
- Завершення дзвінка: один із користувачів надсилає сигнал про завершення, а інший очікує його, щоб закрити з’єднання.
Даний протокол дзвінку можна використовувати для мануал та інтеграційних тестів.
Підводні камені: розв’язання проблем
Під час інтеграції я зіткнувся з кількома проблемами:
Вхідний дзвінок на кількох пристроях
Коли користувач отримує push-повідомлення про вхідний дзвінок на кількох своїх пристроях, важливо, щоб після прийняття дзвінка на одному з них нотифікації на інших зникли. Для цього потрібно додати сигнал скасування дзвінка, який відправлятиметься на інші пристрої. Цей функціонал нестабільно працює у багатьох сучасних месенджерах, тому йому потрібно приділити час. Також варто додати таймер очікування відповіді (наприклад, 30 секунд), після якого дзвінок вважатиметься пропущеним.
Перемикання між камерами
Другий кейс — перемикання між софт-камерами. На сучасних мобільних пристроях може бути до кількох фізичних камер, але запис іде не з «фізичної» камери напряму, а з софт-камери, яка їх комбінує. Під час створення відеотреку можна перемикатися між ними прямо у стримінгу — наприклад, з передньої на задню. На деяких девайсах таких софт-камер більше двох (дві передні, дві задні), тож це варто врахувати в інтерфейсі: або показувати кілька варіантів для передніх/задніх камер, або на етапі вибору камер зафіксувати одну передню й одну задню та перемикатися лише між ними.
Ризик небажаного контенту
Ви можете зіткнутися з ситуацією, коли користувачі відправлятимуть небажаний контент 18+ під час відеодзвінка. Згідно app review guidelines, будь-який user-generated контент має бути промодерованим або має бути можливість заблокувати небажаного юзера. Як варіант ви можете відокремити голосові та відеодзвінки й заборонити вмикати камеру під час голосового дзвінку. Ми ж вирішили залишити можливість вмикати камеру, оскільки це важливо для демонстрації товару.
Повторна ініціалізація з’єднання (Renegotiation)
Іноді WebRTC може вимагати повторно узгодити параметри з’єднання. Цього не варто боятися — просто потрібно знову відправити «offer» та «answer». Часто це відбувається, якщо ви динамічно додаєте відеотрек до вже встановленого голосового з’єднання. Щоб уникнути затримки, краще одразу вказувати в налаштуваннях WebRTC, що можливий як голосовий, так і відеострим. У цьому випадку навіть якщо спочатку надсилатиметься лише аудіо, WebRTC буде готовий до передачі відео.
Відсутність стандартного Singalling API
Коли я передав специфікацію того, яким має бути signaling API, бекенд знайшов готове рішення, яке вони сприйняли як «стандартний сигналінг» для WebRTC. Проблема в тому, що «стандартного» сигналінгу не існує, і такі рішення зазвичай працюють за моделлю «кімнати»: усі клієнти під’єднуються до кімнати та встановлюють P2P-з’єднання один з одним. Теоретично цей варіант можна налаштувати під потреби, але це значно ускладнює розробку клієнтських застосунків.
Одночасні вхідні дзвінки
Що робити, якщо два користувачі одночасно намагаються зателефонувати один одному (одночасно відправляють offer). Вам потрібно розробити алгоритм, який визначить, чий offer є пріоритетним. Наприклад порівняти свій user_id з user_id іншого користувача. Хто молодший, той хай буде більш ввічливим. Ввічливий user в цьому випадку скасує свій offer (зробить rollback або створить повторно RTC connection) і прийме remote offer. Менш ввічливий просто проігнорує remote offer і буде чекати на відповідь за своїм offer.
Чи можна використовувати WebRTC для відеоконференцій
Теоретично можна, але при цьому кожен з учасників відкриє WebRTC-з’єднання з кожним іншим користувачем і для кожного відкритого з’єднання на клієнті будуть працювати по 2 кодеки на кодування і декодування стримів, що значно збільшить навантаження на процесор. Тому частіше використовують іншу схему: всі клієнти встановлюють WebRTC-з’єднання з сервером. На сервері встановлюють MCU, що ретранслює стрими між усіма клієнтами.
Отже, реалізація P2P-дзвінків — це перетин експертизи про мережі, медіа та мобільні платформи. WebRTC дає фундамент, але найскладніше — звести все до стабільної системи з надійним інтерфейсом. Сподіваюся, цей досвід стане у пригоді, зекономить вам тижні й допоможе уникнути пасток, про які зазвичай не пишуть у документації.

12 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів