WebRTC学习(十)非音视频数据传输
在前面的学习中,我们传输的数据都是音视频数据,实际上webrtc是一个强大的库,不只可以处理这些音视频数据,还可以处理非音视频数据!比如端对端的聊天,文件的传输(二进制传输也可以),网络的加速...
一:WebRTC传输非音视频数据
(一)createDataChannel API基本格式
(二)Option选项
ordered:传输非音视频数据的时候,数据包是不是按序到达。webrtc在传输音视频数据的时候,使用的RTP协议是基于UDP的,而UDP本身是不保证可达和按序。webrtc在上层中实现了这两个功能
maxPacketLifeTime/maxRetransmits:包存活时间和传输次数(包丢失后,重传次数),两者不相容
negotiated:协商,在创建DataChannel的时候进行协商
id:用于协商时使用的id,标识同一个通道
Options使用案例:
(三)DataChannel事件
onmessage:当对端有数据到达,会触发事件
onopen:当创建好dataChannel后,就会触发该事件
onclose:当dataChannel关闭时触发
onerror:当dataChannel出错时触发
(四)创建RTCDataChannel案例
(五)非音视频数据传输方式
补充:SCTP是流控stream control transport,是UDP的上层协议。流控应用,比如拥塞控制
二:实现端到端文本聊天
(一)代码实现
<html> <head> <title> WebRTC PeerConnection </title> <link href="./css/main.css" rel="stylesheet" /> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script> </head> <body> <div> <button id=connserver>ConnSignal</button> <button id="leave" disabled>Leave</button> </div> <div> <label>BandWidth:</label> <select id="bandwidth" disabled> <!--带宽限制--> <option value="unlimited" selected>unlimited</option> <option value="125">125</option> <option value="250">250</option> <option value="500">500</option> <option value="1000">1000</option> <option value="2000">2000</option> </select> kbps </div> <div id="preview"> <div> <h2>Local:</h2> <video autoplay playsinline id="localvideo"></video> </div> <div> <h2>Remote:</h2> <video autoplay playsinline id="remotevideo"></video> </div> </div> <!--端到端文本聊天--> <div> <h2>Chat:</h2> <textarea id="chat" disabled></textarea> <textarea id="sendtext" disabled></textarea> <button id="send" disabled>Send</button> </div> <div class="graph-container" id="bitrateGraph"> <div>Bitrate</div> <canvas id="bitrateCanvas"></canvas> </div> <div class="graph-container" id="packetGraph"> <div>Packets sent per second</div> <canvas id="packetCanvas"></canvas> </div> </body> <script type="text/javascript" src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script type="text/javascript" src="./js/main4.js"></script> <script type="text/javascript" src="./js/third_party/graph.js"></script> </html>
main4.js
'use strict' var localVideo = document.querySelector("video#localvideo"); var remoteVideo = document.querySelector("video#remotevideo"); var btnConn = document.querySelector("button#connserver"); var btnLeave = document.querySelector("button#leave"); var SltBW = document.querySelector("select#bandwidth"); var textChat = document.querySelector("textarea#chat"); var textSendT = document.querySelector("textarea#sendtext"); var btnSend = document.querySelector("button#send"); //绘制图像,在获取了本地媒体流之后设置 var bitrateGraph; var bitrateSeries; var packetGraph; var packetSeries; var localStream = null; //保存本地流为全局变量 var socket = null; var roomid = "111111"; var state = "init"; //客户端状态机 var pc = null; //定义全局peerconnection变量 var dc = null; //定义全局datachannel变量 var lastResult = null; //全局变量,获取统计值 function sendMessage(roomid,data){ console.log("send SDP message",roomid,data); if(socket){ socket.emit("message",roomid,data); } } function getOffer(desc){ pc.setLocalDescription(desc); sendMessage(roomid,desc); //发送SDP信息到对端 } //这里我们本机是远端,收到了对方的offer,一会需要把自己本端的数据回去!!!!! function getAnswer(desc){ //在offer获取后,设置了远端描述 pc.setLocalDescription(desc); //这里只需要设置本端了 sendMessage(roomid,desc); //本端已经收到offer,开始回复answer,说明本端协商完成 SltBW.disabled = false; } //媒体协商方法,发起方调用,创建offer function call(){ if(state === "joined_conn"){ if(pc){ var options = { offerToReceiveAudio:1, offerToReceiveVideo:1 }; pc.createOffer(options) .then(getOffer) .catch(handleError); } } } //创建peerconnection,监听一些事件:candidate,当收到candidate事件之后(TURN服务返回),之后转发给另外一端(SIGNAL 服务器实现) //将本端的媒体流加入peerconnection中去 function createPeerConnection(){ console.log("Create RTCPeerConnection!"); if(!pc){ //设置ICEservers var pcConfig = { "iceServers" : [{ 'urls':"turn:82.156.184.3:3478", 'credential':"ssyfj", 'username':"ssyfj" }] } pc = new RTCPeerConnection(pcConfig); pc.onicecandidate = (e)=>{ //处理turn服务返回的candidate信息,媒体协商之后SDP规范中属性获取 if(e.candidate){ //发送candidate消息给对端 console.log("find a new candidate",e.candidate); sendMessage(roomid,{ type:"candidate", label:e.candidate.sdpMLineIndex, id:e.candidate.sdpMid, candidate:e.candidate.candidate }); } }; //使得远端监听ondatachannel事件 pc.ondatachannel = (e)=>{ if(!dc){ //注意:进行判断,本端始终会在处理otherjoin中将dc赋值,所以这里的dc赋值只会针对远端 dc = e.channel; dc.onmessage = receivemsg; //复用即可 dc.onopen = dataChannelStateChange; dc.onclose = dataChannelStateChange; } } pc.ontrack = (e)=>{ //获取到远端的轨数据,设置到页面显示 remoteVideo.srcObject = e.streams[0]; } } if(localStream){ //将本端的流加入到peerconnection中去 localStream.getTracks().forEach((track)=>{ pc.addTrack(track,localStream); }); } } //销毁当前peerconnection的流信息 function closeLocalMedia(){ if(localStream && localStream.getTracks()){ localStream.getTracks().forEach((track)=>{ track.stop(); }) } localStream = null; } //关闭peerconnection function closePeerConnection(){ console.log("close RTCPeerConnection"); if(pc){ pc.close(); pc = null; } } function receivemsg(e){ var msg = e.data; //获取了对方传输过来的数据 if(msg){ chat.value +="->"+msg+"\r\n"; }else{ console.error("received msg is null"); } } function dataChannelStateChange(e){ var readyState = dc.readyState; if(readyState === "open"){ //通道打开了 textSendT.disabled = false; btnSend.disabled = false; }else{ //通道关闭了 textSendT.disabled = true; btnSend.disabled = true; } } function conn(){ socket = io.connect(); //与信令服务器建立连接,io对象是在前端引入的socket.io文件创立的全局对象 //开始注册处理服务端的信令消息 socket.on("joined",(roomid,id)=>{ console.log("receive joined message:",roomid,id); //修改状态 state = "joined"; createPeerConnection(); //加入房间后,创建peerconnection,加入流,等到有新的peerconnection加入,就要进行媒体协商 btnConn.disabled = true; btnLeave.disabled = false; console.log("receive joined message:state=",state); }); socket.on("otherjoin",(roomid,id)=>{ console.log("receive otherjoin message:",roomid,id); //修改状态,注意:对于一个特殊状态joined_unbind状态需要创建新的peerconnection if(state === "joined_unbind"){ createPeerConnection(); } //-----------直接在这里实现,不需要去getoffer或者getanswer单独设置 //-----这里是协商negotiated=false,本端创建datachannel,对端监听即可 dc = pc.createDataChannel("chat"); //没有设置可选项 dc.onmessage = receivemsg; dc.onopen = dataChannelStateChange; dc.onclose = dataChannelStateChange; state = "joined_conn"; //原本joined,现在变为conn //媒体协商 call(); console.log("receive otherjoin message:state=",state); }); socket.on("full",(roomid,id)=>{ console.log("receive full message:",roomid,id); state = "leaved"; console.log("receive full message:state=",state); socket.disconnect(); //断开连接,虽然没有加入房间,但是连接还是存在的,所以需要进行关闭 alert("the room is full!"); btnLeave.disabled = true; btnConn.disabled = false; }); socket.on("leaved",(roomid,id)=>{ //------资源的释放在发送leave消息给服务器的时候就释放了,符合离开流程图 console.log("receive leaved message:",roomid,id); state = "leaved"; //初始状态 console.log("receive leaved message:state=",state); //这里断开连接 socket.disconnect(); btnLeave.disabled = true; btnConn.disabled = false; }); socket.on("bye",(roomid,id)=>{ console.log("receive bye message:",roomid,id); state = "joined_unbind"; console.log("receive bye message:state=",state); //开始处理peerconneciton closePeerConnection(); }); socket.on("message",(roomid,data)=>{ console.log("receive client message:",roomid,data); //处理媒体协商数据,进行转发给信令服务器,处理不同类型的数据,如果是流媒体数据,直接p2p转发 if(data){ //只有下面3种数据,对于媒体流数据,走的是p2p路线,不经过信令服务器中转 if(data.type === "offer"){ //这里表示我们本机是远端,收到了对方的offer,一会需要把自己本端的数据回去!!!!! pc.setRemoteDescription(new RTCSessionDescription(data)); //需要把传输过来的文本转对象 pc.createAnswer() .then(getAnswer) .catch(handleError); }else if(data.type === "answer"){ pc.setRemoteDescription(new RTCSessionDescription(data)); //收到对端发送过来的SDP信息,说明协商完成 SltBW.disabled = false; }else if(data.type === "candidate"){ //在双方设置完成setLocalDescription之后,双方开始交换candidate,每当收集一个candidate之后都会触发pc的onicecandidate事件 var candidate = new RTCIceCandidate({ sdpMLineIndex:data.label, //媒体行的行号 m=video ... candidate:data.candidate }); //生成candidate,是从TURN/STUN服务端获取的,下面开始添加到本地pc中去,用于发送到远端 //将candidate添加到pc pc.addIceCandidate(candidate); //发送到对端,触发对端onicecandidate事件 }else{ console.error("the message is invalid!",data); } } }); //开始发送加入消息 socket.emit("join",roomid); return; } function getMediaStream(stream){ localStream = stream; //保存到全局变量,用于传输到对端 localVideo.srcObject = localStream; //显示在页面中,本端 //-------与signal server进行连接,接受信令消息!!------ conn(); //绘制图像,渲染显示 bitrateSeries = new TimelineDataSeries(); bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas'); bitrateGraph.updateEndDate(); packetSeries = new TimelineDataSeries(); packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas'); packetGraph.updateEndDate(); } function handleError(err){ console.error(err.name+":"+err.message); } //初始化操作,获取本地音视频数据 function start(){ if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ console.error("the getUserMedia is not support!"); return; }else{ var constraints = { video : true, audio : false }; navigator.mediaDevices.getUserMedia(constraints) .then(getMediaStream) .catch(handleError); } } function connSignalServer(){ //开启本地视频 start(); return true; } function leave(){ if(socket){ socket.emit("leave",roomid); } //释放资源 closePeerConnection(); closeLocalMedia(); btnConn.disabled = false; btnLeave.disabled = true; } function changeBW(){ SltBW.disabled = true; var bw = SltBW.options[SltBW.selectedIndex].value; if(bw==="unlimited"){ return; } //获取所有的发送器 var senders = pc.getSenders(); var vdsender = null; //开始对视频流进行限流 senders.forEach((sender)=>{ if(sender && sender.track &&sender.track.kind === "video"){ vdsender = sender; //获取到视频流的sender } }); //获取参数 var parameters = vdsender.getParameters(); if(!parameters.encodings){ //从编解码器中设置最大码率 return; } parameters.encodings[0].maxBitrate = bw*1000; vdsender.setParameters(parameters) .then(()=>{ SltBW.disabled = false; console.log("Success to set parameters"); }) .catch(handleError); } //设置定时器,每秒触发 window.setInterval(()=>{ if(!pc || !pc.getSenders()) return; var sender = pc.getSenders()[0]; //因为我们只有视频流,所以不进行判断,直接去取 if(!sender){ return; } sender.getStats() .then((reports)=>{ reports.forEach((report)=>{ if(report.type === "outbound-rtp"){ //获取输出带宽 if(report.isRemote){ //表示是远端的数据,我们只需要自己本端的 return; } var curTs = report.timestamp; var bytes = report.bytesSent; var packets = report.packetsSent; //上面的bytes和packets是累计值。我们只需要差值 if(lastResult && lastResult.has(report.id)){ var biterate = 8*(bytes-lastResult.get(report.id).bytesSent)/(curTs-lastResult.get(report.id).timestamp); var packetCnt = packets - lastResult.get(report.id).packetsSent; bitrateSeries.addPoint(curTs,biterate); bitrateGraph.setDataSeries([bitrateSeries]); bitrateGraph.updateEndDate(); packetSeries.addPoint(curTs,packetCnt); packetGraph.setDataSeries([packetSeries]); packetGraph.updateEndDate(); } } }); lastResult = reports; }) .catch(handleError); },1000); //本端发送 function sendText(){ //发送非音视频数据 var data = textSendT.value; if(data){ dc.send(data); //datachannel,在双方协商好之后创建 } textSendT.value = ""; chat.value += "<-"+data+"\r\n"; } //设置触发事件 btnConn.onclick = connSignalServer; //获取本地音视频数据,展示在页面,socket连接建立与信令服务器,注册信令消息处理函数,发送join信息给信令服务器 btnLeave.onclick = leave; SltBW.onchange = changeBW; btnSend.onclick = sendText;
(二)结果显示
(三)文件传输要点