Юзаєм WebRTC сокети для дзвінків із чистого браузера
Зміст статті
Технологіям для дзвінків з браузера вже багато років: Java, ActiveX, Adobe Flash… В останні кілька років стало ясно, що плагіни та ліві віртуальні машини не блищать зручністю (навіщо мені взагалі щось встановлювати?) і, найголовніше, безпекою. Що ж робити? Вихід є!
WebRTC з боку веб-розробника
З точки зору веб-розробника WebRTC складається із двох основних частин:
Що будемо робити?
MediaStream
Перший і найпростіший компонент WebRTC - MediaStream. Він надає браузеру доступ до медіапотоків з камери та мікрофона локального комп'ютера. У Chrome для цього необхідно викликати функцію navigator.webkitGetUserMedia() (оскільки стандарт ще не завершено, всі функції йдуть з префіксом, і Firefox ця ж функція називається navigator.mozGetUserMedia()). Під час її виклику користувачу буде виведено запит про дозвіл доступу до камери та мікрофону. Продовжити дзвінок можна буде лише після того, як користувач дасть свою згоду. Як параметри цієї функції передаються параметри необхідного медіапотоку та дві callback-функції: перша буде викликана у разі успішного отримання доступу до камери/мікрофону, друга – у разі помилки. Для початку створимо HTML-файл rtctest1.html з кнопкою та елементом:
Microsoft CU-RTC-Web
Увімкнення локального потоку
Всередині тегів нашого HTML-файлу оголосимо глобальну змінну для медіапотоку:
Або вказати додаткові параметри:
Другим параметром методу getUserMedia необхідно передати callback-функцію, яка буде викликана у разі його успішного виконання:
Третій параметр - callback-функція обробник помилки, який буде викликаний у разі помилки
Власне виклик методу getUserMedia - запит доступу до мікрофона та камери при натисканні на першу кнопку
Неможливо отримати доступ до медіапотоку з файлу, відкритого локально. Якщо спробувати так зробити, отримаємо помилку:
Викладемо файл, що вийшов на сервер, відкриємо в браузері і у відповідь на запит дозволимо доступ до камери і мікрофону.
Запит на доступ до камери та мікрофону
Хакер #176. Анонімність в інтернеті
Вибрати пристрої, до яких отримає доступ Chrome, можна в Settings (Налаштування), лінк Show advanced settings (Показати додаткові налаштування), розділ Privacy (Особисті дані), кнопка Content (Налаштування контенту). У браузерах Firefox і Opera вибір пристроїв здійснюється з списку, що випадає, безпосередньо при дозволі доступу.
При використанні протоколу HTTP дозвіл буде запрошуватися щоразу при отриманні доступу до медіапотоку після завантаження сторінки. Перехід на HTTPS дозволить виводити запит одноразово, тільки при першому доступі до медіапотоку.
RTCMediaConnection
RTCMediaConnection — об'єкт, призначений для встановлення та передачі медіапотоків по мережі між учасниками. Крім того, цей об'єкт відповідає за формування опису медіасесії (SDP), отримання інформації про ICE-кандидатів для проходження через NAT або мережеві екрани (локальні та за допомогою STUN) та взаємодію з TURN-сервером. Кожен учасник повинен мати один RTCMediaConnection на кожне з'єднання. Медіапотоки передаються зашифрованим протоколом SRTP.
TURN-сервери
Загальний формат для STUN/TURN-серверів:
Публічних STUN-серверів в Інтернеті багато. Великий список, наприклад, є тут. На жаль, вирішують вони надто малу частину проблем. Публічних же TURN-серверів, на відміну STUN, практично немає. Пов'язано це з тим, що TURN-сервер пропускає через себе медіапотоки, які можуть завантажувати і мережевий канал, і сам сервер. Тому найпростіший спосіб підключитися до TURN-серверів - встановити його самому (зрозуміло, що буде потрібно публічний IP). З усіх серверів, як на мене, найкращий rfc5766-turn-server. Під нього є готовий образ для Amazon EC2.
Для RTCMediaConnection необхідний додатковий механізм обміну інформацією, що управляє, для встановлення з'єднання — хоча він і формує ці дані, але не передає їх, і передачу іншим учасниками необхідно реалізовувати окремо.

Вибір способу передачі доручається розробника — хоч вручну. Як тільки обмін необхідними даними пройде, RTCMediaConnection встановить медіапотоки автоматично (якщо вийде, звісно).
Модель offer-answer
Для встановлення та зміни медіапотоків використовується модель offer/answer (пропозиція/відповідь; описана в RFC3264) та протокол SDP (Session Description Protocol). Вони використовуються і протоколом SIP. У цій моделі виділяється два агенти: Offerer - той, хто генерує SDP-опис сесії для створення нової або модифікації існуючої (Offer SDP), і Answerer - той, хто отримує SDP-опис сесії від іншого агента та відповідає йому власним описом сесії (Answer SDP). При цьому специфікація потребує наявності протоколу вищого рівня (наприклад, SIP або власного поверху веб-сокетів, як у нашому випадку), що відповідає за передачу SDP між агентами.
Які дані необхідно передати між двома RTCMediaConnection, щоб вони змогли успішно встановити медіапотоки:
- Перший учасник, який ініціює з'єднання,формує Offer, в якому передає структуру даних SDP (цей же протокол для тієї ж мети використовується в SIP), що описує можливі характеристики медіапотоку, який він збирається почати передавати. Цей блок даних потрібно передати другому учаснику. Другий учасник формує Answer, зі своїм SDP та пересилає його першому.
- І перший та другий учасники виконують процедуру визначення можливих ICE-кандидатів, за допомогою яких до них зможе передати медіапотік другий учасник. У міру визначення кандидатів інформація про них має передаватись іншому учаснику.

Формування Offer
Для формування Offer нам знадобляться дві функції. Перша буде викликатись у разі його успішного формування. Другий параметр методу createOffer() - callback-функція, що викликається у разі помилки при його виконанні (за умови, що локальний потік вже доступний).
Додатково знадобляться два обробники подій: onicecandidate при визначенні нового ICE-кандидата і onaddstream при підключенні медіапотоку від дальньої сторони. Повернемось до нашого файлу. Додамо в HTML після рядків з елементами ще один:
І після рядка з елементом (на майбутнє):
Параметри для підготовки Offer SDP
Перший параметр методу createOffer() - callback-функція, що викликається при успішному формуванні Offer
Другий параметр - callback-функція, яка буде викликана у разі помилки
І оголосимо callback-функцію, якою передаватимуться ICE-кандидати в міру їх визначення:
А також callback-функцію для додавання медіапотоку від дальньої сторони (на майбутнє, тому що поки що у нас тільки один RTCPeerConnection):
При натисканні на кнопку «createOffer» створимо RTCPeerConnection, задамометоди onicecandidate і onaddstream і запросимо формування Offer SDP, викликавши метод createOffer():
Формування Answer SDP та обмін ICE-кандидатами
І Offer SDP, і кожного з ICE-кандидатів необхідно передати іншій стороні і там після їх отримання RTCPeerConnection викликати методи setRemoteDescription для Offer SDP і addIceCandidate для кожного ICE-кандидата, отриманого від дальньої сторони; аналогічно у зворотний бік для Answer SDP та віддалених ICE-кандидатів. Сам Answer SDP формується аналогічно Offer; Різниця в тому, що викликається не метод createOffer, а метод createAnswer і перед цим RTCPeerConnection методом setRemoteDescription передається Offer SDP, отриманий від зухвалої сторони.
Обробка Offer та Answer SDP
Формування Answer SDP дуже схоже на Offer. У callback-функції, що викликається при успішному формуванні Answer, аналогічно Offer, віддамо локальний опис і передамо отриманий Answer SDP першому учаснику:
Callback-функція, що викликається у разі помилки при формуванні Answer, повністю аналогічна Offer:
Параметри для формування Answer SDP:
При отриманні Offer другим учасником створимо RTCPeerConnection та сформуємо Answer аналогічно Offer:
Обробник події готовності ICE-кандидатів другого учасника дзеркально подібний до першого:
Сallback-функцію для додавання медіапотоку від першого учасника:
Завершення з'єднання
Додамо ще одну кнопку в HTML
І функцію для завершення з'єднання
Збережемо як rtctest3.html, викладемо на сервер та відкриємо у браузері. У цьому прикладі реалізовано двосторонню передачу медіапотоків між двома RTCPeerConnection в рамках однієї закладки браузера. Щоб організувати через мережу обмін Offer та Answer SDP, ICE-кандидатамиміж учасниками та іншою інформацією, потрібно замість прямого виклику процедур реалізувати обмін між учасниками за допомогою будь-якого транспорту, у нашому випадку веб-сокетів.
Трансляція екрану
Функцією getUserMedia можна також захопити екран і транслювати як MediaStream, вказавши такі параметри:
Для успішного доступу до екрану має виконуватись декілька умов:
- увімкнути прапор знімка екрана в getUserMedia() у chrome://flags/,chrome://flags/;
- вихідний файл має бути завантажений за HTTPS (SSL origin);
- аудіопотік не повинен запитуватись;
- не потрібно виконувати кілька запитів в одній закладці браузера.
Бібліотеки для WebRTC
Хоча WebRTC ще й не закінчено, вже з'явилося кілька бібліотек, що на ньому базуються. JsSIP призначена для створення браузерних софтфонів, що працюють із SIP-комутаторами, такими як Asterisk та Camalio. PeerJS спростить створення P2P-мереж для обміну даними, а Holla скоротить обсяг розробки, необхідний P2P-зв'язку з браузерів.
Node.js та socket.io
Для того, щоб організувати обмін SDP та ICE-кандидатами між двома RTCPeerConnection через мережу, використовуємо Node.js з модулем socket.io.
Встановлення останньої стабільної версії Node.js (для Debian/Ubuntu) описано тут
Встановлення під інші операційні системи описано тут
За допомогою npm (Node Package Manager) встановимо socket.io та додатковий модуль express:
Перевіримо, створивши файл nodetest2.js для серверної частини:
І nodetest2.html для клієнтської частини:
Обмін інформацією між RTCPeerConnection через веб-сокети
Клієнтська частина
Збережемо наш основний приклад (rtcdemo3.html) під новим ім'ям rtcdemo4.html. Підключимо велемент бібліотеки socket.io:
Замінимо прямий виклик функцій іншого учасника надсиланням йому повідомлення через веб-сокети:
У функції hangup() замість прямого виклику функцій другого учасника передамо повідомлення через веб-сокети:
І додамо обробники отримання повідомлення:
Серверна частина
На серверній стороні збережемо файл nodetest2 під новим ім'ям rtctest4.js і всередині функції io.sockets.on('connection', function (socket) < … >додамо прийом та відправку повідомлень клієнтів:
Крім цього, змінимо ім'я HTML-файлу: