WebRTC实现本地视频通话 | 全流程详解

源码见仓库 Breezli/WebRTC_Demo: 基于WebRTC技术实现的本地网页端视频通话

为了更好的项目体验,本文的项目引用了三张svg,位于github仓库的public目录下

分为前后端两个文件夹

webrtc-client (客户端)

webrtc-server (服务端)

预先准备

webrtc-client

前端页面绘制

1
2
3
4
5
6
依赖下载
pnpm create vite@latest webrtc-client -- --template vue-ts
cd webrtc-client
pnpm install -D tailwindcss@3.4.17 postcss autoprefixer
pnpm install socket.io-client
npx tailwindcss init -p

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{
"name": "webrtc-client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"socket.io-client": "^4.8.1",//重要依赖,并且是运行时依赖,要下在生产环境
"vue": "^3.5.13"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",//自动添加css前缀
"postcss": "^8.4.49",
"tailwindcss": "^4.1.11",//tailwindcss样式库(不用也罢,可以简单画一个页面)
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
}
}
App.vue
<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"></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>
main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

style.css 这里使用了tailwind的样式库

1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;

tailwind.config.js 此配置文件中添加所有模板文件的路径

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

到此前端页面绘制完成

1
pnpm dev

webrtc-server

初始化服务端项目,在 webrtc-server 目录下

1
pnpm init

socket.io 封装了websocket应用创建服务

nodemon 充当热更新功能

1
pnpm install socket.io nodemon

package.json 添加start命令,使用nodemon启动项目

1
2
3
4
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},

创建index.js(最初版,之后后端的所有操作都在这个文件上进行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const socket = require('socket.io');//封装了websocket应用创建服务
const http = require('http');//引入http模块

const server = http.createServer()//创建服务器

const io = socket(server, {
cors: {
origin: '*' // 配置跨域
}
});

/////接下来的主体部分/////
io.on('connection', sock => {
console.log('连接成功...')
sock.emit('connectionSuccess');// 向客户端发送连接成功的消息
})

//...

io.on('disconnect', () => {
console.log('连接断开');
sock.emit('connectionFalse');// 向客户端发送连接断开的消息
});
/////接下来的主体部分/////

server.listen(3000, () => {
console.log('服务器启动成功');
});

!!!插播socket重要指令!!!

1
2
.emit   发送事件
.on 监听事件

常见内置事件说明

事件名称 触发场景 使用位置
connect 客户端成功连接到服务端时 客户端
connection 服务端接收到新客户端连接时 服务端
disconnect 连接断开时 客户端/服务端
error 连接发生错误时 客户端/服务端

后端至此搭建完成,在3000端口可启动node服务

1
pnpm run start

前端连接信令服务器

依靠的就是socket.io-client

接下来我们将在App.vue中开始编写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { io, Socket } from "socket.io-client";

const socket = ref<Socket>() // 前端Socket实例

onMounted(() => {
const sock = io('localhost:3000'); // 通过 io('localhost:3000') 创建的 Socket.IO 客户端实例

// 连接成功
sock.on('connectionSuccess', () => {
console.log('连接成功')
});

socket.value = sock;// 存储Socket实例
})
</script>

还记得吗,就在前两步,服务端在连接成功后向前端.emit(发送)了connectionSuccess事件

1
2
3
4
io.on('connection', sock => {
console.log('连接成功...')
sock.emit('connectionSuccess');// 向客户端发送连接成功的消息
})

而在此时的前端加入的

1
2
3
sock.on('connectionSuccess', () => {// 接收到服务端发来的connectionSuccess(连接成功)事件
console.log('连接成功')
});

交互逻辑在这里就正式开始了

发起视频请求

获取本地音视频流

前端

1
2
3
4
5
6
7
8
9
10
11
const getLocalStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({// 获取音视频流
video: true,
audio: true,
})
localVideo.value!.srcObject = stream // 将媒体流设置到 video 标签上播放
localVideo.value!.play() // 播放音视频流
localStream.value = stream // 存储本地流

return stream
}

“房间”的概念

不妨先停下来思考一下,为什么在同一个网页,不同的客户端可以看见不同的页面,和实时共享的内容

img

我们先来看一看之前给前端写的组件(layout省略版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<video ref="localVideo"></video>    //本地视频
<video ref="remoteVideo"></video> //接收方视频(位于右下角)

<div v-if="caller && calling"> //状态参数---caller:发送方---calling:呼叫中
<p>等待对方接听...</p>
<img @click="hangUp" src="/refuse.svg"/> //挂断
</div>

<div v-if="called && calling"> //状态参数---called:接收方---calling:呼叫中
<p>收到视频邀请...</p>

<img @click="hangUp" src="/refuse.svg"/> //挂断
<img @click="acceptCall" src="/accept.svg"/>//接听

<button @click="callRemote">发起视频</button>
<button @click="hangUp">挂断视频</button>
</div>

不难发现我们是依靠状态参数来控制页面显示的

本项目参数表如下

1
2
3
4
5
6
7
8
9
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 localStream = ref<MediaStream>() // 本地流

const roomId = '001' // 房间ID

顺便带上实例创建

1
2
const socket = ref<Socket>() // Socket实例
const peer = ref<any>() // RTCPeerConnection实例

而对于我们这个项目,房间的概念就很简单

A (左) 向B (右) 发起接通请求,在B接收请求后,双方都将加入这个视频通话的”房间”

房间 可以通过自主分配id划分,每个房间也可以加入很多用户 (如直播系统)

服务端含有加入房间的操纵代码 sock.join(Id)

加入房间请求

前端

1
2
3
4
sock.on('connectionSuccess', () => {
console.log('连接服务器成功...');
sock.emit('joinRoom', roomId) // 前端发送加入房间事件
})

后端

1
2
3
sock.on('joinRoom', (roomId) => {
sock.join(roomId) // 加入房间
})

发起视频请求

A方按钮触发函数

前端

1
2
3
4
5
6
7
const callRemote = async () => {
console.log('发起视频');// 状态更新
caller.value = true;// 发送方标记
calling.value = true;// 呼叫中
await getLocalStream()// 获取本地音视频流函数
socket.value?.emit('callRemote', roomId)// 发送视频请求事件
}

后端

1
2
3
sock.on('callRemote', (roomId) => {// 收到发送方的视频请求事件
sock.to(roomId).emit('receiveCall')// 向这个房间中的其他人广播事件receiveCall
})

前端

1
2
3
4
5
6
sock.on('receiveCall', () => {// 房间内所有接收方监听到视频请求事件
if (!caller.value) {// 非发起方
calling.value = true// 呼叫中
called.value = true// 接收方标记
}
})

此时A(发起)方启动摄像头和麦克风,并等待对方接听

B(接收)方出现 接听/挂断 按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div v-if="caller && calling">  //状态参数---caller:发送方---calling:呼叫中
<p>等待对方接听...</p>
<img @click="hangUp" src="/refuse.svg"/> //挂断
</div>

<div v-if="called && calling"> //状态参数---called:接收方---calling:呼叫中
<p>收到视频邀请...</p>

<img @click="hangUp" src="/refuse.svg"/> //挂断
<img @click="acceptCall" src="/accept.svg"/>//接听

<button @click="callRemote">发起视频</button>//发起
<button @click="hangUp">挂断视频</button> //挂断
</div>

接收 / 挂断 视频

趁着现在,我们添上其他按钮绑定的函数

1
2
3
4
5
6
7
8
// 挂断视频
const hangUp = () => {
socket.value.emit('hangUp', roomId)
}
// 接收视频请求(接收方)
const acceptCall = () => {
socket.value.emit('acceptCall', roomId)
}

接着来看后端怎么处理

1
2
3
4
5
6
7
8
// 挂断视频
sock.on('hangUp', (roomId) => {
io.to(roomId).emit('hangUp')// 向Id房间内所有人广播这个事件
})
// 接收视频请求
sock.on('acceptCall', (roomId) => {
io.to(roomId).emit('acceptCall')// 向Id房间内所有人广播这个事件
})

返回前端,处理我们接收到的 hangUp || acceptCall

挂断视频

1
2
3
sock.on('hangUp', () => {
reset()// 重置状态
})

重置状态函数

1
2
3
4
5
6
7
8
9
10
11
const reset = () => {
called.value = false
caller.value = false
calling.value = false
communicating.value = false
peer.value = null
localVideo.value!.srcObject = null // 关闭本地视频
remoteVideo.value!.srcObject = null // 关闭远程视频
localStream.value?.getTracks()[0].stop() // 关闭本地流
// localStream.value = undefined // 关闭本地流(相同效果)
}

接受视频请求

而这个就变复杂了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sock.on('acceptCall', async () => {
if (caller.value) {// 发送方
peer.value = new RTCPeerConnection()// 创建RTCPeerConnection对象
peer.value.addStream(localStream.value)// 添加本地音视频流

peer.value.onicecandidate = (event: any) => {// 获取candidate信息
if (event.candidate) {
sock.emit('sendCandidate', { roomId, candidate: event.candidate })// 向服务器发送candidate信息
}
}
// 获取对方的音视频流
peer.value.onaddstream = (event: any) => {
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)// 设置本地描述的offer
sock.emit('sendOffer', { roomId, offer })// 发送offer
}
})

交换 SDP 信息和 candidate 信息

概念

SDP :WebRTC的一种协议,用于描述设备支持的媒体格式

candidate :网络信息

媒体协商 :交换SDP

ICE :WebRTC的通信机制

工作原理

1.双方收集本地网络地址(分 共有 | 私有)+ STUN 和 TURN 服务器获取候选地址

2.双方通过信令服务器交换这些候选地址

3.双方使用候选地址进行连接测试,确定最佳的可用地址,开始实时音视频通话

媒体协商

接下来是最重要的信令交换步骤(WebRTC核心)

主要用到以下方法

媒体协商

createOffer

createAnswer

setLocalDesccription

setRemoteDesccription

重要事件

onicecandidate

onaddstream

img

步骤流程重绘(可以对标号有个印象)

img

以上标号的所有逻辑均在前端实现,服务端起到向前端广播作用

紧接着接收方接收到对方同意的消息

信息交换全程

前端(发送方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
sock.on('acceptCall', async () => {
if (caller.value) {// 发送方
peer.value = new RTCPeerConnection()// 创建RTCPeerConnection对象
peer.value.addStream(localStream.value)// 添加本地音视频流

// 获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
// 向服务器发送candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}

// 获取对方的音视频流
peer.value.onaddstream = (event: any) => {
calling.value = false
communicating.value = true
// 拿到对方的视频流
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}

---①---
const offer = await peer.value.createOffer({// 生成offer
offerToReceiveAudio: true, // 是否接收对方的音频
offerToReceiveVideo: true, // 是否接收对方的视频
})
---②---
await peer.value.setLocalDescription(offer)// 设置本地描述的offer
---③---
sock.emit('sendOffer', { roomId, offer })// 发送offer !!!注意,这里offer已经当参数传过去了!!!
}
})

后端

1
2
3
4
5
6
7
8
9
sock.on('sendCandidate', ({ roomId, candidate }) => {
sock.to(roomId).emit('receiveCandidate', candidate)// 向这个房间里其他人广播candidate信息
})
sock.on('sendOffer', ({ roomId, offer }) => {// !!!offer在这里!!!
// io.to(roomId).emit('sendOffer', offer) // 向这个房间里所有人广播offer信息 && 向接收方发送这个offer
// 在重新检查文档时我发现,👆这个写法并不可取。这会导致发送方自己也收到自己发的 offer,并触发不必要的逻辑,可能创建多个 PeerConnection,这会在在多用户房间中出现很严重的错误
// 正确做法是排除发送者自己,所有的信令事件都应该改为 sock (第一版教程我都用的io,现在全部改过来了)
sock.to(roomId).emit('sendOffer', offer) // 向这个房间的其他人广播offer信息
})

前端(接收方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
sock.on('sendOffer', async (offer: any) => {// !!!offer传回前端(判断被标注的接收方)!!!
if (called.value) {// 接收方
const stream = await getLocalStream()// 获取本地音视频流
peer.value = new RTCPeerConnection()// 接收方创建自己的RTCPeerConnection对象
peer.value.addStream(stream)// 添加本地音视频流

peer.value.onicecandidate = (event: any) => {// 获取candidate信息
if (event.candidate) {
// 向服务器发送candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}

// 获取对方的音视频流
peer.value.onaddstream = (event: any) => {
calling.value = false
communicating.value = true
// 拿到对方的视频流
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}

---④---
await peer.value.setRemoteDescription(new RTCSessionDescription(offer))// 设置远端描述信息

---⑤---
const answer = await peer.value.createAnswer()// 生成answer
---⑥---
await peer.value.setLocalDescription(answer)// 设置本地描述信息
---⑦---
sock.emit('sendAnswer', { roomId, answer })// 发送answer !!!
}
})

后端

1
2
3
4
5
6
sock.on('sendCandidate', ({ roomId, candidate }) => {// !!!
sock.to(roomId).emit('receiveCandidate', candidate)// 向这个房间里其他人广播candidate信息
})
sock.on('sendAnswer', ({ roomId, answer }) => {
sock.to(roomId).emit('receiveAnswer', answer)// 向这个房间里其他人广播answer信息 && 向接收方发送这个answer
})

前端

1
2
3
4
5
6
sock.on('receiveAnswer', (answer: any) => {
if (caller.value) {// 发送方
---⑧---
peer.value.setRemoteDescription(new RTCSessionDescription(answer))// 设置远端描述信息
}
})

为了让文章不那么乱,最后再补一下candidate

1
2
3
sock.on('receiveCandidate', async (candidate: any) => {
await peer.value.addIceCandidate(candidate) // 添加candidate信息
})

一路下来是不是逻辑非常清晰明了😁 (bushi)

完整代码

前端 (App.vue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import { ref, onMounted } from 'vue'
import { io, Socket } from 'socket.io-client'

const roomId = '001' // 房间ID

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 localStream = ref<MediaStream>() // 本地流
const socket = ref<Socket>() // Socket实例
const peer = ref<any>() // RTCPeerConnection实例

const config = { // 新增:STUN/TURN 服务器配置
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
}

onMounted(() => {
const sock = io('localhost:3001') // 连接服务器

// 连接成功
sock.on('connectionSuccess', () => {
// 进入房间
sock.emit('joinRoom', roomId)
})

// 接收方监听收到视频请求事件
sock.on('receiveCall', () => {
if (!caller.value) {
calling.value = true
called.value = true
}
})

// 发送方收到同意视频事件
sock.on('acceptCall', async () => {
if (caller.value) {
// 发送方
// 创建RTCPeerConnection对象
peer.value = new RTCPeerConnection(config)) // 新增:传入 STUN/TURN
// 添加本地音视频流
peer.value.addStream(localStream.value)

// 获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
// 向服务器发送candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}

// 获取对方的音视频流
peer.value.onaddstream = (event: any) => {
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, // 是否接收对方的视频
})
// 设置本地描述的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)

// 获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
// 向服务器发送candidate信息
sock.emit('sendCandidate', { roomId, candidate: event.candidate })
}
}

// 获取对方的音视频流
peer.value.onaddstream = (event: any) => {
// 拿到对方的视频流
calling.value = false
communicating.value = true
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}

// 设置远端描述信息
await peer.value.setRemoteDescription(new RTCSessionDescription(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(new RTCSessionDescription(answer))
}
})

// 接收candidate信息
sock.on('receiveCandidate', async (candidate: any) => {
await peer.value.addIceCandidate(candidate) // 添加candidate信息
})

// 收到挂断视频请求
sock.on('hangUp', () => {
reset()
})

socket.value = sock
})

// 获取本地音视频流
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
}

// 发起视频请求(发起方)
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)
}

// 挂断视频
const hangUp = () => {
socket.value.emit('hangUp', roomId)
}

// 状态重置
const reset = () => {
called.value = false
caller.value = false
calling.value = false
communicating.value = false
peer.value = null
localVideo.value!.srcObject = null // 关闭本地视频
remoteVideo.value!.srcObject = null // 关闭远程视频
localStream.value?.getTracks()[0].stop() // 关闭本地流
// localStream.value = undefined // 关闭本地流
}

服务端 (index.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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('连接成功...')

// 向客户端发送连接成功的消息
sock.emit('connectionSuccess');

// 监听客户端进入房间的事件
sock.on('joinRoom', (roomId) => {
// 加入房间
sock.join(roomId)
})

// 收到发送方的视频请求事件
sock.on('callRemote', roomId => {
// 向这个房间中的其他人广播这个事件
sock.to(roomId).emit('receiveCall')
})

// 收到接收方的同意视频事件
sock.on('acceptCall', roomId => {
// 向这个房间中的其他人广播这个事件
sock.to(roomId).emit('acceptCall')
})

// 收到发送方的offer
sock.on('sendOffer', ({ roomId, offer }) => {
// 向接收方发送这个offer
sock.to(roomId).emit('sendOffer', offer)
})

// 收到接收方的answer
sock.on('sendAnswer', ({ roomId, answer }) => {
// 向这个房间中的其他人广播这个事件
sock.to(roomId).emit('receiveAnswer', answer)
})

// 收到发送方的candidate信息
sock.on('sendCandidate', ({ roomId, candidate }) => {
// 向这个房间中的其他人广播candidate信息
sock.to(roomId).emit('receiveCandidate', candidate)
})

// 收到挂断视频请求
sock.on('hangUp', roomId => {
// 向这个房间中的所有人广播这个事件
io.to(roomId).emit('hangUp')
})
})

server.listen(3000, () => {
console.log('服务器启动成功');
});

项目启动方式

后端启动服务

1
pnpm start

前端

1
2
pnpm install
pnpm dev