SDP规范
- 会话层(全局的,类似于全局变量)
- 媒体层(局部的,类似于局部变量)
会话层
- 会话的名称与目的
- 会话的存活时间
- 会话中包括多个媒体信息
SDP媒体信息
- 媒体格式
- 传输协议
- 传输IP和端口(价值不大,因为webrtc中会使用ice中收集的IP和端口)
- 媒体负载类型
SDP格式
- 由多个<type>=<value>组成
- 一个会话级描述
- 多个媒体级描述
SDP结构
- Session Description
- Time Description
- Media Description
Session Description
- v=(protocol version)
- 0=(owner/create and session identifiter)
- s=(session name)
- c=*(conn info - optional if included at session-level)
- a=*(zero or more session attribute lines)
Time Description
- t=(time the session is active)
- r=*(zero or more repeat times)
Media Description
- m=(media name and transport address)
- c=*(conn info - optional if included at session-level)
- b=*(bandwidth information)
- a=*(zero or more session attribute lines)
字段含义
- Version必选
- Session Name 必选
- Origion/Owner 必选
- Connection Data可选
- Media Announcements 必选
- Suggested Attributes 可选
例子: a=framerate:《帧速率》
- rtpmap 可选
- fmtp可选

STUN/TURN服务器选型
- rfc5766-turn-server:谷歌开源,但是比较老了
- coTurn :rfc的升级版,比较活跃,选这个
- ResTurn :也是比较流行的,但是比较老
coTurn服务器搭建与部署
- 下载coTurn
- ./configure --prefix=/usr/local/turn
- 编译 make&&make install
一、下载corturn
//将安装的文件克隆到本地
git clone https://github.com/coturn/coturn
//安装依赖libevent-dev
sudo apt-get install libevent-dev
//进入corturn执行下面的代码。注意此处后面的路径是安装的路径,可以自选;执行成功如下
 ./configure --prefix=/usr/local/coturn

执行成功之后查看makefile
三、最后install

coTurn服务器配置

修改turnserver.conf.default文件
smileyqp@smileyqp:/usr/local/coturn/etc$ sudo vim turnserver.conf.default 
//简单设置几个参数
turnserver.conf.default 
user=smileyqp:123456
listening-port=3478
环境变量中添加coturn
sudo vim ~/.bashrc 
export PATH=/usr/local/coturn/bin
source  ~/.bashrc
开启服务
//如果没添加到环境变量种种就用,加了就直接turnserver
./bin/turnserver -c ./etc/turnserver.conf.default
//查看turn服务是否启动
ps -ef | grep turn
PTCPeerConnection







客户端信令消息
- 
message中分为三个:offer、answer、candidate 
  



音视频实时互动信令服务器
'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');
客户状态机及处理逻辑






//main.js
'use strict'
var start = document.querySelector('button#start');
var restart = document.querySelector('button#restart');
var pc1 = new RTCPeerConnection();
var pc2 = new RTCPeerConnection();
function handleError(err){
    console.log('Failed to create offer', err);
}
function getPc1Answer(desc){
    console.log('getPc1Answer', desc.sdp);
    pc2.setLocalDescription(desc);
    pc1.setRemoteDescription(desc);
    /*
    pc2.createOffer({offerToRecieveAudio:1, offerToReceiveVideo:1})
        .then(getPc2Offer)
        .catch(handleError);
        */
}
function getPc1Offer(desc){
    console.log('getPc1Offer', desc.sdp);
    pc1.setLocalDescription(desc);
    pc2.setRemoteDescription(desc);
    pc2.createAnswer().then(getPc1Answer).catch(handleError);
}
function getPc2Answer(desc){
    console.log('getPc2Answer');
    pc1.setLocalDescription(desc);
    pc2.setRemoteDescription(desc);
}
function getPc2Offer(desc){
    console.log('getPc2Offer');
    pc2.setLocalDescription(desc);
    pc1.setRemoteDescription(desc);
    pc1.createAnswer().then(getPc2Answer).catch(handleError);
}
function startTest(){
    pc1.createOffer({offerToReceiveAudio:1, offerToRecieveVideo:1})
        .then(getPc1Offer)
        .catch(handleError);
}
function getMediaStream(stream){
    stream.getTracks().forEach((track) => {
        pc1.addTrack(track, stream);    
    });
    var offerConstraints = {
        offerToReceiveAudio: 1,
        offerToRecieveVideo: 1,
        iceRestart:false 
    }
    pc1.createOffer(offerConstraints)
        .then(getPc1Offer)
        .catch(handleError);
}
function startICE(){
    var constraints = {
        audio: true,
        video: true
    }
    navigator.mediaDevices.getUserMedia(constraints)
                .then(getMediaStream)
                .catch(handleError);
}
start.onclick = startTest;
restart.onclick = startICE;
<html>
    <head>
        <title>
            test createOffer from different client
        </title>
    </head>
    <body>
        <button id="start">Start</button>
        <button id="restart">reStart ICE</button>
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        <script src="js/main.js"></script>
    </body>
</html>
实战demo
//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 offer = document.querySelector('textarea#offer');
var answer = document.querySelector('textarea#answer');
var shareDeskBox  = document.querySelector('input#shareDesk');
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';
// 以下代码是从网上找的
//=========================================================================================
//如果返回的是false说明当前操作系统是手机端,如果返回的是true则说明当前的操作系统是电脑端
function IsPC() {
    var userAgentInfo = navigator.userAgent;
    var Agents = ["Android", "iPhone","SymbianOS", "Windows Phone","iPad", "iPod"];
    var flag = true;
    for (var v = 0; v < Agents.length; v++) {
        if (userAgentInfo.indexOf(Agents[v]) > 0) {
            flag = false;
            break;
        }
    }
    return flag;
}
//如果返回true 则说明是Android  false是ios
function is_android() {
    var u = navigator.userAgent, app = navigator.appVersion;
    var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //g
    var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
    if (isAndroid) {
        //这个是安卓操作系统
        return true;
    }
    if (isIOS) {
        //这个是ios操作系统
        return false;
    }
}
//获取url参数
function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}
//=======================================================================
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);
        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();
        offer.value = '';
        answer.value = '';
        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') {
            
            offer.value = data.sdp;
            pc.setRemoteDescription(new RTCSessionDescription(data));
            //create answer
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleAnswerError);
        }else if(data.hasOwnProperty('type') && data.type == 'answer'){
            answer.value = data.sdp;
            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);  
        
        }else{
            console.log('the message is invalid!', data);
        
        }
    
    });
    roomid = getQueryVariable('room');
    socket.emit('join', roomid);
    return true;
}
function connSignalServer(){
    
    //开启本地视频
    start();
    return true;
}
function getMediaStream(stream){
    if(localStream){
        stream.getAudioTracks().forEach((track)=>{
            localStream.addTrack(track);    
            stream.removeTrack(track);
        });
    }else{
        localStream = stream;   
    }
    localVideo.srcObject = localStream;
    //这个函数的位置特别重要,
    //一定要放到getMediaStream之后再调用
    //否则就会出现绑定失败的情况
    //
    //setup connection
    conn();
    //btnStart.disabled = true;
    //btnCall.disabled = true;
    //btnHangup.disabled = true;
}
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;
        if( shareDeskBox.checked && shareDesk()){
            constraints = {
                video: false,
                audio:  {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: true
                }
            }
        }else{
            constraints = {
                video: true,
                audio:  {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: true
                }
            }
        }
        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);
    answer.value = desc.sdp;
    //send answer sdp
    sendMessage(roomid, desc);
}
function getOffer(desc){
    pc.setLocalDescription(desc);
    offer.value = desc.sdp;
    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.warning('the pc have be created!');
    }
    return; 
}
//绑定永远与 peerconnection在一起,
//所以没必要再单独做成一个函数
function bindTracks(){
    console.log('bind tracks into RTCPeerConnection!');
    if( pc === null || pc === 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) {
        offerdesc = null;
        
        pc.close();
        pc = null;
    }
}
function closeLocalMedia(){
    if(localStream && localStream.getTracks()){
        localStream.getTracks().forEach((track)=>{
            track.stop();
        });
    }
    localStream = null;
}
function leave() {
    if(socket){
        socket.emit('leave', roomid); //notify server
    }
    hangup();
    closeLocalMedia();
    offer.value = '';
    answer.value = '';
    btnConn.disabled = false;
    btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//main_signal.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 localStream = null;
var roomid;
var socket = 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'
        btnConn.disabled = true;
        btnLeave.disabled = false;
        console.log('receive joined message, state=', state);
    });
    socket.on('otherjoin', (roomid) => {
        console.log('receive joined message:', roomid, state);
        state = 'joined_conn';
        console.log('receive other_join message, state=', state);
    });
    socket.on('full', (roomid, id) => {
        console.log('receive full message', roomid, id);
        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'
        btnConn.disabled = false;
        btnLeave.disabled = true;
    });
    socket.on('bye', (room, id) => {
        console.log('receive bye message', roomid, id);
        state = 'joined_unbind';
        console.log('receive bye message, state=', state);
    });
    socket.on('disconnect', (socket) => {
        console.log('receive disconnect message!', roomid);
        state = 'leaved';
        console.log('receive disconnect message, state=', state);
    });
    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') {
            
        }else if(data.hasOwnProperty('type') && data.type == 'answer'){
        
        }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
        
        }else{
            console.log('the message is invalid!', data);
        }
    
    });
    roomid = getQueryVariable('room');
    socket.emit('join', roomid);
    return true;
}
function connSignalServer(){
    
    //开启本地视频
    start();
    return true;
}
function getMediaStream(stream){
    localStream = stream;   
    localVideo.srcObject = localStream;
    conn();
}
function handleError(err){
    if(err){
        console.error('Failed to get Media Stream:', err);
    }else {
        console.error('Failed to get Media Stream!', );
    }
    return;
}
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 closeLocalMedia(){
    if(localStream && localStream.getTracks()){
        localStream.getTracks().forEach((track)=>{
            track.stop();
        });
    }
    localStream = null;
}
function leave() {
    if(socket){
        socket.emit('leave', roomid); //notify server
    }
    btnConn.disabled = false;
    btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//main_sdp.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 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 conn(){
    socket = io.connect();
    socket.on('joined', (roomid, id) => {
        console.log('receive joined message!', roomid, id);
        state = 'joined'
        createPeerConnection();
        btnConn.disabled = true;
        btnLeave.disabled = false;
        console.log('receive joined message, state=', state);
    });
    socket.on('otherjoin', (roomid) => {
        console.log('receive joined message:', roomid, state);
        if(state === 'joined_unbind'){
            createPeerConnection();
        }
        state = 'joined_conn';
        call();
        console.log('receive other_join message, state=', state);
    });
    socket.on('full', (roomid, id) => {
        console.log('receive full message', roomid, id);
        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 = 'joined_unbind';
        hangup();
        console.log('receive bye message, state=', state);
    });
    socket.on('disconnect', (socket) => {
        console.log('receive disconnect message!', roomid);
        state = 'leaved';
    
        console.log('receive disconnect message, state=', state);
    });
    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'){
            answer.value = data.sdp;
            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);  
        
        }else{
            console.log('the message is invalid!', data);
        
        }
    
    });
    roomid = getQueryVariable('room');
    socket.emit('join', roomid);
    return true;
}
function connSignalServer(){
    
    //开启本地视频
    start();
    return true;
}
function getMediaStream(stream){
    localStream = stream;   
    localVideo.srcObject = localStream;
    //setup connection
    conn();
}
function handleError(err){
    if(err){
        console.error('Failed to get Media Stream!', err);  
    }else {
        console.error('Failed to get Media Stream!');
    }
}
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 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 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);
    //send answer sdp
    sendMessage(roomid, desc);
}
function getOffer(desc){
    pc.setLocalDescription(desc);
    sendMessage(roomid, offerdesc); 
}
function call(){
    
    if(state === 'joined_conn'){
        var offerOptions = {
            offerToRecieveAudio: 1,
            offerToRecieveVideo: 1
        }
        pc.createOffer(offerOptions)
            .then(getOffer)
            .catch(handleOfferError);
    }
}
function createPeerConnection(){
    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.warning('the pc have be created!');
    }
    console.log('bind tracks into RTCPeerConnection!');
    if(localStream === null || localStream === undefined) {
        console.error('localstream is null or undefined!');
        return false;
    }
    //add all track into peer connection
    localStream.getTracks().forEach((track)=>{
        pc.addTrack(track, localStream);    
    });
    return true;    
}
function hangup(){
    if(pc) {
        pc.close();
        pc = null;
    }
}
function closeLocalMedia(){
    if(localStream && localStream.getTracks()){
        localStream.getTracks().forEach((track)=>{
            track.stop();
        });
    }
    localStream = null;
}
function leave() {
    if(socket){
        socket.emit('leave', roomid); //notify server
    }
    hangup();
    closeLocalMedia();
    btnConn.disabled = false;
    btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//main_connection.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 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 state = 'init';
function conn(){
    socket = io.connect();
    socket.on('joined', (roomid, id) => {
        console.log('receive joined message!', roomid, id);
        state = 'joined'
        createPeerConnection();
        btnConn.disabled = true;
        btnLeave.disabled = false;
        console.log('receive joined message, state=', state);
    });
    socket.on('otherjoin', (roomid) => {
        console.log('receive joined message:', roomid, state);
        if(state === 'joined_unbind'){
            createPeerConnection();
        }
        state = 'joined_conn';
        call();
        console.log('receive other_join message, state=', state);
    });
    socket.on('full', (roomid, id) => {
        console.log('receive full message', roomid, id);
        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 = 'joined_unbind';
        hangup();
        console.log('receive bye message, state=', state);
    });
    socket.on('disconnect', (socket) => {
        console.log('receive disconnect message!', roomid);
        state = 'leaved';
    
        console.log('receive disconnect message, state=', state);
    });
    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') {
        }else if(data.hasOwnProperty('type') && data.type == 'answer'){
        
        }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
        
        }else{
            console.log('the message is invalid!', data);
        
        }
    
    });
    roomid = getQueryVariable('room');
    socket.emit('join', roomid);
    return true;
}
function connSignalServer(){
    
    //开启本地视频
    start();
    return true;
}
function getMediaStream(stream){
    localStream = stream;   
    localVideo.srcObject = localStream;
    //setup connection
    conn();
}
function handleError(err){
    if(err){
        console.error('Failed to get Media Stream!', err);  
    }else {
        console.error('Failed to get Media Stream!');
    }
}
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 createPeerConnection(){
    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.warning('the pc have be created!');
    }
    console.log('bind tracks into RTCPeerConnection!');
    if(localStream === null || localStream === undefined) {
        console.error('localstream is null or undefined!');
        return false;
    }
    //add all track into peer connection
    localStream.getTracks().forEach((track)=>{
        pc.addTrack(track, localStream);    
    });
    return true;    
}
function call(){
    
    if(state === 'joined_conn'){
        var offerOptions = {
            offerToRecieveAudio: 1,
            offerToRecieveVideo: 1
        }
        pc.createOffer(offerOptions)
            .then(getOffer)
            .catch(handleOfferError);
    }
}
function hangup(){
    if(pc) {
        pc.close();
        pc = null;
    }
}
function closeLocalMedia(){
    if(localStream && localStream.getTracks()){
        localStream.getTracks().forEach((track)=>{
            track.stop();
        });
    }
    localStream = null;
}
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 leave() {
    if(socket){
        socket.emit('leave', roomid); //notify server
    }
    hangup();
    closeLocalMedia();
    btnConn.disabled = false;
    btnLeave.disabled = true;
}
btnConn.onclick = connSignalServer
btnLeave.onclick = leave;
//index.html
<html>
    <head>
        <title>really peer connection</title>
        <link rel="stylesheet" href="./css/main.css">
        <script language="javascript" type="text/javascript">
            function gotoNextPage(){
                var roomid = document.querySelector('input#room');
                if(roomid.value === null || roomid.value === ''){
                    alert('roomid is null');
                }else {
                    window.location.href="room.html?room="+ roomid.value;
                }
            }
        </script>
    </head>
    <body>
        <table align="center">
            <tr><td><div>
                    <label>roomid:</label>
                    <input type="input" id="room">
            </div></td></tr>
            <tr><td><div>
                        <button id="join" onclick="gotoNextPage()">Join</button>
            </div></td></tr>
        </table>
    </body>
</html>
//room.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="start" disabled>Start</button>  
                <button id="call" disabled>Call</button>    
                <button id="hangup" disabled>HangUp</button>    
                -->
                <button id="leave" disabled>Leave</button>  
            </div>
            <div>
                <input id="shareDesk" type="checkbox"/><label for="shareDesk">Share Desktop</label>
            </div>
            <div id="preview">
                <div >
                    <h2>Local:</h2>
                    <video id="localvideo" autoplay playsinline muted></video>
                    <h2>Offer SDP:</h2>
                    <textarea id="offer"></textarea>
                </div>
                <div>
                    <h2>Remote:</h2>
                    <video id="remotevideo" autoplay playsinline></video>
                    <h2>Answer SDP:</h2>
                    <textarea id="answer"></textarea>
                </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.js"></script>
    </body>
</html>
如果远程共享桌面
//只是提几个重要api
var promise=navigator.mediaDevices.getDisplayMedia(contraints);
//constraints可选
//constraints中约束与getUserMedia函数中的基本一致
- getDisplayMedia无法同时采集音频和桌面,一般都是采集桌面
- 桌面采集时候分辨率是否可调整
总结
- 网络连接要在音视频数据获取到之后,否则有可能绑定音视频失败
- 当一段退出房间后,另一端的PeerConnection要关闭重建,否则与新用户互通时媒体协商会失败
- 异步事件处理










