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

本文详细描述了WebRTC一对一视频通话中的offer、answer和candidate信令流程,包括客户端和服务端的交互,以及如何在RTCPeerConnection上设置和转发SDP。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇博客主要讲解offer、answer、candidate信令实现,涵盖了媒体协商和网络协商相关实现。
本系列博客主要记录一对一WebRTC视频通话实现过程中的一些重点,代码全部进行了注释,便于理解WebRTC整体实现。


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

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


offer、answer、candidate信令实现

整体实现思路

整体实现思路(红色部分为客户端,蓝色为服务端):
(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()

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;
应从消息监听函数入手,完成对offeranswercandidate这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,它会将消息发送给远程用户。
实现原理如下:

  1. 获取房间ID和用户ID。
  2. 获取房间Map。
  3. 检查用户ID是否存在于房间Map中。
  4. 如果远程用户存在,将消息发送给远程用户。
  5. 如果不存在,输出错误信息。
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);
    }
}
### 使用 UDP 和 WebRTC 实现视频聊天 #### 创建 RTCPeerConnection 对象并配置 ICE 代理 为了使两个浏览器之间可以互相发送和接收音视频数据,需要创建 `RTCPeerConnection` 对象。此对象负责管理整个通信过程中的交互细节。 ```javascript const configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; let pc = new RTCPeerConnection(configuration); ``` 上述代码片段展示了如何初始化一个带有 STUN 服务器设置的 Peer Connection[^4]。 #### 添加本地媒体轨道到连接中 获取用户的摄像头权限并将捕获的数据流添加至已建立好的 peer connection 中: ```javascript async function start() { let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }); document.querySelector('video').srcObject = stream; stream.getTracks().forEach(track => { pc.addTrack(track, stream); }); } start(); ``` 这段脚本实现了请求访问用户设备上的摄像机资源,并将其关联到 HTML 页面内的 `<video>` 元素上显示出来;同时也把每一条 track 注册到了之前定义过的 PC 实例里去。 #### 处理候选地址 (ICE Candidates) 当发现新的网络接口时,PeerConnection 将触发 `icecandidate` 事件来通知开发者有可用的新路径可用于两方之间的直接联系。这些信息对于穿越 NAT 或防火墙至关重要。 ```javascript pc.onicecandidate = event => { if (!event.candidate) return; console.log(`New candidate found:\n${JSON.stringify(event.candidate.toJSON())}`); // Send this information to the remote party via signaling channel. }; ``` 每当检测到一个新的 ICE 候选者时就会执行上面指定的日志记录逻辑并向远端传递该消息以便于它能更新自己的路由表。 #### 发送 Offer 并等待 Answer 发起呼叫的一侧应当构建 SDP offer 来提议本次会话所使用的编解码器及其他参数组合。之后通过信令通道传送给接听者的应用层程序处理。 ```javascript async function doCall() { try { const offer = await pc.createOffer(); await pc.setLocalDescription(offer); console.log(`Offer from local\n${pc.localDescription.sdp}`); // The offer needs to be sent through a signaling server or another out-of-band mechanism. } catch(err){ console.error("Error creating offer:", err); } } // Call when ready... doCall(); ``` 一旦设置了本地描述符后就可以准备向另一终端发出邀请了。注意这里并没有具体说明怎样完成实际的消息传送工作——这取决于应用程序的设计需求以及选用的具体协议栈(比如 WebSocket、HTTP Long Polling 等)。同样地,在接收到响应 answer 后也需要调用相应方法来进行确认。 #### 接收远程 Answer 并继续协商 被叫方在接收到 offer 后应该解析其中的内容并与自身的硬件条件相匹配从而给出最合适的回应方案即所谓的 "answer" 。随后再次经过相同的流程返回给原作者以达成最终一致意见。 ```javascript function handleAnswer(answerSdp) { var desc = new RTCSessionDescription({ type: 'answer', sdp: answerSdp, }); pc.setRemoteDescription(desc).then(() => { console.log("Successfully set remote description."); }).catch((error) => { console.error(error.name + ": " + error.message); }); } ``` 以上步骤完成后即可建立起稳定的双向实时多媒体传输链路。值得注意的是在整个过程中涉及到大量异步操作所以建议采用现代 JavaScript 特性如 async/await 结构化错误捕捉机制提高可读性和健壮性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

君莫笑lucky

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值