חיבורי עמיתים הם החלק במפרטי WebRTC שמתייחס לחיבור בין שתי אפליקציות במחשבים שונים כדי לתקשר באמצעות פרוטוקול עמית לעמית (P2P). התקשורת בין עמיתים יכולה להיות וידאו, אודיו או נתונים בינאריים שרירותיים (ללקוחות שתומכים ב-API של RTCDataChannel). כדי לגלות איך שני עמיתים יכולים להתחבר, שני הלקוחות צריכים לספק הגדרת שרת ICE. זהו שרת STUN או TURN, והתפקיד שלו הוא לספק מועמדים ל-ICE לכל לקוח, שמועברים לאחר מכן לעמית המרוחק. ההעברה הזו של מועמדים ל-ICE נקראת בדרך כלל איתות.
איתות
המפרט של WebRTC כולל ממשקי API לתקשורת עם שרת ICE (הקמת קישוריות לאינטרנט), אבל רכיב האיתות לא נכלל בו. העברת אותות נדרשת כדי ששני עמיתים יוכלו לשתף את אופן ההתחברות שלהם. בדרך כלל, הבעיה הזו נפתרת באמצעות Web API רגיל שמבוסס על HTTP (כלומר, שירות REST או מנגנון RPC אחר) שבו אפליקציות אינטרנט יכולות להעביר את המידע הדרוש לפני הפעלת חיבור העמיתים.
בקטע הקוד הבא אפשר לראות איך אפשר להשתמש בשירות האיתות הפיקטיבי הזה כדי לשלוח ולקבל הודעות באופן אסינכרוני. המשתנה הזה ישמש בדוגמאות שנותרו במדריך הזה, במקומות שבהם הדבר נחוץ.
// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
// New message from remote client received
});
// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');
אפשר להטמיע את האיתות בדרכים רבות ושונות, ובמפרט של WebRTC לא מצוין פתרון מועדף.
יצירת חיבורים בין עמיתים
כל חיבור עמית לעמית מטופל על ידי אובייקט RTCPeerConnection. הבונה של המחלקה הזו מקבל אובייקט RTCConfiguration יחיד כפרמטר. האובייקט הזה מגדיר איך נוצר החיבור בין העמיתים, והוא צריך לכלול מידע על שרתי ה-ICE שבהם צריך להשתמש.
אחרי שיוצרים את RTCPeerConnection, צריך ליצור הצעת SDP או תשובה, בהתאם לשאלה אם אנחנו הצד המתקשר או הצד שמקבל את השיחה. אחרי שיוצרים את הצעת ה-SDP או את התשובה, צריך לשלוח אותה לצד המרוחק דרך ערוץ אחר. העברת אובייקטים של SDP לעמיתים מרוחקים נקראת איתות, והיא לא מכוסה במפרט של WebRTC.
כדי ליזום את הגדרת חיבור העמיתים מצד המתקשר, אנחנו יוצרים אובייקט RTCPeerConnection ואז קוראים ל-createOffer() כדי ליצור אובייקט RTCSessionDescription. תיאור הסשן הזה מוגדר כתיאור מקומי באמצעות setLocalDescription() ואז נשלח דרך ערוץ האיתות שלנו לצד המקבל. הגדרנו גם מאזין לערוץ האיתות שלנו, כדי לקבל תשובה לתיאור הסשן שהצענו מהצד המקבל.
async function makeCall() {
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.answer) {
const remoteDesc = new RTCSessionDescription(message.answer);
await peerConnection.setRemoteDescription(remoteDesc);
}
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingChannel.send({'offer': offer});
}
בצד המקבל, אנחנו מחכים להצעה נכנסת לפני שאנחנו יוצרים את מופע RTCPeerConnection. אחרי שמקבלים את המבצע, מגדירים אותו באמצעות התג setRemoteDescription(). לאחר מכן, אנחנו קוראים ל-createAnswer() כדי ליצור תשובה להצעה שהתקבלה. התשובה הזו מוגדרת כתיאור המקומי באמצעות התג setLocalDescription() ואז נשלחת לצד המתקשר דרך שרת האיתות שלנו.
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.offer) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signalingChannel.send({'answer': answer});
}
});
אחרי ששני העמיתים מגדירים את תיאורי הסשן המקומי והמרוחק, הם יודעים מה היכולות של העמית המרוחק. זה לא אומר שהחיבור בין העמיתים מוכן. כדי שזה יפעל, אנחנו צריכים לאסוף את מועמדי ה-ICE בכל עמית ולהעביר אותם (דרך ערוץ האיתות) לעמית השני.
מועמדים ל-ICE
כדי ששני עמיתים יוכלו לתקשר באמצעות WebRTC, הם צריכים להחליף ביניהם מידע על הקישוריות. מכיוון שתנאי הרשת יכולים להשתנות בהתאם למספר גורמים, בדרך כלל משתמשים בשירות חיצוני כדי לגלות את המועמדים האפשריים לחיבור לעמית. השירות הזה נקרא ICE והוא משתמש בשרת STUN או בשרת TURN. STUN הוא ראשי תיבות של Session Traversal Utilities for NAT, ובדרך כלל נעשה בו שימוש עקיף ברוב האפליקציות של WebRTC.
TURN (Traversal Using Relay NAT) הוא פתרון מתקדם יותר שמשלב את פרוטוקולי STUN, ורוב השירותים המסחריים שמבוססים על WebRTC משתמשים בשרת TURN כדי ליצור חיבורים בין עמיתים. WebRTC API תומך ישירות ב-STUN וב-TURN, והוא נכלל במונח המקיף יותר Internet Connectivity Establishment (הקמת קישוריות לאינטרנט). כשיוצרים חיבור WebRTC, בדרך כלל מספקים שרת ICE אחד או יותר בהגדרה של אובייקט RTCPeerConnection.
Trickle ICE
אחרי שיוצרים אובייקט RTCPeerConnection, המסגרת הבסיסית משתמשת בשרתי ה-ICE שסופקו כדי לאסוף מועמדים ליצירת קישוריות (מועמדים ל-ICE). האירוע icegatheringstatechange ב-RTCPeerConnection מציין את מצב איסוף נתוני ICE (new, gathering או complete).
אפשר לחכות עד לסיום איסוף ה-ICE, אבל בדרך כלל יעיל יותר להשתמש בטכניקה של 'העברת ICE בהדרגה' ולהעביר כל מועמד ICE לצד המרוחק ברגע שהוא מתגלה. כך זמן ההגדרה של חיבור העמיתים יתקצר באופן משמעותי, ושיחות הווידאו יתחילו עם פחות עיכובים.
כדי לאסוף מועמדים ל-ICE, פשוט מוסיפים listener לאירוע icecandidate.
ה-RTCPeerConnectionIceEvent שמופק ב-listener הזה יכיל את המאפיין candidate שמייצג מועמד חדש שצריך לשלוח לעמית המרוחק (ראו Signaling).
// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate) {
signalingChannel.send({'new-ice-candidate': event.candidate});
}
});
// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
if (message.iceCandidate) {
try {
await peerConnection.addIceCandidate(message.iceCandidate);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
}
});
החיבור נוצר
אחרי שמתקבלים מועמדים ל-ICE, מצב החיבור בין העמיתים אמור להשתנות בסופו של דבר למצב מחובר. כדי לזהות את זה, אנחנו מוסיפים מאזין ל-RTCPeerConnection שבו אנחנו מאזינים לאירועי connectionstatechange.
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});