行业知识
一对一WebRTC视频通话系列(四)——offer、answer、candidate信令实现
本篇博客主要讲解offer、answer、candidate信令实现,涵盖了媒体协商和网络协商相关实现。
本系列博客主要记录一对一WebRTC视频通话实现过程中的一些重点,代码全部进行了注释,便于理解WebRTC整体实现。
一对一WebRTC视频通话系列往期博客
一对一WebRTC视频通话系列(一)—— 创建页面并显示摄像头画面
一对一WebRTC视频通话系列(二)——websocket和join信令实现
一对一WebRTC视频通话系列(三)——leave和peer-leave信令实现
offer、answer、candidate信令实现
- 整体实现思路
- 1. 客户端
- 2. 服务端
整体实现思路
整体实现思路(红色部分为客户端,蓝色为服务端):
(1)收到newpeer (handleRemoteNewPeer处理),作为发起者创建RTCPeerConnection,绑定事件响应函数,加入本地流;
(2)创建offer sdp,设置本地sdp,并将offer sdp发送到服务器;
(3)服务器收到offer sdp 转发给指定的remoteClient;
(4)接收者收到offer,也创建RTCPeerConnection,绑定事件响应函数,加入本地流;
(5)接收者设置远程sdp,并创建answer sdp,然后设置本地sdp并将answer sdp发送到服务器;
(6)服务器收到answer sdp 转发给指定的remoteClient;
(7)发起者收到answer sdp,则设置远程sdp;
(8)发起者和接收者都收到ontrack回调事件,获取到对方码流的对象句柄;
(9)发起者和接收者都开始请求打洞,通过onIceCandidate获取到打洞信息(candidate)并发送给对方
(10)如果P2P能成功则进行P2P通话,如果P2P不成功则进行中继转发通话。
1. 客户端
(1)创建RTCPeerConnection,绑定事件响应函数,加入本地流
handleRemoteNewPeer->doOffer->ceratePeerConnection()
function doOffer() { //创建RTCPeerConnection对象 if(pc == null) ceratePeerConnection(); pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);}function ceratePeerConnection() { //创建RTCPeerConnection对象 pc = new RTCPeerConnection(null); pc.onicecandidate = handleIceCandidate; pc.ontrack = handleRemoteStreamAdd; localStream.getTracks().forEach(track => { pc.addTrack(track, localStream); });}(2)创建offer sdp,设置本地sdp,并将offer sdp发送到服务器
handleRemoteNewPeer->doOffer->
pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);
function createOfferAndSendMessage(session){ pc.setLocalDescription(session).then(function(){ var jsonMsg = { 'cmd': 'offer', 'roomId': roomId, 'uid': localUserId, 'remoteUid':remoteUserId, 'msg': JSON.stringify(session) }; var message = JSON.stringify(jsonMsg); //将json对象转换为字符串 zeroRTCEngine.sendMessage(message); //设计方法:用实现方法而不是直接用变量 console.info("send offer message: " + message); }).catch(function(error){ console.error('offer setLocalDiscription failed: ' + error.toString()); });}(4)接收者收到offer,也创建RTCPeerConnection,绑定事件响应函数,加入本地流
ZeroRTCEngine.prototype.onmessage()解析收到信息。
当信令为SIGNAL_TYPE_OFFER时,调用handleRemoteOffer()进行处理。
function handleRemoteOffer(message) { console.info("handleRemoteOffer"); if(pc == null){ ceratePeerConnection(); } var desc = JSON.parse(message.msg); pc.setRemoteDescription(desc); doAnswer();}(5)接收者设置远程sdp,并创建answer sdp,然后设置本地sdp并将answer sdp发送到服务器;
在(4)完成后,调用doAnswer()函数实现。
function doAnswer() { pc.createAnswer().then(createAnswerAndSendMessage).catch(handleCreateAnswerError);}function createAnswerAndSendMessage(session){ pc.setLocalDescription(session).then(function(){ var jsonMsg = { 'cmd': 'answer', 'roomId': roomId, 'uid': localUserId, 'remoteUid':remoteUserId, 'msg': JSON.stringify(session) }; var message = JSON.stringify(jsonMsg); //将json对象转换为字符串 zeroRTCEngine.sendMessage(message); //设计方法:用实现方法而不是直接用变量 console.info("send answer message: " + message); }).catch(function(error){ console.error('answer setLocalDiscription failed: ' + error.toString()); });}(7)发起者收到answer sdp,则设置远程sdp;
ZeroRTCEngine.prototype.onmessage()解析收到信息。
当信令为SIGNAL_TYPE_ANSWER时,调用handleRemoteAnswer()进行处理。
function handleRemoteAnswer(message) { console.info("handleRemoteAnswer"); var desc = JSON.parse(message.msg); pc.setRemoteDescription(desc);}(8)发起者和接收者都收到ontrack回调事件,获取到对方码流的对象句柄; ???
(9)发起者和接收者都开始请求打洞,通过onIceCandidate获取到打洞信息(candidate)并发送给对方
function createPeerConnection() { pc = new RTCPeerConnection(null); pc.onicecandidate = handleIceCandidate; pc.ontrack = handleRemoteStreamAdd; localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));}function handleIceCandidate(event) { console.info("handleIceCandidate"); if (event.candidate) { var jsonMsg = { 'cmd': 'candidate', 'roomId': roomId, 'uid': localUserId, 'remoteUid': remoteUserId, 'msg': JSON.stringify(event.candidate) }; var message = JSON.stringify(jsonMsg); zeroRTCEngine.sendMessage(message); console.info("send candidate message"); } else { console.warn("End of candidates"); }}function handleRemoteCandidate(message) { console.info("handleRemoteCandidate"); var candidate = JSON.parse(message.msg); pc.addIceCandidate(candidate).catch(e => { console.error("addIceCandidate failed:" + e.name); });}2. 服务端
主要完成以下两点:
(3)服务器收到offer sdp 转发给指定的remoteClient;
(6)服务器收到answer sdp 转发给指定的remoteClient;
应从消息监听函数入手,完成对offer、answer、candidate这3种情况的处理。
// 监听客户端发送的消息conn.on("text", function (str) { console.info("Received msg:"+str); var jsonMsg = JSON.parse(str); switch(jsonMsg.cmd){ case SIGNAL_TYPE_JOIN: handleJoin(jsonMsg, conn); break; case SIGNAL_TYPE_LEAVE: handleLeave(jsonMsg); break; case SIGNAL_TYPE_OFFER://新添1 handleOffer(jsonMsg); break; case SIGNAL_TYPE_ANSWER://新添2 handleAnswer(jsonMsg); break; case SIGNAL_TYPE_CANDIDATE://新添3 handleCandidate(jsonMsg); break; }});首先完成offer信令处理函数:
当收到视频流 offer 消息时,它会提取房间ID、用户ID和远程用户ID,然后检查房间Map中是否存在该用户ID。如果存在用户ID,它会将消息发送给远程用户。
实现原理如下:
- 获取房间ID和用户ID。
- 获取房间Map。
- 检查用户ID是否存在于房间Map中。
- 如果远程用户存在,将消息发送给远程用户。
- 如果不存在,输出错误信息。
function handleOffer(message){ // 获取房间ID和用户ID var roomId = message.roomId; var uid = message.uid; var remoteUid = message.remoteUid; console.info("handleOffer uid:" + uid + " send offer to remoteUid: " + remoteUid); // 获取房间Map var roomMap = roomTableMap.get(roomId); if(roomMap == null){ console.error("roomId:" + roomId + " is not exist"); return; } if(roomMap.get(uid) == null){ console.error("uid:" + uid + " is not exist in roomId:" + roomId); return; } var remoteClient = roomMap.get(remoteUid); if(remoteClient){ var msg = JSON.stringify(message); remoteClient.conn.sendText(msg); }else{ console.error("remoteUid:" + remoteUid + " is not exist in roomId:" + roomId); }}answer和candidate信令处理函数逻辑与offer几乎一样,简单修改函数名称和打印信息即可:
function handleAnswer(message){ // 获取房间ID和用户ID var roomId = message.roomId; var uid = message.uid; var remoteUid = message.remoteUid; console.info("handleAnswer uid:" + uid + " send answer to remoteUid: " + remoteUid); // 获取房间Map var roomMap = roomTableMap.get(roomId); if(roomMap == null){ console.error("roomId:" + roomId + " is not exist"); return; } if(roomMap.get(uid) == null){ console.error("uid:" + uid + " is not exist in roomId:" + roomId); return; } var remoteClient = roomMap.get(remoteUid); if(remoteClient){ var msg = JSON.stringify(message); remoteClient.conn.sendText(msg); }else{ console.error("remoteUid:" + remoteUid + " is not exist in roomId:" + roomId); }}function handleCandidate(message){ // 获取房间ID和用户ID var roomId = message.roomId; var uid = message.uid; var remoteUid = message.remoteUid; console.info("handleCandidate uid:" + uid + " send Candidate to remoteUid: " + remoteUid); // 获取房间Map var roomMap = roomTableMap.get(roomId); if(roomMap == null){ console.error("roomId:" + roomId + " is not exist"); return; } if(roomMap.get(uid) == null){ console.error("uid:" + uid + " is not exist in roomId:" + roomId); return; } var remoteClient = roomMap.get(remoteUid); if(remoteClient){ var msg = JSON.stringify(message); remoteClient.conn.sendText(msg); }else{ console.error("remoteUid:" + remoteUid + " is not exist in roomId:" + roomId); }}
上一篇:dhcp服务器配置怎么做?
下一篇:idea服务器怎么破解?