WebRTC(Real-Time Communication)
Q & A
如何发现对方
在P2P通信的过程中,双方需要交换一些元数据比如媒体信息、网络数据等等信息,我们通常称这一过程叫做信令,对应的服务器即 信令服务器,用ws搭建信令服务器
不同的音视频编解码能力如何沟通
在webRTC中,SDP协议,参与音视频通讯的双方想要了解对方支持的媒体格式,必须要交换SDP信息。而交换SDP的过程: 媒体协商
如何联系上对方
在webRTC中,用ICE机制建立网络连接,ICE协议通过一系列的技术(如STUN、TURN服务器)帮助通信双方发现和协商可用的公共网络地址,从而实现 NAT穿越
ICE 工作原理
- 通信双方各自收集自己可用的网络地址,包括:
- 本地私有 IP 地址(局域网内)
- 公网 IP 地址(如果设备直连公网)
- 通过 STUN 服务器获取的公网映射地址(NAT 后的公网端口)
- 通过 TURN 服务器中继的地址(当直接连接失败时作为备用)
- 交换候选地址
- 双方通过信令服务器(如 WebSocket、SIP 等)交换彼此收集到的所有候选地址列表。这是 ICE 协商的前提
- 连接连通性测试
- 双方使用收集到的候选地址对进行“连通性测试”,即尝试从自己的每个候选地址向对方的每个候选地址发送数据包并等待响应
- 建立媒体通道,开始通信
就像两个人想见面,但不知道对方在哪。他们先各自列出所有可能的见面地点(家、公司、咖啡馆、朋友家等),然后互相告诉对方这些地点,接着一个个试哪个地方能碰面,最后选最快最方便的那个地方见面——这就是 ICE 的工作方式
项目搭建
前端项目
采用vue3+vite+ts+elPlus+tailwindcss
bash
npm create vite@latest
npm install tailwindcss @tailwindcss/vite
npm install element-plus --save
创建完成后,直接测试连接后端
ts
import {io, Socket} from 'socket.io-client';
import {onMounted} from 'vue';
const socket: Socket = io('http://localhost:3000');
onMounted(() => {
socket.on('connectionSuccess', () => {
console.log('connectionSuccess from server');
});
});
后端项目
采用node
bash
npm i websocket.io nodemon
启动后端项目,监听3000端口,同时对ws进行监听
js
const socket = require('socket.io');
const http = require('http');
const server = http.createServer();
const io = socket(server, {
cors: {
origin: '*'
}
});
io.on('connection', (sock) => {
console.log('connection');
sock.emit('connectionSuccess');
});
server.listen(3000, () => {
console.log('listening on *:3000');
});
交互
发起呼叫功能
发起呼叫的核心逻辑位于App.vue文件中,通过ElButton组件绑定call方法:
call函数的实现包含以下关键步骤:
状态设置:
jscaller.value = true calling.value = true
媒体流获取:
jsawait getLocalStream()
Socket通信:
jssocket.emit("call", roomId)
Socket call事件发射
当发起呼叫时,客户端会向服务器发送call事件,服务器会将此事件广播给同一房间内的其他用户:
js
// 客户端
socket.emit("call", roomId)
// 服务器端
sock.on("call", (roomId) => {
io.to(roomId).emit("call");
});
acceptCall接听呼叫
接听功能的实现相对简单,主要是发送确认信号:
js
const acceptCall = async () => {
socket.emit("acceptCall", roomId)
}
当用户选择接听时,acceptCall事件会被发送到服务器,服务器会广播给呼叫方。
PeerConnection处理
当呼叫被接受时,发起方会创建RTCPeerConnection并开始建立连接:
ts
socket.on("acceptCall", async () => {
if (caller.value) {
// 创建peer
peer.value = new RTCPeerConnection()
if (peer.value && localStream.value) {
// 添加本地视频流
peer.value.addStream(localStream.value as MediaStream)
// 监听ICE候选事件
peer.value.onicecandidate = (event) => {
if (event.candidate) {
socket.emit("sendIceCandidate", {roomId, candidate: event.candidate})
}
}
// 监听远程流添加事件
peer.value.onaddstream = (event) => {
calling.value = false
communicating.value = true
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}
// 创建Offer并发送
const offer = await peer.value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
})
await peer.value.setLocalDescription(offer)
socket.emit("sendOffer", {roomId, offer})
}
}
})