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 按类型(音视频)返回系统发送媒体的能力
RTCRtpTransceiver
- getTransceiver
从PC获得一组RTCRtpTransceiver对象,每个RTCRtpTransceiver是RTCRtpSender和RTCRtpReciver对
-方法
stop:停止发送和接收媒体数据
控制传输速率
'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');