简介
基于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:
图2: