Ръководство за WebRTC

1. Общ преглед

Когато два браузъра трябва да комуникират, те обикновено се нуждаят от сървър между тях, за да координират комуникацията, предавайки съобщения между тях. Но наличието на сървър в средата води до забавяне на комуникацията между браузърите.

В този урок ще научим за WebRTC, проект с отворен код, който позволява на браузърите и мобилните приложения да комуникират директно помежду си в реално време. След това ще го видим в действие, като напишем просто приложение, което създава връзка peer-to-peer за споделяне на данни между два HTML клиента.

Ще използваме HTML, JavaScript и библиотеката WebSocket заедно с вградената поддръжка на WebRTC в уеб браузърите за изграждане на клиент. И ще изградим сървър за сигнализация с Spring Boot, използвайки WebSocket като комуникационен протокол. Накрая ще видим как да добавим видео и аудио потоци към тази връзка.

2. Основи и концепции на WebRTC

Нека да видим как два браузъра комуникират в типичен сценарий без WebRTC.

Да предположим, че имаме два браузъра и Браузър 1 трябва да изпрати съобщение до Браузър 2 . Браузър 1 първо го изпраща на сървъра :

След като сървърът получи съобщението, той го обработва, намира браузър 2 и му изпраща съобщението:

Тъй като сървърът трябва да обработи съобщението, преди да го изпрати до браузър 2, комуникацията се осъществява в почти реално време. Разбира се, бихме искали да е в реално време.

WebRTC решава този проблем, като създава директен канал между двата браузъра, премахвайки необходимостта от сървъра :

В резултат на това времето, необходимо за предаване на съобщения от един браузър в друг, се намалява драстично, тъй като съобщенията сега се насочват директно от подателя към получателя . Той също така отнема тежкото повдигане и честотната лента, възникнали от сървърите, и причинява споделянето им между участващите клиенти.

3. Поддръжка за WebRTC и вградени функции

WebRTC се поддържа от големи браузъри като Chrome, Firefox, Opera и Microsoft Edge, както и от платформи като Android и iOS.

WebRTC не се нуждае от външни плъгини, които да бъдат инсталирани в нашия браузър, тъй като решението се доставя в комплект с браузъра.

Освен това, в типично приложение в реално време, включващо предаване на видео и аудио, трябва да зависим силно от библиотеките на C ++ и трябва да се справим с много проблеми, включително:

  • Прикриване на загуба на пакети
  • Отмяна на ехото
  • Адаптивност на честотната лента
  • Динамично буфериране на трептене
  • Автоматичен контрол на усилването
  • Намаляване и потискане на шума
  • Изображение „почистване“

Но WebRTC се справя с всички тези проблеми под капака , което улеснява комуникацията в реално време между клиентите.

4. Peer-to-peer връзка

За разлика от комуникацията клиент-сървър, където има известен адрес за сървъра и клиентът вече знае адреса на сървъра, с който да комуникира, при P2P (peer-to-peer) връзка, никой от връстниците няма директен адрес на друг връстник .

За да се установи връзка между партньори, има няколко стъпки, които позволяват на клиентите да:

  • се правят на разположение за комуникация
  • идентифицират се и споделят информация, свързана с мрежата
  • споделят и се съгласяват относно формата на данните, режима и протоколите
  • споделяне на данни

WebRTC определя набор от API и методологии за изпълнение на тези стъпки.

За да могат клиентите да се откриват, да споделят мрежови подробности и след това да споделят формата на данните, WebRTC използва механизъм, наречен сигнализация .

5. Сигнализиране

Сигнализирането се отнася до процесите, свързани с откриването на мрежата, създаването на сесия, управлението на сесията и обмена на метаданни за възможностите на медията.

Това е от съществено значение, тъй като клиентите трябва да се познават предварително, за да започнат комуникацията.

За да постигне всичко това, WebRTC не определя стандарт за сигнализация и го оставя на изпълнението на разработчика. Така че, това ни предоставя гъвкавостта да използваме WebRTC на редица устройства с всякаква технология и поддържащ протокол.

5.1. Изграждане на сървъра за сигнализация

За сървъра за сигнализация ще изградим WebSocket сървър, използвайки Spring Boot . Можем да започнем с празен проект Spring Boot, генериран от Spring Initializr.

За да използваме WebSocket за нашето внедряване, нека добавим зависимостта към нашия pom.xml :

 org.springframework.boot spring-boot-starter-websocket 

Винаги можем да намерим най-новата версия, която да използваме от Maven Central.

Внедряването на сървъра за сигнализация е лесно - ще създадем крайна точка, която клиентско приложение може да използва, за да се регистрира като WebSocket връзка.

За да направите това в Spring Boot, нека напишем клас @Configuration, който разширява WebSocketConfigurer и отменя метода registerWebSocketHandlers :

@Configuration @EnableWebSocket public class WebSocketConfiguration implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new SocketHandler(), "/socket") .setAllowedOrigins("*"); } }

Note that we've identified /socket as the URL that we'll register from the client that we'll be building in the next step. We also passed in a SocketHandler as an argument to the addHandler method — this is actually the message handler that we'll create next.

5.2. Creating Message Handler in Signaling Server

The next step is to create a message handler to process the WebSocket messages that we'll receive from multiple clients.

This is essential to aid the exchange of metadata between the different clients to establish a direct WebRTC connection.

Here, to keep things simple, when we receive the message from a client, we will send it to all other clients except to itself.

To do this, we can extend TextWebSocketHandler from the Spring WebSocket library and override both the handleTextMessage and afterConnectionEstablished methods:

@Component public class SocketHandler extends TextWebSocketHandler { Listsessions = new CopyOnWriteArrayList(); @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws InterruptedException, IOException { for (WebSocketSession webSocketSession : sessions) { if (webSocketSession.isOpen() && !session.getId().equals(webSocketSession.getId())) { webSocketSession.sendMessage(message); } } } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); } } 

As we can see in the afterConnectionEstablished method, we add the received session to a list of sessions so that we can keep track of all the clients.

And when we receive a message from any of the clients, as can be seen in the handleTextMessage, we iterate over all the client sessions in the list and send the message to all other clients except the sender by comparing the session id of the sender and the sessions in the list.

6. Exchanging Metadata

In a P2P connection, the clients can be very different from each other. For example, Chrome on Android can connect to Mozilla on a Mac.

Hence, the media capabilities of these devices can vary widely. Therefore, it's essential for a handshake between peers to agree upon the media types and codecs used for communication.

In this phase, WebRTC uses the SDP (Session Description Protocol) to agree on the metadata between the clients.

To achieve this, the initiating peer creates an offer that must be set as a remote descriptor by the other peer. In addition, the other peer then generates an answer that is accepted as a remote descriptor by the initiating peer.

The connection is established when this process is complete.

7. Setting Up the Client

Let's create our WebRTC client such that it can act both as the initiating peer and the remote peer.

We'll begin by creating an HTML file called index.html and a JavaScript file named client.js which index.html will use.

To connect to our signaling server, we create a WebSocket connection to it. Assuming that the Spring Boot signaling server that we built is running on //localhost:8080, we can create the connection:

var conn = new WebSocket('ws://localhost:8080/socket');

To send a message to the signaling server, we'll create a send method that will be used to pass the message in the upcoming steps:

function send(message) { conn.send(JSON.stringify(message)); }

8. Setting Up a Simple RTCDataChannel

After setting up the client in the client.js, we need to create an object for the RTCPeerConnection class. Here, we set up the object and enable the data channel by passing RtpDataChannels as true:

var peerConnection = new RTCPeerConnection(configuration, { optional : [ { RtpDataChannels : true } ] });

In this example, the purpose of the configuration object is to pass in the STUN (Session Traversal Utilities for NAT) and TURN (Traversal Using Relays around NAT) servers and other configurations that we'll be discussing in the latter part of this tutorial. For this example, it's sufficient to pass in null.

Now, we can create a dataChannel to use for message passing:

var dataChannel = peerConnection.createDataChannel("dataChannel", { reliable: true });

Subsequently, we can create listeners for various events on the data channel:

dataChannel.onerror = function(error) { console.log("Error:", error); }; dataChannel.onclose = function() { console.log("Data channel is closed"); };

9. Establishing a Connection With ICE

The next step in establishing a WebRTC connection involves the ICE (Interactive Connection Establishment) and SDP protocols, where the session descriptions of the peers are exchanged and accepted at both peers.

The signaling server is used to send this information between the peers. This involves a series of steps where the clients exchange connection metadata through the signaling server.

9.1. Creating an Offer

Firstly, we create an offer and set it as the local description of the peerConnection. We then send the offer to the other peer:

peerConnection.createOffer(function(offer) { send({ event : "offer", data : offer }); peerConnection.setLocalDescription(offer); }, function(error) { // Handle error here });

Here, the send method makes a call to the signaling server to pass the offer information.

Note that we are free to implement the logic of the send method with any server-side technology.

9.2. Handling ICE Candidates

Secondly, we need to handle the ICE candidates. WebRTC uses the ICE (Interactive Connection Establishment) protocol to discover the peers and establish the connection.

When we set the local description on the peerConnection, it triggers an icecandidate event.

This event should transmit the candidate to the remote peer so that the remote peer can add it to its set of remote candidates.

To do this, we create a listener for the onicecandidate event:

peerConnection.onicecandidate = function(event) { if (event.candidate) { send({ event : "candidate", data : event.candidate }); } };

The icecandidate event triggers again with an empty candidate string when all the candidates are gathered.

We must pass this candidate object as well to the remote peer. We pass this empty candidate string to ensure that the remote peer knows that all the icecandidate objects are gathered.

Also, the same event is triggered again to indicate that the ICE candidate gathering is complete with the value of candidate object set to null on the event. This need not be passed on to the remote peer.

9.3. Receiving the ICE Candidate

Thirdly, we need to process the ICE candidate sent by the other peer.

The remote peer, upon receiving this candidate, should add it to its candidate pool:

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

9.4. Receiving the Offer

After that, when the other peer receives the offer, it must set it as the remote description. In addition, it must generate an answer, which is sent to the initiating peer:

peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); peerConnection.createAnswer(function(answer) { peerConnection.setLocalDescription(answer); send({ event : "answer", data : answer }); }, function(error) { // Handle error here });

9.5. Receiving the Answer

Finally, the initiating peer receives the answer and sets it as the remote description:

handleAnswer(answer){ peerConnection.setRemoteDescription(new RTCSessionDescription(answer)); }

With this, WebRTC establishes a successful connection.

Now, we can send and receive data between the two peers directly, without the signaling server.

10. Sending a Message

Now that we've established the connection, we can send messages between the peers using the send method of the dataChannel:

dataChannel.send(“message”);

Likewise, to receive the message on the other peer, let's create a listener for the onmessage event:

dataChannel.onmessage = function(event) { console.log("Message:", event.data); };

With this step, we have created a fully functional WebRTC data channel. We can now send and receive data between the clients. Additionally, we can add video and audio channels to this.

11. Adding Video and Audio Channels

When WebRTC establishes a P2P connection, we can easily transfer audio and video streams directly.

11.1. Obtaining the Media Stream

Firstly, we need to obtain the media stream from the browser. WebRTC provides an API for this:

const constraints = { video: true,audio : true }; navigator.mediaDevices.getUserMedia(constraints). then(function(stream) { /* use the stream */ }) .catch(function(err) { /* handle the error */ });

We can specify the frame rate, width, and height of the video using the constraints object.

The constraint object also allows specifying the camera used in the case of mobile devices:

var constraints = { video : { frameRate : { ideal : 10, max : 15 }, width : 1280, height : 720, facingMode : "user" } };

Also, the value of facingMode can be set to “environment” instead of “user” if we want to enable the back camera.

11.2. Sending the Stream

Secondly, we have to add the stream to the WebRTC peer connection object:

peerConnection.addStream(stream);

Adding the stream to the peer connection triggers the addstream event on the connected peers.

11.3. Receiving the Stream

Thirdly, to receive the stream on the remote peer, we can create a listener.

Let's set this stream to an HTML video element:

peerConnection.onaddstream = function(event) { videoElement.srcObject = event.stream; };

12. NAT Issues

In the real world, firewall and NAT (Network Address Traversal) devices connect our devices to the public Internet.

NAT provides the device an IP address for usage within the local network. So, this address is not accessible outside the local network. Without a public address, peers are unable to communicate with us.

To address this issue, WebRTC uses two mechanisms:

  1. STUN
  2. TURN

13. Using STUN

STUN is the simplest approach to this problem. Before sharing the network information to the peer, the client makes a request to a STUN server. The responsibility of the STUN server is to return the IP address from which it receives the request.

So, by querying the STUN server, we get our own public-facing IP address. We then share this IP and port information to the peer we want to connect to. The other peers can do the same to share their public-facing IPs.

To use a STUN server, we can simply pass the URL in the configuration object for creating the RTCPeerConnection object:

var configuration = { "iceServers" : [ { "url" : "stun:stun2.1.google.com:19302" } ] }; 

14. Using TURN

In contrast, TURN is a fallback mechanism used when WebRTC is unable to establish a P2P connection. The role of the TURN server is to relay data directly between the peers. In this case, the actual stream of data flows through the TURN servers. Using the default implementations, TURN servers also act as STUN servers.

TURN servers are publicly available, and clients can access them even if they are behind a firewall or proxy.

But, using a TURN server is not truly a P2P connection, as an intermediate server is present.

Забележка: TURN е последна мярка, когато не можем да установим P2P връзка. Тъй като данните преминават през сървъра TURN, той изисква много честотна лента и в този случай не използваме P2P.

Подобно на STUN, можем да предоставим URL адреса на сървъра TURN в същия обект на конфигурация:

{ 'iceServers': [ { 'urls': 'stun:stun.l.google.com:19302' }, { 'urls': 'turn:10.158.29.39:3478?transport=udp', 'credential': 'XXXXXXXXXXXXX', 'username': 'XXXXXXXXXXXXXXX' }, { 'urls': 'turn:10.158.29.39:3478?transport=tcp', 'credential': 'XXXXXXXXXXXXX', 'username': 'XXXXXXXXXXXXXXX' } ] }

15. Заключение

В този урок обсъдихме какво представлява проектът WebRTC и представихме основните му концепции. След това изградихме просто приложение за споделяне на данни между два HTML клиента.

Също така обсъдихме стъпките, свързани със създаването и установяването на WebRTC връзка.

Освен това разгледахме използването на STUN и TURN сървъри като резервен механизъм, когато WebRTC се провали.

Можете да разгледате примерите, предоставени в тази статия в GitHub.