webrtc搭建最简单的一对一音视频通话系统

简介

基于Linux+Node.js+socket.io+HTML+webrtc搭建最简单的一对一音视频通话系统

登录linux服务器

在终端app中输入

ssh root@你的域名.com

然后输入密码登录linux服务器,我的服务器为CentOS系统。

安装nodejs

yum install nodejs
yum install npm

创建工作目录

mkdir web_server
cd web_server

放入证书

在cert文件夹中放入证书文件。证书名称.key和证书名称.pem。

创建server.js

vi server.js

作为webrtc的信令服务器,代码如下:

'use strict' //使用严格的js语法

var log4js = require('log4js'); //导入log库,用于打印log
var http = require('http'); //导入http库
var https = require('https'); //导入https库
var fs = require('fs'); //导入fs库,用于读取文件中的证书。
var socketIo = require('socket.io'); //导入socket.io库,用于搭建信令服务

//使用express和serve-index开启web server服务
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();

//开放public文件夹给用户,使用户可以直接访问其中的html文件。
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');

//使用fs读取证书
var options = {
	key : fs.readFileSync('./cert/你的证书名称.key'),
	cert: fs.readFileSync('./cert/你的证书名称.pem')
}

//开启https server,socketIo监听https server。
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);

//sockets监听客户端connection请求,回调中可以拿到对应的单个socket对象
io.sockets.on('connection', (socket)=> {

    //socket监听message消息,并转发消息给房间其他人。
	socket.on('message', (room, data)=>{
		socket.to(room).emit('message',room, data);
	});

	//socket监听join消息
	socket.on('join', (room)=>{
		socket.join(room);//将socket加入到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); //给发送方回复一个joined消息。
			if(users > 1){
				socket.to(room).emit('otherjoin', room, socket.id);//给房间其他用户发送一个otherjoin消息。
			}
		}else{
			socket.leave(room);	//房间已满,将socket退出房间。
			socket.emit('full', room, socket.id);//给发送方回复一个full消息。
		}
	});

	//socket监听leave消息。
	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.leave(room);//将socket从room中leave。
		socket.to(room).emit('bye', room, socket.id);//给房间其他人发送bye消息。
		socket.emit('leaved', room, socket.id);//给发送方返回leaved消息。
	});

});

https_server.listen(443, '0.0.0.0');//https server listen.

安装nodejs相关依赖

npm install forever -g
npm install express serve-index
npm install socket.io log4js

开启web server服务

forever start server.js

创建public

mkdir public
cd public
mkdir peerconnection_onebyone
cd peerconnection_onebyone

创建客户端index.html

<html>
	<head>
		<title>really peer connection</title>
		<link rel="stylesheet" href="./css/main.css">
		<script language="javascript" type="text/javascript">
			<!--点击join按钮,将id为room的input中获取value作为url参数,打开room.html-->
			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>
			<!-- 添加两个按钮,一个Connect Sig Server,一个Leave -->
			<div>
				<button id="connserver">Connect Sig Server</button>
				<button id="leave" disabled>Leave</button>	
			</div>

			<!-- 添加checkbox,用于判断是否是分享桌面还是分享摄像头 -->
			<div>
				<input id="shareDesk" type="checkbox"/><label for="shareDesk">Share Desktop</label>
			</div>


			<div id="preview">
				<!-- 添加localvideo和Offer SDP的文本展示 -->
				<div >
					<h2>Local:</h2>
					<video id="localvideo" autoplay playsinline muted></video>
					<h2>Offer SDP:</h2>
					<textarea id="offer"></textarea>
				</div>
				<!-- 添加remotevideo和Answer SDP的文本展示 -->
				<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>

		<!-- 载入js/main.js -->
		<script src="js/main.js"></script>
	</body>
</html>

创建main.js

mkdir js
vi js/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服务的url:3478',
    'credential': "stun服务的密码",
    'username': "stun服务的用户名"
  }]
};

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

//获取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);
}

//用于将offer/answer/candidate发送给对方。
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连接。
	socket = io.connect();

	//socket监听joined回调,将状态置为joined,创建PeerConnection,绑定Tracks。
	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监听otherjoin回调,当状态为joined_unbind时,说明解绑过,需要重新创建PeerConnection和bindTracks。
	//将状态置为joined_conn,调用call去创建offer。
	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监听full回调,进行关闭PeerConnection,关闭本地媒体流的操作,将状态置为leaved
	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监听leaved回调,断开socket连接。
	socket.on('leaved', (roomid, id) => {
		console.log('receive leaved message', roomid, id);
		socket.disconnect();
		console.log('receive leaved message, state=', state);

		btnConn.disabled = false;
		btnLeave.disabled = true;
	});

	//socket监听bye回调,将状态置为joined_unbind,关闭PeerConnection。
	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监听disconnect回调,关闭PeerConnection,关闭本地媒体流,将状态置为leaved。
	socket.on('disconnect', (socket) => {
		console.log('receive disconnect message!', roomid);
		if(!(state === 'leaved')){
			hangup();
			closeLocalMedia();

		}
		state = 'leaved';
	
	});

	//socket监听message回调
	socket.on('message', (roomid, data) => {
		console.log('receive message!', roomid, data);

		if(data === null || data === undefined){
			console.error('the message is invalid!');
			return;	
		}

		//当type为offer时,将对方发来的offer通过pc.setRemoteDescription设置,然后创建answer。
		if(data.hasOwnProperty('type') && data.type === 'offer') {
			
			offer.value = data.sdp;

			pc.setRemoteDescription(new RTCSessionDescription(data));

			//create answer
			pc.createAnswer()
				.then(getAnswer)
				.catch(handleAnswerError);

	  //当type为answer时,将对方发来的answer通过pc.setRemoteDescription设置。
		}else if(data.hasOwnProperty('type') && data.type == 'answer'){
			answer.value = data.sdp;
			pc.setRemoteDescription(new RTCSessionDescription(data));
		
		//当type为candidate时,将对方发来的candidate通过pc.addIceCandidate设置。
		}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);
		
		}
	
	});


	//从url中获取room参数,然后调用socket发送join消息。
	roomid = getQueryVariable('room');
	socket.emit('join', roomid);

	return true;
}

function connSignalServer(){
	
	//开启本地媒体流
	start();

	return true;
}

//将本地媒体流设置到localVideo中进行显示。然后调用conn进行socket信令连接,连接后发送offer、answer、candidate。
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();
}

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);
	}

}

//获取远端媒体流后设置到remoteVideo中。
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);
}

//获取answer后设置到本地描述,然后将answer通过信令服务器转发给对端。
function getAnswer(desc){
	pc.setLocalDescription(desc);
	answer.value = desc.sdp;

	//send answer sdp
	sendMessage(roomid, desc);
}

//获取offer后设置到本地描述,然后将offer通过信令服务器转发给对端。
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);

		//设置收集到iceCandidate的回调监听,将candidate通过信令服务器转发给对端。
		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事件回调,将远端媒体流设置给remoteVideo
		pc.ontrack = getRemoteStream;
	}else {
		console.warning('the pc have be created!');
	}

	return;	
}

//绑定本地媒体流的tracks到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);	
	});

}

//收到socket的otherjoin后,开始收集offer,进行媒体协商,并收集iceCandidate。
function call(){
	
	if(state === 'joined_conn'){

		var offerOptions = {
			offerToRecieveAudio: 1,
			offerToRecieveVideo: 1
		}

		pc.createOffer(offerOptions)
			.then(getOffer)
			.catch(handleOfferError);
	}
}

//关闭PeerConnection
function hangup(){

	if(pc) {

		offerdesc = null;
		
		pc.close();
		pc = null;
	}

}

//关闭本地媒体流
function closeLocalMedia(){

	if(localStream && localStream.getTracks()){
		localStream.getTracks().forEach((track)=>{
			track.stop();
		});
	}
	localStream = null;
}

//用户点击leave后发送消息leave给信令服务器,关闭PeerConnection、本地媒体流。
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;

安装coTurn

cd /root
git clone git@github.com:coturn/coturn.git
cd coturn
sudo ./configure --prefix=/usr/local/coturn
sudo make -j 8
sudo make install
cd /usr/local/coturn/
vi etc/turnserver.conf.default

设置以下配置

listening-port=3478
external-ip=你服务器的外网ip地址
user=username:password
realm=stun.你的域名.com

停止coTurn

ps -ef | grep turnserver
kill -9 xxxx

开启coTurn

bin/turnserver -v -r 外部ip -a -o -c etc/turnserver.conf.default

运行效果如图

图1:
index.html

图2:
room.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王方帅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值