VoIP

written on Sunday, February 24, 2013

В старом посте про WebRTC я уже писал про таки страшные штуки, как SDP, RTP и getUserMedia. Если вы посмотрите на записи под тегом voip в этом бложике, то догадаетесь, что я пиишу какое-то сишечное voip-приложение и оно как-то связано с WebRTC.

Краткое содержание

  • getUserMedia и createObjectURL позволяют браузерному жаваскрипту получить доступ к камере и микрофону
  • SDP - это такой кусок текста, в котором написаны поддерживаемые кодеки, адрес, на который можно слать поток, и другая информация, вроде ключей шифрования

Два клиента могут каким-то образом обменяться 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 или куда дефолт роут смотрит), адресом на который можно коннектиться снаружи. Это очень часто оказывается неправдой. Белые люди из интернета не смогут прислать звук на мой серый ойпи.

RTPProxy

Одним из ходовых вариантов решения проблемы серого айпи является умный сервер с RTP проксей. Работает это так:

  • тупой клиент номер 1 присылает SDP со своим адресом L1:C1 на сервер
  • сервер подменяет айпи первого клиента на свой собственный SS и порт X1 и пересылает второму клиенту
  • второй клиент запоминает адрес SS:X1, как адрес, на который будет гнать звук
  • второй клиент отвечает серверу пакетом с адресом своего локалхоста L2:C2
  • сервер опять подменяет адрес на свой собствнный и порт X2 и пересылает ответ обратно первому клиенту
  • первый клиент запоминает адрес SS:X2
  • оба клиента начинают гнать свой голосовой трафик на разные порты сервера

Самое интересное, что серверу даже не нужно знать, какие у этих клиентов были настоящие адреса! Если кто-то гонит поток на SS:X1, а кто-то другой на SS:X2, достаточно знать только пару X1,X2 чтобы редирект работал.

Недостатки этого метода очевидны - лишняя прокся, добавляющая летентности, жрущая ресурсы и способная упасть, обвалив весь сервис.

ICE

Чтобы избавиться от прокси, умные люди придумали такую штуку, как 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? А WebRTC тут при том, что для установления сессии с браузерным клиентом, поддержка ICE обязательна. Браузер просто не будет слать никаких данных до тех пор, пока ему не ответят на ICE-хендшейк.

Более того, если пир перестал отвечать на айсовые кипелайв-пакеты, вещание RTP потока очень быстро (в течении нескольких секунд) прекращается.

SSRC

Кроме айсовой магии в 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=sssrc нету, он будет считать, что с той стороны никто не будет ничего вещать, что эквивалентно a=recvonly, хотя явно будет указан a=sendrecv.

Браузер будет молча игнорить пакеты входящего RTP-потока SSRC которых он не знает. Такая ситуация диагностируется просто: на дебажной странице chrome://webrtc-internals/ будет статистика только по одному SSRC (вещаюшему) и не будет второго, с которого он принимает поток.

При этом в жаваскриптовой контексте успешно зафайрится коллбек onaddstream в котором будет звуковая дорожка c лейблом "default".

This entry was tagged code, voip and webrtc