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

в 18:44, , рубрики: html, html5, javascript, jquery, python, WebRTC, Веб-разработка, метки: , , ,

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 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js