Skip to content

视频/直播/聊天方案

概述

实时音视频通信和即时通讯是现代应用的重要功能,涉及 WebRTC、WebSocket、流媒体等技术。

WebRTC 实时通信

1. 获取媒体设备

javascript
async function getUserMedia(constraints = { video: true, audio: true }) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints)
    return stream
  } catch (error) {
    console.error('获取媒体设备失败:', error)
    throw error
  }
}

// 使用示例
const localStream = await getUserMedia({
  video: { width: 1280, height: 720 },
  audio: true
})

// 显示本地视频
const localVideo = document.querySelector('#local-video')
localVideo.srcObject = localStream

2. 创建 PeerConnection

javascript
const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'turn:your-turn-server.com', username: 'user', credential: 'pass' }
  ]
}

const peerConnection = new RTCPeerConnection(configuration)

// 添加本地流
localStream.getTracks().forEach(track => {
  peerConnection.addTrack(track, localStream)
})

// 接收远程流
peerConnection.ontrack = (event) => {
  const remoteVideo = document.querySelector('#remote-video')
  remoteVideo.srcObject = event.streams[0]
}

3. 信令交换

javascript
// 创建 Offer
async function createOffer() {
  const offer = await peerConnection.createOffer()
  await peerConnection.setLocalDescription(offer)
  
  // 通过信令服务器发送 offer
  signalingServer.send({
    type: 'offer',
    offer: offer
  })
}

// 创建 Answer
async function createAnswer(offer) {
  await peerConnection.setRemoteDescription(offer)
  const answer = await peerConnection.createAnswer()
  await peerConnection.setLocalDescription(answer)
  
  // 通过信令服务器发送 answer
  signalingServer.send({
    type: 'answer',
    answer: answer
  })
}

// 接收 Answer
async function handleAnswer(answer) {
  await peerConnection.setRemoteDescription(answer)
}

直播推流

1. 推流到服务器

javascript
class LiveStreamer {
  constructor(pushUrl) {
    this.pushUrl = pushUrl
    this.mediaRecorder = null
  }

  async start(stream) {
    const options = {
      mimeType: 'video/webm;codecs=vp9',
      videoBitsPerSecond: 2500000
    }

    this.mediaRecorder = new MediaRecorder(stream, options)
    
    this.mediaRecorder.ondataavailable = async (event) => {
      if (event.data.size > 0) {
        await this.sendChunk(event.data)
      }
    }

    this.mediaRecorder.start(1000) // 每秒发送一次
  }

  async sendChunk(chunk) {
    const formData = new FormData()
    formData.append('chunk', chunk)

    await fetch(this.pushUrl, {
      method: 'POST',
      body: formData
    })
  }

  stop() {
    if (this.mediaRecorder) {
      this.mediaRecorder.stop()
    }
  }
}

2. 拉流播放

javascript
class LivePlayer {
  constructor(videoElement) {
    this.video = videoElement
    this.sourceBuffer = null
    this.mediaSource = new MediaSource()
    this.video.src = URL.createObjectURL(this.mediaSource)
    
    this.mediaSource.addEventListener('sourceopen', () => {
      this.sourceBuffer = this.mediaSource.addSourceBuffer('video/webm; codecs=vp9')
    })
  }

  async play(pullUrl) {
    const response = await fetch(pullUrl)
    const reader = response.body.getReader()

    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      
      this.sourceBuffer.appendBuffer(value)
    }
  }
}

即时通讯

1. WebSocket 连接

javascript
class ChatClient {
  constructor(url) {
    this.url = url
    this.ws = null
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
  }

  connect() {
    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      console.log('WebSocket 连接成功')
      this.reconnectAttempts = 0
    }

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      this.handleMessage(message)
    }

    this.ws.onclose = () => {
      console.log('WebSocket 连接关闭')
      this.reconnect()
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket 错误:', error)
    }
  }

  reconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++
      setTimeout(() => {
        this.connect()
      }, 1000 * this.reconnectAttempts)
    }
  }

  send(message) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message))
    }
  }

  handleMessage(message) {
    // 处理不同类型的消息
    switch (message.type) {
      case 'text':
        this.onTextMessage(message)
        break
      case 'image':
        this.onImageMessage(message)
        break
      case 'file':
        this.onFileMessage(message)
        break
    }
  }
}

2. 消息存储

javascript
class MessageStore {
  constructor() {
    this.dbName = 'ChatDB'
    this.dbVersion = 1
    this.db = null
  }

  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.dbVersion)

      request.onerror = () => reject(request.error)
      request.onsuccess = () => {
        this.db = request.result
        resolve()
      }

      request.onupgradeneeded = (event) => {
        const db = event.target.result
        
        if (!db.objectStoreNames.contains('messages')) {
          const store = db.createObjectStore('messages', { keyPath: 'id' })
          store.createIndex('conversationId', 'conversationId', { unique: false })
          store.createIndex('timestamp', 'timestamp', { unique: false })
        }
      }
    })
  }

  async addMessage(message) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['messages'], 'readwrite')
      const store = transaction.objectStore('messages')
      const request = store.add(message)

      request.onsuccess = () => resolve()
      request.onerror = () => reject(request.error)
    })
  }

  async getMessages(conversationId, limit = 50) {
    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction(['messages'], 'readonly')
      const store = transaction.objectStore('messages')
      const index = store.index('conversationId')
      const request = index.getAll(conversationId)

      request.onsuccess = () => {
        const messages = request.result
          .sort((a, b) => b.timestamp - a.timestamp)
          .slice(0, limit)
        resolve(messages)
      }
      request.onerror = () => reject(request.error)
    })
  }
}

最佳实践

1. 性能优化

  • 使用硬件加速编解码
  • 实现自适应码率
  • 优化网络传输

2. 用户体验

  • 提供网络质量指示
  • 支持美颜滤镜
  • 实现回声消除

3. 安全性

  • 端到端加密
  • 身份验证
  • 防止盗链

常见问题

如何处理网络波动?

  • 实现断线重连
  • 使用 FEC 前向纠错
  • 自适应码率

如何降低延迟?

  • 使用 UDP 传输
  • 优化编码参数
  • 减少缓冲区大小

相关资源

基于 VitePress 的本地知识库