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 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();
}
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=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';
});
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'){
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 connection
conn();
}
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 sdp
sendMessage(roomid, desc);
}
function getOffer(desc){
pc.setLocalDescription(desc);
offerdesc = desc;
//send offer sdp
sendMessage(roomid, offerdesc);
}
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.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
hangup();
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();
//获取一个RTCRtpSender
senders.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 = connSignalServer
btnLeave.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 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();
}
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 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.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
hangup();
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 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);
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
optBw.onchange = chang_bw;
重点代码部分
// 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);
效果
初始化效果
连接后的效果
这里是设置了码率为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 => {
//******省略