WebRTC提供了非常精细化的管理。大家除了可以使用非常方便的上层接口来使用 WebRTC之外,还可以通过对 Sender/Receiver的控制,对网络流量进行控制。另外还可以通过 WebRTC统计数据进行网络质量分析。这些知识你都可以通过本章的内容学习到。…
13-1 【基础铺垫,学前有概念】RTPRReceiver发送器


RTCRtpReceiver.track:返回与当前RTCRtpReceiver实例关联的MediaStreamTrack
通过媒体轨属性可以获取当前轨的类型,是audio/video
RTCRtpReceiver.transport:返回接收到的接收者媒体轨的RTCDTLTransport实例
存放着媒体数据传输相关的属性,其中trnasport用于媒体数据的传输,媒体流通过底层的transport进行传输。transport可以进行复用,多个媒体轨复用一个transport传输!
RTCRtpReceiver.rtcpTransport:返回发送和接收RTCP的RTCDTLTransport实例
与rtcp传输相关的属性,比如传输抖动,丢包数量、延迟….。接受方进行统计,反馈给发送端,发送方根据这些数据进行网络质量的评估,适当调整网络流量的发送,这就是流量控制
https://www.cnblogs.com/ssyfj/p/14823861.html
13-2 【基础铺垫,学前有概念】RTPSender发送器

13-3-4 【来点实战】传输速率的控制

room_bw.html
<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 id="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><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></body></html>
main_bw.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 offer = document.querySelector('textarea#offer');var answer = document.querySelector('textarea#answer');var shareDeskBox = document.querySelector('input#shareDesk');var bandwidth = document.querySelector('select#bandwidth');var pcConfig = {'iceServers': [{'urls': 'turn:stun.al.learningrtc.cn:3478','credential': "mypasswd",'username': "garrylea"}]};var localStream = null;var remoteStream = null;var pc = 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 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 trackcreatePeerConnection();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();}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;});socket.on('bye', (room, id) => {console.log('receive bye message', roomid, id);//state = 'created';//当是多人通话时,应该带上当前房间的用户数//如果当前房间用户不小于 2, 则不用修改状态//并且,关闭的应该是对应用户的peerconnection//在客户端应该维护一张peerconnection表,它是//一个key:value的格式,key=userid, value=peerconnectionstate = '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';});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 answerpc.createAnswer().then(getAnswer).catch(handleAnswerError);}else if(data.hasOwnProperty('type') && data.type === 'answer'){pc.setRemoteDescription(new RTCSessionDescription(data));bandwidth.disabled = false;}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 connectionconn();}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);bandwidth.disabled = false;//send answer sdpsendMessage(roomid, desc);}function getOffer(desc){pc.setLocalDescription(desc);offerdesc = desc;//send offer sdpsendMessage(roomid, offerdesc);}function createPeerConnection(){//如果是多人的话,在这里要创建一个新的连接.//新创建好的要放到一个map表中。//key=userid, value=peerconnectionconsole.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.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 connectionlocalStream.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 serverhangup();closeLocalMedia();btnConn.disabled = false;btnLeave.disabled = true;bandwidth.disabled = true;}function change_bw(){bandwidth.disabled = true;var bw = bandwidth.options[bandwidth.selectedIndex].value;var vsender = null;var senders = pc.getSenders();//获取一个RTCRtpSendersenders.forEach(sender => {if(sender && sender.track.kind === 'video'){vsender = sender;}});//获取RTCRtpSender参数,返回一个RTCRtpParameters对象var parameters = vsender.getParameters();if(!parameters.encodings){parameters.encodings=[{}];}//设置传输码率if(bw === 'unlimited'){delete parameters.encodings[0].maxBitrate;}else{parameters.encodings[0].maxBitrate = bw * 1000;}//更新RTP传输的配置以及WebRTC连接上特定传出媒体轨的编码配置vsender.setParameters(parameters).then(()=>{bandwidth.disabled = false;}).catch(err => {console.error(err)});return;}btnConn.onclick = connSignalServerbtnLeave.onclick = leave;bandwidth.onchange = change_bw;
效果
需要打开两个网页,然后都连接信号服务器,才能设置码率。
查看网络连接状态 chrome://webrtc-internals
旧的版本
13-5-6 【来点实战】WebRTC统计信息
index.html
<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 id="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 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><script src="js/third_party/graph.js"></script><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></body></html>
main.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 optBw = document.querySelector('select#bandwidth');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 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 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 trackcreatePeerConnection();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();}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=peerconnectionstate = '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 answerpc.createAnswer().then(getAnswer).catch(handleAnswerError);}else if(data.hasOwnProperty('type') && data.type === 'answer'){optBw.disabled = falsepc.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 connectionconn();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 sdpsendMessage(roomid, desc);}function getOffer(desc){pc.setLocalDescription(desc);offerdesc = desc;//send offer sdpsendMessage(roomid, offerdesc);}function createPeerConnection(){//如果是多人的话,在这里要创建一个新的连接.//新创建好的要放到一个map表中。//key=userid, value=peerconnectionconsole.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.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 connectionlocalStream.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 serverhangup();closeLocalMedia();btnConn.disabled = false;btnLeave.disabled = true;optBw.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 secondwindow.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 bitrateconst bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /(now - lastResult.get(report.id).timestamp);// append to chartbitrateSeries.addPoint(now, bitrate);bitrateGraph.setDataSeries([bitrateSeries]);bitrateGraph.updateEndDate();// calculate number of packets and append to chartpacketSeries.addPoint(now, packets -lastResult.get(report.id).packetsSent);packetGraph.setDataSeries([packetSeries]);packetGraph.updateEndDate();}}});lastResult = res;});}, 1000);btnConn.onclick = connSignalServerbtnLeave.onclick = leave;optBw.onchange = chang_bw;
重点代码部分
// query getStats every secondwindow.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 bitrateconst bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /(now - lastResult.get(report.id).timestamp);// append to chartbitrateSeries.addPoint(now, bitrate);bitrateGraph.setDataSeries([bitrateSeries]);bitrateGraph.updateEndDate();// calculate number of packets and append to chartpacketSeries.addPoint(now, packets -lastResult.get(report.id).packetsSent);packetGraph.setDataSeries([packetSeries]);packetGraph.updateEndDate();}}});lastResult = res;});}, 1000);
效果
初始化效果
连接后的效果

这里是设置了码率为1000kbps.显示码率,和每秒的包大小。

如果同时传输视频和音频,则在统计时,需要这么做
window.setInterval(() => {if (!pc) {return;}const senders = pc.getSenders();if (!senders) {return;}var vsender;senders.forEach(sender => {if(sender && sender.track.kind === 'video'){ vsender = sender;}});if(!vsender){return;}vsender.getStats().then(res => {//******省略

