# AGH SSM 2025 | WebRTC Lab 1
Celem ćwiczenia jest utworzenie aplikacji webowej umożliwiającej nawiązanie połączenia peer-to-peer (i wymianę mediów) między dwoma komputerami przy użyciu WebRTC.
Plan działania:
1. `getUserMedia`: dostęp do kamery w przeglądarce
1. SDP: co będziemy wysyłać i odbierać?
1. ICE: jak się połączyć?
1. Dodzwoniliśmy się... ale tylko w jedną stronę i w obrębie komputera
1. Serwery sygnalizacyjne
1. Dodzwoniliśmy się, tym razem na serio
1. Debugowanie WebRTC: co robić, kiedy coś nie działa?
## 0 | Przygotowanie
1. Upewnij się, że w Twoim zespole są co najmniej 2 osoby :)
2. Włącz Ubuntu
3. Sklonuj repo https://tinyurl.com/aghssm
> a jeśli nie działa, to https://github.com/elixir-webrtc/agh-ssm-workshop
## 1 | Dostęp do urządzeń wejściowych AV (`getUserMedia`)
0. Otwórz `ex1/script.js` w ulubionym edytorze tekstu i `ex1/index.html` w przeglądarce Chromium
1. Pobierz stream z kamery i mikrofonu. Użyj [MediaDevices.getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#syntax) (`navigator.mediaDevices.getUserMedia`)
```js
const localStream = await ...
```
2. Wyświetl otrzymany stream w elemencie `video` zdefiniowanym w pliku `index.html` ustawiając `localStream` jako obiekt źródłowy odpowiedniego elementu
```js
const localPlayer = document.getElementById('player');
localPlayer.srcObject = localStream;
```
3. Odśwież stronę, a następnie zezwól na dostęp do urządzeń -- zobaczysz obraz ze swojej kamery.
## 2 | Przesyłanie multimediów w jednej karcie
### 2.1 | SDP
Otwórz `ex2_1/script.js` w ulubionym edytorze tekstu i `ex2_1/index.html` w przeglądarce Chromium
Żeby przesłać stream po WebRTC, użyjemy klasy [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)
```js
const pc = new RTCPeerConnection();
```
Na początek dodamy nasz lokalny stream w instancji tej klasy. Streamy w API przeglądarkowym agregują tracki -- nasz `localStream` zawiera teraz track audio i wideo.
API RTCPeerConnection jest tak skonstruowane, że musimy dodać każdy track (audio i wideo) oddzielnie. Robimy to przy pomocy funkcji [RTCPeerConnection.addTrack](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addTrack), przekazując jej każdy track i odpowiadający mu stream.
>Podanie streamu jest opcjonalne, ale warto to zrobić -- informujemy w ten sposób `RTCPeerConnection`, że dane tracki są powiązane i powinny być ze sobą zsynchronizowane.
```js
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
```
Po dodaniu tracków, możemy wygenerować ofertę SDP, którą będziemy mogli wysłać do pozostałych uczestników
Wygeneruj ofertę SDP używając [RTCPeerConnection.createOffer](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer) i wypisz ją w konsoli
```js
const offer = await ...
console.log(offer.sdp);
```
### 2.2 | ICE
0. Otwórz `ex2_2/script.js`...
1. Dodaj w istniejącym kodzie EventHandler dla stworzonego `PeerConnection` wypisujący w konsoli wygenerowanych `iceCandidate`'ów
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#event_handlers)
```js
pc.??? = (event) => console.log(event.candidate);
```
2. Dodaj ustawianie wygenerowanej oferty SDP jako `localDescription` utworzonego `RTCPeerConnection`
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#methods)
```js
await pc.???(offer);
```
3. Odśwież stronę w przeglądarce i odpowiedz:
- Ile kandydatów się wypisuje?
- Jaki mają typ?
- Czym się różnią?
4. Dodaj konfigurację przekazywaną do konstruktora RTCPeerConnection z bundlePolicy ustawionym na `max-bundle`
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)
5. Odśwież stronę w przeglądarce i sprawdź, co się zmieniło
6. Dodaj do konfiguracji `iceServers` STUN
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection)
```js
iceServers: [
{ urls: "stun:stun.l.google.com:19302" }
]
```
7. Uruchom Wireshark (koniecznie z `sudo`) ze słuchaniem na wszystkich interface'ach, dodaj filtr `stun`
8. Odśwież demo w przeglądarce
- Sprawdź, co się zmieniło w konsoli
- Sprawdź, co pojawiło się w Wiresharku
- Jaki jest publiczny adres IP Twojego komputera?
- W jakim atrybucie pakietu STUN jest to przekazane?
9. Dodaj konfigurację `iceServers` TURN w postaci 3 url:
```js
iceServers: [
{
urls: [
"turn:bigfish.jellyfish.ovh:3478?transport=udp",
"turn:bigfish.jellyfish.ovh:3478?transport=tcp",
"turns:bigfish.jellyfish.ovh:5349?transport=tcp"
],
username: "turnuser",
credential: "c1gAaF2Arycz7I3CKd4QvA"
}
]
```
10. Aby wymusić używanie `relay` kandydatów, można użyć dodatkowej opcji:
```js
iceTransportPolicy: 'relay'
```
11. Zresetuj nasłuch w Wiresharku, odśwież stronę i przeanalizuj pakiety
- z filtrem `stun && udp`
- Chromium, 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 == 65.109.153.64 && tcp.port == 3478`
- Któremu URL to odpowiada?
- z filtrem `ip.addr == 65.109.153.64 && tcp.port == 5349`
- Któremu URL to odpowiada?
- Czemu nie widzimy protokołu STUN?
12. Sprawdź wypisanych w konsoli `iceCandidate`'ów
- Jak rozpoznać, który używa UDP, TCP a który TLS? Zwróć uwagę na pole priority
### 2.3 | Wideo w drugim kafelku
0. Otwórz `ex2_3/script.js`...
1. Utwórz dwie instancje `RTCPeerConnection`, `pc1` i `pc2`.
`pc1` będzie nadawać stream, a `pc2` wyłącznie go odbierać
2. Zaimplementuj *w obu PC* callback `onicecandidate` tak, by przekazywały sobie nawzajem swoich kandydatów
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#methods)
```js
pc1.onicecandidate = async (event) => {
await pc2.???(event.candidate);
}
// analogicznie pc2
```
3. Po stronie odbierającej zaimplementuj callback `ontrack` tak, by przypiąć otrzymany stream do elementu wideo `remotePlayer`
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/TrackEvent)
```js
pc2.ontrack = (event) => {
remotePlayer.??? = event.streams[0];
}
```
4. Po utworzeniu i zaaplikowaniu oferty przez `pc1`:
- zaaplikuj ofertę w `pc2`,
- utwórz i zaaplikuj odpowiedź w `pc2`,
- zaaplikuj odpowiedź w `pc1`.
- [Dokumentacja](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection#methods)
5. Odśwież stronę -- zobaczysz stream w obu elementach wideo.
Czemu kafelek `remotePlayer` najpierw jest mały, a wraz z upływem czasu się powiększa?
## 3 | Wymiana multimediów między komputerami
Czego brakuje nam do nawiązania połączenia WebRTC między dwoma "pingowalnymi" komputerami?
A między dwoma komputerami, które się "nie widzą"?
API WebRTC pozwala na to, żeby dowolna ze stron wygenerowała w dowolnym momencie ofertę SDP. Wykonamy jednak w tym ćwiczeniu pewne uproszczenie -- jedna strona komunikacji będzie wyłącznie generowała ofertę SDP, druga wyłącznie na nią odpowiadała.
Do komunikacji będziemy wykorzystywać prosty serwer sygnalizacyjny, z którym łączymy się po `WebSocket`'cie. Jego działanie ogranicza się do przekazywania wszystkich otrzymanych wiadomości od jednego połączonego klienta do drugiego, i vice versa.
Wiadomości przygotowujemy przy użyciu funkcji `candidateEventToMsg, offerToMsg, answerToMsg` -- a wysyłamy z pomocą `sock.send(msg)`
0. Na jednym komputerze otwórz `ex3_offerer/script.js`, a na drugim `ex3_answerer/script.js`
1. W obu plikach uzupełnij numer swojej grupy
```js
const groupId = 42;
```
2. W obu plikach zaimplementuj callback `onicecandidate` -- gdy wygenerujemy kandydata, przekaż go drugiej stronie.
- Dokumentacja serwera sygnalizacyjnego: jest powyżej :)
3. W obu plikach zaimplementuj funkcję `handleCandidate` -- callback wywoływany, gdy druga strona połączenia przekaże nam swojego kandydata. Obsłuż przypadek `candidate.candidate == null`
```js
async function handleCandidate(candidate) {
if (candidate.candidate == null) {
// czy coś tu trzeba robić?
} else {
await pc.???(candidate);
}
}
```
4. [OFFERER] Zaimplementuj funkcję `start`, wywoływaną po wciśnięciu **wielkiego** przycisku `Start` -- wygeneruj ofertę, zaaplikuj ją, a następnie przekaż drugiej stronie.
5. [ANSWERER] Zaimplementuj funkcję `handleOffer` -- co trzeba zrobić?
6. [OFFERER] Zaimplementuj funkcję `handleAnswer`
7. Otwórz na obu komputerach odpowiednie `index.html`. W konsoli powinien pojawić się log:
```
Signaling socket open (server <NR_GRUPY>)
```
8. Po stronie oferującego wciśnij `Start`. Działa?
## 4 | WebRTC Internals
...czyli multitool i Święty Graal programistów WebRTC w jednym :)
Otwórz [chrome://webrtc-internals](chrome://webrtc-internals) i przyjrzyj się statystykom, w szczególności:
- `packetsSent/s, bytesSent_in_bits/s, packetsReceived/s, bytesReceived_in_bits/s` -- jakie są typowe wartości dla bitrate’ów 150, 500, 1500 kbps?
- `nackCount, retransmittedPacketsSent, packetsLost` -- czy packetsLost musi być niemalejące?
- `qualityLimitationDurations` -- jaka jest typowa charakterystyka?
- `jitterBufferDelay/jitterBufferEmittedCount_in_ms` -- co oznacza?
- `pliCount` -- nie może być zbyt duże, dlaczego?
- `freezeCount, totalFreezesDuration`
- `framesReceived - framesDecoded - framesDropped` -- może być ujemne, dlaczego?
Wytłumacznie: https://www.w3.org/TR/webrtc-stats/
Nie wszystkie statystyki są w standardzie (np. pochodne).
Standard pozwala na dodawanie własnych statystyk.
## 5 | Dumpowanie pakietów RTP z przeglądarki
Zbierzemy teraz odszyfrowane pakiety RTP odebrane przez przeglądarkę.
Będą one wypisywane na `stderr` razem z pozostałymi logami -- odróżnimy je po sufiksie `# RTP_DUMP`
Przykładowo:
```
I 12:15:38.309 000000 90 70 5f f3 cd 65 51 fa 4e 5a # RTP_DUMP
```
0. Zamknij wszystkie działające instancje Chromium
1. Uruchom Chromium z logami
```
chromium --enable-logging=stderr -v=3 --force-fieldtrials=WebRTC-Debugging-RtpDump/Enabled/ >log.txt 2>&1
```
2. Odfiltruj pakiety RTP
```
grep RTP_DUMP log.txt >rtp-dump.txt
```
3. Przekonwertuj pakiety RTP w plik PCAP
```
text2pcap -D -t %H:%M:%S.%f -u 5443,62132 rtp-dump.txt rtp-dump.pcap
```
>Narzędzie `text2pcap` dostarczane jest razem z Wiresharkiem.
>Objaśnienie flag:
> * `-D` -- Każdy pakiet zaczyna się od `I` (inbound) lub `O` (outbound)...
> * `-t <timefmt>` -- ...a potem jest timestamp w takim formacie
> * `-u <srcport>,<destport>` -- Zapakuj pakiety w datagramy UDP z takimi portami
4. Otwórz plik PCAP w Wiresharku
```
wireshark rtp-dump.pcap
```
Po co taka zabawa, skoro można po prostu zrobić capture na interfejsie w Wiresharku?
## 6 | [Opcjonalnie] Co dalej?
1. Wypróbuj różne ustawienia PeerConnection
- wprowadź ograniczenia na audio/wideo (hint: `ex1/solution.js`),
- wymuś traffic przez TLS TURN,
- ...
Czy różnice są wyczuwalne? Kiedy jest najlepszy user experience?
2. Połącz offerer i answerer w jedną aplikację (hint: [Perfect Negotiation](https://blog.mozilla.org/webrtc/perfect-negotiation-in-webrtc/))
3. Dodaj **trzeciego** peera do rozmowy
>Podpowiedź: zadanie jest trudne