zoukankan      html  css  js  c++  java
  • 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>
    index4,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+"
    ";
        }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+"
    ";
    }
    
    //设置触发事件
    btnConn.onclick = connSignalServer;    //获取本地音视频数据,展示在页面,socket连接建立与信令服务器,注册信令消息处理函数,发送join信息给信令服务器
    btnLeave.onclick = leave;
    SltBW.onchange = changeBW;
    
    btnSend.onclick = sendText;

    (二)结果显示

    (三)文件传输要点

  • 相关阅读:
    js 运算符优先级
    原生js获取样式
    RGBA 与opacity
    闭包(自己的学习+理解~~水水的)
    css 单位-px、em、rem、百分比
    js之正则1
    querySelector和querySelectorAll
    关于瀑布流的算法(转淘宝ued)
    瀑布流的几个注意点
    jsonp跨域
  • 原文地址:https://www.cnblogs.com/ssyfj/p/14826516.html
Copyright © 2011-2022 走看看