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

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

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


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

一对一WebRTC视频通话系列(一)------ 创建页面并显示摄像头画面
一对一WebRTC视频通话系列(二)------websocket和join信令实现
一对一WebRTC视频通话系列(三)------leave和peer-leave信令实现


offer、answer、candidate信令实现

整体实现思路

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

(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()

javascript 复制代码
function doOffer() {
    //创建RTCPeerConnection对象
    if(pc == null)
        ceratePeerConnection();
    pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);
}
javascript 复制代码
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);

javascript 复制代码
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()进行处理。

javascript 复制代码
	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()函数实现。

javascript 复制代码
function doAnswer() {
    pc.createAnswer().then(createAnswerAndSendMessage).catch(handleCreateAnswerError);
}
javascript 复制代码
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()进行处理。

javascript 复制代码
function handleRemoteAnswer(message) {
    console.info("handleRemoteAnswer");
    var desc = JSON.parse(message.msg);
    pc.setRemoteDescription(desc);
}

(8)发起者和接收者都收到ontrack回调事件,获取到对方码流的对象句柄; ???

(9)发起者和接收者都开始请求打洞,通过onIceCandidate获取到打洞信息(candidate)并发送给对方

javascript 复制代码
function createPeerConnection() {
    pc = new RTCPeerConnection(null);
    pc.onicecandidate = handleIceCandidate;
    pc.ontrack = handleRemoteStreamAdd;

    localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));
}
javascript 复制代码
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");
    }
}
javascript 复制代码
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;

应从消息监听函数入手,完成对offeranswercandidate这3种情况的处理。

javascript 复制代码
// 监听客户端发送的消息
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,它会将消息发送给远程用户。

实现原理如下:

  1. 获取房间ID和用户ID。
  2. 获取房间Map。
  3. 检查用户ID是否存在于房间Map中。
  4. 如果远程用户存在,将消息发送给远程用户。
  5. 如果不存在,输出错误信息。
javascript 复制代码
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几乎一样,简单修改函数名称和打印信息即可:

javascript 复制代码
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);
    }
}
相关推荐
EasyCVR1 小时前
私有化部署视频平台EasyCVR宇视设备视频平台如何构建视频联网平台及升级视频转码业务?
大数据·网络·音视频·h.265
天空中的野鸟1 小时前
Android音频采集
android·音视频
计算机毕设孵化场2 小时前
计算机毕设-基于springboot的高校网上缴费综合务系统视频的设计与实现(附源码+lw+ppt+开题报告)
java·spring boot·计算机外设·音视频·课程设计·高校网上缴费综合务系统视频·计算机毕设ppt
简鹿办公9 小时前
如何提取某站 MV 视频中的音乐为 MP3 音频
音视频·简鹿视频格式转换器·视频提取mp3音频
yufengxinpian9 小时前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
runing_an_min11 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
runing_an_min13 小时前
ffmpeg视频滤镜:提取缩略图-framestep
ffmpeg·音视频·framestep
小曲曲14 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
安静读书16 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
佑华硬盘拷贝机16 小时前
音频档案批量拷贝:专业SD拷贝机解决方案
音视频