stay hungry stay foolish

webrtc通信实践

centos7 下安装 coturn

安装相关依赖

yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel openssl 
yum install git

下载编译安装coturn开源项目

git clone https://github.com/coturn/coturn 
cd coturn 
./configure 
make 
make install

查看是否安装成功

which turnserver

将/usr/local/etc/目录下的turnserver.conf.default文件复制一份,文件名为turnserver.conf

cd /usr/local/etc/
cp turnserver.conf.default turnserver.conf

查看网卡,记录网卡名称和内网地址:ifconfig

cert和pkey配置的自签名证书用Openssl命令生成,提示的相关信息随便填写即可

openssl req -x509 -newkey rsa:2048 -keyout /usr/local/etc/turn_server_pkey.pem -out /usr/local/etc/turn_server_cert.pem -days 99999 -nodes 

修改配置文件信息:vim /usr/local/etc/turnserver.conf

# 网卡名
relay-device=eth0
#内网IP
listening-ip=172.16.0.5 
listening-port=3478
#内网IP
relay-ip=172.16.0.5
tls-listening-port=5349
# 外网IP
external-ip=134.xxx.xxx.xxx
relay-threads=500
#打开密码验证
lt-cred-mech
cert=/usr/local/etc/turn_server_cert.pem
pkey=/usr/local/etc/turn_server_pkey.pem
min-port=40000
max-port=65535
#设置用户名和密码,创建IceServer时使用
user=admin:123456
# 外网IP绑定的域名
realm=lisong.hn.cn
# 服务器名称,用于OAuth认证,默认和realm相同,部分浏览器本段不设可能会引发cors错误。
server-name=lisong.hn.cn
# 认证密码,和前面设置的密码保持一致
cli-password=123456

添加3478端口的tcp/udp规则

#开放端口
firewall-cmd --zone=public --add-port=3478/udp --permanent
firewall-cmd --zone=public --add-port=3478/tcp --permanent
#刷新防火墙
firewall-cmd --reload
#查看当前开放的端口
firewall-cmd --list-port

启动服务:turnserver -o -a -f。备注:turnserver命令会自动寻找到turnserver.conf文件的位置。

终止服务

#搜索 turnserveru 进程
ps -ef | grep turnserver
#杀掉进程(pid换成进程号)
kill -9 pid

在线检测ICE穿透:https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

客户端

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>webrtc</title>
	<script src="./adapter-latest.js"></script>
</head>

<body>
	<button onclick="doOffer()">加入会议</button>
	<div id="videos">
		<video id="localVideo" autoplay muted playsinline>本地窗口</video>
		<video id="remoteVideo" autoplay playsinline>远端窗口</video>
	</div>
	<script>
		var DO_OFFER = 1;
		var DO_ANSWER = 2;
		var SET_DESC = 3;
		var ICE_CANDIDATE = 4;
		var localVideo = document.querySelector('#localVideo');
		var remoteVideo = document.querySelector('#remoteVideo');
		var zeroRTCEngine = null;
		var localStream = null;
		var remoteStream = null;
		var candidate = null;
		var offerSession = null;
		var answerSession = null;

		class ZeroRTCEngine {
			constructor(wsUrl) {
				this.wsUrl = wsUrl;
				this.signaling = null;
				this.createWebsocket();
			}
			createWebsocket() {
				this.signaling = new WebSocket(this.wsUrl);

				this.signaling.onopen = function() {
					console.log("websocket open");
				}

				this.signaling.onmessage = function(event) {
					var message = event.data;
					message = JSON.parse(message);
					console.log('message:', message.type);
					switch (message.type) {
						case DO_ANSWER:
							doAnswer(message.data);
							break;
						case ICE_CANDIDATE:
							addIceCandidate(message.data);
							break;
						case SET_DESC:
							setRemoteDescription(message.data);
					}
				}

				this.signaling.onerror = function(event) {
					console.log("onError: " + event.data);
				}

				this.signaling.onclose = function(event) {
					console.log("onClose -> code: " + event.code + ", reason:" + EventTarget.reason);
				}
			}
			sendMessage(message) {
				this.signaling.send(message);
			}
		}
		
		init();

		function init() {
			zeroRTCEngine = new ZeroRTCEngine('ws://127.0.0.1:8081/');
			navigator.mediaDevices.getUserMedia({
					audio: true,
					video: true
				})
				.then((stream) => {
					localVideo.srcObject = stream;
					localStream = stream;
					createPeerConnection();
				})
				.catch(function(e) {
					alert("getUserMedia() error: " + e.name);
				});
		}

		function createPeerConnection() {
			var defaultConfiguration = {
				bundlePolicy: "max-bundle",
				rtcpMuxPolicy: "require",
				iceTransportPolicy: "all", 
				//iceTransportPolicy: "relay", 
				// 修改ice数组测试效果,需要进行封装
				iceServers: [{
						"urls": [
							"turn:bwg.lisong.hn.cn:3478?transport=udp",
							"turn:bwg.lisong.hn.cn:3478?transport=tcp" // 可以插入多个进行备选
						],
						"username": "test",
						"credential": "123456"
					},
					{
						"urls": [
							"stun:bwg.lisong.hn.cn:3478"
						]
					}
				]
			};

			pc = new RTCPeerConnection(defaultConfiguration); // 音视频通话的核心类
			pc.onicecandidate = (event) => {
				console.info("onicecandidate");
				if (event.candidate) {
					candidate = {
						'sdpMLineIndex': event.candidate.sdpMLineIndex,
						'sdpMid': event.candidate.sdpMid,
						'candidate': event.candidate.candidate
					};
					zeroRTCEngine.sendMessage(JSON.stringify({
						type: ICE_CANDIDATE,
						data: candidate
					}));
				} else {
					console.warn("End of candidates");
				}
			};
			pc.ontrack = (event) => {
				console.log('ontrack')
				remoteStream = event.streams[0];
				remoteVideo.srcObject = remoteStream;
			};
			pc.onconnectionstatechange = () => {
				if (pc != null) {
					console.info("ConnectionState -> " + pc.connectionState);
				}
			};
			pc.oniceconnectionstatechange = () => {
				if (pc != null) {
					console.info("IceConnectionState -> " + pc.iceConnectionState);
				}
			}

			localStream.getTracks().forEach((track) => pc.addTrack(track, localStream)); // 把本地流设置给RTCPeerConnection
		}

		function doOffer() {
			if (pc == null) {
				createPeerConnection();
			}
			pc.createOffer().then((session) => {
				pc.setLocalDescription(session)
					.then(function() {
						offerSession = session;
						zeroRTCEngine.sendMessage(JSON.stringify({
							type: DO_OFFER,
							data: session
						}));
					})
					.catch(function(error) {
						console.error("offer setLocalDescription failed: " + error);
					});

			});
		}

		function doAnswer(session) {
			setRemoteDescription(session);
			pc.createAnswer().then((session) => {
				pc.setLocalDescription(session)
					.then(function() {
						answerSession = session;
						zeroRTCEngine.sendMessage(JSON.stringify({
							type: DO_ANSWER,
							data: session
						}));
					})
					.catch(function(error) {
						console.error("answer setLocalDescription failed: " + error);
					});

			});
		}

		function addIceCandidate(candidate) {
			console.log('addIceCandidate');
			candidate = new RTCIceCandidate(candidate);
			pc.addIceCandidate(candidate).catch(e => {
				console.error("addIceCandidate failed:" + e.name);
			});
		}

		function setRemoteDescription(session) {
			console.log('setRemoteDescription');
			pc.setRemoteDescription(session);
		}
	</script>
</body>

</html>

服务端

const WebSocket = require('ws');

var connMap = {};
var id = 0;
var DO_OFFER = 1;
var DO_ANSWER = 2;
var SET_DESC = 3;
var ICE_CANDIDATE = 4;

const webSocketServer = new WebSocket.Server({
	port: 8081
});

webSocketServer.on('listening', () => {
	console.log('web socket begins listening');
});

webSocketServer.on('connection', function connection(conn) {
	var userId = id++;
	console.log("New connection:", userId);
	connMap[userId] = conn;
	conn.userId = userId;
	conn.on("message", function(str) {
		try {
			var message = JSON.parse(str);
			console.log('recieve', userId, message.type);
			switch (message.type) {
				case DO_OFFER:
					broadcast(userId, {
						type: DO_ANSWER,
						data: message.data
					});
					break;
				case ICE_CANDIDATE:
					broadcast(userId, {
						type: ICE_CANDIDATE,
						data: message.data
					});
					break;
				case DO_ANSWER:
					broadcast(userId, {
						type: SET_DESC,
						data: message.data
					});
			}
		} catch (e) {
			console.log(e);
		}
	});
	conn.on("close", function(code, reason) {
		console.log("Connection closed:", userId);
	});
});

function broadcast(userId, msg) {
	var type = msg.type;
	msg = typeof msg === 'object' ? JSON.stringify(msg) : msg;
	webSocketServer.clients.forEach(function(conn) {
		if (conn.userId !== userId) {
			conn.send(msg);
			console.log('send', conn.userId, type);
		}
	});
}

仓库代码:https://github.com/wanls4583/webrtc.git