WebRTC实践点对点通信

tech2023-11-03  105

技术前言

       前次教程我们一起用NodeJs做为服务端,用纯javascript简单实现了一个基于文本的P2P通讯,两个浏览器终端之间基于Socket.IO技术进行普通文本信息互相传输,其实已实现了一个简单聊天室的基本技术模型。本次教程我们基于上次教程的内容继续探讨WebRTC视频通讯,实现不同浏览器之间进行视频通讯。大家注意本系列教程是采用循序渐进的方式,每次教程的内容都是下次教程的铺垫。WebRTC视频通讯主要是用RTCPeerConnection通讯实现与其它终端的长连接完成双方的视频数据传输。

RTCPeerConnection连接过程图       RTCPeerConnection视频流数据传输完整的生命周期还是比较清晰,如上图所示终端A和终端B在初始化的时候各自创建RTCPeerConnection对像准备数据通道。终端A完成初始化后马上向终端B发送Call信令(标明媒体数据已准备就绪),终端B收到Call后发送Offer信令并设置setLocalDescription属性,注意当setLocalDescription属性改变时会触发onicecandidate事件,通常在onicecandidate事件中实现向对方发送candidate数据的业务逻辑。此时终端A收到Offer后设置自身的setRemoteDescription属性,并且执行createAnswer操作设置setLocalDescription属性后立刻发送Answer信令。终端B收到Answer后设置自身的setRemoteDescription属性同时触发onaddstream事件。在onaddstream事件中实现将远程视频流加载到本地视频容器中的操作。       至此终端A到终端B的P2P视频通讯完成。

接口方法

      RTCPeerConnection 中提供的接口很多,大家可以去官网上查询。 我这里主要说的是有关本教程中的接口,也是最常用的接口。 1. createOffer 在 CreateOffer 中,会获取本地所支持的音视频编码格式,以及传输相关参数信息,一般在此方法中要设置setLocalDescription属性完成RTC对本地视频数据加载。 2. setLocalDescription 设置改变与本地连接关联的本地视频描述。此描述信息中包括媒体格式。如果setLocalDescription()在连接已经建立时被调用,则表示正在进行重新协商(可能是为了适应不断变化的网络状况)。因为在两个对等方就配置达成一致之前将交换描述,所以通过调用提交的描述setLocalDescription()不会立即生效。相反,当前的连接配置将保持不变,直到协商完成。只有这样,商定的配置才会生效。 3. createAnswer 在WebRTC连接期间createAnswer()方法是针对收到的Offer信令后创建 SDP答复信令并回复远程终端。此SDP答复包含有关会话中已附加的所有媒体参数,如浏览器支持的编解码器和选项以及已收集的所有ICE候选者的信息。 4. setRemoteDescription 设置改变与远程连接关联的视频描述。该描述指定了连接远程终端的视频属性,包括媒体格式。通常在通过信令服务器从远程终端收到Answer答复后调用此方法。 因为在两个对等方就配置达成一致之前将交换描述,所以通过调用提交的描述setRemoteDescription()不会立即生效。相反,当前的连接配置将保持不变,直到协商完成。只有这样,商定的配置才会生效。 5. onaddstream 当执行addstream操作时,将发送此类事件。该事件在调用后立即发送setRemoteDescription(),并且不等待SDP协商的结果。 6. onicecandidate 每当本地ICE代理需要通过信令服务器将消息传递到远程终端时,就会触发事件。这使ICE代理可以与远程终端进行协商,而浏览器本身无需知道有关用于信令的技术的任何细节。只需实施此方法即可使用您选择的任何将ICE候选者发送到远程终端的消息传递技术。

实践代码

前端页面

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"> <title>WebRTC实践点对点通信</title> <script src="../javascript/trace.js"></script> <script src="../javascript/socket.io.js"></script> <script src="../javascript/jquery-1.11.1.min.js"></script> <script src="../javascript/jquerysocketio.js"></script> <script src="../javascript/rtc.js"></script> <style type="text/css"> video { border: 1px solid black; background-color: black; max-width: 100%; width: 100px; } </style> <script> var cfg = { ip: '127.0.0.1', port: 6100 } $(function() { $.net.connect(function(person) { trace("在线人数:" + person); if(person.length > 0) { $("#personDiv").empty(); person.forEach(p => { if(p.id != $.net.id) { var html = '<input type="radio" value="' + p.id + '" name="g" id="' + p.id + '" /><label for="' + p.id + '">' + p.id + '</label>'; $("#personDiv").append(html); } }); } }); $.net.receive(function(body) { trace("收到:" + JSON.stringify(body)); }); rtc.init(); $("#StartBtn").click(function() { rtc.call(); }); $("#personDiv").delegate("input[type='radio']", "click", function() { rtc.curVideoFriendId=this.value; }); }) </script> </head> <body> <h1>WebRTC实践点对点通信</h1> <video id="localVideo" autoplay playsinline></video> <video id="remoteVideo" autoplay playsinline></video> <input type="button" value="Start" id="StartBtn" /> <input type="button" value="Close" id="CloseBtn" /> <div id="personDiv"></div> </body> </html>

RTC代码

var rtc = (function() { ///通讯是否创建 var isChannelReady = false; ///房间是否创建 var isInitiator = false; var isStarted = false; var remoteStream; ///当前收到对方的房间号 var curReceiveRoomId = ""; var pc = new RTCPeerConnection(null); pc.onicecandidate = function(event) { trace('icecandidate event: ', event); if (event.candidate) { sendMessage({ type: 'candidate', label: event.candidate.sdpMLineIndex, id: event.candidate.sdpMid, candidate: event.candidate.candidate }); } else { trace('End of candidates.'); } }; pc.onaddstream = function(event) { trace('Remote stream added.'); remoteStream = event.stream; document.getElementById("remoteVideo").srcObject = remoteStream; }; $.net.join(function() { ///通道已准备就绪 isChannelReady = true; }) .call(doOffer) .offer(doAnswer) .answer(function(message) { if (isStarted) { pc.setRemoteDescription(new RTCSessionDescription(message)); } }) .candidate(function(message) { if (isStarted) { pc.addIceCandidate(new RTCIceCandidate({ sdpMLineIndex: message.label, candidate: message.candidate })); } }) .bye(function(message) { if (isStarted) { trace('对方连接已断开。'); pc.close(); pc = null; isStarted = false; isInitiator = false; } }); function sendMessage(message) { $.net.meeting(message); } function doCall() { //var toid = $("input[name='g']:checked").val(); var toid = rtc.curVideoFriendId; if (!toid) { alert('暂无目标好友。'); return; } var cmd = { body: 'got user media', roomid: curReceiveRoomId ? curReceiveRoomId : $.net.id }; $.net.send(cmd, toid, 'media'); trace('发送' + JSON.stringify(cmd)); } function doOffer(msg) { if (msg.roomid) { curReceiveRoomId = msg.roomid; trace('收到对方的房间编号 "' + curReceiveRoomId + '" !'); } if (!isChannelReady) { trace('网络通讯没有建立不能发offer'); return; } if (!isInitiator) { trace('不是房间创建者不能发offer'); return; } trace('发送视频通话邀请函offer'); pc.createOffer(function(description) { pc.setLocalDescription(description); //触发onicecandidate事件 trace('setLocalAndSendMessage sending message', description); sendMessage(description); }, trace); isStarted = true; } function doAnswer(message) { trace('Sending answer to peer.'); pc.setRemoteDescription(new RTCSessionDescription(message)); //触发onaddstream事件 pc.createAnswer().then( function(description) { pc.setLocalDescription(description); //触发onicecandidate事件 trace('setLocalAndSendMessage sending message', description); sendMessage(description); }, trace ); } return { curVideoFriendId: '', close: function() { sendMessage('bye'); /* var room = roomService.get(); if(room.id == $.net.id) { trace('房间 "' + room.id + '"已销毁。'); roomService.delete(); }*/ curReceiveRoomId = ""; }, room: function() { //var room = roomService.get(); if (curReceiveRoomId) { $.net.meeting('join', curReceiveRoomId, function(r) { trace('入进房间:', JSON.stringify(r)); isChannelReady = true; }); } else { //room = roomService.create($.net.id); //room = roomService.create('testRoom'); $.net.meeting('create', $.net.id, function(r) { trace('创建房间', JSON.stringify(r)); isInitiator = true; }); } }, video: function(ctrl) { navigator.mediaDevices.getUserMedia({ audio: false, video: true }).then(stream => { trace('本地视频已开启。'); if (stream) { pc.addStream(stream) document.getElementById(ctrl).srcObject = stream; ///本是视频就绪,向对方主播发送call通知 doCall(); } }); }, call: function() { if (!rtc.curVideoFriendId) { alert('没有选择视频通话的目标好友!'); return; } rtc.room(); rtc.video("localVideo"); }, init: function() { window.onbeforeunload = rtc.close; $(document).delegate("#CloseBtn", "click", function() { rtc.close(); }); } } })();

服务端(Nodejs)

var persons = []; var getPersonSeq = function() { var tempSeq = Math.floor(Math.random() * 5) + 1; persons.forEach(p => { if (p.seq == tempSeq) { tempSeq = getPersonSeq(); } }); return tempSeq; } io.sockets.on('connection', function(socket) { if (persons.indexOf(socket.id) == -1) { persons.push({ id: socket.id, seq: getPersonSeq() }); } function trace() { console.log(arguments); } console.log("用户 '" + socket.id + "' 连接成功!"); socket.emit('ready', socket.id, persons); socket.broadcast.emit('change', persons); socket.on('disconnect', function() { trace('终端(' + socket.id + ')已断开。 '); var tempPersons = []; persons.forEach(e => { if (e.id != socket.id) { tempPersons.push(e); } }); persons = tempPersons; //rooms.del(socket.id); socket.broadcast.emit('change', persons); }); socket.on('message', function(body) { var d = new Date(); body.from = socket.id; body.time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds(); socket.to(body.to).emit('message', body); trace('终端(' + socket.id + "):message>", body); }); socket.on('meeting', function(body) { ///向除自己外所有meeting终端广播消息 socket.broadcast.emit('meeting', body); //sockets.in(room).emit('meeting', body); trace('终端(' + socket.id + "):meeting>", body); }); socket.on('create', function(room, callbackfunc) { var clientsInRoom = io.sockets.adapter.rooms[room]; var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; var temproom = { id: room, count: numClients }; socket.join(room); //rooms.save(temproom); if (typeof(callbackfunc) == 'function') { callbackfunc(temproom); } }); socket.on('join', function(room, callbackfunc) { var clientsInRoom = io.sockets.adapter.rooms[room]; var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; var robj = { id: room, count: numClients } switch (numClients) { case 0: trace('终端(' + socket.id + ')进入的房间 ”' + room + '" 不存在!'); socket.emit('empty', room); break; case 1: ///向房间room中发送消息 io.sockets.in(room).emit('join', room); socket.join(room); trace('房间 ' + room + '中现在有' + numClients + '个终端!'); break; case 2: socket.emit('full', room); trace('房间 ”' + room + '" 已满。'); break; } if (typeof(callbackfunc) == 'function') { callbackfunc(robj); } }); socket.on('ipaddr', function() { var ifaces = os.networkInterfaces(); for (var dev in ifaces) { ifaces[dev].forEach(function(details) { if (details.family === 'IPv4' && details.address !== '127.0.0.1') { socket.emit('ipaddr', details.address); } }); } }); });

运行结果

实践历程

1. WebRTC实践简介 2. WebRTC实践获取视频流 3. WebRTC实践传输视频流 4. WebRTC实践信令服务 5. WebRTC实践点对点通信 6. WebRTC实践视频聊天室 7. WebRTC实践总结

最新回复(0)