一对一WebRTC视频通话系列(四)——offer、answer、candidate信令实现

君莫笑lucky 2024-07-24 12:33:01 阅读 65

本篇博客主要讲解offer、answer、candidate信令实现,涵盖了媒体协商和网络协商相关实现。

本系列博客主要记录一对一WebRTC视频通话实现过程中的一些重点,代码全部进行了注释,便于理解WebRTC整体实现。


一对一WebRTC视频通话系列往期博客

一对一WebRTC视频通话系列(一)—— 创建页面并显示摄像头画面

一对一WebRTC视频通话系列(二)——websocket和join信令实现

一对一WebRTC视频通话系列(三)——leave和peer-leave信令实现


offer、answer、candidate信令实现

整体实现思路1. 客户端2. 服务端

整体实现思路

整体实现思路(红色部分为客户端,蓝色为服务端):

(1)收到new­peer (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()

<code>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;

应从消息监听函数入手,完成对<code>offer、answercandidate这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);

}

}

answercandidate信令处理函数逻辑与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);

}

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。