# WebRTC lab esl repo ubuntu https://www.wikihow.com/Install-Google-Chrome-Using-Terminal-on-Linux ## Lab 1 - WebRTC P2P - b WebRTC to standard stworzony z myślą o przeglądarkach internetowych umożliwiający komunikację * Peer to peer * w czasie rzeczywistym * audio/video + dodatkowe dane Definiuje m.in. * protokoły używane w komunikacji * API przeglądarki umożliwiające ich wykorzystanie * **`MediaStream`** - strumień danych a/v w przeglądarce * Rozszerza standard W3C Media Capture and Streams API rozwijany przez WebRTC Working Group * **Peer-to-peer connections** (`RTCPeerConnection`) - Nawiązywanie połączenia * **RTP Media API** - wysyłanie `MediaStream`ów przez `RTCPeerConnection` * **Peer-to-peer Data API** -`RTCDataChannel` - dwukierunkowy kanał danych o API podobnym do WebSocketów, ale realizowany po SCTP * Wymagane i opcjonalne kodeki audio i video Standard zakłada istnienie kanału komunikacyjnego między klientami (**signaling**) pozwalającego wymieniać wiadomości niezbędne do zestawienia P2P. Zwykle jest realizowane z pomocą serwera (standard nie definiuje, równie dobrze można by wysyłać sobie wiadomości gołębiem pocztowym) i WebSocketów Pierwsza, robocza specyfikacja (W3C Working Draft) została opublikowana w 2011 roku. W 2017 osiągnęła status Candidate Recommendation by w styczniu 2021 (sic!) stać się oficjalną rekomendacją W3C ### SDP i media - m **Zadanie** ``` git clone https://github.com/membraneframework/membrane_webrtc_tutorial.git cd membrane_webrtc_tutorial/assets ``` W pliku `app.js`, w funkcji `init`, pobierz stream z kamery i mikrofonu. Użyj [MediaDevides.getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) ```javascript const localStream = await ... ``` Wyświetl otrzymany stream w elemencie `video`, zdefiniowanym w pliku `index.html` ```javascript document.querySelector('#local-video').srcObject = localStream; ``` Otwórz plik `index.html` w przeglądarce. Powinieneś zobaczyć swój obraz z kamery i usłyszeć dźwięk z mikrofonu. **Zadanie** Żeby przesłać stream po WebRTC, użyjemy klasy [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) ```javascript const peerConnection = new RTCPeerConnection(); ``` Na początek dodamy nasz lokalny stream w instancji tej klasy. Ponieważ RTCPeerConnection operuje na trackach, a nie na streamach, musimy dodać każdy track (audio i video) oddzielnie. Nie zmienia to jednak faktu, że dodając track musimy przekazać również stream, do którego ten track należy, gdyż inaczej mogłoby się np. rozsynchronizować audio i video. Tracki w peer connection dodajemy używając [RTCPeerConnection.add_track](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack), przekazując jej każdy track i odpowiadający mu stream - w tym wypadku będzie to `localStream`. ```javascript localStream.getTracks().forEach(track => { ... }); ``` ***Pytanie: Co to jest SDP i do czego służy?*** // Opowiedzieć o SDP offer-answer **Zadanie** Po dodaniu tracków, możemy wygenerować ofertę SDP, którą będziemy mogli wysłać do pozostałych uczestników konferencji. Wygeneruj ofertę SDP używając [RTCPeerConnection.createOffer](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer) i wypisz ją w konsoli ```javascript const offer = await ... console.log(offer.sdp); ``` **Raport:** Wypisz kodeki audio i video, które wspiera Twoja przeglądarka. // Opowiedzieć o RTCP mux i BUNDLE ### STUN, ICE, TURN - b ### NAT - dyskusja i przypomnienie ***Pytanie: Mamy 2 osoby z przeglądarkami, każda u siebie w domu, w swojej sieci z jakimś dostępem do internetu. Chcemy ustanowić między nimi połączenie - jaki jest problem?*** Odp: NAT #### Jak działa NAT? Zamienia adres źródłowy i port na inne w komunikacji na zewnątrz sieci ukrywając adresację wewnątrz sieci, np. mając publiczny adres 83.142.186.98 i komputer w sieci pod 192.168.0.10 ruch z niego będzie widoczny jako wychodzący z 83.142.186.98. Odpowiedzi też będą szły na ten adres i router pamiętający, że na danym porcie jest mapping zamieni ten adres na 192.168.0.10 kierując pakiety to odpowiedniego komputera w sieci. Na każdym porcie może być inny mapping. #### Dlaczego NAT jest problemem? Bo nie mamy publicznego IP drugiej strony, odpowiedni mapping w NAT jeszcze nie powstał ### STUN Do radzenia sobie z nim stworzono STUN (pierwotnie Simple Traversal of UDP through NAT, obecnie Session Traversal Utilities for NAT), który definiuje protokół do użycia w celu ominięcia NAT, natomiast sam w sobie nie podaje jak to zrobić. To jest określane w ICE (Interactive Connectivity Establishment) #### (*) Budowa ##### Header ![](https://i.imgur.com/KYnZVQP.png) ![](https://i.imgur.com/k6ubgqb.png) * M11-M0 -> Metoda * `0b000000000001` - Binding * C1-C0 - klasa * `0b00` request, * `0b01` success response, * `0b10` error response, * `0b11` indication ##### 0+ attributes ![](https://i.imgur.com/TnIVbgw.png) ### ICE ICE zakłada istnienie kanału signalingu umożliwiającego komunikację obu peerom (w ICE są oni ICE nazywani agentami). Składa się z następujących etapów: * Zbieranie kandydatów (Candidate Gathering) * host - lokalne adresy interfejsów * mDNS (Multicast DNS) - losowe adresy zakończone na .local, obejście problemu wyciekających IP * server-reflexive - uzyskane ze STUN * relay - TURN * Sprawdzanie łączności (Connectivity check) * Nominowanie par kandydatów i kończenie **Zadanie: znaleźć najnowsze RFC dla ICE, znaleźć czym jest ICE lite i czym się różni od pełnej implementacji** #### STUN Binding w ICE Umożliwia poznanie swojego publicznego adresu i "przebicie dziury" w NAT dla danego portu UDP. 1. Klient wysyła stun binding request z 192.168.0.10:42137 do STUNa 2. Router z NAT zmienia Źródłowy adres i port na 83.123.456.789:50000, zapamiętuje mapping 192.168.0.10:42137 <-> 83.123.456.789:50000 (klient o tym nie wie) 3. STUN odpowiada na 83.123.456.789:50000 zawierając w atrybucie STUN ten adres 4. Router zamienia w przychodzącej wiadomości docelowy adres i port na 192.168.0.10:42137 5. Klient dowiaduje się o 83.123.456.789:50000, może przekazać to innym kanałem drugiemu użytkownikowi 6. Wiadomości od innego klienta wysyłane na 83.123.456.789:50000 będą dalej trafiać na 192.168.0.10:42137 * **Ale czy zawsze?** ##### Kiedy STUN może nie wystarczyć * Specyficzne typy NAT mogą uniemożliwić "hole-punching" (np. dostawca mobilnego internetu) * Firewall może blokować ruch UDP * Firewall może blokować ruch inny niż HTTP (TCP port 80) i/lub HTTPS (TCP 443) ##### (*) Typy NAT ![](https://i.imgur.com/7qr1CEI.png) https://dh2i.com/kbs/kbs-2961448-understanding-different-nat-types-and-hole-punching/ #### TURN w ICE Pozwala użyć serwera (z publicznym adresem) jako pośrednika ("relay") w komunikacji pomiędzy klientami. Możliwe protokoły * UDP * TCP * TLS ##### Schemat działania 1. Klient komunikuje się z TURN-em prosząc a zaalokowanie portu do komunikacji (Candidate gathering) * Może być wymagana autoryzacja 2. Po otrzymaniu odpowiedzi przesyła go drugiemu klientowi (po signalingu) 3. Sam wysyła dane z tego samego portu i na ten sam, którego użył do alokacji portu 4. Drugi klient wysyła i odbiera z TURNa przez zaalokowany port **Zadanie: Obserwacja pakietów STUN w Wiresharku** 1. Dodaj w istniejącym kodzie EventHandler dla stworzonego PeerConnection wypisujący w consoli wygenerowanych `iceCandidate`ów * [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#event_handlers) * ```javascript peerConnection.[...] = (event) => console.log(event.candidate); ``` 1. Dodaj ustawianie wygenerowanej oferty SDP jako localDescription utworzonego `RTCPeerConnection` * [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#methods) * ```javascript await peerConnection.[...](offer); ``` 1. Odśwież demo w przeglądarce i odpowiedz: * Ile kandydatów się wypisuje? * Jaki mają typ? * Czym się różnią? 1. Dodaj konfigurację przekazywaną do konstruktora `RTCPeerConnection` z `bundlePolicy` ustawionym na `max-bundle` * [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection) 1. Odśwież demo w przeglądarce i sprawdź, co się zmieniło 1. Dodaj do konfiguracji iceServer STUN * ```javascript iceServers: [ {urls: "stun:turn.membraneframework.org:19302"} ] ``` * Awaryjny URL: ```javascript { urls: "stun:stun.l.google.com:19302" } ``` * [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection) 1. Uruchom Wireshark ze słuchaniem na wszystkich interface'ach, dodaj filtr `stun` 1. Odśwież demo w przeglądarce * sprawdź, co się zmieniło w konsoli * sprawdź, co pojawiło się w Wiresharku * Jaki jest publiczny IP Twojego komputera? * W jakim atrybucie pakietu STUN jest to przekazane? 1. Dodaj konfigurację iceServer TURN w postaci 3 url: ```javascript iceServers: [ { urls: [ "turn:turn.membraneframework.org?transport=udp", "turn:turn.membraneframework.org:3478?transport=tcp", "turns:turn.membraneframework.org:443?transport=tcp" ], username: "turn", credential: "T45B264i89p9" } ] ``` 1. Aby pozbyć się kandydatów lokalnych i server-reflexive, można użyć dodatkowej opcji: ```javascript iceTransportPolicy: 'relay' ``` 3. Zresetuj nasłuch w Wiresharku, odśwież stronę i przeanalizuj pakiety * z filtrem `stun && udp` * Chrome, nawet do TURNa wysyła najpierw BindingRequest (FF tego nie robi) * Jaka nowa metoda wiadomości STUN się tu pojawia? * Jaki port zaalokował TURN na potrzeby komunikacji? (XOR-RELAYED-ADDRESS) * Któremu URL to odpowiada? * z filtrem `ip.addr == 95.216.181.175 && tcp.port == 3478` * Któremu URL to odpowiada? * z filtrem `ip.addr == 95.216.181.175 && tcp.port == 443` * Któremu URL to odpowiada? * Czemu nie widzimy protokołu STUN? 4. Sprawdź wypisanych w konsoli iceCandidate'ów * Jak rozpoznać, który używa UDP, TCP a który TLS (hint: zwróć uwagę na pole priority) ### Łączenie z drugą przeglądarką - m **Zadanie** Żeby nawiązać połączenie WebRTC z drugą przeglądarką, potrzebujemy serwera, który prześle SDP i kandydatów ICE. Użyjemy do tego prostego serwera napisanego we frameworku Phoenix w języku Elixir. Żeby go uruchomić, wyjdź poziom wyżej z katalogu `assets` ``` cd .. ``` pobierz zależności ``` mix deps.get ``` oraz skompiluj i uruchom serwer ``` mix phx.server ``` Stronę możesz teraz odwiedzić pod adresem http://localhost:4000/index.html. Powinna działać tak samo jak dotąd. // Tutaj już będą potrzebne jakieś podstawy elixira... albo ograniczymy backend, albo będzie trzeba zrobić jakieś wprowadzenie już tutaj **Zadanie** Do komunikacji z serwerem użyjemy Phoenix Channels. Jest to lekki wrapper na WebSockety, pozwalający na tworzenie logicznych kanałów w obrębie jednego połączenia. Jest to jednocześnie najprostszy sposób komunikacji dwukierunkowej z serwerem Phoenix. Stworzymy więc socket i w nim kanał `room` ```javascript const socket = new Phoenix.Socket("/socket"); await socket.connect(); const channel = socket.channel("room"); await channel.join(); ``` Po stronie serwera połączenie będzie obsługiwane w module `VideoRoomWeb.PeerChannel`, zaimplementowanym w pliku `lib/videoroom_web/peer_channel.ex`. Przychodzące połączenie obsłużymy implementując funkcję `join` ```elixir @impl true def join("room", _params, socket) do Logger.info("Peer joined") {:ok, socket} end ``` Po uruchomieniu serwera i wejściu na stronę, w terminalu powinno się wyświetlić `Peer joined`. Wróćmy do części przeglądarkowej. Logikę wymiany SDP i kandydatów ICE zaimplementujemy obsługując wiadomości przychodzące otwartym channelem. W tym celu musimy zarejestrować odpowiednie callbacki, używając `Phoenix.Channel.on`. Aby uniknąć race condition, powinniśmy to zrobić ***przed wywołaniem `channel.join`***. Gdy serwer poprosi nas o ofertę SDP, powinniśmy ją wysłać oraz ustawić metodą [RTCPeerConnection.setLocalDescription](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription) ```javascript channel.on("plox_send_offer_sir", async (_data) => { const offer = await ... // stwórz ofertę SDP channel.push("sdp_offer", offer); await ... }); ``` Spowoduje to rozpoczęcie zbierania kandydatów ICE, którzy zostaną nam przekazani w callbacku `RTCPeerConnection.onicecandidate`. Powinniśmy ich również wysłać do serwera. ```javascript peerConnection.onicecandidate = (event) => channel.push("ice_candidate", event.candidate); ``` Po stronie serwera możemy poprosić o wysłanie oferty SDP w callbacku `VideoRoomWeb.PeerChannel.join` ```elixir push(socket, "plox_send_offer_sir", %{}) ``` a następnie odebrać wiadomości implementując callback `handle_in` ```elixir @impl true def handle_in(message_type, _message_payload, socket) do Logger.info("Received message #{message_type}") {:noreply, socket} end ``` Po uruchomieniu serwera i wejściu na stronę w terminalu powinno wypisać się `Received message sdp_offer` i kilkukrotnie `Received message ice_candidate`. **Zadanie** Zaimpementujmy teraz obsługę odpowiedzi SDP. Odpowiedź SDP, podobnie jak oferta, zawiera tracki, które dany peer będzie przesyłać. Zawiera jednak również tracki otrzymane w ofercie, z potencjalnie zawężoną listą obsługiwanych kodeków i rozszerzeń. W przypadku, gdy otrzymamy odpowiedź SDP, wystarczy że ustawimy ją używając [RTCPeerConnection.setRemoteDescription](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription) ```javascript channel.on("sdp_answer", async (answer) => { await peerConnection... }) ``` Gdy natomiast otrzymamy kandydata ICE, dodajemy go używając [RTCPeerConnection.addIceCandidate](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addIceCandidate) ```javascript channel.on("ice_candidate", async (candidate) => { await peerConnection... }) ``` Obsłużyliśmy sytuację, w której peer wysyła ofertę SDP. Powinien to jednak zrobić tylko jeden peer - drugi musi tę ofertę odpowiednio obsłużyć: - ustawić ją w `RTCPeerConnection` przy użyciu [RTCPeerConnection.setRemoteDescription](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription) - wygenerować odpowiedź [RTCPeerConnection.createAnswer](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer) - odesłać odpowiedź - ustawić odpowiedź w `RTCPeerConnection` [RTCPeerConnection.setLocalDescription](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription) ```javascript channel.on("sdp_offer", async (offer) => { await peerConnection... const answer = await ... channel.push("sdp_answer", answer); await peerConnection... }); ``` Na koniec zaimplementuj callback `RTCPeerConnection.ontrack`, dodając stream do elementu ```javascript peerConnection.ontrack = (event) => { document.querySelector('#remote-video').srcObject = event.streams[0]; } ``` Przejdźmy teraz do przekazywania wiadomości na serwerze. Użyjemy do tego modulu `VideoRoom.Room` zdefiniowanego w pliku `lib/videoroom/room.ex`. Pierwszą wiadomością, jaką obsłużymy, będzie wiadomość `join`, w której zapiszemy identyfikator channela w stanie pokoju ```elixir @impl true def handle_info({:join, pid}, peers) do {:noreply, [pid | peers]} end ``` Drugą wiadomością będzie `signal`, którą prześlemy do drugiego peera ```elixir @impl true def handle_info({:signal, message_type, message_payload, pid}, peers) do peer_pid = Enum.find(peers, fn peer_pid -> peer_pid != pid end) send(peer_pid, {:signal, message_type, message_payload}) {:noreply, peers} end ``` Teraz zaimplementujemy wysyłanie tych wiadomości w `VideoRoomWeb.PeerChannel`. W callbacku `join` dodaj ```elixir send(VideoRoom.Room, {:join, self()}) ``` a w `handle_in` dodaj przesyłanie otrzymanych wiadomości do pokoju ```elixir send(VideoRoom.Room, ... ``` Wiadomości od pokoju odbierz w `handle_info` i wyślij channelem ```elixir @impl true def handle_info({:signal, message_type, message_payload}, socket) do push(socket, message_type, message_payload) {:noreply, socket} end ``` Ostatnia zmiana, jaką musimy wykonać, to przeniesienie wysłania prośby o wysłanie SDP do `VideoRoom.Room`. Ofertę SDP powinien wysłać tylko jeden peer i to dopiero wtedy, gdy oba są już podłączone. Usuń linijkę ```elixir push(socket, "plox_send_offer_sir", %{}) ``` z funkcji `VideoRoomWeb.PeerChannel.join` i dodaj ```elixir if peers != [] do send(pid, {:signal, "plox_send_offer_sir", %{}}) end ``` na początek obsługi wiadomości `join` w `VideoRoom.Room`. Uruchom serwer i wejdź na stronę w dwóch kartach przeglądarki. Powinieneś zobaczyć dwa obrazy z kamery w każdej z nich: jeden lokalny, drugi przesłany po WebRTC. Następnie wyłącz serwer. Czy stream zdalny nadal się odtwarza w obu kartach przeglądarki? // Opowiedzieć o webrtc internals ### Szyfrowanie w WebRTC - b * Jest obowiązkowe * DataChannele używają DTLS * Media wykorzystują SRTP * SRTP wykorzystuje handshake DTLS do ustalenia kontekstu kryptograficznego (kluczy szyfrowania i deszyfrowania), potem przesyłane są pakiety RTP z zaszyfrowanym payloadem (w uproszczeniu) #### Handshake DTLS https://docs.oracle.com/en/java/javase/16/security/transport-layer-security-tls-protocol-overview.html#GUID-69ECD56C-3B20-47F4-AEF0-A06EFA13A61D ![](https://docs.oracle.com/en/java/javase/16/security/img/dtls-handshake.png) ### Video processing na frontendzie (?) - b // Raczej nadmiarowe, nie starczy czasu * Render na Canvasie, OpenCV.js, użycie canvas jako źródła * Świeżynka - WebRTC insertable streams ## Lab 2 - WebRTC SFU ### Architektury wideokonferencji - b WebRTC służy do połączeń P2P, ale podstawowym pomysłem zastosowania tej technologii jest wideokonferencja - pokój umożliwiający komunikację w czasie rzeczywistym co najmniej kilku osób. #### Mesh ![](https://meetrix.io/blog/assets/images/webrtc/01-01--what-is-webrtc/webrtc-mesh.png =300x300) * architektura połączeń "każdy z każdym" * Duża liczba połączeń: n * (n-1) / 2 * Duże zużecie pasma: (upload i download rosną liniowo z liczbą użytkowników) * tania w utrzmaniu - koszty przrzucone na userów, wystarczy serwer do signalingu #### MCU - Multipoint Conferencing Unit ![MCU architecture](https://meetrix.io/blog/assets/images/webrtc/01-01--what-is-webrtc/webrtc-mcu.png =340x300) * Znana z telekomunikacji alternatywa * Sygnał od użytkowników zbierany na serwerze, gdzie generowane jest wyjście specyficzne dla każdego z nich * dla audio - zmiksowane źródła z wycięciem audio danego usera * dla wideo - wideo aktywnego mówcy, grid (wszyscy widzą to samo!) * Jedno połączenie (2-kierunkowe) * stałe wymagania pasma * Wymaga bardzo kosztownego obliczeniowo przetwarzania multimediów w czasie rzeczywistym (horrendalne koszty infrastruktury przy wideo) * Umożliwia łatwe tworzenie nagrań w 100% oddających to, co widzą odbiorcy bez dodatkowego processingu * Pozwala dodawać efekty po stronie serwera (np. blur tła) #### SFU - Selective Forwarding Unit ![sfu](https://meetrix.io/blog/assets/images/webrtc/01-01--what-is-webrtc/webrtc-sfu.png =330x300) * W większości przypadków złoty środek * Media przekazywane są bez dekodowania * Może to być wada, gdy potrzebna jest analiza/obróbka po stronie serwera * Stworzenie nagrania wymaga dodatkowej pracy * 1 upload, n * download * Stałe wymagania dla uploadu, liniowo rosnące dla downloadu * akceptowalne przy łączach asymetrycznych * dany klient może ograniczać odbieranie niektórych sygnałów (np. wideo usera, którego nie wyświetla na ekranie) ##### SVC - Scalable Video Coding Pomocne przy wykorzystywaniu SFU mogą być techniki dostarczania sygnału wideo w różnych wariantach jakości - a co za tym idzie o różnych wymaganiach względem pasma. WebRTC wykorzystuje skalowalne enkodowanie wideo https://www.w3.org/TR/webrtc-svc * Temporal layering - pozwala pomijać klatki, generując różne warianty framerate'u ![L1T2](https://www.w3.org/TR/webrtc-svc/images/L1T2.svg) * Simulcast (VP8) - równoległe enkodowanie w kilku niezależnych wariantach o innej rozdzielczości i/lub bitrate. Może być połączone z temporal layering. ![S2T2](https://www.w3.org/TR/webrtc-svc/images/S2T2.svg) * Spatial layering (H264, VP9) - Ramki różnych warstw rozdzielczości zależą od siebie - tj. połączenie ramki z warstwy wyższej i niższej daje ramkę w wyższej rozdzielczości. Bardziej efektywne od simulcast. ![L2T2](https://www.w3.org/TR/webrtc-svc/images/L2T2.svg) ## membrane SFU - m ![](https://i.imgur.com/FxAcKzX.png) ## syntax elixira - b https://devhints.io/elixir * Dynamiczne typowanie (z dodatkowymi checkami np. dla struktur) * Kod w funkcjach zawartych w modułach * Podstawowe typy i struktury danych: * atom (`:ok`, `:dowolny_atom`, `true`, `nil`) * krotka (tuple) `{:ok, 42}` * lista `[1, 2, 3]` * mapa `%{klucz_atom: 42}`, `%{"dowolny_klucz" => 42}` * Pattern matching * Function clauses - pattern matching w nagłówku funkcji * `iex -S mix` - interaktywna konsola w projekcie * `iex -S mix run --no-start` - interaktywna konsola bez startu aplikacji (np. serwera) ale z dostępem do zależności i modułów projektu ## skleic demo videorooma - m https://github.com/membraneframework/membrane_demo/tree/master/webrtc/videoroom ## WebRTC -> HLS (?) https://github.com/membraneframework/membrane_demo/tree/master/webrtc_to_hls