# 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