什么是WebRTC?
WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。
WebRTC的应用场景
- 点对点视频聊天:如 微信视频 等实时视频通话应用。
- 多人视频会议:企业级多人视频会议系统,如飞书、钉钉、腾讯会议等。
- 在线教育:如腾讯课堂、网易云课堂等。
- 直播:游戏直播、课程直播等。
WebRTC的实现过程
- 1.server端新建socket服务(作为信令服务器),当用户进入客户端的时候将用户端与socket建立连接
- 2.当客户端与server端建立连接后,客户端会向server端发起一个加入房间的事件,并携带房间id
- 3.server端监听到加入房间的事件后,会将房间id添加到指定房间中,这样,所有加入同一个房间的客户端都可以接收到在该房间内发送的事件或消息。
- 4.将客户端用户分为发起方和接收方
- 5.当客户端用户发起方发起通话时会向server端发起通话的事件
- 6.当server端接收到这个事件后,会向在这个房间中的所有客户端广播通话事件
- 7.当客户端用户接收方接收到这个事件,如果接收方同意了这个请求则会向server端发送同意视频的事件反之,如果挂断的话会向server端发起拒绝视频的事件
- 8.当server端收到同意视频事件后会向客户端广播同意视频的事件
- 9.当客户端用户发送方收到这个事件后,创建 RTCPeerConnection() 对象,并将音视频流加入到里面,并且将,网络信息和媒体流信息通过事件发送到server端,同时会通过 onaddstream 事件来获取对方的音视频流
- 10.当serve端接收到这些事件后会将这些事件已广播的形式返回,当接收方收到这些事件后会将这些信息添加到 RTCPeerConnection 中,同时接收方也会将这些信息通过事件发送到服务端,同时会通过 onaddstream 事件来获取对方的音视频流
- 11.实现视频通话
总结:上述过程就是通过 WebRTC 提供的 API 获取各端的媒体信息 SDP 以及 网络信息 candidate ,并通过信令服务器交换,进而建立了两端的连接通道完成实时视频语音通话。
代码实现过程
1.建立socket连接
sock.on('connectionSuccess', () => {
console.log('连接成功');
//前端连接成功之后需要向服务器发送一个加入房间的事件
sock.emit('joinRoom', roomId)
})
2.服务器接收到事件后将roomId添加到房间中
//监听加入房间的事件
sock.on('joinRoom', (roomId) => {
sock.join(roomId);
})
3.发起方发起视频请求,发送请求事件,同时将视频流输出到video中
// 发起方发起视频请求
const callRemote = async () => {
console.log('发起视频');
//用户A向用户B发起视频请求
caller.value = true //表示当前用户是发起方,发起了视频请求
calling.value = true //表示是否呼叫中
//在发起视频的时候讲视频流输出到页面上
await getLocalStream()
//通过信令服务器向用户B发起视频请求,?socket.value存在且非null或undefined的情况下,才进行方法调用,
socket.value?.emit('callRemote', roomId)
}
//获取视频流方法
//获取本地音视频流,并进行播放
const getLocalStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
localVideo.value!.srcObject = stream
localVideo.value!.play()
localStream.value = stream
return stream
}
4.服务器接收到事件,对事件进行广播
//监听发起视频通话的请求
sock.on('callRemote', (roomId) => {
//广播事件
io.to(roomId).emit('callRemote');
})
5.接收方接收到广播事件,改变页面状态
// 监听服务器发来的视频请求
sock.on('callRemote', () => {
//如果是发送方自己收到了这个callRemote事件,那么就直接忽略
if (!caller.value) {//不是发送方才会处理
called.value = true//接听方
calling.value = true//视频通话中
}
})
6.接收方同意视频邀请
// 接收方同意视频请求
const acceptCall = () => {
console.log('同意视频邀请');
//通过信令服务器通知用户A
socket.value?.emit('acceptCall', roomId)
}
7.服务器通知发起方用户接受了视频邀请
//通知其他用户接收了通话请求
sock.on("acceptCall", (roomId) => {
io.to(roomId).emit("acceptCall");
})
8.发起方接收到了事件,将candidate信息和offer信息通过事件传递到server中 注意:如果要将此项目部署到外部进行访问则需要搭建coturn服务,此项目支持视频音频以及屏幕的音视频流,如果需要更改 peer.value.addStream(localStream.value)
//监听接收了通话请求
sock.on("acceptCall", async () => {
if (caller.value) {//如果是发送方才会处理其他的逻辑
//用户A收到用户B同意视频的请求
peer.value = new RTCPeerConnection()
//添加本地音视频流
peer.value.addStream(localStream.value)
// 通过监听onicecandidate事件获取candidate信息(网络信息)
peer.value.onicecandidate = (event: any) => {
// console.log('用户A的candidate信息', event.candidate);
if (event.candidate) {
//向服务器发送自己的candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}
//监听onaddstream来获取对方的音视频流
peer.value.onaddstream = (event: any) => {
console.log('用户A收到用户B的stream', event.stream);
communicating.value = true
calling.value = false
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}
//生成offer
const offer = await peer.value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
})
console.log(offer);
//在本地设置offer信息
await peer.value.setLocalDescription(offer)
//发送offer
sock.emit('sendOffer', { offer, roomId })
}
})
9.server端接收到事件,将事件广播
//接收offer
sock.on('sendOffer', ({ offer, roomId }) => {
io.to(roomId).emit('sendOffer', offer);
})
//收到sendCandidate信息
sock.on('sendCandidate', ({ roomId, candidate }) => {
io.to(roomId).emit('sendCandidate', candidate);
})
10.接收方接收到了事件
//收到offer
sock.on("sendOffer", async (offer) => {
if (called.value) {//接收端收到offer之后打印
console.log('收到offer', offer);
// 用户B需要创建自己的RTCPeerConnection,
//添加本地音视频流,设置远端描述信息,生成answer,并且通过信令服务器发送给用户A
// 创建自己的RTCPeerConnection
peer.value = new RTCPeerConnection()
//添加本地音视频流
const stream = await getLocalStream()
peer.value.addStream(stream)
// 通过监听onicecandidate事件获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
//向服务器发送自己的candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}
peer.value.onaddstream = (event: any) => {
console.log('用户B收到用户A的stream', event.stream);
communicating.value = true
calling.value = false
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}
//设置远端描述信息 SDP
await peer.value.setRemoteDescription(offer)
//生成answer
const answer = await peer.value.createAnswer()
console.log('用户B生成answer', answer);
//在本地设置answer信息
await peer.value.setLocalDescription(answer)
//发送answer
sock.emit('sendAnswer', { answer, roomId })
}
})
//收到candidate信息
sock.on("sendCandidate", async (candidate) => {
console.log('收到candidate信息', candidate);
await peer.value.addIceCandidate(candidate)
})
11.server端收到了sendCandidate以及sendAnswer事件,并将事件返回
//接收answer
sock.on('sendAnswer', ({ answer, roomId }) => {
io.to(roomId).emit('sendAnswer', answer);
})
//收到sendCandidate信息
sock.on('sendCandidate', ({ roomId, candidate }) => {
io.to(roomId).emit('sendCandidate', candidate);
})
12.发送方收到了sendAnswer事件
//用户A收到用户B的answer
sock.on("sendAnswer", async (answer) => {
if (caller.value) {
console.log('用户A收到answer', answer);
//设置远端描述信息 SDP
await peer.value.setRemoteDescription(answer)
}
})
实现原理图
最终代码
Client
npm create vite@latest webrtc-client -- --template vue-ts
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
在生成的 tailwind.config.js 配置文件中添加所有模板文件的路径。
/** @type {import('tailwindcss').Config} /
module.exports = {
content: [
"./index.html",
* "./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
修改 style.css 中的内容如下:
@tailwind base;
@tailwind components;
@tailwind utilities;
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { io, Socket } from 'socket.io-client'
//定义房间Id
const roomId = "001"
const called = ref<boolean>(false) // 是否是接收方
const caller = ref<boolean>(false) // 是否是发起方
const calling = ref<boolean>(false) // 呼叫中
const communicating = ref<boolean>(false) // 视频通话中
const localVideo = ref<HTMLVideoElement>() // video标签实例,播放本人的视频
const remoteVideo = ref<HTMLVideoElement>() // video标签实例,播放对方的视频
const socket = ref<Socket>()
const peer = ref<any>()
const localStream = ref<any>()
onMounted(() => {
// 初始化socket
const sock = io('http://localhost:3000')
sock.on('connectionSuccess', () => {
console.log('连接成功');
//前端连接成功之后需要向服务器发送一个加入房间的事件
sock.emit('joinRoom', roomId)
})
// 监听服务器发来的视频请求
sock.on('callRemote', () => {
//如果是发送方自己收到了这个callRemote事件,那么就直接忽略
if (!caller.value) {//不是发送方才会处理
called.value = true//接听方
calling.value = true//视频通话中
}
})
//监听接收了通话请求
sock.on("acceptCall", async () => {
if (caller.value) {//如果是发送方才会处理其他的逻辑
//用户A收到用户B同意视频的请求
peer.value = new RTCPeerConnection()
//添加本地音视频流
peer.value.addStream(localStream.value)
// 通过监听onicecandidate事件获取candidate信息(网络信息)
peer.value.onicecandidate = (event: any) => {
// console.log('用户A的candidate信息', event.candidate);
if (event.candidate) {
//向服务器发送自己的candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}
//监听onaddstream来获取对方的音视频流
peer.value.onaddstream = (event: any) => {
console.log('用户A收到用户B的stream', event.stream);
communicating.value = true
calling.value = false
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}
//生成offer
const offer = await peer.value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
})
console.log(offer);
//在本地设置offer信息
await peer.value.setLocalDescription(offer)
//发送offer
sock.emit('sendOffer', { offer, roomId })
}
})
//收到offer
sock.on("sendOffer", async (offer) => {
if (called.value) {//接收端收到offer之后打印
console.log('收到offer', offer);
// 用户B需要创建自己的RTCPeerConnection,
//添加本地音视频流,设置远端描述信息,生成answer,并且通过信令服务器发送给用户A
// 创建自己的RTCPeerConnection
peer.value = new RTCPeerConnection()
//添加本地音视频流
const stream = await getLocalStream()
peer.value.addStream(stream)
// 通过监听onicecandidate事件获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
//向服务器发送自己的candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}
peer.value.onaddstream = (event: any) => {
console.log('用户B收到用户A的stream', event.stream);
communicating.value = true
calling.value = false
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}
//设置远端描述信息 SDP
await peer.value.setRemoteDescription(offer)
//生成answer
const answer = await peer.value.createAnswer()
console.log('用户B生成answer', answer);
//在本地设置answer信息
await peer.value.setLocalDescription(answer)
//发送answer
sock.emit('sendAnswer', { answer, roomId })
}
})
//用户A收到用户B的answer
sock.on("sendAnswer", async (answer) => {
if (caller.value) {
console.log('用户A收到answer', answer);
//设置远端描述信息 SDP
await peer.value.setRemoteDescription(answer)
}
})
//收到candidate信息
sock.on("sendCandidate", async (candidate) => {
console.log('收到candidate信息', candidate);
await peer.value.addIceCandidate(candidate)
})
sock.on('hangUp', () => {
called.value = false
caller.value = false
calling.value = false
communicating.value = false
peer.value = null
if (localVideo.value) {
const tracks = localVideo.value.srcObject!.getTracks();
tracks.forEach(function (track: any) {
track.stop();
});
localVideo.value.srcObject = null;
}
if (remoteVideo.value) {
const tracks = remoteVideo.value.srcObject!.getTracks();
tracks.forEach(function (track: any) {
track.stop();
});
remoteVideo.value!.srcObject = null
}
localStream.value = undefined
console.log("视频已挂断")
})
socket.value = sock
})
//获取本地音视频流,并进行播放
const getLocalStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
localVideo.value!.srcObject = stream
localVideo.value!.play()
localStream.value = stream
return stream
}
// 发起方发起视频请求
const callRemote = async () => {
console.log('发起视频');
//用户A向用户B发起视频请求
caller.value = true //表示当前用户是发起方,发起了视频请求
calling.value = true //表示是否呼叫中
//在发起视频的时候讲视频流输出到页面上
await getLocalStream()
//通过信令服务器向用户B发起视频请求,?socket.value存在且非null或undefined的情况下,才进行方法调用,
socket.value?.emit('callRemote', roomId)
}
// 接收方同意视频请求
const acceptCall = () => {
console.log('同意视频邀请');
//通过信令服务器通知用户A
socket.value?.emit('acceptCall', roomId)
}
// 挂断视频
const hangUp = () => {
console.log('挂断视频');
socket.value?.emit('hangUp', roomId)
}
</script>
<template>
<div class="flex items-center flex-col text-center p-12 h-screen">
<div class="relative h-full mb-4">
<video ref="localVideo" class="w-96 h-full bg-gray-200 mb-4 object-cover" muted="true"></video>
<video ref="remoteVideo" class="w-32 h-48 absolute bottom-0 right-0 object-cover"></video>
<div v-if="caller && calling" class="absolute top-2/3 left-36 flex flex-col items-center">
<p class="mb-4 text-white">等待对方接听...</p>
<img @click="hangUp" src="/refuse.svg" class="w-16 cursor-pointer" alt="">
</div>
<div v-if="called && calling" class="absolute top-2/3 left-32 flex flex-col items-center">
<p class="mb-4 text-white">收到视频邀请...</p>
<div class="flex">
<img @click="hangUp" src="/refuse.svg" class="w-16 cursor-pointer mr-4" alt="">
<img @click="acceptCall" src="/accept.svg" class="w-16 cursor-pointer" alt="">
</div>
</div>
</div>
<div class="flex gap-2 mb-4">
<button class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white"
@click="callRemote">发起视频</button>
<button class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white" @click="hangUp">挂断视频</button>
</div>
</div>
</template>
server端
npm install socket.io nodemon
const socket = require('socket.io');
const http = require('http');
const server = http.createServer()
const io = socket(server, {
cors: {
origin: '*' // 配置跨域
}
});
//监听connection消息
io.on('connection', sock => {
console.log('连接成功...')
// 向客户端发送连接成功的消息
sock.emit('connectionSuccess');
//监听加入房间的事件
sock.on('joinRoom', (roomId) => {
sock.join(roomId);
})
//监听发起视频通话的请求
sock.on('callRemote', (roomId) => {
//广播事件
io.to(roomId).emit('callRemote');
})
//通知其他用户接收了通话请求
sock.on("acceptCall", (roomId) => {
io.to(roomId).emit("acceptCall");
})
//接收offer
sock.on('sendOffer', ({ offer, roomId }) => {
io.to(roomId).emit('sendOffer', offer);
})
//接收answer
sock.on('sendAnswer', ({ answer, roomId }) => {
io.to(roomId).emit('sendAnswer', answer);
})
//收到sendCandidate信息
sock.on('sendCandidate', ({ roomId, candidate }) => {
io.to(roomId).emit('sendCandidate', candidate);
})
//挂断视频
sock.on('hangUp', (roomId) => {
io.to(roomId).emit('hangUp');
})
})
server.listen(3000, () => {
console.log('服务器启动成功');
});