written on Sunday, February 24, 2013
В старому пості про WebRTC я вже писав про такі страшні штуки, як SDP, RTP та getUserMedia. Якщо ви подивитеся на записи під тегом voip в цьому бложику, то здогадаєтеся, що я пишу якесь сішечне voip-додаток і воно якось пов'язане з WebRTC.
Два клієнти можуть якимось чином обмінятися SDP-документами, після чого почнуть надсилати один одному RTP потік.
Один з важливих кусків інформації, який кодується в SDP — адреса та порт, на які треба гнати RTP потік. Ось кусок SDP з тупого-дерев'яного клієнта:
v=0 o=texr 1344907024 1344907024 IN IP4 192.168.1.7 s=SIP Call t=0 0 c=IN IP4 192.168.1.7 m=audio 26136 RTP/AVP 0 8 101 a=rtpmap:0 PCMU/8000 a=rtpmap:8 PCMA/8000 a=rtpmap:101 telephone-event/8000
В рядках c та m написано, що потік треба гнати на IP4 192.168.1.7:26136. Цікаво, що порт пишеться не в c (connection), а в m (media).
Тупий sip-клієнт дуже часто лінується і вважає адресу, встановлену на локальному інтерфейсі (eth0 або куди дефолт роут дивиться), адресою на яку можна коннектитися ззовні. Це дуже часто виявляється неправдою. Білі люди з інтернету не зможуть прислати звук на мій сірий ойпі.
Одним з ходових варіантів вирішення проблеми сірого айпі є розумний сервер з RTP проксею. Працює це так:
Найцікавіше, що серверу навіть не треба знати, які в цих клієнтів були справжні адреси! Якщо хтось жене потік на SS:X1, а хтось інший на SS:X2, достатньо знати тільки пару X1,X2 щоб редирект працював.
Недоліки цього методу очевидні — зайва прокся, яка додає латентності, жере ресурси та здатна впасти, обваливши весь сервіс.
Щоб позбутися проксі, розумні люди придумали таку штуку як ICE. У випадку з ICE, клієнт має бути трохи розумнішим. Замість того щоб намагатися з'ясувати свій правильний айпі, він має з'ясувати всі можливі адреси, за якими він теоретично може бути доступний. Ці адреси називаються candidates. В жаваскриптовому контексті інформація про них приходить в колбек onicecandidate() по одній за раз.
При цьому треба мати на увазі, що в WebRTC спочатку формується SDP, а потім, по мірі знаходження кандидатів, спрацьовують колбеки, тобто треба або чекати знаходження всіх кандидатів і вручну дописувати їх в SDP, або передавати дані про кандидатів окремо від основного SDP-документа. У випадку інтерконнекту зі звичайними сіп-клієнтами, перший спосіб зручніший — він хоч і додає деяку затримку, зате дозволяє вкластися в один раундтріп INVITE пакета. В браузерних демках, що передають SDP через звичайний HTTP-API, використовується другий варіант.
В SDP цей список виглядає так:
c=IN IP4 192.168.1.7 m=audio 26136 RTP/AVP 0 8 101 a=candidate:c0a80107 1 UDP 2113929471 192.168.1.7 6796 typ host a=candidate:d213de61 1 UDP 2113929471 fdb2:e296:6fde:64f:b8ea:83b4:4b58:74bf 6796 typ host a=candidate:ac140a03 1 UDP 2113929471 172.20.10.3 6796 typ host a=candidate:76adc86a 1 UDP 1677721855 118.173.200.107 13615 typ srflx raddr 192.168.1.7 rport 6796 a=ice-ufrag:nddw a=ice-pwd:WuN7cJA88UOMQMzmOWMGbF
Записи в яких в кінці рядка написано "typ host" — це адреси на локальних мережевих інтерфейсах. Ніхто не пішов ображеним, навіть раптовий IPv6 знайшовся. Рядок з "typ srflx" (self reflex) — це вже біла адреса, яка стирчить ззовні ната та відповідний їй локальний адрес. Щоб з'ясувати srflx, клієнту довелося сходити в мережу та спитати STUN сервер, як виглядає його адреса ззовні. В WebRTC демках часто використовується stun.l.google.com:19302.
При цьому старі записи c та m теж нікуди не поділися і там написана та сама фігня, що й раніше, яку сервер підмінить на адресу проксі.
Навіщо це все треба? Обидва клієнти — той що викликає і той що викликається — обмінюються SDP, в яких є цей список. Далі вони починають перебирати можливі пари адрес (local-remote) і намагатися конектитися один до одного.
Після того як один з адресів виявляється доступним і по ньому проходить ICE-хендшейк, той самий сокет починає використовуватися для передачі RTP-даних. Цікаво, що ICE клієнт при цьому не завершує свою роботу і ганяє кіпалайви весь час. По тому ж сокету, ага.
Крім усього іншого в записів є пріоритети, щоб, наприклад, адреса на дротовому інтерфейсі перевірялася перед якимось лаговим та дорогим 3g-лінком.
Якщо клієнт виявився тупим (без підтримки ICE) або просто зафейлив всі перевірки, то все мовчки деградується до попереднього варіанту з RTP-проксею, якщо все пройшло успішно — дані ходять напряму.
До чого тут WebRTC? А WebRTC тут до того, що для встановлення сесії з браузерним клієнтом підтримка ICE обов'язкова. Браузер просто не буде слати ніяких даних доти, доки йому не відповідають на ICE-хендшейк.
Більше того, якщо пір перестав відповідати на айсові кіпалайв-пакети, віщання RTP потоку дуже швидко (впродовж кількох секунд) припиняється.
Крім айсової магії в SDP-пакетах, які генерує WebRTC, ще є магічні рядки з SSRC:
a=msid-semantic: WMS CTQ1D8BsyfzoptuJsIuieS0H9AZ9pYy6tD2i m=audio 1 RTP/SAVPF 103 104 111 0 8 107 106 105 13 126 a=ssrc:1378032147 cname:VvPCBGsSjrVsVchc a=ssrc:1378032147 msid:CTQ1D8BsyfzoptuJsIuieS0H9AZ9pYy6tD2i 6898a9d4-13df-44c0-8f80-f490a9bb5070 a=ssrc:1378032147 mslabel:CTQ1D8BsyfzoptuJsIuieS0H9AZ9pYy6tD2i a=ssrc:1378032147 label:6898a9d4-13df-44c0-8f80-f490a9bb5070
Хто всі ці люди? cname — це використовуваний в RTCP цього потоку cname, ssrc — це 32-бітне число, присутнє в кожному RTP пакеті з цього потоку. mslabel — ідентифікатор стріму, label — має бути щось внятне, на кшталт "mic01". Найперший рядок з msid-semantic — це атрибут всієї сесії, а не конкретного медіа-потоку.
Це потрібно через те, що один RTP-стрім може використовуватися для передачі кількох різних медіа-потоків. Наприклад для двох каналів з різних мікрофонів або одного мікрофона та однієї камери. Щоб демуксити ці треки, на рівні RTP в них будуть різні SSRC. Ясна річ, що циферки SSRC для приймача та передавача теж мають різнитися і за цим треба стежити.
Текстові рядки — label та mslabel підуть прямо в опис треків в колбеку onstreamadd в жаваскриптовому контексті.
Якщо відповісти браузерному клієнту таким SDP-пакетом, де a=ssrc немає, він буде вважати що з того боку ніхто не буде нічого віщати, що еквівалентно a=recvonly, хоча явно буде вказано a=sendrecv.
Браузер буде мовчки ігнорити пакети вхідного RTP-потоку SSRC яких він не знає. Така ситуація діагностується просто: на дебажній сторінці chrome://webrtc-internals/ буде статистика тільки по одному SSRC (віщаючому) і не буде другого, з якого він приймає потік.
При цьому в жаваскриптовому контексті успішно зафайриться колбек onaddstream в якому буде звукова доріжка з лейблом "default".