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;

(二)结果显示

(三)文件传输要点

原文地址:https://www.cnblogs.com/ssyfj/p/14826516.html