WebRTC - Szybki przewodnik

Internet nie jest już obcy komunikacji w czasie rzeczywistym, jak WebRTC (Web Real-Time Communication)wchodzi w grę. Mimo, że został wydany w maju 2011 roku, wciąż się rozwija i zmienia się jego standardy. Zestaw protokołów jest standaryzowany przez komunikację w czasie rzeczywistym w przeglądarkach internetowych. Grupa robocza pod adresemhttp://tools.ietf.org/wg/rtcweb/ z IETF (Internet Engineering Task Force)podczas gdy nowe zestawy interfejsów API są standaryzowane przez grupę roboczą ds. komunikacji w czasie rzeczywistym w sieci WWW pod adresemhttp://www.w3.org/2011/04/webrtc/ z W3C (World Wide Web Consortium). Wraz z pojawieniem się WebRTC, nowoczesne aplikacje internetowe mogą z łatwością przesyłać strumieniowo zawartość audio i wideo do milionów ludzi.

Schemat podstawowy

WebRTC umożliwia szybkie i łatwe konfigurowanie połączeń peer-to-peer z innymi przeglądarkami internetowymi. Aby zbudować taką aplikację od podstaw, potrzebowałbyś wielu frameworków i bibliotek zajmujących się typowymi problemami, takimi jak utrata danych, zrywanie połączenia i przechodzenie przez NAT. Dzięki WebRTC wszystko to jest wbudowane w przeglądarkę od razu po wyjęciu z pudełka. Ta technologia nie wymaga żadnych wtyczek ani oprogramowania innych firm. Jest open source, a jego kod źródłowy jest bezpłatnie dostępny pod adresemhttp://www.webrtc.org/.

Interfejs API WebRTC obejmuje przechwytywanie, kodowanie i dekodowanie multimediów, warstwę transportową i zarządzanie sesjami.

Media Capture

Pierwszym krokiem jest uzyskanie dostępu do kamery i mikrofonu urządzenia użytkownika. Wykrywamy rodzaj dostępnych urządzeń, uzyskujemy uprawnienia użytkownika do dostępu do tych urządzeń i zarządzamy strumieniem.

Kodowanie i dekodowanie audio i wideo

Przesyłanie strumienia danych audio i wideo przez Internet nie jest łatwym zadaniem. Tutaj używane jest kodowanie i dekodowanie. Jest to proces dzielenia klatek wideo i fal audio na mniejsze fragmenty i kompresji ich. Ten algorytm nazywa sięcodec. Istnieje ogromna liczba różnych kodeków, które są obsługiwane przez różne firmy o różnych celach biznesowych. W WebRTC jest również wiele kodeków, takich jak H.264, iSAC, Opus i VP8. Kiedy dwie przeglądarki łączą się ze sobą, wybierają najbardziej optymalny obsługiwany kodek między dwoma użytkownikami. Na szczęście WebRTC wykonuje większość kodowania za kulisami.

Warstwa transportowa

Warstwa transportowa zarządza kolejnością pakietów, radzi sobie z utratą pakietów i łączeniem się z innymi użytkownikami. Ponownie interfejs API WebRTC zapewnia nam łatwy dostęp do zdarzeń, które informują nas o problemach z połączeniem.

Zarządzanie sesjami

Zarządzanie sesjami zajmuje się zarządzaniem, otwieraniem i organizowaniem połączeń. To się powszechnie nazywasignaling. Jeśli przesyłasz strumienie audio i wideo do użytkownika, sensowne jest również przesyłanie dodatkowych danych. Odbywa się to przezRTCDataChannel API.

Inżynierowie z firm takich jak Google, Mozilla, Opera i inni wykonali świetną robotę, wprowadzając to doświadczenie czasu rzeczywistego do sieci.

Zgodność z przeglądarkami

Standardy WebRTC są jednymi z najszybciej rozwijających się w sieci, więc nie oznacza to, że każda przeglądarka obsługuje te same funkcje w tym samym czasie. Aby sprawdzić, czy Twoja przeglądarka obsługuje WebRTC, czy nie, możesz odwiedzić stronęhttp://caniuse.com/#feat=rtcpeerconnection. We wszystkich samouczkach zalecam używanie przeglądarki Chrome we wszystkich przykładach.

Wypróbuj WebRTC

Zacznijmy teraz korzystać z WebRTC. Przejdź w przeglądarce do witryny demonstracyjnej pod adresemhttps://apprtc.appspot.com/

Kliknij przycisk „DOŁĄCZ”. Powinieneś zobaczyć rozwijane powiadomienie.

Kliknij przycisk „Zezwól”, aby rozpocząć przesyłanie strumieniowe wideo i audio na stronę internetową. Powinieneś zobaczyć swój strumień wideo.

Teraz otwórz adres URL, na którym się obecnie znajdujesz, w nowej karcie przeglądarki i kliknij „DOŁĄCZ”. Powinieneś zobaczyć dwa strumienie wideo - jeden od pierwszego klienta, a drugi od drugiego.

Teraz powinieneś zrozumieć, dlaczego WebRTC jest potężnym narzędziem.

Przypadków użycia

Internet w czasie rzeczywistym otwiera drzwi do zupełnie nowego zakresu aplikacji, w tym czatu tekstowego, udostępniania ekranu i plików, gier, czatu wideo i nie tylko. Oprócz komunikacji możesz używać WebRTC do innych celów, takich jak -

  • marketing w czasie rzeczywistym
  • reklama w czasie rzeczywistym
  • komunikacja back office (CRM, ERP, SCM, FFM)
  • zarządzanie zasobami ludzkimi
  • sieć społecznościowa
  • usługi randkowe
  • konsultacje lekarskie online
  • usługi finansowe
  • surveillance
  • gry wieloosobowe
  • nadawanie na żywo
  • e-learning

Podsumowanie

Teraz powinieneś dobrze zrozumieć termin WebRTC. Powinieneś również mieć pomysł, jakie typy aplikacji można zbudować za pomocą WebRTC, ponieważ wypróbowałeś już to w swojej przeglądarce. Podsumowując, WebRTC to całkiem przydatna technologia.

Ogólna architektura WebRTC ma wysoki poziom złożoności.

Tutaj możesz znaleźć trzy różne warstwy -

  • API for web developers - ta warstwa zawiera wszystkie potrzebne deweloperom sieciowym API, w tym obiekty RTCPeerConnection, RTCDataChannel i MediaStrean.

  • API dla twórców przeglądarek

  • Zastępowalny interfejs API, który twórcy przeglądarek mogą podłączyć.

Komponenty transportowe umożliwiają nawiązywanie połączeń w różnych typach sieci, podczas gdy silniki głosu i wideo są szkieletami odpowiedzialnymi za przesyłanie strumieni audio i wideo z karty dźwiękowej i kamery do sieci. Dla programistów WWW najważniejszą częścią jest WebRTC API.

Jeśli spojrzymy na architekturę WebRTC od strony klient-serwer, zobaczymy, że jeden z najczęściej używanych modeli jest inspirowany trapezem SIP (Session Initiation Protocol).

W tym modelu na obu urządzeniach jest uruchomiona aplikacja internetowa z różnych serwerów. Obiekt RTCPeerConnection konfiguruje strumienie, aby mogły łączyć się ze sobą w trybie peer-to-peer. Ta sygnalizacja odbywa się za pośrednictwem protokołu HTTP lub WebSockets.

Ale najczęściej używanym modelem jest trójkąt -

W tym modelu oba urządzenia korzystają z tej samej aplikacji internetowej. Daje twórcom stron internetowych większą elastyczność podczas zarządzania połączeniami użytkowników.

Interfejs API WebRTC

Składa się z kilku głównych obiektów javascript -

  • RTCPeerConnection
  • MediaStream
  • RTCDataChannel

Obiekt RTCPeerConnection

Ten obiekt jest głównym punktem wejścia do interfejsu API WebRTC. Pomaga nam łączyć się z rówieśnikami, inicjować połączenia i dołączać strumienie multimedialne. Zarządza również połączeniem UDP z innym użytkownikiem.

Głównym zadaniem obiektu RTCPeerConnection jest zestawienie i utworzenie połączenia równorzędnego. Możemy łatwo podpiąć klucze do punktów połączenia, ponieważ ten obiekt wywołuje zestaw zdarzeń, gdy się pojawią. Te zdarzenia dają dostęp do konfiguracji naszego połączenia -

RTCPeerConnection to prosty obiekt javascript, który możesz po prostu utworzyć w ten sposób -

[code] 
var conn = new RTCPeerConnection(conf); 

conn.onaddstream = function(stream) { 
   // use stream here 
}; 

[/code]

Obiekt RTCPeerConnection akceptuje parametr conf , który omówimy później w tych samouczkach. Onaddstream zdarzenie jest zwolniony, gdy użytkownik zdalny dodaje wideo lub strumień audio do ich połączenia peer.

MediaStream API

Nowoczesne przeglądarki zapewniają programiście dostęp do getUserMedia API, znanego również jako MediaStream API. Istnieją trzy kluczowe punkty funkcjonalności -

  • Daje programiście dostęp do obiektu strumienia, który reprezentuje strumienie wideo i audio

  • Zarządza wyborem wejściowych urządzeń użytkownika w przypadku, gdy użytkownik ma wiele kamer lub mikrofonów na swoim urządzeniu

  • Zapewnia poziom bezpieczeństwa, który pyta użytkownika przez cały czas, gdy chce pobrać strumień

Aby przetestować ten interfejs API, stwórzmy prostą stronę HTML. Wyświetli pojedynczy element <video>, poprosi użytkownika o pozwolenie na korzystanie z kamery i pokaże na stronie strumień na żywo z kamery. Utwórz plik index.html i dodaj -

[code] 
<html>
 
   <head> 
      <meta charset = "utf-8"> 
   </head>
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body> 
	 
</html> 
[/code]

Następnie dodaj plik 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]

Teraz otwórz index.html i powinieneś zobaczyć strumień wideo pokazujący twoją twarz.

Ale bądź ostrożny, ponieważ WebRTC działa tylko po stronie serwera. Jeśli po prostu otworzysz tę stronę w przeglądarce, nie zadziała. Musisz hostować te pliki na serwerach Apache lub Node lub na którym preferujesz.

Obiekt RTCDataChannel

Oprócz przesyłania strumieni multimediów między urządzeniami równorzędnymi możesz również wysyłać dodatkowe dane za pomocą interfejsu DataChannel API. Ten interfejs API jest tak prosty, jak interfejs API MediaStream. Głównym zadaniem jest utworzenie kanału pochodzącego z istniejącego obiektu 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]

To wszystko, czego potrzebujesz, tylko dwie linie kodu. Wszystko inne odbywa się na wewnętrznej warstwie przeglądarki. Kanał można utworzyć na dowolnym połączeniu równorzędnym, dopóki obiekt RTCPeerConnection nie zostanie zamknięty.

Podsumowanie

Powinieneś teraz dobrze zrozumieć architekturę WebRTC. Omówiliśmy również interfejsy API MediaStream, RTCPeerConnection i RTCDataChannel. Interfejs API WebRTC to ruchomy cel, więc zawsze bądź na bieżąco z najnowszymi specyfikacjami.

Zanim zaczniemy budować nasze aplikacje WebRTC, powinniśmy ustawić nasze środowisko kodowania. Przede wszystkim powinieneś mieć edytor tekstu lub IDE, w którym możesz edytować HTML i Javascript. Są szanse, że już wybrałeś preferowany, czytając ten samouczek. Jeśli chodzi o mnie, używam WebStorm IDE. Możesz pobrać jego wersję próbną pod adresemhttps://www.jetbrains.com/webstorm/. Używam również Linux Mint jako mojego wybranego systemu operacyjnego.

Innym wymaganiem dla typowych aplikacji WebRTC jest posiadanie serwera do hostowania plików HTML i Javascript. Kod nie zadziała po dwukrotnym kliknięciu plików, ponieważ przeglądarka nie może łączyć się z kamerami i mikrofonami, chyba że pliki są obsługiwane przez rzeczywisty serwer. Odbywa się to oczywiście ze względu na kwestie bezpieczeństwa.

Istnieje mnóstwo różnych serwerów internetowych, ale w tym samouczku będziemy używać Node.js ze statycznym węzłem -

  • Wizyta https://nodejs.org/en/ i pobierz najnowszą wersję Node.js.

  • Rozpakuj go do katalogu / usr / local / nodejs.

  • Otwórz plik /home/YOUR_USERNAME/.profile i dodaj następujący wiersz na końcu - export PATH = $ PATH: / usr / local / nodejs / bin

  • Możesz ponownie uruchomić komputer lub uruchomić plik /home/YOUR_USERNAME/.profile

  • Teraz polecenie węzła powinno być dostępne z wiersza poleceń. Npm polecenia jest również dostępna. NMP to menedżer pakietów dla Node.js. Możesz dowiedzieć się więcej nahttps://www.npmjs.com/.

  • Otwórz terminal i uruchom sudo npm install -g node-static . Spowoduje to zainstalowanie statycznego serwera WWW dla Node.js.

  • Teraz przejdź do dowolnego katalogu zawierającego pliki HTML i uruchom polecenie static w katalogu, aby uruchomić serwer WWW.

  • Możesz przejść do http://localhost:8080 aby zobaczyć swoje pliki.

Jest inny sposób na zainstalowanie nodejs. Po prostu uruchom sudo apt-get install nodejs w oknie terminala.

Aby przetestować instalację Node.js, otwórz terminal i uruchom polecenie node . Wpisz kilka poleceń, aby sprawdzić, jak to działa -

Node.js uruchamia pliki Javascript, a także polecenia wpisywane w terminalu. Utwórz plik index.js z następującą zawartością -

console.log(“Testing Node.js”);

Następnie uruchom polecenie node index . Zobaczysz następujące -

Podczas budowania naszego serwera sygnalizacyjnego użyjemy biblioteki WebSockets dla Node.js. Aby zainstalować w run npm zainstaluj ws w terminalu.

Do testowania naszego serwera sygnalizacyjnego użyjemy narzędzia wscat. Aby go zainstalować, uruchom polecenie npm install -g wscat w oknie terminala.

S.Nr Protokoły i opis
1 Protokoły WebRTC

Aplikacje WebRTC używają UDP (User Datagram Protocol) jako protokołu transportowego. Większość dzisiejszych aplikacji internetowych jest tworzona przy użyciu protokołu TCP (Transmission Control Protocol)

2 Protokół opisu sesji

SDP jest ważną częścią WebRTC. Jest to protokół przeznaczony do opisywania sesji komunikacji medialnej.

3 Znajdowanie trasy

Aby połączyć się z innym użytkownikiem, należy wyznaczyć jasną ścieżkę wokół własnej sieci i sieci innego użytkownika. Istnieją jednak szanse, że używana sieć ma kilka poziomów kontroli dostępu, aby uniknąć problemów z bezpieczeństwem.

4 Protokół transmisji sterowania strumieniem

Dzięki połączeniu peer mamy możliwość szybkiego przesyłania danych wideo i audio. Protokół SCTP jest obecnie używany do wysyłania danych typu blob na podstawie naszego aktualnie skonfigurowanego połączenia równorzędnego podczas korzystania z obiektu RTCDataChannel.

Podsumowanie

W tym rozdziale omówiliśmy kilka technologii umożliwiających połączenia równorzędne, takich jak UDP, TCP, STUN, TURN, ICE i SCTP. Powinieneś teraz mieć zrozumienie na poziomie powierzchni, jak działa SDP i jego przypadki użycia.

Interfejs API MediaStream został zaprojektowany w celu łatwego dostępu do strumieni multimediów z lokalnych kamer i mikrofonów. Metoda getUserMedia () jest podstawowym sposobem uzyskiwania dostępu do lokalnych urządzeń wejściowych.

API ma kilka kluczowych punktów -

  • Strumień mediów w czasie rzeczywistym jest reprezentowany przez obiekt strumienia w postaci obrazu lub dźwięku

  • Zapewnia poziom bezpieczeństwa dzięki uprawnieniom użytkownika pytającym użytkownika, zanim aplikacja internetowa będzie mogła rozpocząć pobieranie strumienia

  • Wybór urządzeń wejściowych jest obsługiwany przez MediaStream API (na przykład, gdy do urządzenia są podłączone dwie kamery lub mikrofony)

Każdy obiekt MediaStream zawiera kilka obiektów MediaStreamTrack. Reprezentują obraz i dźwięk z różnych urządzeń wejściowych.

Każdy obiekt MediaStreamTrack może zawierać kilka kanałów (prawy i lewy kanał audio). Są to najmniejsze części zdefiniowane przez MediaStream API.

Istnieją dwa sposoby generowania obiektów MediaStream. Po pierwsze, możemy wyrenderować dane wyjściowe do elementu wideo lub audio. Po drugie, możemy przesłać dane wyjściowe do obiektu RTCPeerConnection, który następnie wyśle ​​je do zdalnego peera.

Korzystanie z interfejsu API MediaStream

Stwórzmy prostą aplikację WebRTC. Wyświetli element wideo na ekranie, poprosi użytkownika o pozwolenie na korzystanie z kamery i wyświetli strumień wideo na żywo w przeglądarce. Utwórz plik index.html -

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head> 
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body>
	
</html>

Następnie utwórz plik client.js i dodaj następujące elementy;

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"); 
}

Tutaj tworzymy funkcję hasUserMedia (), która sprawdza, czy WebRTC jest obsługiwany, czy nie. Następnie uzyskujemy dostęp do funkcji getUserMedia, w której drugim parametrem jest callback akceptujący strumień pochodzący z urządzenia użytkownika. Następnie ładujemy nasz strumień do elementu wideo za pomocą window.URL.createObjectURL, który tworzy adres URL reprezentujący obiekt podany w parametrze.

Teraz odśwież stronę, kliknij Zezwól, a na ekranie powinna pojawić się Twoja twarz.

Pamiętaj, aby uruchamiać wszystkie swoje skrypty na serwerze WWW. Zainstalowaliśmy już jeden w samouczku dotyczącym środowiska WebRTC.

MediaStream API

Nieruchomości

  • MediaStream.active (read only) - Zwraca wartość true, jeśli MediaStream jest aktywny, lub false w przeciwnym razie.

  • MediaStream.ended (read only, deprecated)- Zwróć wartość true, jeśli zakończone zdarzenie zostało uruchomione na obiekcie, co oznacza, że ​​strumień został całkowicie odczytany, lub false, jeśli koniec strumienia nie został osiągnięty.

  • MediaStream.id (read only) - Unikalny identyfikator obiektu.

  • MediaStream.label (read only, deprecated) - Unikalny identyfikator przypisany przez klienta użytkownika.

Możesz zobaczyć, jak powyższe właściwości wyglądają w mojej przeglądarce -

Obsługa zdarzeń

  • MediaStream.onactive- Procedura obsługi aktywnego zdarzenia, które jest wywoływane , gdy obiekt MediaStream staje się aktywny.

  • MediaStream.onaddtrack- Procedura obsługi zdarzenia addtrack , które jest wywoływane po dodaniu nowego obiektu MediaStreamTrack .

  • MediaStream.onended (deprecated)- Procedura obsługi zakończonego zdarzenia, które jest uruchamiane, gdy przesyłanie strumieniowe jest kończone.

  • MediaStream.oninactive- Procedura obsługi nieaktywnego zdarzenia, które jest wywoływane , gdy obiekt MediaStream staje się nieaktywny.

  • MediaStream.onremovetrack- Procedura obsługi zdarzenia removetrack , które jest wywoływane po usunięciu z niego obiektu MediaStreamTrack .

Metody

  • MediaStream.addTrack()- Dodaje obiekt MediaStreamTrack podany jako argument do MediaStream. Jeśli utwór został już dodany, nic się nie dzieje.

  • MediaStream.clone() - Zwraca klon obiektu MediaStream z nowym identyfikatorem.

  • MediaStream.getAudioTracks()- Zwraca listę obiektów audio MediaStreamTrack z obiektu MediaStream .

  • MediaStream.getTrackById()- Zwraca utwór według identyfikatora. Jeśli argument jest pusty lub identyfikator nie zostanie znaleziony, zwraca wartość null. Jeśli kilka ścieżek ma ten sam identyfikator, zwraca pierwszą.

  • MediaStream.getTracks()- Zwraca listę wszystkich obiektów MediaStreamTrack z obiektu MediaStream .

  • MediaStream.getVideoTracks()- Zwraca listę obiektów wideo MediaStreamTrack z obiektu MediaStream .

  • MediaStream.removeTrack()- Usuwa obiekt MediaStreamTrack podany jako argument z MediaStream. Jeśli ścieżka została już usunięta, nic się nie dzieje.

Aby przetestować powyższe API, zmień plik index.html w następujący sposób -

<!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>

Dodaliśmy kilka przycisków, aby wypróbować kilka interfejsów API MediaStream. Następnie powinniśmy dodać programy obsługi zdarzeń dla naszego nowo utworzonego przycisku. Zmodyfikuj plik client.js w ten sposób -

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]); 
});

Teraz odśwież swoją stronę. Kliknij przycisk getAudioTracks () , a następnie kliknij przycisk removeTrack () - audio . Teraz ścieżka dźwiękowa powinna zostać usunięta. Następnie zrób to samo dla ścieżki wideo.

Jeśli klikniesz przycisk getTracks () , powinieneś zobaczyć wszystkie MediaStreamTracks (wszystkie podłączone wejścia wideo i audio). Następnie kliknij getTrackById (), aby uzyskać dźwięk MediaStreamTrack.

Podsumowanie

W tym rozdziale stworzyliśmy prostą aplikację WebRTC przy użyciu API MediaStream. Teraz powinieneś mieć jasny przegląd różnych API MediaStream, które sprawiają, że WebRTC działa.

Interfejs API RTCPeerConnection jest rdzeniem połączenia peer-to-peer między każdą z przeglądarek. Aby utworzyć obiekty RTCPeerConnection, po prostu napisz

var pc = RTCPeerConnection(config);

gdzie argument config zawiera przynajmniej jeden klucz, iceServers. Jest to tablica obiektów URL zawierających informacje o serwerach STUN i TURN, wykorzystywana podczas wyszukiwania kandydatów ICE. Listę dostępnych publicznych serwerów STUN można znaleźć na code.google.com

W zależności od tego, czy jesteś dzwoniącym, czy odbierającym, obiekt RTCPeerConnection jest używany w nieco inny sposób po każdej stronie połączenia.

Oto przykład przepływu użytkownika -

  • Zarejestruj moduł obsługi onicecandidate . Wysyła wszystkich kandydatów ICE do drugiego peera, gdy są odbierane.

  • Zarejestruj procedurę obsługi onaddstream . Obsługuje wyświetlanie strumienia wideo po odebraniu go od zdalnego partnera.

  • Zarejestruj procedurę obsługi wiadomości . Twój serwer sygnalizacyjny powinien również mieć program obsługi wiadomości otrzymanych od drugiego peera. Jeśli wiadomość zawiera obiekt RTCSessionDescription , należy go dodać do obiektu RTCPeerConnection za pomocą metody setRemoteDescription () . Jeśli wiadomość zawiera obiekt RTCIceCandidate , należy go dodać do obiektu RTCPeerConnection za pomocą metody addIceCandidate () .

  • Użyj metody getUserMedia (), aby skonfigurować lokalny strumień multimediów i dodać go do obiektu RTCPeerConnection za pomocą metody addStream () .

  • Rozpocznij proces negocjacji ofert / odpowiedzi. Jest to jedyny krok, w którym przepływ osoby dzwoniącej różni się od przepływu osoby odbierającej. Obiekt wywołujący rozpoczyna negocjację przy użyciu metody createOffer () i rejestruje wywołanie zwrotne, które odbiera obiekt RTCSessionDescription . Następnie to wywołanie zwrotne powinno dodać ten obiekt RTCSessionDescription do obiektu RTCPeerConnection przy użyciu metody setLocalDescription () . I na koniec dzwoniący powinien wysłać ten RTCSessionDescription do zdalnego peera za pomocą serwera sygnalizacyjnego. Z drugiej strony wywoływany rejestruje to samo wywołanie zwrotne, ale w metodzie createAnswer () . Zwróć uwagę, że przepływ wywoływanych jest inicjowany dopiero po otrzymaniu oferty od dzwoniącego.

RTCPeerConnection API

Nieruchomości

  • RTCPeerConnection.iceConnectionState (read only)- Zwraca wyliczenie RTCIceConnectionState, które opisuje stan połączenia. Po zmianie tej wartości wyzwalane jest zdarzenie iceconnectionstatechange. Możliwe wartości -

    • new - agent ICE czeka na zdalnych kandydatów lub zbiera adresy

    • checking - agent ICE ma zdalnych kandydatów, ale nie znalazł jeszcze połączenia

    • connected - agent ICE znalazł użyteczne połączenie, ale nadal szuka zdalnego kandydata, aby uzyskać lepsze połączenie.

    • completed - agent ICE znalazł użyteczne połączenie i przestał testować zdalnych kandydatów.

    • failed - agent ICE sprawdził wszystkich zdalnych kandydatów, ale nie znalazł dopasowania dla co najmniej jednego komponentu.

    • disconnected - co najmniej jeden składnik już nie żyje.

    • closed - agent ICE jest zamknięty.

  • RTCPeerConnection.iceGatheringState (read only) - Zwraca wyliczenie RTCIceGatheringState, które opisuje stan zbierania ICE dla połączenia -

    • new - obiekt został właśnie utworzony.

    • gathering - agent ICE zbiera kandydatów

    • complete agent ICE zakończył gromadzenie.

  • RTCPeerConnection.localDescription (read only)- Zwraca RTCSessionDescription opisujący lokalną sesję. Może mieć wartość null, jeśli nie została jeszcze ustawiona.

  • RTCPeerConnection.peerIdentity (read only)- Zwraca RTCIdentityAssertion. Składa się z idp (nazwa domeny) i nazwy reprezentującej tożsamość zdalnego peera.

  • RTCPeerConnection.remoteDescription (read only)- Zwróć RTCSessionDescription opisujący sesję zdalną. Może mieć wartość null, jeśli nie została jeszcze ustawiona.

  • RTCPeerConnection.signalingState (read only)- Zwraca wyliczenie RTCSignalingState, które opisuje stan sygnalizacji połączenia lokalnego. Ten stan opisuje ofertę SDP. W przypadku zmiany tej wartości wyzwalane jest sygnalizujące zdarzenie zmiany stanu. Możliwe wartości -

    • stable- Stan początkowy. Nie trwa wymiana ofert / odpowiedzi SDP.

    • have-local-offer - lokalna strona połączenia zastosowała lokalnie ofertę SDP.

    • have-remote-offer - zdalna strona połączenia zastosowała lokalnie ofertę SDP.

    • have-local-pranswer - zastosowano zdalną ofertę SDP i lokalnie zastosowano usługę wyszukiwania SDP.

    • have-remote-pranswer - zastosowano lokalny SDP i zdalnie zastosowano odpowiedź SDP.

    • closed - połączenie jest zamknięte.

Obsługa zdarzeń

S.No. Obsługa i opis zdarzeń
1

RTCPeerConnection.onaddstream

Ta procedura obsługi jest wywoływana po wyzwoleniu zdarzenia addstream. To zdarzenie jest wysyłane, gdy MediaStream jest dodawany do tego połączenia przez zdalnego partnera.

2

RTCPeerConnection.ondatachannel

Ta procedura obsługi jest wywoływana po uruchomieniu zdarzenia datachannel. To zdarzenie jest wysyłane po dodaniu RTCDataChannel do tego połączenia.

3

RTCPeerConnection.onicecandidate

Ta procedura obsługi jest wywoływana, gdy uruchamiane jest zdarzenie icecandidate. To zdarzenie jest wysyłane po dodaniu obiektu RTCIceCandidate do skryptu.

4

RTCPeerConnection.oniceconnectionstatechange

Ta procedura obsługi jest wywoływana po uruchomieniu zdarzenia iceconnectionstatechange. To zdarzenie jest wysyłane, gdy zmienia się wartość iceConnectionState.

5

RTCPeerConnection.onidentityresult

Ten program obsługi jest wywoływany po wyzwoleniu zdarzenia identityresult. To zdarzenie jest wysyłane, gdy podczas tworzenia oferty lub odpowiedzi za pośrednictwem getIdentityAssertion () generowane jest potwierdzenie tożsamości.

6

RTCPeerConnection.onidpassertionerror

Ten program obsługi jest wywoływany po uruchomieniu zdarzenia idpassertionerror. To zdarzenie jest wysyłane, gdy IdP (dostawca tożsamości) napotka błąd podczas generowania potwierdzenia tożsamości.

7

RTCPeerConnection.onidpvalidation

Ten program obsługi jest wywoływany, gdy uruchamiane jest zdarzenie idpvalidationerror. To zdarzenie jest wysyłane, gdy IdP (dostawca tożsamości) napotka błąd podczas sprawdzania poprawności potwierdzenia tożsamości.

8

RTCPeerConnection.onnegotiationneeded

Ten program obsługi jest wywoływany, gdy zostanie wyzwolone zdarzenie negotiationneeded. To zdarzenie jest wysyłane przez przeglądarkę, aby poinformować, że negocjacje będą wymagane w pewnym momencie w przyszłości.

9

RTCPeerConnection.onpeeridentity

Ten program obsługi jest wywoływany, gdy wyzwalane jest zdarzenie peeridentity. To zdarzenie jest wysyłane, gdy tożsamość równorzędna została ustawiona i zweryfikowana w tym połączeniu.

10

RTCPeerConnection.onremovestream

Ten program obsługi jest wywoływany, gdy jest wyzwalane zdarzenie signalingstatechange. To zdarzenie jest wysyłane, gdy zmienia się wartość signalingState.

11

RTCPeerConnection.onsignalingstatechange

Ta procedura obsługi jest wywoływana po wyzwoleniu zdarzenia removestream. To zdarzenie jest wysyłane po usunięciu MediaStream z tego połączenia.

Metody

S.No. Metody i opis
1

RTCPeerConnection()

Zwraca nowy obiekt RTCPeerConnection.

2

RTCPeerConnection.createOffer()

Tworzy ofertę (żądanie) znalezienia zdalnego partnera. Dwa pierwsze parametry tej metody to wywołania zwrotne sukcesu i błędu. Opcjonalny trzeci parametr to opcje, takie jak włączanie strumieni audio lub wideo.

3

RTCPeerConnection.createAnswer()

Tworzy odpowiedź na ofertę otrzymaną przez zdalnego peera podczas procesu negocjacji oferty / odpowiedzi. Dwa pierwsze parametry tej metody to wywołania zwrotne sukcesu i błędu. Opcjonalny trzeci parametr to opcje tworzenia odpowiedzi.

4

RTCPeerConnection.setLocalDescription()

Zmienia opis połączenia lokalnego. Opis określa właściwości połączenia. Połączenie musi obsługiwać zarówno stare, jak i nowe opisy. Metoda przyjmuje trzy parametry, obiekt RTCSessionDescription, callback w przypadku udanej zmiany opisu, callback w przypadku niepowodzenia zmiany opisu.

5

RTCPeerConnection.setRemoteDescription()

Zmienia opis połączenia zdalnego. Opis określa właściwości połączenia. Połączenie musi obsługiwać zarówno stare, jak i nowe opisy. Metoda przyjmuje trzy parametry, obiekt RTCSessionDescription, callback w przypadku udanej zmiany opisu, callback w przypadku niepowodzenia zmiany opisu.

6

RTCPeerConnection.updateIce()

Aktualizuje proces agenta ICE pingowania zdalnych kandydatów i zbierania lokalnych kandydatów.

7

RTCPeerConnection.addIceCandidate()

Udostępnia zdalnego kandydata agentowi ICE.

8

RTCPeerConnection.getConfiguration()

Zwraca obiekt RTCConfiguration. Reprezentuje konfigurację obiektu RTCPeerConnection.

9

RTCPeerConnection.getLocalStreams()

Zwraca tablicę lokalnych połączeń MediaStream.

10

RTCPeerConnection.getRemoteStreams()

Zwraca tablicę zdalnego połączenia MediaStream.

11

RTCPeerConnection.getStreamById()

Zwraca lokalny lub zdalny MediaStream o podanym identyfikatorze.

12

RTCPeerConnection.addStream()

Dodaje MediaStream jako lokalne źródło wideo lub audio.

13

RTCPeerConnection.removeStream()

Usuwa MediaStream jako lokalne źródło wideo lub audio.

14

RTCPeerConnection.close()

Zamyka połączenie.

15

RTCPeerConnection.createDataChannel()

Tworzy nowy RTCDataChannel.

16

RTCPeerConnection.createDTMFSender()

Tworzy nowy RTCDTMFSender, powiązany z określonym MediaStreamTrack. Umożliwia przesyłanie sygnalizacji telefonicznej DTMF (Dual-tone multifrequency) przez połączenie.

17

RTCPeerConnection.getStats()

Tworzy nowy raport RTCStatsReport zawierający statystyki dotyczące połączenia.

18

RTCPeerConnection.setIdentityProvider()

Ustawia IdP. Przyjmuje trzy parametry - nazwę, protokół używany do komunikacji i opcjonalną nazwę użytkownika.

19

RTCPeerConnection.getIdentityAssertion()

Zbiera potwierdzenie tożsamości. Nie oczekuje się, że ta metoda zajmie się w aplikacji. Możesz więc nazwać to wyraźnie tylko po to, aby przewidzieć potrzebę.

Nawiązywanie połączenia

Teraz stwórzmy przykładową aplikację. Najpierw uruchom serwer sygnalizacyjny, który stworzyliśmy w samouczku „Serwer sygnalizacyjny” za pośrednictwem „serwera węzłowego”.

Na stronie będą dwa pola tekstowe, jedno do logowania, a drugie do nazwy użytkownika, z którą chcemy się połączyć. Utwórz plik index.html i dodaj następujący kod -

<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>

Widać, że dodaliśmy pole tekstowe do logowania, przycisk logowania, wpis tekstowy dla innej nazwy użytkownika równorzędnego i przycisk połącz się z nim. Teraz utwórz plik client.js i dodaj następujący kod -

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)); 
};

Widać, że nawiązujemy połączenie przez gniazdo z naszym serwerem sygnalizacyjnym. Gdy użytkownik kliknie przycisk logowania, aplikacja wysyła jego nazwę użytkownika na serwer. Jeśli logowanie powiedzie się, aplikacja tworzy obiekt RTCPeerConnection i konfiguruje procedurę obsługi onicecandidate, która wysyła wszystkie znalezione icecandidates do drugiego peera. Teraz otwórz stronę i spróbuj się zalogować. Powinieneś zobaczyć następujące dane wyjściowe konsoli -

Następnym krokiem jest utworzenie oferty dla innego peera. Dodaj następujący kod do pliku 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)); 
}

Możesz zobaczyć, że gdy użytkownik kliknie przycisk „Ustanów połączenie”, aplikacja wysyła ofertę SDP do drugiego peera. Ustawiliśmy również obsługę OnAnswer i onCandidate . Załaduj ponownie swoją stronę, otwórz ją w dwóch zakładkach, zaloguj się z dwoma użytkownikami i spróbuj nawiązać połączenie między nimi. Powinieneś zobaczyć następujące dane wyjściowe konsoli -

Teraz nawiązane jest połączenie peer-to-peer. W następnych samouczkach dodamy strumienie wideo i audio, a także obsługę czatu tekstowego.

WebRTC jest nie tylko dobry w przesyłaniu strumieni audio i wideo, ale także wszelkich dowolnych danych, które możemy mieć. W tym miejscu do gry wchodzi obiekt RTCDataChannel.

RTCDataChannel API

Nieruchomości

  • RTCDataChannel.label (read only) - Zwraca łańcuch zawierający nazwę kanału danych.

  • RTCDataChannel.ordered (read only) - Zwraca wartość true, jeśli kolejność dostarczania wiadomości jest gwarantowana lub false, jeśli nie jest gwarantowana.

  • RTCDataChannel.protocol (read only) - Zwraca ciąg zawierający nazwę podprotokołu używaną dla tego kanału.

  • RTCDataChannel.id (read only) - Zwraca unikatowy identyfikator kanału, który jest ustawiony podczas tworzenia obiektu RTCDataChannel.

  • RTCDataChannel.readyState (read only)- Zwraca wyliczenie RTCDataChannelState reprezentujące stan połączenia. Możliwe wartości -

    • connecting- Wskazuje, że połączenie nie jest jeszcze aktywne. To jest stan początkowy.

    • open - Wskazuje, że połączenie jest uruchomione.

    • closing- Wskazuje, że trwa zamykanie połączenia. Wiadomości w pamięci podręcznej są w trakcie wysyłania lub odbierania, ale żadne nowo utworzone zadanie nie jest akceptowane.

    • closed - Wskazuje, że nie można nawiązać połączenia lub zostało ono zamknięte.

  • RTCDataChannel.bufferedAmount (read only)- Zwraca ilość bajtów w kolejce do wysłania. To jest ilość danych, które nie zostały jeszcze wysłane przez RTCDataChannel.send ().

  • RTCDataChannel.bufferedAmountLowThreshold- Zwraca liczbę bajtów, przy której RTCDataChannel.bufferedAmount jest przyjmowana jako mała. Gdy wartość RTCDataChannel.bufferedAmount spadnie poniżej tego progu, wyzwalane jest zdarzenie bufferedamountlow.

  • RTCDataChannel.binaryType- Zwraca typ danych binarnych przesyłanych przez połączenie. Może być „blob” lub „arraybuffer”.

  • RTCDataChannel.maxPacketLifeType (read only) - Zwraca krótki znak bez znaku, który wskazuje długość okna w milisekundach, gdy komunikaty są w trybie zawodnym.

  • RTCDataChannel.maxRetransmits (read only) - Zwraca skrót bez znaku, który wskazuje maksymalną liczbę retransmisji danych przez kanał, jeśli nie zostaną one dostarczone.

  • RTCDataChannel.negotiated (read only) - Zwraca wartość logiczną wskazującą, czy kanał został wynegocjowany przez klienta użytkownika, czy przez aplikację.

  • RTCDataChannel.reliable (read only) - Zwraca wartość logiczną wskazującą, że połączenie może wysyłać wiadomości w trybie zawodnym.

  • RTCDataChannel.stream (read only) - synonim RTCDataChannel.id

Obsługa zdarzeń

  • RTCDataChannel.onopen- Ten program obsługi zdarzeń jest wywoływany po uruchomieniu zdarzenia open. To zdarzenie jest wysyłane po nawiązaniu połączenia danych.

  • RTCDataChannel.onmessage- Ten program obsługi zdarzeń jest wywoływany po wyzwoleniu zdarzenia komunikatu. Zdarzenie jest wysyłane, gdy w kanale danych jest dostępna wiadomość.

  • RTCDataChannel.onbufferedamountlow- Ten program obsługi zdarzeń jest wywoływany po wyzwoleniu zdarzenia bufferedamoutlow. To zdarzenie jest wysyłane, gdy wartość RTCDataChannel.bufferedAmount spadnie poniżej właściwości RTCDataChannel.bufferedAmountLowThreshold.

  • RTCDataChannel.onclose- Ten program obsługi zdarzeń jest wywoływany, gdy uruchamiane jest zdarzenie close. To zdarzenie jest wysyłane po zamknięciu połączenia danych.

  • RTCDataChannel.onerror- Ten program obsługi zdarzeń jest wywoływany po wyzwoleniu zdarzenia błędu. To zdarzenie jest wysyłane po napotkaniu błędu.

Metody

  • RTCDataChannel.close() - zamyka kanał danych.

  • RTCDataChannel.send()- Wysyła dane w parametrze przez kanał. Dane mogą być obiektem blob, ciągiem, ArrayBuffer lub ArrayBufferView.

Teraz stwórzmy prosty przykład. Najpierw uruchom serwer sygnalizacyjny, który stworzyliśmy w samouczku „Serwer sygnalizacyjny” za pośrednictwem „serwera węzłowego”.

Na stronie pojawią się trzy pola tekstowe, jedno dla logowania, jedno dla nazwy użytkownika i jedno dla wiadomości, którą chcemy wysłać do drugiego peera. Utwórz plik index.html i dodaj następujący kod -

<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>

Dodaliśmy również trzy przyciski do logowania, nawiązywania połączenia i wysyłania wiadomości. Teraz utwórz plik client.js i dodaj następujący kod -

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)); 
};

Widać, że nawiązujemy połączenie przez gniazdo z naszym serwerem sygnalizacyjnym. Gdy użytkownik kliknie przycisk logowania, aplikacja wysyła jego nazwę użytkownika na serwer. Jeśli logowanie powiedzie się, aplikacja tworzy obiekt RTCPeerConnection i konfiguruje procedurę obsługi onicecandidate, która wysyła wszystkie znalezione icecandidates do drugiego peera. Uruchamia również funkcję openDataChannel (), która tworzy dataChannel. Zwróć uwagę, że podczas tworzenia obiektu RTCPeerConnection drugi argument w konstruktorze jest opcjonalny: [{RtpDataChannels: true}] jest obowiązkowy, jeśli używasz przeglądarki Chrome lub Opera. Następnym krokiem jest utworzenie oferty dla innego peera. Dodaj następujący kod do swojego pliku 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)); 
}

Możesz zobaczyć, że gdy użytkownik kliknie przycisk „Ustanów połączenie”, aplikacja wysyła ofertę SDP do drugiego peera. Ustawiliśmy również obsługę OnAnswer i onCandidate . Na koniec zaimplementujmy funkcję openDataChannel (), która tworzy nasz dataChannel. Dodaj następujący kod do pliku 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); 
});

Tutaj tworzymy dataChannel dla naszego połączenia i dodajemy procedurę obsługi zdarzenia dla przycisku „wyślij wiadomość”. Teraz otwórz tę stronę w dwóch zakładkach, zaloguj się z dwoma użytkownikami, nawiąż połączenie i spróbuj wysyłać wiadomości. Powinieneś je zobaczyć na wyjściu konsoli. Zauważ, że powyższy przykład został przetestowany w Operze.

Teraz możesz zobaczyć, że RTCDataChannel jest niezwykle potężną częścią interfejsu API WebRTC. Istnieje wiele innych przypadków użycia tego obiektu, takich jak gry peer-to-peer lub udostępnianie plików w oparciu o torrent.

Większość aplikacji WebRTC umożliwia komunikację nie tylko za pośrednictwem wideo i audio. Potrzebują wielu innych funkcji. W tym rozdziale zbudujemy podstawowy serwer sygnalizacyjny.

Sygnalizacja i negocjacje

Aby połączyć się z innym użytkownikiem, powinieneś wiedzieć, gdzie on się znajduje w sieci. Adres IP urządzenia umożliwia urządzeniom z dostępem do Internetu bezpośrednie przesyłanie danych między sobą. RTCPeerConnection obiekt jest za to odpowiedzialny. Gdy tylko urządzenia dowiedzą się, jak znaleźć się w Internecie, zaczynają wymieniać dane o protokołach i kodekach obsługiwanych przez każde urządzenie.

Aby komunikować się z innym użytkownikiem, wystarczy wymienić informacje kontaktowe, a resztą zajmie się WebRTC. Proces łączenia się z innym użytkownikiem jest również nazywany sygnalizacją i negocjacjami. Składa się z kilku kroków -

  • Utwórz listę potencjalnych kandydatów do połączenia równorzędnego.

  • Użytkownik lub aplikacja wybiera użytkownika, z którym ma nawiązać połączenie.

  • Warstwa sygnalizacyjna powiadamia innego użytkownika, że ​​ktoś chce się z nim połączyć. Może zaakceptować lub odrzucić.

  • Pierwszy użytkownik zostaje powiadomiony o przyjęciu oferty.

  • Pierwszy użytkownik inicjuje połączenie RTCPeerConnection z innym użytkownikiem.

  • Obaj użytkownicy wymieniają informacje o oprogramowaniu i sprzęcie za pośrednictwem serwera sygnalizacyjnego.

  • Obaj użytkownicy wymieniają się informacjami o lokalizacji.

  • Połączenie powiodło się lub nie.

Specyfikacja WebRTC nie zawiera żadnych standardów dotyczących wymiany informacji. Pamiętaj więc, że powyższe to tylko przykład tego, jak może się zdarzyć sygnalizacja. Możesz użyć dowolnego protokołu lub technologii.

Budowanie serwera

Serwer, który będziemy budować, będzie mógł łączyć ze sobą dwóch użytkowników, którzy nie znajdują się na tym samym komputerze. Stworzymy własny mechanizm sygnalizacyjny. Nasz serwer sygnalizacyjny pozwoli jednemu użytkownikowi zadzwonić do innego. Gdy użytkownik zadzwoni do innego, serwer przekazuje ofertę, odpowiada, kandydatów ICE między nimi i zestawia połączenie WebRTC.

Powyższy diagram przedstawia przepływ wiadomości między użytkownikami podczas korzystania z serwera sygnalizacyjnego. Przede wszystkim każdy użytkownik rejestruje się na serwerze. W naszym przypadku będzie to prosta nazwa użytkownika w postaci ciągu znaków. Po zarejestrowaniu się użytkownicy mogą dzwonić do siebie. Użytkownik 1 składa ofertę z identyfikatorem użytkownika, do którego chce zadzwonić. Drugi użytkownik powinien odpowiedzieć. Wreszcie kandydaci ICE są przesyłani między użytkownikami, dopóki nie będą mogli nawiązać połączenia.

Aby utworzyć połączenie WebRTC, klienci muszą mieć możliwość przesyłania wiadomości bez korzystania z połączenia równorzędnego WebRTC. W tym miejscu użyjemy HTML5 WebSockets - dwukierunkowego połączenia gniazdowego między dwoma punktami końcowymi - serwerem WWW i przeglądarką internetową. Teraz zacznijmy korzystać z biblioteki WebSocket. Utwórz plik server.js i wstaw następujący kod -

//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"); 
});

Pierwsza linia wymaga biblioteki WebSocket, którą już zainstalowaliśmy. Następnie tworzymy serwer gniazd na porcie 9090. Następnie nasłuchujemy zdarzenia połączenia . Ten kod zostanie wykonany, gdy użytkownik nawiąże połączenie WebSocket z serwerem. Następnie odsłuchujemy wszelkie wiadomości wysłane przez użytkownika. Na koniec wysyłamy odpowiedź do podłączonego użytkownika mówiąc „Witaj z serwera”.

Teraz uruchom serwer węzłów, a serwer powinien rozpocząć nasłuchiwanie połączeń gniazd.

Aby przetestować nasz serwer, użyjemy narzędzia wscat , które również już zainstalowaliśmy. To narzędzie pomaga w łączeniu się bezpośrednio z serwerem WebSocket i testowaniu poleceń. Uruchom nasz serwer w jednym oknie terminala, a następnie otwórz inny i uruchom polecenie wscat -c ws: // localhost: 9090 . Po stronie klienta powinieneś zobaczyć następujące informacje -

Serwer powinien również logować podłączonego użytkownika -

Rejestracja Użytkownika

Na naszym serwerze sygnalizacyjnym będziemy używać nazwy użytkownika opartej na ciągach znaków dla każdego połączenia, aby wiedzieć, gdzie wysyłać wiadomości. Zmieńmy nieco naszą obsługę połączenia -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
	
});

W ten sposób akceptujemy tylko wiadomości JSON. Następnie musimy gdzieś przechowywać wszystkich podłączonych użytkowników. Użyjemy do tego prostego obiektu Javascript. Zmień górę naszego pliku -

//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 = {};

Zamierzamy dodać pole typu dla każdej wiadomości przychodzącej od klienta. Na przykład, jeśli użytkownik chce się zalogować, wysyła komunikat typu logowania . Zdefiniujmy to -

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; 
   } 
	
});

Jeśli użytkownik wyśle ​​wiadomość z typem logowania , my -

  • Sprawdź, czy ktoś już się zalogował przy użyciu tej nazwy użytkownika

  • Jeśli tak, powiedz użytkownikowi, że nie udało mu się zalogować

  • Jeśli nikt nie używa tej nazwy użytkownika, dodajemy nazwę użytkownika jako klucz do obiektu połączenia.

  • Jeśli polecenie nie zostanie rozpoznane, wysyłamy błąd.

Poniższy kod jest funkcją pomocniczą do wysyłania wiadomości do połączenia. Dodaj go do pliku server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Powyższa funkcja zapewnia, że ​​wszystkie nasze wiadomości są wysyłane w formacie JSON.

Gdy użytkownik się rozłączy, powinniśmy wyczyścić jego połączenie. Możemy usunąć użytkownika po uruchomieniu zdarzenia close . Dodaj następujący kod do programu obsługi połączenia -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
    } 
});

Teraz przetestujmy nasz serwer za pomocą polecenia login. Pamiętaj, że wszystkie wiadomości muszą być zakodowane w formacie JSON. Uruchom nasz serwer i spróbuj się zalogować. Powinieneś zobaczyć coś takiego -

Wykonywanie połączenia

Po pomyślnym zalogowaniu użytkownik chce zadzwonić do innego. Powinien złożyć ofertę innemu użytkownikowi, aby to osiągnąć. Dodaj obsługę oferty -

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;

Po pierwsze uzyskujemy połączenie użytkownika, do którego próbujemy zadzwonić. Jeśli istnieje, wysyłamy mu szczegóły oferty . Dodajemy również otherName do obiektu połączenia . Ma to na celu ułatwienie późniejszego znalezienia.

Odpowiadanie

Odpowiadanie na odpowiedź ma podobny wzorzec, którego użyliśmy w module obsługi ofert . Nasz serwer po prostu przekazuje wszystkie wiadomości jako odpowiedź do innego użytkownika. Dodaj następujący kod po przekazaniu oferty -

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;

Możesz zobaczyć, jak to jest podobne do obsługi ofert . Zwróć uwagę, że ten kod jest zgodny z funkcjami createOffer i createAnswer w obiekcie RTCPeerConnection .

Teraz możemy przetestować mechanizm oferty / odpowiedzi. Połącz dwóch klientów w tym samym czasie i spróbuj złożyć ofertę i odpowiedzieć. Powinieneś zobaczyć następujące -

W tym przykładzie offer i answer są prostymi ciągami, ale w prawdziwej aplikacji zostaną wypełnione danymi SDP.

Kandydaci ICE

Ostatnią częścią jest obsługa kandydata ICE pomiędzy użytkownikami. Używamy tej samej techniki po prostu do przekazywania wiadomości między użytkownikami. Główna różnica polega na tym, że wiadomości kandydatów mogą pojawiać się wiele razy na użytkownika w dowolnej kolejności. Dodaj kandydata do obsługi -

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;

Powinien działać podobnie do obsługi ofert i odpowiedzi .

Opuszczanie połączenia

Aby umożliwić naszym użytkownikom rozłączenie się z innym użytkownikiem, powinniśmy zaimplementować funkcję rozłączania. Poinformuje również serwer, aby usunął wszystkie odniesienia użytkowników. Dodajleave handler -

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;

Spowoduje to również wysłanie innemu użytkownikowi zdarzenia wyjścia , aby mógł on odpowiednio rozłączyć połączenie równorzędne. Powinniśmy również zająć się przypadkiem, gdy użytkownik zrywa swoje połączenie z serwera sygnalizacyjnego. Zmodyfikujmy naszą bliską obsługę -

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" 
            }); 
         }  
      } 
   } 
});

Teraz, jeśli połączenie zostanie przerwane, nasi użytkownicy zostaną rozłączeni. Blisko zdarzenie zostanie zwolniony, gdy użytkownik zamyka swoje okno przeglądarki, gdy jesteśmy jeszcze w ofercie , odpowiedzi lub kandydującego państwa.

Kompletny serwer sygnalizacyjny

Oto cały kod naszego serwera sygnalizacyjnego -

//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)); 
}

Tak więc praca jest zakończona, a nasz serwer sygnalizacyjny jest gotowy. Pamiętaj, że robienie rzeczy poza kolejnością podczas nawiązywania połączenia WebRTC może powodować problemy.

Podsumowanie

W tym rozdziale zbudowaliśmy prosty i nieskomplikowany serwer sygnalizacyjny. Przeszliśmy przez proces sygnalizacji, rejestracji użytkowników oraz mechanizm ofert / odpowiedzi. Wdrożyliśmy również wysyłanie kandydatów między użytkownikami.

Sieć rozwija się tak szybko i zawsze się poprawia. Codziennie powstają nowe standardy. Przeglądarki pozwalają na instalowanie aktualizacji bez wiedzy użytkownika, więc powinieneś być na bieżąco z tym, co dzieje się w świecie sieci i WebRTC. Oto przegląd tego, co to jest do dzisiaj.

Wsparcie przeglądarki

Nie każda przeglądarka ma wszystkie te same funkcje WebRTC w tym samym czasie. Różne przeglądarki mogą wyprzedzać konkurencję, co powoduje, że niektóre funkcje WebRTC działają w jednej przeglądarce, a nie w innej. Bieżącą obsługę WebRTC w przeglądarce przedstawia poniższy rysunek.

Aktualny stan wsparcia WebRTC można sprawdzić pod adresem http://caniuse.com/#feat=rtcpeerconnection.

Chrome, Firefox i Opera

Najnowsze wersje przeglądarek Chrome, Firefox i Opera w popularnych systemach operacyjnych dla komputerów PC, takich jak Mac OS X, Windows i Linux, obsługują standard WebRTC natychmiast po wyjęciu z pudełka. A co najważniejsze, inżynierowie z zespołów programistów Chrome i Firefox pracowali razem, aby rozwiązać problemy, aby te dwie przeglądarki mogły się ze sobą łatwo komunikować.

System operacyjny Android

W systemach operacyjnych Android aplikacje WebRTC dla przeglądarki Chrome i Firefox powinny działać od razu po zainstalowaniu. Są w stanie współpracować z innymi przeglądarkami po wersji Android Ice Cream Sandwich (4.0). Wynika to z udostępniania kodu między wersjami komputerowymi i mobilnymi.

jabłko

Apple nie ogłosił jeszcze swoich planów obsługi WebRTC w Safari na OS X. Jednym z możliwych obejść dla hybrydowych natywnych aplikacji iOS jest osadzenie kodu WebRTC bezpośrednio w aplikacji i załadowanie tej aplikacji do WebView.

Internet Explorer

Firma Microsoft nie obsługuje WebRTC na komputerach stacjonarnych. Ale oficjalnie potwierdzili, że zamierzają wdrożyć ORTC (Object Realtime Communications) w przyszłych wersjach IE (Edge). Nie planują obsługi WebRTC 1.0. Oznaczyli swój ORTC jako WebRTC 1.1, chociaż jest to tylko rozszerzenie społeczności, a nie oficjalny standard. Niedawno dodali obsługę ORTC do najnowszej wersji Microsoft Edge. Możesz dowiedzieć się więcej nahttps://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoftedge/.

Podsumowanie

Zwróć uwagę, że WebRTC to zbiór interfejsów API i protokołów, a nie pojedynczy interfejs API. Obsługa każdego z nich rozwija się w różnych przeglądarkach i systemach operacyjnych na innym poziomie. Świetny sposób na sprawdzenie najnowszego poziomu wsparciahttp://canisue.com.Śledzi wdrażanie nowoczesnych interfejsów API w wielu przeglądarkach. Najnowsze informacje na temat obsługi przeglądarek, a także demonstracje WebRTC można znaleźć pod adresemhttp://www.webrtc.org, który jest obsługiwany przez Mozillę, Google i Operę.

W świecie mobilnym obsługa WebRTC nie jest na tym samym poziomie, co na komputerach stacjonarnych. Urządzenia mobilne mają swój własny sposób, więc WebRTC to także coś innego na platformach mobilnych.

Podczas tworzenia aplikacji WebRTC na komputery stacjonarne rozważamy korzystanie z przeglądarki Chrome, Firefox lub Opera. Wszystkie z nich obsługują WebRTC po wyjęciu z pudełka. Ogólnie rzecz biorąc, potrzebujesz tylko przeglądarki i nie przejmuj się sprzętem pulpitu.

W świecie mobilnym istnieją obecnie trzy możliwe tryby WebRTC -

  • Aplikacja natywna
  • Aplikacja przeglądarki
  • Natywna przeglądarka

Android

W 2013 roku przeglądarka internetowa Firefox dla Androida została zaprezentowana z obsługą WebRTC po wyjęciu z pudełka. Teraz możesz prowadzić rozmowy wideo na urządzeniach z systemem Android za pomocą przeglądarki mobilnej Firefox.

Posiada trzy główne komponenty WebRTC -

  • PeerConnection - umożliwia połączenia między przeglądarkami

  • getUserMedia - zapewnia dostęp do kamery i mikrofonu

  • DataChannels - zapewnia transfer danych w trybie peer-to-peer

Google Chrome dla Androida zapewnia również obsługę WebRTC. Jak już zauważyłeś, najciekawsze funkcje zwykle pojawiają się najpierw w Chrome.

W zeszłym roku pojawiła się mobilna przeglądarka Opera z obsługą WebRTC. Więc na Androida masz Chrome, Firefox i Opera. Inne przeglądarki nie obsługują WebRTC.

iOS

Niestety, WebRTC nie jest obecnie obsługiwany na iOS. Chociaż WebRTC działa dobrze na komputerach Mac z przeglądarką Firefox, Opera lub Chrome, nie jest obsługiwany w systemie iOS.

Obecnie Twoja aplikacja WebRTC nie będzie działać na urządzeniach mobilnych Apple po wyjęciu z pudełka. Ale jest przeglądarka - Bowser. Jest to przeglądarka internetowa opracowana przez firmę Ericsson, która po wyjęciu z pudełka obsługuje WebRTC. Możesz sprawdzić jego stronę główną pod adresemhttp://www.openwebrtc.org/bowser/.

Obecnie jest to jedyny przyjazny sposób obsługi aplikacji WebRTC na iOS. Innym sposobem jest samodzielne opracowanie aplikacji natywnej.

Telefony z systemem Windows

Firma Microsoft nie obsługuje WebRTC na platformach mobilnych. Ale oficjalnie potwierdzili, że zamierzają wdrożyć ORTC (Object Realtime Communications) w przyszłych wersjach IE. Nie planują obsługi WebRTC 1.0. Oznaczyli swój ORTC jako WebRTC 1.1, chociaż jest to tylko rozszerzenie społeczności, a nie oficjalny standard.

Dlatego dzisiaj użytkownicy Windows Phone nie mogą korzystać z aplikacji WebRTC i nie ma sposobu na pokonanie tej sytuacji.

Jeżyna

Aplikacje WebRTC nie są również w żaden sposób obsługiwane na Blackberry.

Korzystanie z natywnej przeglądarki WebRTC

Najwygodniejszym i najwygodniejszym dla użytkowników przypadkiem korzystania z WebRTC jest użycie natywnej przeglądarki urządzenia. W takim przypadku urządzenie jest gotowe do pracy w dowolnych dodatkowych konfiguracjach.

Obecnie tylko urządzenia z Androidem w wersji 4 lub nowszej zapewniają tę funkcję. Apple nadal nie wykazuje żadnej aktywności ze wsparciem WebRTC. Dlatego użytkownicy Safari nie mogą korzystać z aplikacji WebRTC. Microsoft nie wprowadził go również w Windows Phone 8.

Korzystanie z WebRTC za pośrednictwem aplikacji przeglądarki

Oznacza to korzystanie z aplikacji innych firm (nienatywnych przeglądarek internetowych) w celu zapewnienia funkcji WebRTC. Na razie istnieją dwie takie aplikacje firm trzecich. Bowser, który jest jedynym sposobem na wprowadzenie funkcji WebRTC na urządzenie z systemem iOS i Operę, która jest przyjemną alternatywą dla platformy Android. Pozostałe dostępne przeglądarki mobilne nie obsługują WebRTC.

Natywne aplikacje mobilne

Jak widać, WebRTC nie ma jeszcze dużego wsparcia w świecie mobilnym. Tak więc jednym z możliwych rozwiązań jest opracowanie natywnych aplikacji, które wykorzystują interfejs API WebRTC. Ale nie jest to lepszy wybór, ponieważ główna funkcja WebRTC jest rozwiązaniem wieloplatformowym. W każdym razie w niektórych przypadkach jest to jedyny sposób, ponieważ natywna aplikacja może wykorzystywać funkcje lub funkcje specyficzne dla urządzenia, które nie są obsługiwane przez przeglądarki HTML5.

Ograniczanie strumienia wideo dla urządzeń mobilnych i stacjonarnych

Pierwszy parametr API getUserMedia oczekuje obiektu kluczy i wartości informujących przeglądarkę o sposobie przetwarzania strumieni. Pełen zestaw ograniczeń można sprawdzić pod adresemhttps://tools.ietf.org/html/draft-alvestrand-constraints-resolution-03. Możesz ustawić współczynnik proporcji wideo, szybkość klatek i inne opcjonalne parametry.

Obsługa urządzeń mobilnych jest jednym z największych problemów, ponieważ urządzenia mobilne mają ograniczoną przestrzeń na ekranie i ograniczone zasoby. Możesz chcieć, aby urządzenie mobilne przechwytywało tylko strumień wideo o rozdzielczości 480x320 lub mniejszy, aby oszczędzać energię i przepustowość. Użycie ciągu agenta użytkownika w przeglądarce to dobry sposób na sprawdzenie, czy użytkownik korzysta z urządzenia mobilnego, czy nie. Zobaczmy przykład. Utwórz plik index.html -

<!DOCTYPE html> 
<html lang = "en">
 
   <head> 
      <meta charset = "utf-8" /> 
   </head> 
	
   <body> 
      <video autoplay></video> 
      <script src = "client.js"></script> 
   </body>
	
</html>

Następnie utwórz następujący plik 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"); 
}

Uruchom serwer WWW za pomocą statycznego polecenia i otwórz stronę. Powinieneś zobaczyć, że to 800x600. Następnie otwórz tę stronę w widoku mobilnym za pomocą narzędzi Chrome i sprawdź rozdzielczość. Powinien mieć rozmiar 480 x 320.

Ograniczenia to najłatwiejszy sposób na zwiększenie wydajności aplikacji WebRTC.

Podsumowanie

W tym rozdziale poznaliśmy problemy, które mogą wystąpić podczas tworzenia aplikacji WebRTC na urządzenia mobilne. Odkryliśmy różne ograniczenia obsługi API WebRTC na platformach mobilnych. Uruchomiliśmy również aplikację demonstracyjną, w której ustawiliśmy różne ograniczenia dla przeglądarek stacjonarnych i mobilnych.

W tym rozdziale zamierzamy zbudować aplikację kliencką, która pozwoli dwóm użytkownikom na różnych urządzeniach komunikować się za pomocą WebRTC. Nasza aplikacja będzie miała dwie strony. Jeden do logowania, a drugi do dzwonienia do innego użytkownika.

Dwie strony będą znacznikami DIV . Większość danych wejściowych odbywa się za pomocą prostych programów obsługi zdarzeń.

Serwer sygnalizacyjny

Aby utworzyć połączenie WebRTC, klienci muszą mieć możliwość przesyłania wiadomości bez korzystania z połączenia równorzędnego WebRTC. W tym miejscu użyjemy HTML5 WebSockets - dwukierunkowego połączenia gniazdowego między dwoma punktami końcowymi - serwerem WWW i przeglądarką internetową. Teraz zacznijmy korzystać z biblioteki WebSocket. Utwórz plik server.js i wstaw następujący kod -

//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");
});

Pierwsza linia wymaga biblioteki WebSocket, którą już zainstalowaliśmy. Następnie tworzymy serwer gniazd na porcie 9090. Następnie nasłuchujemy zdarzenia połączenia . Ten kod zostanie wykonany, gdy użytkownik nawiąże połączenie WebSocket z serwerem. Następnie odsłuchujemy wszelkie wiadomości wysłane przez użytkownika. Na koniec wysyłamy odpowiedź do podłączonego użytkownika mówiąc „Witaj z serwera”.

Na naszym serwerze sygnalizacyjnym będziemy używać nazwy użytkownika opartej na ciągach znaków dla każdego połączenia, aby wiedzieć, gdzie wysyłać wiadomości. Zmieńmy nieco naszą obsługę połączenia -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON"); 
      data = {}; 
   } 
});

W ten sposób akceptujemy tylko wiadomości JSON. Następnie musimy gdzieś przechowywać wszystkich podłączonych użytkowników. Użyjemy do tego prostego obiektu Javascript. Zmień górę naszego pliku -

//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 = {};

Zamierzamy dodać pole typu dla każdej wiadomości przychodzącej od klienta. Na przykład, jeśli użytkownik chce się zalogować, wysyła komunikat typu logowania . Zdefiniujmy to -

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;
   }  
});

Jeśli użytkownik wyśle ​​wiadomość z typem logowania , my -

  • Sprawdź, czy ktoś już się zalogował przy użyciu tej nazwy użytkownika

  • Jeśli tak, powiedz użytkownikowi, że nie udało mu się zalogować

  • Jeśli nikt nie używa tej nazwy użytkownika, dodajemy nazwę użytkownika jako klucz do obiektu połączenia.

  • Jeśli polecenie nie zostanie rozpoznane, wysyłamy błąd.

Poniższy kod jest funkcją pomocniczą do wysyłania wiadomości do połączenia. Dodaj go do pliku server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Gdy użytkownik się rozłączy, powinniśmy wyczyścić jego połączenie. Możemy usunąć użytkownika po uruchomieniu zdarzenia close . Dodaj następujący kod do programu obsługi połączenia -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

Po pomyślnym zalogowaniu użytkownik chce zadzwonić do innego. Powinien złożyć ofertę innemu użytkownikowi, aby to osiągnąć. Dodaj obsługę oferty -

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;

Po pierwsze uzyskujemy połączenie użytkownika, do którego próbujemy zadzwonić. Jeśli istnieje, wysyłamy mu szczegóły oferty . Dodajemy również otherName do obiektu połączenia . Ma to na celu ułatwienie późniejszego znalezienia.

Odpowiadanie na odpowiedź ma podobny wzorzec, którego użyliśmy w module obsługi ofert . Nasz serwer po prostu przekazuje wszystkie wiadomości jako odpowiedź do innego użytkownika. Dodaj następujący kod po module obsługi oferty -

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;

Ostatnią częścią jest obsługa kandydata ICE pomiędzy użytkownikami. Używamy tej samej techniki po prostu do przekazywania wiadomości między użytkownikami. Główna różnica polega na tym, że wiadomości kandydatów mogą pojawiać się wiele razy na użytkownika w dowolnej kolejności. Dodaj kandydata do obsługi -

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;

Aby umożliwić naszym użytkownikom rozłączenie się z innym użytkownikiem, powinniśmy zaimplementować funkcję rozłączania. Poinformuje również serwer, aby usunął wszystkie odniesienia użytkowników. Dodaj obsługę urlopu -

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;

Spowoduje to również wysłanie innemu użytkownikowi zdarzenia wyjścia , aby mógł on odpowiednio rozłączyć połączenie równorzędne. Powinniśmy również zająć się przypadkiem, gdy użytkownik zrywa swoje połączenie z serwera sygnalizacyjnego. Zmodyfikujmy naszą bliską obsługę -

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" 
            });
         }
			
      } 
   } 
});

Poniżej znajduje się cały kod naszego serwera sygnalizacyjnego -

//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)); 
}

Aplikacja kliencka

Jednym ze sposobów przetestowania tej aplikacji jest otwarcie dwóch kart przeglądarki i próba połączenia się ze sobą.

Przede wszystkim musimy zainstalować bibliotekę bootstrap . Bootstrap to frontend framework do tworzenia aplikacji internetowych. Możesz dowiedzieć się więcej nahttp://getbootstrap.com/.Utwórz folder o nazwie, na przykład „videochat”. To będzie nasz główny folder aplikacji. Wewnątrz tego folderu utwórz plik package.json (jest to konieczne do zarządzania zależnościami npm) i dodaj następujące -

{ 
   "name": "webrtc-videochat", 
   "version": "0.1.0", 
   "description": "webrtc-videochat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

Następnie uruchom npm install bootstrap . Spowoduje to zainstalowanie biblioteki bootstrap w folderze videochat / node_modules .

Teraz musimy stworzyć podstawową stronę HTML. Utwórz plik index.html w folderze głównym z następującym kodem -

<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>

Ta strona powinna być Ci znana. Dodaliśmy plik bootstrap css. Zdefiniowaliśmy również dwie strony. Na koniec stworzyliśmy kilka pól tekstowych i przycisków do pobierania informacji od użytkownika. Powinieneś zobaczyć dwa elementy wideo dla lokalnych i zdalnych strumieni wideo. Zwróć uwagę, że dodaliśmy łącze do pliku client.js .

Teraz musimy nawiązać połączenie z naszym serwerem sygnalizacyjnym. Utwórz plik client.js w folderze głównym za pomocą następującego kodu -

//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)); 
};

Teraz uruchom nasz serwer sygnalizacyjny za pośrednictwem serwera węzłowego . Następnie w folderze głównym uruchom polecenie statyczne i otwórz stronę w przeglądarce. Powinieneś zobaczyć następujące dane wyjściowe konsoli -

Następnym krokiem jest zaimplementowanie logowania użytkownika przy użyciu unikalnej nazwy użytkownika. Po prostu wysyłamy nazwę użytkownika do serwera, który następnie informuje nas, czy jest zajęta, czy nie. Dodaj następujący kod do pliku 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 
   } 
};

Najpierw wybieramy odniesienia do elementów na stronie. Ukrywamy stronę wywołania. Następnie dodajemy detektor zdarzeń do przycisku logowania. Gdy użytkownik go kliknie, wysyłamy jego nazwę użytkownika na serwer. Na koniec implementujemy wywołanie zwrotne handleLogin. Jeśli logowanie przebiegło pomyślnie, wyświetlamy stronę rozmowy i zaczynamy konfigurować połączenie równorzędne.

Aby rozpocząć połączenie równorzędne, potrzebujemy -

  • Uzyskaj strumień z kamery internetowej.
  • Utwórz obiekt RTCPeerConnection.

Dodaj następujący kod do „bloku selektorów interfejsu użytkownika” -

var localVideo = document.querySelector('#localVideo'); 
var remoteVideo = document.querySelector('#remoteVideo');
 
var yourConn; 
var stream;

Zmodyfikuj funkcję 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); 
      }); 
   } 
};

Teraz, jeśli uruchomisz kod, strona powinna umożliwiać zalogowanie się i wyświetlenie lokalnego strumienia wideo na stronie.

Teraz jesteśmy gotowi do nawiązania połączenia. Po pierwsze, wysyłamy ofertę do innego użytkownika. Gdy użytkownik otrzyma ofertę, tworzy odpowiedź i zaczyna handlować kandydatami ICE. Dodaj następujący kod do pliku 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)); 
};

Dodajemy moduł obsługi kliknięć do przycisku Zadzwoń, który inicjuje ofertę. Następnie implementujemy kilka procedur obsługi oczekiwanych przez procedurę obsługi onmessage . Będą przetwarzane asynchronicznie, dopóki obaj użytkownicy nie nawiążą połączenia.

Ostatnim krokiem jest zaimplementowanie funkcji rozłączania. Spowoduje to zatrzymanie przesyłania danych i poinformowanie innego użytkownika, aby zamknął połączenie. Dodaj następujący kod -

//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; 
};

Gdy użytkownik kliknie przycisk Rozłącz -

  • Wyśle wiadomość „zostaw” do innego użytkownika
  • Spowoduje to zamknięcie połączenia RTCPeerConnection i lokalne zniszczenie połączenia

Teraz uruchom kod. Powinieneś móc zalogować się do serwera za pomocą dwóch zakładek przeglądarki. Następnie możesz zadzwonić na kartę i rozłączyć się.

Poniżej znajduje się cały plik 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; 
};

Podsumowanie

To demo zawiera zestawienie funkcji, których potrzebuje każda aplikacja WebRTC. Aby ulepszyć to demo, możesz dodać identyfikację użytkownika za pośrednictwem platform takich jak Facebook lub Google, obsługiwać wprowadzanie przez użytkownika nieprawidłowych danych. Ponadto połączenie WebRTC może się nie udać z kilku powodów, takich jak brak obsługi technologii lub niemożność przejścia przez zapory ogniowe. Wiele pracy włożono w zapewnienie stabilności dowolnej aplikacji WebRTC.

W tym rozdziale zamierzamy zbudować aplikację kliencką, która pozwoli dwóm użytkownikom na oddzielnych urządzeniach komunikować się za pomocą strumieni audio WebRTC. Nasza aplikacja będzie miała dwie strony. Jeden do logowania, a drugi do wykonywania połączenia głosowego z innym użytkownikiem.

Dwie strony będą znacznikami DIV . Większość danych wejściowych odbywa się za pomocą prostych programów obsługi zdarzeń.

Serwer sygnalizacyjny

Aby utworzyć połączenie WebRTC, klienci muszą mieć możliwość przesyłania wiadomości bez korzystania z połączenia równorzędnego WebRTC. W tym miejscu użyjemy HTML5 WebSockets - dwukierunkowego połączenia gniazdowego między dwoma punktami końcowymi - serwerem WWW i przeglądarką internetową. Teraz zacznijmy korzystać z biblioteki WebSocket. Utwórz plik server.js i wstaw następujący kod -

//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"); 
});

Pierwsza linia wymaga biblioteki WebSocket, którą już zainstalowaliśmy. Następnie tworzymy serwer gniazd na porcie 9090. Następnie nasłuchujemy zdarzenia połączenia . Ten kod zostanie wykonany, gdy użytkownik nawiąże połączenie WebSocket z serwerem. Następnie odsłuchujemy wszelkie wiadomości wysłane przez użytkownika. Na koniec wysyłamy odpowiedź do podłączonego użytkownika mówiąc „Witaj z serwera”.

Na naszym serwerze sygnalizacyjnym będziemy używać nazwy użytkownika opartej na ciągach znaków dla każdego połączenia, aby wiedzieć, gdzie wysyłać wiadomości. Zmieńmy nieco naszą obsługę połączenia -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON");
      data = {}; 
   } 
});

W ten sposób akceptujemy tylko wiadomości JSON. Następnie musimy gdzieś przechowywać wszystkich podłączonych użytkowników. Użyjemy do tego prostego obiektu Javascript. Zmień górę naszego pliku -

//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 = {};

Zamierzamy dodać pole typu dla każdej wiadomości przychodzącej od klienta. Na przykład, jeśli użytkownik chce się zalogować, wysyła komunikat typu logowania . Zdefiniujmy to -

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; 
   }  
});

Jeśli użytkownik wyśle ​​wiadomość z typem logowania , my -

  • Sprawdź, czy ktoś już się zalogował przy użyciu tej nazwy użytkownika.
  • Jeśli tak, powiedz użytkownikowi, że nie udało mu się zalogować.
  • Jeśli nikt nie używa tej nazwy użytkownika, dodajemy nazwę użytkownika jako klucz do obiektu połączenia.
  • Jeśli polecenie nie zostanie rozpoznane, wysyłamy błąd.

Poniższy kod jest funkcją pomocniczą do wysyłania wiadomości do połączenia. Dodaj go do pliku server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Gdy użytkownik się rozłączy, powinniśmy wyczyścić jego połączenie. Możemy usunąć użytkownika po uruchomieniu zdarzenia close . Dodaj poniższy kod do połączenia handler-

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

Po pomyślnym zalogowaniu użytkownik chce zadzwonić do innego. Powinien złożyć ofertę innemu użytkownikowi, aby to osiągnąć. Dodaj obsługę oferty -

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;

Po pierwsze uzyskujemy połączenie użytkownika, do którego próbujemy zadzwonić. Jeśli istnieje, wysyłamy mu szczegóły oferty . Dodajemy również otherName do obiektu połączenia . Ma to na celu ułatwienie późniejszego znalezienia.

Odpowiadanie na odpowiedź ma podobny wzorzec, którego użyliśmy w module obsługi ofert . Nasz serwer po prostu przekazuje wszystkie wiadomości jako odpowiedź do innego użytkownika. Dodaj następujący kod po module obsługi oferty -

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;

Ostatnią częścią jest obsługa kandydata ICE pomiędzy użytkownikami. Używamy tej samej techniki po prostu do przekazywania wiadomości między użytkownikami. Główna różnica polega na tym, że wiadomości kandydatów mogą pojawiać się wiele razy na użytkownika w dowolnej kolejności. Dodaj kandydata do obsługi -

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;

Aby umożliwić naszym użytkownikom rozłączenie się z innym użytkownikiem, powinniśmy zaimplementować funkcję rozłączania. Poinformuje również serwer, aby usunął wszystkie odniesienia użytkowników. Dodaj obsługę urlopu -

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;

Spowoduje to również wysłanie innemu użytkownikowi zdarzenia wyjścia , aby mógł on odpowiednio rozłączyć połączenie równorzędne. Powinniśmy również zająć się przypadkiem, gdy użytkownik zrywa swoje połączenie z serwera sygnalizacyjnego. Zmodyfikujmy naszą bliską obsługę -

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" 
            }); 
         }
			
      } 
   } 
});

Poniżej znajduje się cały kod naszego serwera sygnalizacyjnego -

//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)); 
}

Aplikacja kliencka

Jednym ze sposobów przetestowania tej aplikacji jest otwarcie dwóch kart przeglądarki i próba nawiązania ze sobą połączenia audio.

Przede wszystkim musimy zainstalować bibliotekę bootstrap . Bootstrap to frontend framework do tworzenia aplikacji internetowych. Możesz dowiedzieć się więcej nahttp://getbootstrap.com/.Utwórz folder o nazwie na przykład „audiochat”. To będzie nasz główny folder aplikacji. Wewnątrz tego folderu utwórz plik package.json (jest to konieczne do zarządzania zależnościami npm) i dodaj następujące -

{ 
   "name": "webrtc-audiochat", 
   "version": "0.1.0", 
   "description": "webrtc-audiochat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

Następnie uruchom npm install bootstrap . Spowoduje to zainstalowanie biblioteki bootstrap w folderze audiochat / node_modules .

Teraz musimy stworzyć podstawową stronę HTML. Utwórz plik index.html w folderze głównym z następującym kodem -

<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>

Ta strona powinna być Ci znana. Dodaliśmy plik bootstrap css. Zdefiniowaliśmy również dwie strony. Wreszcie stworzyliśmy kilka pól tekstowych i przycisków do pobierania informacji od użytkownika. Powinieneś zobaczyć dwa elementy audio dla lokalnych i zdalnych strumieni audio. Zwróć uwagę, że dodaliśmy łącze do pliku client.js .

Teraz musimy nawiązać połączenie z naszym serwerem sygnalizacyjnym. Utwórz plik client.js w folderze głównym za pomocą następującego kodu -

//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)); 
};

Teraz uruchom nasz serwer sygnalizacyjny za pośrednictwem serwera węzłowego . Następnie w folderze głównym uruchom polecenie statyczne i otwórz stronę w przeglądarce. Powinieneś zobaczyć następujące dane wyjściowe konsoli -

Następnym krokiem jest zaimplementowanie logowania użytkownika przy użyciu unikalnej nazwy użytkownika. Po prostu wysyłamy nazwę użytkownika do serwera, który następnie informuje nas, czy jest zajęta, czy nie. Dodaj następujący kod do pliku 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 
      //**********************
		         
   } 
	
};

Najpierw wybieramy odniesienia do elementów na stronie. Ukrywamy stronę wywołania. Następnie dodajemy detektor zdarzeń do przycisku logowania. Gdy użytkownik go kliknie, wysyłamy jego nazwę użytkownika na serwer. Na koniec implementujemy wywołanie zwrotne handleLogin. Jeśli logowanie przebiegło pomyślnie, wyświetlamy stronę rozmowy i zaczynamy konfigurować połączenie równorzędne.

Aby rozpocząć połączenie równorzędne, potrzebujemy -

  • Uzyskaj strumień audio z mikrofonu
  • Utwórz obiekt RTCPeerConnection

Dodaj następujący kod do „bloku selektorów interfejsu użytkownika” -

var localAudio = document.querySelector('#localAudio'); 
var remoteAudio = document.querySelector('#remoteAudio'); 

var yourConn; 
var stream;

Zmodyfikuj funkcję 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); 
      }); 
		
   } 
};

Teraz, jeśli uruchomisz kod, strona powinna umożliwiać zalogowanie się i wyświetlenie lokalnego strumienia audio na stronie.

Teraz jesteśmy gotowi do nawiązania połączenia. Po pierwsze, wysyłamy ofertę do innego użytkownika. Gdy użytkownik otrzyma ofertę, tworzy odpowiedź i zaczyna handlować kandydatami ICE. Dodaj następujący kod do pliku 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)); 
};

Dodajemy moduł obsługi kliknięć do przycisku Zadzwoń, który inicjuje ofertę. Następnie implementujemy kilka procedur obsługi oczekiwanych przez procedurę obsługi onmessage . Będą przetwarzane asynchronicznie, dopóki obaj użytkownicy nie nawiążą połączenia.

Ostatnim krokiem jest zaimplementowanie funkcji rozłączania. Spowoduje to zatrzymanie przesyłania danych i poinformowanie innego użytkownika, aby zamknął połączenie. Dodaj następujący kod -

//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;
};

Gdy użytkownik kliknie przycisk Rozłącz -

  • Wyśle wiadomość „zostaw” do innego użytkownika
  • Spowoduje to zamknięcie połączenia RTCPeerConnection i lokalne zniszczenie połączenia

Teraz uruchom kod. Powinieneś móc zalogować się do serwera za pomocą dwóch zakładek przeglądarki. Następnie możesz wykonać połączenie audio z kartą i rozłączyć się.

Poniżej znajduje się cały plik 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; 
};

W tym rozdziale zamierzamy zbudować aplikację kliencką, która pozwoli dwóm użytkownikom na oddzielnych urządzeniach na wzajemne wysyłanie wiadomości za pomocą WebRTC. Nasza aplikacja będzie miała dwie strony. Jeden do logowania, a drugi do wysyłania wiadomości do innego użytkownika.

Dwie strony będą znacznikami DIV . Większość danych wejściowych odbywa się za pomocą prostych programów obsługi zdarzeń.

Serwer sygnalizacyjny

Aby utworzyć połączenie WebRTC, klienci muszą mieć możliwość przesyłania wiadomości bez korzystania z połączenia równorzędnego WebRTC. W tym miejscu użyjemy HTML5 WebSockets - dwukierunkowego połączenia gniazdowego między dwoma punktami końcowymi - serwerem WWW i przeglądarką internetową. Teraz zacznijmy korzystać z biblioteki WebSocket. Utwórz plik server.js i wstaw następujący kod -

//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"); 
});

Pierwsza linia wymaga biblioteki WebSocket, którą już zainstalowaliśmy. Następnie tworzymy serwer gniazd na porcie 9090. Następnie nasłuchujemy zdarzenia połączenia . Ten kod zostanie wykonany, gdy użytkownik nawiąże połączenie WebSocket z serwerem. Następnie odsłuchujemy wszelkie wiadomości wysłane przez użytkownika. Na koniec wysyłamy odpowiedź do podłączonego użytkownika mówiąc „Witaj z serwera”.

Na naszym serwerze sygnalizacyjnym będziemy używać nazwy użytkownika opartej na ciągach znaków dla każdego połączenia, aby wiedzieć, gdzie wysyłać wiadomości. Zmieńmy nieco naszą obsługę połączenia -

connection.on('message', function(message) { 
   var data; 
	
   //accepting only JSON messages 
   try { 
      data = JSON.parse(message); 
   } catch (e) { 
      console.log("Invalid JSON");
      data = {}; 
   } 
});

W ten sposób akceptujemy tylko wiadomości JSON. Następnie musimy gdzieś przechowywać wszystkich podłączonych użytkowników. Użyjemy do tego prostego obiektu Javascript. Zmień górę naszego pliku -

//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 = {};

Zamierzamy dodać pole typu dla każdej wiadomości przychodzącej od klienta. Na przykład, jeśli użytkownik chce się zalogować, wysyła komunikat typu logowania . Zdefiniujmy to -

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;
   } 
});

Jeśli użytkownik wyśle ​​wiadomość z typem logowania , my -

  • Sprawdź, czy ktoś już się zalogował przy użyciu tej nazwy użytkownika.
  • Jeśli tak, powiedz użytkownikowi, że nie udało mu się zalogować.
  • Jeśli nikt nie używa tej nazwy użytkownika, dodajemy nazwę użytkownika jako klucz do obiektu połączenia.
  • Jeśli polecenie nie zostanie rozpoznane, wysyłamy błąd.

Poniższy kod jest funkcją pomocniczą do wysyłania wiadomości do połączenia. Dodaj go do pliku server.js -

function sendTo(connection, message) { 
   connection.send(JSON.stringify(message)); 
}

Gdy użytkownik się rozłączy, powinniśmy wyczyścić jego połączenie. Możemy usunąć użytkownika po uruchomieniu zdarzenia close . Dodaj następujący kod do programu obsługi połączenia -

connection.on("close", function() { 
   if(connection.name) { 
      delete users[connection.name]; 
   } 
});

Po pomyślnym zalogowaniu użytkownik chce zadzwonić do innego. Powinien złożyć ofertę innemu użytkownikowi, aby to osiągnąć. Dodaj obsługę oferty -

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;

Po pierwsze uzyskujemy połączenie użytkownika, do którego próbujemy zadzwonić. Jeśli istnieje, wysyłamy mu szczegóły oferty . Dodajemy również otherName do obiektu połączenia . Ma to na celu ułatwienie późniejszego znalezienia.

Odpowiadanie na odpowiedź ma podobny wzorzec, którego użyliśmy w module obsługi ofert . Nasz serwer po prostu przekazuje wszystkie wiadomości jako odpowiedź do innego użytkownika. Dodaj następujący kod po module obsługi oferty -

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;

Ostatnią częścią jest obsługa kandydata ICE pomiędzy użytkownikami. Używamy tej samej techniki po prostu do przekazywania wiadomości między użytkownikami. Główna różnica polega na tym, że wiadomości kandydatów mogą pojawiać się wiele razy na użytkownika w dowolnej kolejności. Dodaj kandydata do obsługi -

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;

Aby umożliwić naszym użytkownikom rozłączenie się z innym użytkownikiem, powinniśmy zaimplementować funkcję rozłączania. Poinformuje również serwer, aby usunął wszystkie odniesienia użytkowników. Dodaj obsługę urlopu -

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;

Spowoduje to również wysłanie innemu użytkownikowi zdarzenia wyjścia , aby mógł on odpowiednio rozłączyć połączenie równorzędne. Powinniśmy również zająć się przypadkiem, gdy użytkownik zrywa swoje połączenie z serwera sygnalizacyjnego. Zmodyfikujmy naszą bliską obsługę -

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" 
            }); 
         }  
      } 
   } 
});

Poniżej znajduje się cały kod naszego serwera sygnalizacyjnego -

//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)); 
}

Aplikacja kliencka

Jednym ze sposobów przetestowania tej aplikacji jest otwarcie dwóch kart przeglądarki i próba wysłania sobie wiadomości.

Przede wszystkim musimy zainstalować bibliotekę bootstrap . Bootstrap to frontend framework do tworzenia aplikacji internetowych. Możesz dowiedzieć się więcej nahttp://getbootstrap.com/.Utwórz folder o nazwie na przykład „textchat”. To będzie nasz główny folder aplikacji. Wewnątrz tego folderu utwórz plik package.json (jest to konieczne do zarządzania zależnościami npm) i dodaj następujące -

{ 
   "name": "webrtc-textochat", 
   "version": "0.1.0", 
   "description": "webrtc-textchat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

Następnie uruchom npm install bootstrap . Spowoduje to zainstalowanie biblioteki bootstrap w folderze textchat / node_modules .

Teraz musimy stworzyć podstawową stronę HTML. Utwórz plik index.html w folderze głównym z następującym kodem -

<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>

Ta strona powinna być Ci znana. Dodaliśmy plik bootstrap css. Zdefiniowaliśmy również dwie strony. Wreszcie stworzyliśmy kilka pól tekstowych i przycisków do pobierania informacji od użytkownika. Na stronie „chat” powinieneś zobaczyć znacznik DIV z identyfikatorem „chatarea”, w którym będą wyświetlane wszystkie nasze wiadomości. Zwróć uwagę, że dodaliśmy łącze do pliku client.js .

Teraz musimy nawiązać połączenie z naszym serwerem sygnalizacyjnym. Utwórz plik client.js w folderze głównym za pomocą następującego kodu -

//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)); 
};

Teraz uruchom nasz serwer sygnalizacyjny za pośrednictwem serwera węzłowego . Następnie w folderze głównym uruchom polecenie statyczne i otwórz stronę w przeglądarce. Powinieneś zobaczyć następujące dane wyjściowe konsoli -

Następnym krokiem jest zaimplementowanie logowania użytkownika przy użyciu unikalnej nazwy użytkownika. Po prostu wysyłamy nazwę użytkownika do serwera, który następnie informuje nas, czy jest zajęta, czy nie. Dodaj następujący kod do pliku 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 
      //********************** 
   } 
	
};

Najpierw wybieramy odniesienia do elementów na stronie. Ukrywamy stronę wywołania. Następnie dodajemy detektor zdarzeń do przycisku logowania. Gdy użytkownik go kliknie, wysyłamy jego nazwę użytkownika na serwer. Na koniec implementujemy wywołanie zwrotne handleLogin. Jeśli logowanie przebiegło pomyślnie, wyświetlamy stronę rozmowy, zestawiamy połączenie równorzędne i tworzymy kanał danych.

Aby rozpocząć połączenie równorzędne z kanałem danych, potrzebujemy -

  • Utwórz obiekt RTCPeerConnection
  • Utwórz kanał danych wewnątrz naszego obiektu RTCPeerConnection

Dodaj następujący kod do „bloku selektorów interfejsu użytkownika” -

var msgInput = document.querySelector('#msgInput'); 
var sendMsgBtn = document.querySelector('#sendMsgBtn'); 
var chatArea = document.querySelector('#chatarea'); 

var yourConn; 
var dataChannel;

Zmodyfikuj funkcję 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"); 
      };  
   } 
};

Jeśli logowanie przebiegło pomyślnie, aplikacja tworzy obiekt RTCPeerConnection i konfiguruje procedurę obsługi onicecandidate, która wysyła wszystkie znalezione icecandidates do drugiego peera. Tworzy również dataChannel. Zwróć uwagę, że podczas tworzenia obiektu RTCPeerConnection drugi argument w konstruktorze jest opcjonalny: [{RtpDataChannels: true}] jest obowiązkowy, jeśli używasz przeglądarki Chrome lub Opera. Następnym krokiem jest utworzenie oferty dla innego peera. Gdy użytkownik otrzyma ofertę, tworzy odpowiedź i zaczyna handlować kandydatami ICE. Dodaj następujący kod do pliku 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)); 
};

Dodajemy moduł obsługi kliknięć do przycisku Zadzwoń, który inicjuje ofertę. Następnie implementujemy kilka procedur obsługi oczekiwanych przez procedurę obsługi onmessage . Będą przetwarzane asynchronicznie, dopóki obaj użytkownicy nie nawiążą połączenia.

Następnym krokiem jest zaimplementowanie funkcji rozłączania. Spowoduje to zatrzymanie przesyłania danych i poinformowanie innego użytkownika, aby zamknął kanał danych. Dodaj następujący kod -

//hang up 
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   }); 
	
   handleLeave(); 
}); 
 
function handleLeave() { 
   connectedUser = null; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
};

Gdy użytkownik kliknie przycisk Rozłącz -

  • Wyśle wiadomość „zostaw” do innego użytkownika.
  • Spowoduje to zamknięcie połączenia RTCPeerConnection oraz kanału danych.

Ostatnim krokiem jest wysłanie wiadomości do innego peera. Dodaj moduł obsługi „kliknięcia” do przycisku „wyślij wiadomość” -

//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 = ""; 
});

Teraz uruchom kod. Powinieneś móc zalogować się do serwera za pomocą dwóch zakładek przeglądarki. Możesz wtedy ustawić połączenie równorzędne z innym użytkownikiem i wysłać mu wiadomość, a także zamknąć kanał danych, klikając przycisk „Rozłącz”.

Poniżej znajduje się cały plik 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 = ""; 
});

W tym rozdziale dodamy funkcje bezpieczeństwa do serwera sygnalizacyjnego, który stworzyliśmy w rozdziale „Sygnalizacja WebRTC”. Będą dwa ulepszenia -

  • Uwierzytelnianie użytkownika za pomocą bazy danych Redis
  • Włączanie bezpiecznego połączenia przez gniazdo

Po pierwsze, powinieneś zainstalować Redis.

  • Pobierz najnowszą stabilną wersję pod adresem http://redis.io/download(3,05 w moim przypadku)

  • Rozpakuj to

  • W pobranym folderze uruchom sudo make install

  • Po zakończeniu instalacji uruchom make test, aby sprawdzić, czy wszystko działa poprawnie.

Redis ma dwa wykonywalne polecenia -

  • redis-cli - interfejs wiersza poleceń dla Redis (część klienta)

  • redis-server - Magazyn danych Redis

Aby uruchomić serwer Redis, wpisz redis-server w konsoli terminala. Powinieneś zobaczyć następujące -

Teraz otwórz nowe okno terminala i uruchom redis-cli, aby otworzyć aplikację kliencką.

Zasadniczo Redis to baza danych klucz-wartość. Aby utworzyć klucz z wartością ciągu, należy użyć polecenia SET. Aby odczytać wartość klucza, należy użyć polecenia GET. Dodajmy dla nich dwóch użytkowników i hasła. Klucze będą nazwami użytkowników, a wartości tych kluczy będą odpowiadającymi im hasłami.

Teraz powinniśmy zmodyfikować nasz serwer sygnalizacyjny, aby dodać uwierzytelnianie użytkownika. Dodaj następujący kod na początku pliku server.js -

//require the redis library in Node.js 
var redis = require("redis");
 
//creating the redis client object 
var redisClient = redis.createClient();

W powyższym kodzie wymagamy biblioteki Redis dla Node.js i stworzenia klienta redis dla naszego serwera.

Aby dodać uwierzytelnianie, zmodyfikuj procedurę obsługi wiadomości w obiekcie połączenia -

//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*******

W powyższym kodzie, jeśli użytkownik próbuje się zalogować, otrzymujemy od Redisa jego hasło, sprawdzamy, czy zgadza się z zapisanym, a jeśli się powiedzie, przechowujemy jego nazwę użytkownika na serwerze. Dodajemy również flagę isAuth do połączenia, aby sprawdzić, czy użytkownik jest uwierzytelniony. Zwróć uwagę na ten kod -

//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; 
   } 
}

Jeśli nieuwierzytelniony użytkownik próbuje wysłać ofertę lub opuścić połączenie, po prostu odsyłamy z powrotem błąd.

Następnym krokiem jest włączenie bezpiecznego połączenia przez gniazdo. Jest to wysoce zalecane dla aplikacji WebRTC. PKI (infrastruktura klucza publicznego) to podpis cyfrowy wydany przez CA (urząd certyfikacji). Następnie użytkownicy sprawdzają, czy klucz prywatny użyty do podpisania certyfikatu jest zgodny z kluczem publicznym certyfikatu urzędu certyfikacji. Do celów rozwojowych. użyjemy certyfikatu bezpieczeństwa z podpisem własnym.

Użyjemy openssl. Jest to narzędzie typu open source, które implementuje protokoły SSL (Secure Sockets Layer) i TLS (Transport Layer Security). Często jest instalowany domyślnie w systemach Unix. Uruchom openssl version -a, aby sprawdzić, czy jest zainstalowany.

Aby wygenerować publiczne i prywatne klucze certyfikatów bezpieczeństwa, należy wykonać czynności podane poniżej -

  • Generate a temporary server password key

openssl genrsa -des3 -passout pass:x -out server.pass.key 2048

  • Generate a server private key

openssl rsa -passin pass:12345 -in server.pass.key -out server.key

  • Generate a signing request. You will be asked additional questions about your company. Just hit the “Enter” button all the time.

openssl req -new -key server.key -out server.csr

  • Generate the certificate

openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt

Teraz masz dwa pliki, certyfikat (server.crt) i klucz prywatny (server.key). Skopiuj je do folderu głównego serwera sygnalizacyjnego.

Aby włączyć bezpieczne połączenie przez gniazdo, zmodyfikuj nasz serwer sygnalizacyjny.

//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

W powyższym kodzie wymagamy, aby biblioteka fs odczytała klucz prywatny i certyfikat, utworzy obiekt cfg z powiązanym portem i ścieżkami dla klucza prywatnego i certyfikatu. Następnie tworzymy serwer HTTPS z naszymi kluczami wraz z serwerem WebSocket na porcie 9090.

Teraz otwarte https://localhost:9090w Operze. Powinieneś zobaczyć następujące -

Kliknij przycisk „Mimo to kontynuuj”. Powinien zostać wyświetlony komunikat „OK”.

Aby przetestować nasz bezpieczny serwer sygnalizacyjny, zmodyfikujemy aplikację czatu, którą stworzyliśmy w samouczku „WebRTC Text Demo”. Musimy tylko dodać pole hasła. Poniżej znajduje się cały plik 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>

Musimy również włączyć bezpieczne połączenie przez gniazdo w pliku client.js za pomocą tego wiersza var conn = new WebSocket ('wss: // localhost: 9090'); . Zwróć uwagę na protokół wss . Następnie osoba obsługująca przycisk logowania musi zostać zmodyfikowana, aby wysłać hasło wraz z nazwą użytkownika -

loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
   var pwd = passwordInput.value;
	
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name, 
         password: pwd 
      }); 
   } 
	
});

Poniżej znajduje się cały plik 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 = ""; 
});

Teraz uruchom nasz bezpieczny serwer sygnalizacyjny za pośrednictwem serwera węzłowego . Uruchom node static w zmodyfikowanym folderze demonstracyjnym czatu. otwartylocalhost:8080na dwóch kartach przeglądarki. Spróbuj się zalogować. Pamiętaj, że tylko „user1” z „password1” i „user2” z „password2” mogą się zalogować. Następnie nawiąż połączenie RTCPeerConnection (zadzwoń do innego użytkownika) i spróbuj wysłać wiadomość.

Poniżej znajduje się cały kod naszego bezpiecznego serwera sygnalizacyjnego -

//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)); 
}

Podsumowanie

W tym rozdziale dodaliśmy uwierzytelnianie użytkownika do naszego serwera sygnalizacyjnego. Dowiedzieliśmy się również, jak tworzyć certyfikaty SSL z podpisem własnym i wykorzystywać je w aplikacjach WebRTC.