WebRTC學(xué)習(xí)(四)

RTP Media

  • Receiver(接收器)
    • getReceivers:獲得一組RTCRtpReceiver對(duì)象锥余,用于接收數(shù)據(jù)
  • Sender(發(fā)送器)
    • getSenders:獲取一組RTCRtpSender對(duì)象镐依,用于發(fā)送數(shù)據(jù)骇陈,每個(gè)對(duì)象對(duì)應(yīng)一個(gè)媒體軌

RTCRtpReceiver/RTCRtpSender屬性

  • MediaStreamTrack 媒體軌
  • RTCDtlsTransport Transport媒體數(shù)據(jù)傳輸相關(guān)的屬性
  • RTCDtlsTransport rtcpTransport與rtcp傳輸相關(guān)的屬性

RTCRtpReceiver

  • getParameters 返回RTCRtpParameter對(duì)象
  • getSynchronizationSources 返回一組SynchronizationSources實(shí)例
  • getContributingSources 返回一組ContributingSources實(shí)例
  • getStats RTCStatsReport里面包含輸入流統(tǒng)計(jì)信息
  • getCapabilities 返回系統(tǒng)能接收的媒體能力(音頻灾炭,視頻)

RTCRtpSender屬性

  • getParameters 返回RTCPtpParameter對(duì)象
  • setParameters 設(shè)置RTCP傳輸相關(guān)的參數(shù)
  • getStats 提供了輸出流統(tǒng)計(jì)信息
  • replaceTrack 用另一個(gè)track替換現(xiàn)在的track,如切換攝像頭
  • getCapabilities 按類(lèi)型(音視頻)返回系統(tǒng)發(fā)送媒體的能力
1.png

RTCRtpTransceiver

  • getTransceiver

從PC獲得一組RTCRtpTransceiver對(duì)象坡氯,每個(gè)RTCRtpTransceiver是RTCRtpSender和RTCRtpReciver對(duì)
-方法
stop:停止發(fā)送和接收媒體數(shù)據(jù)

控制傳輸速率

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'

        //如果是多人的話泉瞻,第一個(gè)人不該在這里創(chuàng)建peerConnection
        //都等到收到一個(gè)otherjoin時(shí)再創(chuàng)建
        //所以莽囤,在這個(gè)消息里應(yīng)該帶當(dāng)前房間的用戶(hù)數(shù)
        //
        //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);

        //如果是多人的話咙鞍,每上來(lái)一個(gè)人都要?jiǎng)?chuàng)建一個(gè)新的 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';
        //當(dāng)是多人通話時(shí)房官,應(yīng)該帶上當(dāng)前房間的用戶(hù)數(shù)
        //如果當(dāng)前房間用戶(hù)不小于 2, 則不用修改狀態(tài)
        //并且,關(guān)閉的應(yīng)該是對(duì)應(yīng)用戶(hù)的peerconnection
        //在客戶(hù)端應(yīng)該維護(hù)一張peerconnection表续滋,它是
        //一個(gè)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(){
    
    //開(kāi)啟本地視頻
    start();

    return true;
}

function getMediaStream(stream){

    localStream = stream;   
    localVideo.srcObject = localStream;

    //這個(gè)函數(shù)的位置特別重要,
    //一定要放到getMediaStream之后再調(diào)用
    //否則就會(huì)出現(xiàn)綁定失敗的情況
    
    //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(){

    //如果是多人的話疲酌,在這里要?jiǎng)?chuàng)建一個(gè)新的連接.
    //新創(chuàng)建好的要放到一個(gè)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; 
}

//綁定永遠(yuǎn)與 peerconnection在一起,
//所以沒(méi)必要再單獨(dú)做成一個(gè)函數(shù)
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是第三方庫(kù)朗恳,用于繪制實(shí)時(shí)傳輸速率曲線圖
/*
 *  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共用一個(gè)

傳輸非音視頻數(shù)據(jù)

  • 基本格式

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

  • 參數(shù)
    • label:人類(lèi)可讀的字符串
    • options:可選的

Option選項(xiàng)

ordered:包亂序了重排序
maxPacketLifeTime/maxRetransmits: 包傳輸時(shí)間和重傳遞次數(shù)湿颅,二選一
negotiated:
    如果為false,一端使用createDataChannel創(chuàng)建通道粥诫,另一端監(jiān)聽(tīng)ondatachannel事件
    如果為true油航,兩端都可以使用createDataChannel創(chuàng)建通道,通過(guò)id來(lái)標(biāo)識(shí)同一個(gè)通道
id:就是上面說(shuō)的id

DataChannel事件

onmessage:對(duì)方有數(shù)據(jù)過(guò)來(lái)的時(shí)候觸發(fā)
onopen:創(chuàng)建好DataChannel時(shí)候觸發(fā)
onclose:DataChannel關(guān)閉的時(shí)候觸發(fā)
onerror:DataChannel發(fā)生錯(cuò)誤的時(shí)候觸發(fā)

demo注意點(diǎn)

  • 創(chuàng)建datachannel的時(shí)機(jī)與文本聊天一樣
  • 通過(guò)js的FileReader從文本中讀取數(shù)據(jù)
  • 以數(shù)據(jù)塊為單位發(fā)送數(shù)據(jù)
  • 發(fā)送數(shù)據(jù)先要將文件的基本信息以信令方式通知對(duì)方
'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'

        //如果是多人的話,第一個(gè)人不該在這里創(chuàng)建peerConnection
        //都等到收到一個(gè)otherjoin時(shí)再創(chuàng)建
        //所以怀浆,在這個(gè)消息里應(yīng)該帶當(dāng)前房間的用戶(hù)數(shù)
        //
        //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);

        //如果是多人的話谊囚,每上來(lái)一個(gè)人都要?jiǎng)?chuàng)建一個(gè)新的 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';
        //當(dāng)是多人通話時(shí),應(yīng)該帶上當(dāng)前房間的用戶(hù)數(shù)
        //如果當(dāng)前房間用戶(hù)不小于 2, 則不用修改狀態(tài)
        //并且执赡,關(guān)閉的應(yīng)該是對(duì)應(yīng)用戶(hù)的peerconnection
        //在客戶(hù)端應(yīng)該維護(hù)一張peerconnection表镰踏,它是
        //一個(gè)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(){
    
    //開(kāi)啟本地視頻
    start();

    return true;
}

function getMediaStream(stream){

    localStream = stream;   
    localVideo.srcObject = localStream;

    //這個(gè)函數(shù)的位置特別重要沙合,
    //一定要放到getMediaStream之后再調(diào)用
    //否則就會(huì)出現(xiàn)綁定失敗的情況
    
    //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(){

    //如果是多人的話余境,在這里要?jiǎng)?chuàng)建一個(gè)新的連接.
    //新創(chuàng)建好的要放到一個(gè)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; 
}

//綁定永遠(yuǎn)與 peerconnection在一起,
//所以沒(méi)必要再單獨(dú)做成一個(gè)函數(shù)
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); //發(fā)給除自己之外的房間內(nèi)的所有人
            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); //發(fā)給自己
        //socket.broadcast.emit('joined', room, socket.id); //發(fā)給除自己之外的這個(gè)節(jié)點(diǎn)上的所有人
        //io.in(room).emit('joined', room, socket.id); //發(fā)給房間內(nèi)的所有人
    });

    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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末芳来,一起剝皮案震驚了整個(gè)濱河市含末,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌即舌,老刑警劉巖佣盒,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異顽聂,居然都是意外死亡肥惭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)紊搪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜜葱,“玉大人,你說(shuō)我怎么就攤上這事耀石∏6冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵滞伟,是天一觀的道長(zhǎng)揭鳞。 經(jīng)常有香客問(wèn)我,道長(zhǎng)梆奈,這世上最難降的妖魔是什么野崇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮亩钟,結(jié)果婚禮上乓梨,老公的妹妹穿的比我還像新娘。我一直安慰自己清酥,他們只是感情好督禽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著总处,像睡著了一般。 火紅的嫁衣襯著肌膚如雪睛蛛。 梳的紋絲不亂的頭發(fā)上鹦马,一...
    開(kāi)封第一講書(shū)人閱讀 51,215評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音忆肾,去河邊找鬼荸频。 笑死,一個(gè)胖子當(dāng)著我的面吹牛客冈,可吹牛的內(nèi)容都是我干的旭从。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼和悦!你這毒婦竟也來(lái)了退疫?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸽素,失蹤者是張志新(化名)和其女友劉穎褒繁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體馍忽,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡棒坏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了遭笋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坝冕。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瓦呼,靈堂內(nèi)的尸體忽然破棺而出喂窟,到底是詐尸還是另有隱情,我是刑警寧澤吵血,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布谎替,位于F島的核電站,受9級(jí)特大地震影響蹋辅,放射性物質(zhì)發(fā)生泄漏钱贯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一侦另、第九天 我趴在偏房一處隱蔽的房頂上張望秩命。 院中可真熱鬧,春花似錦褒傅、人聲如沸弃锐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)霹菊。三九已至,卻和暖如春支竹,著一層夾襖步出監(jiān)牢的瞬間旋廷,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工礼搁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饶碘,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓馒吴,卻偏偏與公主長(zhǎng)得像扎运,于是被迫代替她去往敵國(guó)和親瑟曲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容