WebRTC, веб-коммуникация в реальном времени, стандарт, который позволяет разработчикам создавать приложения для живого аудио, видео и текстового чата, будь то в Интернете или на мобильных устройствах, в этой статье я объясню его реализацию в Интернете, чтобы вы могли начните создавать собственное веб-приложение или сайт для чата.
Стандарт поддерживается множеством браузеров, но они отличаются в некоторых деталях, реализация, описанная в этой статье, должна помочь вам начать работу в Google Chrome, а также в Mozilla Firefox, так как я все еще работаю над его использованием в других веб-браузеры, такие как Apple Safari и Microsoft Edge.
Концепция
Идея этого стандарта проста, он создает одноранговые соединения между сторонами, которые хотят позвонить, путем представления возможностей их устройств - кодирования и декодирования аудио и видео - и сетевых деталей друг другу, другими словами. Когда вы звоните своему другу Питу, вы обмениваетесь с ним мультимедийными данными и сетевыми данными своего устройства, чтобы вы оба могли соединиться во время разговора.
Основные протоколы
Стандарт работает, по сути, поверх двух основных протоколов, во-первых, это SDP «Протокол описания сеанса», который определяет возможности и ограничения мультимедиа устройства, и протокол ICE «Установление подключения к Интернету», который определяет сетевую информацию, которую другие устройства могут использовать для связи с твое устройство.
Устройство Медиа
Перед тем, как инициировать вызов, вы должны инициализировать захват мультимедиа на вашем собственном устройстве, то есть вам нужен доступ к микрофону для потоковой передачи звука и веб-камера или экран для потоковой передачи видео, конечно, вы можете выбрать только аудиозвонок или видео. Вы можете получить поток ваших мультимедийных устройств в JavaScript вашего приложения следующим образом:
var localStream; navigator.mediaDevices.getUserMedia({video: true, audio: true}).then(function(stream){ localStream = stream; handlePendingData(); // Defined later on. }).catch(function(error){ console.log(error); });
Обратите внимание на объект, присвоенный методу getUserMedia
выше, он решает, получать ли аудио, видео или и то, и другое, для просмотра захваченного потока вам понадобится тег video
в вашем представлении html, но с добавленным атрибутом muted
, чтобы вы не услышите свой собственный звук, затем в сценарии вы напишете $('#your-video-id')[0].srcObject = localStream;
, конечно, вам решать, как вы назовете и выберете свой видеоэлемент - и да, знак$
- это jQuery.
Предостережения, которые нужно знать, прежде чем мы продолжим
Первое, что следует иметь в виду, это то, что большинство связанных с WebRTC методов - это async
функции, которые возвращают Promise
, т.е. следующий шаг после вызова любого из них должен выполняться в закрытии, переданном методу then
возвращенного Promise
, или с использованием выражение await
, если применимо.
Во-вторых, вы должны понимать, что ваше собственное устройство не должно определяться как одноранговое, т.е. вы не создаете объект RTCPeerConnection
для своего собственного устройства, а вы создаете его для других устройств, которые связываются с вами.
Объект RTCPeerConnection
- это то, что определяет соединение между вашим устройством и другим устройством, поэтому при вызове между двумя сторонами у каждой стороны будет один RTCPeerConnection
, который связывает его с другим устройством, в групповом вызове из четырех сторон каждая сторона будет имея три RTCPeerConnection
объекта, рассматривайте этот объект как линию, соединяющую две точки вместе, и каждая точка является одноранговым узлом или, другими словами, устройством.
STUN, TURN и серверы сигнализации
Одноранговые узлы - устройства - должны знать сетевую и мультимедийную информацию друг друга, чтобы иметь надлежащее соединение, где может происходить вызов, для достижения этого стандарта WebRTC требуется помощь серверам в знакомстве этих узлов друг с другом.
Во-первых, это STUN-сервер - утилиты обхода сеанса для NAT, который определяет детали вашей сети таким образом, чтобы другие одноранговые узлы могли связываться с вашим устройством через Интернет.
Во-вторых, это сервер TURN - обход с использованием реле вокруг NAT, который можно использовать для ретрансляции связи между вашим устройством и другими одноранговыми узлами, чтобы ваше общение с другими устройствами больше не было прямым одноранговым, поскольку поток вызовов проходит через этот сервер сначала затем, в свою очередь, подталкивает поток к другому концу вызова.
Эти два типа серверов собирают то, что называется RTCIceCandidate
, который определяет возможный сетевой путь для удаленных одноранговых узлов - других устройств - для использования в их связи с вашим устройством, и, как вы заметили, собранные данные представляют собой детали вашего собственного устройства, Итак, как обмениваться этими данными с другими устройствами, включая SDP, - это то, что делает сервер сигнализации.
Вы можете использовать некоторые из серверов STUN и TURN, которые находятся здесь и здесь.
Давай начнем
Теперь, когда мы знаем основы и инициализировали наши мультимедийные устройства в переменной localStream
, мы можем приступить к определению основных требований в сценарии нашего приложения, следуя блоку кода, написанному выше, который мы добавляем:
// ICE candidates gathering servers, you can use up to five at once.
var iceServers = [
{url: "stun:stun3.l.google.com:19302"},
{url: "stun:stun3.l.google.com:19302"}
];
// Initializing the signaling server, which is basically a websocket.
var signalingChannel = new WebSocket("wss://your_websocket_server");
var peers = {}, pendingPeers = [], pendingMessages = [], pendingSdps = {}, pendingCandidates = {}, ready = false, currentId = Math.floor(Date.now() / 1000)
;
Серверы STUN и TURN предоставляются в виде облачных сервисов, некоторые будут бесплатными, а некоторые будут платными и потребуют учетных данных, но сервер сигнализации является нашим, чтобы реализовать, вам решать, как его реализовать, будь то в socket.io, node -js или php с Ratchet, как показано здесь.
Определим несколько обработчиков
Следующие функции будут обрабатывать различные события, происходящие в приложении, изначально для приложения установлено значение не ready
, что означает, что мы еще не получили разрешение на доступ к мультимедийным устройствам пользователя, как только будет задано localStream
, и следующая функция будет называться:
function handlePendingData(){ ready = true; for(var i in pendingPeers) initPeer(pedingPeers[i]) pendingPeers = []; setTimeout(function(){ for(var i in pendingMessages) handleSignalingMessage(pendingMessages[i]); pendingMessages = []; }, 1000); } function initPeer(peerId){ peers[peerId] = new RTCPeerConnection({ iceServers: iceServers }); initRemoteStream(peerId); addIceListeners(peerId); // Creating an offer SDP for the joining peer. if(peerId > currentId){ var peer = peers[peerId]; peer.createOffer().then(function(offer){ return peer.setLocalDescription(offer); }).then(function(){ pendingSdps[peerId] = { action: 'offer', id: peer.id, offer: peer.localDescription }; }); } }
Функция в основном инициализирует всех существующих пиров, которые были в комнате, когда мы вошли, добавляя новые теги video
в наше представление html для каждого из них и добавляя их соответствующие слушатели потока в функцию initRemoteStream
, а также настраивая слушателей событий кандидатов ICE в функции addIceListeners
давайте подробно рассмотрим эти две функции:
function initRemoteStream(peerId){ var peer = peers[peerId]; $('body').append('<video id="'+peerId+'"></video>'); if(!sendLocalStream(peer)) var localStreamSent = setInterval(function(){ if(sendLocalStream(peer)) clearInterval(localStreamSent); }, 100); var stream = new MediaStream(); peer.addEventListener('track', function(e){ var peerVideo = $('#' + peerId).find('video')[0]; stream.addTrack(e.track); if(!peerVideo.srcObject) peerVideo.srcObject = stream; console.log('Remote Track Added', e.track); }); } function addIceListeners(peerId){ var peer = peers[peerId]; peer.addEventListener('icecandidate', function(e){ if(e.candidate) signalingChannel.send(JSON.stringify({ action: 'candidate', id: peerId, candidate: e.candidate })); }); peer.addEventListener('icecandidateerror', function(e){ console.error('ICE error:', e); }); peer.addEventListener('icegatheringstatechange', function(e){ var connection = e.target; if(connection.iceGatheringState == 'complete' && pendingSdps[peerId] != null){ signalingChannel.send(JSON.stringify(pendingSdps[peerId])); pendingSdps[peerId] = null; } }); }
Теперь, если вы внимательно посмотрите на последний добавленный прослушиватель событий icegatheringstatechange
, вы увидите, что signalingChannel
используется для отправки ожидающего SDP другому партнеру, который является объектом мультимедийных возможностей и ограничений вашего устройства, что означает мы отправляем SDP не после его создания, а скорее когда собраны все кандидаты ICE.
И что касается вызова sendLocalStream
выше, это не одноразовый вызов, потому что поток нашего устройства, возможно, еще не был инициализирован, поэтому он был помещен в повторяющийся интервал, который останавливается после успешной инициализации потока, давайте посмотрим, что делает эта функция :
function sendLocalStream(peerId){ var peer = peers[peerId]; if(localStream != null){ localStream.getTracks().forEach(function(track){ peer.addTrack(track); console.log('Local Track Added', track); }); return true; } return false; }`
Короче говоря, предыдущая функция отправляет наш медиапоток - аудио и видео в зависимости от нашей инициализации - другому одноранговому узлу через его RTCPeerConnection
объект, то есть peer
является объектом RTCPeerConnection
, здесь и во всех других упомянутых функциях.
Подводя итоги!
Я знаю, что это был долгий путь, но потерпите еще немного, и к концу его у вас будет полная картина создания функциональной видеоконференции WebRTC - или комнаты чата - давайте определим функцию handleSignalingMessage
, которая обрабатывает входящие сообщения сервера сигнализации, которые отправляются нам другими узлами в комнате - или самим сервером.
function handleSignalingMessage(message){ switch(message.action){ case 'offer': handleOffer(message.offer, message.senderId); break; case 'answer': handleAnswer(message.answer, message.senderId); break; case 'candidate': handleCandidate(message.candidate, message.senderId); break; case 'close': peers.splice(message.id, 1); $('#' + message.id).remove(); } } function handleOffer(offer, senderId){ var peer = peers[senderId]; peer.setRemoteDescription(new RTCSessionDescription(offer)).then(function(){ handlePendingCandidates(peerId); return peer.createAnswer(); }).then(function(answer){ return peer.setLocalDescription(answer); }).then(function(){ pendingSdps[senderId] = { action: 'answer', id: senderId, answer: peer.localDescription }; }); } function handleAnswer(answer, peerId){ var peer = peers[peerId]; if(!peer.remoteDescription){ var peer = peers[peerId]; peer.setRemoteDescription(new RTCSessionDescription(answer)).then(function(){ handlePendingCandidates(peerId); }); } } function handleCandidate(candidate, peerId){ var peer = peers[peerId]; if(!peer.remoteDescription){ if(pendingCandidates[peerId] === undefined) pendingCandidates[peerId] = [candidate]; else pendingCandidates[peerId].push(candidate); } else peer.addIceCandidate(new RTCIceCandidate(candidate)).catch(function(e){ console.error('Could not add received ICE candidate', e); }); } function handlePendingCandidates(peerId){ var peer = peers[peerId]; pendingCandidates[peerId].forEach(function(candidate){ peer.handleCandidate(candidate); }); pendingCandidates[peerId] = []; } signalingChannel.onmessage = function(e){ var message = JSON.parse(e.data); if(message.action == 'init'){ if(ready) initPeer(message.id); else pendingPeers.push(message.id); } else if(ready) handleSignalingMessage(message); else pendingMessages.push(message); }; signalingChannel.onopen = function(){ signalingChannel.send(JSON.stringify({ action: 'init', id: currentId })); };
Вот и вся логика приложения, предыдущие обработчики handleOffer
, handleAnswer
и handleCandidate
обрабатывают входящие SDP предложений, SDP ответов и кандидатов ICE от других одноранговых узлов, обработка некоторых из них откладывается до тех пор, пока приложение не будет установлено в состояние готовности, что происходит, когда доступ к веб-камере или экранному каналу предоставляется - что касается кандидатов, они не обрабатываются до тех пор, пока не будет установлен удаленный SDP однорангового узла - если обработать раньше, сбор кандидатов текущего однорангового узла остановится, как это видно в Google Chrome.
Начальной точкой приложения является обработчик onopen
, установленный на signalingChannel
, который отправляет идентификатор текущего однорангового узла всем другим одноранговым узлам, как только канал - веб-сокет - открывается, и все другие действия запускаются в onmessage
обработчике канала.
Таким образом, это веб-приложение собрания открывает вашу веб-камеру, и всякий раз, когда кто-то присоединяется, запустив приложение, он вызывает все другие участники собрания - включая вас - путем создания предложения SDP, а затем отправки его им, созданное предложение SDP становится установлен в качестве локального описания для однорангового узла, это вызовет запуск кандидатов ICE - сетевых путей - сбора однорангового узла и отправки их другим сторонам для подключения.
Наконец, когда сбор кандидатов завершен, созданный SDP предложения - вместе с любыми другими собранными SDP - отправляется, другие одноранговые узлы, получающие SDP предложения, устанавливают их как удаленное описание, создают ответные SDP и устанавливают их как локальное описание, что запускает кандидатов ICE собираются на их стороне - затем, наконец, отправьте их обратно коллегам, которые отправили им предложения SDP, собранные кандидаты на отвечающей стороне отправляются на сторону предложения, и теперь все стороны знают сетевые и медиа-данные друг друга, чтобы они могли беспрепятственно подключаться и транслируют свои веб-камеры - или записи - друг другу.
Я надеюсь, что я дал вам наиболее четкое представление о WebRTC и, как начать его использовать, я постараюсь предоставить функциональный прототип приложения, описанного в этой статье на GitHub, когда позволит время. Спасибо, что дочитали до этого места, и, если есть какие-либо вопросы, не стесняйтесь спрашивать.