P2P в браузері


Автор: Олександр Трищенко

Я розповім про своє хобі — організації відеотрансляцій у браузері за технологією WebRTC (Web Real-Time Communication — веб-комунікація в режимі реального часу). Цей проект з відкритим вихідним кодом Google активно розвиває з 2012 р., а перший стабільний реліз з'явився в 2013 р. Зараз WebRTC вже добре підтримується найпоширенішими сучасними браузерами, за винятком Safari.

Технологія WebRTC дозволяє влаштувати відеоконференцію між двома або кількома користувачами за принципом P2P. Таким чином, дані між користувачами передаються безпосередньо, а не через сервер. Втім, сервер нам все одно знадобиться, але про це скажу далі. Насамперед, WebRTC розрахована на роботу в браузері, але є й бібліотеки для різних платформ, які теж дозволяють використовувати WebRTC-з'єднання.

Якщо ми використовуємо WebRTC, ми вирішуємо наступні проблеми:

  • Знижуємо витрати на утримання серверів. Сервери потрібні тільки для ініціалізації з'єднання і щоб користувачі обмінялися мережевий інформацією один про одного. Також вони використовуються для розсилки якихось подій, наприклад, оповіщень про підключення і відключення користувачів (щоб інформація на кожному клієнті була актуальною).
  • Збільшуємо швидкість передачі даних і зменшуємо затримки при передачі відео і звуку — адже сервер для цього не потрібен.
  • Підсилюємо приватність даних: немає третьої боку, через який йшов потік даних (звичайно, за винятком шлюзів, через які проходять дані до виходу в мережу).

Ініціалізація з'єднання

JavaScript Session Establishment Protocol

З'єднання ініціалізується за протоколом JavaScript Session Establishment   зараз є тільки чернетка, який описує специфікацію цього рішення. У ньому описується:

  • процес підключення до сервера;
  • генерація унікального токен для нового користувача, який дозволить ідентифікувати його на back-end'е;
  • отримання користувачем ключа для доступу до розмови — signaling;
  • ініціалізація RTCStreamConnection для з'єднання двох користувачів;
  • отримання всіма учасниками розмови широкомовного повідомлення про нового учасника.


Підключення нового учасника до відеоконференції відбувається наступним чином: ми відправляємо цьому користувачеві повідомлення, що у нас вже йде відеоконференція, користувач надсилає запит на підключення, який ми задовольняємо, виславши йому всю необхідну інформацію для підключення. У той же час ми висилаємо всім учасникам конференції інформацію для з'єднання з новим користувачем.

Як це працює? Є рівень браузера, який дозволяє клієнтам безпосередньо обмінюватися медіаданими, і є рівень сервера-маяка (signaling server), на якому відбувається інше взаємодія між клієнтами:



Session Description Protocol

Для протоколу опису сесій (SDP — Session Description Protocol), який дозволяє описати інформацію про конкретного користувача, є вже затверджена специфікація RFC 4556.

SDP описує наступні параметри:



  • v= (версія протоколу; зараз версія завжди 0);
  • o= (ідентифікатори творця/власника та сесії);
  • s= (ім'я сесії, не може бути порожнім);
  • i=* (інформація про сесії);
  • u=* (URL-адреса, що використовується WWW-клієнтами, з додатковою інформацією про сесії);
  • e=* (e-mail особи, відповідальної за конференцію);
  • p=* (номер телефону особи, відповідальної за конференцію);
  • c=* (інформація для з'єднання — не потрібно, якщо є в описі всіх медіаданих);
  • b=* (інформація про займану смугу пропускання каналу зв'язку);
  • одна і більше рядків з описом параметрів часу (приклад див. нижче);
  • z=* (установка для тимчасової зони);
  • k=* (ключ шифрування);
  • a=* (одна або кілька рядків з описом атрибутів сесії, див. нижче).


SDP-інформація про будь-якого клієнта виглядає приблизно так:

Interactive Connectivity Establishment (ICE)

Технологія ICE дозволяє підключитися користувачам, що знаходиться за фаєрволом. Вона включає в себе чотири специфікації:

  • RFC 5389: Session Traversal Utilities for NAT (STUN).
  • RFC 5766: Traversal Using Relays around NAT (TURN: Relay Extensions to STUN.
  • RFC 5245: Interactive Connectivity Establishment (ICE: A Protocol for NAT Traversal for Offer/Answer Protocols.
  • RFC 6544: TCP Candidates with Interactive Connectivity Establishment (ICE)


STUN — клієнт-серверний протокол, який активно застосовується для VoIP. STUN-сервера тут вважається пріоритетним: він дозволяє спрямовувати UDP-трафік. Якщо ми не зможемо скористатися STUN-сервера, WebRTC спробує підключитися до сервера TURN. Також у списку є RFC по самому ICE і RFC по TCP-кандидатам.

Реалізація клієнта для ICE-серверів в WebRTC вже встановлена, так що ми можемо просто вказати кілька серверів STUN або TURN. Більш того, для цього при ініціалізації досить просто передати об'єкт з відповідними параметрами (далі я наведу приклади).

GetUserMedia API

Одна з найцікавіших частин WebRTC API — GetUserMedia API, який дозволяє захоплювати аудіо — та відеоінформацію безпосередньо з клієнта і транслювати її іншим учасникам. Цей API активно просуває Google — так, з кінця 2014 р. Hangouts працює повністю на WebRTC. Також GetUserMedia повноцінно працює в Chrome, Firefox і Opera; є також розширення, яке дозволяє працювати з WebRTC на IE. В Safari ж ця технологія не підтримується.

Важливо враховувати, що, згідно з нещодавно вийшов обмеження, в Chrome GetUserMedia API буде працювати тільки під HTTPS на сервері-маяку. Тому багато прикладів, що лежать в мережі на HTTP-серверах, у вас працювати не будуть.

Цікаво, що раніше GetUserMedia API дозволяв транслювати екран, але зараз такої можливості немає — є тільки custom-вирішення за допомогою додатків, або ж в Firefox можна включити відповідний прапор на час розробки.

Зараз у GetUserMedia є наступні можливості:

  • вибір мінімального, "ідеального" і максимального дозволу для відеопотоку, що передбачає можливість змінювати дозвіл відео в залежності від швидкості підключення;
  • можливість вибрати будь-яку з камер на телефоні;
  • можливість вказати частоту кадрів.


Все це налаштовується дуже просто. Коли ми викликаємо GetUserMedia API, ми передаємо туди об'єкт, який має дві властивості — «audio» і «video»:

{ audio: true, video: { width: 1280, height: 720 } }


Де video, там також може стояти «true» — тоді налаштування за замовчуванням. Якщо буде стояти «false», то аудіо або відео буде відключено. Ми можемо налаштувати відео — вказати ширину і висоту. Або ж ми можемо, наприклад, встановити мінімальну, ідеальну і максимальну ширину:

width: { min: 1280 }


width: { min: 1024, ideal: 1280, max: 1920 }


А ось так ми вибираємо камеру, яку хочемо використовувати («user» — фронтальна, «environment» — задня):

video: { facingMode: "user" }


video: { facingMode: "environment" }


Також ми можемо вказати мінімальну, ідеальну і максимальну частоту кадрів, яка буде вибиратися в залежності від швидкості підключення і ресурсів комп'ютера:

video: { frameRate: { ideal: 10, max: 15 }


Такі можливості GetUserMedia API. З іншого боку, в ньому явно не вистачає можливості визначити бітрейт відео — і аудіо потоку і можливості як-небудь працювати з потоком до його фактичної передачі. Також, звичайно, не завадило б повернути можливість трансляції екрану, присутню раніше.

Особливості WebRTC

WebRTC дозволяє нам змінювати аудіо — і відеокодеки — їх можна вказати безпосередньо в переданої інформації SDP. Взагалі, в WebRTC використовуються два аудіокодека, G711 і OPUS (вибираються автоматично в залежності від браузера), а також відеоформат VP8 (WebM від Google, який відмінно працює з HTML5-відео).

Технологія WebRTC в тій чи іншій мірі підтримується Chromium 17+, Opera 12+, Firefox 22+. Для інших браузерів можна використовувати розширення webrtc4all, але особисто мені не вдалося запустити на Safari. Є також С++ бібліотеки для підтримки WebRTC — швидше за все, це говорить про те, що в майбутньому ми зможемо побачити реалізації WebRTC у вигляді настільних додатків.

Для забезпечення безпеки використовується DTLS, протокол безпеки транспортного рівня, який описує RFC 6347. А для з'єднання користувачів, які перебувають за NAT і міжмережними екранами, як я вже говорив, використовуються TURN і STUN-сервери.

Маршрутизація

Тепер розглянемо детально, як здійснюється маршрутизація за допомогою STUN та TURN.

Traversal Using Relay NAT TURN) — це протокол, який дозволяє вузлу за NAT або фаєрволом отримувати вхідні дані через TCP або UDP-з'єднання. Це вже стара технологія, тому в пріоритеті варто використання Session Traversal Utilities for NAT (STUN) — мережного протоколу, що дозволяє встановити тільки UDP з'єднання.

Для забезпечення відмовостійкості є можливість вибрати кілька STUN-серверів, як це зробити, показано в інструкції. А протестувати підключення до STUN — і TURN-серверів можна тут.

STUN — і TURN-сервери працюють наступним чином. Припустимо, є два клієнта з внутрішніми IP і з зовнішнім виходом в мережу через міжмережеві екрани:



Щоб пов'язати ці два клієнта і перенаправити всі порти, нам необхідно використовувати STUN та TURN-сервери, після чого користувачеві передається необхідна інформація і встановлюється пряме з'єднання між комп'ютерами.

Алгоритм роботи з ICE-серверами

  • Забезпечити доступ до STUN-сервера на момент ініціалізації RTCPeerConnection. Можна використовувати публічні STUN-сервери (наприклад, Google).
  • Слухати подія ініціалізації підключення і в разі успіху починати передачу потоку.
  • У разі помилки виконати запит до TURN-сервера і постаратися поєднати клієнтів через нього.
  • Подбати про достатньої пропускної спроможності TURN-сервера.


Продуктивність і швидкість передачі відеопотоку

  • 720p at 30 FPS: 1.0~2.0 Mbps
  • 360p at 30 FPS: 0.5~1.0 Mbps
  • 180p at 30 FPS: 0.1~0.5 Mbps


Найсильніше впливає на продуктивність WebRTC трафік, який потрібно виділяти на трансляцію відео. Якщо в трансляції беруть участь лише два користувача, то можна організувати без проблем хоч FullHD, в разі ж відеоконференції на 20 осіб у нас будуть проблеми.

Ось, наприклад, результати, отримані з MacBook Air 2015 (4 Гб оперативної пам'яті, процесор Core i на 2 ГГц). Просто реалізація GetUserMedia вантажить процесор на 11 %. При ініціалізації з'єднання Chrome-Chrome процесор завантажений вже на 50 %, якщо три Chrome — на 70 %, чотири — повне завантаження процесора, а п'ятий клієнт призводить вже до гальмах, і WebRTC в результаті вилітає. На мобільних браузерах, звичайно, ще гірше: мені вдалося зв'язати тільки двох користувачів, а при спробі підключення третього все стало гальмувати і з'єднання обірвалося.

Як з цим можна впоратися?

Масштабування і вирішення проблем

Отже, що ми маємо? На кожного користувача конференції треба відкривати своє UDP з'єднання і передавати дані. У підсумку на трансляцію 480p-відео зі звуком одному користувачеві потрібно 1-2 мебагита, і 2-4 мегабіта у разі двостороннього зв'язку. На залізо яке транслювало лягає велике навантаження.

Проблема продуктивності вирішується шляхом використання проксі-сервера (ретранслятора), який буде отримувати дані від віщає, якщо це конференція, і роздавати їх усім іншим. Якщо потрібно, для цього можна використовувати не один сервер, а кілька.

Варто також варто враховувати, що, якщо ми проводимо відеотрансляцію, передаємо дуже багато надлишкової клієнтської інформації, від якої можемо відмовитися. Наприклад, при проведенні курсу нам від інших може бути потрібен тільки звук без відео або навіть тільки повідомлення. Також після зміни активного доповідача може мати сенс перервати одне із з'єднань, змінити тип інформації, який ми отримуємо в потоці, після чого підключитися знову. Це дозволяє оптимізувати мережеву навантаження.

Важливо врахувати програмні обмеження — ми можемо підключити не більше 256 бенкетів до одного инстансу WebRTC. Це позбавляє нас можливості використовувати, наприклад, який-небудь величезний і дорогий інстанси на Amazon для масштабування, т. ч. рано чи пізно нам доведеться пов'язувати між собою кілька серверів.

CreateOffer API

Як це все працює? Є API CreateOffer(), який дозволяє створити SDP-дані, які ми будемо надсилати:



CreateOffer — promise, який після створення offer дозволяє отримати безпосередньо localDescription, помістити в нього offer і використовувати його далі як SDP-поле. sendToServer — абстрактна функція для запиту до сервера, де написано ім'я нашої машини (name), ім'я цільової машини (target), тип offer і SDP.

Хочу сказати пару слів про сервер: для даних цілей дуже зручно використовувати WebSocket. Як тільки хтось підключається, ми можемо заэмитить подія і додати до нього слухача. Також нам буде досить просто эмитить свої події з клієнтом.

GetUserMedia API в дії

В реальному житті все можна зробити трохи простіше — ми можемо звернутися до GetUserMedia API. Ось як тоді виглядає ініціалізація дзвінка:



Тут ми хочемо передати і відео і звук. На виході отримуємо промис, який приймає в себе об'єкт mediaStream (це і є потік даних).

А ось так ми відповідаємо на дзвінок:



Коли хтось хоче відповісти на дзвінок, він отримує наш offer (getRemoteOffer— абстрактна функція, яка отримує наші дані з сервера). Потім getUserMedia ініціалізує потокову передачу, а про onaddstream і addstream я вже сказав. setRemoteDescription — ми ініціалізуємо для нашого RTC і вказуємо його в якості offer'а й передаємо відповідь (answer). send the answer… → тут ми просто описуємо процес передачі offer на сервер, після чого відбувається встановлення зв'язку між браузерами і початок трансляції.

Клієнт-серверна архітектура для WebRTC

Як все це виглядає, якщо ми масштабується через сервер? Як звичайна клієнт-серверна архітектура:



Тут між транслює і веб-сервером, який ретранслює потік, є WebSocket-з'єднання і RTCPeerConnection. Решта встановлюють RTCPeerConnections. Інформацію про транслирующем можна отримати пулингом, можна заощадити на кількості WebSocket-з'єднань.

Способів масштабування такої архітектури поки не дуже багато, і всі вони схожі. Ми можемо:

  • Збільшити кількість серверів.
  • Розмістити сервери в зонах найбільшого скупчення цільової аудиторії.
  • Забезпечити надлишкову продуктивність — адже технологія ще активно розвивається, так що можливі збої в роботі.


Підвищуємо отказоусточивость

По-перше, для підвищення відмовостійкості має сенс розділити сервер-маяк і сервер-ретранслятор. Ми можемо використовувати WebSocket-сервер, який буде роздавати мережеву інформацію про клієнтів. Сервер-ретранслятор буде спілкуватися з WebSocket-сервером і транслювати через себе медіапотік.

Далі, ми можемо організувати можливість швидкого перемикання з одного яке транслювало сервера на інший у разі відмови пріоритетного

Для крайнього випадку, можна передбачити можливість встановлення прямого з'єднання між користувачами в разі відмови транслюють серверів, але це годиться тільки на той випадок, коли в нашій конференції бере участь небагато людей (точно менше 10).

Відображення медіапотоку

Ось приклад, як ми можемо відобразити потік, яким обмінялися між клієнтами:



onaddstream — ми додаємо слухача. Коли додається слухач, створюємо елемент video, додаємо цей елемент на нашу сторінку, а в якості джерела вказуємо потік. Тобто щоб побачити все це в браузері, просто вказуємо source HTML5-відео як потік, який отримали. Все дуже просто.

У разі завершення дзвінка (метод endCall) ми проходимо по елементам відео, зупиняємо ці відео і закриваємо peer connection, щоб не було ніяких артефактів і зависань. У випадку помилки завершуємо дзвінок так само.

Корисні бібліотеки

Наостанок — деякі бібліотеки, які ви можете використовувати для роботи з WebRTC. Вони дозволяють написати простий клієнт в кілька десятків рядків:

  • Simple peer — мені дуже подобається ця бібліотека, яка вирішує задачу створення з'єднання між двома користувачами.
  • Easyrtc — дозволяє створювати відеоконференції.
  • SimpleWebRTC — аналогічна бібліотека.
  • js-platform/p2p.
  • node-webrtc — бібліотека дозволяє організувати сервер-маяк.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.