Prva tri članka o WebRTC-u dotakla su se teorije o WebRTC-u, uputa kako napraviti jednostavan signalni poslužitelj i klijentsku WebRTC web aplikaciju te kako napraviti jednostavnu klijentsku WebRTC Android aplikaciju, no prikazani primjeri su više-manje bili jednostavni, bez previše kompleksnosti. Za početak je to dobro, no u stvarnom svijetu često nam je potrebno rješavanje malo složenijih zahtjeva, pronalaženje novih načina za rješavanje problema i rukovanje potencijalnim pogreškama koje se pojave na tom putu. Upravo to obrađujemo u ovom članku.
Istražimo opcije
Korištenje standardne implementacije “vanilla” WebRTC-a izvrsno je i većinu vremena je sve što nam treba u svrhu kreiranja jednostavnog WebRTC aplikacijskog rješenja. Međutim, ponekad nam je potrebna malo veća kontrola nad našim aplikacijama, možda da promijenimo neke interne parametre koji su jedinstveni za naš slučaj upotrebe (kao što je korištenje određenog certifikata, relejnog poslužitelja, audio/video kodeka, brzine prijenosa itd.), a srećom WebRTC-u ne nedostaje i podrška za te situacije. Istražimo nekoliko takvih opcija.
Napomena: svi isječci koda prikazani u tekstu ispod također su dostupni na Githubu ovog članka.
Parametri peer konekcije
Na početku zaronimo u jednu opciju koju nam WebRTC daje, a koja bi ponekad mogla ostati neotkrivena – stvaranje objekta peer konekcije, uzimajući u obzir sve/većinu njegovih dostupnih parametara. Sljedeća metoda pokazuje kako stvoriti WebRTC peer konekciju u TypeScript-u (Angular), primjenom svih dostupnih parametara:
createPeerConnection(
iceServers: RTCIceServer[],
sdpSemantics: ‘unified-plan’ | ‘plan-b’,
bundlePolicy: RTCBundlePolicy,
iceTransportPolicy: RTCIceTransportPolicy,
rtcpMuxPolicy: RTCRtcpMuxPolicy,
peerIdentity: string,
certificates: RTCCertificate[],
iceCandidatePoolSize: number): RTCPeerConnection {
return new RTCPeerConnection({
iceServers, sdpSemantics, bundlePolicy, iceTransportPolicy, rtcpMuxPolicy, peerIdentity, certificates, iceCandidatePoolSize
} as RTCConfiguration);
}
Kao što se vidi, trenutno se može primijeniti čak osam različitih parametara peer konekcije za prilagođavanje iste. Parametri su sljedeći:
1) ICE servers
Popis poslužitelja koje koristiti Interactive Connectivity Establishment (ICE) agent. Postoje 2 vrste servera: STUN ili TURN. Važni su kako bi se udaljeni peerovi mogli pronaći preko Interneta, da bi se veza pokrenula i da bi služili kao relej za prijenos medijskih podataka ako je potrebno. Više o ICE poslužiteljima i njihovoj namjeni možete pročitati u prvom WebRTC članku.
2) SDP semantics
Session Description Protocol (SDP) semantics definira koji će se interni WebRTC algoritam koristiti za peer konekciju na oba kraja. To može biti “plan-b” (zastarjela semantika koju je Google izradio za Google Chrome u prošlosti) ili “unified-plan” (trenutačno standardna semantika WebRTC-a koju podržava svaki moderni preglednik i izvorna platforma).
3) Bundle policy
Definira kako postupati s pregovorima kandidata kada udaljeni peer uređaj nije kompatibilan sa standardom SDP paketa. Može se postaviti na:
- “balanced” – ICE agent stvara jedan objekt “RTCDtlsTransport” za svaku vrstu sadržaja: audio, video i podatkovni kanal.
- “max-compat” – po medijskoj traci se kreira jedan objekt “RTCDtlsTransport te dodatni za podatkovne kanale.
- “max-bundle” – kreira se jedan objekt “RTCDtlsTransport” za prijenos svih podataka o peer konekcijama.
4) ICE transportna politika
Politika koja određuje kako se ICE kandidati prevoze od jednog peera do drugog. Može se postaviti na “all” (svi ICE kandidati se razmatraju) ili “relay” (razmatrat će se samo relejni ICE kandidati, što znači uglavnom oni koji prolaze kroz TURN server).
5) RTCP mux politika
Politika koja će se koristiti prilikom prikupljanja ICE kandidata (za podršku nemultipleksiranog RTCP). Može biti ili “negotiate” (potrebni su i RTP i RTCP kandidati) ili “require” (samo RTP kandidati).
6) Identitet peerova
Određuje ciljni peer identitet za peer. Ako je ova vrijednost postavljena, peer konekcija se neće povezati s udaljenim peerom osim ako se ne može autentificirati s danim imenom.
7) Certifikati
Popis RTC certifikata koje će peer konekcija koristiti za autentifikaciju.
8) ICE candidate pool size
Poželjna veličina skupine kandidata za ICE. Ovo se može koristiti za ograničavanje broja ICE kandidata koji su okupljeni, čime se brže uspostavlja veza.
Korištenje ovih parametara može stvarno pozitivno utjecati na peer konekciju, pogotovo ako ju pokušavamo optimizirati ili je prilagoditi nekom vrlo specifičnom use-caseu. Više o svakom parametru možete pronaći na Mozilla developer stranicama za klasu WebRTC peer konekcije.
Promjena audio/video kodeka
Sljedeća zanimljiva mogućnost koja je izvediva s WebRTC-om je ručni odabir audio/video kodeka koji će peerovi koristiti prilikom slanja medijskog sadržaja. Prema zadanim postavkama, WebRTC ima algoritam koji automatski bira podržane kodeke, a to je u većini slučajeva “OPUS” (sa zadanim parametrima) za audio i “VP8” za video. Ako to želimo promijeniti (na primjer, postaviti “VP9” ili “H264” kao video kodeke), moramo koristiti malo “hacky“ metodu zvanu SDP munging.
SDP munging se u osnovi odnosi na proces ručnog mijenjanja SDP sadržaja (putem koda), bez korištenja API-ja koje pruža WebRTC. To je učinjeno jer WebRTC API-ji još ne pružaju podršku za sve mogućnosti koje nudi WebRTC. Kako vrijeme bude prolazilo, WebRTC programeri će dodavati podršku za sve više i više stvari sa svojim API-jima, ali za sada je SDP munging jedino održivo rješenje za takve slučajeve upotrebe.
Kako bismo promijenili kodeke putem SDP munginga, moramo preurediti broj njihovog payloada unutar SDP linija tako da je željeni payload kodeka naveden ispred ostalih. Typescript (Angular) metode za takav pristup su sljedeće:
setCodecs(sdp: RTCSessionDescriptionInit, type: ‘audio’ | ‘video’, codecMimeType: string): RTCSessionDescriptionInit {
const sdpLines = sdp.sdp.split(‘\r\n’);
sdpLines.forEach((str, i) => {
if (str.startsWith(‘m=’ + type)) {
const lineWords = str.split(‘ ‘);
const payloads = this.getPayloads(sdp.sdp, codecMimeType);
if (payloads.length > 0) {
payloads.forEach(codec => {
const index = lineWords.indexOf(codec, 2);
lineWords.splice(index, 1);
});
str = lineWords[0] + ‘ ‘ + lineWords[1] + ‘ ‘ + lineWords[2];
payloads.forEach(codec => {
str = str + ‘ ‘ + codec;
});
for (let k = 3; k < lineWords.length; k++) {
str = str + ‘ ‘ + lineWords[k];
}
}
sdpLines[i] = str;
}
});
sdp = new RTCSessionDescription({
type: sdp.type,
sdp: sdpLines.join(‘\r\n’),
});
return sdp;
}
getPayloads(sdp: string, codec: string): string[] {
const payloads = [];
const sdpLines = sdp.split(‘\r\n’);
sdpLines.forEach((str, i) => {
if (str.indexOf(‘a=rtpmap:’) !== –1 && str.indexOf(codec) !== –1) {
payloads.push(str.split(‘a=rtpmap:’).pop().split(‘ ‘)[0]);
}
});
return payloads.filter((v, i) => payloads.indexOf(v) === i);
}
Naravno, da bismo znali koji kodeci su dostupni za korištenje, moramo dobiti taj popis od WebRTC API-ja. Typescript (Angular) primjer dohvaćanja dostupnih kodeka je sljedeći:
getCodecs(type: ‘audio’ | ‘video’): string[] {
return RTCRtpSender.getCapabilities(type).codecs.map(c => c.mimeType).filter((value, index, self) => self.indexOf(value) === index);
}
Nakon što se promijeni redoslijed kodeka u SDP linijama, WebRTC bi trebao početi koristiti preferirane kodeke za medijsku komunikaciju.
Promjena bitratea
Bitrateovi s kojima WebRTC radi (u SDP-u) su podijeljeni u tri glavne kategorije:
1) Start bitrate – brzina prijenosa s kojom će WebRTC pokušati pokrenuti medijsku sesiju. Ako je ovdje navedena previsoka vrijednost, WebRTC će automatski odabrati najvišu dostupnu.
2) Minimalna (min) brzina prijenosa – brzina prijenosa ispod koje WebRTC nikada neće ići. Ako je navedena previsoka vrijednost, WebRTC će ponovno ići na najvišu dostupnu brzinu prijenosa i neće ići niže.
3) Maksimalna (maks.) brzina prijenosa – najviša granica bitratea koju WebRTC nikada neće premašiti. Definiranje preniske maksimalne brzine prijenosa može rezultirati gubitkom kvalitete zvuka/videa, dok previsoka može rezultirati nepotrebnom upotrebom mrežnih podataka, stoga treba biti jako oprezan s postavljanjem ovog parametra.
Prema zadanim postavkama, ako jedna od brzina prijenosa nije navedena, WebRTC će automatski odabrati onu koja je optimalna prema svom algoritmu.
Što se tiče implementacije, promjena bitratea se ponovno vrši putem SDP munginga jer još nije dostupan WebRTC API. Primjer koda za Typescript (Angular) možete vidjeti ovdje:
changeBitrate(sdp: RTCSessionDescriptionInit, start: string, min: string, max: string): any | RTCSessionDescriptionInit {
const sdpLines = sdp.sdp.split(‘\r\n’);
sdpLines.forEach((str, i) => {
if (str.indexOf(‘a=fmtp’) !== –1) {
if (str.indexOf(‘x-google-‘) === –1) {
sdpLines[i] = str + `;x-google-max-bitrate=${max};x-google-min-bitrate=${min};x-google-start-bitrate=${start}`;
} else {
sdpLines[i] = str.split(‘;x-google-‘)[0] + `;x-google-max-bitrate=${max};x-google-min-bitrate=${min};x-google-start-bitrate=${start}`;
}
}
});
sdp = new RTCSessionDescription({
type: sdp.type,
sdp: sdpLines.join(‘\r\n’),
});
return sdp;
}
Kao što se vidi u primjeru koda, manipulacija bitrateova se vrši dodavanjem/promjenom vrijednosti “x-google-start/min/max-bitrate=” u SDP sadržaju.
Oporavak od grešaka
Algoritam za uspostavljanje WebRTC sesije je proces od više koraka u kojem svaki korak mora biti sinkroniziran sa svim prethodnim kako bi mogao funkcionirati (više o tome u našem drugom članku o WebRTC-u). Ako nešto pođe po zlu u jednom od koraka (na primjer postoji mrežna pogreška, pogreška sinkronizacije ili slično), često smo zapeli u nestabilnom stanju u kojem sesija nije uspostavljena, a cijelo korisničko iskustvo se svodi na crni ekran na reprodukciji videa ili tišinu na reprodukciji zvuka. Kako bi se uhvatio u koštac s tim, WebRTC je uveo koncept “ICE restart” – metodu koja omogućuje WebRTC aplikaciji da jednostavno zatraži da se prikupljanje ICE kandidata ponovi na oba kraja veze. Na taj način možemo spasiti našu WebRTC vezu od nekih pogrešaka koje bi inače rezultirale neuspjehom sesije. Ponovno pokretanje ICE-a može se obaviti ili pozivanjem metode peer konekcije “restartIce” u preglednicima koji je podržavaju ili slanje novog SDP offera ponude s parametrom “iceRestart: true“, kao što se vidi u primjeru koda Typescript (Angular) u nastavku:
doIceRestart(peerConnection: RTCPeerConnection | any, messageSender: MessageSender): void {
try {
// try using new restartIce method
peerConnection.restartIce();
} catch(error) {
// if it is not supported, use the old implementation
peerConnection.createOffer({
iceRestart: true
})
.then((sdp: RTCSessionDescriptionInit) => {
peerConnection.setLocalDescription(sdp);
messageSender.sendMessage(sdp);
});
}
}
Rješavanje problema
Ako ponovno pokretanje ICE-a ne pomogne, moramo pronaći način da analiziramo peer konekciju i točno ukažemo što (i gdje) je pošlo po zlu. Kako bi pomogao programerima u tome, WebRTC stvara dobro organiziran skup statistika koje se prikupljaju tijekom svake WebRTC sesije. Te se statistike mogu pregledati uživo na jedan od dva načina: putem WebRTC stranice sa statistikom preglednika ili putem “getStats” WebRTC API-ja.
WebRTC stranica sa statistikom preglednika
Gotovo svaki internetski preglednik koji podržava WebRTC ima svoju WebRTC stranicu sa statistikom koja korisnicima omogućuje pristup podacima WebRTC statistike u stvarnom vremenu. Za Google Chrome i Opera preglednike stranicu za statistiku možete pronaći na chrome://webrtc-internals/, dok je za Firefox na about:webrtc. Svaka stranica je prilično slična, sadrži sve podatke o ICE kandidatima, audio/video streamovima, promjenama stanja veze itd. Ovdje možete vidjeti primjer WebRTC stranice sa statistikom:
“getStats” WebRTC API
Sve statistike koje su prikazane na WebRTC stranici sa statistikom preglednika također se mogu dobiti iz WebRTC aplikacije pomoću WebRTC API-ja iz koda. Za to su programeri WebRTC-a kreirali “getStats” API koji nam daje izvješća o podacima iz kojih možemo uzeti sve WebRTC statističke podatke koji su nam potrebni i djelovati prema njima. Primjer Typescript (Angular) poziva metode “getStats“, koji uzima sve statistike veze i prikazuje se u konzoli, može se vidjeti u nastavku:
logStats(peerConnection: RTCPeerConnection, type: ‘inbound’ | ‘outbound’ | ‘all’) {
peerConnection.getStats().then(stat => {
stat.forEach(report => {
switch(type) {
case ‘inbound’:
if (report.type === ‘inbound-rtp’) {
console.log(report);
}
break;
case ‘outbound’:
if (report.type === ‘outbound-rtp’) {
console.log(report);
}
break;
default:
console.log(report);
}
});
});
}
Tehnologija za moderne web-bazirane komunikacijske sustave u stvarnom vremenu
U ovom članku smo pokazali da, iako je WebRTC jednostavan za implementaciju i korištenje, nisu svi use-casevi tako jednostavni. Ono što je važno je da za te slučajeve WebRTC također ima rješenje. Bez obzira uključuje li to korištenje ne tako dobro poznatog WebRTC API-ja, kao što je “iceRestart” ili “getStats”, ili malo SDP munginga, WebRTC pokriva gotovo svaki složeni zahtjev koji naiđe. To je razlog zašto je i ostat će najbolji odabir tehnologije za moderne web-bazirane komunikacijske sustave u stvarnom vremenu!