Привет, %USERNAME%! Спустя пару недель с последней записки у меня таки проснулась совесть и я решил написать о своей текущей работе в области программирования. А расскажу я о перспективной технологии Web RealTime Communication, позволяющей браузерам обмениваться медиа-контентом по сетям p2p.
Интересующихся прошу под кат.
WebRTC.org встречает нас минималистичным оформлением, но морем информации. Я не буду рассказывать тебе об истории создания протокола и сопутствующего ему «обвеса», не буду говорить о том, как она крута и так далее, и тому подобное.
Я хочу рассказать о куда более интересной вещи: как это все заставить работать.
Итак, приступим!
Let’s the Magic begins!!
Как уже ясно из названия, использовать эту штуку мы будем для общения в реальном времени. Общения через веб- камеры. Да, без флеша. Да, без плагинов. Да, по пиринговым сетям. Удивлен? Вооот, и я тоже был удивлен, когда в ходе выполнения исследовательской работы наткнулся на это чудо.
Как мы видим, поддерживается технология и все нужные кодеки только в, как же это очевидно, Google Chrome. Ну, и в Mozilla Aurora, где оно работает ну совсем нестабильно. Это значит, что в близжайшие полгода клиентам придется пользоваться ради этой разработки хромом. На самом деле, не такая уж и потеря.
Но что- то я ушел от темы. Как я уже сказал выше, наткнулся на WebRTC я в ходе выполнения исследовательской работы по теме: «Создание веб- видеочата с использованием современных клиентских и серверных технологий». Первоначально предполагалось использовать флеш, но… Запара с медиасервером для RTMP выправила меня на путь истинный.
Отступая от темы, сейчас уже готов базовый функционал видеочата ViPeer: видеостриминг, текстовый чат, авторизация через соц. сети, личный кабинет( этого и предыдущего нет в stable- ветке) и организация многопользовательских конференций( за что жиды из Microsoft берут деньги в Skype). Посмотреть можно по адресу vipeer.ru.
Меньше слов, больше дела
И опять я заговорился. Для исследовательской работы изначально планировалось использование node.js, а потому библиотеки для WebRTC разыскивались именно для него. И они таки нашлись. Конечно же, на гитхабе.
Либа состоит из сервер- сайда( Node.js) WebRTC.io и клиент- сайда WebRTC.io-client. Последнее включается в script src. Эти две библиотеки позволяют за короткий срок получить вполне себе работоспособный видеочат с разделением клиентов по отдельным конференциям со встроенным текстовым чатом. По- большому счету, это делается за пару минут с помощью, опять же уже готового, webrtc.io-demo. Именно demo стал основой для ViPeer. По мере освоения мною nodejs, он был тотально переделан и оброс множеством полезных функций, вроде отображения списка пользователей в онлайне, авторизации etc.
Поскольку я не работал с чистым API WebRTC, а использовал библиотеку WebRTC.io, то и плясать буду от нее. И тебе рекомендую сделать так же: разобраться в либах всегда успеешь, а вот забить себе голову кучей полезностей получится не сразу, даже если оочень стараться.
Итак. Качаем все нужное на свой сервер и «взлетаем!» ©
Для начала, разберемся с сервером. Тот, что представлен в демо совсем не подходит для конфигурации моего VPS, а потому был переделан до такого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//инициализация webrtc.io на 443 порту. Необходимо для обхода клиентских файрволлов. var webRTC = require('webrtc.io').listen(443); //функция, которая рулит сообщениями в чате. Реагирует на клиентский запрос chat_msg webRTC.rtc.on('chat_msg', function(data, socket) { //Берем список клиентов в комнате, откуда пришло сообщение var roomList = webRTC.rtc.rooms[data.room] || []; //и отсылаем каждому клиенту, кроме себя самого for (var i = 0; i < roomList.length; i++) { var socketId = roomList[i]; if (socketId !== socket.id) { var soc = webRTC.rtc.getSocket(socketId); //Если клиент еще не отключился if (soc) { soc.send(JSON.stringify({ "eventName": "receive_chat_msg", "data": { "messages": data.messages, "nickname": data.nickname } }), function(error) { if (error) { console.log(error); } }); } } } }); |
Сие отличается от demo только наличием дополнительной строки nickname, ее значение, я думаю, очевидно.
Ии…. Все! Это весь сервер-сайд! Теперь мы можем запустить его из командной строки линупсов( или окошек) строкой node server.js. И любоваться на пустующую консоль..
Теперь займемся клиент- сайдом.
Что такое клиент? Клиент, в самом простом понимании- страничка, заходя куда мы получим алерт об использовании камеры и стримы от других пользователей. На ViPeer есть еще и красивая формочка на Twitter Bootstrap с предложением ввести никнейм и название конференции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Войти в конференцию | ViPeer Alpha v0.1</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> <!-- Le styles --> <link href="/css/bootstrap.css" rel="stylesheet"> <link href="/css/main.css" rel="stylesheet"> <link href="/css/bootstrap-responsive.css" rel="stylesheet"> <!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> </head> <body onload="browser_check(); log_by_link();"> <div class="container"> <form class="form-signin" action="/" method="POST" onSubmit="return auth_saveData(this);"> <h2 class="form-signin-heading">Войдите в конференцию</h2> <input type="text" name="conf" class="input-block-level" placeholder="Название конференции" required> <input type="text" name="nickname" class="input-block-level" placeholder="Ваш никнейм" required> <input type="hidden" name="passw" class="input-block-level" value="some_pass"> <button class="btn btn-large btn-primary" type="submit">Подключиться</button> </form> </div> <!-- /container --> <script src="/js/vipeer.js"></script> <script src="/js/jquery.js"></script> <script src="/js/bootstrap.js"></script> </body> </html> |
И vipeer.js, обслуживающий алерты, авторизацию, быстрый вход( при переходе по ссылке вида vipeer.ru/#conf_name поле «имя конференции» уже будет содержать значение хэша):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
var WS_SERVER = "ws://*****"; var storage= localStorage; function browser_check() { var PeerCon = window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; if (PeerCon === undefined) { window.location = '/support.html'; } } function auth_saveData(data) { var regexp = /\w/; if (!regexp.test(data.conf.value) && !document.querySelector(".alert")) { create_alert('alert-error', 'Имя конференции может состоять только из латиницы и цифр!'); } else { var form = document.querySelector("form"); var ws = new WebSocket(WS_SERVER); ws.onopen = function() { ws.send(JSON.stringify({ "eventName": "auth_user", "data": { "nickname": form.nickname.value, "passw": form.passw.value } })); document.querySelector('form > .btn.btn-large.btn-primary').innerHTML = 'Выполняется запрос <div class="ball"></div><div class="ball1">'; }; ws.onmessage = function(evt) { var received_msg = JSON.parse(evt.data); if (received_msg.data.answer === 'true') { storage.conf=document.querySelector('form > input[name=conf]').value; storage.nickname=document.querySelector('form > input[name=nickname]').value; storage.passw=document.querySelector('form > input[name=passw]').value; window.location.hash=''; window.location.pathname='vchat.html'; } else { create_alert('alert-error', 'Ошибка авторизации! Неверный пароль!'); } document.querySelector('form > .btn.btn-large.btn-primary').innerHTML = 'Подключиться'; }; ws.onclose = function() { // websocket is closed. console.log("Соединение закрыто"); }; } return false; } function create_alert(type, text) { var form = document.querySelector("form"); var alert = document.createElement('div'); alert.setAttribute('class', 'alert ' + type); alert.innerHTML = '<button type="button" class="close" data-dismiss="alert">×</button> ' + text; var top = document.querySelector('form > h2'); var first = document.querySelector('form > div:first-child'); if (first) { form.removeChild(first); form.insertBefore(alert, top); } else { form.insertBefore(alert, top); } } function log_by_link() { var hash = window.location.hash.slice(1); var form = document.querySelector('form'); if (hash != '') { form.conf.value = hash; form.nickname.focus(); } else { form.conf.focus(); } } |
Хоть фактически авторизации еще нет, но нужный функционал уже реализован функцией auth_saveData. Просто от делать нефиг :))
В случае, если ты введешь все верно, то скрипт редиректит тебя на vipeer.ru/vchat.html
Он, что очевидно, тоже сверстан на Twitter bootstrap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Веб- видеочат</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content=""> <!-- Le styles --> <link href="/css/chat.css" rel="stylesheet"> <link href="/css/bootstrap.css" rel="stylesheet"> <link href="/css/bootstrap-responsive.css" rel="stylesheet"> <link rel="stylesheet" href="http://code.jquery.com/ui/1.10.0/themes/base/jquery-ui.css" /> <style type="text/css"> body { padding-top: 20px; padding-bottom: 40px; } </style> <!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> </head> <body onload="init();"> <div class="container-fluid"> <div class="row-fluid"> <div class="span9"> <div class="videogroup"> <div class="videos" id="videos"> <!--<canvas class="fakevideo"></canvas>--> <video id="you" class="flip" autoplay></video> </div> <script src="/js/vipeer_canvas.js"></script> <p class="margin-pure"> <button id="rounded" class="btn btn-danger" onclick="goAway();"><i class="icon-off icon-white"></i> Выйти из разговора</button> <button name="fullscreen" id="rounded" class="btn btn-success leftpad"><i class="icon-fullscreen icon-white"></i> На весь экран</button> </p> </div> </div><!--/.fluid-container--> <div class="span3"> <div class="well"> <ul class="nav nav-list"> <li class="nav-header">Ссылка на конференцию</li> <li><input type="text" name="room_url" rel="tooltip" data-placement="top" data-original-title="Нажмите на адрес и скопируйте ссылку" onClick="select(this);"></li> </ul> </div><!--/.well --> <div class="well sidebar-nav"> <ul class="nav nav-list"> <li class="nav-header">Сейчас в чате</li> <ul class="nav nav-list" id="online" style="background:#ffffff;border-radius:5px;"> </ul> </ul> </div><!--/.well --> <div class="well "> <ul class="nav nav-list"> <li class="nav-header">Чат</li> <li><div class="messagebox"></div></li> <li> <div class="control-group"> <label class="control-label" for="inputIcon"></label> <div class="controls"> <div class="input-append"> <input style="width:83%" name="chatinput" type="text"> <span class="add-on" onclick="sendMessage();"><i class="icon-pencil"></i></span> </div> </div> </div> </li> </ul> </div><!--/.well --> <hr> <footer> <p>© <a href="http://techstories.ru/">Губарев Владимир</a> 2013</p> </footer> </div> </div> </div><!--/span--> <!-- Le javascript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="/js/jquery.js"></script> <script src="/js/bootstrap.js"></script> <script src="/js/webrtc.io.js"></script> <script src="/js/vipeer_chat.js"></script> <script src="/js/jquery.ui.js"></script> </body> </html> |
И самое вкусное: vipeer-chat.js. То, без чего все вышесказанное- пустой звук. Этот код, собственно, реализует функционал WebRTC, используя API библиотеки WebRTC.io-client
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
var videos = []; var rooms = [1, 2, 3, 4, 5]; var PeerConnection = window.PeerConnection || window.webkitPeerConnection00 || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; var ls = localStorage; function getNumPerRow() { var len = videos.length; var biggest; // Ensure length is even for better division. if (len % 2 === 1) { len++; } biggest = Math.ceil(Math.sqrt(len)); while (len % biggest !== 0) { biggest++; } return biggest; } function subdivideVideos() { var perRow = getNumPerRow(); var numInRow = 0; for (var i = 0, len = videos.length; i < len; i++) { var video = videos[i]; setWH(video, i); numInRow = (numInRow + 1) % perRow; } } function setWH(video, i) { var perRow = getNumPerRow(); var videogroup = $('.videogroup'); var perColumn = Math.ceil(videos.length / perRow); var width = Math.floor((videogroup.innerWidth() - 45) / perRow); var height = Math.floor((videogroup.innerHeight() - 45 - 190) / perColumn); video.width = width; video.height = height; //video.style.position = "relative"; //video.style.left = (i % perRow) * width + "px"; //video.style.top = Math.floor(i / perRow) * height + "px"; } function cloneVideo(domId, socketId) { var video = document.getElementById(domId); var clone = video.cloneNode(true); clone.id = "remote" + socketId; document.querySelector(".videos").appendChild(clone); videos.push(clone); return clone; } function removeVideo(socketId) { var video = document.getElementById('remote' + socketId); if (video) { videos.splice(videos.indexOf(video), 1); video.parentNode.removeChild(video); } } function addToChat(msg, nickname) { var messages = document.querySelector('.messagebox'); msg = sanitize(msg); if (nickname != ls.nickname) { nickname = '<span style="color:#43b2ff;">' + nickname + '</span>'; } msg = '<span style="padding-left: 15px;"><strong>' + nickname + '</strong>: ' + msg + '</span>'; messages.innerHTML = messages.innerHTML + msg + '<br>'; messages.scrollTop = 10000; } function sanitize(msg) { return msg.replace(/</g, '<'); } function initFullScreen() { var button = document.querySelector("[name=fullscreen]"); button.addEventListener('click', function(event) { var elem = document.querySelector(".videos"); //show full screen if (elem.requestFullscreen) { elem.requestFullscreen(); } else if (elem.mozRequestFullScreen) { elem.mozRequestFullScreen(); } else if (elem.webkitRequestFullscreen) { elem.webkitRequestFullscreen(); } }); } function initChat() { var input = document.querySelector("[name=chatinput]"); input.addEventListener('keydown', function(event) { var key = event.which || event.keyCode; if (key === 13) { sendMessage(input); } }, false); rtc.on('receive_chat_msg', function(data) { addToChat(data.messages, data.nickname); }); } function sendMessage(input) { if (!input) { var input = document.querySelector('[name=chatinput]'); } var room = ls.conf; var nickname = ls.nickname; var regexp = /^\s/; if (input.value !== "" && !regexp.test(input.value)) { rtc._socket.send(JSON.stringify({ "eventName": "chat_msg", "data": { "messages": input.value, "room": room, "nickname": nickname } })); addToChat(input.value, nickname); input.value = ""; } } function add2online(name, socket) { var add = document.createElement('li'); add.innerHTML = '<a href="#"><i class="icon-ok-circle"></i>' + name + '</a>'; add.id = socket; document.querySelector('#online').appendChild(add); } function rem2online(name) { var e = document.getElementById(name); e.parentNode.removeChild(e); } function monitorOnline() { rtc.on('add_to_online', function(data) { add2online(data.nickname, data.new_peer_socket); rtc._socket.send(JSON.stringify({ "eventName": "kk_i_add_you", "data": { "reSocket": data.new_peer_socket, "nickname": ls.nickname } })); }); rtc.on('kk_i_add_you_resend', function(data) { add2online(data.nickname, data.socket); }); } function init() { var room = ls.conf; rtc.connect("ws://***/", room); rtc.on('add remote stream', function(stream, socketId) { var clone = cloneVideo('you', socketId); document.getElementById(clone.id).setAttribute("class", ""); rtc.attachStream(stream, clone.id); $(".videos video").draggable({containment: ".videos", scroll: false}); subdivideVideos(); }); rtc.on('disconnect stream', function(data) { console.log('remove ' + data); removeVideo(data); rem2online(data); subdivideVideos(); }); if (PeerConnection) { rtc.createStream({"video": true, "audio": true}, function(stream) { document.getElementById('you').src = URL.createObjectURL(stream); videos.push(document.getElementById('you')); //rtc.attachStream(stream, 'you'); $(".videos video").draggable({containment: ".videos", scroll: false}); subdivideVideos(); setTimeout(function() { rtc._socket.send(JSON.stringify({ "eventName": "hey_guys_add_me", "data": { "room": ls.conf, "nickname": ls.nickname } })); }, 1500); add2online(ls.nickname, 'you'); }); } else { alert('Your browser is not supported or you have to turn on flags. In chrome you go to chrome://flags and turn on Enable PeerConnection remember to restart chrome'); } initFullScreen(); roomlink(); initChat(); monitorOnline(); document.querySelector('.videogroup').style.height = window.innerHeight - 85 + 'px'; document.querySelector('.videos').style.height = window.innerHeight - 145 + 'px'; document.querySelector('.margin-pure').style.top = window.innerHeight - 105 + 'px'; } function roomlink() { var wl = window.location; document.querySelector('[name=room_url]').value = wl.protocol + '//' + wl.hostname + '/#' + ls.conf; } window.onresize = function(event) { document.querySelector('.videogroup').style.height = window.innerHeight - 85 + 'px'; document.querySelector('.videos').style.height = window.innerHeight - 145 + 'px'; document.querySelector('.margin-pure').style.top = window.innerHeight - 105 + 'px'; subdivideVideos(); }; document.querySelector('.videogroup').onresize = function() { subdivideVideos(); } $('[name=room_url]').tooltip('hide'); function goAway() { window.location = '/'; delete(ls.conf); } |
Во многом, код стабильной ветки похож на demo chat.js. Ненуачо? Я подумал и решил, что зачем менять и переписывать и без того хорошую штуку? Вот и я о том же.
Шутка ли, но это все. Да, это все, что нужно, что бы десятки пользователей могли одновременно общаться друг с другом по видеосвязи с щикарным качеством передачи, приличной скоростью и совсем не напрягая сервер, где стоит node.js. Кайф, да и только!
Я понимаю, что записка получилась скомканной и вообще не очень, но я обязательно исправлюсь.
Спасибо за внимание, %USERNAME%, чистого кода тебе и адекватных заказчиков!