# 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


* M11-M0 -> Metoda
* `0b000000000001` - Binding
* C1-C0 - klasa
* `0b00` request,
* `0b01` success response,
* `0b10` error response,
* `0b11` indication
##### 0+ attributes

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

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

* 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

* 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

* 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 
* 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. 
* 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. 
## membrane SFU - m

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