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

  • Tutorial

Введение


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


Общая часть


Само 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() передавались немного другие параметры.

Инициализация и запуск RTCPeerConnection



function initialize() {
	//получение элементов страницы для последующей вставки или удаления видео потока
	localVideo = document.getElementById("localVideo");
	miniVideo = document.getElementById("miniVideo");
	remoteVideo = document.getElementById("remoteVideo");
        //получение потока локального видео назначение его нужному элементу на странице и если все удачно то вызов PeerConnection
	getUserMedia(
		{'audio':true, 'video':{"mandatory": {}, "optional": []}}, 
		function(localVideo, stream){
			// Вызывает создание PeerConnection.
			localVideo.src = window.URL.createObjectURL(stream);
			if (initiator) maybeStart();
		}, 
		function(error){console.log("Failed to get access to local media. Error code was " + error.code);}
	);
       if (initiator) maybeStart();
	sendMessage();
}


Обмен сообщениями


На этом этапе браузеры обмениваются разными сообщениями чтоб узнать как связаться друг с другом. В сообщениях типа 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;
}

setTimeout(initialize, 1);


Серверная часть


Наша серверная часть должна быть довольно простой, сервер должен координировать браузеры перед тем, как они смогут связаться напрямую.
И еще нюанс, параметр 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

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 33

    +1
    Хорошая штука. Жаль, что ни о какой стабильности и поддержки речи пока не идет. Но, тем не менее, радует, что в будущем проглядывается счастье.

    Раньше был опыт с пиринговым видео… Столько велосипедов было и глюков. Использовали только Adobe Stratus.
      0
      Ну судя по всему ждать осталось не так долго, опера перешла на webkit. Mozilla старается в реализации webrtc. Думаю что постепенно google glass и другие будут видео с камеры транслировать именно через webrtc.
        +1
        А смысл от google glass — в чате надо видеть собеседника? Или вы имели ввиду видеоблог на этой технологии?
          0
          Можно же в зеркало смотреть ) Гугл дакфэйс, так сказать. А кроме шуток, не вижу особого смысла на собеседника смотреть, если только не виделись давно. Можно нормально собеседнику показывать какое-либо место и т.п.
            +1
            Да, я имел виду видео блог или какой нибудь интерактивный дневник. Думаю что общение с бюрократией в данном девайсе тоже будет иметь успех.
        0
        Технология перспективная, но больше всего интересует поддержка браузерами. Пару лет назад была печаль, и с тех пор особых изменений у них на сайте не видел. Пока я так понимаю технология доступна в стабильной версии хрома, и сборке Firefox.
          0
          Пока что более менее всё нормально только в хроме, но даже тут — почему-то с одной стороны звук не поулчилось передать. Кроме того, захват звука идёт не с микрофона, а с общего канала — возможно в будущем это всё будет как-то регулироваться. В хромиуме (вроде ж тот же движок, цифра в цифру версия) — почему-то RTCPeerConnection не пытается состыковаться через локальную сеть — только через полученный внешний IP.
            0
            Возможно нужно поднять локальный STUN -сервер, т.к. по-умолчанию используют гугловский
              0
              Возможно. Хоть и не понятна причина дискриминации — демо вроде как предназначено для разных браузеров. Конечно, могу предположить, что в хроме используется какая-то проприетарная доработка специально для стыковки со своим STUN-сервером, но как-то это странно было бы.
          0
          И еще. А можно где взять ваш код целиком, чтобы в локальной сети поиграться.
          0
          Плюс остается узнать как принять видео-поток на сервере
            +1
            Я думаю, можно рассмотреть вариант задействовать QtWebKit
              +1
              Отличная идея! Спасибо за совет
            –1
            Уважаемый, а Вас не учили оставлять ссылки на первоисточники (исходников в том числе)?
              –2
              Разве автор не дал ссылку?

              apprtc.appspot.com демка от гугла
                –1
                Прочитайте внимательно мой комментарий, особенно то что в скобках
                  0
                  Вы никогда не пробовали в браузере нажать Ctrl-U?
                    0
                    Глубоко уважаемый, Александр. Не размещение ссылок на первоисточник (даже исходного кода) это не только моветон, но и избавляет автора от ответов на подобные комментарии. А на счет Ctr+U сделайте мне эту комбинацию на серверном питоновом скрипте этой демки
                    P.S. Всегда умиляют такие наивные люди как Вы.
                      0
                      Глубоко уважаемый, Иван. Читаете ли Вы статьи, прежде чем их комментировать? Например, обратите внимание, что автор написал серверную часть по-своему и к данным Вами исходникам она не имеет ни малейшего отношения. А ответы на вопросы о собственных исходниках (а также о мотивации автора давать или не давать эти исходники), я таки оставлю отвечать ему самому.
                        0
                        «написал серверную часть по-своему»
                        Не используя исходники от google?
                          0
                          Ну, полагаю, приведённый в статье фрагмент можно сравнить с кодом по Вашей ссылке — наверно, начиная со 173 строки
                            0
                            Читаем что такое первоисточник, думаем, еще раз думаем, перечитываем мой комментарий, синтезируем полученные знания и не пытаемся мне что-то доказать.
                              0
                              Встречное предложение подумать над ответами на Ваши комментарии. А заодно, поразмыслить над таким неординарным теоретическим вопросом, как что является предметом рассмотрения данной статьи — объект реального мира в виде демки с прилагающимися к нему исходниками, на что ссылка дана, и авторской реализацией серверной части, либо некий блок кода, являющийся, вероятно, исходником серверной части, на котором на самом деле работает демка, на который ссылаетесь Вы, но автором статьи не рассмотренный?
                              И в целом — предлагаю данный душещипательный философский вопрос вынести в личку, поскольку здесь это обсуждение скатывается в безбожный оффтопик. Имхо, здесь в теме хватило бы просто сослаться на то, что исходники демки существуют в открытом виде, о чём автор, вероятно, не знал.
                        0
                        А на счет Ctr+U сделайте мне эту комбинацию на серверном питоновом скрипте этой демки
                        На питоновский код если честно не смотрел, там заточено для app engine, используется база гугла и тд. В том коде что я привел используется ajax, mongodb, две простеньких функции. Конечно у меня нет проверок исключительных ситуаций ошибок и всего остального, но это ж не продакшен, это простой пример.
                          0
                          Вы отрицаете, что использовали код предоставленный google?
                            0
                            Честно говоря я вас немного не понимаю. Вам приведенный мной python код кажется настолько сложным что его невозможно написать самому? Если вы усматриваете откровенный копипаст то приведите пожалуйста пример. Или имелась в виду клиентская часть?
                              0
                              Вы отрицаете, что использовали код предоставленный google?
                                0
                                man diff
                                  –1
                                  Ваш код в студию!
                                    0
                                    Вы настойчивы
                                    Набросал пример для bottle вначале необходимо выполнить:
                                    pip install jinja2
                                    pip install bottle
                                    

                                    также поставить mongodb

                                    создать необходимый шаблон, и запустить питоновский код

                                    
                                    from bottle import run, route, request, jinja2_template as templ
                                    
                                    def connect():
                                    	mongo = Connection('localhost', 27017)
                                    	db = mongo['db']
                                    	db.authenticate('user', 'pass')
                                    	return db
                                    
                                    db = connect()
                                    
                                    @route('/chat/<room>')
                                    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)
                                    
                                    @route('/chat', method='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})
                                    
                                    def get_post(name, default = None):
                                    	return request.POST[name] if name in request.POST else default
                                    
                                    run(host='localhost', port=8080)
                                    
                –1
                  0
                  а для чего функция maybeStart? Я не нашел её в js коде, какое её применение?
                  Так же прошу исходники rtc.tpl
                  P.S. пытался реализовать на node.js и websocket-ах, но как-то не вышло…

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое