Webrtc

written on Wednesday, August 15, 2012

Делаю тут одну штуку по поводу voip и решил поразбираться с тем, как с этим обстоят дела в прогрессивном будущем анархокоммунизма.

Анархокоммунизм, VoIP и хром вместо браузера

Будущее анархокоммузма в браузере состоит из нескольких кусков:

  • ручка GetUserMedia(), чтобы получить доступ к потоку звука или картинки от локального оборудования
  • тег <video>, который может показывать картинку со звуком
  • ручка URL.createObjectURL, которая из потока делает что-то, придуривающееся урлом и пригодное для подсовывание в качестве параметра src тегу <video>
  • ручка PeerConnection, которая дает двухсторонний (remote и local) канал, в который можно гнать свой поток от камеры, а получать другой поток, который прислали с другой стороны.

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

Зеркало из тега video

Чтобы показать на html странице изображение с камеры, достаточно первых трех пунктов из предыдущего списка - добавляем пустой тег <video> с включенным autoplay в нужное место страницы, потом делаем магический пасс:

navigator.webkitGetUserMedia({audio: video:true},
    function(stream) {
        var localVideo = document.getElementById("localVideo");
        localVideo.src = webkitURL.createObjectURL(stream);
    },
    function(err) {
        alert("Шеф, фсе пропало! Вот например "+err)
    }
)

Запросил поток - получи поток. Есть поток, есть куда показывать - нате, смотрите. Просто, как паравоз, например.

Деревянный VoIP

Чтобы получить минимально рабочую телефонию, нужно иметь как минимум:

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

Про SDP

Для того, чтобы описать ответы на второй и третий вопросы, существует SDP (session definition protocol). SDP изобретен приблизительно за триста лет до захвата цивилизованного мира мерзкими христианскими оккупантами, поэтому в нем нету божественного света XML, а есть понятный простому языческому атеисту plain text:

v=0
o=yate 1344907024 1344907024 IN IP4 109.23.33.80
s=SIP Call
c=IN IP4 109.23.33.80
t=0 0
m=audio 26136 RTP/SAVPF 0 8 101
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000

Вызывающий аппарат вспоминает свой IP, список поддерживаемых кодеков и формирует такую колбасу, которую поток как-то отправляет вызываемому. Как отправляет - это уже не проблемы SDP. Хоть аяксом в браузере, хоть по UDP соседу, хоть голубиной почтой - это все протокола верхнего уровня.

Offer-answer

Колбаса, которую сформировал вызывающий аппарат, называется "offer". Если вызываемый аппарат с "той стороны" нашелся и колбасу ему передали, то он на нее смотрит и сравнивает присланный список кодеков со своим собственным.

Если все хорошо и подходящий кодек, поддерживаемый с обеих сторон нашелся, то формируется "answer" - колбаса с выбранным кодеком, своим адресом и шлется обратно.

Стандарт SDP описывает формат пересылаемой колбасы и процедуру согласования, чтобы все все правильно поняли и по итогам обмена данными начали вещать по нужному адресу поток, кодированный одним и тем же кодеком.

Обратно Webrtc

Для начала с обоих сторон создается объект peer, через который будет происходить управление коммуникацией, потом к этому объекту подключается локальный поток от камеры (полученный через GetUserMedia, как показано в примере с зеркалом).

Дальше с вызывющей стороны генерируется offer, кодируется в SDP и отправляется аяксом куда-нибудь на сервер, где разберутся, какого абонанта подключать. При этом offer устанавливается, как LocalDescription, а полученный впоследствии answer - как RemoteDescription.

pc = new PeerConnection();
pc.addStream(localStream, null);
pc.startIce();
offer = pc.createOffer({audio:true})
pc.setLocalDescription(pc.SDP_OFFER, offer)
// закодировали offer в SDP и куда-то отправили:
// ajax_send_(offer.toSdp())
// получили обратно anser в виде текста
pc.setRemoteDescription(pc.SDP_ANSWER, new SessionDescription(answer_sdp));

С вызываемой все симметрично - декодим полученный offer из строки в объект, устанавливем, как RemoteDescription, генерируем answer, устанавливем, как LocalDescription, кодируем в строку и шлем аяксом в обратную сторону.

pc = new PeerConnection();
// поймали sdp в виде текста
pc.setRemoteDescription(pc.SDP_OFFER, new SessionDescription(offer_sdp)).
pc.startIce();
pc.addStream(localStream, null);
answer = pc.createAnswer(offer_sdp, {audio:true})
pc.setLocalDescription(pc.SDP_ANSWER, answer)
// отправили в обратку answer.toSdp()

Магический вызов startIce() включает ICE agent - компонент, занимающийся выяснением собственных айпи и доступности удаленных, указанных в описании сессии. Для чего ручка, дергающая его вызов, выведена в апи я не понял.

Что с подключеним дальше делать

Объект peer, как любая приличная жаваскриптовая хренована, кидается разными ивентами, например "addstream" и "removestream". На них нужно вешаться, чтобы поймать момент установления связи и вывести полученный поток в тег video таким же способом, каким делалось зеркало:

pc.onaddstream = function(event) {
    var remoteVideo = document.getElementById("remoteVideo");
    remoteVideo.src = webkitURL.createObjectURL(event.stream);
}

Невебовый оффтопик

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

Можно проводить аналогии с SIP-телефонией. Собственно протокол SIP сам по себе занимается тем, что кодирует сообщение "вам тут звонят", рассказывает кто и откуда, а аттачем привозит ту же самую колбасу SDP, описывюащую как именно звонят. При этом сигнальный трафик (SIP) может ходить по одному маршруту, а в SDP быть указан другой.

Как и в случае SIP-телефонов, сам поток с голосом и картинкой упаковывается в RTP, отсюда напрашивается идея взять SDP offer от webrtc, запихнуть в сигнальный пакет SIP-протокола и получить голосовой линк от телефона к браузеру напрямую без гейтов и перекодирования.

Все было бы красиво и просто, но кто-то очень умный и красивый решил сделать в webrtc обязательное шифрование - это чтобы всем было удобно и безопасно, а то вдруг забудем включить. Поскольку писатели сип-телефонов не такие умные и красивые, на указанный в sdp offer ключик a=crypto они делают большие глаза и нервно хихикая шлют отлуп "488 Not acceptable here".

Браузер аналогично реагирует на попытку подсунуть sdp offer без включенного шифрования - вылетает с мутным "DOM Exception 12".

Так что халявы не будет - быстро-быстро начинаем бегать и лепить во все сиповые библиотеки поддержку шифрования, где она и так была - пересобираем клиент с ее включением, а где все совсем плохо - терминируем шифрованный трафик на гейте.

This entry was tagged code, voip and webrtc