Введение
Webrtc на хабре уже неоднократно упоминался, хотелось бы рассказать немного про техническую часть реализации и осветить создание небольшого видео чата. Хочу сразу оговорится, что реализация webrtc постоянно меняется, в том числе названия функций api, их параметры.
Всем, кому просто хотелось бы посмотреть сразу как это все работает, сюда: apprtc.appspot.com демка от гугла все что нужно — это перейти по ссылке и послать её еще кому-нибудь уже с номером комнаты. В конце нужно поменять цифры если окажется что комната переполнена. Кому интересно как это все работает добро пожаловать под кат
Также довольно криво работает под разными браузерами. Например, так и не получилось на сегодняшний день нормально связать cromium и chrome, а также мобильный chrome и декстопный, хотя возможно уже завтра ситуация резко изменится.
Общая часть
Само API webrtc состоит из трех частей:
- getUserMedia (MediaStream), если упрощено, то это захват видео потока в браузере, например просто посмотреть на самого себя ;).
На хабре есть хорошая статья. - RTCPeerConnection используется для связи между браузерами напрямую. Собственно, об RTCPeerConnection речь в основном и пойдет дальше.
- RTCDataChannel: необходим для обмена различными данными: текстом, файлами и другими. На данный момент пишут, что он в 25 chrome доступен только в тестовом варианте, без включения флагов он станет доступен лишь в 27 chrome.
Peer Connection
Итак, начнем. На самом деле, чтоб не изобретать велосипед, было решено взять код из этой демки, немного сделать его более универсальным (он привязан к google app engine ) и упростить в паре мест. Тут подключается еще одна библиотека adapter.js — она нужна для некоторой унификации кода, потому что многое еще пишется с префиксами, а также различается для основных браузеров.
Сам RTCPeerConnection вызывается довольно просто:
// Stun сервер необходим для того чтоб могли связаться между собой те, кто находится за NAT, ну и, конечно, google нам любезно его предоставляет. var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]}; var pc_constraints = {"optional": [{"DtlsSrtpKeyAgreement": true}]}; pc = new RTCPeerConnection(pc_config, pc_constraints); pc.onicecandidate = onIceCandidate; pc.onaddstream = onRemoteStreamAdded;
В старом варианте в RTCPeerConnection() передавались немного другие параметры.
Обмен сообщениями
На этом этапе браузеры обмениваются разными сообщениями чтоб узнать как связаться друг с другом. В сообщениях типа candidate приходят разные варианты, в том числе полученные от stun сервера.
// тут два потока видео и аудио указан номер кандидата и айпишник. S->C: {"type":"candidate","label":1,"id":"video","candidate":"a=candidate:2437072876 1 udp 2113937151 192.168.1.2 35191 typ host generation 0\r\n"} S->C: {"type":"candidate","label":0,"id":"audio","candidate":"a=candidate:941443129 1 udp 1845501695 111.222.111.222 35191 typ srflx raddr 192.168.1.2 rport 35191 generation 0\r\n"}
// CallBack функция, с помощью которой RTCPeerConnection и отправляет на сервер сообщения, которые сервер должен вернуть другому браузеру. Технически, для реализации связи канал не имеет значения - либо в websokets, либо ajax. pc.onicecandidate = onIceCandidate; function onIceCandidate(event) { if (event.candidate) { sendMessage({type: 'candidate', label: event.candidate.sdpMLineIndex, id: event.candidate.sdpMid, candidate: event.candidate.candidate}); } else { console.log("End of candidates."); } }
Наша функция отправки сообщения через сервер довольно проста, поэтому решено было воспользоватся аяксом как более простым и доступным вариантом для написания небольшого тестового варианта и для реализации серверной части:
function sendMessage(message) { var msgString = JSON.stringify(message); console.log('C->S: ' + msgString); $.ajax({ type: "POST", url: "/chat/tv", dataType: "json", data: { room:room, user_id:user_id, last:last, mess:msgString, is_new:is_new }, success: function(data){ console.log(['data.msg', data.msg]) if( data.last) last = data.last; for (var res in data.msg){ var msg = data.msg[res]; processSignalingMessage(msg[2]); } } }); is_new = 0; function repeat() { timeout = setTimeout(repeat, 5000); sendMessage(); } if (!timeout) repeat(); }
Если запрос выполнился удачно, то в ответ приходят накопившиеся сообщения от другого браузера:
function processSignalingMessage(message) { // В функции проверяются разные варианты ответов и в зависимости от типа ответа выполняется соответствующее действие. // в основном это вызов одного из методов peerСonnection var msg = JSON.parse(message); if (msg.type === 'offer') { if (!initiator && !started){ if (!started && localStream ) { createPeerConnection(); pc.addStream(localStream); started = true; if (initiator) pc.createOffer(setLocalAndSendMessage, null, {"optional": [], "mandatory": {"MozDontOfferDataChannel": true}}); } pc.setRemoteDescription(new RTCSessionDescription(msg)); pc.createAnswer(setLocalAndSendMessage, null, sdpConstraints); } else if (msg.type === 'answer' && started) { pc.setRemoteDescription(new RTCSessionDescription(msg)); } else if (msg.type === 'candidate' && started) { var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label, candidate:msg.candidate}); pc.addIceCandidate(candidate); } else if (msg.type === 'bye' && started) { pc.close(); } } function setLocalAndSendMessage(sessionDescription) { // функция preferOpus устанавливает аудиокодек. sessionDescription.sdp = preferOpus(sessionDescription.sdp); pc.setLocalDescription(sessionDescription); sendMessage(sessionDescription); }
Вообщем то это практически и все, теперь остается присвоить видео поток элементу <video>
pc.onaddstream = onRemoteStreamAdded; function onRemoteStreamAdded(event) { remoteVideo.src = window.URL.createObjectURL(event.stream); remoteStream = event.stream; }
Серверная часть
Наша серверная часть должна быть довольно простой, сервер должен координировать браузеры перед тем, как они смогут связаться напрямую.
И еще нюанс, параметр var initiator = {{ initiator }} определяет, какой из браузеров будет устанавливать соединение, а какой ждет.
То есть у одного он должен быть 0 соответственно у другого 1.
Серверная часть довольно простая, на GET запрос мы создаем комнату в базе передаем её id в шаблон, если её нет в базе создаем новую.
def chat(room): doc = db.chat.find_one({'_id':room}) initiator = 1 if not doc: initiator = 0 doc = {'_id':room, 'mess': []} db.chat.save(doc) return templ('rtc.tpl', initiator = initiator, room=room)
На POST запрос мы принимаем данные от клиента и если клиент передал не пустое сообщение то заносим его содержание в комнату, затем в форе проверяем что сообщения полученые именно «от браузера визави в чате» и они новые тогда возвращаем их своему браузеру.
def chat_post(): lst = 0.0; msg = [] room = get_post('room') user_id= get_post('user_id') last= float(get_post('last', 0)) mess= get_post('mess') doc = db.chat.find_one({'_id':room}) if mess: doc['mess'].append((time.time(), mess, user_id)) db.chat.save(doc) for i_time, i_msg, i_user in doc['mess']: if i_user != user_id and i_time > last: lst = i_time msg.append((i_time, i_user, i_msg)) if not lst: lst = last return json.dumps({'result': 'ok', 'last': lst, 'msg': msg})
На этом описание северной части можно закончить.
Источники:
Справка по webrtc на html5rocks.com
Официальный сайт webrtc
Заранее приношу извинения за найденные грамматические ошибки :).
ссылка на оригинал статьи http://habrahabr.ru/post/171477/
Добавить комментарий