- 12-1 【来点实战】STUN_TURN服务器搭建
- 12-2 【参数介绍】再论RTCPeerConnection
- 12-3 【必备原理】直播系统中的信令及其逻辑关系
- 12-4 【来点实战】实现1:1音视频实时互动信令服务器
- 12-5 【参数介绍】再论CreateOffer
- 12-6 【必备原理】WebRTC客户端状态机及处理逻辑
- 12-7【来点实战-基本结构】-WebRTC客户端的实现
- 12-8 【来点实战-增加PeerConnecton逻辑】WebRTC客户端的实现-1
- 12-9 【来点实战-增加PeerConnecton逻辑】WebRTC客户端的实现-2
- 12-10 【来点实战-增加媒体协商的逻辑】WebRTC客户端的实现-3
- 12-11 【阶段作业,练练手吧】共享远程桌面
- 12-12 完整代码
本章将带你学习真正的1V1音视频实时互动直播系统的实现。这部分内容比较重,里边有大量的实现,相信同学位可以从本章收获大量的知识。
12-1 【来点实战】STUN_TURN服务器搭建


https://github.com/coturn/coturn
云服务器位置
/home/ubuntu/webrtc/coturn
配置文件目录
/usr/local/coturn/etc
cp turnserver.conf.default
turnserver.conf
1.14.148.67
sudo turnserver -c ls /usr/local/coturn/etc/turnserver.conf
# nohup是重定向命令,输出都将附加到当前目录的 nohup.out 文件中; 命令后加 & ,后台执行起来后按
ctr+c,不会停止
sudo nohup turnserver -L 0.0.0.0 -a -u charles:123456 -v -f -r nort.gov &
https://www.yuque.com/caokunchao/va2wrk/ylgv06
12-2 【参数介绍】再论RTCPeerConnection






12-3 【必备原理】直播系统中的信令及其逻辑关系
12-4 【来点实战】实现1:1音视频实时互动信令服务器

主要修改:
添加人数限制
添加otherjoin,full指令
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 servervar 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 servervar 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');
12-5 【参数介绍】再论CreateOffer


<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>
'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;

单击两次Start,终端搜索ice-uflag,发现有四个,但是实际两个是一样值。
ice-ufrag 和 ice-pwd 是用户名和密码。当 A 与 B 建立连接时,A 每次发送数据到B,都要带着它的用户名和密码过来,此时 B 端就可以通过验证 A 带来的用户名和密码与 SDP 中的用户名和密码是否一致的,来判断 A 是否是一个合法用户了。
除此之外,fingerprint也是验证合法性的关键一步,它是存放公钥证书的指纹(或叫信息摘要),在通过 ice-ufrag 和 ice-pwd 验证用户的合法性之余,还要对它发送的证书做验证,看看证书在传输的过程中是否被窜改了。
(黄色片段来自 https://zhuanlan.zhihu.com/p/100007626 )
12-6 【必备原理】WebRTC客户端状态机及处理逻辑




12-7【来点实战-基本结构】-WebRTC客户端的实现

conn()
12-8 【来点实战-增加PeerConnecton逻辑】WebRTC客户端的实现-1
12-9 【来点实战-增加PeerConnecton逻辑】WebRTC客户端的实现-2
createPeerConnection()
leave()
hangup()
12-10 【来点实战-增加媒体协商的逻辑】WebRTC客户端的实现-3
call()
getOffer(desc)
sendMessage(roomid, data)
handleError(err)
socket.on(‘message’, (roomid, data)
getAnswer
1) 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>
2) 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是iosfunction is_android() {var u = navigator.userAgent, app = navigator.appVersion;var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //gvar isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端if (isAndroid) {//这个是安卓操作系统return true;}if (isIOS) {//这个是ios操作系统return false;}}//获取url参数function getQueryVariable(variable){//JS 脚本捕获页面 GET 方式请求的参数?其实直接使用 window.location.search 获得,//然后通过 split 方法结合循环遍历自由组织数据格式。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 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);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();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 answerpc.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 connectionconn();//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 sdpsendMessage(roomid, desc);}function getOffer(desc){pc.setLocalDescription(desc);offer.value = desc.sdp;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.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 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) {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 = connSignalServerbtnLeave.onclick = leave;
3)函数流程
btnConn.onclick
-> connSignalServer()
-> start()
-> shareDeskBox.checked && shareDesk( //是否共享桌面,不是则是共享摄像头
-> getMediaStream(stream)
-> conn()
12-11 【阶段作业,练练手吧】共享远程桌面

if( shareDeskBox.checked && shareDesk())




