WebSocket+WebRTC的点对点视频音频通信

最近在学习视频音频流,于是从最简单的WebRTC开始,从github上获取了一个很少代码的demo,只实现最基本的视频音频通话。

这是gitbub镜像地址获取的

GitHub - shushushv/webrtc-p2pContribute to shushushv/webrtc-p2p development by creating an account on GitHub.https://bgithub.xyz/shushushv/webrtc-p2p.git环境:

虚拟机:Ubuntu20.04

工具:VSCode,需要插件open in browser ,node.js

总共三个文件代码:

index.js

const app = require('express')();
const wsInstance = require('express-ws')(app);

app.ws('/', ws => {
	ws.on('message', data => {
		// 未做业务处理,收到消息后直接广播
		wsInstance.getWss().clients.forEach(server => {
			if (server !== ws) {
				server.send(data);
			}
		});
	});
});

app.get('/', (req, res) => {
	res.sendFile('./client/index.html', { root: __dirname });
});

app.get('/p2p', (req, res) => {
	res.sendFile('./client/p2p.html', { root: __dirname });
});

app.listen(8080);

index.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>p2p webrtc</title>
	<style>
	.container {
		width: 250px;
		margin: 100px auto;
		padding: 10px 30px;
		border-radius: 4px;
    border: 1px solid #ebeef5;
    box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
    color: #303133;
	}
	</style>
</head>
<body>
	<div class="container">
		<p>流程:</p>
		<ul>
			<li>打开<a href="http://localhost:8080/p2p?type=answer" target="_blank">接收方页面</a>;</li>
			<li>打开<a href="http://localhost:8080/p2p?type=offer" target="_blank">发起方页面</a>;</li>
			<li>确认双方都已建立ws连接;</li>
			<li>发起方点击 start 按钮。</li>
		</ul>
	</div>
</body>
</html>

p2p.html

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title></title>
	<style>
		* {
			padding: 0;
			margin: 0;
			box-sizing: border-box;
		}
		.container {
			width: 100%;
			display: flex;
			display: -webkit-flex;
			justify-content: space-around;
			padding-top: 20px;
		}
		.video-box {
			position: relative;
			width: 800px;
			height: 400px;
		}
		#remote-video {
			width: 100%;
			height: 100%;
			display: block;
			object-fit: cover;
			border: 1px solid #eee;
			background-color: #F2F6FC;
		}
		#local-video {
			position: absolute;
			right: 0;
			bottom: 0;
			width: 240px;
			height: 120px;
			object-fit: cover;
			border: 1px solid #eee;
			background-color: #EBEEF5;
		}
		.start-button {
			position: absolute;
			left: 50%;
			top: 50%;
			width: 100px;
			display: none;
			line-height: 40px;
			outline: none;
			color: #fff;
			background-color: #409eff;
			border: none;
			border-radius: 4px;
			cursor: pointer;
			transform: translate(-50%, -50%);
		}
		.logger {
			width: 40%;
			padding: 14px;
			line-height: 1.5;
			color: #4fbf40;
			border-radius: 6px;
			background-color: #272727;
		}
		.logger .error {
			color: #DD4A68;
		}
	</style>
</head>
<body>
	<div class="container">
		<div class="video-box">
			<video id="remote-video"></video>
			<video id="local-video" muted></video>
			<button class="start-button" onclick="startLive()">start</button>
		</div>
		<div class="logger"></div>
	</div>
	<script>
		const message = {
			el: document.querySelector('.logger'),
			log (msg) {
				this.el.innerHTML += `<span>${new Date().toLocaleTimeString()}:${msg}</span><br/>`;
			},
			error (msg) {
				this.el.innerHTML += `<span class="error">${new Date().toLocaleTimeString()}:${msg}</span><br/>`;
			}
		};
		//location.search 是浏览器中 window.location 对象的一部分,表示当前页面 URL 中的查询字符串部分(即 ? 后面的内容)。
		//slice(6),从第 6 个字符开始获取剩余部分
		const target = location.search.slice(6);
		const localVideo = document.querySelector('#local-video');
		const remoteVideo = document.querySelector('#remote-video');
		const button = document.querySelector('.start-button');

		localVideo.onloadeddata = () => {
			message.log('播放本地视频');
			localVideo.play();
		}
		remoteVideo.onloadeddata = () => {
			message.log('播放对方视频');
			remoteVideo.play();
		}

		document.title = target === 'offer' ? '发起方' : '接收方';

		message.log('信令通道(WebSocket)创建中......');
		//创建 WebSocket 连接 
		// ws:// 是 WebSocket 协议,localhost 表示本地机器,8080 是服务器监听的端口。
		const socket = new WebSocket('ws://localhost:8080');
		//监听 WebSocket 连接成功
		//socket.onopen 是 WebSocket 连接成功时触发的事件
		socket.onopen = () => {
			message.log('信令通道创建成功!');
			target === 'offer' && (button.style.display = 'block');
		}
		//socket.onerror 监听 WebSocket 连接发生错误时触发的事件
		socket.onerror = () => message.error('信令通道创建失败!');
		//处理收到的 WebSocket 消息
		//socket.onmessage 是 WebSocket 接收到消息时触发的事件
		socket.onmessage = e => {
			//解析 WebSocket 传递的消息数据。消息的格式假设为 JSON,包含三个字段:
			// type:表示消息的类型(例如 offer、answer 等)。
			// sdp:Session Description Protocol(会话描述协议),用于描述媒体会话的参数(例如视频编解码方式)。
			// iceCandidate:ICE 候选者,WebRTC 用于建立连接的网络信息。
			const { type, sdp, iceCandidate } = JSON.parse(e.data)
			if (type === 'answer') {
				//RTCSessionDescription 是 WebRTC 中用于描述会话的信息,包含媒体流的参数(例如视频的编码格式、分辨率等)。
				peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
			} else if (type === 'answer_ice') {
				//调用 peer.addIceCandidate 方法将 ICE 候选者信息(网络连接信息)添加到 WebRTC 的对等连接中。
				// 这是 WebRTC 中用来建立连接的步骤
				peer.addIceCandidate(iceCandidate);
			} else if (type === 'offer') {
				//调用 startLive 函数开始媒体会话。
				//传递 RTCSessionDescription 来描述会话的 SDP 信息(如视频编解码方式)
				startLive(new RTCSessionDescription({ type, sdp }));
			} else if (type === 'offer_ice') {
				//用来协商网络连接
				peer.addIceCandidate(iceCandidate);
			}
		};
		//window.RTCPeerConnection 是标准的 WebRTC API 实现,适用于大多数现代浏览器。
		//window.mozRTCPeerConnection 是 Firefox 浏览器的实现。
		//window.webkitRTCPeerConnection 是早期 WebKit 浏览器(如 Safari)的实现。
		const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
		//如果浏览器不支持 WebRTC,PeerConnection 会是 undefined,这时通过 !PeerConnection 判断并调用 message.error('浏览器不支持WebRTC!') 提示用户浏览器不支持 WebRTC。
		!PeerConnection && message.error('浏览器不支持WebRTC!');
		//peer 是一个 RTCPeerConnection 实例,负责与远端建立点对点连接并进行媒体流传输(如音频、视频)
		const peer = new PeerConnection();

		//peer.ontrack 是 WebRTC 中的一个事件,当远端媒体流被接收到时触发
		//e.streams 是一个包含远端音频/视频流的数组。通常情况下,e.streams[0] 会是我们需要的流。
		peer.ontrack = e => {
			if (e && e.streams) {
				message.log('收到对方音频/视频流数据...');
				// 将流设置为 HTML <video> 元素(remoteVideo)的 srcObject,这会使得远端的视频流显示在用户界面中。
				remoteVideo.srcObject = e.streams[0];
			}
		};

		//peer.onicecandidate 事件会在 ICE 候选者被收集时触发
		peer.onicecandidate = e => {
			//如果 e.candidate 存在,表示新的候选者被收集到了
			if (e.candidate) {
				message.log('搜集并发送候选人');
				//通过 socket.send 将候选者信息发送到对端。这是为了帮助对端使用相同的候选者建立连接。
				socket.send(JSON.stringify({
					//根据当前角色(发起方或响应方),它的值是 ${target}_ice,比如如果是发起方,则 type 会是 offer_ice。
					type: `${target}_ice`,
					iceCandidate: e.candidate
				}));
			} else {
				message.log('候选人收集完成!');
			}
		};


		async function startLive (offerSdp) {
			//target 是一个标识当前是发起方(offer)还是接收方(answer)的变量。
			// offer 表示发起连接的一方。如果是发起方,隐藏用于启动直播的按钮(例如,视频通话按钮)。
			target === 'offer' && (button.style.display = 'none');
			//获取本地媒体流(摄像头和麦克风)
			let stream;
			try {
				message.log('尝试调取本地摄像头/麦克风');
				stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
				message.log('摄像头/麦克风获取成功!');
				localVideo.srcObject = stream;
			} catch {
				message.error('摄像头/麦克风获取失败!');
				return;
			}

			message.log(`------ WebRTC ${target === 'offer' ? '发起方' : '接收方'}流程开始 ------`);
			message.log('将媒体轨道添加到轨道集');
			//stream.getTracks() 获取媒体流中的所有轨道(视频和音频轨道)
			stream.getTracks().forEach(track => {
				//每个轨道添加到 RTCPeerConnection 的轨道集合中,使得这些轨道可以通过 WebRTC 进行传输。
				peer.addTrack(track, stream);
			});

			//根据是否传入 offerSdp 来分两种情况处理,分别是发起方和接收方
			//如果是发起方(offerSdp 为 false):
			if (!offerSdp) {
				message.log('创建本地SDP');
				//创建一个本地的 SDP(Offer)
				const offer = await peer.createOffer();
				//设置本地描述
				await peer.setLocalDescription(offer);
				
				message.log(`传输发起方本地SDP`);
				//将本地 SDP(即 Offer)发送给远端设备。offer 会被序列化为 JSON 格式进行发送。
				socket.send(JSON.stringify(offer));
			} else {
				message.log('接收到发送方SDP');
				await peer.setRemoteDescription(offerSdp);

				message.log('创建接收方(应答)SDP');
				//创建一个应答 SDP(Answer)
				const answer = await peer.createAnswer();
				message.log(`传输接收方(应答)SDP`);
				//将应答 SDP(Answer)发送给远端设备
				socket.send(JSON.stringify(answer));
				await peer.setLocalDescription(answer);
			}
		}
	</script>
</body>
</html>

启动命令:

node index.js启动端口

打开index.html在网页中,分别点击两个链接,然后点击发送方按钮,实现视频通信

注意事项:

我的是笔记本,虚拟机中要能使用摄像头需要有两步操作:

1,选择虚拟机设置中的USB控制器,USB兼容性选USB 3.1

2,选择虚拟机的可移动设备->Quanta HP HD Camera->连接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值