本例使用socket.io作为信令服务,在node运行环境下进行的WebRTC交互通信。由于规范限制等原因,样例只能在本地localhost或者https链接地址环境下测试,如果想在局域网测试,可以在谷歌快捷键目标位置加上:
--unsafely-treat-insecure-origin-as-secure="http://ip:3001" --user-data-dir="本地文件目录"
废话不多说,直接上代码==============================================
1.服务端server.js代码:
'use strict'
const users = {}; // 保存用户
const socks = {}; // 保存客户端对应的socket
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
app.use('/css',express.static('css'));
app.use('/js',express.static('js'));
app.use('/img',express.static('img'));
app.get('/',function(req,res){
res.sendFile(__dirname+'/main.html');
});
io.on('connection', sock => { // 打开连接
sock.on('join', data => { // 加入连接
sock.join(data.roomid, () => {
console.log('有用户加入连接! [' + JSON.stringify(data) + ' ' + new Date() + ']');
if (!users[data.roomid]) {
users[data.roomid] = [];
}
let joinObj = {
id: sock.id,
account: data.account
};
let arr = users[data.roomid].filter(v => v.account === data.account);
if (!arr.length) {
users[data.roomid].push(joinObj);
}
socks[data.account] = sock;
io.in(data.roomid).emit('joined', users[data.roomid], data.account, sock.id); // 发给房间内所有人
// sock.to(data.roomid).emit('joined',data.account);
});
});
sock.on('offer', data => {
sock.to(data.roomid).emit('offer', data);
});
sock.on('answer', data => {
sock.to(data.roomid).emit('answer', data);
});
sock.on('__ice_candidate', data => {
sock.to(data.roomid).emit('__ice_candidate', data);
});
// 1 v 1 连接
sock.on('apply', data => { // 转发申请
socks[data.account].emit('apply', data);
});
sock.on('reply', data => { // 转发回复
socks[data.account].emit('reply', data);
});
sock.on('1v1answer', data => { // 转发 answer
socks[data.account].emit('1v1answer', data);
});
sock.on('1v1ICE', data => { // 转发 ICE
socks[data.account].emit('1v1ICE', data);
});
sock.on('1v1offer', data => { // 转发 Offer
socks[data.account].emit('1v1offer', data);
});
sock.on('1v1hangup', data => { // 转发 hangup
socks[data.account].emit('1v1hangup', data);
});
sock.on('disconnect', (sock) => { // 断开连接
for (let k in users) {
users[k] = users[k].filter(v => v.id !== sock.id);
}
});
});
// 在端口3001监听:
const port = 3001;
http.listen(port, () => {
console.log('app server started at port ...' + port);
});
2.客户端main.html代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebRTC</title>
<link rel="stylesheet" href="css/default.css"/>
<link rel="stylesheet" href="css/main.css"/>
</head>
<body onload="initMain()">
<div id="container">
<h1>WebRTC网络对等连接</h1>
<video id="localVideo" playsinline autoplay muted></video>
<video id="remoteVideo" playsinline autoplay></video>
<div>
<span id="myAccount"></span>
<span>--></span>
<select id="userSelect"></select>
<button type="button" id="applyButton">呼叫</button>
<button type="button" id="hangupButton">关闭</button>
</div>
</div>
<script src="js/socket.io.js"></script>
<script src="js/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
3.客户端main.js代码:
'use strict'
///
const clientVar = { // 定义客户端变量
account: window.sessionStorage.account || '', // 当前用户
isJoin: false, // 是否加入房间
roomid: 'webrtc_1v1', // 指定房间ID
userList: [], // 用户列表
isCall: false, // 正在通话的对象
loading: false, // 呼叫中等待
isToPeer: false, // 是否建立了 P2P 连接
localStream: null, // 本地流
peer: null, // 建立的输出端 PeerConnection
pc1: null,
pc2: null,
config: { // 免费的google ICE服务器地址,如果外网还需要NAT穿越
'iceServers': [{
'urls': 'stun:stun.l.google.com:19302'
}]
},
offerOptions: { // offer配置
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
}
};
const socket = io.connect(); // socket.io作为信令服务
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');
const applyButton = document.getElementById('applyButton');
const hangupButton = document.getElementById('hangupButton');
applyButton.disabled = true;
hangupButton.disabled = true;
applyButton.onclick = apply;
hangupButton.onclick = hangup;
///
function initSocket() { // 初始化客户端socket
socket.on('joined', (data) => { // 获取用户列表
clientVar.userList = data;
initSelectOption();
});
socket.on('reply', async (data) => { // 收到回复
clientVar.loading = false;
switch (data.type) {
case '1': // 对方同意
clientVar.isCall = data.self;
await createP2P(data); // 对方同意之后创建自己的 Peer 等待对方的 offer
createOffer(data); // 并创建自己的 offer 发送给对方
break;
case '2': // 对方拒绝
alert('对方 ' + data.self + ' 拒绝了你的请求!');
break;
case '3': // 对方正在通话中
alert('对方 ' + data.self + ' 正在通话中!');
break;
case '4': // 对方获取不到媒体流
console.log(new Date().getTime() + ' ' + data.self + data.error);
break;
default:
break;
}
});
socket.on('apply', async (data) => { // 收到请求
if (clientVar.isCall && clientVar.isToPeer) {
reply(data.self, '3'); // 回复请求端自己正在通话中
return;
}
if (confirm(data.self + ' 向你请求视频通话, 是否同意?')) {
await createP2P(data); // 自己同意之后创建自己的 peer 等待对方的 offer
clientVar.isCall = data.self;
reply(data.self, '1'); // 回复请求端同意请求
} else {
reply(data.self, '2'); // 回复请求端不同意请求
}
});
socket.on('1v1answer', (data) => { // 接收到 answer
onAnswer(data); // 接收到 answer 处理
});
socket.on('1v1ICE', (data) => { // 接收到 ICE
onIce(data); // 接收到 ICE 处理
});
socket.on('1v1offer', (data) => { // 接收到 offer
onOffer(data); // 接收到 offer 处理
});
socket.on('1v1hangup', (data) => { // 通话挂断
alert('对方 ' + data.self + ' 已断开连接!');
clientVar.peer.close();
clientVar.peer = null;
clientVar.isToPeer = false;
clientVar.isCall = false;
hangupButton.disabled = true; // p2p是连接上时改变按钮状态
applyButton.disabled = false; // 对方挂断后可以在呼叫
});
}
///
function join() { // 加入连接方法
if (!clientVar.account) return;
clientVar.isJoin = true;
let myAccount = document.getElementById('myAccount');
myAccount.innerText = clientVar.account;
window.sessionStorage.account = clientVar.account;
socket.emit('join', {
roomid: clientVar.roomid,
account: clientVar.account
});
console.log(new Date().getTime() + ' ' + clientVar.account + ' join ' + clientVar.roomid + ' ...');
}
function hangup() { // 挂断通话
socket.emit('1v1hangup', {
account: clientVar.isCall,
self: clientVar.account
});
clientVar.peer.close();
clientVar.peer = null;
clientVar.isToPeer = false;
clientVar.isCall = false;
hangupButton.disabled = true; // p2p是连接上时改变按钮状态
applyButton.disabled = false; // 本地挂断后可以在呼叫
console.log(new Date().getTime() + ' ' + clientVar.account + 'hangup ...');
}
function apply() { // 呼叫申请
clientVar.loading = true;
let selectObj = document.getElementById('userSelect');
let selectIndex = selectObj.selectedIndex; //序号,取当前选中选项的序号
let selectUser = selectObj.options[selectIndex].value;
if (selectUser!==clientVar.account && !clientVar.isToPeer) {
// account 对方account self 是自己的account
socket.emit('apply', {
account: selectUser,
self: clientVar.account
});
console.log(new Date().getTime() + ' ' + clientVar.account + ' apply ' + selectUser + ' ...');
} else if (selectUser==clientVar.account) {
alert('不能呼叫自己!');
} else {
alert('已经在通话中!');
}
}
function reply(account, type, error) { // 回复信息
// account 对方account self 是自己的account
socket.emit('reply', {
account: account,
self: clientVar.account,
type: type,
error: error
});
console.log(new Date().getTime() + ' ' + clientVar.account + ' reply ' + account + ' ' + type +' ...');
}
async function createP2P(data) {
clientVar.loading = true;
await createMedia(data);
}
async function createMedia(data) {
try { // 保存本地流到全局
clientVar.localstream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
});
localVideo.srcObject = clientVar.localstream;
console.log(new Date().getTime() + ' ' + clientVar.account + ' getUserMedia ...');
} catch (e) {
console.log(new Date().getTime() + ' ' + clientVar.account + ' getUserMedia: ' + e.message);
reply(data.self, '4', ' getUserMedia: ' + e.message); // 未获取到本地流回复
}
initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
}
function initPeer(data) {
// 创建输出端 PeerConnection
let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
// clientVar.peer = new PeerConnection(clientVar.config);
clientVar.peer = new PeerConnection();
if (clientVar.localstream) clientVar.peer.addStream(clientVar.localstream); // 添加本地流
// 监听ICE候选信息 如果收集到,就发送给对方
clientVar.peer.onicecandidate = (event) => {
if (event.candidate) {
console.log(new Date().getTime() + ' ' + clientVar.account + ' onicecandidate ...');
socket.emit('1v1ICE', {
account: data.self,
self: clientVar.account,
sdp: event.candidate
});
}
};
// 监听是否有媒体流接入,如果有就赋值给远程流的src
clientVar.peer.onaddstream = (event) => {
console.log(new Date().getTime() + ' ' + clientVar.account + ' onaddstream ...');
clientVar.isToPeer = true;
clientVar.loading = false;
remoteVideo.srcObject = event.stream;
hangupButton.disabled = false; // p2p是连接上时改变按钮状态
applyButton.disabled = true;
};
console.log(new Date().getTime() + ' ' + clientVar.account + ' peerConnection ...');
}
async function createOffer(data) { // 创建并发送 offer
try {
// 创建offer
let offer = await clientVar.peer.createOffer(clientVar.offerOptions);
// 呼叫端设置本地 offer 描述
await clientVar.peer.setLocalDescription(offer);
// 给对方发送 offer
socket.emit('1v1offer', {
account: data.self,
self: clientVar.account,
sdp: offer
});
console.log(new Date().getTime() + ' ' + clientVar.account + ' createOffer ...');
} catch (e) {
console.log(new Date().getTime() + ' ' + clientVar.account + ' createOffer: ' + e.message);
}
}
async function onOffer(data) { // 接收offer并发送 answer
try {
// 接收端设置远程 offer 描述
await clientVar.peer.setRemoteDescription(data.sdp);
// 接收端创建 answer
let answer = await clientVar.peer.createAnswer();
// 接收端设置本地 answer 描述
await clientVar.peer.setLocalDescription(answer);
// 给对方发送 answer
socket.emit('1v1answer', {
account: data.self,
self: clientVar.account,
sdp: answer
});
console.log(new Date().getTime() + ' ' + clientVar.account + ' onOffer ...');
} catch (e) {
console.log(new Date().getTime() + ' ' + clientVar.account + ' onOffer: ' + e.message);
}
}
async function onAnswer(data) { // 接收answer
try {
await clientVar.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述
console.log(new Date().getTime() + ' ' + clientVar.account + ' onAnswer ...');
} catch (e) {
console.log(new Date().getTime() + ' ' + clientVar.account + ' onAnswer: ' + e.message);
}
}
async function onIce(data) { // 接收 ICE 候选
try {
await clientVar.peer.addIceCandidate(data.sdp); // 设置远程 ICE
console.log(new Date().getTime() + ' ' + clientVar.account + ' onIce ...');
} catch (e) {
console.log(new Date().getTime() + ' ' + clientVar.account + ' onIce: ' + e.message);
}
}
///
function initMain() { // 页面加载初始化
initSocket(); // 初始化本地socket
if (clientVar.account) {
join(); // 进入页面加入房间
} else {
//第一个参数是提示文字,第二个参数是文本框中默认的内容
let account = prompt("请输入你的昵称", "");
if (account) {
clientVar.account = account;
join(); // 进入页面加入房间
} else {
clientVar.isJoin = false;
}
}
}
function initSelectOption() { // 动态改变用户下拉框
let selectObj = document.getElementById('userSelect');
selectObj.options.length=0;
if (clientVar.userList && clientVar.userList.length > 0) {
applyButton.disabled = clientVar.userList.length > 1 ? false:true; // 本地有可以通话的用户呼叫
for (let index=0; index<clientVar.userList.length; index++) {
let userAccount = clientVar.userList[index].account;
selectObj.add(new Option(userAccount, userAccount));
}
}
}
4.在node环境下运行 node server.js 命令访问 http://localhost:3001/ 即可。