Введение
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 0rn"}
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 0rn"}
// 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
Заранее приношу извинения за найденные грамматические ошибки :).
Автор: Alex10