written on Wednesday, August 15, 2012
Делаю тут одну штуку по поводу voip и решил поразбираться с тем, как с этим обстоят дела в прогрессивном будущем анархокоммунизма.
Будущее анархокоммузма в браузере состоит из нескольких кусков:
Первые три штуки у нормальных людей уже есть, а вот последняя еще толком не готова, поэтому даже в хроме работает только после включения кнопки в настройках.
Чтобы показать на html странице изображение с камеры, достаточно первых трех пунктов из предыдущего списка - добавляем пустой тег <video> с включенным autoplay в нужное место страницы, потом делаем магический пасс:
navigator.webkitGetUserMedia({audio: video:true}, function(stream) { var localVideo = document.getElementById("localVideo"); localVideo.src = webkitURL.createObjectURL(stream); }, function(err) { alert("Шеф, фсе пропало! Вот например "+err) } )
Запросил поток - получи поток. Есть поток, есть куда показывать - нате, смотрите. Просто, как паравоз, например.
Чтобы получить минимально рабочую телефонию, нужно иметь как минимум:
Для того, чтобы описать ответы на второй и третий вопросы, существует 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" - колбаса с выбранным кодеком, своим адресом и шлется обратно.
Стандарт SDP описывает формат пересылаемой колбасы и процедуру согласования, чтобы все все правильно поняли и по итогам обмена данными начали вещать по нужному адресу поток, кодированный одним и тем же кодеком.
Для начала с обоих сторон создается объект 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".
Так что халявы не будет - быстро-быстро начинаем бегать и лепить во все сиповые библиотеки поддержку шифрования, где она и так была - пересобираем клиент с ее включением, а где все совсем плохо - терминируем шифрованный трафик на гейте.