WebRTC å 许å¨ä¸¤ä¸ªè®¾å¤ä¹é´è¿è¡å®æ¶ç对çåªä½äº¤æ¢ãéè¿ç§°ä¸ºä¿¡ä»¤çåç°åååè¿ç¨å»ºç«è¿æ¥ãæ¬æç¨å°æå¯¼ä½ æå»ºååè§é¢éè¯ã
WebRTC æ¯ä¸ä¸ªå®å ¨å¯¹çææ¯ï¼ç¨äºå®æ¶äº¤æ¢é³é¢ãè§é¢åæ°æ®ï¼åæ¶æä¾ä¸ä¸ªä¸å¿è¦åãå¦å ¶ä»å°æ¹æè®¨è®ºçï¼å¿ é¡»è¿è¡ä¸ç§åç°ååªä½æ ¼å¼ååï¼ä»¥ä½¿ä¸åç½ç»ä¸ç两个设å¤ç¸äºå®ä½ãè¿ä¸ªè¿ç¨è¢«ç§°ä¸ºä¿¡ä»¤ï¼å¹¶æ¶å两个设å¤è¿æ¥å°ç¬¬ä¸ä¸ªå ±ååå®çæå¡å¨ãéè¿è¿ä¸ªç¬¬ä¸æ¹æå¡å¨ï¼è¿ä¸¤å°è®¾å¤å¯ä»¥ç¸äºå®ä½ï¼å¹¶äº¤æ¢ååæ¶æ¯ã
卿¬æä¸ï¼æä»¬å°è¿ä¸æ¥æ©å WebSocket chat ä½ä¸ºæä»¬ç WebSocket ææ¡£çä¸é¨åï¼æ¬æé¾æ¥å³å°åå¸;å®å®é ä¸è¿æ²¡æå¨çº¿ï¼ï¼ä»¥æ¯æå¨ç¨æ·ä¹é´çååè§é¢éè¯ãä½ å¯ä»¥å¨Glitch䏿¥çè¿ä¸ªä¾åï¼ä½ ä¹å°è¯ä¿®æ¹è¿ä¸ªä¾åãä½ è¿å¯ä»¥å¨GitHub䏿¥ç宿´ç项ç®ä»£ç ã
夿³¨ï¼ å¦æä½ å°è¯å¨ Glitch çä¾åï¼è¯·æ³¨æä»»ä½ä»£ç çæ¹å¨å°ç«å³éç½®ææè¿æ¥ãå¹¶ä¸è¿ä¸ªä¾åæçæçå»¶è¿ï¼Glitch çä¾åä» ä» ä½ä¸ºç®åçå®éªåæµè¯ç¨éã
信令æå¡å¨ä¸¤ä¸ªè®¾å¤ä¹é´å»ºç« WebRTC è¿æ¥éè¦ä¸ä¸ªä¿¡ä»¤æå¡å¨æ¥å®ç°åæ¹éè¿ç½ç»è¿è¡è¿æ¥ã信令æå¡å¨çä½ç¨æ¯ä½ä¸ºä¸ä¸ªä¸é´äººå¸®å©åæ¹å¨å°½å¯è½å°çæ´é²éç§çæ åµä¸å»ºç«è¿æ¥ã飿们å¦ä½å®ç°è¿ä¸ªæå¡å¨å¹¶ä¸å®æ¯å¦ä½å·¥ä½çå¢ï¼
WebRTC 并没ææä¾ä¿¡ä»¤ä¼ éæºå¶ï¼ä½ å¯ä»¥ä½¿ç¨ä»»ä½ä½ 忬¢çæ¹å¼å¦WebSocket æè
XMLHttpRequest
ççï¼æ¥äº¤æ¢å½¼æ¤ç令çä¿¡æ¯ã
éè¦çæ¯ä¿¡ä»¤æå¡å¨å¹¶ä¸éè¦çè§£åè§£éä¿¡ä»¤æ°æ®å 容ãè½ç¶å®åºäº SDPä½è¿å¹¶ä¸éè¦ï¼éè¿ä¿¡ä»¤æå¡å¨çæ¶æ¯çå 容å®é 䏿¯ä¸ä¸ªé»çãéè¦çæ¯ï¼å½ICEåç³»ç»æç¤ºä½ å°ä¿¡ä»¤æ°æ®åéç»å¦ä¸ä¸ªå¯¹çæ¹æ¶ï¼ä½ å°±è¿æ ·åï¼èå¦ä¸ä¸ªå¯¹çæ¹ç¥éå¦ä½æ¥æ¶æ¤ä¿¡æ¯å¹¶å°å ¶ä¼ éç»èªå·±ç ICE åç³»ç»ãä½ æè¦åçå°±æ¯æ¥åä¼ éä¿¡æ¯ãå 容对信令æå¡å¨ä¸ç¹é½ä¸éè¦ã
å¼å§åå¤è天æå¡å¨æ¥å¤ç信令æä»¬çè天æå¡å¨å客æ·ç«¯ä½¿ç¨ WebSocket API JSON æ ¼å¼çå符串æ¥ä¼ éæ°æ®ãæå¡å¨æ¯æå¤ç§æ¶æ¯æ ¼å¼æ¥å¤çä¸åçä»»å¡ï¼æ¯å¦æ³¨åæ°ç¨æ·ãè®¾ç½®ç¨æ·åãåéå ¬å ±ä¿¡æ¯ççã
为äºè®©æå¡å¨æ¯æä¿¡ä»¤å ICE ååï¼æä»¬éè¦å级代ç ï¼æä»¬éè¦ç´æ¥åéè天系ç»å°æå®çç¨æ·è䏿¯åéç»ææäººï¼å¹¶ä¸ä¿è¯æå¡å¨å¨ä¸éè¦çè§£æ°æ®å å®¹çæ åµä¸ä¼ éæªè¢«è®¤å¯ç任使¶æ¯ç±»åãè¿è®©æä»¬å¯ä»¥ä½¿ç¨ä¸å°æå¡å¨æ¥ä¼ éä¿¡ä»¤åæ¶æ¯è䏿¯å¤å°ã
让æä»¬çä¸ä¸æä»¬è¿éè¦åäºä»ä¹è®©å®æ¯æ WebRTC 信令ã代ç å¨ chatserver.js.ä¸å®ç°ã
é¦å
æ¥ç sendToOneUser()
彿°ï¼å¦åæç¤ºå®åé 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;
}
}
}
è¿ä¸ªå½æ°éåææå¨çº¿ç¨æ·ç´å°æ¾å°ç»å®çç¨æ·åç¶ååéæ°æ® msgString
ä¸ä¸ª JSON åç¬¦ä¸²å¯¹è±¡ï¼æä»¬å¯ä»¥è®©å®æ¥æ¶æä»¬çåå§æ¶æ¯å¯¹è±¡ï¼ä½æ¯å¨å½åè¿ç§æ
åµä¸å®çæçæ´é«å 为æä»¬çæ¶æ¯å·²ç»å符串åäºï¼æä»¬è¾¾å°äºä¸éè¦è¿ä¸æ¥å¤çå°±å¯ä»¥åéæ¶æ¯çç®çã
æä»¬åæ¥ç DEMO ä¸è½åéæ¶æ¯å°æå®çç¨æ·ï¼æä»¬å¯ä»¥éè¿ä¿®æ¹ WebSocket æ¶æ¯å¤ç奿æ¥å®ç°è¿ä¸ªåè½ï¼è¿éè¦å¨ 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
屿§ãè¿ä¸ªå±æ§å
å«äºæä»¬æ³è¦åéç»ç人çç¨æ·åã妿æä¾äº target
屿§ï¼éè¿è°ç¨ sendToOneUser()
æ¶æ¯å°åªåéç»æå®ç人ãå¦åçè¯å°éåå¨çº¿å表åéç»æ¯ä¸ä¸ªäººã
ç±äºç°è¡ç代ç å¯ä»¥åéä»»æç±»åçæ¶æ¯ï¼æä»¥æä»¬ä¸éè¦åä»»ä½çä¿®æ¹ãç°å¨æä»¬ç客æ·ç«¯å¯ä»¥åéä»»ææ¶æ¯ç»æå®çç¨æ·ã
æä»¬éè¦åç卿å¡å¨è¿è¾¹ï¼ç°å¨æä»¬æ¥èè信令åè®®ç设计ä¸å®ç°ã
设计信令åè®®ç°å¨æä»¬è¦æå»ºä¸å¥ä¿¡æ¯äº¤æ¢è§åï¼æä»¬éè¦ä¸å¥åè®®æ¥å®ä¹æ¶æ¯æ ¼å¼ãå®ç°è¿ä¸ªæå¥½å¤ç§åæ³ï¼demo éåªæ¯å ¶ä¸ä¸ç§ï¼å¹¶ä¸æ¯å¯ä¸ã
ä¾åä¸çæå¡å¨ä½¿ç¨å符串åç JSON 对象æ¥å客æ·ç«¯éä¿¡ï¼æå³çæä»¬çä¿¡ä»¤æ¶æ¯ä¹å°ä½¿ç¨ JSON æ ¼å¼ï¼å ¶å 容æå®æ¶æ¯ç±»ååå¦ä½å¤çè¿äºæ¶æ¯ã
交æ¢ä¼è¯æè¿°ä¿¡æ¯å¼å§å¤çä¿¡å·çæ¶åï¼ç¨æ·çåå§åæä½ä¼å建ä¸ä¸ªè¯·æ±ï¼offerï¼ ï¼æ ¹æ® SDP åè®®å
¶ä¸ä¼å
å«ä¸ä¸ª session æè¿°ç¬¦ï¼å¹¶ä¸éè¦æè¿ä¸ªåéå°æä»¬ç§°ä¹ä¸º**æ¥æ¶è
ï¼calleeï¼é£éï¼æ¥åè
éè¦è¿åä¸ä¸ªå
å«æè¿°ç¬¦çåºçï¼answerï¼**ä¿¡æ¯ãæä»¬çæå¡å¨ä½¿ç¨ WebSocket æ¥ä¼ é "video-offer"
"video-answer"
两ç§ç±»åçæ¶æ¯æ°æ®ãè¿äºæ¶æ¯å
å«ä»¥ä¸å±æ§ï¼
type
æ¶æ¯ç±»å; "video-offer"
æ "video-answer"
name
åéè ç¨æ·å
target
æ¥åè çç¨æ·åï¼å¦æå¼å«è æ£å¨åéæ¶æ¯ï¼åæå®è¢«å¼å«è ï¼åä¹äº¦ç¶ï¼
sdp
æè¿°è¿æ¥æ¬å°ç«¯ SDPï¼Session Description Protocolï¼åè®®å符串ï¼ä»æ¥æ¶è çè§åº¦æ¥çï¼å®æè¿°è¿ç¨ç«¯ï¼
å°æ¤ä¸ºæ¢åæ¹é½ç¥é使ç¨ä»ä¹æ ·ç代ç ååæ°è¿è¡éä¿¡äºãå°½ç®¡å¦æ¤ä»ä»¬ä»ç¶ä¸ç¥éèªå·±è¯¥å¦ä½ä¼ éåªä½æ°æ®ã Interactive Connectivity Establishment (ICE)å议该ä¸åºäºã
äº¤æ¢ ICE åé两个èç¹éè¦äº¤æ¢ ICE å鿥ååä»ä»¬èªå·±å ·ä½å¦ä½è¿æ¥ãæ¯ä¸ä¸ª ICE åéæè¿°ä¸ä¸ªåéè 使ç¨çéä¿¡æ¹æ³ï¼æ¯ä¸ªèç¹æç §ä»ä»¬è¢«åç°ç顺åºåéåéå¹¶ä¸ä¿æåéç´å°éåºï¼å³ä½¿åªä½æ°æ®æµå·²ç»å¼å§ä¼ éä¹è¦å¦æ¤ã
ä½¿ç¨ pc.setLocalDescription(offer)
æ·»å æ¬å°æè¿°ç¬¦åä¸ä¸ª icecandidate
äºä»¶å°è¢«åéå° RTCPeerConnection
䏿¦ä¸¤ç«¯åæäºä¸ä¸ªäºç¸å ¼å®¹çåéï¼è¯¥åéç SDP å°±è¢«ç¨æ¥å建并æå¼ä¸ä¸ªè¿æ¥ï¼éè¿è¯¥è¿æ¥åªä½æµå°±å¼å§è¿è½¬ã妿ä¹åä»ä»¬åæäºä¸ä¸ªæ´å¥½ï¼é常æ´é«æï¼çåéï¼æµäº¦ä¼æéåæ´æ ¼å¼ã
è½ç¶å½åå¹¶æªè¢«æ¯æï¼ä¸ä¸ªåéå¨åªä½æµå·²ç»å¼å§è¿è½¬ä¹åç论ä¸å¦æéè¦çè¯ä¹å¯ä»¥é级è³ä¸ä¸ªä½å¸¦å®½çè¿æ¥ã
æ¯ä¸ª ICE åééè¿ä¿¡ä»¤æå¡å¨åéä¸ä¸ª "new-ice-candidate"
ç±»åç JSON ä¿¡æ¯æ¥éç»è¿ç¨çå¦ä¸ç«¯ãæ¯ä¸ªåéä¿¡æ¯å
æ¬ä»¥ä¸å段ï¼
ç±»å
æ¶æ¯ç±»åï¼ "new-ice-candidate"
.
ç®æ
å¾ å»ºç«è系人çç¨æ·åï¼æå¡å¨å°ä» ä¼ç®¡çä¸è¯¥ç¨æ·çä¿¡æ¯ã
åé
SDP åéåç¬¦ä¸²ï¼æè¿°äºè®¡åçè¿æ¥æ¹æ³ãé常ä¸éè¦æ¥çæ¤å符串çå 容ãä½ éè¦åçææä»£ç 齿¯ä½¿ç¨ä¿¡ä»¤æå¡å¨å°å ¶è·¯ç±å°è¿ç¨å¯¹çæºã
æ¯ä¸ª ICE æ¶æ¯é½å»ºè®®æä¾ä¸ä¸ªéä¿¡åè®®ï¼TCP æ UDPï¼ãIP å°åã端å£å·ãè¿æ¥ç±»åï¼ä¾å¦ï¼æå®ç IP æ¯å¯¹çæºæ¬èº«è¿æ¯ä¸ç»§æå¡å¨ï¼ï¼ä»¥åå°ä¸¤å°è®¡ç®æºè¿æ¥å¨ä¸èµ·æéçå ¶ä»ä¿¡æ¯ãè¿å æ¬ NAT æå ¶ä»ç½ç»é®é¢ã
夿³¨ï¼ æéè¦æ³¨æçæ¯ï¼ä½ ç代ç å¨ ICE ååæé´å¯ä¸éè¦è´è´£çæ¯ä» ICE 屿¥åå¤ååéå¹¶éè¿ä¸å¦ä¸ç«¯çä¿¡å·è¿æ¥åéä»ä»¬ï¼å½ä½ ç onicecandidate
æ§å¶å¨å·²ç»æ§è¡åï¼åæ¶ä»ä¿¡ä»¤æå¡å¨æ¥æ¶ ICE åéæ¶æ¯ (彿¥æ¶å° "new-ice-candidate"
æ¶æ¯æ¶) ç¶åéè¿è°ç¨RTCPeerConnection.addIceCandidate()
åéä»ä»¬å°ä½ ç ICE å±ãå¯ï¼å°±æ¯è¿æ ·ã
SDP çå å®¹åºæ¬ä¸å¨æææ åµä¸é½æ¯ä¸ä½ ä¸ç¸å ³çãå¨ä½ çæ£ç¥éèªå·±å¨åä»ä¹ä¹åï¼ä¸è¦è¯å¾è®©äºæ å徿´å¤æãå¦åæ åµä¼é常混乱ã
ä½ ç信令æå¡å¨ç°å¨éè¦åçå°±æ¯åéå®è¯·æ±çæ¶æ¯ãä½ ç工使µè¿å¯è½éè¦ç»å½/身份éªè¯åè½ï¼ä½è¿äºç»è齿¯å¤§åå°å¼çã
信令äºå¡æµç¨ä¿¡ä»¤è¿ç¨æ¶åå°ä½¿ç¨ä¸é´å±ä¿¡ä»¤æå¡å¨å¨ä¸¤ä¸ªå¯¹çæºä¹é´äº¤æ¢æ¶æ¯ãå½ç¶ï¼å ·ä½çå¤çè¿ç¨ä¼ææä¸åï¼ä½ä¸è¬æ¥è¯´ï¼å¤çä¿¡ä»¤æ¶æ¯çå ³é®ç¹æä»¥ä¸å 个ï¼
信令è¿ç¨æ¶åå¤ä¸ªç¹ä¹é´çæ¶æ¯äº¤æ¢ï¼
å设 Naomi å Priya æ£å¨ä½¿ç¨è天软件è¿è¡è®¨è®ºï¼Naomi å³å®å¨ä¸¤äººä¹é´æå¼ä¸ä¸ªè§é¢éè¯ã以䏿¯é¢æçäºä»¶é¡ºåºï¼
卿¬æçæ´ä¸ªè¿ç¨ä¸ï¼æä»¬å°çå°æ´è¯¦ç»çä¿¡æ¯ã
ICE åé交æ¢è¿ç¨å½æ¯ç«¯ç ICE å±å¼å§åéåéæ¶ï¼å®ä¼å¨é¾ä¸çå个ç¹ä¹é´è¿è¡äº¤æ¢ï¼å¦ä¸æç¤ºï¼
æ¯ä¸ç«¯ä»æ¬å°ç ICE 屿¥æ¶åéæ¶ï¼é½ä¼å°å ¶åéç»å¦ä¸æ¹ï¼ä¸åå¨è½®æµæææ¹çåéã䏿¦ä¸¤ç«¯å°±ä¸ä¸ªåéè¾¾æä¸è´ï¼åæ¹å°±é½å¯ä»¥ç¨æ¤åéæ¥äº¤æ¢åªä½æ°æ®ï¼åªä½æ°æ®å°±å¼å§æµå¨ãå³ä½¿å¨åªä½æ°æ®å·²ç»å¼å§æµå¨ä¹åï¼æ¯ä¸ç«¯é½ä¼ç»§ç»ååéåéæ¶æ¯ï¼ç´å°ä»ä»¬æ²¡æéæ©çä½å°ãè¿æ ·åæ¯ä¸ºäºæ¾å°æ¯æåéæ©çæ´å¥½çéæ©ã
妿æ¡ä»¶åçååï¼ä¾å¦ç½ç»è¿æ¥æ¶åï¼ä¸ä¸ªæä¸¤ä¸ªå¯¹çæ¹å¯è½å»ºè®®åæ¢å°è¾ä½å¸¦å®½çåªä½å辨çï¼æå ¶ä»ç¼è§£ç å¨ãè¿å°è§¦åæ°çåé交æ¢ï¼ä¹åå¯è½ä¼åçå¦ä¸ç§åªä½æ ¼å¼å/æç¼è§£ç 卿´æ¹ã
ä½ä¸ºå¯éé¡¹ï¼æ¥ç RFC 5245: Interactive Connectivity Establishment, section 2.6 ("Concluding ICE")å¦æä½ æ³æ´æ·±å ¥å°äºè§£è¿ä¸è¿ç¨ï¼å°±è¦å¨ ICE å±å é¨å®æãä½ åºè¯¥æ³¨æå°ï¼åé交æ¢åï¼ä¸æ¦ ICE 屿»¡è¶³è¦æ±ï¼åªä½æ°æ®å°±å¼å§æµå¨ãææè¿äºé½æ¯å¨å¹åå¤ç端ãæä»¬çä»»å¡å°±æ¯ç®åå°éè¿ä¿¡ä»¤æå¡å¨æ¥ååéåéã
客æ·ç«¯åºç¨ä»»ä½ä¿¡å·å¤ççæ ¸å¿æ¯å ¶æ¶æ¯å¤çãä½¿ç¨ WebSockets æ¥åéä¿¡å·å¹¶ä¸æ¯å¿ é¡»çï¼ä½è¿æ¯ä¸ç§å¸¸è§çè§£å³æ¹æ¡ãå½ç¶ï¼ä½ åºè¯¥éæ©ä¸ç§æºå¶æ¥äº¤æ¢éåä½ çåºç¨ç¨åºçä¿¡å·ä¿¡æ¯ã
让æä»¬æ´æ°è天客æ·ç«¯ä»¥æ¯æè§é¢å¼å«ã
æ´æ° HTMLæä»¬å®¢æ·ç«¯ç 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>
æ¤å¤å®ä¹ç页é¢ç»æä½¿ç¨äº <div>
å
ç´ ï¼éè¿å¯ç¨ CSSï¼æä»¬å¯ä»¥å®å
¨æ§å¶é¡µé¢å¸å±ãæä»¬å°è·³è¿æ¬æåä¸çå¸å±ç»èï¼ä½ä½ å¯ä»¥çç GitHub ä¸ç CSSï¼äºè§£å¦ä½å¤çå®ã注æè¿ä¸¤ä¸ª <video>
å
ç´ ï¼ä¸ä¸ªç¨äºè§çèªå·±ï¼ä¸ä¸ªç¨äºè¿æ¥ï¼è¿æ <button>
å
ç´ ã
id
为 "received_video
" ç <video>
å
ç´ å°æ¾ç¤ºä»è¿æ¥çç¨æ·æ¥æ¶çè§é¢ãæä»¬æå®äºautoplay
屿§ï¼ç¡®ä¿ä¸æ¦è§é¢å°è¾¾ï¼å®ç«å³ææ¾ãè¿æ¶é¤äºå¨ä»£ç 䏿¾å¼å¤çåæ¾çä»»ä½éè¦ã"local_video
" <video>
å
ç´ æ¾ç¤ºç¨æ·ç¸æºçé¢è§ï¼æå® muted
屿§ï¼å 为æä»¬ä¸éè¦å¨æ¤é¢è§é¢æ¿ä¸å¬å°æ¬å°é³é¢ã
æåï¼å®ä¹"hangup-button
" <button>
æ¥ææä¸ä¸ªå¼å«ï¼å¹¶å°å
¶é
置为ç¦ç¨å¯å¨ï¼å°æ¤è®¾ç½®ä¸ºæªè¿æ¥ä»»ä½è°ç¨æ¶çé»è®¤è®¾ç½®ï¼ï¼å¹¶å¨å廿¶è°ç¨å½æ° hangUpCall()
ãè¿ä¸ªå½æ°çä½ç¨æ¯å
³éè°ç¨ï¼å¹¶åå¦ä¸ä¸ªå¯¹ç端åéä¸ä¸ªä¿¡å·æå¡å¨éç¥ï¼è¯·æ±å®ä¹å
³éã
æä»¬å°æè¿æ®µä»£ç åå为å¤ä¸ªåè½åºï¼ä»¥ä¾¿æ´å®¹æå°æè¿°å®æ¯å¦ä½å·¥ä½çã该代ç ç主ä½ä½äº connect()
彿°ä¸ï¼å®å¨ 6503 端å£ä¸æå¼ä¸ä¸ªWebSocket
æå¡å¨ï¼å¹¶å»ºç«ä¸ä¸ªå¤çç¨åºæ¥æ¥æ¶ JSON å¯¹è±¡æ ¼å¼çæ¶æ¯ãæ¤ä»£ç é常å以å飿 ·å¤çææ¬èå¤©æ¶æ¯ã
卿´ä¸ªä»£ç ä¸ï¼æä»¬è°ç¨ sendToServer()
以便å信令æå¡å¨åéæ¶æ¯ãæ¤å½æ°ä½¿ç¨WebSocketè¿æ¥æ§è¡å
¶å·¥ä½ï¼
function sendToServer(msg) {
var msgJSON = JSON.stringify(msg);
connection.send(msgJSON);
}
éè¿è°ç¨JSON.stringify()
ï¼å°ä¼ éå°æ¤æ¹æ³çæ¶æ¯å¯¹è±¡è½¬æ¢ä¸º json å符串ï¼ç¶åè°ç¨ WebSocket è¿æ¥ç send()
æ¹æ³å°æ¶æ¯ä¼ è¾å°æå¡å¨ã
å¤ç "userlist"
æ¶æ¯ç代ç ä¼è°ç¨ handleUserlistMsg()
ãå¨è¿éï¼æä»¬å¨è天颿¿å·¦ä¾§æ¾ç¤ºçç¨æ·å表ä¸ä¸ºæ¯ä¸ªè¿æ¥çç¨æ·è®¾ç½®å¤çç¨åºãæ¤æ¹æ³æ¥æ¶ä¸ä¸ªæ¶æ¯å¯¹è±¡ï¼å
¶ users
屿§æ¯ä¸ä¸ªå符串æ°ç»ï¼æå®æ¯ä¸ªè¿æ¥ç¨æ·çç¨æ·åã
function handleUserlistMsg(msg) {
var i;
var listElem = document.querySelector(".userlistbox");
while (listElem.firstChild) {
listElem.removeChild(listElem.firstChild);
}
msg.users.forEach(function (username) {
var item = document.createElement("li");
item.appendChild(document.createTextNode(username));
item.addEventListener("click", invite, false);
listElem.appendChild(item);
});
}
å¨è·å¾å¯¹ <ul>
çå¼ç¨ï¼å
¶ä¸å
å«åé listElem
ä¸çç¨æ·åå表ï¼åï¼æä»¬éè¿å é¤å
¶æ¯ä¸ªåå
ç´ æ¸
空å表ã
夿³¨ï¼ æ¾ç¶ï¼éè¿æ·»å åå é¤åä¸ªç¨æ·è䏿¯æ¯æ¬¡æ´æ¹æ¶é½éæ°æå»ºæ´ä¸ªåè¡¨æ¥æ´æ°åè¡¨ä¼æ´ææï¼ä½å¯¹äºæ¬ä¾èè¨ï¼è¿å·²ç»è¶³å¤å¥½äºã
ç¶åæä»¬ä½¿ç¨ forEach()
è¿ä»£ç¨æ·åæ°ç»ãå¯¹äºæ¯ä¸ªåç§°ï¼æä»¬å建ä¸ä¸ªæ°ç <li>
å
ç´ ï¼ç¶å使ç¨createTextNode()
å建ä¸ä¸ªå
å«ç¨æ·åçæ°ææ¬èç¹ãè¯¥ææ¬èç¹è¢«æ·»å 为 <li>
å
ç´ çåèç¹ãæ¥ä¸æ¥ï¼æä»¬ä¸ºå表项ä¸ç click
äºä»¶è®¾ç½®ä¸ä¸ªå¤çç¨åºï¼åå»ç¨æ·åå°è°ç¨ invite()
æ¹æ³ï¼æä»¬å°å¨ä¸ä¸è䏿¥çè¯¥æ¹æ³ã
å½ç¨æ·åå»è¦è°ç¨çç¨æ·åæ¶ï¼å°è°ç¨ invite()
彿°ä½ä¸ºè¯¥äºä»¶çäºä»¶å¤çç¨åº click
äºä»¶ï¼
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);
}
}
è¿ä»ä¸ä¸ªåºæ¬çå¥å
¨æ§æ£æ¥å¼å§ï¼ç¨æ·æ¯å¦è¿å¨ä¸èµ·ï¼å¦ææ²¡æRTCPeerConnection
ï¼ä»ä»¬æ¾ç¶æ æ³è¿è¡å¼å«ãç¶åï¼ä»äºä»¶ç®æ ç textContent
屿§ä¸è·ååå»çç¨æ·çåç§°ï¼å¹¶æ£æ¥ä»¥ç¡®ä¿å°è¯å¯å¨è°ç¨ç䏿¯åä¸ä¸ªç¨æ·ã
ç¶åæä»¬å°è¦è°ç¨çç¨æ·çåç§°å¤å¶å°åé targetUsername
ä¸ï¼å¹¶è°ç¨ createPeerConnection()
ï¼è¯¥å½æ°å°å建并æ§è¡RTCPeerConnection
çåºæ¬é
ç½®ã
å建 RTCPeerConnection
åï¼æä»¬éè¿è°ç¨ MediaDevices.getUserMedia()
ï¼è¯·æ±è®¿é®ç¨æ·çç¸æºå麦å
é£ï¼è¯¥å½ä»¤éè¿ Navigator.mediaDevices.getUserMedia
屿§åæä»¬å
¬å¼ã彿å宿è¿åç promise æ¶ï¼å°æ§è¡æä»¬ç then
å¤çç¨åºã宿¥æ¶ä¸ä¸ª MediaStream
对象ä½ä¸ºè¾å
¥ï¼è¯¥å¯¹è±¡è¡¨ç¤ºæ¥èªç¨æ·éº¦å
é£çé³é¢åæ¥èªç½ç»æåæºçè§é¢æµã
夿³¨ï¼ æä»¬å¯ä»¥éè¿è°ç¨ navigator.mediaDevices.enumerateDevices()
è·å设å¤åè¡¨ï¼æ ¹æ®æéæ¡ä»¶çéç»æå表ï¼ç¶åä½¿ç¨æéè®¾å¤ deviceId
ä¼ å
¥ getUserMedia()
ç mediaConstraints
对象ç deviceId
åæ®µä¸çå¼ãäºå®ä¸ï¼é¤éå¿
é¡»è¦ä¸ç¶å¾å°è¿æ ·ç¨ï¼å 为大é¨åå·¥ä½é½æ¯ç± getUserMedia()
ä¸ºä½ å®æçã
æä»¬éè¿è®¾ç½®å
ç´ ç srcObject
屿§ï¼å°ä¼ å
¥æµéå å°æ¬å°é¢è§ <video>
å
ç´ ãç±äºå
ç´ è¢«é
置为èªå¨ææ¾ä¼ å
¥çè§é¢ï¼å æ¤æµå¼å§å¨æ¬å°é¢è§æ¡ä¸ææ¾ã
ç¶åéåæµä¸çç£éï¼è°ç¨ addTrack()
å°æ¯ä¸ªç£éæ·»å å° RTCPeerConnection
ãå°½ç®¡è¿æ¥å°æªå®å
¨å»ºç«ï¼ä½å¿
须尽快å¼å§åå
¶åéåªä½æ°æ®ï¼å 为åªä½æ°æ®å°å¸®å© ICE å±å³å®éåçæä½³è¿æ¥æ¹å¼ï¼è¿æå©äºååè¿ç¨ã
䏿¦åªä½æ°æ®è¿æ¥å° RTCPeerConnection
ï¼å°±ä¼å¨è¿æ¥å¤è§¦å negotiationneeded
äºä»¶ï¼ä»¥ä¾¿å¯å¨ ICE ååã
妿å¨å°è¯è·åæ¬å°åªä½æµæ¶åçé误ï¼catch åå¥å°è°ç¨handleGetUserMediaError()
ï¼æ ¹æ®éè¦åç¨æ·æ¾ç¤ºéå½çé误ã
妿 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();
}
é¤äºä¸æ¡é误信æ¯å¤ï¼æææ
åµä¸é½ä¼æ¾ç¤ºä¸æ¡é误信æ¯ã卿¬ä¾ä¸ï¼æä»¬å¿½ç¥"SecurityError"
å "PermissionDeniedError"
ç»æï¼å¤çæç»æäºä½¿ç¨åªä½ç¡¬ä»¶çæéä¸ç¨æ·åæ¶å¼å«çæ¹æ³æ¯ç¸åçã
ä¸ç®¡å°è¯è·åæµå¤±è´¥çåå æ¯ä»ä¹ï¼æä»¬è°ç¨ closeVideoCall()
彿°å
³é RTCPeerConnection
ï¼å¹¶éæ¾å°è¯è°ç¨è¿ç¨ä¸å·²åé
çä»»ä½èµæºãæ¤ä»£ç æ¨å¨å®å
¨å°å¤çé¨åå¯å¨çè°ç¨ã
è°ç¨æ¹å被è°ç¨æ¹é½ä½¿ç¨ createPeerConnection()
彿°æ¥æé å®ä»¬ç RTCPeerConnection
对象åå
¶åèªç WebRTC è¿æ¥ç«¯ãå½è°ç¨è
è¯å¾å¯å¨è°ç¨æ¶ï¼ç± invite()
è°ç¨ï¼å½è¢«è°ç¨è
ä»è°ç¨è
æ¥æ¶å°è¦çº¦æ¶æ¯æ¶ï¼ç±handleVideoOfferMsg()
è°ç¨ã
function createPeerConnection() {
myPeerConnection = new RTCPeerConnection({
iceServers: [
// Information about ICE servers - Use your own!
{
urls: "stun:stun.stunprotocol.org",
},
],
});
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.ontrack = handleTrackEvent;
myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
myPeerConnection.onremovetrack = handleRemoveTrackEvent;
myPeerConnection.oniceconnectionstatechange =
handleICEConnectionStateChangeEvent;
myPeerConnection.onicegatheringstatechange =
handleICEGatheringStateChangeEvent;
myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
}
å½ä½¿ç¨ RTCPeerConnection()
æé 彿°æ¶ï¼æä»¬å°æå®ä¸ä¸ªRTCConfiguration
-å
¼å®¹å¯¹è±¡ï¼ä¸ºè¿æ¥æä¾é
ç½®åæ°ãå¨è¿ä¸ªä¾åä¸ï¼æä»¬åªä½¿ç¨å
¶ä¸çä¸ä¸ªï¼ iceServers
ãè¿æ¯æè¿° ICE å±ç STUN å/æ TURN æå¡å¨ç对象æ°ç»ï¼å¨å°è¯å¨å¼å«è
å被å¼å«è
ä¹é´å»ºç«è·¯ç±æ¶ä½¿ç¨ãè¿äºæå¡å¨ç¨äºç¡®å®å¨å¯¹ç端ä¹é´éä¿¡æ¶è¦ä½¿ç¨çæä½³è·¯ç±ååè®®ï¼å³ä½¿å®ä»¬ä½äºé²ç«å¢åé¢æä½¿ç¨ NATã
夿³¨ï¼ ä½ åºè¯¥å§ç»ä½¿ç¨ä½ æ¥æçæä½ æç¹å®ææä½¿ç¨çSTUN/TURNæå¡å¨ãè¿ä¸ªä¾åæ¯ä½¿ç¨ä¸ä¸ªå·²ç¥çå ¬å ±æå¡å¨ï¼ä½æ¯æ»¥ç¨è¿äºæ¯ä¸å¥½çã
iceServers
ä¸çæ¯ä¸ªå¯¹è±¡è³å°å
å«ä¸ä¸ª urls
åæ®µï¼è¯¥å段æä¾å¯ä»¥è®¿é®æå®æå¡å¨ç URLsãå®è¿å¯ä»¥æä¾ username
å credential
å¼ï¼ä»¥ä¾¿å¨éè¦æ¶è¿è¡èº«ä»½éªè¯ã
å¨åå»ºäº RTCPeerConnection
ä¹åï¼æä»¬ä¸ºå¯¹æä»¬éè¦çäºä»¶è®¾ç½®äºå¤çç¨åºã
åä¸ä¸ªäºä»¶å¤çç¨åºæ¯å¿ éçï¼ä½ å¿ é¡»å¤çå®ä»¬æè½ä½¿ç¨ WebRTC æ§è¡ä»»ä½æ¶åæµåªä½çæä½ãå ¶ä½ç并䏿¯ä¸¥æ ¼è¦æ±çï¼ä½å¯è½æç¨ï¼æä»¬å°å¯¹æ¤è¿è¡æ¢è®¨ãå¨è¿ä¸ªä¾åä¸ï¼è¿æä¸äºå ¶ä»çäºä»¶æä»¬æ²¡æä½¿ç¨ãä¸é¢æ¯æä»¬å°è¦å®ç°çæ¯ä¸ªäºä»¶å¤çç¨åºçæè¦ï¼
RTCPeerConnection.onicecandidate
å½éè¦ä½ éè¿ä¿¡ä»¤æå¡å¨å°ä¸ä¸ª ICE åéåéç»å¦ä¸ä¸ªå¯¹ç端æ¶ï¼æ¬å° ICE å±å°ä¼è°ç¨ä½ ç icecandidate
äºä»¶å¤çç¨åºãæå
³æ´å¤ä¿¡æ¯ï¼è¯·åé
äº¤æ¢ ICE åé 以æ¥çæ¤ç¤ºä¾ç代ç ã
RTCPeerConnection.ontrack
å½åè¿æ¥ä¸æ·»å ç£éæ¶ï¼track
äºä»¶çæ¤å¤çç¨åºç±æ¬å° WebRTC å±è°ç¨ãä¾å¦ï¼å¯ä»¥å°ä¼ å
¥åªä½è¿æ¥å°å
ç´ ä»¥æ¾ç¤ºå®ãè¯¦è§æ¥æ¶æ°çæµæ°æ® ã
RTCPeerConnection.onnegotiationneeded
æ¯å½ WebRTC åºç¡ç»æéè¦ä½ éæ°å¯å¨ä¼è¯ååè¿ç¨æ¶ï¼é½ä¼è°ç¨æ¤å½æ°ãå®ç工使¯å建ååéä¸ä¸ªè¯·æ±ï¼ç»è¢«å«æ¹ï¼è¦æ±å®ä¸æä»¬èç³»ãåè§å¼å§ååäºè§£æä»¬å¦ä½å¤çè¿ä¸é®é¢ã
RTCPeerConnection.onremovetrack
è°ç¨ä¸ ontrack
ç¸å¯¹åºç对象æ¥å¤ç removetrack
äºä»¶ï¼å½è¿ç¨å¯¹çç«¯ä»æ£å¨åéçåªä½ä¸å é¤ç£éæ¶ï¼å®å°åéå°RTCPeerConnection
ãåè§å¤çæµçç§»é¤ ã
RTCPeerConnection.oniceconnectionstatechange
ICE å±åé iceconnectionstatechange
äºä»¶ï¼è®©ä½ äºè§£ ICE è¿æ¥ç¶æçæ´æ¹ãè¿å¯ä»¥å¸®å©ä½ äºè§£è¿æ¥ä½æ¶å¤±è´¥æä¸¢å¤±ãæä»¬å°å¨ä¸é¢çICE è¿æ¥ç¶æä¸æ¥çæ¤ç¤ºä¾ç代ç ã
RTCPeerConnection.onicegatheringstatechange
å½ ICE ä»£çæ¶éåé对象çè¿ç¨ä»ä¸ä¸ªç¶æåæ¢å°å¦ä¸ä¸ªç¶æï¼ä¾å¦å¼å§æ¶éåé对象æå®æååï¼æ¶ï¼ICE å±å°åä½ åéäºä»¶ï¼âICegulatingStateChangeâï¼äºä»¶ãè§ä¸æ ICE æ¶éç¶æã
RTCPeerConnection.onsignalingstatechange
å½ä¿¡ä»¤è¿ç¨çç¶ææ´æ¹æ¶ï¼æå¦æå°ä¿¡ä»¤æå¡å¨çè¿æ¥æ´æ¹æ¶ï¼ï¼WebRTC æ¶æå°åä½ åé signalingstatechange
æ¶æ¯ãåè§ICE ä¿¡ä»¤ç¶ææ¥çæä»¬ç代ç ã
䏿¦è°ç¨è
å建äºå
¶ RTCPeerConnection
ï¼å建äºåªä½æµï¼å¹¶å°å
¶ç£éæ·»å å°è¿æ¥ä¸ï¼å¦ å¼å§éè¯çäº¤äº æç¤ºï¼æµè§å¨å°å RTCPeerConnection
ä¼ éä¸ä¸ª negotiationneeded
äºä»¶ï¼ä»¥æç¤ºå®å·²åå¤å¥½å¼å§ä¸å
¶ä»å¯¹çæ¹ååã以䏿¯æä»¬å¤ç negotiationneeded
äºä»¶ç代ç ï¼
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);
}
è¦å¼å§ååè¿ç¨ï¼æä»¬éè¦å建ä¸ä¸ª SDP 请æ±å¹¶å°å
¶åéç»æä»¬æ³è¦è¿æ¥ç对çç«¯ãæ¤è¯·æ±å
æ¬æ¯æçè¿æ¥é
ç½®å表ï¼å
æ¬æå
³æä»¬å¨æ¬å°æ·»å å°è¿æ¥çåªä½æµï¼å³ï¼æä»¬å¸æåéå°å¼å«å¦ä¸ç«¯çè§é¢ï¼çä¿¡æ¯ï¼ä»¥å ICE å±å·²ç»æ¶éå°çä»»ä½ ICE åéãæä»¬éè¿è°ç¨ myPeerConnection.createOffer()
å建æ¤è¯·æ±ã
å½ createOffer()
æåï¼æ§è¡ promiseï¼æ¶ï¼æä»¬å°å建ç请æ±ä¿¡æ¯ä¼ éå°myPeerConnection.setLocalDescription()
ï¼å®ä¸ºè°ç¨æ¹çè¿æ¥ç«¯é
ç½®è¿æ¥ååªä½é
ç½®ç¶æã
夿³¨ï¼ 仿æ¯ä¸è®²ï¼ createOffer()
è¿åçå符串æ¯RFC 3264 请æ±ã
æä»¬ç¥éæè¿°æ¯ææçï¼å¹¶ä¸å¨æ»¡è¶³setLocalDescription()
è¿åç promise æ¶å·²ç»è®¾ç½®å¥½äºãä¹å°±æ¯è¯´æä»¬å建äºä¸ä¸ªå
嫿¬å°æè¿°ï¼ç°å¨ä¸è¯·æ±ç¸åï¼çæ° "video-offer"
æ¶æ¯ï¼ç¶åéè¿æä»¬ç信令æå¡å¨å°è¯·æ±åéç»è¢«å«æ¹ãè¯·æ±æä»¥ä¸è¦ç´ ï¼
type
æ¶æ¯ç±»åï¼"video-offer"
.
name
è°ç¨æ¹çç¨æ·åã
target
被è°ç¨æ¹çç¨æ·å
sdp
SDP å符串æè¿°äºè¯·æ±
妿å¨åå§ createOffer()
æåé¢çä»»ä½å®ç°å¤çç¨åºä¸åçé误ï¼åéè¿è°ç¨ reportError()
彿°æ¥åé误ã
å¨ setLocalDescription()
çå®ç°å¤çç¨åºè¿è¡åï¼ICE 代çå¼å§åå
¶åç°çæ¯ä¸ªæ½å¨ RTCPeerConnection
é
ç½®åé icecandidate
äºä»¶ãæä»¬ç icecandidate
äºä»¶å¤çç¨åºè´è´£å°åéå¯¹è±¡ä¼ è¾å°å¦ä¸ä¸ªå¯¹çæ¹ã
æ¢ç¶æä»¬å·²ç»å¼å§ä¸å¦ä¸ä¸ªå¯¹çæ¹è¿è¡ååå¹¶ä¼ è¾äºä¸ä¸ªè¯·æ±ï¼é£ä¹è®©æä»¬æ¥çä¸ä¸å¨è¿æ¥çè¢«å«æ¹æ¹é¢ä¼åçä»ä¹ã被è°ç¨æ¹æ¥æ¶è¯¥è¯·æ±å¹¶è°ç¨ handleVideoOfferMsg()
彿°æ¥å¤çå®ã让æä»¬ççè¢«å«æ¹å¦ä½å¤ç "video-offer"
æ¶æ¯ã
å½è¯·æ±å°è¾¾æ¶ï¼è°ç¨è¢«è°ç¨æ¹ç handleVideoOfferMsg()
彿°æ¶ä¼æ¶å°"video-offer"
æ¶æ¯ãè¿ä¸ªå½æ°éè¦å两件äºãé¦å
ï¼å®éè¦å建èªå·±ç RTCPeerConnection
å¹¶æ·»å å
å«éº¦å
é£åç½ç»æå头çé³é¢åè§é¢çç£éãå
¶æ¬¡ï¼å®éè¦å¯¹æ¶å°ç请æ±è¿è¡å¤çï¼æå»ºå¹¶è¿ååºçã
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;
localStream
.getTracks()
.forEach((track) => myPeerConnection.addTrack(track, localStream));
})
.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);
}
æ¤ä»£ç 䏿们å¨å¼å§éè¯ç交äºä¸å¨ invite()
彿°ä¸æåçé常ç¸ä¼¼ãå®é¦å
ä½¿ç¨ createPeerConnection()
彿°å建åé
ç½®RTCPeerConnection
ãç¶åï¼å®ä»æ¶å°ç "video-offer"
æ¶æ¯ä¸è·å SDP 请æ±ï¼å¹¶ä½¿ç¨å®å建ä¸ä¸ªè¡¨ç¤ºè°ç¨æ¹ä¼è¯æè¿°çæ° RTCSessionDescription
对象ã
ç¶åå°è¯¥ä¼è¯æè¿°ä¼ éå° myPeerConnection.setRemoteDescription()
ãè¿å°ææ¥æ¶å°ç请æ±å»ºç«ä¸ºè¿æ¥çè¿ç¨ï¼è°ç¨æ¹ï¼ç«¯çæè¿°ã妿æåï¼promise æåå¤çç¨åºï¼å¨ then() åå¥ä¸ï¼å°ä½¿ç¨ getUserMedia()
ï¼å°ç£éæ·»å å°è¿æ¥ï¼ä»¥æ¤ç±»æ¨ï¼å¦åé¢å¨ invite()
ä¸çå°ç飿 ·ã
䏿¦ä½¿ç¨ myPeerConnection.createAnswer()
å建äºåºçï¼éè¿è°ç¨myPeerConnection.setLocalDescription()
è¿æ¥æ¬å°ç«¯çæè¿°è¢«è®¾ç½®ä¸ºåºçç SDPï¼åéè¿ä¿¡ä»¤æå¡å¨å°åºçåéç»è°ç¨è
ï¼è®©ä»ä»¬ç¥éåºçæ¯ä»ä¹ã
ææå°çä»»ä½é误é½ä¼è¢«ä¼ éç» handleGetUserMediaError()
ï¼è¯¦è§ å¤ç getUserMedia() é误 ã
夿³¨ï¼ ä¸è°ç¨è
çæ
åµä¸æ ·ï¼ä¸æ¦ setLocalDescription()
å®ç°å¤çç¨åºè¿è¡å®æ¯ï¼æµè§å¨å°å¼å§è§¦å被è°ç¨è
å¿
é¡»å¤çç icecandidate
äºä»¶ï¼æ¯ä¸ªéè¦ä¼ è¾å°è¿ç¨å¯¹çæ¹çåéäºä»¶å¯¹åºä¸ä¸ªäºä»¶ã
ICE ååè¿ç¨æ¶åå°æ¯ä¸ä¸ªå¯¹çç«¯ä¸æå°åå¦ä¸ä¸ªå¯¹ç端åéåéï¼ç´å°å®ç¨å°½äºæ¯æ RTCPeerConnection
çåªä½ä¼ è¾éæ±çæ½å¨æ¹æ³ãå 为 ICE ä¸ç¥éä½ ç信令æå¡å¨ï¼æä»¥ä½ çå¤çç¨åºä»£ç éè¦å¤ç icecandidate
äºä»¶ä¸æ¯ä¸ªåéçä¼ è¾ã
ä½ ç onicecandidate
å¤çç¨åºæ¥æ¶ä¸ä¸ªäºä»¶ï¼è¯¥äºä»¶çåé屿§æ¯æè¿°è¯¥åéç SDPï¼æä¸º null
ï¼è¡¨ç¤º ICE å±å·²èå°½å»ºè®®çæ½å¨é
ç½®ï¼ãåéçå
容æ¯ä½ éè¦ä½¿ç¨ä¿¡ä»¤æå¡å¨ä¼ è¾çå
容ãä¸é¢æ¯æä»¬ç示ä¾å®ç°ï¼
function handleICECandidateEvent(event) {
if (event.candidate) {
sendToServer({
type: "new-ice-candidate",
target: targetUsername,
candidate: event.candidate,
});
}
}
è¿å°æå»ºä¸ä¸ªå
å«åé对象ç对象ï¼ç¶å使ç¨åé¢å¨ å信令æå¡å¨åéä¿¡æ¯ ä¸æè¿°çsendToServer()
彿°å°å
¶åéç»å¦ä¸ä¸ªå¯¹çæ¹ãæ¶æ¯å±æ§ä¸ºï¼
type
æ¶æ¯ç±»åï¼"new-ice-candidate"
.
target
ICE åééè¦ä¼ éå°çç¨æ·åãè¿å 许信令æå¡å¨è·¯ç±æ¶æ¯ã
candidate
代表 ICE 屿³è¦ä¼ è¾ç»å¦ä¸ä¸ªå¯¹çä½çåéä½ç SDPã
æ¤æ¶æ¯çæ ¼å¼ï¼ä¸å¤çä¿¡å·æ¶æåçæææä½ä¸æ ·ï¼å®å ¨åå³äºä½ çéè¦ï¼ä½ å¯ä»¥æ ¹æ®éè¦æä¾å ¶ä»ä¿¡æ¯ã
夿³¨ï¼ éè¦çæ¯è¦è®°ä½ï¼icecandidate
äºä»¶ä¸ä¼å¨ ICE åéä»å¼å«çå¦ä¸ç«¯å°è¾¾æ¶åéãç¸åï¼å®ä»¬æ¯ç±ä½ èªå·±çå¼å«ç«¯åéçï¼è¿æ ·ä½ å°±å¯ä»¥æ¿æ
éè¿ä½ éæ©çä»»ä½ééä¼ è¾æ°æ®çä»»å¡ãå½ä½ åæ¥è§¦ WebRTC æ¶ï¼è¿ä¼è®©äººå°æã
信令æå¡å¨ä½¿ç¨å®éæ©ç任使¹æ³å°æ¯ä¸ª ICE åéä¼ éç»ç®æ å¯¹çæºï¼å¨æä»¬ç示ä¾ä¸ï¼æä»¬ç¨çæ¯ JSON å¯¹è±¡ï¼ type
屿§å
å«å符串 "new-ice-candidate"
ãæä»¬ç r handleNewICECandidateMsg()
彿°ç±ä¸»WebSocketä¼ å
¥æ¶æ¯ä»£ç è°ç¨ï¼ä»¥å¤çè¿äºæ¶æ¯ï¼
function handleNewICECandidateMsg(msg) {
var candidate = new RTCIceCandidate(msg.candidate);
myPeerConnection.addIceCandidate(candidate).catch(reportError);
}
æ¤å½æ°éè¿å°æ¥æ¶å°ç SDP ä¼ éç»å®çæé 彿°æ¥æé ä¸ä¸ª RTCIceCandidate
对象ï¼ç¶åéè¿myPeerConnection.addIceCandidate()
å°åéä¼ éç» ICE å±ãè¿ææ°å»ºç ICE åé交ç»äºå½å°ç ICE å±ï¼æç»ï¼æä»¬å¨å¤çæ´ä¸ªåéçè¿ç¨ä¸çè§è²å°±å®æ´çäºã
æ¯ä¸ä¸ªå¯¹ç端åå¦ä¸ä¸ªå¯¹ç端åéä¸ä¸ªåéçå¯è½ä¼ è¾é ç½®ï¼å®è®¤ä¸ºè¿å¯¹äºæ£å¨äº¤æ¢çåªä½å¯è½æ¯å¯è¡çã卿ç§ç¨åº¦ä¸ï¼è¿ä¸¤ç«¯è®¤ä¸ºï¼ä¸ä¸ªç»å®çå鿝ä¸ä¸ªå¾å¥½çéæ©ï¼äºæ¯ä»ä»¬æå¼è¿æ¥ï¼å¼å§å享åªä½æ°æ®ãç¶èï¼éç¹è¦æ³¨æçæ¯ï¼ä¸æ¦åªä½æ°æ®å¼å§æµå¨ï¼ICE ä¸ååå°±ä¸ä¼åæ¢ãç¸åï¼å¨å¯¹è¯å¼å§åï¼åé对象å¯è½ä»ç¶å¨ä¸æå°è¿è¡äº¤æ¢ï¼å¯è½æ¯å¨è¯å¾æ¾å°æ´å¥½çè¿æ¥æ¹æ³çåæ¶ï¼ä¹å¯è½åªæ¯å 为å¨å¯¹çæ¹æå建ç«è¿æ¥æ¶ï¼ä»ä»¬å·²ç»å¨ä¼ è¾ä¸äºã
æ¤å¤ï¼å¦æåçä»ä¹äºæ
å¯¼è´æµåºæ¯åçååï¼ååå°å次å¼å§ï¼å°äºä»¶ negotiationneeded
äºä»¶åéå°RTCPeerConnection
ï¼æ´ä¸ªè¿ç¨å°å¦åæè¿°éæ°å¼å§ãè¿å¯è½åçå¨åç§æ
åµä¸ï¼å
æ¬ï¼
彿°çç£éæ·»å å° RTCPeerConnection
æ¶ââéè¿è°ç¨å
¶addTrack()
æ¹æ³ï¼æè
ç±äºéæ°ååæµçæ ¼å¼ââå¯¹äºæ·»å å°è¿æ¥çæ¯ä¸ªç£éï¼ä¸ä¸ª track
äºä»¶è®¾ç½®ä¸º RTCPeerConnection
ãä½¿ç¨æ°æ·»å çåªä½éè¦å®ç° track
äºä»¶çå¤çç¨åºã常è§çéè¦æ¯å°ä¼ å
¥çåªä½éå å°éå½ç HTML å
ç´ ã卿们ç示ä¾ä¸ï¼æä»¬å°ç£éçæµæ·»å å°æ¾ç¤ºä¼ å
¥è§é¢ç <video>
å
ç´ ï¼
function handleAddStreamEvent(event) {
document.getElementById("received_video").srcObject = event.stream;
document.getElementById("hangup-button").disabled = false;
}
ä¼ å
¥æµéå å° "received_video"
<video>
å
ç´ ï¼å¹¶ä¸å¯ç¨"Hang Up" <button>
å
ç´ ï¼ä»¥ä¾¿ç¨æ·ææå¼å«ã
宿æ¤ä»£ç åï¼å ¶ä»å¯¹çæ¹åéçè§é¢å°æ¾ç¤ºå¨æ¬å°æµè§å¨çªå£ä¸ï¼
å¤çæµçç§»é¤å½è¿ç¨å¯¹çæ¹éè¿è°ç¨RTCPeerConnection.removeTrack()
.ä»è¿æ¥ä¸å é¤ç£éæ¶ï¼ä½ ç代ç å°æ¥æ¶äºä»¶ removetrack
äºä»¶ã"removetrack"
çå¤çç¨åºæ¯ï¼
function handleRemoveTrackEvent(event) {
var stream = document.getElementById("received_video").srcObject;
var trackList = stream.getTracks();
if (trackList.length == 0) {
closeVideoCall();
}
}
æ¤ä»£ç ä»"received_video"
<video>
å
ç´ ç srcobject
屿§è·åä¼ å
¥è§é¢ getTracks()
æ¹æ³è·åæµçç£éæ°ç»ã
妿æ°ç»çé¿åº¦ä¸ºé¶ï¼æå³çæµä¸æ²¡æå©ä½çç£éï¼åéè¿è°ç¨ closeVideoCall()
ç»æè°ç¨ãè¿æ ·å°±å¯ä»¥å°æä»¬çåºç¨ç¨åºæ¢å¤å°å¯ä»¥å¯å¨ææ¥æ¶å¦ä¸ä¸ªå¼å«çç¶æã请åé
ç»æéè¯ äºè§£ closeVideoCall()
çå·¥ä½åçã
éè¯å¯è½ç»æçåå æå¾å¤ãä¸ä¸ªéè¯å¯è½å·²ç»ç»æï¼å½ä¸æ¹æåæ¹é½ææäºçµè¯ãå¯è½åçäºç½ç»æ éï¼æè æä¸ªç¨æ·éåºäºæµè§å¨ï¼æè åçäºç³»ç»å´©æºãæ 论å¦ä½ï¼ä¸åç¾å¥½çäºç©é½å¿ é¡»ç»æã
ææºå½ç¨æ·åå»"Hang Up"æé®ç»æè°ç¨æ¶ï¼å°è°ç¨ hangUpCall()
彿°ï¼
function hangUpCall() {
closeVideoCall();
sendToServer({
name: myUsername,
target: targetUsername,
type: "hang-up",
});
}
hangUpCall()
æ§è¡ closeVideoCall()
æ¥å
³éå¹¶éç½®è¿æ¥å¹¶éæ¾èµæºãç¶åå®ä¼çæä¸ä¸ª "hang-up"
æ¶æ¯ï¼å¹¶å°å
¶åéå°å¼å«çå¦ä¸ç«¯ï¼åè¯å¦ä¸ä¸ªå¯¹ç端æ´é½å°å
³éèªå·±ã
closeVideoCall()
彿°ï¼å¦ä¸æç¤ºï¼è´è´£åæ¢æµãæ¸
çåå¤ç RTCPeerConnection
对象ï¼
function closeVideoCall() {
var remoteVideo = document.getElementById("received_video");
var localVideo = document.getElementById("local_video");
if (myPeerConnection) {
myPeerConnection.ontrack = null;
myPeerConnection.onremovetrack = null;
myPeerConnection.onremovestream = null;
myPeerConnection.onicecandidate = null;
myPeerConnection.oniceconnectionstatechange = null;
myPeerConnection.onsignalingstatechange = null;
myPeerConnection.onicegatheringstatechange = null;
myPeerConnection.onnegotiationneeded = null;
if (remoteVideo.srcObject) {
remoteVideo.srcObject.getTracks().forEach((track) => track.stop());
}
if (localVideo.srcObject) {
localVideo.srcObject.getTracks().forEach((track) => track.stop());
}
myPeerConnection.close();
myPeerConnection = null;
}
remoteVideo.removeAttribute("src");
remoteVideo.removeAttribute("srcObject");
localVideo.removeAttribute("src");
remoteVideo.removeAttribute("srcObject");
document.getElementById("hangup-button").disabled = true;
targetUsername = null;
}
å¨å¼ç¨äºä¸¤ä¸ª <video>
å
ç´ ä¹åï¼æä»¬æ£æ¥ WebRTC è¿æ¥æ¯å¦ä»ç¶åå¨ï¼å¦æåå¨ï¼åç»§ç»æå¼å¹¶å
³éè°ç¨ï¼
MediaStreamTrack.stop()
æ¹æ³å
³éæ¯ä¸ªç£éãmyPeerConnection.close()
.å
³é RTCPeerConnection
ãmyPeerConnection
为 null
ï¼ç¡®ä¿æä»¬ç代ç ç¥éæ²¡ææ£å¨è¿è¡çè°ç¨ï¼å½ç¨æ·åå»ç¨æ·å表ä¸çåç§°æ¶ï¼è¿å¾æç¨ãç¶åï¼å¯¹äºä¼ å
¥åä¼ åºç<video>
å
ç´ ï¼æä»¬ä½¿ç¨å®ä»¬çremoveAttribute()
æ¹æ³å é¤å®ä»¬ç srcobject
åsrc
屿§ãè¿å°±å®æäºæµä¸è§é¢å
ç´ çå离ã
æåï¼æä»¬å¨"Hang Up"æé®ä¸å°disabled
屿§è®¾ç½®ä¸º true
ï¼ä½¿å
¶å¨æ²¡æè°ç¨çæ
åµä¸ä¸å¯ç¹å»ï¼ç¶åæä»¬å°targetUsername
设置为 null
ï¼å 为æä»¬ä¸åä¸ä»»ä½äººäº¤è°ãè¿å
è®¸ç¨æ·å¼å«å¦ä¸ä¸ªç¨æ·ï¼ææ¥æ¶æ¥çµã
è¿æè®¸å¤å
¶ä»äºä»¶å¯ä»¥è®¾ç½®çå¬å¨ï¼ç¨äºéç¥ä»£ç åç§ç¶ææ´æ¹ãæä»¬ä½¿ç¨ä¸ç§æ¹æ³ï¼iceconnectionstatechange
ãicegatheringstatechange
å signalingstatechange
ã
äºä»¶ iceconnectionstatechange
å½è¿æ¥ç¶ææ´æ¹æ¶ï¼ä¾å¦ï¼å½ä»å¦ä¸ç«¯ç»æ¢è°ç¨æ¶ï¼ï¼ç± ICE å±å°äºä»¶åéå°RTCPeerConnection
ã
function handleICEConnectionStateChangeEvent(event) {
switch (myPeerConnection.iceConnectionState) {
case "closed":
case "failed":
case "disconnected":
closeVideoCall();
break;
}
}
è¿éï¼å½ ICE è¿æ¥ç¶ææ´æ¹ä¸º"closed"
ï¼"failed"
ï¼æè
"disconnected"
æ¶ï¼æä»¬å°åºç¨ closeVideoCall()
彿°ãè¿å°å¤çå
³éæä»¬çè¿æ¥ç«¯ï¼ä»¥ä¾¿æä»¬åå¤å¥½éæ°å¼å§ææ¥åå¼å«ã
åæ ·ï¼æä»¬çå¬ signalingstatechange
äºä»¶ãå¦æä¿¡å·ç¶æå为 closed
ï¼æä»¬åæ ·å
³éå¼å«ã
myPeerConnection.onsignalingstatechange = function (event) {
switch (myPeerConnection.signalingState) {
case "closed":
closeVideoCall();
break;
}
};
夿³¨ï¼ closed
çä¿¡ä»¤ç¶æå·²è¢«å¼ç¨ï¼åè代ä¹çæ¯ closed
iceConnectionState
ãæä»¬å¨è¿éçå¬å®ä»¥å¢å ä¸ç¹ååå
¼å®¹æ§ã
icegatheringstatechange
äºä»¶ç¨äºè®©ä½ ç¥é使¶ ICE åéæ¶éè¿ç¨ç¶æåçæ´æ¹ãæä»¬ç示ä¾å¹¶æ²¡æå°å
¶ç¨äºä»»ä½ç¨éï¼ä½æ¯ä¸ºäºè°è¯çç®çè§å¯è¿äºäºä»¶ä»¥åæ£æµåééå使¶å®æé½æ¯æç¨çã
function handleICEGatheringStateChangeEvent(event) {
// Our sample just logs information to console here,
// but you can do whatever you need.
}
ä¸ä¸æ¥
ç°å¨ä½ å¯ä»¥å¨ Glitch ä¸å°è¯è¿ä¸ªä¾åï¼ä»¥çå°å®çå®é ææãæå¼ä¸¤ä¸ªè®¾å¤ä¸ç Web æ§å¶å°å¹¶æ¥çè®°å½çè¾åºï¼å°½ç®¡ä½ å¨ä¸é¢æç¤ºç代ç ä¸çä¸å°å®ï¼ä½æ¯æå¡å¨ä¸ï¼ä»¥åGitHubä¸ï¼çä»£ç æå¾å¤æ§å¶å°è¾åºï¼å æ¤ä½ å¯ä»¥çå°ä¿¡ä»¤åè¿æ¥è¿ç¨å¨å·¥ä½ã
å¦ä¸ä¸ªææ¾çæ¹è¿æ¯æ·»å äºä¸ä¸ªâé声âåè½ï¼è¿æ ·ä¸æ¥ï¼ä¸ä¸ª"ç¨æ· X æ£å¨å¼å«ãä½ æ¯å¦è¦åºçï¼" æç¤ºä¼é¦å åºç°ï¼èä¸ä» ä» æ¯è¯·æ±ç¨æ·å 许使ç¨ç¸æºå麦å é£ã
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4