WebRTC — Обзор
Сеть больше не является чужой для общения в режиме реального времени, так как WebRTC (Web Real-Time Communication) вступает в игру. Хотя он был выпущен в мае 2011 года, он все еще развивается, и его стандарты меняются. Набор протоколов стандартизирован Рабочей группой WEB-браузеров в режиме реального времени по адресу http://tools.ietf.org/wg/rtcweb/ IETF (Internet Engineering Task Force), а новые наборы API стандартизированы Рабочая группа веб-коммуникаций в реальном времени на сайте http://www.w3.org/2011/04/webrtc/ W3C (World Wide Web Consortium) . С появлением WebRTC современные веб-приложения могут легко передавать аудио и видео контент миллионам людей.
Основная схема
WebRTC позволяет быстро и легко устанавливать одноранговые соединения с другими веб-браузерами. Чтобы создать такое приложение с нуля, вам понадобится множество фреймворков и библиотек, занимающихся типичными проблемами, такими как потеря данных, разрыв соединения и обход NAT. С WebRTC все это встроено в браузер «из коробки». Эта технология не требует никаких плагинов или стороннего программного обеспечения. Он имеет открытый исходный код, и его исходный код находится в свободном доступе по адресу http://www.webrtc.org/.
API WebRTC включает захват медиаданных, кодирование и декодирование аудио и видео, транспортный уровень и управление сеансами.
Захват медиа
Первый шаг — получить доступ к камере и микрофону устройства пользователя. Мы определяем тип доступных устройств, получаем разрешение пользователя на доступ к этим устройствам и управляем потоком.
Кодирование и декодирование аудио и видео
Отправка потока аудио и видео данных через Интернет — непростая задача. Это где кодирование и декодирование используются. Это процесс разделения видеокадров и звуковых волн на более мелкие фрагменты и сжатия их. Этот алгоритм называется кодеком . Существует огромное количество разных кодеков, которые поддерживаются разными компаниями с разными бизнес-целями. Есть также много кодеков внутри WebRTC, таких как H.264, iSAC, Opus и VP8. Когда два браузера соединяются вместе, они выбирают наиболее оптимальный поддерживаемый кодек между двумя пользователями. К счастью, WebRTC выполняет большую часть кодирования за кулисами.
Транспортный уровень
Транспортный уровень управляет порядком пакетов, занимается потерей пакетов и подключением к другим пользователям. И снова API WebRTC предоставляет нам легкий доступ к событиям, которые сообщают нам, когда возникают проблемы с соединением.
Управление сессиями
Управление сессиями занимается управлением, открытием и организацией соединений. Это обычно называется сигнализацией . Если вы передаете аудио и видео потоки пользователю, также имеет смысл передавать сопутствующие данные. Это делается с помощью API RTCDataChannel .
Инженеры из таких компаний, как Google, Mozilla, Opera и другие, проделали огромную работу, чтобы перенести этот опыт в реальном времени в Интернет.
Совместимость браузера
Стандарты WebRTC являются одними из самых быстро развивающихся в сети, поэтому это не означает, что каждый браузер поддерживает все одинаковые функции одновременно. Чтобы проверить, поддерживает ли ваш браузер WebRTC или нет, вы можете посетить http://caniuse.com/#feat=rtcpeerconnection. На протяжении всех уроков я рекомендую использовать Chrome для всех примеров.
Попробовать WebRTC
Давайте начнем использовать WebRTC прямо сейчас. Перейдите в своем браузере на демонстрационный сайт по адресу https://apprtc.appspot.com/.
Нажмите кнопку «ПРИСОЕДИНИТЬСЯ». Вы должны увидеть выпадающее уведомление.
Нажмите кнопку «Разрешить», чтобы начать потоковую передачу видео и аудио на веб-страницу. Вы должны увидеть видеопоток о себе.
Теперь откройте URL, на котором вы находитесь, в новой вкладке браузера и нажмите «ПРИСОЕДИНИТЬСЯ». Вы должны увидеть два видеопотока — один от вашего первого клиента и другой от второго.
Теперь вы должны понять, почему WebRTC является мощным инструментом.
Случаи применения
Сеть в реальном времени открывает доступ к целому ряду новых приложений, включая текстовый чат, обмен экранами и файлами, игры, видеочат и многое другое. Помимо общения вы можете использовать WebRTC для других целей, таких как —
- маркетинг в реальном времени
- реклама в реальном времени
- связь с бэк-офисом (CRM, ERP, SCM, FFM)
- Управление персоналом
- социальная сеть
- службы знакомств
- онлайн медицинские консультации
- финансовые услуги
- наблюдение
- многопользовательские игры
- прямая трансляция
- Электронное обучение
Резюме
Теперь у вас должно быть четкое понимание термина WebRTC. Вы также должны иметь представление о том, какие типы приложений могут быть созданы с помощью WebRTC, поскольку вы уже пробовали это в своем браузере. Подводя итог, WebRTC — довольно полезная технология.
WebRTC — Архитектура
Общая архитектура WebRTC имеет большой уровень сложности.
Здесь вы можете найти три разных слоя —
-
API для веб-разработчиков — этот уровень содержит все необходимые API-интерфейсы веб-разработчика, включая объекты RTCPeerConnection, RTCDataChannel и MediaStrean.
-
API для производителей браузеров
-
Переопределяемый API, который могут подключить создатели браузера.
API для веб-разработчиков — этот уровень содержит все необходимые API-интерфейсы веб-разработчика, включая объекты RTCPeerConnection, RTCDataChannel и MediaStrean.
API для производителей браузеров
Переопределяемый API, который могут подключить создатели браузера.
Транспортные компоненты позволяют устанавливать соединения в различных типах сетей, в то время как механизмы обработки голоса и видео являются средами, отвечающими за передачу аудио- и видеопотоков со звуковой карты и камеры в сеть. Для веб-разработчиков наиболее важной частью является API WebRTC.
Если мы посмотрим на архитектуру WebRTC со стороны клиент-сервер, то увидим, что одна из наиболее часто используемых моделей основана на трапеции SIP (протокол инициации сеанса).
В этой модели на обоих устройствах запущено веб-приложение с разных серверов. Объект RTCPeerConnection настраивает потоки так, чтобы они могли соединяться друг с другом, одноранговыми. Эта сигнализация осуществляется через HTTP или WebSockets.
Но наиболее часто используемая модель — треугольник
В этой модели оба устройства используют одно и то же веб-приложение. Это дает веб-разработчику больше гибкости при управлении пользовательскими подключениями.
API WebRTC
Он состоит из нескольких основных объектов JavaScript —
- RTCPeerConnection
- MediaStream
- RTCDataChannel
Объект RTCPeerConnection
Этот объект является основной точкой входа в API WebRTC. Это помогает нам подключаться к одноранговым узлам, инициализировать соединения и прикреплять медиапотоки. Он также управляет соединением UDP с другим пользователем.
Основная задача объекта RTCPeerConnection — настроить и создать одноранговое соединение. Мы можем легко перехватить ключевые точки соединения, потому что этот объект запускает множество событий, когда они появляются. Эти события дают вам доступ к конфигурации нашего соединения —
RTCPeerConnection — это простой объект javascript, который вы можете просто создать следующим образом:
[code] var conn = new RTCPeerConnection(conf); conn.onaddstream = function(stream) { // use stream here }; [/code]
Объект RTCPeerConnection принимает параметр conf , который мы рассмотрим позже в этих руководствах. Событие onaddstream наступает, когда удаленный пользователь добавляет видео- или аудиопоток в одноранговое соединение.
MediaStream API
Современные браузеры предоставляют разработчикам доступ к API getUserMedia , также известному как MediaStream API. Есть три ключевых момента функциональности —
-
Это дает разработчику доступ к объекту потока, который представляет видео и аудио потоки.
-
Он управляет выбором пользовательских устройств ввода в случае, если у пользователя есть несколько камер или микрофонов на его устройстве.
-
Это обеспечивает уровень безопасности, запрашивающий пользователя все время, когда он хочет получить поток
Это дает разработчику доступ к объекту потока, который представляет видео и аудио потоки.
Он управляет выбором пользовательских устройств ввода в случае, если у пользователя есть несколько камер или микрофонов на его устройстве.
Это обеспечивает уровень безопасности, запрашивающий пользователя все время, когда он хочет получить поток
Чтобы протестировать этот API, давайте создадим простую HTML-страницу. Он покажет один элемент <video>, спросит у пользователя разрешения на использование камеры и покажет прямую трансляцию с камеры на странице. Создайте файл index.html и добавьте —
[code] <html> <head> <meta charset = "utf-8"> </head> <body> <video autoplay></video> <script src = "client.js"></script> </body> </html> [/code]
Затем добавьте файл client.js —
[code] //checks if the browser supports WebRTC function hasUserMedia() { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; return !!navigator.getUserMedia; } if (hasUserMedia()) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; //get both video and audio streams from user's camera navigator.getUserMedia({ video: true, audio: true }, function (stream) { var video = document.querySelector('video'); //insert stream into the video tag video.src = window.URL.createObjectURL(stream); }, function (err) {}); }else { alert("Error. WebRTC is not supported!"); } [/code]
Теперь откройте index.html, и вы увидите видеопоток, отображающий ваше лицо.
Но будьте осторожны, потому что WebRTC работает только на стороне сервера. Если вы просто откроете эту страницу в браузере, она не будет работать. Вам нужно разместить эти файлы на серверах Apache или Node или на том, который вы предпочитаете.
Объект RTCDataChannel
Помимо отправки потоков мультимедиа между партнерами, вы также можете отправлять дополнительные данные с помощью API DataChannel . Этот API так же прост, как MediaStream API. Основная задача — создать канал из существующего объекта RTCPeerConnection —
[code] var peerConn = new RTCPeerConnection(); //establishing peer connection //... //end of establishing peer connection var dataChannel = peerConnection.createDataChannel("myChannel", dataChannelOptions); // here we can start sending direct messages to another peer [/code]
Это все, что вам нужно, всего две строчки кода. Все остальное делается на внутреннем слое браузера. Вы можете создать канал в любом одноранговом соединении, пока объект RTCPeerConnection не будет закрыт.
Резюме
Теперь вы должны четко понимать архитектуру WebRTC. Мы также рассмотрели API-интерфейсы MediaStream, RTCPeerConnection и RTCDataChannel. API WebRTC является движущейся целью, поэтому всегда следите за последними спецификациями.
WebRTC — Окружающая среда
Прежде чем мы начнем создавать наши приложения WebRTC, мы должны установить нашу среду кодирования. Прежде всего, у вас должен быть текстовый редактор или IDE, где вы можете редактировать HTML и Javascript. Есть вероятность, что вы уже выбрали предпочтительный вариант, когда читаете этот учебник. Что касается меня, я использую WebStorm IDE. Вы можете загрузить его пробную версию по адресу https://www.jetbrains.com/webstorm/ . Я также использую Linux Mint в качестве своей операционной системы.
Другое требование для обычных приложений WebRTC — наличие сервера для размещения файлов HTML и Javascript. Код не будет работать, если дважды щелкнуть файлы, поскольку браузеру не разрешено подключаться к камерам и микрофонам, если файлы не обслуживаются реальным сервером. Это сделано, очевидно, из-за проблем безопасности.
Существует множество различных веб-серверов, но в этом руководстве мы собираемся использовать Node.js с node-static —
-
Посетите https://nodejs.org/en/ и загрузите последнюю версию Node.js.
-
Распакуйте его в каталог / usr / local / nodejs.
-
Откройте файл /home/YOUR_USERNAME/.profile и добавьте в конец следующую строку — экспорт PATH = $ PATH: / usr / local / nodejs / bin
-
Вы можете перезагрузить компьютер или запустить источник /home/YOUR_USERNAME/.profile
-
Теперь команда узла должна быть доступна из командной строки. Команда npm также доступна. NMP — менеджер пакетов для Node.js. Вы можете узнать больше на https://www.npmjs.com/ .
-
Откройте терминал и запустите sudo npm install -g node-static . Это установит статический веб-сервер для Node.js.
-
Теперь перейдите к любому каталогу, содержащему файлы HTML, и запустите статическую команду внутри каталога, чтобы запустить веб-сервер.
-
Вы можете перейти на http: // localhost: 8080, чтобы увидеть ваши файлы.
Посетите https://nodejs.org/en/ и загрузите последнюю версию Node.js.
Распакуйте его в каталог / usr / local / nodejs.
Откройте файл /home/YOUR_USERNAME/.profile и добавьте в конец следующую строку — экспорт PATH = $ PATH: / usr / local / nodejs / bin
Вы можете перезагрузить компьютер или запустить источник /home/YOUR_USERNAME/.profile
Теперь команда узла должна быть доступна из командной строки. Команда npm также доступна. NMP — менеджер пакетов для Node.js. Вы можете узнать больше на https://www.npmjs.com/ .
Откройте терминал и запустите sudo npm install -g node-static . Это установит статический веб-сервер для Node.js.
Теперь перейдите к любому каталогу, содержащему файлы HTML, и запустите статическую команду внутри каталога, чтобы запустить веб-сервер.
Вы можете перейти на http: // localhost: 8080, чтобы увидеть ваши файлы.
Есть еще один способ установить nodejs. Просто запустите sudo apt-get install nodejs в окне терминала.
Чтобы проверить вашу установку Node.js, откройте ваш терминал и выполните команду узла . Введите несколько команд, чтобы проверить, как это работает —
Node.js запускает файлы Javascript, а также команды, набранные в терминале. Создайте файл index.js со следующим содержимым —
console.log(“Testing Node.js”);
Затем выполните команду индекса узла . Вы увидите следующее —
При создании нашего сигнального сервера мы будем использовать библиотеку WebSockets для Node.js. Для установки в проге npm установите ws в терминал.
Для тестирования нашего сервера сигнализации мы будем использовать утилиту wscat. Чтобы установить его, запустите npm install -g wscat в окне терминала.
S.No | Протоколы и описание |
---|---|
1 | Протоколы WebRTC
Приложения WebRTC используют UDP (протокол пользовательских дейтаграмм) в качестве транспортного протокола. Большинство веб-приложений сегодня создаются с использованием протокола TCP (Transmission Control Protocol) |
2 | Протокол описания сеанса
SDP является важной частью WebRTC. Это протокол, предназначенный для описания сеансов связи с мультимедиа. |
3 | В поисках маршрута
Чтобы подключиться к другому пользователю, вы должны найти четкий путь вокруг своей сети и сети другого пользователя. Но есть вероятность, что сеть, которую вы используете, имеет несколько уровней контроля доступа, чтобы избежать проблем безопасности. |
4 | Протокол управления потоком передачи
Благодаря одноранговому соединению у нас есть возможность быстро отправлять видео и аудио данные. Протокол SCTP используется сегодня для отправки данных больших двоичных объектов поверх нашего текущего настроенного однорангового соединения при использовании объекта RTCDataChannel. |
Приложения WebRTC используют UDP (протокол пользовательских дейтаграмм) в качестве транспортного протокола. Большинство веб-приложений сегодня создаются с использованием протокола TCP (Transmission Control Protocol)
SDP является важной частью WebRTC. Это протокол, предназначенный для описания сеансов связи с мультимедиа.
Чтобы подключиться к другому пользователю, вы должны найти четкий путь вокруг своей сети и сети другого пользователя. Но есть вероятность, что сеть, которую вы используете, имеет несколько уровней контроля доступа, чтобы избежать проблем безопасности.
Благодаря одноранговому соединению у нас есть возможность быстро отправлять видео и аудио данные. Протокол SCTP используется сегодня для отправки данных больших двоичных объектов поверх нашего текущего настроенного однорангового соединения при использовании объекта RTCDataChannel.
Резюме
В этой главе мы рассмотрели несколько технологий, которые обеспечивают одноранговые соединения, такие как UDP, TCP, STUN, TURN, ICE и SCTP. Теперь вы должны иметь представление о том, как работает SDP и его варианты использования.
WebRTC — API MediaStream
MediaStream API был разработан для легкого доступа к медиапотокам с локальных камер и микрофонов. Метод getUserMedia () является основным способом доступа к локальным устройствам ввода.
У API есть несколько ключевых моментов —
-
Поток мультимедиа в реальном времени представлен объектом потока в виде видео или аудио
-
Он обеспечивает уровень безопасности посредством пользовательских разрешений, запрашивающих пользователя, прежде чем веб-приложение сможет начать извлечение потока.
-
Выбор устройств ввода обрабатывается MediaStream API (например, когда к устройству подключены две камеры или микрофоны)
Поток мультимедиа в реальном времени представлен объектом потока в виде видео или аудио
Он обеспечивает уровень безопасности посредством пользовательских разрешений, запрашивающих пользователя, прежде чем веб-приложение сможет начать извлечение потока.
Выбор устройств ввода обрабатывается MediaStream API (например, когда к устройству подключены две камеры или микрофоны)
Каждый объект MediaStream включает в себя несколько объектов MediaStreamTrack. Они представляют видео и аудио с разных устройств ввода.
Каждый объект MediaStreamTrack может включать в себя несколько каналов (правый и левый аудиоканалы). Это самые маленькие части, определенные MediaStream API.
Есть два способа вывода объектов MediaStream. Во-первых, мы можем визуализировать вывод в видео или аудио элемент. Во-вторых, мы можем отправить вывод объекту RTCPeerConnection, который затем отправит его удаленному узлу.
Использование MediaStream API
Давайте создадим простое приложение WebRTC. Он покажет элемент видео на экране, спросит у пользователя разрешения на использование камеры и покажет прямой поток видео в браузере. Создать файл index.html —
<!DOCTYPE html> <html lang = "en"> <head> <meta charset = "utf-8" /> </head> <body> <video autoplay></video> <script src = "client.js"></script> </body> </html>
Затем создайте файл client.js и добавьте следующее;
function hasUserMedia() { //check if the browser supports the WebRTC return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); } if (hasUserMedia()) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; //enabling video and audio channels navigator.getUserMedia({ video: true, audio: true }, function (stream) { var video = document.querySelector('video'); //inserting our stream to the video tag video.src = window.URL.createObjectURL(stream); }, function (err) {}); } else { alert("WebRTC is not supported"); }
Здесь мы создаем функцию hasUserMedia (), которая проверяет, поддерживается ли WebRTC или нет. Затем мы получаем доступ к функции getUserMedia, где вторым параметром является обратный вызов, который принимает поток, поступающий с устройства пользователя. Затем мы загружаем наш поток в элемент video с помощью window.URL.createObjectURL, который создает URL-адрес, представляющий объект, указанный в параметре.
Теперь обновите страницу, нажмите «Разрешить», и вы должны увидеть свое лицо на экране.
Не забудьте запустить все свои скрипты с помощью веб-сервера. Мы уже установили один из них в учебном пособии по среде WebRTC.
MediaStream API
свойства
-
MediaStream.active (только для чтения) — возвращает true, если MediaStream активен, или false в противном случае.
-
MediaStream.ended (только для чтения, устарел) — возвращает true, если завершенное событие было инициировано для объекта, что означает, что поток был полностью прочитан, или false, если конец потока не был достигнут.
-
MediaStream.id (только для чтения) — уникальный идентификатор объекта.
-
MediaStream.label (только для чтения, не рекомендуется) — уникальный идентификатор, назначаемый пользовательским агентом.
MediaStream.active (только для чтения) — возвращает true, если MediaStream активен, или false в противном случае.
MediaStream.ended (только для чтения, устарел) — возвращает true, если завершенное событие было инициировано для объекта, что означает, что поток был полностью прочитан, или false, если конец потока не был достигнут.
MediaStream.id (только для чтения) — уникальный идентификатор объекта.
MediaStream.label (только для чтения, не рекомендуется) — уникальный идентификатор, назначаемый пользовательским агентом.
Вы можете посмотреть, как эти свойства выглядят в моем браузере —
Обработчики событий
-
MediaStream.onactive — обработчик активного события, которое запускается, когда объект MediaStream становится активным.
-
MediaStream.onaddtrack — обработчик для события addtrack , которое запускается при добавлении нового объекта MediaStreamTrack .
-
MediaStream.onended (устарело) — обработчик завершенного события, которое запускается при прекращении потоковой передачи.
-
MediaStream.oninactive — обработчик неактивного события, которое запускается, когда объект MediaStream становится неактивным.
-
MediaStream.onremovetrack — обработчик события removetrack, которое вызывается при удалении из него объекта MediaStreamTrack .
MediaStream.onactive — обработчик активного события, которое запускается, когда объект MediaStream становится активным.
MediaStream.onaddtrack — обработчик для события addtrack , которое запускается при добавлении нового объекта MediaStreamTrack .
MediaStream.onended (устарело) — обработчик завершенного события, которое запускается при прекращении потоковой передачи.
MediaStream.oninactive — обработчик неактивного события, которое запускается, когда объект MediaStream становится неактивным.
MediaStream.onremovetrack — обработчик события removetrack, которое вызывается при удалении из него объекта MediaStreamTrack .
методы
-
MediaStream.addTrack () — добавляет объект MediaStreamTrack, указанный в качестве аргумента для MediaStream. Если трек уже добавлен, ничего не происходит.
-
MediaStream.clone () — возвращает клон объекта MediaStream с новым идентификатором.
-
MediaStream.getAudioTracks () — возвращает список аудио объектов MediaStreamTrack из объекта MediaStream .
-
MediaStream.getTrackById () — Возвращает трек по ID. Если аргумент пуст или идентификатор не найден, возвращается ноль. Если несколько дорожек имеют одинаковый идентификатор, возвращается первый.
-
MediaStream.getTracks () — возвращает список всех объектов MediaStreamTrack из объекта MediaStream .
-
MediaStream.getVideoTracks () — возвращает список объектов MediaStreamTrack из объекта MediaStream .
-
MediaStream.removeTrack () — удаляет объект MediaStreamTrack, указанный в качестве аргумента, из MediaStream. Если дорожка уже удалена, ничего не происходит.
MediaStream.addTrack () — добавляет объект MediaStreamTrack, указанный в качестве аргумента для MediaStream. Если трек уже добавлен, ничего не происходит.
MediaStream.clone () — возвращает клон объекта MediaStream с новым идентификатором.
MediaStream.getAudioTracks () — возвращает список аудио объектов MediaStreamTrack из объекта MediaStream .
MediaStream.getTrackById () — Возвращает трек по ID. Если аргумент пуст или идентификатор не найден, возвращается ноль. Если несколько дорожек имеют одинаковый идентификатор, возвращается первый.
MediaStream.getTracks () — возвращает список всех объектов MediaStreamTrack из объекта MediaStream .
MediaStream.getVideoTracks () — возвращает список объектов MediaStreamTrack из объекта MediaStream .
MediaStream.removeTrack () — удаляет объект MediaStreamTrack, указанный в качестве аргумента, из MediaStream. Если дорожка уже удалена, ничего не происходит.
Чтобы проверить вышеуказанные API, измените файл index.html следующим образом:
<!DOCTYPE html> <html lang = "en"> <head> <meta charset = "utf-8" /> </head> <body> <video autoplay></video> <div><button id = "btnGetAudioTracks">getAudioTracks() </button></div> <div><button id = "btnGetTrackById">getTrackById() </button></div> <div><button id = "btnGetTracks">getTracks()</button></div> <div><button id = "btnGetVideoTracks">getVideoTracks() </button></div> <div><button id = "btnRemoveAudioTrack">removeTrack() - audio </button></div> <div><button id = "btnRemoveVideoTrack">removeTrack() - video </button></div> <script src = "client.js"></script> </body> </html>
Мы добавили несколько кнопок, чтобы опробовать несколько API MediaStream. Затем мы должны добавить обработчики событий для нашей вновь созданной кнопки. Изменить файл client.js следующим образом —
var stream; function hasUserMedia() { //check if the browser supports the WebRTC return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); } if (hasUserMedia()) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; //enabling video and audio channels navigator.getUserMedia({ video: true, audio: true }, function (s) { stream = s; var video = document.querySelector('video'); //inserting our stream to the video tag video.src = window.URL.createObjectURL(stream); }, function (err) {}); } else { alert("WebRTC is not supported"); } btnGetAudioTracks.addEventListener("click", function(){ console.log("getAudioTracks"); console.log(stream.getAudioTracks()); }); btnGetTrackById.addEventListener("click", function(){ console.log("getTrackById"); console.log(stream.getTrackById(stream.getAudioTracks()[0].id)); }); btnGetTracks.addEventListener("click", function(){ console.log("getTracks()"); console.log(stream.getTracks()); }); btnGetVideoTracks.addEventListener("click", function(){ console.log("getVideoTracks()"); console.log(stream.getVideoTracks()); }); btnRemoveAudioTrack.addEventListener("click", function(){ console.log("removeAudioTrack()"); stream.removeTrack(stream.getAudioTracks()[0]); }); btnRemoveVideoTrack.addEventListener("click", function(){ console.log("removeVideoTrack()"); stream.removeTrack(stream.getVideoTracks()[0]); });
Теперь обновите свою страницу. Нажмите кнопку getAudioTracks () , затем нажмите кнопку removeTrack () — аудио . Аудиодорожка должна быть удалена. Затем сделайте то же самое для видео дорожки.
Если вы нажмете кнопку getTracks () , вы увидите все MediaStreamTracks (все подключенные видео и аудио входы). Затем нажмите на getTrackById (), чтобы получить аудио MediaStreamTrack.
Резюме
В этой главе мы создали простое приложение WebRTC с использованием MediaStream API. Теперь у вас должен быть четкий обзор различных API MediaStream, которые обеспечивают работу WebRTC.
WebRTC — API-интерфейсы RTCPeerConnection
RTCPeerConnection API является ядром однорангового соединения между каждым из браузеров. Для создания объектов RTCPeerConnection просто напишите
var pc = RTCPeerConnection(config);
где аргумент config содержит хотя бы ключ, iceServers. Это массив объектов URL, содержащих информацию о серверах STUN и TURN, используемых при поиске кандидатов на ICE. Вы можете найти список доступных общедоступных серверов STUN на code.google.com.
В зависимости от того, являетесь ли вы абонентом или вызываемым абонентом, объект RTCPeerConnection используется немного по-разному на каждой стороне соединения.
Вот пример потока пользователя —
-
Зарегистрируйте обработчик onicecandidate . Он отправляет кандидатов ICE другому партнеру по мере их поступления.
-
Зарегистрируйте обработчик onaddstream . Он управляет отображением видеопотока после его получения от удаленного узла.
-
Зарегистрируйте обработчик сообщений . Ваш сервер сигнализации также должен иметь обработчик для сообщений, полученных от другого партнера. Если сообщение содержит объект RTCSessionDescription , его следует добавить в объект RTCPeerConnection с помощью метода setRemoteDescription () . Если сообщение содержит объект RTCIceCandidate , его следует добавить в объект RTCPeerConnection с помощью метода addIceCandidate () .
-
Используйте getUserMedia (), чтобы настроить локальный поток мультимедиа и добавить его в объект RTCPeerConnection с помощью метода addStream () .
-
Начать процесс предложения / ответа. Это единственный шаг, когда поток звонящего отличается от потока звонящего. Вызывающая сторона начинает согласование с помощью метода createOffer () и регистрирует обратный вызов, который получает объект RTCSessionDescription . Затем этот обратный вызов должен добавить этот объект RTCSessionDescription к вашему объекту RTCPeerConnection с помощью setLocalDescription () . И, наконец, вызывающая сторона должна отправить эту RTCSessionDescription удаленному узлу с помощью сервера сигнализации. Вызываемый, с другой стороны, регистрирует тот же обратный вызов, но в методе createAnswer () . Обратите внимание, что поток вызываемого абонента инициируется только после получения предложения от вызывающего абонента.
Зарегистрируйте обработчик onicecandidate . Он отправляет кандидатов ICE другому партнеру по мере их поступления.
Зарегистрируйте обработчик onaddstream . Он управляет отображением видеопотока после его получения от удаленного узла.
Зарегистрируйте обработчик сообщений . Ваш сервер сигнализации также должен иметь обработчик для сообщений, полученных от другого партнера. Если сообщение содержит объект RTCSessionDescription , его следует добавить в объект RTCPeerConnection с помощью метода setRemoteDescription () . Если сообщение содержит объект RTCIceCandidate , его следует добавить в объект RTCPeerConnection с помощью метода addIceCandidate () .
Используйте getUserMedia (), чтобы настроить локальный поток мультимедиа и добавить его в объект RTCPeerConnection с помощью метода addStream () .
Начать процесс предложения / ответа. Это единственный шаг, когда поток звонящего отличается от потока звонящего. Вызывающая сторона начинает согласование с помощью метода createOffer () и регистрирует обратный вызов, который получает объект RTCSessionDescription . Затем этот обратный вызов должен добавить этот объект RTCSessionDescription к вашему объекту RTCPeerConnection с помощью setLocalDescription () . И, наконец, вызывающая сторона должна отправить эту RTCSessionDescription удаленному узлу с помощью сервера сигнализации. Вызываемый, с другой стороны, регистрирует тот же обратный вызов, но в методе createAnswer () . Обратите внимание, что поток вызываемого абонента инициируется только после получения предложения от вызывающего абонента.
RTCPeerConnection API
свойства
-
RTCPeerConnection.iceConnectionState (только для чтения) — возвращает перечисление RTCIceConnectionState, которое описывает состояние соединения. Событие iceconnectionstatechange вызывается при изменении этого значения. Возможные значения —
-
новый — агент ICE ждет удаленных кандидатов или собирает адреса
-
проверка — у агента ICE есть удаленные кандидаты, но он еще не нашел соединение
-
подключен — агент ICE обнаружил пригодное для использования соединение, но все еще проверяет более удаленного кандидата на лучшее соединение.
-
завершено — агент ICE нашел работоспособное соединение и прекратил тестирование удаленных кандидатов.
-
ошибка — агент ICE проверил все удаленные кандидаты, но не нашел соответствия хотя бы для одного компонента.
-
отключен — хотя бы один компонент больше не работает.
-
закрыт — агент ICE закрыт.
-
-
RTCPeerConnection.iceGatheringState (только для чтения) — возвращает перечисление RTCIceGatheringState, которое описывает состояние сбора ICE для соединения.
-
новый — объект был только что создан.
-
сбор — агент ICE находится в процессе сбора кандидатов
-
завершить ICE агент завершил сбор.
-
-
RTCPeerConnection.localDescription (только для чтения) — возвращает описание RTCSessionDescription, описывающее локальный сеанс. Он может быть нулевым, если он еще не установлен.
-
RTCPeerConnection.peerIdentity (только для чтения) — возвращает RTCIdentityAssertion. Он состоит из idp (доменного имени) и имени, представляющего личность удаленного партнера.
-
RTCPeerConnection.remoteDescription (только для чтения) — возвращает описание RTCSessionDescription, описывающее удаленный сеанс. Он может быть нулевым, если он еще не установлен.
-
RTCPeerConnection.signalingState (только для чтения) — возвращает перечисление RTCSignalingState, которое описывает состояние сигнализации локального соединения. Это состояние описывает предложение SDP. Событие signalingstatechange вызывается при изменении этого значения. Возможные значения —
-
стабильный — начальное состояние. В настоящее время обмен предложениями и ответами SDP не осуществляется.
-
have-local-offer — локальная сторона соединения локально применила предложение SDP.
-
have-remote-offer — удаленная сторона соединения локально применила предложение SDP.
-
have-local-pranswer — удаленное предложение SDP было применено, а локальный pranswer SDP.
-
have-remote-pranswer — применен локальный SDP, а удаленный SDP.
-
закрыто — соединение закрыто.
-
RTCPeerConnection.iceConnectionState (только для чтения) — возвращает перечисление RTCIceConnectionState, которое описывает состояние соединения. Событие iceconnectionstatechange вызывается при изменении этого значения. Возможные значения —
новый — агент ICE ждет удаленных кандидатов или собирает адреса
проверка — у агента ICE есть удаленные кандидаты, но он еще не нашел соединение
подключен — агент ICE обнаружил пригодное для использования соединение, но все еще проверяет более удаленного кандидата на лучшее соединение.
завершено — агент ICE нашел работоспособное соединение и прекратил тестирование удаленных кандидатов.
ошибка — агент ICE проверил все удаленные кандидаты, но не нашел соответствия хотя бы для одного компонента.
отключен — хотя бы один компонент больше не работает.
закрыт — агент ICE закрыт.
RTCPeerConnection.iceGatheringState (только для чтения) — возвращает перечисление RTCIceGatheringState, которое описывает состояние сбора ICE для соединения.
новый — объект был только что создан.
сбор — агент ICE находится в процессе сбора кандидатов
завершить ICE агент завершил сбор.
RTCPeerConnection.localDescription (только для чтения) — возвращает описание RTCSessionDescription, описывающее локальный сеанс. Он может быть нулевым, если он еще не установлен.
RTCPeerConnection.peerIdentity (только для чтения) — возвращает RTCIdentityAssertion. Он состоит из idp (доменного имени) и имени, представляющего личность удаленного партнера.
RTCPeerConnection.remoteDescription (только для чтения) — возвращает описание RTCSessionDescription, описывающее удаленный сеанс. Он может быть нулевым, если он еще не установлен.
RTCPeerConnection.signalingState (только для чтения) — возвращает перечисление RTCSignalingState, которое описывает состояние сигнализации локального соединения. Это состояние описывает предложение SDP. Событие signalingstatechange вызывается при изменении этого значения. Возможные значения —
стабильный — начальное состояние. В настоящее время обмен предложениями и ответами SDP не осуществляется.
have-local-offer — локальная сторона соединения локально применила предложение SDP.
have-remote-offer — удаленная сторона соединения локально применила предложение SDP.
have-local-pranswer — удаленное предложение SDP было применено, а локальный pranswer SDP.
have-remote-pranswer — применен локальный SDP, а удаленный SDP.
закрыто — соединение закрыто.
Обработчики событий
S.No. | Обработчики событий и описание |
---|---|
1 |
RTCPeerConnection.onaddstream Этот обработчик вызывается при возникновении события addstream. Это событие отправляется, когда MediaStream добавляется к этому соединению удаленным узлом. |
2 |
RTCPeerConnection.ondatachannel Этот обработчик вызывается при запуске события канала данных. Это событие отправляется, когда RTCDataChannel добавляется к этому соединению. |
3 |
RTCPeerConnection.onicecandidate Этот обработчик вызывается при запуске события icecandidate. Это событие отправляется, когда объект RTCIceCandidate добавляется в сценарий. |
4 |
RTCPeerConnection.oniceconnectionstatechange Этот обработчик вызывается при запуске события iceconnectionstatechange. Это событие отправляется при изменении значения iceConnectionState. |
5 |
RTCPeerConnection.onidentityresult Этот обработчик вызывается при возникновении события identityresult. Это событие отправляется, когда создается удостоверение личности во время создания предложения или ответа через getIdentityAssertion (). |
6 |
RTCPeerConnection.onidpassertionerror Этот обработчик вызывается при возникновении события idpassertionerror. Это событие отправляется, когда IdP (поставщик идентификаторов) обнаруживает ошибку при создании подтверждения личности. |
7 |
RTCPeerConnection.onidpvalidation Этот обработчик вызывается при возникновении события idpvalidationerror. Это событие отправляется, когда IdP (Identitry Provider) обнаруживает ошибку при проверке подтверждения личности. |
8 |
RTCPeerConnection.onnegotiationneeded Этот обработчик вызывается при возникновении события с необходимостью согласования. Это событие отправляется браузером, чтобы сообщить, что переговоры потребуются в какой-то момент в будущем. |
9 |
RTCPeerConnection.onpeeridentity Этот обработчик вызывается, когда происходит событие peeridentity. Это событие отправляется, когда в этом соединении установлена и проверена идентификация однорангового узла. |
10 |
RTCPeerConnection.onremovestream Этот обработчик вызывается при возникновении события signalingstatechange. Это событие отправляется при изменении значения signalingState. |
11 |
RTCPeerConnection.onsignalingstatechange Этот обработчик вызывается при возникновении события removestream. Это событие отправляется, когда MediaStream удаляется из этого соединения. |
RTCPeerConnection.onaddstream
Этот обработчик вызывается при возникновении события addstream. Это событие отправляется, когда MediaStream добавляется к этому соединению удаленным узлом.
RTCPeerConnection.ondatachannel
Этот обработчик вызывается при запуске события канала данных. Это событие отправляется, когда RTCDataChannel добавляется к этому соединению.
RTCPeerConnection.onicecandidate
Этот обработчик вызывается при запуске события icecandidate. Это событие отправляется, когда объект RTCIceCandidate добавляется в сценарий.
RTCPeerConnection.oniceconnectionstatechange
Этот обработчик вызывается при запуске события iceconnectionstatechange. Это событие отправляется при изменении значения iceConnectionState.
RTCPeerConnection.onidentityresult
Этот обработчик вызывается при возникновении события identityresult. Это событие отправляется, когда создается удостоверение личности во время создания предложения или ответа через getIdentityAssertion ().
RTCPeerConnection.onidpassertionerror
Этот обработчик вызывается при возникновении события idpassertionerror. Это событие отправляется, когда IdP (поставщик идентификаторов) обнаруживает ошибку при создании подтверждения личности.
RTCPeerConnection.onidpvalidation
Этот обработчик вызывается при возникновении события idpvalidationerror. Это событие отправляется, когда IdP (Identitry Provider) обнаруживает ошибку при проверке подтверждения личности.
RTCPeerConnection.onnegotiationneeded
Этот обработчик вызывается при возникновении события с необходимостью согласования. Это событие отправляется браузером, чтобы сообщить, что переговоры потребуются в какой-то момент в будущем.
RTCPeerConnection.onpeeridentity
Этот обработчик вызывается, когда происходит событие peeridentity. Это событие отправляется, когда в этом соединении установлена и проверена идентификация однорангового узла.
RTCPeerConnection.onremovestream
Этот обработчик вызывается при возникновении события signalingstatechange. Это событие отправляется при изменении значения signalingState.
RTCPeerConnection.onsignalingstatechange
Этот обработчик вызывается при возникновении события removestream. Это событие отправляется, когда MediaStream удаляется из этого соединения.
методы
S.No. | Методы и описание |
---|---|
1 |
RTCPeerConnection () Возвращает новый объект RTCPeerConnection. |
2 |
RTCPeerConnection.createOffer () Создает предложение (запрос) на поиск удаленного партнера. Двумя первыми параметрами этого метода являются обратные вызовы успешности и ошибок. Необязательный третий параметр — это параметры, такие как включение аудио или видео потоков. |
3 |
RTCPeerConnection.createAnswer () Создает ответ на предложение, полученное удаленным узлом в процессе согласования предложения / ответа. Двумя первыми параметрами этого метода являются обратные вызовы успешности и ошибок. Необязательный третий параметр — это варианты ответа, который будет создан. |
4 |
RTCPeerConnection.setLocalDescription () Изменяет описание локального соединения. Описание определяет свойства соединения. Соединение должно поддерживать как старые, так и новые описания. Метод принимает три параметра: объект RTCSessionDescription, обратный вызов, если изменение описания выполнено успешно, обратный вызов, если изменение описания не удается. |
5 |
RTCPeerConnection.setRemoteDescription () Изменяет описание удаленного соединения. Описание определяет свойства соединения. Соединение должно поддерживать как старые, так и новые описания. Метод принимает три параметра: объект RTCSessionDescription, обратный вызов, если изменение описания выполнено успешно, обратный вызов, если изменение описания не удается. |
6 |
RTCPeerConnection.updateIce () Обновляет процесс агента ICE для проверки связи с удаленными кандидатами и сбора локальных кандидатов. |
7 |
RTCPeerConnection.addIceCandidate () Предоставляет удаленного кандидата агенту ICE. |
8 |
RTCPeerConnection.getConfiguration () Возвращает объект RTCConfiguration. Он представляет конфигурацию объекта RTCPeerConnection. |
9 |
RTCPeerConnection.getLocalStreams () Возвращает массив локального соединения MediaStream. |
10 |
RTCPeerConnection.getRemoteStreams () Возвращает массив удаленного соединения MediaStream. |
11 |
RTCPeerConnection.getStreamById () Возвращает локальный или удаленный MediaStream по заданному идентификатору. |
12 |
RTCPeerConnection.addStream () Добавляет MediaStream в качестве локального источника видео или аудио. |
13 |
RTCPeerConnection.removeStream () Удаляет MediaStream как локальный источник видео или аудио. |
14 |
RTCPeerConnection.close () Закрывает соединение. |
15 |
RTCPeerConnection.createDataChannel () Создает новый RTCDataChannel. |
16 |
RTCPeerConnection.createDTMFSender () Создает новый RTCDTMFSender, связанный с конкретным MediaStreamTrack. Позволяет отправлять DTMF (двухтональные многочастотные) телефонные сигналы по соединению. |
17 |
RTCPeerConnection.getStats () Создает новый RTCStatsReport, который содержит статистику, касающуюся соединения. |
18 |
RTCPeerConnection.setIdentityProvider () Устанавливает IdP. Принимает три параметра — имя, протокол, используемый для связи, и необязательное имя пользователя. |
19 |
RTCPeerConnection.getIdentityAssertion () Собирает утверждение личности. Не ожидается, чтобы иметь дело с этим методом в приложении. Таким образом, вы можете назвать это явно, только чтобы предвидеть необходимость. |
RTCPeerConnection ()
Возвращает новый объект RTCPeerConnection.
RTCPeerConnection.createOffer ()
Создает предложение (запрос) на поиск удаленного партнера. Двумя первыми параметрами этого метода являются обратные вызовы успешности и ошибок. Необязательный третий параметр — это параметры, такие как включение аудио или видео потоков.
RTCPeerConnection.createAnswer ()
Создает ответ на предложение, полученное удаленным узлом в процессе согласования предложения / ответа. Двумя первыми параметрами этого метода являются обратные вызовы успешности и ошибок. Необязательный третий параметр — это варианты ответа, который будет создан.
RTCPeerConnection.setLocalDescription ()
Изменяет описание локального соединения. Описание определяет свойства соединения. Соединение должно поддерживать как старые, так и новые описания. Метод принимает три параметра: объект RTCSessionDescription, обратный вызов, если изменение описания выполнено успешно, обратный вызов, если изменение описания не удается.
RTCPeerConnection.setRemoteDescription ()
Изменяет описание удаленного соединения. Описание определяет свойства соединения. Соединение должно поддерживать как старые, так и новые описания. Метод принимает три параметра: объект RTCSessionDescription, обратный вызов, если изменение описания выполнено успешно, обратный вызов, если изменение описания не удается.
RTCPeerConnection.updateIce ()
Обновляет процесс агента ICE для проверки связи с удаленными кандидатами и сбора локальных кандидатов.
RTCPeerConnection.addIceCandidate ()
Предоставляет удаленного кандидата агенту ICE.
RTCPeerConnection.getConfiguration ()
Возвращает объект RTCConfiguration. Он представляет конфигурацию объекта RTCPeerConnection.
RTCPeerConnection.getLocalStreams ()
Возвращает массив локального соединения MediaStream.
RTCPeerConnection.getRemoteStreams ()
Возвращает массив удаленного соединения MediaStream.
RTCPeerConnection.getStreamById ()
Возвращает локальный или удаленный MediaStream по заданному идентификатору.
RTCPeerConnection.addStream ()
Добавляет MediaStream в качестве локального источника видео или аудио.
RTCPeerConnection.removeStream ()
Удаляет MediaStream как локальный источник видео или аудио.
RTCPeerConnection.close ()
Закрывает соединение.
RTCPeerConnection.createDataChannel ()
Создает новый RTCDataChannel.
RTCPeerConnection.createDTMFSender ()
Создает новый RTCDTMFSender, связанный с конкретным MediaStreamTrack. Позволяет отправлять DTMF (двухтональные многочастотные) телефонные сигналы по соединению.
RTCPeerConnection.getStats ()
Создает новый RTCStatsReport, который содержит статистику, касающуюся соединения.
RTCPeerConnection.setIdentityProvider ()
Устанавливает IdP. Принимает три параметра — имя, протокол, используемый для связи, и необязательное имя пользователя.
RTCPeerConnection.getIdentityAssertion ()
Собирает утверждение личности. Не ожидается, чтобы иметь дело с этим методом в приложении. Таким образом, вы можете назвать это явно, только чтобы предвидеть необходимость.
Установление соединения
Теперь давайте создадим пример приложения. Во-первых, запустите сервер сигнализации, который мы создали в руководстве «Сервер сигнализации», через «сервер узла».
На странице будет два текстовых ввода: один для логина и один для имени пользователя, к которому мы хотим подключиться. Создайте файл index.html и добавьте следующий код —
<html lang = "en"> <head> <meta charset = "utf-8" /> </head> <body> <div> <input type = "text" id = "loginInput" /> <button id = "loginBtn">Login</button> </div> <div> <input type = "text" id = "otherUsernameInput" /> <button id = "connectToOtherUsernameBtn">Establish connection</button> </div> <script src = "client2.js"></script> </body> </html>
Вы можете видеть, что мы добавили ввод текста для входа в систему, кнопку входа в систему, ввод текста для имени другого однорангового пользователя и кнопку подключения к нему. Теперь создайте файл client.js и добавьте следующий код —
var connection = new WebSocket('ws://localhost:9090'); var name = ""; var loginInput = document.querySelector('#loginInput'); var loginBtn = document.querySelector('#loginBtn'); var otherUsernameInput = document.querySelector('#otherUsernameInput'); var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); var connectedUser, myConnection; //when a user clicks the login button loginBtn.addEventListener("click", function(event){ name = loginInput.value; if(name.length > 0){ send({ type: "login", name: name }); } }); //handle messages from the server connection.onmessage = function (message) { console.log("Got message", message.data); var data = JSON.parse(message.data); switch(data.type) { case "login": onLogin(data.success); break; case "offer": onOffer(data.offer, data.name); break; case "answer": onAnswer(data.answer); break; case "candidate": onCandidate(data.candidate); break; default: break; } }; //when a user logs in function onLogin(success) { if (success === false) { alert("oops...try a different username"); } else { //creating our RTCPeerConnection object var configuration = { "iceServers": [{ "url": "stun:stun.1.google.com:19302" }] }; myConnection = new webkitRTCPeerConnection(configuration); console.log("RTCPeerConnection object was created"); console.log(myConnection); //setup ice handling //when the browser finds an ice candidate we send it to another peer myConnection.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; } }; connection.onopen = function () { console.log("Connected"); }; connection.onerror = function (err) { console.log("Got error", err); }; // Alias for sending messages in JSON format function send(message) { if (connectedUser) { message.name = connectedUser; } connection.send(JSON.stringify(message)); };
Вы можете видеть, что мы устанавливаем сокет-соединение с нашим сигнальным сервером. Когда пользователь нажимает кнопку входа в систему, приложение отправляет свое имя пользователя на сервер. Если вход выполнен успешно, приложение создает объект RTCPeerConnection и настраивает обработчик onicecandidate, который отправляет все найденные icecandidates другому партнеру. Теперь откройте страницу и попробуйте войти. Вы должны увидеть следующий вывод консоли —
Следующим шагом является создание предложения для другого партнера. Добавьте следующий код в ваш файл client.js —
//setup a peer connection with another user connectToOtherUsernameBtn.addEventListener("click", function () { var otherUsername = otherUsernameInput.value; connectedUser = otherUsername; if (otherUsername.length > 0) { //make an offer myConnection.createOffer(function (offer) { console.log(); send({ type: "offer", offer: offer }); myConnection.setLocalDescription(offer); }, function (error) { alert("An error has occurred."); }); } }); //when somebody wants to call us function onOffer(offer, name) { connectedUser = name; myConnection.setRemoteDescription(new RTCSessionDescription(offer)); myConnection.createAnswer(function (answer) { myConnection.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("oops...error"); }); } //when another user answers to our offer function onAnswer(answer) { myConnection.setRemoteDescription(new RTCSessionDescription(answer)); } //when we got ice candidate from another user function onCandidate(candidate) { myConnection.addIceCandidate(new RTCIceCandidate(candidate)); }
Вы можете видеть, что когда пользователь нажимает кнопку «Установить соединение», приложение делает предложение SDP другому партнеру. Мы также устанавливаем обработчики onAnswer и onCandidate . Перезагрузите страницу, откройте ее на двух вкладках, войдите с двумя пользователями и попытайтесь установить связь между ними. Вы должны увидеть следующий вывод консоли —
Теперь одноранговое соединение установлено. В следующих уроках мы добавим видео и аудио потоки, а также поддержку текстового чата.
WebRTC — API-интерфейсы RTCDataChannel
WebRTC хорош не только для передачи аудио и видео потоков, но и для любых произвольных данных, которые у нас могут быть. Это где объект RTCDataChannel вступает в игру.
RTCDataChannel API
свойства
-
RTCDataChannel.label (только для чтения) — возвращает строку, содержащую имя канала данных.
-
RTCDataChannel.ordered (только для чтения) — возвращает true, если порядок доставки сообщений гарантирован, или false, если он не гарантирован.
-
RTCDataChannel.protocol (только для чтения) — возвращает строку, содержащую имя подпротокола, используемого для этого канала.
-
RTCDataChannel.id (только для чтения) — возвращает уникальный идентификатор для канала, который устанавливается при создании объекта RTCDataChannel.
-
RTCDataChannel.readyState (только для чтения) — возвращает перечисление RTCDataChannelState, представляющее состояние соединения. Возможные значения —
-
подключение — указывает, что соединение еще не активно. Это начальное состояние.
-
open — указывает, что соединение установлено.
-
закрытие — указывает на то, что соединение находится в процессе отключения. Кэшированные сообщения находятся в процессе отправки или получения, но ни одна вновь созданная задача не принимает.
-
закрыт — указывает на то, что соединение не может быть установлено или было отключено.
-
-
RTCDataChannel.bufferedAmount (только для чтения) — возвращает количество байтов, которые были поставлены в очередь для отправки. Это объем данных, которые еще не были отправлены через RTCDataChannel.send ().
-
RTCDataChannel.bufferedAmountLowThreshold — возвращает количество байтов, при котором RTCDataChannel.bufferedAmount считается низким. Когда значение RTCDataChannel.bufferedAmount уменьшается ниже этого порога, вызывается событие bufferedamountlow.
-
RTCDataChannel.binaryType — возвращает тип двоичных данных, передаваемых соединением. Может быть «blob» или «arraybuffer».
-
RTCDataChannel.maxPacketLifeType (только для чтения) — возвращает беззнаковое короткое замыкание, указывающее длину в миллисекундах окна, когда обмен сообщениями происходит в ненадежном режиме.
-
RTCDataChannel.maxRetransmits (только для чтения) — возвращает короткое число без знака, указывающее максимальное количество раз, когда канал будет повторно передавать данные, если они не доставлены.
-
RTCDataChannel.negotiated (только для чтения) — возвращает логическое значение, указывающее, был ли канал согласован агентом пользователя или приложением.
-
RTCDataChannel.reliable (только для чтения) — возвращает логическое значение, указывающее, что соединение может отправлять сообщения в ненадежном режиме.
-
RTCDataChannel.stream (только для чтения) — синоним RTCDataChannel.id
RTCDataChannel.label (только для чтения) — возвращает строку, содержащую имя канала данных.
RTCDataChannel.ordered (только для чтения) — возвращает true, если порядок доставки сообщений гарантирован, или false, если он не гарантирован.
RTCDataChannel.protocol (только для чтения) — возвращает строку, содержащую имя подпротокола, используемого для этого канала.
RTCDataChannel.id (только для чтения) — возвращает уникальный идентификатор для канала, который устанавливается при создании объекта RTCDataChannel.
RTCDataChannel.readyState (только для чтения) — возвращает перечисление RTCDataChannelState, представляющее состояние соединения. Возможные значения —
подключение — указывает, что соединение еще не активно. Это начальное состояние.
open — указывает, что соединение установлено.
закрытие — указывает на то, что соединение находится в процессе отключения. Кэшированные сообщения находятся в процессе отправки или получения, но ни одна вновь созданная задача не принимает.
закрыт — указывает на то, что соединение не может быть установлено или было отключено.
RTCDataChannel.bufferedAmount (только для чтения) — возвращает количество байтов, которые были поставлены в очередь для отправки. Это объем данных, которые еще не были отправлены через RTCDataChannel.send ().
RTCDataChannel.bufferedAmountLowThreshold — возвращает количество байтов, при котором RTCDataChannel.bufferedAmount считается низким. Когда значение RTCDataChannel.bufferedAmount уменьшается ниже этого порога, вызывается событие bufferedamountlow.
RTCDataChannel.binaryType — возвращает тип двоичных данных, передаваемых соединением. Может быть «blob» или «arraybuffer».
RTCDataChannel.maxPacketLifeType (только для чтения) — возвращает беззнаковое короткое замыкание, указывающее длину в миллисекундах окна, когда обмен сообщениями происходит в ненадежном режиме.
RTCDataChannel.maxRetransmits (только для чтения) — возвращает короткое число без знака, указывающее максимальное количество раз, когда канал будет повторно передавать данные, если они не доставлены.
RTCDataChannel.negotiated (только для чтения) — возвращает логическое значение, указывающее, был ли канал согласован агентом пользователя или приложением.
RTCDataChannel.reliable (только для чтения) — возвращает логическое значение, указывающее, что соединение может отправлять сообщения в ненадежном режиме.
RTCDataChannel.stream (только для чтения) — синоним RTCDataChannel.id
Обработчики событий
-
RTCDataChannel.onopen — этот обработчик события вызывается при запуске события open. Это событие отправляется, когда соединение для передачи данных установлено.
-
RTCDataChannel.onmessage — этот обработчик события вызывается при возникновении события сообщения. Событие отправляется, когда сообщение доступно на канале данных.
-
RTCDataChannel.onbufferedamountlow — этот обработчик события вызывается при возникновении события bufferedamoutlow. Это событие отправляется, когда RTCDataChannel.bufferedAmount уменьшается ниже свойства RTCDataChannel.bufferedAmountLowThreshold.
-
RTCDataChannel.onclose — этот обработчик события вызывается при запуске события close. Это событие отправляется, когда соединение для передачи данных было закрыто.
-
RTCDataChannel.onerror — этот обработчик события вызывается при возникновении события ошибки. Это событие отправляется при обнаружении ошибки.
RTCDataChannel.onopen — этот обработчик события вызывается при запуске события open. Это событие отправляется, когда соединение для передачи данных установлено.
RTCDataChannel.onmessage — этот обработчик события вызывается при возникновении события сообщения. Событие отправляется, когда сообщение доступно на канале данных.
RTCDataChannel.onbufferedamountlow — этот обработчик события вызывается при возникновении события bufferedamoutlow. Это событие отправляется, когда RTCDataChannel.bufferedAmount уменьшается ниже свойства RTCDataChannel.bufferedAmountLowThreshold.
RTCDataChannel.onclose — этот обработчик события вызывается при запуске события close. Это событие отправляется, когда соединение для передачи данных было закрыто.
RTCDataChannel.onerror — этот обработчик события вызывается при возникновении события ошибки. Это событие отправляется при обнаружении ошибки.
методы
-
RTCDataChannel.close () — Закрывает канал данных.
-
RTCDataChannel.send () — отправляет данные в параметре по каналу. Данные могут быть BLOB-объектами, строками, ArrayBuffer или ArrayBufferView.
RTCDataChannel.close () — Закрывает канал данных.
RTCDataChannel.send () — отправляет данные в параметре по каналу. Данные могут быть BLOB-объектами, строками, ArrayBuffer или ArrayBufferView.
WebRTC — отправка сообщений
Теперь давайте создадим простой пример. Во-первых, запустите сервер сигнализации, который мы создали в руководстве «Сервер сигнализации», через «сервер узла».
На странице будет три текстовых ввода: один для логина, один для имени пользователя и один для сообщения, которое мы хотим отправить другому партнеру. Создайте файл index.html и добавьте следующий код —
<html lang = "en"> <head> <meta charset = "utf-8" /> </head> <body> <div> <input type = "text" id = "loginInput" /> <button id = "loginBtn">Login</button> </div> <div> <input type = "text" id = "otherUsernameInput" /> <button id = "connectToOtherUsernameBtn">Establish connection</button> </div> <div> <input type = "text" id = "msgInput" /> <button id = "sendMsgBtn">Send text message</button> </div> <script src = "client.js"></script> </body> </html>
Мы также добавили три кнопки для входа в систему, установления соединения и отправки сообщения. Теперь создайте файл client.js и добавьте следующий код —
var connection = new WebSocket('ws://localhost:9090'); var name = ""; var loginInput = document.querySelector('#loginInput'); var loginBtn = document.querySelector('#loginBtn'); var otherUsernameInput = document.querySelector('#otherUsernameInput'); var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); var msgInput = document.querySelector('#msgInput'); var sendMsgBtn = document.querySelector('#sendMsgBtn'); var connectedUser, myConnection, dataChannel; //when a user clicks the login button loginBtn.addEventListener("click", function(event) { name = loginInput.value; if(name.length > 0) { send({ type: "login", name: name }); } }); //handle messages from the server connection.onmessage = function (message) { console.log("Got message", message.data); var data = JSON.parse(message.data); switch(data.type) { case "login": onLogin(data.success); break; case "offer": onOffer(data.offer, data.name); break; case "answer": onAnswer(data.answer); break; case "candidate": onCandidate(data.candidate); break; default: break; } }; //when a user logs in function onLogin(success) { if (success === false) { alert("oops...try a different username"); } else { //creating our RTCPeerConnection object var configuration = { "iceServers": [{ "url": "stun:stun.1.google.com:19302" }] }; myConnection = new webkitRTCPeerConnection(configuration, { optional: [{RtpDataChannels: true}] }); console.log("RTCPeerConnection object was created"); console.log(myConnection); //setup ice handling //when the browser finds an ice candidate we send it to another peer myConnection.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; openDataChannel(); } }; connection.onopen = function () { console.log("Connected"); }; connection.onerror = function (err) { console.log("Got error", err); }; // Alias for sending messages in JSON format function send(message) { if (connectedUser) { message.name = connectedUser; } connection.send(JSON.stringify(message)); };
Вы можете видеть, что мы устанавливаем сокет-соединение с нашим сигнальным сервером. Когда пользователь нажимает кнопку входа в систему, приложение отправляет свое имя пользователя на сервер. Если вход выполнен успешно, приложение создает объект RTCPeerConnection и настраивает обработчик onicecandidate, который отправляет все найденные icecandidates другому партнеру. Он также запускает функцию openDataChannel (), которая создает dataChannel. Обратите внимание, что при создании объекта RTCPeerConnection второй аргумент в конструкторе необязательный: [{RtpDataChannels: true}] является обязательным, если вы используете Chrome или Opera. Следующим шагом является создание предложения для другого партнера. Добавьте следующий код в ваш файл client.js —
//setup a peer connection with another user connectToOtherUsernameBtn.addEventListener("click", function () { var otherUsername = otherUsernameInput.value; connectedUser = otherUsername; if (otherUsername.length > 0) { //make an offer myConnection.createOffer(function (offer) { console.log(); send({ type: "offer", offer: offer }); myConnection.setLocalDescription(offer); }, function (error) { alert("An error has occurred."); }); } }); //when somebody wants to call us function onOffer(offer, name) { connectedUser = name; myConnection.setRemoteDescription(new RTCSessionDescription(offer)); myConnection.createAnswer(function (answer) { myConnection.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("oops...error"); }); } //when another user answers to our offer function onAnswer(answer) { myConnection.setRemoteDescription(new RTCSessionDescription(answer)); } //when we got ice candidate from another user function onCandidate(candidate) { myConnection.addIceCandidate(new RTCIceCandidate(candidate)); }
Вы можете видеть, что когда пользователь нажимает кнопку «Установить соединение», приложение делает предложение SDP другому партнеру. Мы также устанавливаем обработчики onAnswer и onCandidate . Наконец, давайте реализуем функцию openDataChannel (), которая создает наш dataChannel. Добавьте следующий код в ваш файл client.js —
//creating data channel function openDataChannel() { var dataChannelOptions = { reliable:true }; dataChannel = myConnection.createDataChannel("myDataChannel", dataChannelOptions); dataChannel.onerror = function (error) { console.log("Error:", error); }; dataChannel.onmessage = function (event) { console.log("Got message:", event.data); }; } //when a user clicks the send message button sendMsgBtn.addEventListener("click", function (event) { console.log("send message"); var val = msgInput.value; dataChannel.send(val); });
Здесь мы создаем dataChannel для нашего соединения и добавляем обработчик событий для кнопки «отправить сообщение». Теперь откройте эту страницу в двух вкладках, войдите в систему с двумя пользователями, установите соединение и попробуйте отправлять сообщения. Вы должны увидеть их в выводе консоли. Обратите внимание, что приведенный выше пример протестирован в Opera.
Теперь вы можете видеть, что RTCDataChannel является чрезвычайно мощной частью WebRTC API. Есть много других вариантов использования этого объекта, таких как одноранговые игры или обмен файлами через торрент.
WebRTC — Сигнализация
Большинство приложений WebRTC не просто способны общаться через видео и аудио. Им нужно много других функций. В этой главе мы собираемся создать базовый сервер сигнализации.
Сигнализация и переговоры
Чтобы подключиться к другому пользователю, вы должны знать, где он находится в Интернете. IP-адрес вашего устройства позволяет подключенным к Интернету устройствам отправлять данные непосредственно между собой. За это отвечает объект RTCPeerConnection . Как только устройства узнают, как найти друг друга через Интернет, они начинают обмениваться данными о том, какие протоколы и кодеки поддерживает каждое устройство.
Для общения с другим пользователем вам просто необходимо обменяться контактной информацией, а остальное сделает WebRTC. Процесс подключения к другому пользователю также известен как сигнализация и согласование. Он состоит из нескольких шагов —
-
Создайте список потенциальных кандидатов для однорангового соединения.
-
Пользователь или приложение выбирает пользователя для подключения.
-
Слой сигнализации уведомляет другого пользователя, что кто-то хочет подключиться к нему. Он может принять или отклонить.
-
Первый пользователь уведомляется о принятии предложения.
-
Первый пользователь инициирует RTCPeerConnection с другим пользователем.
-
Оба пользователя обмениваются программной и аппаратной информацией через сервер сигнализации.
-
Оба пользователя обмениваются информацией о местоположении.
-
Соединение успешно или неудачно.
Создайте список потенциальных кандидатов для однорангового соединения.
Пользователь или приложение выбирает пользователя для подключения.
Слой сигнализации уведомляет другого пользователя, что кто-то хочет подключиться к нему. Он может принять или отклонить.
Первый пользователь уведомляется о принятии предложения.
Первый пользователь инициирует RTCPeerConnection с другим пользователем.
Оба пользователя обмениваются программной и аппаратной информацией через сервер сигнализации.
Оба пользователя обмениваются информацией о местоположении.
Соединение успешно или неудачно.
Спецификация WebRTC не содержит каких-либо стандартов обмена информацией. Так что имейте в виду, что вышеизложенное является лишь примером того, как может происходить передача сигналов. Вы можете использовать любой протокол или технологию, которая вам нравится.
Сборка сервера
Сервер, который мы собираемся построить, сможет соединить двух пользователей, которые не находятся на одном компьютере. Мы создадим наш собственный механизм сигнализации. Наш сигнальный сервер позволит одному пользователю звонить другому. Как только пользователь вызвал другого, сервер передает предложение, ответ, кандидатов ICE между ними и устанавливает соединение WebRTC.
Приведенная выше диаграмма представляет собой поток сообщений между пользователями при использовании сервера сигнализации. Прежде всего, каждый пользователь регистрируется на сервере. В нашем случае это будет простая строка имени пользователя. После регистрации пользователи могут звонить друг другу. Пользователь 1 делает предложение с идентификатором пользователя, которому он хочет позвонить. Другой пользователь должен ответить. Наконец, кандидаты ICE отправляются между пользователями, пока они не смогут установить соединение.
Чтобы создать соединение WebRTC, клиенты должны иметь возможность передавать сообщения без использования однорангового соединения WebRTC. Здесь мы будем использовать HTML5 WebSockets — двунаправленное сокетное соединение между двумя конечными точками — веб-сервером и веб-браузером. Теперь давайте начнем использовать библиотеку WebSocket. Создайте файл server.js и вставьте следующий код —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message){ console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
Первая строка требует библиотеки WebSocket, которую мы уже установили. Затем мы создаем сервер сокетов на порту 9090. Затем мы прослушиваем событие подключения . Этот код будет выполнен, когда пользователь устанавливает соединение WebSocket с сервером. Затем мы слушаем любые сообщения, отправленные пользователем. Наконец, мы отправляем ответ подключенному пользователю, говоря «Привет с сервера».
Теперь запустите сервер узла, и сервер должен начать прислушиваться к сокетным соединениям.
Для тестирования нашего сервера мы будем использовать утилиту wscat, которую мы также уже установили. Этот инструмент помогает подключаться напрямую к серверу WebSocket и тестировать команды. Запустите наш сервер в одном окне терминала, затем откройте другое и выполните команду wscat -c ws: // localhost: 9090 . Вы должны увидеть следующее на стороне клиента —
Сервер также должен регистрировать подключенного пользователя —
Регистрация пользователя
На нашем сервере сигнализации мы будем использовать имя пользователя на основе строки для каждого соединения, чтобы мы знали, куда отправлять сообщения. Давайте немного изменим наш обработчик соединения —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
Таким образом, мы принимаем только сообщения JSON. Далее нам нужно где-то хранить всех подключенных пользователей. Мы будем использовать простой объект Javascript для него. Изменить верх нашего файла —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
Мы собираемся добавить поле типа для каждого сообщения от клиента. Например, если пользователь хочет войти, он отправляет сообщение типа входа . Давайте определим это —
connection.on('message', function(message){ var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } });
Если пользователь отправляет сообщение с типом логина , мы —
-
Проверьте, если кто-то уже вошел в систему с этим именем пользователя
-
Если это так, то сообщите пользователю, что он не вошел в систему
-
Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
-
Если команда не распознана, мы отправляем ошибку.
Проверьте, если кто-то уже вошел в систему с этим именем пользователя
Если это так, то сообщите пользователю, что он не вошел в систему
Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
Если команда не распознана, мы отправляем ошибку.
Следующий код является вспомогательной функцией для отправки сообщений в соединение. Добавьте его в файл server.js —
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Вышеуказанная функция гарантирует, что все наши сообщения отправляются в формате JSON.
Когда пользователь отключается, мы должны очистить его соединение. Мы можем удалить пользователя при возникновении события закрытия . Добавьте следующий код в обработчик соединения —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
Теперь давайте проверим наш сервер командой login. Помните, что все сообщения должны быть закодированы в формате JSON. Запустите наш сервер и попробуйте войти. Вы должны увидеть что-то вроде этого —
Звонить
После успешного входа в систему пользователь хочет позвонить другому. Он должен сделать предложение другому пользователю для достижения этого. Добавить обработчик предложения —
case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null){ //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break;
Во-первых, мы получаем соединение с пользователем, которого пытаемся вызвать. Если он существует, мы отправляем ему детали предложения . Мы также добавляем otherName к объекту соединения . Это сделано для простоты поиска позже.
Ответ
Ответ на ответ имеет аналогичную схему, которую мы использовали в обработчике предложений . Наш сервер просто проходит через все сообщения как ответ другому пользователю. Добавьте следующий код после предложения Hander —
case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break;
Вы можете увидеть, как это похоже на обработчик предложений . Обратите внимание, что этот код следует за функциями createOffer и createAnswer объекта RTCPeerConnection .
Теперь мы можем проверить наш механизм предложения / ответа. Соедините двух клиентов одновременно и попробуйте сделать предложение и ответить. Вы должны увидеть следующее —
В этом примере предложение и ответ являются простыми строками, но в реальном приложении они будут заполнены данными SDP.
ЛЕД Кандидаты
Заключительная часть — обработка кандидата ICE между пользователями. Мы используем ту же технику, просто передавая сообщения между пользователями. Основное отличие состоит в том, что сообщения-кандидаты могут появляться несколько раз на пользователя в любом порядке. Добавьте обработчик кандидата —
case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break;
Он должен работать аналогично обработчикам предложений и ответов .
Оставляя связь
Чтобы позволить нашим пользователям отключаться от другого пользователя, мы должны реализовать функцию зависания. Он также скажет серверу удалить все пользовательские ссылки. Добавьте обработчик отпуска —
case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break;
Это также отправит другому пользователю событие ухода, чтобы он мог соответствующим образом отключить одноранговое соединение. Также следует разобраться со случаем, когда пользователь сбрасывает соединение с сервера сигнализации. Давайте изменим наш близкий обработчик —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } });
Теперь, если соединение прекращается, наши пользователи будут отключены. Событие close будет вызвано, когда пользователь закроет окно своего браузера, пока мы все еще в состоянии предложения , ответа или состояния кандидата .
Полный сигнальный сервер
Вот весь код нашего сигнального сервера —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {}; //when a user connects to our sever wss.on('connection', function(connection) { console.log("User connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; default: sendTo(connection, { type: "error", message: "Command not found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); connection.send("Hello world"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Итак, работа выполнена, и наш сервер сигнализации готов. Помните, что неправильная работа при подключении к WebRTC может вызвать проблемы.
Резюме
В этой главе мы создали простой и понятный сервер сигнализации. Мы прошли через процесс сигнализации, регистрации пользователей и механизма предложения / ответа. Мы также реализовали отправку кандидатов между пользователями.
WebRTC — поддержка браузера
Сеть движется так быстро и постоянно совершенствуется. Новые стандарты создаются каждый день. Браузеры позволяют устанавливать обновления без ведома пользователя, поэтому вы должны следить за тем, что происходит в мире Интернета и WebRTC. Вот обзор того, что это до сегодняшнего дня.
Поддержка браузера
Каждый браузер не имеет одинаковых функций WebRTC одновременно. Различные браузеры могут быть впереди, поэтому некоторые функции WebRTC работают в одном браузере, а не в другом. Текущая поддержка WebRTC в браузере показана на следующем рисунке.
Вы можете проверить актуальный статус поддержки WebRTC на http://caniuse.com/#feat=rtcpeerconnection.
Chrome, Firefox и Opera
Последние версии Chrome, Firefox и Opera для основных операционных систем ПК, таких как Mac OS X, Windows и Linux, поддерживают WebRTC «из коробки». И самое главное, инженеры из команд разработчиков Chrome и Firefox работали вместе над устранением проблем, чтобы эти два браузера могли легко общаться друг с другом.
ОС Android
В операционных системах Android приложения WebRTC для Chrome и Firefox должны работать «из коробки». Они могут работать с другими браузерами после версии Android Ice Cream Sandwich (4.0). Это связано с совместным использованием кода для настольных и мобильных версий.
яблоко
Apple еще не объявила о своих планах поддержки WebRTC в Safari на OS X. Один из возможных обходных путей для гибридных нативных приложений iOS заключается в том, чтобы внедрить код WebRTC непосредственно в приложение и загрузить это приложение в WebView.
Internet Explorer
Microsoft не поддерживает WebRTC на настольных компьютерах. Но они официально подтвердили, что собираются внедрить ORTC (Object Realtime Communications) в будущих версиях IE (Edge). Они не планируют поддерживать WebRTC 1.0. Они пометили свой ORTC как WebRTC 1.1, хотя это просто улучшение сообщества, а не официальный стандарт. Недавно они добавили поддержку ORTC в последнюю версию Microsoft Edge. Вы можете узнать больше на https://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoftedge/.
Резюме
Обратите внимание, что WebRTC — это набор API и протоколов, а не один API. Поддержка каждого из них развивается в разных браузерах и операционных системах на разных уровнях. Отличный способ проверить последний уровень поддержки — через http://canisue.com. Он отслеживает принятие современных API через несколько браузеров. Вы также можете найти последнюю информацию о поддержке браузеров, а также демонстрации WebRTC по адресу http://www.webrtc.org, которая поддерживается Mozilla, Google и Opera.
WebRTC — мобильная поддержка
В мобильном мире поддержка WebRTC не на том же уровне, как на настольных ПК. У мобильных устройств свой путь, поэтому на мобильных платформах WebRTC также отличается от других.
При разработке приложения WebRTC для настольных компьютеров мы рассматриваем возможность использования Chrome, Firefox или Opera. Все они поддерживают WebRTC из коробки. В общем, вам просто нужен браузер, а не беспокоиться об оборудовании настольного компьютера.
В мобильном мире сегодня существует три возможных режима для WebRTC —
- Родное приложение
- Приложение браузера
- Родной браузер
Android
В 2013 году веб-браузер Firefox для Android был представлен с поддержкой WebRTC из коробки. Теперь вы можете совершать видеозвонки на устройствах Android с помощью мобильного браузера Firefox.
Он имеет три основных компонента WebRTC —
-
PeerConnection — разрешает звонки между браузерами
-
getUserMedia — предоставляет доступ к камере и микрофону
-
DataChannels — обеспечивает одноранговую передачу данных
PeerConnection — разрешает звонки между браузерами
getUserMedia — предоставляет доступ к камере и микрофону
DataChannels — обеспечивает одноранговую передачу данных
Google Chrome для Android также обеспечивает поддержку WebRTC. Как вы уже заметили, наиболее интересные функции обычно появляются в Chrome.
В прошлом году мобильный браузер Opera появился с поддержкой WebRTC. Так что для Android у вас есть Chrome, Firefox и Opera. Другие браузеры не поддерживают WebRTC.
IOS
К сожалению, WebRTC сейчас не поддерживается на iOS. Хотя WebRTC хорошо работает на Mac при использовании Firefox, Opera или Chrome, он не поддерживается на iOS.
В настоящее время ваше приложение WebRTC не будет работать на мобильных устройствах Apple «из коробки». Но есть браузер — Bowser. Это веб-браузер, разработанный компанией Ericsson, и он поддерживает WebRTC из коробки. Вы можете проверить его домашнюю страницу по адресу http://www.openwebrtc.org/bowser/.
На сегодняшний день это единственный дружественный способ поддержки вашего приложения WebRTC на iOS. Другой способ — разработать собственное приложение самостоятельно.
Телефоны Windows
Microsoft не поддерживает WebRTC на мобильных платформах. Но они официально подтвердили, что собираются реализовать ORTC (Object Realtime Communications) в будущих версиях IE. Они не планируют поддерживать WebRTC 1.0. Они пометили свой ORTC как WebRTC 1.1, хотя это просто улучшение сообщества, а не официальный стандарт.
Таким образом, сегодня пользователи Window Phone не могут использовать приложения WebRTC, и нет никакой возможности справиться с этой ситуацией.
ежевика
Приложения WebRTC также не поддерживаются на Blackberry.
Использование собственного WebRTC-браузера
Наиболее удобный и удобный вариант использования WebRTC для пользователей — использование встроенного браузера устройства. В этом случае устройство готово к работе с любыми дополнительными конфигурациями.
На сегодняшний день эту функцию предоставляют только устройства Android версии 4 или выше. Apple по-прежнему не проявляет активности с поддержкой WebRTC. Поэтому пользователи Safari не могут использовать приложения WebRTC. Microsoft также не представила его в Windows Phone 8.
Использование WebRTC через браузерные приложения
Это означает использование сторонних приложений (не нативных веб-браузеров) для предоставления функций WebRTC. На данный момент существует два таких сторонних приложения. Bowser, который является единственным способом перенести функции WebRTC на устройства iOS и Opera, которая является хорошей альтернативой для платформы Android. Остальные доступные мобильные браузеры не поддерживают WebRTC.
Собственные мобильные приложения
Как вы видите, WebRTC пока не имеет большой поддержки в мобильном мире. Таким образом, одним из возможных решений является разработка собственных приложений, использующих API WebRTC. Но это не лучший выбор, потому что основная функция WebRTC — это кроссплатформенное решение. Во всяком случае, в некоторых случаях это единственный способ, потому что нативное приложение может использовать специфичные для устройства функции или функции, которые не поддерживаются браузерами HTML5.
Ограничение видеопотока для мобильных и настольных устройств
Первый параметр API getUserMedia ожидает объект ключей и значений, указывающих браузеру, как обрабатывать потоки. Вы можете проверить полный набор ограничений по адресу https://tools.ietf.org/html/draft-alvestrand-constraints-resolution-03. Вы можете настроить соотношение сторон видео, frameRate и другие дополнительные параметры.
Поддержка мобильных устройств является одной из самых больших проблем, потому что мобильные устройства имеют ограниченное пространство экрана и ограниченные ресурсы. Возможно, вы захотите, чтобы мобильное устройство захватывало только видеопоток с разрешением 480×320 или меньше для экономии энергии и пропускной способности. Использование строки пользовательского агента в браузере — хороший способ проверить, находится ли пользователь на мобильном устройстве или нет. Давайте посмотрим на пример. Создайте файл index.html —
<!DOCTYPE html> <html lang = "en"> <head> <meta charset = "utf-8" /> </head> <body> <video autoplay></video> <script src = "client.js"></script> </body> </html>
Затем создайте следующий файл client.js —
//constraints for desktop browser var desktopConstraints = { video: { mandatory: { maxWidth:800, maxHeight:600 } }, audio: true }; //constraints for mobile browser var mobileConstraints = { video: { mandatory: { maxWidth: 480, maxHeight: 320, } }, audio: true } //if a user is using a mobile browser if(/Android|iPhone|iPad/i.test(navigator.userAgent)) { var constraints = mobileConstraints; } else { var constraints = desktopConstraints; } function hasUserMedia() { //check if the browser supports the WebRTC return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia); } if (hasUserMedia()) { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; //enabling video and audio channels navigator.getUserMedia(constraints, function (stream) { var video = document.querySelector('video'); //inserting our stream to the video tag video.src = window.URL.createObjectURL(stream); }, function (err) {}); } else { alert("WebRTC is not supported"); }
Запустите веб-сервер с помощью статической команды и откройте страницу. Вы должны увидеть это 800×600. Затем откройте эту страницу в мобильном окне просмотра с помощью инструментов Chrome и проверьте разрешение. Это должно быть 480×320.
Ограничения — это самый простой способ повысить производительность вашего приложения WebRTC.
Резюме
В этой главе мы узнали о проблемах, которые могут возникнуть при разработке приложений WebRTC для мобильных устройств. Мы обнаружили различные ограничения поддержки API WebRTC на мобильных платформах. Мы также запустили демонстрационное приложение, в котором мы установили различные ограничения для настольных и мобильных браузеров.
WebRTC — видео демо
В этой главе мы собираемся создать клиентское приложение, которое позволит двум пользователям на разных устройствах общаться через WebRTC. Наше приложение будет иметь две страницы. Один для входа в систему, а другой для вызова другого пользователя.
Две страницы будут тегами div . Большая часть ввода осуществляется через простые обработчики событий.
Сигнальный сервер
Чтобы создать соединение WebRTC, клиенты должны иметь возможность передавать сообщения без использования однорангового соединения WebRTC. Здесь мы будем использовать HTML5 WebSockets — двунаправленное сокетное соединение между двумя конечными точками — веб-сервером и веб-браузером. Теперь давайте начнем использовать библиотеку WebSocket. Создайте файл server.js и вставьте следующий код —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message) { console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
Первая строка требует библиотеки WebSocket, которую мы уже установили. Затем мы создаем сервер сокетов на порту 9090. Затем мы прослушиваем событие подключения . Этот код будет выполнен, когда пользователь устанавливает соединение WebSocket с сервером. Затем мы слушаем любые сообщения, отправленные пользователем. Наконец, мы отправляем ответ подключенному пользователю, говоря «Привет с сервера».
На нашем сервере сигнализации мы будем использовать имя пользователя на основе строки для каждого соединения, чтобы мы знали, куда отправлять сообщения. Давайте немного изменим наш обработчик соединения —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
Таким образом, мы принимаем только сообщения JSON. Далее нам нужно где-то хранить всех подключенных пользователей. Мы будем использовать простой объект Javascript для него. Изменить верх нашего файла —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
Мы собираемся добавить поле типа для каждого сообщения от клиента. Например, если пользователь хочет войти, он отправляет сообщение типа входа . Давайте определим это —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } });
Если пользователь отправляет сообщение с типом логина , мы —
-
Проверьте, если кто-то уже вошел в систему с этим именем пользователя
-
Если это так, то сообщите пользователю, что он не вошел в систему
-
Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
-
Если команда не распознана, мы отправляем ошибку.
Проверьте, если кто-то уже вошел в систему с этим именем пользователя
Если это так, то сообщите пользователю, что он не вошел в систему
Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
Если команда не распознана, мы отправляем ошибку.
Следующий код является вспомогательной функцией для отправки сообщений в соединение. Добавьте его в файл server.js —
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Когда пользователь отключается, мы должны очистить его соединение. Мы можем удалить пользователя при возникновении события закрытия . Добавьте следующий код в обработчик соединения —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
После успешного входа в систему пользователь хочет позвонить другому. Он должен сделать предложение другому пользователю для достижения этого. Добавить обработчик предложения —
case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break;
Во-первых, мы получаем соединение с пользователем, которого пытаемся вызвать. Если он существует, мы отправляем ему детали предложения . Мы также добавляем otherName к объекту соединения . Это сделано для простоты поиска позже.
Ответ на ответ имеет аналогичную схему, которую мы использовали в обработчике предложений . Наш сервер просто проходит через все сообщения как ответ другому пользователю. Добавьте следующий код после обработчика предложения —
case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break;
Заключительная часть — обработка кандидата ICE между пользователями. Мы используем ту же технику, просто передавая сообщения между пользователями. Основное отличие состоит в том, что сообщения-кандидаты могут появляться несколько раз на пользователя в любом порядке. Добавьте обработчик кандидата —
case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break;
Чтобы позволить нашим пользователям отключаться от другого пользователя, мы должны реализовать функцию зависания. Он также скажет серверу удалить все пользовательские ссылки. Добавьте обработчик отпуска —
case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break;
Это также отправит другому пользователю событие ухода, чтобы он мог соответствующим образом отключить одноранговое соединение. Также следует разобраться со случаем, когда пользователь сбрасывает соединение с сервера сигнализации. Давайте изменим наш близкий обработчик —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } });
Ниже приведен полный код нашего сервера сигнализации.
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {}; //when a user connects to our sever wss.on('connection', function(connection) { console.log("User connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; default: sendTo(connection, { type: "error", message: "Command not found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); connection.send("Hello world"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Клиентское приложение
Один из способов проверить это приложение — открыть две вкладки браузера и попытаться вызвать друг друга.
Прежде всего, нам нужно установить библиотеку начальной загрузки . Bootstrap — это интерфейс для разработки веб-приложений. Вы можете узнать больше на http://getbootstrap.com/. Создайте папку с именем, например, «видеочат». Это будет наша корневая папка приложения. Внутри этой папки создайте файл package.json (он необходим для управления зависимостями npm) и добавьте следующее:
{ "name": "webrtc-videochat", "version": "0.1.0", "description": "webrtc-videochat", "author": "Author", "license": "BSD-2-Clause" }
Затем запустите npm install bootstrap . Это установит библиотеку начальной загрузки в папку videochat / node_modules .
Теперь нам нужно создать простую HTML-страницу. Создайте файл index.html в корневой папке со следующим кодом —
<html> <head> <title>WebRTC Video Demo</title> <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> </head> <style> body { background: #eee; padding: 5% 0; } video { background: black; border: 1px solid gray; } .call-page { position: relative; display: block; margin: 0 auto; width: 500px; height: 500px; } #localVideo { width: 150px; height: 150px; position: absolute; top: 15px; right: 15px; } #remoteVideo { width: 500px; height: 500px; } </style> <body> <div id = "loginPage" class = "container text-center"> <div class = "row"> <div class = "col-md-4 col-md-offset-4"> <h2>WebRTC Video Demo. Please sign in</h2> <label for = "usernameInput" class = "sr-only">Login</label> <input type = "email" id = "usernameInput" c lass = "form-control formgroup" placeholder = "Login" required = "" autofocus = ""> <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"> Sign in</button> </div> </div> </div> <div id = "callPage" class = "call-page"> <video id = "localVideo" autoplay></video> <video id = "remoteVideo" autoplay></video> <div class = "row text-center"> <div class = "col-md-12"> <input id = "callToUsernameInput" type = "text" placeholder = "username to call" /> <button id = "callBtn" class = "btn-success btn">Call</button> <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> </div> </div> </div> <script src = "client.js"></script> </body> </html>
Эта страница должна быть вам знакома. Мы добавили файл начальной загрузки css. Мы также определили две страницы. Наконец, мы создали несколько текстовых полей и кнопок для получения информации от пользователя. Вы должны увидеть два видеоэлемента для локальных и удаленных видеопотоков. Обратите внимание, что мы добавили ссылку на файл client.js .
Теперь нам нужно установить соединение с нашим сервером сигнализации. Создайте файл client.js в корневой папке со следующим кодом —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); };
Теперь запустите наш сервер сигнализации через сервер узлов . Затем внутри корневой папки запустите статическую команду и откройте страницу в браузере. Вы должны увидеть следующий вывод консоли —
Следующим шагом является реализация входа пользователя с уникальным именем пользователя. Мы просто отправляем имя пользователя на сервер, который затем сообщает нам, занято оно или нет. Добавьте следующий код в ваш файл client.js —
//****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); //hide call page callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; if (name.length > 0) { send({ type: "login", name: name }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { //display the call page if login is successful loginPage.style.display = "none"; callPage.style.display = "block"; //start peer connection } };
Во-первых, мы выбираем несколько ссылок на элементы на странице. Мы скрываем страницу вызова. Затем мы добавляем прослушиватель событий на кнопку входа. Когда пользователь щелкает по нему, мы отправляем его имя пользователя на сервер. Наконец, мы реализуем обратный вызов handleLogin. Если вход был успешным, мы показываем страницу вызова и начинаем устанавливать одноранговое соединение.
Чтобы начать одноранговое соединение нам нужно —
- Получить поток с веб-камеры.
- Создайте объект RTCPeerConnection.
Добавьте следующий код в «блок селекторов пользовательского интерфейса» —
var localVideo = document.querySelector('#localVideo'); var remoteVideo = document.querySelector('#remoteVideo'); var yourConn; var stream;
Изменить функцию handleLogin —
function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //getting local video stream navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) { stream = myStream; //displaying local video stream on the page localVideo.src = window.URL.createObjectURL(stream); //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration); // setup stream listening yourConn.addStream(stream); //when a remote user adds stream to the peer connection, we display it yourConn.onaddstream = function (e) { remoteVideo.src = window.URL.createObjectURL(e.stream); }; // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; }, function (error) { console.log(error); }); } };
Теперь, если вы запустите код, страница должна позволить вам войти в систему и отобразить ваш локальный видеопоток на странице.
Теперь мы готовы инициировать вызов. Сначала мы отправляем предложение другому пользователю. Как только пользователь получает предложение, он создает ответ и начинает торговать кандидатами ICE. Добавьте следующий код в файл client.js —
//initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); };
Мы добавляем обработчик кликов к кнопке Call, которая инициирует предложение. Затем мы реализуем несколько обработчиков, ожидаемых обработчиком onmessage . Они будут обрабатываться асинхронно, пока оба пользователя не установят соединение.
Последний шаг — реализация функции зависания. Это прекратит передачу данных и скажет другому пользователю закрыть вызов. Добавьте следующий код —
//hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; remoteVideo.src = null; yourConn.close(); yourConn.onicecandidate = null; yourConn.onaddstream = null; };
Когда пользователь нажимает кнопку Hang Up —
- Он отправит сообщение «оставить» другому пользователю
- Это закроет RTCPeerConnection и уничтожит соединение локально
Теперь запустите код. Вы должны иметь возможность войти на сервер, используя две вкладки браузера. Затем вы можете позвонить на вкладку и повесить трубку.
Ниже приведен весь файл client.js —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); }; //****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); var localVideo = document.querySelector('#localVideo'); var remoteVideo = document.querySelector('#remoteVideo'); var yourConn; var stream; callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; if (name.length > 0) { send({ type: "login", name: name }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //getting local video stream navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) { stream = myStream; //displaying local video stream on the page localVideo.src = window.URL.createObjectURL(stream); //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration); // setup stream listening yourConn.addStream(stream); //when a remote user adds stream to the peer connection, we display it yourConn.onaddstream = function (e) { remoteVideo.src = window.URL.createObjectURL(e.stream); }; // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; }, function (error) { console.log(error); }); } }; //initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); }; //hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; remoteVideo.src = null; yourConn.close(); yourConn.onicecandidate = null; yourConn.onaddstream = null; };
Резюме
Эта демонстрация предоставляет базовый набор функций, которые нужны каждому приложению WebRTC. Чтобы улучшить эту демонстрацию, вы можете добавить идентификацию пользователя с помощью таких платформ, как Facebook или Google, обрабатывать ввод пользователя для неверных данных. Кроме того, соединение WebRTC может прерваться из-за нескольких причин, таких как не поддержка технологии или неспособность пройти через брандмауэры. Работа над тем, чтобы сделать любое приложение WebRTC стабильным.
WebRTC — Голосовая демонстрация
В этой главе мы собираемся создать клиентское приложение, которое позволит двум пользователям на отдельных устройствах общаться с помощью аудиопотоков WebRTC. Наше приложение будет иметь две страницы. Один для входа в систему, а другой для совершения аудиовызова другому пользователю.
Две страницы будут тегами div . Большая часть ввода осуществляется через простые обработчики событий.
Сигнальный сервер
Чтобы создать соединение WebRTC, клиенты должны иметь возможность передавать сообщения без использования однорангового соединения WebRTC. Здесь мы будем использовать HTML5 WebSockets — двунаправленное сокетное соединение между двумя конечными точками — веб-сервером и веб-браузером. Теперь давайте начнем использовать библиотеку WebSocket. Создайте файл server.js и вставьте следующий код —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message) { console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
Первая строка требует библиотеки WebSocket, которую мы уже установили. Затем мы создаем сервер сокетов на порту 9090. Затем мы прослушиваем событие подключения . Этот код будет выполнен, когда пользователь устанавливает соединение WebSocket с сервером. Затем мы слушаем любые сообщения, отправленные пользователем. Наконец, мы отправляем ответ подключенному пользователю, говоря «Привет с сервера».
На нашем сервере сигнализации мы будем использовать имя пользователя на основе строки для каждого соединения, чтобы мы знали, куда отправлять сообщения. Давайте немного изменим наш обработчик соединения —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
Таким образом, мы принимаем только сообщения JSON. Далее нам нужно где-то хранить всех подключенных пользователей. Мы будем использовать простой объект Javascript для него. Изменить верх нашего файла —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
Мы собираемся добавить поле типа для каждого сообщения от клиента. Например, если пользователь хочет войти, он отправляет сообщение типа входа . Давайте определим это —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } });
Если пользователь отправляет сообщение с типом логина , мы —
- Проверьте, если кто-то уже вошел в систему с этим именем пользователя.
- Если это так, то сообщите пользователю, что он не вошел в систему.
- Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
- Если команда не распознана, мы отправляем ошибку.
Следующий код является вспомогательной функцией для отправки сообщений в соединение. Добавьте его в файл server.js —
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Когда пользователь отключается, мы должны очистить его соединение. Мы можем удалить пользователя при возникновении события закрытия . Добавьте следующий код в обработчик соединения :
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
После успешного входа в систему пользователь хочет позвонить другому. Он должен сделать предложение другому пользователю для достижения этого. Добавить обработчик предложения —
case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break;
Во-первых, мы получаем соединение с пользователем, которого пытаемся вызвать. Если он существует, мы отправляем ему детали предложения . Мы также добавляем otherName к объекту соединения . Это сделано для простоты поиска позже.
Ответ на ответ имеет аналогичную схему, которую мы использовали в обработчике предложений . Наш сервер просто проходит через все сообщения как ответ другому пользователю. Добавьте следующий код после обработчика предложения —
case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break;
Заключительная часть — обработка кандидата ICE между пользователями. Мы используем ту же технику, просто передавая сообщения между пользователями. Основное отличие состоит в том, что сообщения-кандидаты могут появляться несколько раз на пользователя в любом порядке. Добавьте обработчик кандидата —
case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break;
Чтобы позволить нашим пользователям отключаться от другого пользователя, мы должны реализовать функцию зависания. Он также скажет серверу удалить все пользовательские ссылки. Добавьте обработчик отпуска —
case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break;
Это также отправит другому пользователю событие ухода, чтобы он мог соответствующим образом отключить одноранговое соединение. Также следует разобраться со случаем, когда пользователь сбрасывает соединение с сервера сигнализации. Давайте изменим наш близкий обработчик —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } });
Ниже приведен полный код нашего сервера сигнализации.
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {}; //when a user connects to our sever wss.on('connection', function(connection) { console.log("User connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; default: sendTo(connection, { type: "error", message: "Command not found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); connection.send("Hello world"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Клиентское приложение
Один из способов протестировать это приложение — открыть две вкладки браузера и попытаться сделать аудиозвонок друг другу.
Прежде всего, нам нужно установить библиотеку начальной загрузки . Bootstrap — это интерфейс для разработки веб-приложений. Вы можете узнать больше на http://getbootstrap.com/. Создайте папку с именем, например, «audiochat». Это будет наша корневая папка приложения. Внутри этой папки создайте файл package.json (он необходим для управления зависимостями npm) и добавьте следующее:
{ "name": "webrtc-audiochat", "version": "0.1.0", "description": "webrtc-audiochat", "author": "Author", "license": "BSD-2-Clause" }
Затем запустите npm install bootstrap . Это установит библиотеку начальной загрузки в папку audiochat / node_modules .
Теперь нам нужно создать простую HTML-страницу. Создайте файл index.html в корневой папке со следующим кодом —
<html> <head> <title>WebRTC Voice Demo</title> <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> </head> <style> body { background: #eee; padding: 5% 0; } </style> <body> <div id = "loginPage" class = "container text-center"> <div class = "row"> <div class = "col-md-4 col-md-offset-4"> <h2>WebRTC Voice Demo. Please sign in</h2> <label for = "usernameInput" class = "sr-only">Login</label> <input type = "email" id = "usernameInput" class = "form-control formgroup" placeholder = "Login" required = "" autofocus = ""> <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"> Sign in</button> </div> </div> </div> <div id = "callPage" class = "call-page"> <div class = "row"> <div class = "col-md-6 text-right"> Local audio: <audio id = "localAudio" controls autoplay></audio> </div> <div class = "col-md-6 text-left"> Remote audio: <audio id = "remoteAudio" controls autoplay></audio> </div> </div> <div class = "row text-center"> <div class = "col-md-12"> <input id = "callToUsernameInput" type = "text" placeholder = "username to call" /> <button id = "callBtn" class = "btn-success btn">Call</button> <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> </div> </div> </div> <script src = "client.js"></script> </body> </html>
Эта страница должна быть вам знакома. Мы добавили файл начальной загрузки css. Мы также определили две страницы. Наконец, мы создали несколько текстовых полей и кнопок для получения информации от пользователя. Вы должны увидеть два аудиоэлемента для локальных и удаленных аудиопотоков. Обратите внимание, что мы добавили ссылку на файл client.js .
Теперь нам нужно установить соединение с нашим сервером сигнализации. Создайте файл client.js в корневой папке со следующим кодом —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); };
Теперь запустите наш сервер сигнализации через сервер узлов . Затем внутри корневой папки запустите статическую команду и откройте страницу в браузере. Вы должны увидеть следующий вывод консоли —
Следующим шагом является реализация входа пользователя с уникальным именем пользователя. Мы просто отправляем имя пользователя на сервер, который затем сообщает нам, занято оно или нет. Добавьте следующий код в ваш файл client.js —
//****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; if (name.length > 0) { send({ type: "login", name: name }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** } };
Во-первых, мы выбираем несколько ссылок на элементы на странице. Мы скрываем страницу вызова. Затем мы добавляем прослушиватель событий на кнопку входа. Когда пользователь щелкает по нему, мы отправляем его имя пользователя на сервер. Наконец, мы реализуем обратный вызов handleLogin. Если вход был успешным, мы показываем страницу вызова и начинаем устанавливать одноранговое соединение.
Чтобы начать одноранговое соединение нам нужно —
- Получить аудиопоток с микрофона
- Создайте объект RTCPeerConnection
Добавьте следующий код в «блок селекторов пользовательского интерфейса» —
var localAudio = document.querySelector('#localAudio'); var remoteAudio = document.querySelector('#remoteAudio'); var yourConn; var stream;
Изменить функцию handleLogin —
function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //getting local audio stream navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) { stream = myStream; //displaying local audio stream on the page localAudio.src = window.URL.createObjectURL(stream); //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration); // setup stream listening yourConn.addStream(stream); //when a remote user adds stream to the peer connection, we display it yourConn.onaddstream = function (e) { remoteAudio.src = window.URL.createObjectURL(e.stream); }; // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", }); } }; }, function (error) { console.log(error); }); } };
Теперь, если вы запустите код, страница должна позволить вам войти в систему и отобразить ваш локальный аудиопоток на странице.
Теперь мы готовы инициировать вызов. Сначала мы отправляем предложение другому пользователю. Как только пользователь получает предложение, он создает ответ и начинает торговать кандидатами ICE. Добавьте следующий код в файл client.js —
//initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); };
Мы добавляем обработчик кликов к кнопке Call, которая инициирует предложение. Затем мы реализуем несколько обработчиков, ожидаемых обработчиком onmessage . Они будут обрабатываться асинхронно, пока оба пользователя не установят соединение.
Последний шаг — реализация функции зависания. Это прекратит передачу данных и скажет другому пользователю закрыть вызов. Добавьте следующий код —
//hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; remoteAudio.src = null; yourConn.close(); yourConn.onicecandidate = null; yourConn.onaddstream = null; };
Когда пользователь нажимает кнопку Hang Up —
- Он отправит сообщение «оставить» другому пользователю
- Это закроет RTCPeerConnection и уничтожит соединение локально
Теперь запустите код. Вы должны иметь возможность войти на сервер, используя две вкладки браузера. Затем вы можете сделать аудиозвонок на вкладку и повесить трубку.
Ниже приведен весь файл client.js —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); }; //****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); var localAudio = document.querySelector('#localAudio'); var remoteAudio = document.querySelector('#remoteAudio'); var yourConn; var stream; callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; if (name.length > 0) { send({ type: "login", name: name }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //getting local audio stream navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) { stream = myStream; //displaying local audio stream on the page localAudio.src = window.URL.createObjectURL(stream); //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration); // setup stream listening yourConn.addStream(stream); //when a remote user adds stream to the peer connection, we display it yourConn.onaddstream = function (e) { remoteAudio.src = window.URL.createObjectURL(e.stream); }; // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; }, function (error) { console.log(error); }); } }; //initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); }; //hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; remoteAudio.src = null; yourConn.close(); yourConn.onicecandidate = null; yourConn.onaddstream = null; };
WebRTC — текстовая демонстрация
В этой главе мы собираемся создать клиентское приложение, которое позволяет двум пользователям на отдельных устройствах отправлять сообщения друг другу с помощью WebRTC. Наше приложение будет иметь две страницы. Один для входа в систему, а другой для отправки сообщений другому пользователю.
Две страницы будут тегами div . Большая часть ввода осуществляется через простые обработчики событий.
Сигнальный сервер
Чтобы создать соединение WebRTC, клиенты должны иметь возможность передавать сообщения без использования однорангового соединения WebRTC. Здесь мы будем использовать HTML5 WebSockets — двунаправленное сокетное соединение между двумя конечными точками — веб-сервером и веб-браузером. Теперь давайте начнем использовать библиотеку WebSocket. Создайте файл server.js и вставьте следующий код —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message) { console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
Первая строка требует библиотеки WebSocket, которую мы уже установили. Затем мы создаем сервер сокетов на порту 9090. Затем мы прослушиваем событие подключения . Этот код будет выполнен, когда пользователь устанавливает соединение WebSocket с сервером. Затем мы слушаем любые сообщения, отправленные пользователем. Наконец, мы отправляем ответ подключенному пользователю, говоря «Привет с сервера».
На нашем сервере сигнализации мы будем использовать имя пользователя на основе строки для каждого соединения, чтобы мы знали, куда отправлять сообщения. Давайте немного изменим наш обработчик соединения —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
Таким образом, мы принимаем только сообщения JSON. Далее нам нужно где-то хранить всех подключенных пользователей. Мы будем использовать простой объект Javascript для него. Изменить верх нашего файла —
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
Мы собираемся добавить поле типа для каждого сообщения от клиента. Например, если пользователь хочет войти, он отправляет сообщение типа входа . Давайте определим это —
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } });
Если пользователь отправляет сообщение с типом логина , мы —
- Проверьте, если кто-то уже вошел в систему с этим именем пользователя.
- Если это так, то сообщите пользователю, что он не вошел в систему.
- Если никто не использует это имя пользователя, мы добавляем имя пользователя в качестве ключа к объекту подключения.
- Если команда не распознана, мы отправляем ошибку.
Следующий код является вспомогательной функцией для отправки сообщений в соединение. Добавьте его в файл server.js —
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Когда пользователь отключается, мы должны очистить его соединение. Мы можем удалить пользователя при возникновении события закрытия . Добавьте следующий код в обработчик соединения —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
После успешного входа в систему пользователь хочет позвонить другому. Он должен сделать предложение другому пользователю для достижения этого. Добавить обработчик предложения —
case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null){ //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); break;
Во-первых, мы получаем соединение с пользователем, которого пытаемся вызвать. Если он существует, мы отправляем ему детали предложения . Мы также добавляем otherName к объекту соединения . Это сделано для простоты поиска позже.
Ответ на ответ имеет аналогичную схему, которую мы использовали в обработчике предложений . Наш сервер просто проходит через все сообщения как ответ другому пользователю. Добавьте следующий код после обработчика предложения —
case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break;
Заключительная часть — обработка кандидата ICE между пользователями. Мы используем ту же технику, просто передавая сообщения между пользователями. Основное отличие состоит в том, что сообщения-кандидаты могут появляться несколько раз на пользователя в любом порядке. Добавьте обработчик кандидата —
case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break;
Чтобы позволить нашим пользователям отключаться от другого пользователя, мы должны реализовать функцию зависания. Он также скажет серверу удалить все пользовательские ссылки. Добавьте обработчик отпуска —
case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break;
Это также отправит другому пользователю событие ухода, чтобы он мог соответствующим образом отключить одноранговое соединение. Также следует разобраться со случаем, когда пользователь сбрасывает соединение с сервера сигнализации. Давайте изменим наш близкий обработчик —
connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } });
Ниже приведен полный код нашего сервера сигнализации.
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {}; //when a user connects to our sever wss.on('connection', function(connection) { console.log("User connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; default: sendTo(connection, { type: "error", message: "Command not found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); connection.send("Hello world"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Клиентское приложение
Один из способов проверить это приложение — открыть две вкладки браузера и попытаться отправить сообщение друг другу.
Прежде всего, нам нужно установить библиотеку начальной загрузки . Bootstrap — это интерфейс для разработки веб-приложений. Вы можете узнать больше на http://getbootstrap.com/. Создайте папку с именем, например, «textchat». Это будет наша корневая папка приложения. Внутри этой папки создайте файл package.json (он необходим для управления зависимостями npm) и добавьте следующее:
{ "name": "webrtc-textochat", "version": "0.1.0", "description": "webrtc-textchat", "author": "Author", "license": "BSD-2-Clause" }
Затем запустите npm install bootstrap . Это установит библиотеку начальной загрузки в папку textchat / node_modules .
Теперь нам нужно создать простую HTML-страницу. Создайте файл index.html в корневой папке со следующим кодом —
<html> <head> <title>WebRTC Text Demo</title> <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> </head> <style> body { background: #eee; padding: 5% 0; } </style> <body> <div id = "loginPage" class = "container text-center"> <div class = "row"> <div class = "col-md-4 col-md-offset-4"> <h2>WebRTC Text Demo. Please sign in</h2> <label for = "usernameInput" class = "sr-only">Login</label> <input type = "email" id = "usernameInput" class = "form-control formgroup" placeholder = "Login" required = "" autofocus = ""> <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"> Sign in</button> </div> </div> </div> <div id = "callPage" class = "call-page container"> <div class = "row"> <div class = "col-md-4 col-md-offset-4 text-center"> <div class = "panel panel-primary"> <div class = "panel-heading">Text chat</div> <div id = "chatarea" class = "panel-body text-left"></div> </div> </div> </div> <div class = "row text-center form-group"> <div class = "col-md-12"> <input id = "callToUsernameInput" type = "text" placeholder = "username to call" /> <button id = "callBtn" class = "btn-success btn">Call</button> <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> </div> </div> <div class = "row text-center"> <div class = "col-md-12"> <input id = "msgInput" type = "text" placeholder = "message" /> <button id = "sendMsgBtn" class = "btn-success btn">Send</button> </div> </div> </div> <script src = "client.js"></script> </body> </html>
Эта страница должна быть вам знакома. Мы добавили файл начальной загрузки css. Мы также определили две страницы. Наконец, мы создали несколько текстовых полей и кнопок для получения информации от пользователя. На странице «чат» вы должны увидеть тег div с идентификатором «chatarea», где будут отображаться все наши сообщения. Обратите внимание, что мы добавили ссылку на файл client.js .
Теперь нам нужно установить соединение с нашим сервером сигнализации. Создайте файл client.js в корневой папке со следующим кодом —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); };
Теперь запустите наш сервер сигнализации через сервер узлов . Затем внутри корневой папки запустите статическую команду и откройте страницу в браузере. Вы должны увидеть следующий вывод консоли —
Следующим шагом является реализация входа пользователя с уникальным именем пользователя. Мы просто отправляем имя пользователя на сервер, который затем сообщает нам, занято оно или нет. Добавьте следующий код в ваш файл client.js —
//****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; if (name.length > 0) { send({ type: "login", name: name }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** } };
Во-первых, мы выбираем несколько ссылок на элементы на странице. Мы скрываем страницу вызова. Затем мы добавляем прослушиватель событий на кнопку входа. Когда пользователь щелкает по нему, мы отправляем его имя пользователя на сервер. Наконец, мы реализуем обратный вызов handleLogin. Если вход был успешным, мы показываем страницу вызова, устанавливаем одноранговое соединение и создаем канал данных.
Чтобы начать одноранговое соединение с каналом данных нам нужно —
- Создайте объект RTCPeerConnection
- Создайте канал данных внутри нашего объекта RTCPeerConnection
Добавьте следующий код в «блок селекторов пользовательского интерфейса» —
var msgInput = document.querySelector('#msgInput'); var sendMsgBtn = document.querySelector('#sendMsgBtn'); var chatArea = document.querySelector('#chatarea'); var yourConn; var dataChannel;
Изменить функцию handleLogin —
function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; //creating data channel dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); dataChannel.onerror = function (error) { console.log("Ooops...error:", error); }; //when we receive a message from the other peer, display it on the screen dataChannel.onmessage = function (event) { chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; }; dataChannel.onclose = function () { console.log("data channel is closed"); }; } };
Если вход был успешным, приложение создает объект RTCPeerConnection и настраивает обработчик onicecandidate, который отправляет все найденные icecandidates другому партнеру. Он также создает канал данных. Обратите внимание, что при создании объекта RTCPeerConnection второй аргумент в конструкторе необязательный: [{RtpDataChannels: true}] является обязательным, если вы используете Chrome или Opera. Следующим шагом является создание предложения для другого партнера. Как только пользователь получает предложение, он создает ответ и начинает торговать кандидатами ICE. Добавьте следующий код в файл client.js —
//initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); };
Мы добавляем обработчик кликов к кнопке Call, которая инициирует предложение. Затем мы реализуем несколько обработчиков, ожидаемых обработчиком onmessage . Они будут обрабатываться асинхронно, пока оба пользователя не установят соединение.
Следующим шагом является реализация функции зависания. Это прекратит передачу данных и скажет другому пользователю закрыть канал данных. Добавьте следующий код —
//hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; yourConn.close(); yourConn.onicecandidate = null; };
Когда пользователь нажимает кнопку Hang Up —
- Он отправит сообщение «оставить» другому пользователю.
- Он закроет RTCPeerConnection и канал данных.
Последний шаг — отправка сообщения другому узлу. Добавьте обработчик «click» к кнопке «send message» —
//when user clicks the "send message" button sendMsgBtn.addEventListener("click", function (event) { var val = msgInput.value; chatArea.innerHTML += name + ": " + val + "<br />"; //sending a message to a connected peer dataChannel.send(val); msgInput.value = ""; });
Теперь запустите код. Вы должны иметь возможность войти на сервер, используя две вкладки браузера. Затем вы можете установить одноранговое соединение с другим пользователем и отправить ему сообщение, а также закрыть канал данных, нажав кнопку «Отбой».
Ниже приведен весь файл client.js —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); }; //****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); var msgInput = document.querySelector('#msgInput'); var sendMsgBtn = document.querySelector('#sendMsgBtn'); var chatArea = document.querySelector('#chatarea'); var yourConn; var dataChannel; callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; if (name.length > 0) { send({ type: "login", name: name }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...try a different username"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; //creating data channel dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); dataChannel.onerror = function (error) { console.log("Ooops...error:", error); }; //when we receive a message from the other peer, display it on the screen dataChannel.onmessage = function (event) { chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; }; dataChannel.onclose = function () { console.log("data channel is closed"); }; } }; //initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); }; //hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; yourConn.close(); yourConn.onicecandidate = null; }; //when user clicks the "send message" button sendMsgBtn.addEventListener("click", function (event) { var val = msgInput.value; chatArea.innerHTML += name + ": " + val + "<br />"; //sending a message to a connected peer dataChannel.send(val); msgInput.value = ""; });
WebRTC — Безопасность
В этой главе мы собираемся добавить функции безопасности на сервер сигнализации, который мы создали в главе «Сигнализация WebRTC». Там будет два улучшения —
- Аутентификация пользователя с использованием базы данных Redis
- Включение безопасного сокета
Во-первых, вы должны установить Redis.
-
Загрузите последнюю стабильную версию с http://redis.io/download (3,05 в моем случае)
-
Распаковать его
-
Внутри загруженной папки запустите sudo make install
-
После завершения установки запустите make test, чтобы проверить, все ли работает правильно.
Загрузите последнюю стабильную версию с http://redis.io/download (3,05 в моем случае)
Распаковать его
Внутри загруженной папки запустите sudo make install
После завершения установки запустите make test, чтобы проверить, все ли работает правильно.
Redis имеет две исполняемые команды —
-
redis-cli — интерфейс командной строки для Redis (клиентская часть)
-
redis-server — хранилище данных Redis
redis-cli — интерфейс командной строки для Redis (клиентская часть)
redis-server — хранилище данных Redis
Для запуска сервера Redis введите redis-server в консоли терминала. Вы должны увидеть следующее —
Теперь откройте новое окно терминала и запустите redis-cli, чтобы открыть клиентское приложение.
По сути, Redis — это база данных ключ-значение. Чтобы создать ключ со строковым значением, вы должны использовать команду SET. Чтобы прочитать значение ключа, вы должны использовать команду GET. Давайте добавим двух пользователей и пароли для них. Ключи будут именами пользователей, а значения этих ключей будут соответствующими паролями.
Теперь мы должны изменить наш сервер сигнализации, чтобы добавить аутентификацию пользователя. Добавьте следующий код в начало файла server.js —
//require the redis library in Node.js var redis = require("redis"); //creating the redis client object var redisClient = redis.createClient();
В приведенном выше коде нам требуется библиотека Redis для Node.js и создание клиента Redis для нашего сервера.
Чтобы добавить аутентификацию, измените обработчик сообщений на объекте соединения —
//when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //check whether a user is authenticated if(data.type != "login") { //if user is not authenticated if(!connection.isAuth) { sendTo(connection, { type: "error", message: "You are not authenticated" }); return; } } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //get password for this username from redis database redisClient.get(data.name, function(err, reply) { //check if password matches with the one stored in redis var loginSuccess = reply === data.password; //if anyone is logged in with this username or incorrect password then refuse if(users[data.name] || !loginSuccess) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; connection.isAuth = true; sendTo(connection, { type: "login", success: true }); } }); break; } }); } //... //*****other handlers*******
В приведенном выше коде, если пользователь пытается войти в систему, мы получаем от Redis свой пароль, проверяем, совпадает ли он с сохраненным, и, если он успешен, мы сохраняем его имя пользователя на сервере. Мы также добавляем флаг isAuth к соединению, чтобы проверить, аутентифицирован ли пользователь. Обратите внимание на этот код —
//check whether a user is authenticated if(data.type != "login") { //if user is not authenticated if(!connection.isAuth) { sendTo(connection, { type: "error", message: "You are not authenticated" }); return; } }
Если неаутентифицированный пользователь пытается отправить предложение или покинуть соединение, мы просто отправляем ошибку обратно.
Следующим шагом является включение безопасного подключения к сокету. Настоятельно рекомендуется для приложений WebRTC. PKI (Инфраструктура открытых ключей) — это цифровая подпись от CA (центра сертификации). Затем пользователи проверяют, что закрытый ключ, используемый для подписи сертификата, соответствует открытому ключу сертификата CA. Для целей разработки. мы будем использовать самозаверяющий сертификат безопасности.
Мы будем использовать openssl. Это инструмент с открытым исходным кодом, который реализует протоколы SSL (Secure Sockets Layer) и TLS (Transport Layer Security). Он часто устанавливается по умолчанию в системах Unix. Запустите openssl версию -a, чтобы проверить, установлена ли она.
Чтобы сгенерировать открытые и закрытые ключи сертификата безопасности, вы должны выполнить следующие шаги:
-
Создать временный ключ пароля сервера
Создать временный ключ пароля сервера
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
-
Создать личный ключ сервера
Создать личный ключ сервера
openssl rsa -passin pass:12345 -in server.pass.key -out server.key
-
Создайте запрос на подпись. Вам будут заданы дополнительные вопросы о вашей компании. Просто нажмите кнопку «Ввод» все время.
Создайте запрос на подпись. Вам будут заданы дополнительные вопросы о вашей компании. Просто нажмите кнопку «Ввод» все время.
openssl req -new -key server.key -out server.csr
-
Создать сертификат
Создать сертификат
openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt
Теперь у вас есть два файла: сертификат (server.crt) и закрытый ключ (server.key). Скопируйте их в корневую папку сервера сигнализации.
Чтобы включить безопасное соединение через сокет, измените наш сервер сигнализации.
//require file system module var fs = require('fs'); var httpServ = require('https'); //https://github.com/visionmedia/superagent/issues/205 process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //out secure server will bind to the port 9090 var cfg = { port: 9090, ssl_key: 'server.key', ssl_cert: 'server.crt' }; //in case of http request just send back "OK" var processRequest = function(req, res) { res.writeHead(200); res.end("OK"); }; //create our server with SSL enabled var app = httpServ.createServer({ key: fs.readFileSync(cfg.ssl_key), cert: fs.readFileSync(cfg.ssl_cert) }, processRequest).listen(cfg.port); //require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({server: app}); //all connected to the server users var users = {}; //require the redis library in Node.js var redis = require("redis"); //creating the redis client object var redisClient = redis.createClient(); //when a user connects to our sever wss.on('connection', function(connection){ //...other code
В приведенном выше коде мы требуем, чтобы библиотека fs считала закрытый ключ и сертификат, создала объект cfg с портом привязки и путями для секретного ключа и сертификата. Затем мы создаем сервер HTTPS с нашими ключами вместе с сервером WebSocket на порту 9090.
Теперь откройте https: // localhost: 9090 в Opera. Вы должны увидеть следующее —
Нажмите кнопку «Все равно продолжить». Вы должны увидеть сообщение «ОК».
Чтобы протестировать наш защищенный сервер сигнализации, мы изменим приложение чата, которое мы создали в учебном пособии «WebRTC Text Demo». Нам просто нужно добавить поле пароля. Ниже приведен весь файл index.html —
<html> <head> <title>WebRTC Text Demo</title> <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> </head> <style> body { background: #eee; padding: 5% 0; } </style> <body> <div id = "loginPage" class = "container text-center"> <div class = "row"> <div class = "col-md-4 col-md-offset-4"> <h2>WebRTC Text Demo. Please sign in</h2> <label for = "usernameInput" class = "sr-only">Login</label> <input type = "email" id = "usernameInput" class = "form-control formgroup" placeholder = "Login" required = "" autofocus = ""> <input type = "text" id = "passwordInput" class = "form-control form-group" placeholder = "Password" required = "" autofocus = ""> <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock" >Sign in</button> </div> </div> </div> <div id = "callPage" class = "call-page container"> <div class = "row"> <div class = "col-md-4 col-md-offset-4 text-center"> <div class = "panel panel-primary"> <div class = "panel-heading">Text chat</div> <div id = "chatarea" class = "panel-body text-left"></div> </div> </div> </div> <div class = "row text-center form-group"> <div class = "col-md-12"> <input id = "callToUsernameInput" type = "text" placeholder = "username to call" /> <button id = "callBtn" class = "btn-success btn">Call</button> <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> </div> </div> <div class = "row text-center"> <div class = "col-md-12"> <input id = "msgInput" type = "text" placeholder = "message" /> <button id = "sendMsgBtn" class = "btn-success btn">Send</button> </div> </div> </div> <script src = "client.js"></script> </body> </html>
Нам также необходимо включить безопасное соединение через сокет в файле client.js через эту строку var conn = new WebSocket (‘wss: // localhost: 9090’); , Обратите внимание на протокол wss . Затем, кнопка входа в систему необходимо изменить, чтобы отправить пароль вместе с именем пользователя —
loginBtn.addEventListener("click", function (event) { name = usernameInput.value; var pwd = passwordInput.value; if (name.length > 0) { send({ type: "login", name: name, password: pwd }); } });
Ниже приведен весь файл client.js —
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('wss://localhost:9090'); conn.onopen = function () { console.log("Connected to the signaling server"); }; //when we got a message from a signaling server conn.onmessage = function (msg) { console.log("Got message", msg.data); var data = JSON.parse(msg.data); switch(data.type) { case "login": handleLogin(data.success); break; //when somebody wants to call us case "offer": handleOffer(data.offer, data.name); break; case "answer": handleAnswer(data.answer); break; //when a remote peer sends an ice candidate to us case "candidate": handleCandidate(data.candidate); break; case "leave": handleLeave(); break; default: break; } }; conn.onerror = function (err) { console.log("Got error", err); }; //alias for sending JSON encoded messages function send(message) { //attach the other peer username to our messages if (connectedUser) { message.name = connectedUser; } conn.send(JSON.stringify(message)); }; //****** //UI selectors block //****** var loginPage = document.querySelector('#loginPage'); var usernameInput = document.querySelector('#usernameInput'); var passwordInput = document.querySelector('#passwordInput'); var loginBtn = document.querySelector('#loginBtn'); var callPage = document.querySelector('#callPage'); var callToUsernameInput = document.querySelector('#callToUsernameInput'); var callBtn = document.querySelector('#callBtn'); var hangUpBtn = document.querySelector('#hangUpBtn'); var msgInput = document.querySelector('#msgInput'); var sendMsgBtn = document.querySelector('#sendMsgBtn'); var chatArea = document.querySelector('#chatarea'); var yourConn; var dataChannel; callPage.style.display = "none"; // Login when the user clicks the button loginBtn.addEventListener("click", function (event) { name = usernameInput.value; var pwd = passwordInput.value; if (name.length > 0) { send({ type: "login", name: name, password: pwd }); } }); function handleLogin(success) { if (success === false) { alert("Ooops...incorrect username or password"); } else { loginPage.style.display = "none"; callPage.style.display = "block"; //********************** //Starting a peer connection //********************** //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; //creating data channel dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); dataChannel.onerror = function (error) { console.log("Ooops...error:", error); }; //when we receive a message from the other peer, display it on the screen dataChannel.onmessage = function (event) { chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; }; dataChannel.onclose = function () { console.log("data channel is closed"); }; } }; //initiating a call callBtn.addEventListener("click", function () { var callToUsername = callToUsernameInput.value; if (callToUsername.length > 0) { connectedUser = callToUsername; // create an offer yourConn.createOffer(function (offer) { send({ type: "offer", offer: offer }); yourConn.setLocalDescription(offer); }, function (error) { alert("Error when creating an offer"); }); } }); //when somebody sends us an offer function handleOffer(offer, name) { connectedUser = name; yourConn.setRemoteDescription(new RTCSessionDescription(offer)); //create an answer to an offer yourConn.createAnswer(function (answer) { yourConn.setLocalDescription(answer); send({ type: "answer", answer: answer }); }, function (error) { alert("Error when creating an answer"); }); }; //when we got an answer from a remote user function handleAnswer(answer) { yourConn.setRemoteDescription(new RTCSessionDescription(answer)); }; //when we got an ice candidate from a remote user function handleCandidate(candidate) { yourConn.addIceCandidate(new RTCIceCandidate(candidate)); }; //hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; yourConn.close(); yourConn.onicecandidate = null; }; //when user clicks the "send message" button sendMsgBtn.addEventListener("click", function (event) { var val = msgInput.value; chatArea.innerHTML += name + ": " + val + "<br />"; //sending a message to a connected peer dataChannel.send(val); msgInput.value = ""; });
Теперь запустите наш безопасный сигнальный сервер через сервер узлов . Запустите узел static внутри модифицированной демонстрационной папки чата. Откройте localhost: 8080 в двух вкладках браузера. Попробуйте войти в систему. Помните, что только «user1» с «password1» и «user2» с «password2» могут войти в систему. Затем установите RTCPeerConnection (позвоните другому пользователю) и попробуйте отправить сообщение.
Ниже приведен полный код нашего защищенного сервера сигнализации.
//require file system module var fs = require('fs'); var httpServ = require('https'); //https://github.com/visionmedia/superagent/issues/205 process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //out secure server will bind to the port 9090 var cfg = { port: 9090, ssl_key: 'server.key', ssl_cert: 'server.crt' }; //in case of http request just send back "OK" var processRequest = function(req, res){ res.writeHead(200); res.end("OK"); }; //create our server with SSL enabled var app = httpServ.createServer({ key: fs.readFileSync(cfg.ssl_key), cert: fs.readFileSync(cfg.ssl_cert) }, processRequest).listen(cfg.port); //require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({server: app}); //all connected to the server users var users = {}; //require the redis library in Node.js var redis = require("redis"); //creating the redis client object var redisClient = redis.createClient(); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //check whether a user is authenticated if(data.type != "login") { //if user is not authenticated if(!connection.isAuth) { sendTo(connection, { type: "error", message: "You are not authenticated" }); return; } } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //get password for this username from redis database redisClient.get(data.name, function(err, reply) { //check if password matches with the one stored in redis var loginSuccess = reply === data.password; //if anyone is logged in with this username or incorrect password then refuse if(users[data.name] || !loginSuccess) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; connection.isAuth = true; sendTo(connection, { type: "login", success: true }); } }); break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; } }); connection.send("Hello from server"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
Резюме
В этой главе мы добавили аутентификацию пользователя на наш сервер сигнализации. Мы также узнали, как создавать самозаверяющие SSL-сертификаты и использовать их в приложениях WebRTC.