- 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 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');
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是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)
{
//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 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;
3)函数流程
btnConn.onclick
-> connSignalServer()
-> start()
-> shareDeskBox.checked && shareDesk( //是否共享桌面,不是则是共享摄像头
-> getMediaStream(stream)
-> conn()
12-11 【阶段作业,练练手吧】共享远程桌面
if( shareDeskBox.checked && shareDesk())