Vue3+WebRTC实时视频协作系统开发实战:从入门到企业级应用 | 前端音视频技术

2025-08-10 0 196

基于最新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
  }
})
Vue3+WebRTC实时视频协作系统开发实战:从入门到企业级应用 | 前端音视频技术
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 vue3 Vue3+WebRTC实时视频协作系统开发实战:从入门到企业级应用 | 前端音视频技术 https://www.taomawang.com/web/vue3/787.html

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务