Signaling and video calling
ì°¸ê³ : ì´ ê¸ì í¸ì§ ë° ê²í ê° íìíë¤. ëìì ì¤ ì ìë ë°©ë²ì ì´í´ë³´ì.WebRTCë ìì§ê¹ì§ ì¤íì ì¸ ê¸°ì ì´ë¤. ì¼ë¶ì 기ì ì¤íì´ ìì íê° ëì§ ìì기 ë문ì ê° ë¸ë¼ì°ì ¸ìì ì¬ì©ê°ë¥í í¸íì± ì 보를 íì¸í´ì¼íë¤. ëí, 기ì ì 문ë²ê³¼ í¨í´ë¤ì ì¤íì´ ë°ëë ê²ì²ë¼ ë¸ë¼ì°ì ¸ì ë²ì ì´ ëìì§ë¤ë©´ ë³ê²½ë ì ìë¤.
Summary
WebRTC ë ë¦¬ì¼ íì ìì±, ìì, ë°ì´í° êµíì í ì ìë ìì í p2p 기ì ì´ë¤. ë¤ë¥¸ ê³³ìì ë ¼ìí ê² ì²ë¼ ìë¡ ë¤ë¥¸ ë¤í¸ìí¬ì ìë 2ê°ì ëë°ì´ì¤ë¤ì ìë¡ ìì¹ìí¤ê¸° ìí´ìë, ê° ëë°ì´ì¤ë¤ì ìì¹ë¥¼ ë°ê²¬íë ë°©ë²ê³¼ 미ëì´ í¬ë§· íìê° íìíë¤. ì´ íë¡ì¸ì¤ë¥¼ ìê·¸ëë§ signaling ì´ë¼ ë¶ë¥´ê³ ê° ëë°ì´ì¤ë¤ì ìí¸ê°ì ëìë ìë²(socket.io í¹ì websocketì ì´ì©í ìë²)ì ì°ê²°ìí¨ë¤. ì´ ìë²ë ê° ëë°ì´ì¤ë¤ì´ negotiation(íì) ë©ì¸ì§ë¤ì êµíí ì ìëë¡ íë¤.
ì´ ê¸ìì ì°ë¦¬ë ë ëìê° ì ì ë¤ê°ì ìë°©í¥ì¼ë¡ íì íµíê° ëë ìì ì¸ WebSocket chat(ì¹ìì¼ ë¬¸ì를 ìì±í기 ìí´ ë§ë¤ì´ì¡ì¼ë©°, ë§í¬ë ê³§ íì±í ë ê²ì´ë¤. ìì§ì ì¨ë¼ì¸ì¼ë¡ í ì¤í¸ê° ë¶ê°ë¥íë¤.)ì ìëì´ ëëë¡ ë§ë¤ ìì ì´ë¤. ì´ê²ì ê´í´ ìí ì íì¸í´ ë³´ê±°ë Githubìì ì ì²´ íë¡ì í¸ë¥¼ íì¸í´ë³¼ ì ìë¤.
ì°¸ê³ : ê¹íì ìë í ì¤í¸ ìë² ì½ëë ìì¼ë¡ ê³µë¶í ìì ì½ëë³´ë¤ ìµì ë²ì ì´ë¤. ì´ ê¸ì íì¬ ì ë°ì´í¸ ì§í ì¤ì´ë©°, ê³§ ìë£ë ìì ì´ë¤. ì ë°ì´í¸ê° ìë£ëë¤ë©´ ì´ ê¸ì ì¬ë¼ì§ ê²ì´ë¤.
ì°¸ê³ : ìì¼ë¡ ëì¬ ìì ë¤ì promise 를 ì¬ì©íë¤. ë§ì½ ëê° ì´ê²ì ì 모른ë¤ë©´ ì´ ê¸ì ì½ì´ 보길 ë°ëë¤.
The signaling server
ë ëë°ì´ì¤ë¤ ì¬ì´ì WebRTC 커ë¥ì ì ë§ë¤ê¸° ìí´, ì¸í°ë· ë¤í¸ìí¬ìì ê·¸ ëì ì°ê²° ìí¤ë ìì ì í´ì¤ signaling server ê° íìíë¤. ì´ë»ê² ì´ ìë²ë¥¼ ë§ë¤ê³ ì¤ì ë¡ ìê·¸ëë§ ê³¼ì ì´ ì´ë»ê² ëëì§ ì´í´ë³´ì.
ê°ì¥ 먼ì , ìê·¸ëë§ ìë² ìì²´ê° íìíë¤. WebRTCë ìê·¸ëë§ ì ë³´ì ê´í transport ë©ì»¤ëì¦ì ì ìíì§ ìëë¤. ë í¼ì´ë¤ ì¬ì´ìì í´ë¦¬í¬í°ì ë¶ìì´ì²ë¼ ìê·¸ëë§ì ê´ë ¨ë ì ë³´ë¤ì ì ë¬í´ì¤ ì ìë ê²ì´ë©´ WebSocket ì´ë XMLHttpRequest ë ìê´ìë¤.
ì¬ê¸°ì ì¤ìí ì ì ìê·¸ëë§ ìë²ë ìê·¸ëë§ ë°ì´í° ë´ì©ì 몰ë¼ë ëë¤ë ê²ì´ë¤. ë¹ë¡ ì´ê²ì SDP ì´ì§ë§, 몰ë¼ë í° ë¬¸ì ê° ëì§ ìëë¤. ë©ì¸ì§ì ë´ì©ë¤ì ê·¸ì ìê·¸ëë§ ìë²ë¥¼ íµí´ ìëí¸ì¼ë¡ ê°ê¸°ë§ íë©´ëë¤. ì¤ìí ì ì ICE subsystemì´ ì í¸ ë°ì´í°ë¥¼ ë¤ë¥¸ í¼ì´ìê² ë³´ë´ëë¡ ì§ìíë©´, ë¤ë¥¸ í¼ì´ë ì´ ì 보를 ìì íì¬ ìì²´ ICE subsystemì ì ë¬íë ë°©ë²ì ìê³ ìë¤ë ê²ì´ë¤.
Readying the chat server for signaling
ì´ chat server ë í´ë¼ì´ì¸í¸ì ìë² ì¬ì´ì WebSocket APIì íµí´ JSON stringì¼ë¡ ë°ì´í°ë¥¼ ì ì¡íë¤. ìë²ë ìë¡ì´ ì ì 를 ë±ë¡íë ê², usernameì ì¸í íë ê², ì±í ë©ì¸ì§ë¥¼ ì ì¡íë ê² ë±ë±ì ìì ë¤ì í기 ìí´ ë¤ìí ë©ì¸ì§ íì ë¤ì ë¤ë£¬ë¤. ìê·¸ëë§ê³¼ ICE negotiation ì ìë²ê° ì²ë¦¬í기 ìí´ì ì½ë를 ìì±í´ì¼íë¤. 모ë ë¡ê·¸ì¸ë ì ì ë¤ìê² ë¸ë¡ëìºì¤í íë ê²ì´ ìëë¼, í¹ì í ì ì ìê² ì§ì ë©ì¸ì§ë¥¼ ì ë¬í´ì¼íë¤. ê·¸ë¦¬ê³ ìë²ê° ë°ë¡ ì²ë¦¬í íì ìì´, ìì ë ìíì§ ìì ë©ì¸ì§ íì ë¤ì ì²ë¦¬íë¤. ì´ë¥¼ íµí´ ì¬ë¬ ìë²ë¥¼ ë§ë¤ íììì´ ëì¼í ìë²ë¥¼ ì´ì©íì¬ ìê·¸ë ë©ìì§ë¥¼ ë³´ë¼ ì ìë¤. ì´ ê°ë ì WebRTCê° ìëë¼ WebSocketì ê´í ê°ë ì´ë¤.
ì´ì , WebRTC ìê·¸ëë§ì ì§ìíë chat server를 ë§ë¤ê¸° ìí´ ì´ë»ê² í´ì¼íëì§ ë³´ì. ìì¼ë¡ ëì¤ë ì½ëë¤ì chatserver.js ìì ìë ì½ëì´ë¤.
ì°ì sendToOneUser()í¨ì를 ì¶ê°íì. ì´ë¦ì´ ë§íë¯, stringified JSON ë©ì¸ì§ë¥¼ í¹ì í ì ì ìê² ë³´ë´ë ê²ì´ë¤.
function sendToOneUser(target, msgString) {
var isUnique = true;
var i;
for (i = 0; i < connectionArray.length; i++) {
if (connectionArray[i].username === target) {
connectionArray[i].sendUTF(msgString);
break;
}
}
}
ì´ í¨ìë ì°ê²°ë ì ì 리ì¤í¸ë¥¼ íì¸íë©´ì í¹ì í usernameì ê°ì§ë ì ì ì ì°¾ê³ , ì´ ì ì ìê² ë©ì¸ì§ë¥¼ ë³´ë¸ë¤. í¨ìì ì¸ìë¡ ë¤ì´ê°ë ë©ìì§ msgStringì stringified JSON object ì´ë¤. Stringified ê° ìë ì본ì ë©ì¸ì§ object를 ë°ëë¡ í´ë ëì§ë§, JSONì´ ë ì ì©íê³ í¸íë¤. ì´ ë©ì¸ì§ë ì´ë¯¸ stringified ë ìíë¡ í¨ìì ì ë¬ë기 ë문ì, ë ì´ìì ë©ì¸ì§ì ê´í ì²ë¦¬ ìì´ ë©ì¸ì§ë¥¼ ê·¸ëë¡ ë³´ë´ê¸°ë§ íë©´ ëë¤.
ì본 chat demoë í¹ì ì ì ìê² ë©ì¸ì§ë¥¼ ë³´ë´ë ê²ì ì§ìíì§ ìëë¤. Main WebSocket message handler를 ìì í´ì¼ ì´ê²ì´ ê°ë¥íê² ëë©°, 구체ì ì¼ë¡ëconnection.on()í¨ìì ë§ì§ë§ ë¶ë¶ì ìì íë©´ ëë¤.
if (sendToClients) {
var msgString = JSON.stringify(msg);
var i;
// If the message specifies a target username, only send the
// message to them. Otherwise, send it to every user.
if (msg.target && msg.target !== undefined && msg.target.length !== 0) {
sendToOneUser(msg.target, msgString);
} else {
for (i = 0; i < connectionArray.length; i++) {
connectionArray[i].sendUTF(msgString);
}
}
}
ì´ ì½ëë ë©ì¸ì§ìì target í¹ì±ì´ ì ìëìëì§ ì²´í¬íë¤. ì´ í¹ì±ì ë©ì¸ì§ë¥¼ ì ë¬íê³ ì¶ì ì¬ëì usernameì¼ë¡ ì ìí ì ìë¤. ë§ì½ targetíë¼ë¯¸í°ê° ì¡´ì¬íë¤ë©´, sendToOneUser()í¨ì를 ì½íë©´ì ê·¸ ì ì ìê² ë©ì¸ì§ë¥¼ ì ì¡íë¤. ê·¸ë ì§ ìë¤ë©´, 모ë ì ì ìê² ë©ì¸ì§ë¥¼ ë¸ë¡ëì¼ì¤í¸ë¥¼ íë¤.
ìì ìë ì½ëë ë³ëì ìì ì´ íì ìì´ ììì ë©ì¸ì§ íì ë¤ì ë³´ë¼ ì ìë¤. í´ë¼ì´ì¸í¸ë¤ì ì´ì í¹ì í ì ì ìê² unknown íì ì ë©ì¸ì§ë ë³´ë¼ì ìê³ , ìê·¸ëë§ ë©ì¸ì§ë¥¼ ìíë ëë¡ ë³´ë¼ ì ìë¤. 구체ì ì¸ ë´ì©ì ë¤ìì ì´í´ë³´ì.
Designing the signaling protocol
ì´ì ì°ë¦¬ë ë©ì¸ì§ë¥¼ êµííë ë©ì»¤ëì¦ì ë§ë¤ìë¤. ì´ì ë©ì¸ì§ë¤ì ì´ë»ê² 구ì±í ì§ì ëí íë¡í ì½ì´ íìíë¤. ì´ê²ì ì¬ë¬ ê°ì§ ë°©ë²ì¼ë¡ ê°ë¥íë°, ì¬ê¸°ì ë¤ë£¨ë ê²ì ê·¸ ì¤ íëì ìê·¸ëë§ ë©ì¸ì§ 구조ì´ë¤.
ì°ë¦¬ê° ì ê³µíë ìê·¸ëë§ ìë²ë stringified JSON object ì ê°ì§ê³ í´ë¼ì¸í¸ê°ì ë°ì´í°ë¥¼ ì£¼ê³ ë°ëë¤. ì¦, ì´ê²ì ìê·¸ëë§ ë©ì¸ì§ë¤ì´ JSON formatì¼ë¡ ëì´ìì¼ë©°, ë©ì¸ì§ì type ë± ë©ì¸ì§ë¥¼ ì ì íê² ì²ë¦¬í ì ìëë¡ ì¬ë¬ ì ë³´ë¤ì´ í¬í¨ëì´ ìë¤.
Exchanging session descriptions
ìê·¸ëë§ íë¡ì¸ì¤ë¥¼ ììí ë, callì ìì íë ì ì ê° *offer *ë ê²ì ë§ë ë¤. ì´ offerë ì¸ì
ì 보를 SDP í¬ë§·ì¼ë¡ ê°ì§ê³ ìì¼ë©°, 커ë¥ì
ì´ ì´ì´ì§ê¸°ë¥¼ ìíë ì ì (callee)ìê² ì ë¬ëì´ì¼ íë¤. Callee ë ì´ offerì SDP descriptionì í¬í¨íë *answer *ë©ì¸ì§ë¥¼ ë³´ë´ì¼íë¤. ì°ë¦¬ê° ì¬ì©í offer ë©ì¸ì§ë¤ì "video-offer" ì´ë¼ë íì
ì ì¬ì©í ê²ì´ê³ answer ë©ì¸ì§ë¤ì "video-answer" íì
ì ë©ì¸ì§ë¥¼ ì¬ì©í ê²ì´ë¤. ì´ ë©ì¸ì§ë¤ì ìëì ê°ì field를 ê°ì§ë¤.
type-
ë©ì¸ì§ì íì ì´ë¼;
"video-offer"ëë"video-answer". name-
ë³´ë´ë ì¬ëì username ì´ë¤.
target-
ë°ë ì¬ëì usernameì´ë¤. (ë§ì½ callerê° ë©ì¸ì§ë¥¼ ë³´ë¸ë¤ë©´, targetì callee 를 ë»íë¤, vice-versa.)
sdp-
커ë¥ì ì local ì 보를 ì¤ëª íë SDP (Session Description Protocol) ì¤í¸ë§(e.g. ìì ìì ê´ì ì¼ë¡ ë³¼ ë, SDPë 커ë¥ì ì remote ì ë³´ì´ë¤.)
í ìì ìì ë í¼ì´ë¤ì ì´ callì ëí´ ì´ë¤ ì½ë±ë¤ê³¼ ì´ë¤ video parameterë¤ì´ ì¬ì©ë ì§ ìê² ëë¤. íì§ë§, ê·¸ë¤ì ì¬ì í 미ëì´ ë°ì´í° ì체를 ì ì¡íë ë°©ë²ì 모른ë¤. ì¬ê¸°ì Interactive Connectivity Establishment (ICE)ê° ì¬ì©ëë¤.
Exchanging ICE candidates
SDP를 ìë¡ êµíí íì, ë í¼ì´ë¤ì ICE candidate(ICE íë³´)ë¤ì êµíí기 ììíë¤. ê° ICE candidateë ë°ì í¼ì´ ì ì¥ìì íµì ì í ì ìë ë°©ë²ì ì¤ëª íë¤. ê° í¼ì´ë ê²ìëë ììëë¡ candidate를 ë³´ë´ê³ 미ëì´ê° ì´ë¯¸ ì¤í¸ë¦¬ë°ì ìì íëë¼ë 모ë ê°ë¥í candidateê° ì ì¡ ìë£ë ëê¹ì§ ê³ì ë³´ë¸ë¤. ë í¼ì´ê° ìë¡ í¸íëë candidate를 ì ìíë¤ë©´, 미ëì´ë íµì ì ììíë¤. ë§ì½ ëì¤ì ë ëì ë°©ë²ì´ ìë¤ë©´(ë ëì ì±ë¥ì ê°ì§ë), ê·¸ ì¤í¸ë¦¼ì íìì ë°ë¼ í¬ë§·ì ë°ê¿ ìë ìë¤.
ë¹ë¡ ì§ê¸ì ì§ìíì§ ìì§ë§, ì´ ê¸°ì ì ì´ë¡ ì ë®ì bandwidthì ì°ê²°ì ëí´ ë¤ì´ê·¸ë ì´ëì ì¬ì©ë ì ìë¤.
ìê·¸ëë§ ìë²ë¥¼ íµí´ ì ë¬ëë ICE candidateë¤ì ê´í ë©ì¸ì§ì íì
ì "new-ice-candidate" ì´ë©°, ì´ ë©ì¸ì§ë¤ì ìë field를 ê°ì§ë¤.
type-
ë©ì¸ì§ íì :
"new-ice-candidate". target-
íì¬ íìì ì§í ì¤ì¸ ì¬ëì username. ìê·¸ëë§ ìë²ë ì´ ì ì ìê²ë§ ì§ì ë©ì¸ì§ë¥¼ ë³´ë¸ë¤.
candidate-
ì ìë 커ë¥ì ë°©ë²ì ì¤ëª íë SDP candidate string.
ê° ICE ë©ì¸ì§ë¤ì ë ê°ì ì»´í¨í°ë¥¼ ìë¡ ì°ê²°í기 ìí ì ë³´ë¤ì ë§ë¶ì¬ íë¡í ì½(TCP or UDP), IP 주ì, í¬í¸ ëë², 커ë¥ì íì ë±ì ì ìíë¤. ì¬ê¸°ìë NAT í¹ì ë¤ë¥¸ ë³µì¡í ë¤í¸ìí¹ì í¬í¨íë¤.
ì°¸ê³ :
ì¤ì. ICE negotiation ëì ëì ì½ëê° í´ì¼í ê²ì ì¤ì§ ICE layerìì ì¸ë¶ë¡ ëê° candidateë¤ì ì ííë ê²ê³¼, icecandidate_eventhandlerê° ë¶ë ¸ì ë ìê·¸ëë§ ìë²ë¥¼ íµí´ ê·¸ê²ë¤ì ë¤ë¥¸ í¼ì´ì ë³´ë´ë ê²ì´ë¤. ê·¸ë¦¬ê³ ìê·¸ëë§ ìë²ë¡ë¶í° ICE candidate ë©ì¸ì§ë¥¼ ë°ê³ RTCPeerConnection.addIceCandidate()를 í¸ì¶íì¬ ëì ICE layerì ê·¸ë¤ì ì ë¬íë¤. ê·¸ê² ë¿ì´ë¤. ì íí 무ìì íëì§ ì기 ì ê¹ì§, ë ì´ì ê¹ì´ ìê°íì§ ë§ì!
ëì ìê·¸ëë§ ìë²ê° ì´ì í´ì¼í ì¼ì ìì²ë ë©ì¸ì§ë¥¼ ë³´ë´ë ê²ì´ë¤. ë¶ê°ì ì¼ë¡ login/authentication ê°ì 기ë¥ë¤ì´ íìí ìë ìëë°, ìì¸í ë´ì©ì ë¬ë¼ì§ ì ìë¤.
Signaling transaction flow
ìê·¸ëë§ ì ë³´ë ì°ê²°í ë í¼ì´ë¤ ì¬ì´ìì êµíëë¤. ì주 기ì´ì ì¸ ìì¤ìì ì´ë¤ ë©ì¸ì§ë¤ì´ ëê° ë구ìê² ì ì ë¬í´ì¼íëì§ ë³´ì.
ìê·¸ëë§ íë¡ì¸ì¤ë ë¤ìí ë¶ë¶ìì ë¤ìê³¼ ê°ì ë©ìì§ êµíì í¬í¨íë¤. ê° ì ì ì ì±í ìì¤í ì ì¹ ì í리ì¼ì´ì ì¸ì¤í´ì¤, ê° ì ì ì ë¸ë¼ì°ì , ìê·¸ëë§ ìë² ê·¸ë¦¬ê³ í¸ì¤í ì¹ ìë² ë±.
Naomiì Priyaë ì±í ìíí¸ì¨ì´ë¥¼ ì¬ì©í´ ëíì ì°¸ì¬íê³ Naomië ë ì¬ì´ì ìì íµí를 íê¸°ë¡ ê²°ì íë¤. ë¤ì íë ì´ë²¤í¸ë¤ì´ ë°ìíë ê³¼ì ì´ë¤.
ê³§ ë ìì¸í ì¤ëª ì ë³¼ ì ìë¤.
ICE candidate exchange process
ê° í¼ì´ë¤ì ICE layerìì candidateë¤ì ë³´ë´ê¸° ììí ë, ë¤ì 그림과 ê°ì êµíì´ ì¼ì´ëë¤.
ê° í¼ì´ë¤ì candidate ë¤ì ì ì¡íê³ , ì¤ë¹ê° ëë©´ ë°ì candidate ë¤ì ì²ë¦¬íë¤. Candidateë¤ì ì í¼ì´ë¤ì´ ëìí ëê¹ì§ ê³ì êµíëë©°, 미ëì´ê° ì¡ìì ëëë¡ ë§ë ë¤. "ICE exchange"ì ìì¸¡ì´ êµëë¡ ì ììíë ê²ì ì미íì§ ìëë¤. ì¬ë°ë¥´ê² ìëí ê²½ì°, ê° í¼ì´ë¤ì 모ë ìì§ëê±°ë ìë¡ ëìí ëê¹ì§ ìëë°©ìê² ì ìí candidate ë¤ì ê³ì ì ì¡íë¤.
ë§ì½ ì¡°ê±´ë¤ì´ ë°ëë¤ë©´, ì를ë¤ì´ ë¤í¸ìí¬ ì»¤ë¥ì ì´ ì íëë©´, íë í¹ì ì í¼ì´ë¤ì ë®ì bandwidthì 미ëì´ í´ìëë¡ ë°ê¾¸ê±°ë ë¤ë¥¸ ì½ë±ì ì¬ì©íìê³ ì ìí ê²ì´ë¤. ë¤ì candidate êµíìì ì í¼ì´ 모ë ìë¡ì´ í¬ë§·ì ëìíë¤ë©´, ë¤ë¥¸ 미ëì´ í¬ë§· í¹ì ë¤ë¥¸ ì½ë±ì¼ë¡ ë°ë ìë ìë¤.
ë¶ê°ì ì¼ë¡ ë§ì½ ICE layer ë´ë¶ì íë¡ì¸ì¤ë¥¼ ë ìì¸í ì´í´íê³ ì¶ë¤ë©´ RFC 5245: Interactive Connectivity Establishment,section 2.6 ("Concluding ICE") 를 참조í´ë¼. ICE layerê° ì¤ë¹ ëìë§ì candiateë¤ì´ êµíëê³ ë¯¸ëì´ë¤ì íµì ë기 ììíë¤ë ê²ì 기ìµí´ë¼. ì´ ëª¨ë ê²ì ë¤ìì ììì ëìê°ë¤. ì°ë¦¬ì ìí ì ê·¸ì ìê·¸ëë§ ìë²ë¥¼ íµí´ candidateë¤ì ìë¡ìê² ë³´ë´ë ê²ì´ë¤.
The client application
ì§ê¸ë¶í° ììì ì¤ëª í ê°ë ë¤ì ìí ì½ë를 íµí´ì ìì¸í ë°°ìë³´ì.
ì´ë¤ ìê·¸ëë§ íë¡ì¸ì¤ë ì§ íµì¬ì ë©ì¸ì§ í¸ë¤ë§ì ìë¤. Websocketì ìê·¸ëë§ì ê¼ ì¬ì©í íìë ìì§ë§, ì¼ë°ì ì¸ ì루ì ì¼ë¡ ì°ì¸ë¤. ë¤ë¥¸ ì루ì ë ì¶©ë¶í ë¹ ë¥´ê³ ê°ì 결과를 ë³¼ ì ìë¤.
Updating the HTML
í´ë¼ì´ì¸í¸ë ë¹ëì¤ë¥¼ íìí ê³µê°ì´ íìíë¤. 2ê°ì videoì ì í를 걸 button ì ì ìí HTML ì½ëì´ë¤.
<div class="flexChild" id="camera-container">
<div class="camera-box">
<video id="received_video" autoplay></video>
<video id="local_video" autoplay muted></video>
<button id="hangup-button" onclick="hangUpCall();" disabled>Hang Up</button>
</div>
</div>
ìì ìë page structureì <div>í그를 ì´ì©íê³ CSS ì¬ì©ì íì©í¨ì¼ë¡ì¨ íì´ì§ ë ì´ìì ì 체를 구ì±íë¤. ì¬ê¸°ìë ë ì´ììì ê´í ìì¸í ë´ì©ì ì¤íµíì§ë§, ìì ì½ëê° ì´ë»ê² ëìê°ëì§ íì¸í´ë³´ì. take a look at the CSS on Github. ëê°ì <video> ì¤ íëë ëì self videoì´ê³ ë¤ë¥¸ íëë ìëë°©ì video를 ìí ììì´ë¤.
idê° "received_video" ì¸ <video>elementë ì°ê²°ë ìëë°©ì¼ë¡ë¶í° ìì ëë ë¹ëì¤ë¥¼ ë³´ì¬ì£¼ë ê³³ì´ë¤. autoplayattributeë ë¹ëì¤ê° ëë¬í기 ììíë©´ ì¦ì ì¬ììí¤ë ìí ì íë¤. ì´ê²ì ë°ë¡ ì¬ìì ê´ë ¨ë ì½ë를 ì²ë¦¬í íì를 ìì ì¤ë¤. idê° "local_video" ì¸ <video>elementìë ëì ì¹´ë©ë¼ì ììì´ ëì¤ê²ëë¤. muted attributeë ëì ë¡ì»¬ ì¤ëì¤ë¥¼ ììê±°íë¤.
ë§ì§ë§ì¼ë¡, íµí를 ëì ì ìë idê° "hangup-button"ì¸ <button>ì ë¹íì±í ë ìí(ì무 ì íë ì°ê²°ëì§ ìì default ìí)ë¡ êµ¬ì±ëë¤. ê·¸ë¦¬ê³ ì´ ë²í¼ì í´ë¦ìì hangUpCall()í¨ìê° ì¤í ëë¤. ì´ í¨ìì ìí ì íì¬ ì°ê²°ë callì ëê³ ë¤ë¥¸ í¼ì´ìê² ì°ê²°ì ëì¼ë¼ë ë©ì¸ì§ë¥¼ ì ë¬íë¤.
The JavaScript code
ì´ë»ê² ëìê°ëì§ ì기 ì½ê² í기 ìí´ ê° ê¸°ë¥ë³ë¡ ì½ë를 ëëìë¤. ì´ ì½ëì ë©ì¸ ë¶ë¶ì connect()í¨ì ìì ìë¤. ì´ í¨ì ììì 6503 í¬í¸ë¡ WebSocketserverì ì°ê²°íë©°, JSON object formatì ë©ì¸ì§ë¥¼ ë°ê¸° ìí handler를 ì¤ì íë¤. ~~ì´ ì½ëë ì¼ë°ì ì¼ë¡ ì´ì ì²ë¼ 문ì ì±í
ë©ì¸ì§ë¥¼ ì²ë¦¬íë¤.~~
Sending messages to the signaling server
ì½ë ì ë°ì 걸ì³ì ìê·¸ëë§ ìë²ì ë©ì¸ì§ë¥¼ ë³´ë´ê¸° ìí´ sendToServer()í¨ì를 í¸ì¶íë¤. ì´ í¨ìë WebSocket 커ë¥ì
ì ì´ì©íì¬ ìëíë¤.
function sendToServer(msg) {
var msgJSON = JSON.stringify(msg);
connection.send(msgJSON);
}
ì ë¬ë ë©ì¸ì§ objectë JSON.stringify()í¨ìì ìí´ JSON stringì¼ë¡ ë°ëë¤. ê·¸ í, WebSocket 커ë¥ì
ì send()í¨ì를 íµí´ ìë²ë¡ ì ë¬ëë¤.
UI to start a call
"userlist"ì ê´í ì½ëë handleUserlistMsg()í¨ìì ìë¤. ì¼ìª½ ì±í
í¨ëì ë³´ì¬ì§ë ì ì 리ì¤í¸ì ìë ê° ì°ê²°ë ì ì ë§ë¤ handler 를 걸ì´ì¤ë¤. ì´ í¨ìë (ì¨ë¼ì¸ ìíì¸ ì ì ë¤ì usernameì ë°°ì´ë¡ ì ì¥íê³ ìë) usersproperty를 ê°ì§ê³ ìë ë©ì¸ì§ object를 ë°ëë¤. ì´í´í기 ì½ëë¡ ì¬ë¬ ì¹ì
ë¤ìì ì´ ì½ë를 ì´í´ ë³´ê² ë¤.
function handleUserlistMsg(msg) {
var i;
var listElem = document.getElementById("userlistbox");
while (listElem.firstChild) {
listElem.removeChild(listElem.firstChild);
}
// â¦
listElemë³ì를 íµí´ usernameë¤ì 리ì¤í¸ì¸ <ul>ì 참조íë¤. ê·¸ë° ë¤ìì ê° child element를 íëì© ì ê±°íë©´ì 목ë¡ì ë¹ì´ë¤ .
ì°¸ê³ : ëª ë°±í, ë°ë ëë§ë¤ ì ì²´ 리ì¤í¸ë¥¼ ìë¡ ë§ëë ê²ë³´ë¤, ê°ê°ì¸ì ì¶ê° ë° ì ê±° í ì ë°ì´í¸íë ê²ì´ ë í¨ì¨ì ì´ë¤. ê·¸ë¬ë, ìì ì´ë¯ë¡ ë¨ìíê² íê² ë¤.
ê·¸ í, ìë¡ì´ user 리ì¤í¸ë¥¼ ë§ë ë¤.
// â¦
for (i=0; i < msg.users.length; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode(msg.users[i]));
item.addEventListener("click", invite, false);
listElem.appendChild(item);
}
}
ë¤ìì¼ë¡ (ì±í
ìë²ì) íì¬ ì°ê²°ë ê° ì ì ë¤ ê°ê°ì ëíë´ë <li>elementë¤ì DOMì ì¶ê°íë¤. ê·¸ë° ë¤ìì, usernameì´ í´ë¦ ëìì ë invite()í¨ì를 ì¤íìí¤ë listenerì ì¶ê°íë¤. ì´ í¨ì ì´ê²ì ë¤ë¥¸ ì ì ìê² callì íë process를 ììíë¤.
Starting a call
íµí를 íê³ ì¶ì ì ì ì usernameì í´ë¦ì íë©´, click eventì handlerì¸invite()í¨ìê° ì¤íëë¤.
var mediaConstraints = {
audio: true, // We want an audio track
video: true, // ...and we want a video track
};
function invite(evt) {
if (myPeerConnection) {
alert("You can't start a call because you already have one open!");
} else {
var clickedUsername = evt.target.textContent;
if (clickedUsername === myUsername) {
alert(
"I'm afraid I can't let you talk to yourself. That would be weird.",
);
return;
}
targetUsername = clickedUsername;
createPeerConnection();
navigator.mediaDevices
.getUserMedia(mediaConstraints)
.then(function (localStream) {
document.getElementById("local_video").srcObject = localStream;
myPeerConnection.addStream(localStream);
})
.catch(handleGetUserMediaError);
}
}
ê°ì¥ 먼ì í´ì¼í ì¼ì ë¹ ë¥´ê² ì¬ë¬ ìíë¤ì ì ê²íë ê²ì´ë¤. ì ì ê° ì´ë¯¸ callì ì´ìëì§, í¹ì ì ì ê° ìì ìê² callì ì ì²íëì§ ë±, ì´ ì¼ì´ì¤ë¤ìë ìë¡ì´ callì ìëí ì´ì ê° ìë¤. ë°ë¼ì ì callì íì§ ëª»íëì§ alert()를 íµí´ ì¤ëª
íë¤.
ê·¸ ë¤ìì callì íë ¤ë ì ì ì ì´ë¦ì targetUsernameë³ì ìì ë£ê³ createPeerConnection()í¨ì를 ì¤íìí¨ë¤. ì´ í¨ìë RTCPeerConnection ì 기본ì ì¸ êµ¬ì±ê³¼ 기ë¥ì ìííë¤.
RTCPeerConnection ì´ ìì±ëë©´, Navigator.mediaDevices.getUserMediaí¨ì를 íµí´ ì ì ì ì¹´ë©ë¼ì ë§ì´í¬ì ê¶íì ìì²íë¤. ì¹´ë©ë¼ì ë§ì´í¬ìì ëì¤ë ë¡ì»¬ ì¤í¸ë¦¼ì ë¡ì»¬ ë¹ëì¤ previewì srcObjectpropertyì ì¤ì íë¤. ê·¸ë¦¬ê³ <video>elementê° ìëì¼ë¡ ë¤ì´ì¤ë ë¹ëì¤ë¥¼ ì¬ìíëë¡ êµ¬ì±ëì기 ë문ì, streamì ë¡ì»¬ preview boxìì ì¬ìì ììíë¤.
ê·¸ ë¤ìì RTCPeerConnectionì streamì ì¶ê°í기 ìí´ myPeerConnection.addStream()í¨ì를 ì¤ííë¤. WebRTC 커ë
ì
ì´ ìì í ì¤ë¹ëì§ ììëë¼ë WebRTC 커ë¥ì
ì streamì ë³´ë´ê¸° ììíë¤.
ë§ì½ local media streamì ê°ì ¸ì¤ë ëì ìë¬ê° ë°ìíë¤ë©´, catch clauseê° handleGetUserMediaError()í¨ì를 ë¶ë¬ íìì ë°ë¼ ì ì ìê² ì ì í ìë¬ ë©ì¸ì§ë¥¼ ë³´ì¬ì¤ ê²ì´ë¤.
Handling getUserMedia() errors
getUserMedia()ì ìí´ ë¦¬í´ë promiseê° ì¤í¨ë¡ ëëë©´, handleGetUserMediaError()í¨ìê° ì¤íëë¤.
function handleGetUserMediaError(e) {
switch (e.name) {
case "NotFoundError":
alert(
"Unable to open your call because no camera and/or microphone" +
"were found.",
);
break;
case "SecurityError":
case "PermissionDeniedError":
// Do nothing; this is the same as the user canceling the call.
break;
default:
alert("Error opening your camera and/or microphone: " + e.message);
break;
}
closeVideoCall();
}
ìë¬ ë©ì¸ì§ë 모ë ì¼ì´ì¤ ì¤ íëë§ íìëë¤. ì´ ìì ìì callì ì·¨ìíë ê±°ì ê°ì´, 미ëì´ íëì¨ì´ì ì ê·¼ ê¶íì ê±°ë¶íë ê²ì ëí´ ë°ìíë ìë¬ë¤( "SecurityError" ì"PermissionDeniedError")ì 무ìíë¤.
Streamì ê°ì ¸ì¤ë ê²ì ì¤í¨íë ì´ì ì ê´ê³ ìì´, RTCPeerConnectionì ë«ê¸° ìí´ closeVideoCall()function를 ë¶ë¥¸ë¤. ê·¸ë¦¬ê³ callì í기 ìí´ í ë¹ë 리ìì¤ë¤ì ë°ë©íë¤. ì´ ì½ëë ì¼ë¶ë¶ë§ ì¤íë callì ìì íê² ì²ë¦¬í ì ìëë¡ ì¤ê³ëìë¤.
Creating the peer connection
createPeerConnection()í¨ìë callerì calleeìì WebRTC 커ë¥ì
ì ê° ì¢
ì ì ëíë´ë RTCPeerConnectionobject를 ìì±íëë° ì¬ì©ëë¤. Callerë invite()í¨ì를 íµí´, calleeë handleVideoOfferMsg() ì ìí´ ì¤íëë¤.
ì´ê²ì ìë¹í ëª ë£íë¤:
var myHostname = window.location.hostname;
function createPeerConnection() {
myPeerConnection = new RTCPeerConnection({
iceServers: [ // Information about ICE servers - Use your own!
{
urls: "turn:" + myHostname, // A TURN server
username: "webrtc",
credential: "turnserver"
}
]
});
// â¦
ì¹ìë²ì ê°ì í¸ì¤í¸ì STUN/TURN ìë²ë¥¼ ëë¦¬ê³ ì기 ë문ì, STUN/TURN ìë²ì ëë©ì¸ ì´ë¦ì location.hostnameì ì¬ì©íì¬ ì¤ì íë¤. ë§ì½ ë¤ë¥¸ ìë²ì STUN/TURN ìë²ë¥¼ ì¬ì©íë¤ë©´ urls ê°ì ê·¸ ìë²ë¡ ë°ê¿ì£¼ë©´ ëë¤.
RTCPeerConnectionì ë§ë¤ ë, callì 구ì±íë íë¼ë¯¸í°ë¤ì ëª
ìí´ì¤ì¼íë¤. ê°ì¥ ì¤ìí ê²ì STUN/TURN ìë²ì 리ì¤í¸(ICE layerìì callerì calleeì ê²½ë¡ë¥¼ ì°¾ëë° ì¬ì©ëë ìë²)를 ë´ê³ ìë iceServersì´ë¤. (주ì. ì¹ìì¼ì ì´ì©í ìê·¸ëë§ ìë²ì ì í ë¤ë¥¸ ê°ë
ì´ë¤). WebRTCë ë í¼ì´ê° ë°©íë²½ì´ë NAT ë¤ì ì¨ì´ ìì´ë, ê° í¼ì´ë¤ì ìë¡ ì°ê²°ë ì ìëë¡ í¼ì´ê° ì°ê²° ê²½ë¡ë¥¼ ì°¾ì주ë íë¡í ì½(STUN, TURN)ì ì¬ì©íë¤.
ì°¸ê³ : ì§ì ë§ë í¹ì ì¬ì©í ê¶íì ê°ì§ê³ ìë STUN/TURN ìë²ë¥¼ ì¬ì©í´ì¼ íë¤.
iceServersparameterë objectì ë°°ì´ì´ê³ ê°ê°ì STUN/TURN ìë²ì URLì¸ urlsfield를 무조건 í¬í¨íë¤. ìì ìì, ICE layerìì ë¤ë¥¸ í¼ì´ë¥¼ ì°¾ì ì°ê²° ìí¤ê¸° ìí ìë²ë¥¼ ì ê³µíë¤. ì´ ìë²ë TURN ìë²ì´ë©°, Web ìë²ì ê°ì hostnameìì ëìê°ë¤. TURN ìë²ì descriptionì usernameê³¼credentialfieldì ê°ê° usernameê³¼ password ì 보를 íì í¬í¨ìì¼ì¼íë¤ë ê²ì ì ìí´ë¼.
Set up event handlers
RTCPeerConnectionì´ ìì±ëë©´, ì¤ìí ì´ë²¤í¸ë¤ì ìí handler를 ì¤ì í´ì¼íë¤.
// â¦
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.onaddstream = handleAddStreamEvent;
myPeerConnection.onremovestream = handleRemoveStreamEvent;
myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
}
ìì ìë ì´ë²¤í¸ í¸ë¤ë¬ ì¤ ì²ì ë ê°ë íìì´ë¤. WebRTCë¡ ì¤í¸ë¦¬ë°ë 미ëì´ì ê´ë ¨ë ê²ë¤ì ë¤ë£¨ê¸°ìí´ ë í¸ë¤ë¬ë¥¼ ì¤ì í´ì¼íë¤. removestreameventë ì¤í¸ë¦¬ë°ì´ ì¤ë¨ë ê²ì ê°ì§íëë° ì ì©íë¤. ë°ë¼ì ìë§ ì´ê²ë ì¬ì©íê² ë ê²ì´ë¤. ë¨ì ìë 4ê°ë íìì ì¸ ê²ì ìëë, ì§ì ì¬ì©í´ë³´ì. ì´ê²ë¤ ì¸ìë ë¤ë¥¸ ì´ë²¤í¸ë¤ì ì¬ì©í ì ìì¼ë ì¬ê¸°ììë ë¤ë£¨ì§ ìê² ë¤. ê° í¸ë¤ë¬ì ê´í ìì½ ì¤ëª
ì´ë¤.
RTCPeerConnection.onicecandidate-
ë¡ì»¬ ICE layerë ìê·¸ëë§ ìë²ë¥¼ íµí´ ë¤ë¥¸ í¼ì´ì ICE candidate를 ì ì¡íê³ ì í ë, ëì
icecandidateevent handler를 í¸ì¶íë¤. RTCPeerConnection.onaddstream-
addstreamevent를 ìí ì´ í¸ë¤ë¬ë ëì 커ë¥ì ì remote streamì´ ì¶ê°ë ê²ì ìë ¤ì£¼ê¸° ìí´, ë¡ì»¬ WebRTC layerì ìí´ ë¶ë ¤ì§ë¤. ì를ë¤ì´, ì´ê²ì ë¤ì´ì¤ë streamì elementì ì°ê²°ìì¼ ëì¤íë ì´ ëê² ë§ë¤ ë ì¬ì©ëë¤. ë ìì¸í ë´ì©ì Receiving new streams ì 참조í´ë¼. RTCPeerConnection.onremovestream-
커ë¥ì ìì remoteê° streamì ì ê±°í ë,
onaddstreamì ë°ëì¸onremovestreamìremovestreameventì ì²ë¦¬í기ìí´ ì¤íëë¤. RTCPeerConnection.oniceconnectionstatechange-
ICE 커ë¥ì ì ìí ë³ê²½ì ì리기ìí´ ICE layerê°
iceconnectionstatechangeevent 를 ë³´ë¸ë¤. ì´ê²ì íµí´ 커ë¥ì ì´ ì¤í¨íê±°ë ëì´ì§ë ê²ì ì ì ìë¤. ì´ ê²ì ëí ìì 를 ìëì ICE connection state ìì ë³¼ ê²ì´ë¤. RTCPeerConnection.onicegatheringstatechange-
íëì ìíìì ë¤ë¥¸ ìí(ì를ë¤ì´, candidate를 모ì¼ê¸° ììíê±°ë negotiationì´ ëë¬ì ë)ë¡ ICE agentì candidate ìì§ íë¡ì¸ì¤ê° ë³íë©´, ICE layerë
icegatheringstatechangeevent를 ë³´ë¸ë¤. ìëì ICE gathering state ì 참조í´ë¼. RTCPeerConnection.onsignalingstatechange-
ìê·¸ëë§ íë¡ì¸ì¤ì stateê° ë°ëê² ë ë, WebRTC ì¸íë¼ë ëìê²
signalingstatechangemessage를 ë³´ë¸ë¤. Signaling state ìì ì½ë를 ë³¼ ì ìë¤. RTCPeerConnection.onnegotiationneeded-
ì´ í¨ìë WebRTC ì¸íë¼ê° session negotiation íë¡ì¸ì¤ë¥¼ ìë¡ ììí´ì¼í ëë§ë¤ ë¶ë¦°ë¤. ì´ê²ì ì¼ì calleeìê² offer를 ìì± í ì ë¬íê³ , ì°ë¦¬ìê² ì°ê²°ì í ê²ì¸ì§ 물ì´ë³´ë ê²ì´ë¤. ì´ë»ê² ì²ë¦¬íëì§ Starting negotiation 를 참조í´ë¼.
Starting negotiation
Callerê° ìì ì RTCPeerConnectionê³¼ media streamì ìì±íê³ Starting a callìì ë³´ì´ë ê²ì²ë¼ 커ë¥ì
ì ì¶ê°íë©´, ë¸ë¼ì°ì ¸ë ë¤ë¥¸ í¼ì´ì 커ë¥ì
ì´ ì¤ë¹ê° ë ë negotiationneeded event를 íì±í ìí¬ ê²ì´ë¤. ë°ìë ì´ë²¤í¸ë¥¼ í¸ë¤ë§íë ì½ëì´ë¤.
function handleNegotiationNeededEvent() {
myPeerConnection
.createOffer()
.then(function (offer) {
return myPeerConnection.setLocalDescription(offer);
})
.then(function () {
sendToServer({
name: myUsername,
target: targetUsername,
type: "video-offer",
sdp: myPeerConnection.localDescription,
});
})
.catch(reportError);
}
Negotiation íë¡ì¸ì¤ë¥¼ ììí기 ìí´, ì°ë¦¬ê° ì°ê²°íê³ ì íë í¼ì´ìê² SDP offer를 ìì±íê³ ì ì¡í´ì¼íë¤. ì´ offerë 커ë¥ì
ì ë¡ì»¬ë¡ ì¶ê°í media stream ì ë³´(callì ë¤ë¥¸ í¼ì´ìê² ì ë¬íê³ ì¶ì ë¹ëì¤)ì ICE layerì ìí´ ë¯¸ë¦¬ 모ì ëì ICE candidates ì ë³´ë¤ì í¬í¨í´, 커ë¥ì
ì ì§ìëë êµ¬ì± ëª©ë¡ë¤ì í¬í¨íë¤. myPeerConnection.createOffer()를 í¸ì¶í¨ì¼ë¡ì¨ ì´ offer를 ìì±íë¤. ì´ ê²ì´ ì±ê³µíë¤ë©´(promiseìì fulfillëë©´), myPeerConnection.setLocalDescription()ì¼ë¡ ìì±ë offer ì 보를 ì ë¬íë¤.myPeerConnection.setLocalDescription()ì 커ë¥ì
ìì ìì ì 미ëì´ êµ¬ì± ìíë ì°ê²° ì ë³´ë¤ì 구ì±íë¤.
ì°¸ê³ :
기ì ì ì¼ë¡ ë§íìë©´, createOffer()ì ìí´ ë¦¬í´ëë blobì RFC 3264 offer ì´ë¤.
setLocalDescription()ì´ ìë£ëì´ promise를 리í´íë©´, description ì´ ì í¨íê³ ì¸í
ëììì ì ì ìë¤. ê·¸ ì´íì local descriptionì í¬í¨íë ìë¡ì´ "video-offer"message를 ë§ë¤ì´ ìê·¸ëë§ ìë²ë¥¼ íµí´ ë¤ë¥¸ í¼ì´ìê² ì ì¡íë¤. ì´ offerë ë¤ìê³¼ ê°ì ë´ì©ì ê°ì§ë¤.
type-
ë©ì¸ì§ì íì ì
"video-offer". name-
callerì username.
target-
callì íê³ ì íë userì name.
sdp-
offerì ê´í ì¤ëª ì íë SDP blob.
createOffer()ì´ë ë¤ë¥¸ fulfillment í¸ë¤ë¬ìì ìë¬ê° ë°ìíë¤ë©´, reportError()í¨ìê° ì¤íëì´ ìë¬ë¥¼ ë³´ê³ íë¤.
setLocalDescription()ì fulfillment í¸ë¤ë¬ê° ì¤íëë©´, ICE agentë icecandidateeventë¤ì ì²ë¦¬í기 ììíë¤.
Session negotiation
ì´ì ë¤ë¥¸ í¼ì´ì íìì í ê²ì´ë¤. ë¤ë¥¸ í¼ì´ë ì°ë¦¬ì offer를 ë°ì ê²ì´ê³ , handleVideoOfferMsg()ì ì ë¬íë¤. Calleeìê² "video-offer"messageê° ëì°© íì ëì ì´ì¼ê¸°ë¥¼ ê³ìí´ë³´ì.
Handling the invitation
offerê° ëì°©í ë, calleeì handleVideoOfferMsg()í¨ìê° ì¤íëê³ , offer를 í¬í¨í "video-offer"message를 ë°ì ê²ì´ë¤. ì´ ì½ëë 2ê°ì§ë¥¼ í´ì¼íë¤. 첫째, ì기 ìì ì RTCPeerConnectionê³¼ media streamì ìì±í´ì¼ íë¤. ëë²ì§¸, ë°ì offer를 ë¶ìíê³ ì ì´ì ëí answer를 ë§ë¤ì´ ë³´ë´ì¼íë¤.
function handleVideoOfferMsg(msg) {
var localStream = null;
targetUsername = msg.name;
createPeerConnection();
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection.setRemoteDescription(desc).then(function () {
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function(stream) {
localStream = stream;
document.getElementById("local_video").srcObject = localStream;
return myPeerConnection.addStream(localStream);
})
// â¦
ì´ ì½ëë Starting a callì ìë invite()í¨ìì ë§¤ì° ë¹ì·íë¤. 먼ì , createPeerConnection()í¨ì를 ì´ì©í´ì RTCPeerConnection를 ìì±íê³ êµ¬ì±íë¤. ê·¸ íì, "video-offer"messageë¡ë¶í° ì»ì SDP offer를 ê°ì§ê³ callerì session descriptionì ëíë´ë RTCSessionDescriptionobject를 ìì±íë¤.
ê·¸ íì, session descriptionì myPeerConnection.setRemoteDescription() ìì¼ë¡ ì ë¬ëë¤. ì´ë¥¼ íµí´, ë°ì offer를 callerì session ì ë³´ë¡ ì ì¥íë¤. ì¤ì ì ì±ê³µíë¤ë©´, promise fulfillment handler(then()clause)ì calleeì ì¹´ë©ë¼ì ë§ì´í¬ì ì ê·¼íê³ streamì ì¤ì íë ë± ì´ì ì invite()ìì 본 ê²ê³¼ ê°ì íë¡ì¸ì¤ë¥¼ ììíë¤.
local streamì´ ìëíë¤ë©´, ì´ì SDP answer를 ë§ë í callerìê² ë³´ë´ì¼ íë¤.
.then(function() {
return myPeerConnection.createAnswer();
})
.then(function(answer) {
return myPeerConnection.setLocalDescription(answer);
})
.then(function() {
var msg = {
name: myUsername,
target: targetUsername,
type: "video-answer",
sdp: myPeerConnection.localDescription
};
sendToServer(msg);
})
.catch(handleGetUserMediaError);
}
RTCPeerConnection.addStream() ì´ ì±ê³µì ì¼ë¡ ìë£ëìë¤ë©´, ê·¸ ë¤ì fulfillment handlerê° ì¤íë ê²ì´ë¤. SDP answer stringì ë§ë¤ê¸° ìí´ myPeerConnection.createAnswer()를 ì¤ííë¤. 커ë¥ì
ìì calleeì ë¡ì»¬ descriptionì ì¤ì í기 ìí´ myPeerConnection.setLocalDescriptionì ìì±í SDP를 ì ë¬íë¤.
ìµì¢
answerë callerìê² ë³´ë´ì ¸ì, ì´ë»ê² calleeìê² ë¿ì ì ìëì§ ìê²í´ì¤ë¤. "video-answer"messageì sdppropertyì calleeì answer를 í¬í¨íê³ , callerìê² ì´ ë©ì¸ì§ë¥¼ ì ë¬íë¤.
ìë¬ê° ë°ìíë©´ handleGetUserMediaError()ì¼ë¡ ì ë¬ëê³ , Handling getUserMedia() errorsì ì ì¤ëª
ëì´ ìë¤.
ì°¸ê³ :
callerì ë§ì°¬ê°ì§ë¡ setLocalDescription()fulfillment handlerê° ì¤íëë©´, ë¸ë¼ì°ì ¸ë calleeê° ë°ëì ì²ë¦¬í´ì¼íë icecandidateeventë¤ì ì²ë¦¬í기 ììíë¤.
Sending ICE candidates
callerê° calleeë¡ë¶í° answer를 ë°ì¼ë©´ 모ë ê²ì´ ëë¬ë¤ê³ ìê°í ì ìì§ë§, ê·¸ë ì§ ìë¤. ë·ë¨ ììë ê° í¼ì´ë¤ì ICE agentë¤ì´ ì´ì¬í ICE candidate messageë¤ì êµííë¤. 미ëì´ íµì ì´ ì´ë»ê² ì°ê²°ë ì ìëì§ì ëí ë°©ë²ë¤ì ì릴 ëê¹ì§, ê° í¼ì´ë¤ì ìëë°©ìê² ê³ìí´ì candidateë¤ì ë³´ë¸ë¤. ì´ candidateë¤ì ëì ìê·¸ëë§ ìë²ë¥¼ íµí´ì ì ì¡ëì´ì¼ íë¤. ICEë ëì ìê·¸ëë§ ìë²ì ëí´ ëª¨ë¥´ê¸° ë문ì, ëë icecandidateevent를 ìí í¸ë¤ë¬ë¥¼ ë¶ë¬ì ì ì¡ë candidate ë¤ì ëì ì½ëë¡ ì§ì ì²ë¦¬í´ì¼íë¤.
ëì onicecandidatehandlerë candidatepropertyê° candidateì ì 보를 ë´ê³ ìë SDP(ë¨, candidateë¤ì ëìënullì´ ì°íìë¤) ì¸ ì´ë²¤í¸ë¤ì ë°ëë¤. ì´ê²ì´ ëì ìê·¸ëë§ ìë²ë¥¼ íµí´ ë¤ë¥¸ í¼ì´ìê² ì ì¡í´ì¼í ê²ë¤ì´ë¤. ë°ì 구í ìì ê° ìë¤.
function handleICECandidateEvent(event) {
if (event.candidate) {
sendToServer({
type: "new-ice-candidate",
target: targetUsername,
candidate: event.candidate,
});
}
}
ì´ ì½ëìì candidate를 í¬í¨íë object를 ë§ë¤ê³ ë¤ë¥¸ í¼ì´ì ë³´ë¸ë¤. sendToServer()í¨ìë ììì ì´ë¯¸ ë¤ë¤ì¼ë©° Sending messages to the signaling serverì ì½ëê° ìë¤. messageì propertyë¤ì´ ì미íë ê²ì ë¤ìê³¼ ê°ë¤.
target-
ICE candidateê° ë³´ë´ì¼íë ê³³ì username. ì´ê²ì íµí´ ìê·¸ëë§ ìë²ê° ë©ì¸ì§ë¥¼ íê²ìê² ì ë¬íë¤.
type-
ë©ì¸ì§ íì ì
"new-ice-candidate". candidate-
ICE layerê° ë¤ë¥¸ í¼ì´ìê² ì ì¡íê³ ìíë candidate object.
ë©ì¸ì§ì í¬ë§·(ìê·¸ëë§ì ì²ë¦¬íë 모ë ë©ì¸ì§ë¤ì)ì 모ë ëì ììì´ê³ , ëê° íìí ê²ì ë¬ë ¸ë¤. ëê° ëë¤ë¥¸ íìí ì ë³´ê° ìë¤ë©´ ì¶ê°í ì ìë¤. ë©ì¸ì§ë ê·¸ì JSON stringfied ëì´ ìëë°©ìê² ì ë¬ë ë¿ì´ë¤.
ì°¸ê³ :
Callì ë¤ë¥¸ í¼ì´ë¡ë¶í° ICE candidateê° ëì°©í ë, icecandidateeventê° ì ì¡ëë ê²ì´ ìëì íì ëª
ì¬í´ë¼. ëì ì ë ìì ì´ callì í ë ë³´ë´ë ê²ì¼ë¡, ëê° ìíë ì±ëì íµí´ data를 ë³´ë¼ ì ìë¤. WebRTC를 ì²ì ì íë¤ë©´ ë§¤ì° í·ê°ë¦´ ê²ì´ë¤.
Receiving ICE candidates
ìê·¸ëë§ ìë²ë ì´ë¤ ë°©ë²ì ê³ ë¥´ë ê°ì ê° ICE candidate를 목ì ì§ê¹ì§ ë°°ë¬íë¤. ì´ë² ìì ììë typeì´ "new-ice-candidate"ì¸ JSON object를 ì¬ì©íë¤. handleNewICECandidateMsg()í¨ìë ì´ ë©ì¸ì§ë¤ì ì²ë¦¬í기 ìí´ ì¤íëë¤.
function handleNewICECandidateMsg(msg) {
var candidate = new RTCIceCandidate(msg.candidate);
myPeerConnection.addIceCandidate(candidate).catch(reportError);
}
ìì ë SDP를 RTCIceCandidate ìì±ìì ì¸ìë¡ì ì ë¬íì¬ object를 ìì±íê³ , ì´ object를 myPeerConnection.addIceCandidate() ì ì ë¬íë¤. ì´ í¨ì를 íµí´ ìë¡ì´ ICE candidate를 local ICE layerì ì ë¬íê³ , ëëì´ candidate 를 í¸ë¤ë§íë íë¡ì¸ì¤ìì ì°ë¦¬ì ìí ì ëë¬ë¤.
ê° í¼ì´ë ìëí ê²ì¼ë¡ ë³´ì´ë ê° ì»¤ë¥ì ë©ìëì candidate를 ë¤ë¥¸ í¼ì´ìê² ë³´ë¸ë¤. ì측ì í©ìì ëë¬íê³ ì»¤ë¥ì ì openíë¤. íì½ì ì§í ì¤ìë ë ëì 커ë¥ì ë©ìë를 찾거ë, ë¨ìí í¼ì´ê° 커ë¥ì ì ì¤ì í ë candidate êµíì´ ì§í ì¤ì´ìì ì ì기 ë문ì, candidateë ì¬ì í ì¡,ìì ë ì ììì 기ìµí´ë¼.
Receiving new streams
ë¦¬ëª¨í¸ í¼ì´ê° RTCPeerConnection.addStream()를 ë¶ë¦ì¼ë¡ì¨, ëë stream formatì ëí renegotiation(ì¬íì)ì ìí´ ìë¡ì´ ì¤í¸ë¦¼ì´ 커ë¥ì
ì ì¶ê°ëìì ë, addstreameventê° ë°ìíë¤. ì´ë»ê² ì²ë¦¬íëì§ ìë ì½ë를 ë³´ì.
function handleAddStreamEvent(event) {
document.getElementById("received_video").srcObject = event.stream;
document.getElementById("hangup-button").disabled = false;
}
ì´ í¨ìë ë¤ì´ì¤ë streamì idê° "received_video"ì¸ <video>elementì í ë¹íê³ , ì ì ê° ì í를 ë°ì ì ìëë¡ ë²í¼ì íì±ííë¤.
ì´ ì½ëê° ì ëë¡ ì¤íëë¤ë©´, ëëì´ ë¤ë¥¸ í¼ì´ìì ì¤ë ë¹ëì¤ë¥¼ ë¡ì»¬ ë¸ë¼ì°ì ìì ë³¼ ì ìê² ëë¤!
Handling the removal of streams
ë¦¬ëª¨í¸ í¼ì´ê° RTCPeerConnection.removeStream()를 í¸ì¶íì¬ ì»¤ë¥ì
ì¼ë¡ë¶í° ì¤í¸ë¦¼ì ìì ë©´, removestreameventê° ë°ìíê² ëë¤.
function handleRemoveStreamEvent(event) {
closeVideoCall();
}
ì´ í¨ìë closeVideoCall()í¨ì를 ì¤íìì¼ callì´ ë«íëë¡ ë§ë¤ê³ , ë¤ë¥¸ 커ë¥ì
ì ììí ì ìëë¡ ê¸°ì¡´ ì¸í°íì´ì¤ë¥¼ ë²ë¦°ë¤. ì´ë»ê² ì½ëê° ëìíëì§ Ending the callì 참조í´ë¼.
Ending the call
There are many reasons why calls may end. A call might have completed, with one or both sides having hung up. Perhaps a network failure has occurred. Or one user might have quit their browser, or had a systen crash.
Hanging up
When the user clicks the "Hang Up" button to end the call, the hangUpCall() function is apllied:
function hangUpCall() {
closeVideoCall();
sendToServer({
name: myUsername,
target: targetUsername,
type: "hang-up",
});
}
hangUpCall() executes closeVideoCall(), shutting down and resetting the connection and related resources. We then build a "hang-up" message, sending this to the other end of the call, allowing the other peer to neatly shut down.
Ending the call
ìëì ìë closeVideoCall()í¨ìë streamë¤ì ë©ì¶ê³ ì§ì´ íì,RTCPeerConnectionobject를 ìì¤ë¤.
function closeVideoCall() {
var remoteVideo = document.getElementById("received_video");
var localVideo = document.getElementById("local_video");
if (myPeerConnection) {
if (remoteVideo.srcObject) {
remoteVideo.srcObject.getTracks().forEach((track) => track.stop());
remoteVideo.srcObject = null;
}
if (localVideo.srcObject) {
localVideo.srcObject.getTracks().forEach((track) => track.stop());
localVideo.srcObject = null;
}
myPeerConnection.close();
myPeerConnection = null;
}
document.getElementById("hangup-button").disabled = true;
targetUsername = null;
}
2ê°ì <video>element를 참조í ì´íì, WebRTC 커ë¥ì
ì´ ì¡´ì¬íëì§ ì²´í¬íë¤. ë§ì½ ìë¤ë©´, callì ëê³ ë«ëë¤:
- 리모í¸ì ë¡ì»¬ ë¹ëì¤ streamì ëí´ì, ê° trackë¤ ë§ë¤
MediaTrack.stop()를 ì¤íìí¨ë¤. - ì ë¹ëì¤ì
HTMLMediaElement.srcObjectproperty를nullë¡ ë°ê¿ streamì ê´í 모ë 참조를 í¼ë¤. myPeerConnection.close()를 ë¶ë¬RTCPeerConnectionì ë«ëë¤.myPeerConnectionë³ìì ê°ìnullë¡ ë°ê¿ì ê³ì ì§íì¤ì¸ callì´ ìë¤ë ê²ì ì ì²´ ì½ëê° ìê² íë¤. ì´ê²ì ì ì ê° ì ì 리ì¤í¸ìì usernameì í´ë¦í ë ì¬ì©ëë¤.
ë§ì§ë§ì¼ë¡, "Hang Up" ë²í¼ì disabled property를 trueë¡ ë°ê¿ì callì´ ìë ëììë í´ë¦ì´ ë¶ê°ë¥íê² ë§ë ë¤. ê·¸ ë¤ìì ëì´ì íµí를 íì§ ìì¼ë¯ë¡ targetUsernameì nullë¡ ë°ê¾¼ë¤. ì´ê²ì íµí´ ë ë¤ë¥¸ ì ì ìê² callì íê±°ë ìë¡ì´ callì ë°ì ì ìë¤.
Dealing with state changes
ë¤ìí ìí ë³í를 ëì ì½ëì ì리기 ìí´ listener를 ì¸í
í ì ìë ë¤ìí ì´ë²¤í¸ë¤ì´ ìë¤. ê·¸ ì¤ì ë¤ì 3ê°ì§ë¥¼ ì¬ì©íê² ë¤.: iceconnectionstatechange, icegatheringstatechange, and signalingstatechange.
ICE connection state
커ë¥ì
stateê° ë°ëë©´(ì를ë¤ì´, callì´ ë¤ë¥¸ìª½ìì ì¤ë¨ ë ë) ICE layerê° iceconnectionstatechangeevent를 ì°ë¦¬ìê² ë³´ë¸ë¤.
function handleICEConnectionStateChangeEvent(event) {
switch (myPeerConnection.iceConnectionState) {
case "closed":
case "failed":
case "disconnected":
closeVideoCall();
break;
}
}
ICE connection stateê° "closed", ëë"failed", ëë "disconnected"ì¼ë¡ ë°ë ë closeVideoCall()í¨ì를 ì¤ííë¤. 커ë¥ì
ì ëì¼ë©°, ì²ì(ëë accept) call ìíë¡ ëìê°ë¤.
ICE signaling state
ë§ì°¬ê°ì§ë¡ signalingstatechangeevent를 ë°ì ì ìëë°, ìê·¸ëë§ ìíê° "closed"ì¼ë¡ ë°ëë©´ callì ìì í ì¢
ë£ìí¨ë¤.
myPeerConnection.onsignalingstatechange = function (event) {
switch (myPeerConnection.signalingState) {
case "closed":
closeVideoCall();
break;
}
};
ICE gathering state
icegatheringstatechange events are used to let you know when the ICE candidate gathering process state changes. Our example doesn't use this for anything, but we're implementing it for logging, observing via the console log how the whole process works.
function handleICEGatheringStateChangeEvent(event) {
// Our sample just logs information to console here,
// but you can do whatever you need.
}
Next steps
You can now play with this sample to see it in action. Open the Web console on both devices and look at the logged outputâalthough you don't see it in the code as shown above, the code on the server (and on GitHub) has a lot of console output so you can see the signaling and connection processes at work.