WebRTC实战 | 快速搭建1对1音视频通话
WebRTC 系列教程分为三篇进行介绍,本篇为第二篇,上一篇WebRTC 这么火 🔥,前端靓仔,请收下这篇入门教程。
通过上一篇文章,各位小伙伴们应该都了解了 WebRTC 相关概念以及通信过程后,趁热打铁,我们来搭建一个 WebRTC 一对一音视频通话项目。

WebRTC 一对一通话分为服务端和客户端,服务端使用 node 实现信令服务,客户端使用 vue 实现逻辑。
服务端实现:使用 Socket.io 搭建信令服务器
信令服务器主要是处理通信双端进行媒体协商和 ICE 候选(NAT 穿越信息)
按照 Socket.IO 的官网教程很快便能搭建一个聊天应用程序。
-
安装 socket.io 和 express
Terminal window yarn add socket.io express -
使用 express 初始化 socket 服务
let app = express()let http_server = http.createServer(app)http_server.listen(3333)let io = new Server(http_server, {// 允许跨域访问cors: {origin: '*',},})http_server.on('listening', () => {let addr = http_server.address()if (addr) {let port = typeof addr === 'string' ? addr : addr.port}})通过
new Server初始化后,返回的io就是一个 socket 实例 -
监听
connection事件,获取客户端参数使用上一步的 socket 实例监听
connection消息,Socket 会加入由其自己的 id 标识的房间,此 id 是一个随机的 20 个字符的标识符;handshake对象是 Socket.IO 会话开始时发生的握手的一些详细信息,从这里可以拿到客户端传过来的query参数。io.on('connection', (socket) => {const { query } = socket.handshake// 获取socket连接参数 username和roomconst { username, room } = query...}) -
加入房间
当获取到客户端传递的
room房间号后,我们就可以开始加入房间,通过join和leave可以加入和离开房间,即socket.join(room)// 房间人数满了 不再加入if (members.length === 2) {return}// 连接管理let user = { userId: socket.id, username }members.push(user)// 房间管理socket.join(room)// 每次连接向房间发送用户列表io.to(room).emit('userList', members)向当前房间发送用户信息,房间存在两个人时就可以发起音视频通信了
-
监听 WebRTC 信令服务所需要监听的事件
// 接收到《接收者》发送candidate连接成功消息,转发给《接收者》socket.on('candidate', (room, candidate) => {socket.to(room).emit('candidate', candidate)})// 接收到《发起者》发送offer,转发给《接收者》socket.on('offer', (room, offer) => {socket.to(room).emit('offer', offer)})// 接收到《接收者》发送answer,转发给《发起者》socket.on('answer', (room, answer) => {socket.to(room).emit('answer', answer)})注册以上三种事件,用于处理 WebRTC 双端进行媒体协商和 ICE 候选的过程
-
离开房间/断开连接
除了
disconnect表示 socket 在断开连接时触发,还有disconnecting也有同样的作用,可以看下官网对这两个特殊事件的说明socket.on('disconnect', () => {members = members.filter((m) => m.username !== user.username)// 断开连接发送用户列表io.to(room).emit('userList', members)})当客户端调用
socket.disconnect()断开连接时,服务端向房间内推送当前用户信息,客户端做相应处理。
到这里,信令服务器服务端的工作已经完成,接下来我们看下客户端。
客户端实现:搭建 1 对 1 音视频聊天
客户端使用 vite + vue + antd 搭建,初始化 vue 项目后开始下列操作
采集媒体流
因为需要同时显示本地和远端视频,所以需要两个<video>标签
<template> <div class="video"> <section class="local-video"> <video ref="localVideoRef"></video> </section> <section class="remote-video"> <video ref="remoteVideoRef"></video> </section> </div> ...</template>
<script setup>import Video from './Video.vue'import { ref, onMounted } from 'vue'
const emits = defineEmits(['streamSuccess', 'leave'])
const remoteVideoRef = ref(null)const localVideoRef = ref(null)
onMounted(() => initVideo(localVideoRef.value.$el))// 初始化本地视频const initVideo = async (video) => { if (!video) return try { let config = { video: true, audio: true, } let stream = await navigator.mediaDevices.getUserMedia(config) video.srcObject = stream emits('streamSuccess', { stream, remoteVideoRef }) video.play() } catch (e) { console.log(`error: `, e) }}...</script>在挂载时,开始初始化本地视频:
使用navigator.mediaDevices.getUserMedia(config)获取到视频流后,通过video.srcObject可以将获取到的视频流在 video 元素上进行播放。
初始化 Socket.io 客户端
在采集媒体流完成后,回调streamSuccess方法,开始初始化 socket 服务
// 等待本地视频初始化完成后发送信令服务const streamSuccess = ({ stream, remoteVideoRef }) => { const info = { ...userInfo, localStream: stream, remoteVideoRef } socket = initSocket(info)}// initSocketconst initSocket = ({ username, room, remoteVideoRef, localStream }) => { let localPc // 连接server 携带username和room const socket = io('http://localhost:3333', { path: '/', query: { username, room }, }).connect()
// 当有人加入房间时 socket.on('userList', async (res) => { // 房间少于两人时 对方掉线 则关闭对方视频 if (res.length < 2) { let video = remoteVideoRef.value.$el video.srcObject = null return } if (username === res[0]?.username) sendOffer() })}我这里的逻辑是:当有人加入房间时,服务端 Socket 发送房间人数列表,当房间存在两个人时,第一个加入的人作为呼叫方,第二个人作为被叫方,双方开始媒体协商
媒体协商
上面我们在说到房间存在两人时,呼叫方开始发送 offer,即sendOffer方法,在此方法内部需要做以下事情:
创建 RTCPeerConnection 对象
我们在上篇文章提到 RTCPeerConnection 对象的作用,还不太清楚的小伙伴可以回头再看一下。
为了让 WebRTC 的相关 api 在各个浏览器中都能够正常的运行,强烈建议使用补充库,例如强大并且被广泛支持的 Adapter.js,引入它可以确保 WebRTC 在各个浏览器中的兼容性。
两种方法引入:
-
直接引入
在 html 中添加
https://webrtc.github.io/adapter/adapter-latest.js -
npm 方式
Terminal window yarn add webrtc-adapter在项目中引入即可
// 仅仅只需要引入 adapter 即可import adapter from 'webrtc-adapter'
这里的rtcConfig用于设置新连接的选项:
export const rtcConfig = { iceServers: [ { urls: ['stun:stun.l.google.com:19302'], }, { urls: ['turn:wangxiang.website:3478'], username: 'admin', credential: 'admin', }, ],}如上所示,我们配置了iceServers,数组内每个对象描述一个可能被 ICE 代理使用的服务器,也就是 STUN 或 TURN 服务器。如果未指定,则将在没有可用的 STUN 或 TURN 服务器的情况下进行连接尝试,就只能使用 host 类型的 candidate 尝试连接,即局域网内 P2P 通讯。
const sendOffer = async () => { // 初始化RTCPeerConnection对象 localPc = new RTCPeerConnection(rtcConfig) // 添加RTC流 localStream.getTracks().forEach((track) => { localPc.addTrack(track, localStream) }) // 创建offer
// 收集candidate}处理 offer
这里通过createOffer方法创建 offer,通过setLocalDescription存储为本地描述,接着通过信令服务将 offer 传递到被叫方。
const sendOffer = async () => { // 初始化RTCPeerConnection对象 // 添加RTC流 // 处理offer let offer = await localPc.createOffer() await localPc.setLocalDescription(offer) socket.emit('offer', room, offer)}处理 candidate
当通过setLocalDescription方法更改本地描述之后,RTCPeerConnection 对象会抛出icecandidate事件。通过监听此事件,可以获取到收集到的 candidate,接着通过信令服务器将 candidate 传递到被叫方。
const sendOffer = async () => { // 初始化RTCPeerConnection对象 // 添加RTC流 // 创建offer // 处理 candidate localPc.onicecandidate = (event) => { console.log('localPc:', event.candidate, event) if (event.candidate) socket.emit(SOCKET_ON_RTC.CANDIDATE, room, { pc: 'local', candidate: event.candidate, }) }呼叫方 offer 发送完毕,等待被叫方传回 answer。

被叫方处理
被叫方首先通过信令服务器获取到 offer:
socket.on('offer', async (offer) => { console.log(`接收到offer`, offer) // 存储offer 发送answer sendAnswer(offer)})接着在sendAnswer方法中执行与呼叫方同样的操作,即创建 RTCPeerConnection 对象、处理 answer、处理 candidate 等
const sendAnswer = offer => { localPc = new RTCPeerConnection(rtcConfig) // 添加RTC流 localStream.getTracks().forEach((track) => { localPc.addTrack(track, localStream) }) // 处理answer await localPc.setRemoteDescription(offer) const answer = await localPc.createAnswer() await localPc.setLocalDescription(answer) socket.emit('answer', room, answer) // 处理candidate localPc.onicecandidate = (event) => { console.log('localPc:', event.candidate, event) if (event.candidate) socket.emit('candidate', room, { pc: 'remote', candidate: event.candidate }) }}呼叫方接收 answer
// 接收answersocket.on('answer', async (answer) => { console.log(`接收到answer`, answer) // 完善本地remote描述 await localPc.setRemoteDescription(answer)})媒体协商过程结束。
建立连接
icecandidate 收集后,通过信令服务器发送到对端,双端交换 candidate 信息,建立连接。
localPc.addIceCandidate(candidate)处理音视频数据
初始化 RTCPeerConnection 对象完成后,即localPc,我们调用了addTrack()方法可以将本地视频流localStream的每个轨道添加到localPc对象的一组轨道上。
当建立连接完成后,这些 tracks 会源源不断的传输给对端,也就是我们称的推流。
当对端收到 tracks 推送时,通过回调函数localPc.ontrack,就可以拿到远程推送的流媒体对象:
let video = remoteVideoRef.value.$ellocalPc.ontrack = (e) => { video.srcObject = e.streams[0] video.oncanplay = () => video.play()}当互相拿到对方的流媒体对象时,将流媒体通过video.srcObject赋给<video />标签,此时音视频数据就展现在页面上了 🎉。
更多
通过 RTCDataChannel 传输文本、文件等
RTCDataChannel 接口是 WebRTC API 的一个功能,可以为通信双方打开一个通道,通过该通道发送和接收任意数据,如:文本、文件等
dataChannel = RTCPeerConnection.createDataChannel(label[, options]);通过 RTCPeerConnection 提供的createDataChannel方法来创建 RTCDataChannel,当通道连接成功后,触发回调函数onopen,这时候就可以通过dataChannel.send(message)发送消息了。
let dataChannelconst sendMessage = (username) => { const button = document.querySelector('.data-channel__button') const input = document.querySelector('.data-channel__input') button.disabled = false button.onclick = () => { if (!input.value) return const message = `${username}: ${input.value}` dataChannel.send(message) input.value = '' receiveMessage(message) }}const receiveMessage = (message) => { const output = document.querySelector('.data-channel__output') output.scrollTop = output.scrollHeight //窗口总是显示最后的内容 output.value = output.value + message + '\r'}const openDataChannel = (localPc, username) => { // 创建datachannel通道 dataChannel = localPc.createDataChannel('test') // datachannel通道打开 开始发送消息 dataChannel.onopen = () => sendMessage(username) localPc.ondatachannel = (event) => { // 成功拿到 RTCDataChannel const dataChannel = event.channel dataChannel.onmessage = (event) => receiveMessage(event.data) }}
export default openDataChannel通过监听ondatachannel事件,获取到对端的RTCDataChannel,接着监听onmessage事件,即可拿到对端传输的消息。
中继方式:搭建 TURN 服务器
我们上面搭建的音视频通信只能在同一网络中进行,如果不同网络连接就会失败,这时候就需要 TURN 服务器来解决这个问题。
TURN 服务有两个作用:一是提供 STUN 服务,客户端通过 STUN 服务获取自己的公网地址;二是提供数据中继服务(当数据通信双方无法通过 P2P 传输时,就需要通过中继的方式让通信双方的数据可以互通)。
-
下载源码
目前最著名的 TURN 服务器是由 Google 发起的开源项目 coturn,coturn 服务器完整的实现了 STUN/TURN/ICE 协议,支持 P2P 穿透防火墙。
Terminal window git clone https://github.com/coturn/coturn -
生成 Markfile
Terminal window cd coturn./configure --prefix=/usr/local/coturn需要安装 libevent 库,不然会报错

coturn-make-error.png -
安装 libevent
如果已经安装过,则忽略这一步
首先安装运行 coturn 需要依赖的环境
Terminal window yum install openssl openssl-libs libevent2 libevent-devel如果 libevent2 安装失败,手动安装 下载地址:http://libevent.org/
Terminal window # 解压tar -xf libevent-2.0.22-stable.tar.gzcd ibevent-2.0.22-stable# 生成Markfile./configure --prefix=/usr# 执行make编译libeventmake# 安装make install完成后,测试是否安装成功
Terminal window ls -a /usr/lib |grep libevent
libevent-success.png -
继续安装 coturn
Terminal window ./configuremakemake install -
验证 coturn 服务是否安装成功
/usr/local/bin/turnserver which turnserver -
生成签名
Terminal window cd /usr/local/etc/openssl req -x509 -newkey rsa:2048 -keyout /usr/local/etc/turn_server_pkey.pem -out /usr/local/etc/turn_server_cert.pem -days 99999 -nodes一路回车即可,完成后在/use/local/etc 目录下就有了
turn_server_pkey.pem和turn_server_cert.pem两个文件。 -
修改配置文件
Terminal window cp turnserver.conf.default turnserver.confvim turnserver.confTerminal window # 网卡listening-device=eth0# 监听的端口listening-port=3478# 绑定的公网地址external-ip=43.140.xxx.xx# 用户名和密码user=admin:123456# 名称realm=admin -
启动 turnserver
Terminal window ./turnserver ../etc/turnserver.conf查看是否启动成功
Terminal window ps -ef|grep turnserver -
检测
在线检测 ICE 穿透的地址:https://webrtc.github.io…
上面这个地址用于检测 turn 服务是否可用,注意
查看服务器端口是否已开启3478`
coturn-server-success.png 如果你测试一个 STUN 服务器,你能收集到一个类型为“srflx”的候选者,它就可以工作。如果你测试一个 TURN 服务器,你能收集到一个类型为“relay”的候选人,它就会工作。
-
搭建完成后,将可以将此链接复制,作为 RTCPeerConnection 连接时的 iceServers 配置。完成后,在不同网络之间,也能实现音视频通话了。

总结
本篇文章是在之前原理的基础上,从 0 到 1 实现一对一的音视频通话,接下来还有 WebRTC 多人通信的架构,以及如何使用 LiveKit 快速搭建多人音视频通话。
以上就是本文的全部内容,希望这篇文章对你有所帮助,欢迎点赞和收藏 🙏,如果发现有什么错误或者更好的解决方案及建议,欢迎随时联系。