Webrtc, Peer Connection — создание полноценного видео чата в браузере

от автора

image

Введение

Webrtc на хабре уже неоднократно упоминался, хотелось бы рассказать немного про техническую часть реализации и осветить создание небольшого видео чата. Хочу сразу оговорится, что реализация webrtc постоянно меняется, в том числе названия функций api, их параметры.
Всем, кому просто хотелось бы посмотреть сразу как это все работает, сюда: apprtc.appspot.com демка от гугла все что нужно — это перейти по ссылке и послать её еще кому-нибудь уже с номером комнаты. В конце нужно поменять цифры если окажется что комната переполнена. Кому интересно как это все работает добро пожаловать под кат

Также довольно криво работает под разными браузерами. Например, так и не получилось на сегодняшний день нормально связать cromium и chrome, а также мобильный chrome и декстопный, хотя возможно уже завтра ситуация резко изменится.

Общая часть

Само API webrtc состоит из трех частей:

  1. getUserMedia (MediaStream), если упрощено, то это захват видео потока в браузере, например просто посмотреть на самого себя ;).
    На хабре есть хорошая статья.
  2. RTCPeerConnection используется для связи между браузерами напрямую. Собственно, об RTCPeerConnection речь в основном и пойдет дальше.
  3. 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/