WebRTC学习(四)

RTP Media

  • Receiver(接收器)
    • getReceivers:获得一组RTCRtpReceiver对象,用于接收数据
  • Sender(发送器)
    • getSenders:获取一组RTCRtpSender对象,用于发送数据,每个对象对应一个媒体轨

RTCRtpReceiver/RTCRtpSender属性

  • MediaStreamTrack 媒体轨
  • RTCDtlsTransport Transport媒体数据传输相关的属性
  • RTCDtlsTransport rtcpTransport与rtcp传输相关的属性

RTCRtpReceiver

  • getParameters 返回RTCRtpParameter对象
  • getSynchronizationSources 返回一组SynchronizationSources实例
  • getContributingSources 返回一组ContributingSources实例
  • getStats RTCStatsReport里面包含输入流统计信息
  • getCapabilities 返回系统能接收的媒体能力(音频,视频)

RTCRtpSender属性

  • getParameters 返回RTCPtpParameter对象
  • setParameters 设置RTCP传输相关的参数
  • getStats 提供了输出流统计信息
  • replaceTrack 用另一个track替换现在的track,如切换摄像头
  • getCapabilities 按类型(音视频)返回系统发送媒体的能力
1.png

RTCRtpTransceiver

  • getTransceiver

从PC获得一组RTCRtpTransceiver对象,每个RTCRtpTransceiver是RTCRtpSender和RTCRtpReciver对
-方法
stop:停止发送和接收媒体数据

控制传输速率

2.png
3.png
4.png
5.png
'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 optBw = document.querySelector('select#bandwidth');

var chat = document.querySelector('textarea#chat');
var send_txt = document.querySelector('textarea#sendtxt');
var btnSend = document.querySelector('button#send');

var bitrateGraph;
var bitrateSeries;

var packetGraph;
var packetSeries;

var lastResult;

var pcConfig = {
  'iceServers': [{
    'urls': 'turn:stun.al.learningrtc.cn:3478',
    'credential': "mypasswd",
    'username': "garrylea"
  }]
};

var localStream = null;
var remoteStream = null;

var pc = null;
var dc = null;

var roomid;
var socket = null;

var offerdesc = null;
var state = 'init';



function sendMessage(roomid, data){

    console.log('send message to other end', roomid, data);
    if(!socket){
        console.log('socket is null');
    }
    socket.emit('message', roomid, data);
}

function dataChannelStateChange() {
  var readyState = dc.readyState;
  console.log('Send channel state is: ' + readyState);
  if (readyState === 'open') {
    send_txt.disabled = false;
    send.disabled = false;
  } else {
    send_txt.disabled = true;
    send.disabled = true;
  }
}

function conn(){

    socket = io.connect();

    socket.on('joined', (roomid, id) => {
        console.log('receive joined message!', roomid, id);
        state = 'joined'

        //如果是多人的话,第一个人不该在这里创建peerConnection
        //都等到收到一个otherjoin时再创建
        //所以,在这个消息里应该带当前房间的用户数
        //
        //create conn and bind media track
        createPeerConnection();
        bindTracks();

        btnConn.disabled = true;
        btnLeave.disabled = false;

        console.log('receive joined message, state=', state);
    });

    socket.on('otherjoin', (roomid) => {
        console.log('receive joined message:', roomid, state);

        //如果是多人的话,每上来一个人都要创建一个新的 peerConnection
        //
        if(state === 'joined_unbind'){
            createPeerConnection();
            bindTracks();
        }

        //create data channel for transporting non-audio/video data
        dc = pc.createDataChannel('chatchannel');
        dc.onmessage = receivemsg;
        dc.onopen = dataChannelStateChange;
        dc.onclose = dataChannelStateChange;

        state = 'joined_conn';
        call();

        console.log('receive other_join message, state=', state);
    });

    socket.on('full', (roomid, id) => {
        console.log('receive full message', roomid, id);
        socket.disconnect();
        hangup();
        closeLocalMedia();
        state = 'leaved';
        console.log('receive full message, state=', state);
        alert('the room is full!');
    });

    socket.on('leaved', (roomid, id) => {
        console.log('receive leaved message', roomid, id);
        state='leaved'
        socket.disconnect();
        console.log('receive leaved message, state=', state);

        btnConn.disabled = false;
        btnLeave.disabled = true;
        optBw.disabled = true;
    });

    socket.on('bye', (room, id) => {
        console.log('receive bye message', roomid, id);
        //state = 'created';
        //当是多人通话时,应该带上当前房间的用户数
        //如果当前房间用户不小于 2, 则不用修改状态
        //并且,关闭的应该是对应用户的peerconnection
        //在客户端应该维护一张peerconnection表,它是
        //一个key:value的格式,key=userid, value=peerconnection
        state = 'joined_unbind';
        hangup();
        console.log('receive bye message, state=', state);
    });

    socket.on('disconnect', (socket) => {
        console.log('receive disconnect message!', roomid);
        if(!(state === 'leaved')){
            hangup();
            closeLocalMedia();

        }
        state = 'leaved';

        btnConn.disabled = false;
        btnLeave.disabled = true;
        optBw.disabled = true;
    
    });

    socket.on('message', (roomid, data) => {
        console.log('receive message!', roomid, data);

        if(data === null || data === undefined){
            console.error('the message is invalid!');
            return; 
        }

        if(data.hasOwnProperty('type') && data.type === 'offer') {
            
            pc.setRemoteDescription(new RTCSessionDescription(data));
            //create answer
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleAnswerError);

        }else if(data.hasOwnProperty('type') && data.type === 'answer'){
            optBw.disabled = false
            pc.setRemoteDescription(new RTCSessionDescription(data));
        
        }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
            var candidate = new RTCIceCandidate({
                sdpMLineIndex: data.label,
                candidate: data.candidate
            });
            pc.addIceCandidate(candidate)
                .then(()=>{
                    console.log('Successed to add ice candidate');  
                })
                .catch(err=>{
                    console.error(err); 
                });
        
        }else{
            console.log('the message is invalid!', data);
        
        }
    
    });


    roomid = '111111'; 
    socket.emit('join', roomid);

    return true;
}

function connSignalServer(){
    
    //开启本地视频
    start();

    return true;
}

function getMediaStream(stream){

    localStream = stream;   
    localVideo.srcObject = localStream;

    //这个函数的位置特别重要,
    //一定要放到getMediaStream之后再调用
    //否则就会出现绑定失败的情况
    
    //setup connection
    conn();

    bitrateSeries = new TimelineDataSeries();
    bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
    bitrateGraph.updateEndDate();

    packetSeries = new TimelineDataSeries();
    packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
    packetGraph.updateEndDate();
}

function getDeskStream(stream){
    localStream = stream;
}

function handleError(err){
    console.error('Failed to get Media Stream!', err);
}

function shareDesk(){

    if(IsPC()){
        navigator.mediaDevices.getDisplayMedia({video: true})
            .then(getDeskStream)
            .catch(handleError);

        return true;
    }

    return false;

}

function start(){

    if(!navigator.mediaDevices ||
        !navigator.mediaDevices.getUserMedia){
        console.error('the getUserMedia is not supported!');
        return;
    }else {

        var constraints = {
            video: true,
            audio: false 
        }

        navigator.mediaDevices.getUserMedia(constraints)
                    .then(getMediaStream)
                    .catch(handleError);
    }

}

function getRemoteStream(e){
    remoteStream = e.streams[0];
    remoteVideo.srcObject = e.streams[0];
}

function handleOfferError(err){
    console.error('Failed to create offer:', err);
}

function handleAnswerError(err){
    console.error('Failed to create answer:', err);
}

function getAnswer(desc){
    pc.setLocalDescription(desc);

    optBw.disabled = false;
    //send answer sdp
    sendMessage(roomid, desc);
}

function getOffer(desc){
    pc.setLocalDescription(desc);
    offerdesc = desc;

    //send offer sdp
    sendMessage(roomid, offerdesc); 

}

function receivemsg(e){
    var msg = e.data;
    if(msg){
        console.log(msg);
        chat.value += "->" + msg + "\r\n";
    }else{
        console.error('received msg is null');
    }
}

function createPeerConnection(){

    //如果是多人的话,在这里要创建一个新的连接.
    //新创建好的要放到一个map表中。
    //key=userid, value=peerconnection
    console.log('create RTCPeerConnection!');
    if(!pc){
        pc = new RTCPeerConnection(pcConfig);

        pc.onicecandidate = (e)=>{

            if(e.candidate) {
                sendMessage(roomid, {
                    type: 'candidate',
                    label:event.candidate.sdpMLineIndex, 
                    id:event.candidate.sdpMid, 
                    candidate: event.candidate.candidate
                });
            }else{
                console.log('this is the end candidate');
            }
        }

        pc.ondatachannel = e=> {
            if(!dc){
                dc = e.channel;
                dc.onmessage = receivemsg; 
                dc.onopen = dataChannelStateChange;
                dc.onclose = dataChannelStateChange;
            }

        }

        pc.ontrack = getRemoteStream;
    }else {
        console.log('the pc have be created!');
    }

    return; 
}

//绑定永远与 peerconnection在一起,
//所以没必要再单独做成一个函数
function bindTracks(){

    console.log('bind tracks into RTCPeerConnection!');

    if( pc === null && localStream === undefined) {
        console.error('pc is null or undefined!');
        return;
    }

    if(localStream === null && localStream === undefined) {
        console.error('localstream is null or undefined!');
        return;
    }

    //add all track into peer connection
    localStream.getTracks().forEach((track)=>{
        pc.addTrack(track, localStream);    
    });

}

function call(){
    
    if(state === 'joined_conn'){

        var offerOptions = {
            offerToRecieveAudio: 1,
            offerToRecieveVideo: 1
        }

        pc.createOffer(offerOptions)
            .then(getOffer)
            .catch(handleOfferError);
    }
}

function hangup(){

    if(!pc) {
        return;
    }

    offerdesc = null;
    
    pc.close();
    pc = null;

}

function closeLocalMedia(){

    if(!(localStream === null || localStream === undefined)){
        localStream.getTracks().forEach((track)=>{
            track.stop();
        });
    }
    localStream = null;
}

function leave() {

    socket.emit('leave', roomid); //notify server

    dc.close();
    dc = null;

    hangup();
    closeLocalMedia();

    btnConn.disabled = false;
    btnLeave.disabled = true;
    optBw.disabled = true;

    send_txt.disabled = true;
    send.disabled = true;
}

function chang_bw()
{
    optBw.disabled = true;
    var bw = optBw.options[optBw.selectedIndex].value;

    var vsender = null;
    var senders = pc.getSenders();

    senders.forEach( sender => {
        if(sender && sender.track.kind === 'video'){
            vsender = sender;   
        }   
    });

    var parameters = vsender.getParameters();
    if(!parameters.encodings){
        return; 
    }

    if(bw === 'unlimited'){
        return; 
    }

    parameters.encodings[0].maxBitrate = bw * 1000;

    vsender.setParameters(parameters)
        .then(()=>{
            optBw.disabled = false;
            console.log('Successed to set parameters!');
        })
        .catch(err => {
            console.error(err);
        })
}

// query getStats every second
window.setInterval(() => {
  if (!pc) {
    return;
  }
  const sender = pc.getSenders()[0];
  if (!sender) {
    return;
  }
  sender.getStats().then(res => {
    res.forEach(report => {
      let bytes;
      let packets;
      if (report.type === 'outbound-rtp') {
        if (report.isRemote) {
          return;
        }
        const now = report.timestamp;
        bytes = report.bytesSent;
        packets = report.packetsSent;
        if (lastResult && lastResult.has(report.id)) {
          // calculate bitrate
          const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /
            (now - lastResult.get(report.id).timestamp);

          // append to chart
          bitrateSeries.addPoint(now, bitrate);
          bitrateGraph.setDataSeries([bitrateSeries]);
          bitrateGraph.updateEndDate();

          // calculate number of packets and append to chart
          packetSeries.addPoint(now, packets -
            lastResult.get(report.id).packetsSent);
          packetGraph.setDataSeries([packetSeries]);
          packetGraph.updateEndDate();
        }
      }
    });
    lastResult = res;
  });
}, 1000);

function sendText(){
    var data = send_txt.value;
    if(data != null){
        dc.send(data);
    }

    //更好的展示
    send_txt.value = "";
    chat.value += '<- ' + data + '\r\n';
}

btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
optBw.onchange = chang_bw;

btnSend.onclick = sendText;
<html>
    <head>
        <title>WebRTC PeerConnection</title>
        <link href="./css/main.css" rel="stylesheet" />
    </head>

    <body>
        <div>

            <div>
                <button id="connserver">Connect Sig Server</button>
                <button id="leave" disabled>Leave</button>  
            </div>

            <div>
                <label>BandWidth:</label>
                <select id="bandwidth" disabled>
                    <option value="unlimited" selected>unlimited</option>
                    <option value="2000">2000</option>
                    <option value="1000">1000</option>
                    <option value="500">500</option>
                    <option value="250">250</option>
                    <option value="125">125</option>
                </select>
                kbps
            </div>


            <div class="preview">
                <div>
                    <h2>Local:</h2>
                    <video id="localvideo" autoplay playsinline muted></video>

                    <h2>Remote:</h2>
                    <video id="remotevideo" autoplay playsinline></video>
                </div>
                <div>
                    <h2>Chat:<h2>
                    <textarea id="chat" disabled></textarea>
                    <textarea id="sendtxt" disabled></textarea>
                    <button id="send" disabled>Send</button>
                </div>

            </div>

            <div class="preview">
                <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>
            </div>

        </div>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        <script src="js/main_bw.js"></script>
        <script src="js/third_party/graph.js"></script>
    </body>
</html>
//注意:graph.js是第三方库,用于绘制实时传输速率曲线图
/*
 *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */

button {
  margin: 10px 20px 25px 0;
  vertical-align: top;
  width: 134px;
}

table {
  margin: 200px (50% - 100) 0 0; 
}

textarea#chat {
  color: #444;
  font-size: 0.9em;
  font-weight: 300;
  height: 325px; 
  margin: 5px;
  padding: 5px;
  width: calc(100% - 10px);
}

textarea#sendtxt {
  color: #444;
  font-size: 0.9em;
  font-weight: 300;
  height: 125px; 
  margin: 5px;
  padding: 5px;
  width: calc(100% - 10px);
}

div#getUserMedia {
  padding: 0 0 8px 0;
}

div.input {
  display: inline-block;
  margin: 0 4px 0 0;
  vertical-align: top;
  width: 310px;
}

div.input > div {
  margin: 0 0 20px 0;
  vertical-align: top;
}

div.output {
  background-color: #eee;
  display: inline-block;
  font-family: 'Inconsolata', 'Courier New', monospace;
  font-size: 0.9em;
  padding: 10px 10px 10px 25px;
  position: relative;
  top: 10px;
  white-space: pre;
  width: 270px;
}

div.label {
    display: inline-block;
    font-weight: 400;
    width: 120px;
}

div.graph-container {
  background-color: #ccc;
  float: left;
  margin: 10px;
}

div.preview {
  border-bottom: 1px solid #eee;
  margin: 0 0 1em 0;
  padding: 0 0 0.5em 0;
}

div.preview > div {
  display: inline-block;
  vertical-align: top;
  width: calc(50% - 40px);
}

section#statistics div {
  display: inline-block;
  font-family: 'Inconsolata', 'Courier New', monospace;
  vertical-align: top;
  width: 308px;
}

section#statistics div#senderStats {
  margin: 0 20px 0 0;
}

section#constraints > div {
  margin: 0 0 20px 0;
}

h2 {
  margin: 0 0 1em 0;
}


section#constraints label {
  display: inline-block;
  width: 156px;
}

section {
  margin: 0 0 20px 0;
  padding: 0 0 15px 0;
}

video {
  background: #222;
  margin: 0 0 0 0;
  --width: 100%;
  width: var(--width);
  height: 225px;
}

@media screen and (max-width: 720px) {
  button {
    font-weight: 500;
    height: 56px;
    line-height: 1.3em;
    width: 90px;
  }

  div#getUserMedia {
    padding: 0 0 40px 0;
  }

  section#statistics div {
    width: calc(50% - 14px);
  }

}

server.js和下面的案例demo共用一个

传输非音视频数据

  • 基本格式

aPromise=pc.createDataChannel(label[,options]);

  • 参数
    • label:人类可读的字符串
    • options:可选的

Option选项

ordered:包乱序了重排序
maxPacketLifeTime/maxRetransmits: 包传输时间和重传递次数,二选一
negotiated:
    如果为false,一端使用createDataChannel创建通道,另一端监听ondatachannel事件
    如果为true,两端都可以使用createDataChannel创建通道,通过id来标识同一个通道
id:就是上面说的id

DataChannel事件

onmessage:对方有数据过来的时候触发
onopen:创建好DataChannel时候触发
onclose:DataChannel关闭的时候触发
onerror:DataChannel发生错误的时候触发

demo注意点

  • 创建datachannel的时机与文本聊天一样
  • 通过js的FileReader从文本中读取数据
  • 以数据块为单位发送数据
  • 发送数据先要将文件的基本信息以信令方式通知对方
'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 optBw = document.querySelector('select#bandwidth');

const bitrateDiv = document.querySelector('div#bitrate');
const fileInput = document.querySelector('input#fileInput');

const statusMessage = document.querySelector('span#status');
const downloadAnchor = document.querySelector('a#download');

const sendProgress = document.querySelector('progress#sendProgress');
const receiveProgress = document.querySelector('progress#receiveProgress');

const btnSendFile = document.querySelector('button#sendFile');
const btnAbort = document.querySelector('button#abortButton');

var bitrateGraph;
var bitrateSeries;

var packetGraph;
var packetSeries;

var lastResult;

var pcConfig = {
  'iceServers': [{
    'urls': 'turn:stun.al.learningrtc.cn:3478',
    'credential': "mypasswd",
    'username': "garrylea"
  }]
};

var localStream = null;
var remoteStream = null;

var pc = null;
var dc = null;

var roomid;
var socket = null;

var offerdesc = null;
var state = 'init';

var fileReader = null;

var fileName = "";
var fileSize = 0;
var lastModifyTime = 0;
var fileType = "data";

var receiveBuffer = [];
var receivedSize = 0;

function sendMessage(roomid, data){

    console.log('send message to other end', roomid, data);
    if(!socket){
        console.log('socket is null');
    }
    socket.emit('message', roomid, data);
}

function sendData(){

    var offset = 0;
    var chunkSize = 16384;
    var file = fileInput.files[0];
    console.log(`File is ${[file.name, file.size, file.type, file.lastModified].join(' ')}`);


    // Handle 0 size files.
    statusMessage.textContent = '';
    downloadAnchor.textContent = '';
    if (file.size === 0) {
        bitrateDiv.innerHTML = '';
        statusMessage.textContent = 'File is empty, please select a non-empty file';
        return;
    }

    sendProgress.max = file.size;

    fileReader = new FileReader();
    fileReader.onerror = error => console.error('Error reading file:', error);
    fileReader.onabort = event => console.log('File reading aborted:', event);
    fileReader.onload = e => {
        console.log('FileRead.onload ', e);
        dc.send(e.target.result);
        offset += e.target.result.byteLength;
        sendProgress.value = offset;
        if (offset < file.size) {
            readSlice(offset);
        }
    }

    var readSlice = o => {
        console.log('readSlice ', o);
        const slice = file.slice(offset, o + chunkSize);
        fileReader.readAsArrayBuffer(slice);
    };

    readSlice(0);

}

function dataChannelStateChange(){
    if(dc){
        var readyState = dc.readyState;
        console.log('Send channel state is: ' + readyState);
        if (readyState === 'open') {
            fileInput.disabled = false;
        } else {
            fileInput.disabled = true;
        }
    }else{
        fileInput.disabled = true;  
    }
}

function conn(){

    socket = io.connect();

    socket.on('joined', (roomid, id) => {
        console.log('receive joined message!', roomid, id);
        state = 'joined'

        //如果是多人的话,第一个人不该在这里创建peerConnection
        //都等到收到一个otherjoin时再创建
        //所以,在这个消息里应该带当前房间的用户数
        //
        //create conn and bind media track
        createPeerConnection();
        bindTracks();

        btnConn.disabled = true;
        btnLeave.disabled = false;

        console.log('receive joined message, state=', state);
    });

    socket.on('otherjoin', (roomid) => {
        console.log('receive joined message:', roomid, state);

        //如果是多人的话,每上来一个人都要创建一个新的 peerConnection
        //
        if(state === 'joined_unbind'){
            createPeerConnection();
            bindTracks();
        }

        //create data channel for transporting non-audio/video data
        dc = pc.createDataChannel('chatchannel');
        dc.onmessage = receivemsg;
        dc.onopen = dataChannelStateChange;
        dc.onclose = dataChannelStateChange;

        state = 'joined_conn';
        call();

        console.log('receive other_join message, state=', state);
    });

    socket.on('full', (roomid, id) => {
        console.log('receive full message', roomid, id);
        socket.disconnect();
        hangup();
        closeLocalMedia();
        state = 'leaved';
        console.log('receive full message, state=', state);
        alert('the room is full!');
    });

    socket.on('leaved', (roomid, id) => {
        console.log('receive leaved message', roomid, id);
        state='leaved'
        socket.disconnect();
        console.log('receive leaved message, state=', state);

        btnConn.disabled = false;
        btnLeave.disabled = true;
        optBw.disabled = true;
    });

    socket.on('bye', (room, id) => {
        console.log('receive bye message', roomid, id);
        //state = 'created';
        //当是多人通话时,应该带上当前房间的用户数
        //如果当前房间用户不小于 2, 则不用修改状态
        //并且,关闭的应该是对应用户的peerconnection
        //在客户端应该维护一张peerconnection表,它是
        //一个key:value的格式,key=userid, value=peerconnection
        state = 'joined_unbind';
        hangup();
        console.log('receive bye message, state=', state);
    });

    socket.on('disconnect', (socket) => {
        console.log('receive disconnect message!', roomid);
        if(!(state === 'leaved')){
            hangup();
            closeLocalMedia();

        }
        state = 'leaved';

        btnConn.disabled = false;
        btnLeave.disabled = true;
        optBw.disabled = true;
    
    });

    socket.on('message', (roomid, data) => {
        console.log('receive message!', roomid, data);

        if(data === null || data === undefined){
            console.error('the message is invalid!');
            return; 
        }

        if(data.hasOwnProperty('type') && data.type === 'offer') {
            
            pc.setRemoteDescription(new RTCSessionDescription(data));
            //create answer
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleAnswerError);

        }else if(data.hasOwnProperty('type') && data.type === 'answer'){
            optBw.disabled = false
            pc.setRemoteDescription(new RTCSessionDescription(data));
        
        }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
            var candidate = new RTCIceCandidate({
                sdpMLineIndex: data.label,
                candidate: data.candidate
            });
            pc.addIceCandidate(candidate)
                .then(()=>{
                    console.log('Successed to add ice candidate');  
                })
                .catch(err=>{
                    console.error(err); 
                });
        
        }else if(data.hasOwnProperty('type') && data.type === 'fileinfo'){
            fileName = data.name;
            fileType = data.filetype;
            fileSize = data.size;
            lastModifyTime = data.lastModify;   
            receiveProgress.max = fileSize;
        }else{
            console.log('the message is invalid!', data);
        
        }
    
    });


    roomid = '111111'; 
    socket.emit('join', roomid);

    return true;
}

function connSignalServer(){
    
    //开启本地视频
    start();

    return true;
}

function getMediaStream(stream){

    localStream = stream;   
    localVideo.srcObject = localStream;

    //这个函数的位置特别重要,
    //一定要放到getMediaStream之后再调用
    //否则就会出现绑定失败的情况
    
    //setup connection
    conn();

    bitrateSeries = new TimelineDataSeries();
    bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
    bitrateGraph.updateEndDate();

    packetSeries = new TimelineDataSeries();
    packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
    packetGraph.updateEndDate();
}

function getDeskStream(stream){
    localStream = stream;
}

function handleError(err){
    console.error('Failed to get Media Stream!', err);
}

function shareDesk(){

    if(IsPC()){
        navigator.mediaDevices.getDisplayMedia({video: true})
            .then(getDeskStream)
            .catch(handleError);

        return true;
    }

    return false;

}

function start(){

    if(!navigator.mediaDevices ||
        !navigator.mediaDevices.getUserMedia){
        console.error('the getUserMedia is not supported!');
        return;
    }else {

        var constraints = {
            video: true,
            audio: false 
        }

        navigator.mediaDevices.getUserMedia(constraints)
                    .then(getMediaStream)
                    .catch(handleError);
    }

}

function getRemoteStream(e){
    remoteStream = e.streams[0];
    remoteVideo.srcObject = e.streams[0];
}

function handleOfferError(err){
    console.error('Failed to create offer:', err);
}

function handleAnswerError(err){
    console.error('Failed to create answer:', err);
}

function getAnswer(desc){
    pc.setLocalDescription(desc);

    optBw.disabled = false;
    //send answer sdp
    sendMessage(roomid, desc);
}

function getOffer(desc){
    pc.setLocalDescription(desc);
    offerdesc = desc;

    //send offer sdp
    sendMessage(roomid, offerdesc); 

}

function receivemsg(e){

    console.log(`Received Message ${event.data.byteLength}`);
    receiveBuffer.push(event.data);
    receivedSize += event.data.byteLength;

    receiveProgress.value = receivedSize;

    if (receivedSize === fileSize) {
        var received = new Blob(receiveBuffer);
        receiveBuffer = [];

        downloadAnchor.href = URL.createObjectURL(received);
        downloadAnchor.download = fileName;
        downloadAnchor.textContent =
            `Click to download '${fileName}' (${fileSize} bytes)`;
        downloadAnchor.style.display = 'block';
    }
}

function createPeerConnection(){

    //如果是多人的话,在这里要创建一个新的连接.
    //新创建好的要放到一个map表中。
    //key=userid, value=peerconnection
    console.log('create RTCPeerConnection!');
    if(!pc){
        pc = new RTCPeerConnection(pcConfig);

        pc.onicecandidate = (e)=>{

            if(e.candidate) {
                sendMessage(roomid, {
                    type: 'candidate',
                    label:event.candidate.sdpMLineIndex, 
                    id:event.candidate.sdpMid, 
                    candidate: event.candidate.candidate
                });
            }else{
                console.log('this is the end candidate');
            }
        }

        pc.ondatachannel = e=> {
            if(!dc){
                dc = e.channel;
                dc.onmessage = receivemsg; 
                dc.onopen = dataChannelStateChange;
                dc.onclose = dataChannelStateChange; 
            }

        }

        pc.ontrack = getRemoteStream;
    }else {
        console.log('the pc have be created!');
    }

    return; 
}

//绑定永远与 peerconnection在一起,
//所以没必要再单独做成一个函数
function bindTracks(){

    console.log('bind tracks into RTCPeerConnection!');

    if( pc === null && localStream === undefined) {
        console.error('pc is null or undefined!');
        return;
    }

    if(localStream === null && localStream === undefined) {
        console.error('localstream is null or undefined!');
        return;
    }

    //add all track into peer connection
    localStream.getTracks().forEach((track)=>{
        pc.addTrack(track, localStream);    
    });

}

function call(){
    
    if(state === 'joined_conn'){

        var offerOptions = {
            offerToRecieveAudio: 1,
            offerToRecieveVideo: 1
        }

        pc.createOffer(offerOptions)
            .then(getOffer)
            .catch(handleOfferError);
    }
}

function hangup(){

    if(!pc) {
        return;
    }

    offerdesc = null;
    
    pc.close();
    pc = null;

}

function closeLocalMedia(){

    if(!(localStream === null || localStream === undefined)){
        localStream.getTracks().forEach((track)=>{
            track.stop();
        });
    }
    localStream = null;
}

function leave() {

    socket.emit('leave', roomid); //notify server

    dc.close();
    dc = null;

    hangup();
    closeLocalMedia();

    btnConn.disabled = false;
    btnLeave.disabled = true;
    optBw.disabled = true;

    btnSendFile.disabled = true;
    btnAbort.disabled = true;

}

function chang_bw()
{
    optBw.disabled = true;
    var bw = optBw.options[optBw.selectedIndex].value;

    var vsender = null;
    var senders = pc.getSenders();

    senders.forEach( sender => {
        if(sender && sender.track.kind === 'video'){
            vsender = sender;   
        }   
    });

    var parameters = vsender.getParameters();
    if(!parameters.encodings){
        return; 
    }

    if(bw === 'unlimited'){
        return; 
    }

    parameters.encodings[0].maxBitrate = bw * 1000;

    vsender.setParameters(parameters)
        .then(()=>{
            optBw.disabled = false;
            console.log('Successed to set parameters!');
        })
        .catch(err => {
            console.error(err);
        })
}

// query getStats every second
window.setInterval(() => {
  if (!pc) {
    return;
  }
  const sender = pc.getSenders()[0];
  if (!sender) {
    return;
  }
  sender.getStats().then(res => {
    res.forEach(report => {
      let bytes;
      let packets;
      if (report.type === 'outbound-rtp') {
        if (report.isRemote) {
          return;
        }
        const now = report.timestamp;
        bytes = report.bytesSent;
        packets = report.packetsSent;
        if (lastResult && lastResult.has(report.id)) {
          // calculate bitrate
          const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /
            (now - lastResult.get(report.id).timestamp);

          // append to chart
          bitrateSeries.addPoint(now, bitrate);
          bitrateGraph.setDataSeries([bitrateSeries]);
          bitrateGraph.updateEndDate();

          // calculate number of packets and append to chart
          packetSeries.addPoint(now, packets -
            lastResult.get(report.id).packetsSent);
          packetGraph.setDataSeries([packetSeries]);
          packetGraph.updateEndDate();
        }
      }
    });
    lastResult = res;
  });
}, 1000);


function sendfile(){
    sendData();
    btnSendFile.disabled = true;
}

function abort(){
    if(fileReader && fileReader.readyState === 1){
        console.log('abort read');
        fileReader.abort();
    }

}

function handleFileInputChange() {
    var file = fileInput.files[0];
    if (!file) {
        console.log('No file chosen');
    } else {
        fileName = file.name;
        fileSize = file.size;
        fileType = file.type;
        lastModifyTime = file.lastModified;

        sendMessage(roomid, {
            type: 'fileinfo',
            name: file.name,
            size: file.size,
            filetype: file.type,
            lastmodify: file.lastModified
        });

        btnSendFile.disabled = false;
        sendProgress.value = 0;
        receiveProgress.value = 0;

        receiveBuffer = [];
        receivedSize = 0;
    }
}

btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
optBw.onchange = chang_bw;

btnSendFile.onclick=sendfile;
btnAbort.onclick=abort;
fileInput.onchange = handleFileInputChange;
<html>
    <head>
        <title>WebRTC PeerConnection</title>
        <link href="./css/main.css" rel="stylesheet" />
    </head>

    <body>
        <div>

            <div>
                <button id="connserver">Connect Sig Server</button>
                <button id="leave" disabled>Leave</button>  
            </div>

            <div>
                <label>BandWidth:</label>
                <select id="bandwidth" disabled>
                    <option value="unlimited" selected>unlimited</option>
                    <option value="2000">2000</option>
                    <option value="1000">1000</option>
                    <option value="500">500</option>
                    <option value="250">250</option>
                    <option value="125">125</option>
                </select>
                kbps
            </div>


            <div class="preview">
                <div>
                    <h2>Local:</h2>
                    <video id="localvideo" autoplay playsinline muted></video>
                </div>

                <div>
                    <h2>Remote:</h2>
                    <video id="remotevideo" autoplay playsinline></video>
                </div>

            </div>

            <div >
                <form id="fileInfo">
                    <input type="file" id="fileInput" name="files" disabled />
                </form>
                <button disabled id="sendFile">Send</button>
                <button disabled id="abortButton">Abort</button>
            </div>

            <div class="progress">
                <div class="label">Send progress: </div>
                <progress id="sendProgress" max="0" value="0"></progress>
            </div>

            <div class="progress">
                <div class="label">Receive progress: </div>
                <progress id="receiveProgress" max="0" value="0"></progress>
            </div>

            <div id="bitrate"></div>
            <a id="download"></a>
            <span id="status"></span>

            <div class="preview">
                <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>
            </div>

        </div>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        <script src="js/main_bw.js"></script>
        <script src="js/third_party/graph.js"></script>
    </body>
</html>
//server.js
'use strict'

var log4js = require('log4js');
var http = require('http');
var https = require('https');
var fs = require('fs');
var socketIo = require('socket.io');

var express = require('express');
var serveIndex = require('serve-index');

var USERCOUNT = 3;

log4js.configure({
    appenders: {
        file: {
            type: 'file',
            filename: 'app.log',
            layout: {
                type: 'pattern',
                pattern: '%r %p - %m',
            }
        }
    },
    categories: {
       default: {
          appenders: ['file'],
          level: 'debug'
       }
    }
});

var logger = log4js.getLogger();

var app = express();
app.use(serveIndex('./public'));
app.use(express.static('./public'));



//http server
var http_server = http.createServer(app);
http_server.listen(80, '0.0.0.0');

var options = {
    key : fs.readFileSync('./cert/1557605_www.learningrtc.cn.key'),
    cert: fs.readFileSync('./cert/1557605_www.learningrtc.cn.pem')
}

//https server
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);

io.sockets.on('connection', (socket)=> {

    socket.on('message', (room, data)=>{
        socket.to(room).emit('message',room, data);
    });

    socket.on('join', (room)=>{
        socket.join(room);
        var myRoom = io.sockets.adapter.rooms[room]; 
        var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
        logger.debug('the user number of room is: ' + users);

        if(users < USERCOUNT){
            socket.emit('joined', room, socket.id); //发给除自己之外的房间内的所有人
            if(users > 1){
                socket.to(room).emit('otherjoin', room, socket.id);
            }
        
        }else{
            socket.leave(room); 
            socket.emit('full', room, socket.id);
        }
        //socket.emit('joined', room, socket.id); //发给自己
        //socket.broadcast.emit('joined', room, socket.id); //发给除自己之外的这个节点上的所有人
        //io.in(room).emit('joined', room, socket.id); //发给房间内的所有人
    });

    socket.on('leave', (room)=>{
        var myRoom = io.sockets.adapter.rooms[room]; 
        var users = (myRoom)? Object.keys(myRoom.sockets).length : 0;
        logger.debug('the user number of room is: ' + (users-1));
        //socket.emit('leaved', room, socket.id);
        //socket.broadcast.emit('leaved', room, socket.id);
        socket.to(room).emit('bye', room, socket.id);
        socket.emit('leaved', room, socket.id);
        //io.in(room).emit('leaved', room, socket.id);
    });

});

https_server.listen(443, '0.0.0.0');

RTP-SRTP

6.png
7.png
8.png
9.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容