WebRTC 双向视频通话
一、项目概述
WebRTC(Web Real - Time Communication)是一种支持浏览器之间进行实时通信的技术,它使得在网页上实现音视频通话、文件共享等功能变得更加容易。为了体验这个技术,所以我实现了webrtc - local
二、项目结构
项目主要分为两个主要部分:webrtc - server
(服务端)和 webrtc - client
(客户端)。
1. 服务端(webrtc - server)
服务端使用 Node.js 搭建,借助 socket.io
(也可以是其他)实现信令的一个交换,其实就是一个信息中转的地方。
2. 客户端(webrtc - client)
客户端基于 Vue 3 和 TypeScript 构建,使用 socket.io - client
与服务端进行通信。
三、项目启动步骤
详看 README
四、代码实现分析
1. 服务端(index.js)
服务端使用 https
协议创建服务器,并使用 socket.io
处理实时通信。以下是主要逻辑:
javascript
const socket = require('socket.io')
const https = require('https')
const fs = require('fs')
const path = require('path')
const server = https.createServer({
key: fs.readFileSync(path.join(__dirname, '../cert/key.pem')),
cert: fs.readFileSync(path.join(__dirname, '../cert/cert.pem')),
})
const io = socket(server, {
cors: {
origin: '*', // 配置跨域
},
})
io.on('connection', (sock) => {
console.log('连接成功...')
sock.emit('connectionSuccess')
// 监听客户端进入房间的事件
sock.on('joinRoom', (roomId) => {
sock.join(roomId)
})
// 处理各种视频通话相关事件
sock.on('callRemote', (roomId) => {
io.to(roomId).emit('receiveCall')
})
sock.on('acceptCall', (roomId) => {
io.to(roomId).emit('acceptCall')
})
// 处理 offer、answer 和 candidate 信息
sock.on('sendOffer', ({ roomId, offer }) => {
io.to(roomId).emit('sendOffer', offer)
})
sock.on('sendAnswer', ({ roomId, answer }) => {
io.to(roomId).emit('receiveAnswer', answer)
})
sock.on('sendCandidate', ({ roomId, candidate }) => {
io.to(roomId).emit('receiveCandidate', candidate)
})
sock.on('hangUp', (roomId) => {
io.to(roomId).emit('hangUp')
})
})
server.listen(3001, () => {
console.log('服务器启动成功')
})
服务端主要负责监听客户端的连接和各种事件,用来交换不同客户端的信令等数据。
2. 客户端(App.vue)
客户端使用 Vue 3,结合 socket.io - client
和 RTCPeerConnection
实现视频通话。以下是主要逻辑及相关知识解释:
4.2.1 获取本地音视频流
typescript
// 获取本地音视频流
const getLocalStream = async () => {
// 获取音视频流
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
})
// 将媒体流设置到 video 标签上播放
localVideo.value!.srcObject = stream
// 播放音视频流
localVideo.value!.play()
// 存储本地流
localStream.value = stream
return stream
}
知识解释 :
navigator.mediaDevices.getUserMedia
是 WebRTC 提供的一个 API,用于请求访问用户的摄像头和麦克风。它接受一个约束对象作为参数,该对象指定了需要获取的媒体类型(如视频、音频)以及其他可选的配置。当用户允许访问后,该方法会返回一个 Promise
,该 Promise
会解析为一个 MediaStream
对象,该对象包含了用户的音视频流。
4.2.2 处理视频请求
typescript
// 发起视频请求(发起方)
const callRemote = async () => {
if (calling.value || communicating.value) {
return
}
calling.value = true
// 获取本地音视频流
await getLocalStream()
// 向服务器发送发起视频请求的事件
caller.value = true
socket.value.emit('callRemote', roomId)
}
// 接收视频请求(接受方)
const acceptCall = () => {
// 向服务器发送接受视频请求的事件
socket.value.emit('acceptCall', roomId)
}
知识解释 :
在 WebRTC 视频通话中,发起方首先需要获取本地音视频流,然后通过 socket.io
向服务端发送视频请求。服务端接收到请求后,将其广播给房间内的其他客户端。接收方接收到请求后,可以选择接受或拒绝。如果接受,接收方会向服务端发送接受请求的事件,服务端再将该事件广播给发起方。
4.2.3 交换 offer/answer
typescript
// 发送方收到同意视频事件
sock.on('acceptCall', async () => {
if (caller.value) {
// 发送方
// 创建RTCPeerConnection对象
peer.value = new RTCPeerConnection()
// 添加本地音视频流
peer.value.addStream(localStream.value)
// 生成offer
const offer = await peer.value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
})
// 设置本地描述的offer
await peer.value.setLocalDescription(offer)
// 发送offer
sock.emit('sendOffer', { roomId, offer })
}
})
// 接收方收到offer
sock.on('sendOffer', async (offer: any) => {
if (called.value) {
const stream = await getLocalStream()
// 接收方创建自己的RTCPeerConnection对象
peer.value = new RTCPeerConnection()
// 添加本地音视频流
peer.value.addStream(stream)
// 设置远端描述信息
await peer.value.setRemoteDescription(offer)
// 生成answer
const answer = await peer.value.createAnswer()
// 设置本地描述信息
await peer.value.setLocalDescription(answer)
// 发送answer
sock.emit('sendAnswer', { roomId, answer })
}
})
// 发送方收到接收方的answer
sock.on('receiveAnswer', (answer: any) => {
if (caller.value) {
// 设置远端描述信息
peer.value.setRemoteDescription(answer)
}
})
知识解释:
- offer :发起方通过
RTCPeerConnection.createOffer
方法创建一个offer
,该offer
包含了发起方的会话描述信息,如支持的编解码器、媒体类型等。然后使用RTCPeerConnection.setLocalDescription
方法将该offer
设置为本地描述,并通过socket.io
发送给接收方。 - answer :接收方收到
offer
后,使用RTCPeerConnection.setRemoteDescription
方法设置远端描述,然后通过RTCPeerConnection.createAnswer
方法创建一个answer
,该answer
包含了接收方的会话描述信息。同样,使用RTCPeerConnection.setLocalDescription
方法将该answer
设置为本地描述,并通过socket.io
发送给发起方。 - 会话描述协议(SDP) :
offer
和answer
都是基于会话描述协议(SDP)的,SDP 是一种用于描述多媒体会话的格式,它包含了会话的各种信息,如媒体类型、编解码器、传输地址等。通过交换offer
和answer
,双方可以协商出一个共同支持的会话配置。
4.2.4 交换 candidate 信息
typescript
// 获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
// 向服务器发送candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}
// 接收candidate信息
sock.on('receiveCandidate', async (candidate: any) => {
await peer.value.addIceCandidate(candidate)
})
知识解释:
- ICE(交互式连接建立) :由于双方可能位于不同的网络环境中,需要通过 ICE 机制来找到双方之间的最佳通信路径。ICE 会收集双方的网络地址信息,这些信息被称为
candidate
。 - candidate :
candidate
包含了设备的网络地址和端口信息,可能是本地地址、反射地址(通过 NAT 获得)或中继地址(通过 TURN 服务器获得)。当RTCPeerConnection
对象收集到一个candidate
时,会触发onicecandidate
事件,此时可以将该candidate
通过socket.io
发送给对方。 - addIceCandidate :对方接收到
candidate
后,使用RTCPeerConnection.addIceCandidate
方法将其添加到自己的RTCPeerConnection
对象中,这样双方就可以尝试通过该candidate
建立连接。
五、总结
webrtc - local
项目通过结合 WebRTC 技术和 socket.io
实现了简单的局域
双向视频通话功能,客户端和服务端在同一网络环境下运行。
在实际应用中,可以考虑使用 STUN/TURN 服务器来解决跨网络通信的问题。