基于最新WebRTC技术构建企业级视频会议解决方案
一、WebRTC技术核心
现代实时通信技术对比:
技术方案 | 延迟 | 兼容性 | 开发复杂度 |
---|---|---|---|
WebSocket+转码 | 高(1-3秒) | 高 | 高 |
RTMP | 中(0.5-1秒) | 中 | 中 |
WebRTC | 低(0.1-0.3秒) | 中高 | 中高 |
二、系统架构设计
1. 分层架构设计
客户端 → 信令服务 → 媒体服务 → 业务服务 → 数据存储 ↑ ↑ ↑ ↑ WebRTC连接 会话管理 流处理转发 会议状态管理
2. 信令交互流程
加入房间 → 交换SDP → ICE协商 → 建立连接 → 媒体传输
↑ ↑ ↑ ↑ ↑
身份验证 媒体能力协商 NAT穿透 加密传输 数据通道维护
三、核心模块实现
1. WebRTC管理器
class WebRTCManager {
constructor() {
this.peerConnections = new Map()
this.localStream = null
this.dataChannels = new Map()
}
async initLocalStream(constraints) {
try {
this.localStream = await navigator.mediaDevices.getUserMedia(constraints)
return this.localStream
} catch (err) {
console.error('获取媒体设备失败:', err)
throw err
}
}
createPeerConnection(remoteUserId) {
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.example.com',
username: 'your_username',
credential: 'your_credential'
}
]
}
const pc = new RTCPeerConnection(config)
this.peerConnections.set(remoteUserId, pc)
// 添加本地流
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
pc.addTrack(track, this.localStream)
})
}
// ICE候选处理
pc.onicecandidate = (event) => {
if (event.candidate) {
this.sendSignal({
type: 'ice-candidate',
candidate: event.candidate,
target: remoteUserId
})
}
}
// 数据通道
const dc = pc.createDataChannel('chat')
this.setupDataChannel(dc, remoteUserId)
return pc
}
async createOffer(remoteUserId) {
const pc = this.createPeerConnection(remoteUserId)
try {
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
return {
sdp: offer.sdp,
type: offer.type
}
} catch (err) {
console.error('创建offer失败:', err)
throw err
}
}
}
2. 信令服务集成
const useSignaling = () => {
const socket = ref(null)
const roomId = ref('')
const userId = ref('')
const connect = (serverUrl) => {
socket.value = new WebSocket(serverUrl)
socket.value.onopen = () => {
console.log('信令服务器连接成功')
}
socket.value.onmessage = (event) => {
const message = JSON.parse(event.data)
handleSignal(message)
}
}
const joinRoom = (room, user) => {
roomId.value = room
userId.value = user
sendSignal({
type: 'join',
room: roomId.value,
user: userId.value
})
}
const sendSignal = (message) => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify({
...message,
room: roomId.value,
sender: userId.value
}))
}
}
return {
connect,
joinRoom,
sendSignal
}
}
四、高级功能实现
1. 多路视频混流
class VideoMixer {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.videoElements = new Map()
this.layout = 'grid' // grid | spotlight | vertical
}
addVideo(id, stream) {
const video = document.createElement('video')
video.srcObject = stream
video.autoplay = true
this.videoElements.set(id, video)
video.onloadedmetadata = () => {
this.render()
}
}
removeVideo(id) {
this.videoElements.delete(id)
this.render()
}
setLayout(layout) {
this.layout = layout
this.render()
}
render() {
const videos = Array.from(this.videoElements.values())
if (videos.length === 0) return
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
switch (this.layout) {
case 'grid':
this.renderGrid(videos)
break
case 'spotlight':
this.renderSpotlight(videos)
break
case 'vertical':
this.renderVertical(videos)
break
}
}
renderGrid(videos) {
const cols = Math.ceil(Math.sqrt(videos.length))
const rows = Math.ceil(videos.length / cols)
const itemWidth = this.canvas.width / cols
const itemHeight = this.canvas.height / rows
videos.forEach((video, index) => {
const col = index % cols
const row = Math.floor(index / cols)
const x = col * itemWidth
const y = row * itemHeight
this.ctx.drawImage(
video,
x, y, itemWidth, itemHeight
)
})
}
}
2. 实时协作白板
const useWhiteboard = (canvas, dataChannel) => {
const drawing = ref(false)
const color = ref('#000000')
const lineWidth = ref(3)
const ctx = canvas.getContext('2d')
const startDrawing = (e) => {
drawing.value = true
draw(e)
}
const endDrawing = () => {
drawing.value = false
ctx.beginPath()
}
const draw = (e) => {
if (!drawing.value) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
ctx.lineWidth = lineWidth.value
ctx.lineCap = 'round'
ctx.strokeStyle = color.value
ctx.lineTo(x, y)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(x, y)
// 通过数据通道同步绘制动作
if (dataChannel && dataChannel.readyState === 'open') {
dataChannel.send(JSON.stringify({
type: 'draw',
x,
y,
color: color.value,
lineWidth: lineWidth.value,
action: e.type === 'mousedown' ? 'start' : 'move'
}))
}
}
const handleRemoteDraw = (data) => {
if (data.action === 'start') {
ctx.beginPath()
ctx.moveTo(data.x, data.y)
} else {
ctx.lineWidth = data.lineWidth
ctx.strokeStyle = data.color
ctx.lineTo(data.x, data.y)
ctx.stroke()
}
}
return {
startDrawing,
endDrawing,
draw,
handleRemoteDraw,
color,
lineWidth
}
}
五、性能优化策略
1. 自适应码率控制
class BitrateAdaptor {
constructor() {
this.lastBitrate = 0
this.networkStats = {
packetLoss: 0,
latency: 0,
throughput: 0
}
}
monitorConnection(pc) {
setInterval(async () => {
const stats = await pc.getStats()
const remoteOutbound = [...stats.values()].find(
s => s.type === 'remote-outbound-rtp'
)
if (remoteOutbound) {
this.networkStats.packetLoss = remoteOutbound.packetsLost / remoteOutbound.packetsSent
this.networkStats.latency = remoteOutbound.roundTripTime
this.networkStats.throughput = remoteOutbound.bytesSent / (1024 * 1024) // MBps
this.adjustBitrate()
}
}, 5000)
}
adjustBitrate() {
let newBitrate = this.lastBitrate
if (this.networkStats.packetLoss > 0.1) {
newBitrate = Math.max(300, this.lastBitrate * 0.7)
} else if (this.networkStats.latency > 500) {
newBitrate = Math.max(300, this.lastBitrate * 0.8)
} else {
newBitrate = Math.min(2000, this.lastBitrate * 1.2 || 1000)
}
if (newBitrate !== this.lastBitrate) {
this.applyBitrate(newBitrate)
this.lastBitrate = newBitrate
}
}
applyBitrate(bitrate) {
const sender = this.getVideoSender()
if (sender) {
const parameters = sender.getParameters()
if (!parameters.encodings) {
parameters.encodings = [{}]
}
parameters.encodings[0].maxBitrate = bitrate * 1000
sender.setParameters(parameters)
}
}
}
2. ICE候选优化
class IceOptimizer {
constructor() {
this.candidates = []
this.gatheringTimeout = null
}
startGathering(pc) {
this.candidates = []
this.gatheringTimeout = setTimeout(() => {
this.processCandidates()
}, 1000)
}
addCandidate(candidate) {
if (candidate) {
this.candidates.push(candidate)
}
}
processCandidates() {
if (this.candidates.length === 0) return
// 按优先级排序
this.candidates.sort((a, b) => {
if (a.protocol !== b.protocol) {
return a.protocol === 'udp' ? -1 : 1
}
return b.priority - a.priority
})
// 选择最佳候选
const bestCandidate = this.findBestCandidate()
this.sendCandidate(bestCandidate)
clearTimeout(this.gatheringTimeout)
}
findBestCandidate() {
// 优先选择主机候选
const hostCandidate = this.candidates.find(c =>
c.candidate.indexOf('typ host') !== -1
)
if (hostCandidate) return hostCandidate
// 其次选择反射候选
const srflxCandidate = this.candidates.find(c =>
c.candidate.indexOf('typ srflx') !== -1
)
if (srflxCandidate) return srflxCandidate
// 最后选择中继候选
return this.candidates[0]
}
}
六、实战案例:在线教育系统
1. 课堂房间组件
<template>
<div class="classroom">
<div class="video-container">
<video ref="localVideo" autoplay muted></video>
<canvas ref="mixedVideo"></canvas>
<div class="controls">
<button @click="toggleCamera">{{ cameraActive ? '关闭' : '开启' }}摄像头</button>
<button @click="toggleMic">{{ micActive ? '静音' : '取消静音' }}</button>
<select v-model="selectedLayout">
<option value="grid">网格视图</option>
<option value="spotlight">主讲人视图</option>
</select>
</div>
</div>
<div class="whiteboard-container">
<canvas
ref="whiteboard"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="endDrawing"
@mouseleave="endDrawing">
</canvas>
<div class="toolbar">
<input type="color" v-model="drawColor">
<input type="range" v-model="lineWidth" min="1" max="10">
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useWebRTC } from './composables/webrtc'
import { useWhiteboard } from './composables/whiteboard'
const props = defineProps({
roomId: String,
userId: String
})
const {
localVideo,
mixedVideo,
cameraActive,
micActive,
toggleCamera,
toggleMic,
selectedLayout
} = useWebRTC(props.roomId, props.userId)
const {
whiteboard,
startDrawing,
draw,
endDrawing,
drawColor,
lineWidth
} = useWhiteboard()
</script>
2. 信令状态管理
const useSignalingStore = defineStore('signaling', () => {
const connection = ref(null)
const room = ref(null)
const users = ref([])
const messages = ref([])
function connect(serverUrl) {
connection.value = new WebSocket(serverUrl)
connection.value.onmessage = (event) => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'user-list':
users.value = message.users
break
case 'chat-message':
messages.value.push(message)
break
case 'whiteboard-draw':
// 处理白板绘制消息
break
}
}
}
function sendChat(content) {
if (connection.value) {
connection.value.send(JSON.stringify({
type: 'chat-message',
content,
room: room.value,
timestamp: Date.now()
}))
}
}
return {
connection,
room,
users,
messages,
connect,
sendChat
}
})