written on Wednesday, August 15, 2012
Роблю тут одну штуку по поводу voip і вирішив розібратися, як з цим обстоїть справа в прогресивному майбутньому анархокомунізму.
Майбутнє анархокомунізму в браузері складається з кількох шматків:
Перші три штуки в нормальних людей уже є, а от остання ще толком не готова, тому навіть у хромі працює тільки після вмикання кнопки в налаштуваннях.
Щоб показати на html сторінці зображення з камери, достатньо перших трьох пунктів з попереднього списку - додаємо пустий тег
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".
Так що халяви не буде - швидко-швидко починаємо бігати і ліпити в усі сіпові бібліотеки підтримку шифрування, де вона і так була - перезбираємо клієнт з її ввімкненням, а де все зовсім погано - термінуємо зашифрований трафік на гейті.