基于WebRTC的网络点对点实时音视频通信测试样例

本例使用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/  即可。

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

txp1993

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

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

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

打赏作者

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

抵扣说明:

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

余额充值