Włącz komunikację w czasie rzeczywistym przez WebRTC

1. Zanim zaczniesz

Dzięki nim dowiesz się, jak stworzyć aplikację umożliwiającą pobieranie filmów, robienie zdjęć kamerą internetową i udostępnianie ich w ramach protokołu peer-to-peer przez WebRTC. Dowiedz się też, jak korzystać z podstawowych interfejsów API WebRTC, oraz skonfigurować serwer wiadomości za pomocą Node.js.

Wymagania wstępne

  • Podstawowa wiedza o HTML, CSS i JavaScript

Co stworzysz

  • Pobierz film z kamery internetowej.
  • Przesyłaj strumieniowo wideo z RTCPeerConnection.
  • Transmisja danych: RTCDataChannel.
  • Skonfiguruj usługę sygnalizacyjną do wymiany wiadomości.
  • Połącz z połączeniem równorzędnym i sygnałem.
  • Zrób zdjęcie i udostępnij je kanałowi.

Czego potrzebujesz

  • Chrome 47 lub nowszy,
  • Serwer WWW dla Chrome lub dowolny inny serwer WWW
  • dowolnego edytora tekstu.
  • Node.js

2. Pobieranie przykładowego kodu

Pobierz kod

  1. Jeśli znasz język Git, uruchom następujące polecenie, aby skopiować kod tego ćwiczenia z GitHuba:
git clone https://github.com/googlecodelabs/webrtc-web

Możesz też kliknąć ten link, aby pobrać plik ZIP z kodem:

  1. Otwórz pobrany plik ZIP, aby rozpakować folder projektu o nazwie webrtc-web-master zawierający po 1 folderze na każdy etap ćwiczenia i wszystkich potrzebnych zasobów.

Cały kod działa w katalogu work.

Foldery step-nn zawierają ostateczną wersję każdego ćwiczenia. Mają tu charakter informacyjny.

Instalowanie i weryfikowanie serwera WWW

Chociaż możesz swobodnie korzystać z własnego serwera WWW, te ćwiczenia z programowania zostały opracowane tak, aby dobrze współpracowały z serwerem WWW dla Chrome.

  1. Jeśli nie masz serwera WWW dla Chrome, kliknij ten link, by zainstalować go z Chrome Web Store:

d0a4649b4920cf3.png

  1. Kliknij Dodaj do Chrome, aby zainstalować serwer WWW dla Chrome i automatycznie otwierać aplikacje Google w nowej karcie.
  2. Kliknij Serwer WWW:

27fce4494f641883

Pojawi się okno dialogowe, w którym można skonfigurować lokalny serwer WWW:

A300381a486b9e22.png

  1. Kliknij Wybierz folder.
  2. Wybierz utworzony folder work.

W sekcji Adresy URL serwera WWW znajdziesz adres URL, pod którym możesz sprawdzać postęp pracy.

Chrome.

  1. W sekcji Opcje (może wymagać ponownego uruchomienia) zaznacz pole wyboru Automatycznie pokazuj indeks.html.
  2. Aby wyłączyć i ponownie uruchomić serwer, dwukrotnie kliknij przełącznik Serwer WWW: uruchomiony.

f23cafb3993dfac1.png

  1. Kliknij URL w sekcji URL-e serwera WWW, aby wyświetlić swoją pracę w przeglądarce.

Powinna wyświetlić się strona work/index.html podobna do tej:

18a705cb6ccc5181.png

Oczywiście ta aplikacja nie robi jeszcze nic interesującego. To tylko minimalny szkielet, aby zapewnić prawidłowe działanie serwera WWW. W kolejnych krokach dodasz funkcje i układy strony.

3. Przesyłaj obraz wideo ze swojej kamery internetowej

Pełna wersja tego kroku jest w folderze step-01.

Dodaj paski kodu HTML

Skopiuj ten kod i wklej go w pliku index.html w katalogu work, by dodać element video i script:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

Dodaj szczypc JavaScript

Skopiuj ten kod i wklej go w pliku main.js w folderze js:

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handle error and log a message to the console with the error message
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

Wypróbuj

Otwórz plik index.html w przeglądarce i pojawi się coś podobnego, ale z widoku z kamery internetowej:

9297048e43ed0f3d.png

Jak to działa

Po wywołaniu getUserMedia() przeglądarka prosi o dostęp do kamery, jeśli jest to pierwsze żądanie dostępu do kamery w bieżącym źródle.

Jeśli operacja się uda, zostanie zwrócony element MediaStream, którego element media może użyć za pomocą atrybutu srcObject:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Argument constraints umożliwia określenie multimediów. W tym przykładzie multimedia są tylko wideo, bo dźwięk jest domyślnie wyłączony:

const mediaStreamConstraints = {
  video: true,
};

Możesz też używać ograniczeń takich jak rozdzielczość filmu:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

Specyfikacja MediaTrackConstraints zawiera listę wszystkich potencjalnych typów ograniczeń, ale nie wszystkie opcje są obsługiwane przez wszystkie przeglądarki. Jeśli wymagana rozdzielczość nie jest obsługiwana przez obecnie wybraną kamerę, getUserMedia() będzie odrzucana przez OverconstrainedError i pojawi się prośba o pozwolenie na dostęp do aparatu.

Jeśli getUserMedia() zakończy się pomyślnie, strumień wideo z kamery internetowej zostanie ustawiony jako źródło elementu wideo:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Zdobądź dodatkowe punkty

  • Obiekt localStream przekazany do getUserMedia() jest w zakresie globalnym, więc możesz go sprawdzić w konsoli przeglądarki. Otwórz konsolę, wpisz stream, i naciśnij Enter (Return na Macu). Aby wyświetlić konsolę w Chrome, naciśnij Control+Shift+J (lub Command+Option+J na Macu).
  • Co zwraca localStream.getVideoTracks()?
  • Zadzwoń do firmy localStream.getVideoTracks()[0].stop().
  • Spójrz na obiekt ograniczeń. Co się stanie, gdy zmienisz go na: {audio: true, video: true}?
  • Jaki jest rozmiar elementu wideo? Jak można określić naturalny rozmiar filmu z JavaScriptu w porównaniu z rozmiarem wyświetlacza? Sprawdź Narzędzia dla programistów w Google Chrome.
  • Dodaj filtry CSS do elementu wideo, jak w przykładach poniżej:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • Dodaj filtry SVG w ten sposób:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

Wskazówki

Sprawdzona metoda

Upewnij się, że Twój element wideo nie przekracza swojego kontenera. Dzięki tym ćwiczeniom dodaliśmy width i max-width, by ustawić preferowany rozmiar i maksymalny rozmiar filmu. Wysokość jest obliczana automatycznie przez przeglądarkę.

video {
  max-width: 100%;
  width: 320px;
}

4. Odtwarzaj filmy za pomocą interfejsu RTCPeerConnection API

Pełna wersja tego kroku jest w folderze step-2.

Dodawanie elementów wideo i przycisków sterowania

W pliku index.html zastąp pojedynczy element video dwoma elementami video i button:

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

Jeden element wideo wyświetla strumień z getUserMedia(), a drugi – ten sam film przesyłany strumieniowo przez RTCPeerconnection. W prawdziwym świecie jeden element video będzie pokazywać strumień lokalny, a drugi – zdalny.

Dodaj podkład adapt.js

Skopiuj ten element skryptu i wklej go nad elementem skryptu dla zapytania main.js:

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Twój plik index.html powinien teraz wyglądać tak:

<!DOCTYPE html>
<html>

<head>
  <title>Real-time communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Real-time communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

Instalowanie kodu aplikacji RTCPeerConnection

Zastąp main.js wersją w folderze step-02.

Zadzwoń

  1. Otwórz plik index.html.
  2. Kliknij Uruchom, aby pobrać film z kamery internetowej.
  3. Kliknij Zadzwoń, aby nawiązać połączenie równorzędne

Ten sam film z Twojej kamery powinien być widoczny w obu elementach video.

  1. Aby wyświetlić dzienniki WebRTC, wyświetl konsolę przeglądarki.

Jak to działa

To duży krok.

WebRTC używa interfejsu API RTCPeerConnection do konfigurowania połączenia do strumieniowego przesyłania wideo między klientami WebRTC (tzw. peera).

W tym przykładzie dwa obiekty RTCPeerConnection znajdują się na tej samej stronie: pc1 i pc2.

Konfiguracja połączenia między innymi aplikacjami WebRTC obejmuje 3 zadania:

  1. Utwórz RTCPeerConnection dla każdego zakończenia rozmowy i dodaj strumień lokalny z getUserMedia().
  2. Pobieranie i udostępnianie informacji o sieci.

Potencjalne punkty końcowe połączenia są nazywane kandydatami ICE.

  1. Pobieranie i udostępnianie opisów lokalnych i zdalnych.

Metadane multimediów lokalnych mają format Session Description Protocol (SDP).

Wyobraź sobie, że Alicja i Robert chcą skonfigurować czat wideo za pomocą aplikacji RTCPeerConnection.

Najpierw Alicja i Robert wymieniają się informacjami o sieci. Wyrażenie znajdowanie kandydatów odnosi się do procesu wyszukiwania interfejsów sieciowych i portów za pomocą platformy ICE.

  1. Alicja tworzy obiekt RTCPeerConnection z modułem obsługi onicecandidate (addEventListener('icecandidate')).

To kod na main.js:

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alicja wywołuje funkcję getUserMedia() i dodaje do niej strumień przekazany:
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. Moduł obsługi onicecandidate z pierwszego kroku jest wywoływany, gdy propozycje sieci staną się dostępne.
  2. Alicja wysyła do Roberta zserializowane dane kandydatów.

W rzeczywistej aplikacji proces ten, nazywany sygnałem, odbywa się za pomocą usługi przesyłania wiadomości. O tym, jak to zrobić, dowiesz się z późniejszego kroku. Oczywiście na tym etapie oba obiekty RTCPeerConnection znajdują się na tej samej stronie i mogą komunikować się bezpośrednio bez konieczności stosowania wiadomości zewnętrznych.

  1. Gdy Robert otrzymuje wiadomość od kandydata na temat Alicji, wywołuje użytkownika addIceCandidate(), aby dodać kandydata do opisu zdalnego peera:
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

Muszą oni też znajdować i wymieniać lokalne i zdalne dane audio i wideo, na przykład związane z rozdzielczością i kodekiem. Sygnalizacja wymiany informacji o multimediach polega na wymianie blobów metadanych, znanych jako oferta i odpowiedź, w formacie SDP.

  1. Alicja uruchamia metodę RTCPeerConnection createOffer().

Zwrócona deklaracja zawiera RTCSessionDescription—opis sesji lokalnej Alicji:

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. Jeśli tak jest, Alicja nadaje opisowi lokalnemu za pomocą funkcji setLocalDescription(), a następnie wysyła taki opis sesji do Roberta za pomocą swojego kanału.
  2. Robert określa, że Alicja wysłała mu opis zdalny jako setRemoteDescription().
  3. Robert uruchamia metodę RTCPeerConnection createAnswer() i przekazuje tym zdalnym opisom uzyskanym od Alicji, co powoduje wygenerowanie sesji lokalnej, która jest z nią zgodna.
  4. Obietnica createAnswer() przekazuje RTCSessionDescription, który Robert ustawia jako lokalny opis, a następnie wysyła Alicji.
  5. Kiedy Alicja uzyskuje opis sesji Roberta, ustawia go jako zdalny opis z setRemoteDescription().
// Logs offer creation and sets peer connection session descriptions
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer-connection session descriptions
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}

Zdobądź dodatkowe punkty

  1. Otwórz chrome://webrtc-internals.

Na tej stronie znajdują się dane debugowania i statystyki WebRTC. (Pełną listę adresów URL Chrome znajdziesz na chrome://about).

  1. Nadaj stronie styl CSS:
  2. Ustaw filmy obok siebie.
  3. Zmień szerokość przycisków na taką samą szerokość dzięki większemu tekstowi.
  4. Upewnij się, że układ działa na urządzeniach mobilnych.
  5. W konsoli Narzędzi dla programistów Chrome zwróć uwagę na localStream, localPeerConnection i remotePeerConnection.
  6. Otwórz konsolę localPeerConnectionpc1.localDescription.

Jak wygląda format SDP?

Wskazówki

  • Więcej informacji o podkładce adapter.js znajdziesz w repozytorium GitHub adpter.js.
  • Przyjrzyj się AppRTC i jego kodowi, projektowi kanonicznemu WebRTC do obsługi wywołań WebRTC. Czas konfiguracji połączenia nie przekracza 500 ms.

Sprawdzona metoda

Aby przygotować swój kod na przyszłość, użyj nowych interfejsów API Promise i włącz zgodność z przeglądarkami, które nie obsługują ich za pomocą pliku adapter.js.

5. Używanie kanału danych do wymiany danych

Pełna wersja tego kroku jest w folderze step-03.

Zaktualizuj kod HTML

W tym kroku używasz kanałów danych WebRTC, które wysyłają tekst między 2 elementami textarea na tej samej stronie. To niezbyt przydatne, ale pokazuje, w jaki sposób WebRTC może służyć do udostępniania danych oraz transmitowania wideo.

Usuń elementy video i button z elementu index.html, i zastąp je tym kodem HTML:

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

Jeden textarea służy do wpisywania tekstu, a drugi do wyświetlania go w strumieniu między innymi.

Twój plik index.html powinien teraz wyglądać tak:

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

Zaktualizuj JavaScript

  1. Zastąp main.js zawartością step-03/js/main.js.
  1. Spróbuj przesyłać dane między podobnymi aplikacjami:
  2. Otwórz aplikację index.html.
  3. Kliknij Start (Rozpocznij), aby skonfigurować połączenie równorzędne.
  4. Wpisz tekst w kolumnie textarea po lewej stronie.
  5. Kliknij Wyślij, aby przenieść tekst za pomocą kanału danych WebRTC.

Jak to działa

Ten kod wykorzystuje RTCPeerConnection i RTCDataChannel, by umożliwić wymianę SMS-ów.

Duża część kodu w tym kroku jest taka sama jak w przykładzie RTCPeerConnection. Funkcje sendData() i createConnection() zawierają większość nowego kodu:

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

Składnia RTCDataChannel jest celowo podobna do elementu WebSocket za pomocą metody send() i zdarzenia message.

Zwróć uwagę na typ dataConstraint. Kanały danych można skonfigurować tak, aby umożliwić udostępnianie różnych typów danych, na przykład priorytetowe wyświetlanie w bezpieczny sposób zamiast wydajności.

Zdobądź dodatkowe punkty

  1. Dzięki SCTP protokół używany przez kanały danych WebRTC jest domyślnie włączony – niezawodne i uporządkowane dostarczanie danych. Kiedy RTCDataChannel może potrzebować bezpiecznego przesyłania danych, a kiedy wydajność może być ważniejsza, nawet jeśli to oznacza utratę części danych?
  2. Użyj CSS, aby poprawić układ strony i dodać atrybut zastępczy do dataChannelReceive textarea.
  3. Przetestuj stronę na urządzeniu mobilnym.

Więcej informacji

6. Skonfiguruj usługę sygnalizacyjną do wymiany wiadomości

Wiesz już, jak wymieniać dane z innymi użytkownikami na tej samej stronie. Jak jednak to zrobić? Najpierw musisz skonfigurować kanał sygnalizujący wymianę wiadomości metadanych.

Pełna wersja tego kroku jest w folderze step-04.

Informacje o aplikacji

WebRTC używa interfejsu JavaScript API po stronie klienta, ale w rzeczywistym świecie wymaga on również serwera sygnalizacyjnego (przesyłania wiadomości), a także serwerów STUN i TURN. Więcej informacji znajdziesz tutaj.

W tym kroku utworzysz prosty serwer sygnacyjny Node.js, korzystając z modułu Socket.IO Node.js i biblioteki JavaScript do przesyłania wiadomości.

W tym przykładzie serwer (aplikacja Node.js) jest zaimplementowany w środowisku index.js, a klient, który na nim działa (aplikacja internetowa), jest wdrażany w index.html.

Aplikacja Node.js w tym kroku ma 2 zadania.

Działa ona jako usługa przekaźnika wiadomości:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

Następnie zarządza pokojami czatu wideo WebRTC:

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

W prostej aplikacji WebRTC można udostępniać pokój maksymalnie 2 osobom.

HTML i JavaScript

  1. Zaktualizuj plik index.html, aby wyglądał tak:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

Na tym etapie na stronie nie zobaczysz niczego. Wszystkie dane są rejestrowane w konsoli przeglądarki. Aby wyświetlić konsolę w Chrome, naciśnij Control+Shift+J (lub Command+Option+J na Macu).

  1. Zamień js/main.js na:
'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

Skonfiguruj plik Socket.IO do uruchamiania w Node.js

W pliku HTML możesz zauważyć, że używasz pliku Socket.IO:

<script src="/socket.io/socket.io.js"></script>
  1. Na najwyższym poziomie katalogu work utwórz plik o nazwie package.json z następującą treścią:
{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

To jest plik manifestu aplikacji, który informuje menedżera pakietu węzłów (npm) o projekcie

do zainstalowania.

  1. Aby zainstalować zależności, takie jak /socket.io/socket.io.js, uruchom to polecenie z terminala wiersza poleceń w katalogu work:
npm install

Powinien wyświetlić się dziennik instalacji z następującym fragmentem:

3ab06b7bcc7664b9.png

Jak widać, npm zainstalował(a) zależności zdefiniowane w package.json.

  1. Utwórz nowy plik index.js na najwyższym poziomie katalogu work (a nie katalogu js), a następnie dodaj następujący kod:
'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // Convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // For a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});
  1. Z poziomu terminala wiersza poleceń uruchom następujące polecenie w katalogu work:
node index.js
  1. W przeglądarce otwórz stronę http://localhost:8080.

Za każdym razem, gdy otworzysz ten URL, pojawi się prośba o podanie nazwy pokoju.

Aby dołączyć do tego samego pokoju, musisz wpisać tę samą nazwę za każdym razem, np. foo.

  1. Otwórz nową kartę i ponownie wpisz http://localhost:8080, a następnie wpisz tę samą nazwę pokoju.
  2. Otwórz nową nową kartę, przejdź do sekcji http://localhost:8080 i ponownie wpisz tę samą nazwę pokoju.
  3. Sprawdź konsolę na każdej z kart.

Powinny pojawić się dane logowania z kodu JavaScript.

Zdobądź dodatkowe punkty

  • Jakie mogą być alternatywne mechanizmy przesyłania wiadomości? Jakie problemy mogą wystąpić przy używaniu czystej aplikacji WebSocket?
  • Jakie problemy mogą powodować skalowanie tej aplikacji? Czy możesz opracować metodę testowania tysięcy lub milionów jednoczesnych próśb o dostęp do sal?
  • Ta aplikacja używa monitu JavaScript do pobrania nazwy pokoju. Dowiedz się, jak pobrać nazwę pokoju z adresu URL. Na przykład http://localhost:8080/foo nadaje mu nazwę foo.

Więcej informacji

7. Połącz z połączeniem równorzędnym i sygnałem

Pełna wersja tego kroku jest w folderze step-05.

Zastąp HTML i JavaScript

  1. Zamień zawartość pliku index.html na taką:
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>
  1. Zastąp js/main.js zawartością step-05/js/main.js.

Uruchamianie serwera Node.js

Jeśli nie przećwiczysz tych ćwiczeń z programowania z katalogu work, może być konieczne zainstalowanie zależności dla folderu step-05 lub bieżącego folderu roboczego.

  1. Uruchom następujące polecenie w katalogu roboczym:
npm install
  1. Jeśli serwer Node.js nie jest uruchomiony, po jego zainstalowaniu uruchom to polecenie w katalogu work:
node index.js

Sprawdź, czy korzystasz z wersji index.js z poprzedniego kroku, który pozwala zaimplementować funkcję Socket.IO. Więcej informacji o operacji wejścia-wyjścia w węzłach i gniazdach znajdziesz w sekcji Konfigurowanie usługi sygnalizacyjnej w celu wymiany wiadomości.

  1. W przeglądarce otwórz stronę http://localhost:8080.
  2. Otwórz nową kartę i ponownie przejdź do http://localhost:8080.

Jeden element video wyświetla strumień lokalny z serwera getUserMedia(), a drugi film zdalny przesyłany przez RTCPeerconnection.

  1. Wyświetl logowanie w konsoli przeglądarki.

Wynik punktowy

  • Ta aplikacja obsługuje tylko czat wideo jeden do jednego. Jak możesz zmienić wygląd, by więcej osób mogło korzystać z tego samego pokoju czatu wideo?
  • W tym przykładzie nazwa pokoju foo jest zakodowana na stałe. Jak najlepiej włączyć inne nazwy sal?
  • W jaki sposób użytkownicy mogą udostępniać nazwę pokoju? Spróbuj utworzyć alternatywę dla udostępniania nazw sal.
  • Jak możesz ją zmienić?

Wskazówki

  • Znajdź statystyki WebRTC i dane debugowania na chrome://webrtc-internals.
  • Użyj narzędzia do rozwiązywania problemów z WebRTC, by sprawdzić lokalne środowisko i przetestować kamerę oraz mikrofon.
  • Jeśli masz dziwne problemy z pamięcią podręczną, wypróbuj te rozwiązania:
  1. Naciśnij Control i kliknij Załaduj ponownie tę stronę.
  2. Ponownie uruchom przeglądarkę.
  3. Uruchom npm cache clean z poziomu wiersza poleceń.

8. Robienie zdjęć i udostępnianie ich za pomocą kanału danych

Pełna wersja tego kroku jest w folderze step-06.

Jak to działa

Wcześniej dowiedzieliśmy się, jak wymieniać SMS-y za pomocą RTCDataChannel. Dzięki temu możesz udostępniać całe pliki. W tym przykładzie zdjęcia zostały zrobione za pomocą getUserMedia().

Główne etapy tego kroku:

  1. Utwórz kanał danych.

W tym kroku nie dodajesz strumieni multimediów do połączenia równorzędnego.

  1. Przechwytuj strumień wideo z kamery internetowej za pomocą getUserMedia():
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. Kliknij Przyciągnij, aby uzyskać zrzut (klatkę wideo) ze strumienia wideo i wyświetlić go w elemencie canvas:
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. Kliknij Wyślij, by przekonwertować obraz na bajty i wysłać go przez kanał danych:
function sendPhoto() {
  // Split the data-channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // Split the photo and send in chunks of approximately 64KB.
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // Send the reminder, if applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

Strona odbierająca konwertuje bajty wiadomości kanału danych na obraz i wyświetla go użytkownikowi:

function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // The trail is the element that holds the incoming images.
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

Pobierz kod

  1. Zastąp zawartość folderu work zawartością step-06.

Twój plik index.html w formacie work powinien teraz wyglądać tak:****

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>
  1. Jeśli nie przećwiczysz tych ćwiczeń z programowania z katalogu work, może być konieczne zainstalowanie zależności dla folderu step-06 lub bieżącego folderu roboczego. Po prostu uruchom następujące polecenie w katalogu roboczym:
npm install
  1. Jeśli serwer Node.js nie jest uruchomiony, po jego zainstalowaniu uruchom to polecenie w katalogu work:
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

pamiętaj, aby ponownie uruchomić serwer Node.js, jeśli wprowadzisz zmiany.

Więcej informacji o węzłach i gniazdach.IO znajdziesz w sekcji Konfigurowanie sygnalizowania

do wymiany wiadomości.

  1. W razie potrzeby kliknij Zezwól, by zezwolić aplikacji na używanie kamery internetowej.

Aplikacja tworzy losowy identyfikator pokoju i dodaje go do adresu URL.

  1. Otwórz adres URL na pasku adresu w nowej karcie lub nowym oknie przeglądarki.
  2. Kliknij Przyciągnij i prześlij, a następnie sprawdź Przychodzące zdjęcia na drugiej karcie u dołu strony.

Aplikacja przenosi zdjęcia między kartami.

Powinien pojawić się ekran podobny do tego:

911b40f36ba6ba8.png

Zdobądź dodatkowe punkty

Jak możesz zmienić kod, by udostępniać dowolny typ pliku?

Więcej informacji

9. Gratulacje

Udało Ci się utworzyć aplikację do strumieniowego przesyłania danych w czasie rzeczywistym i wymiany danych.

Więcej informacji