WebRTC实时音视频通话实现指南

9 阅读2分钟

WebRTC实时音视频通话实现(Vue + Spring Boot)

整体流程概述

graph TD
    A[用户A] -->|1. 获取媒体设备| B[前端Vue]
    A -->|7. 接收媒体流| B
    C[用户B] -->|1. 获取媒体设备| D[前端Vue]
    C -->|7. 接收媒体流| D
    B -->|2. 创建PeerConnection| E[信令服务器]
    D -->|2. 创建PeerConnection| E
    E -->|3. 转发SDP/ICE| B
    E -->|3. 转发SDP/ICE| D
    B -->|4. 交换SDP| D
    D -->|5. 交换ICE| B
    B -->|6. P2P媒体流| D

核心组件功能

  1. 前端Vue

    • 媒体设备访问:通过getUserMedia获取音视频流
    • RTCPeerConnection:核心WebRTC对象,处理连接建立
    • RTCSessionDescription:交换SDP(媒体配置信息)
    • RTCIceCandidate:处理NAT穿透的ICE候选
    • WebSocket:与信令服务器实时通信
  2. 后端Spring Boot

    • 信令服务器:使用WebSocket转发SDP/ICE消息
    • STUN/TURN服务配置(可选):穿透复杂网络环境
    • 房间管理:用户配对逻辑
  3. WebRTC服务

    • STUN服务器(免费):stun.l.google.com:19302
    • TURN服务器(需自建):处理对称NAT穿透

详细实现步骤

一、前端Vue实现(关键代码)

  1. 初始化WebRTC
// 创建RTCPeerConnection
const pc = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    // 如需TURN服务器 { urls: "turn:your_turn_server", username: "...", credential: "..." }
  ]
});

// 获取本地媒体流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    localVideo.srcObject = stream;
    stream.getTracks().forEach(track => pc.addTrack(track, stream));
  });

// ICE候选处理
pc.onicecandidate = event => {
  if (event.candidate) {
    socket.send(JSON.stringify({ 
      type: "candidate", 
      candidate: event.candidate 
    }));
  }
};

// 接收远程流
pc.ontrack = event => {
  remoteVideo.srcObject = event.streams[0];
};
  1. 信令交换
// 发起方创建Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket.send(JSON.stringify({ type: "offer", sdp: offer.sdp }));

// 接收方处理Offer
socket.on("offer", async offer => {
  await pc.setRemoteDescription(new RTCSessionDescription({ type: "offer", sdp: offer.sdp }));
  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);
  socket.send(JSON.stringify({ type: "answer", sdp: answer.sdp }));
});

// 处理Answer
socket.on("answer", answer => {
  pc.setRemoteDescription(new RTCSessionDescription({ type: "answer", sdp: answer.sdp }));
});

// 处理ICE候选
socket.on("candidate", candidate => {
  pc.addIceCandidate(new RTCIceCandidate(candidate));
});

二、后端Spring Boot实现

  1. WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(signalHandler(), "/signal")
            .setAllowedOrigins("*");
  }

  @Bean
  public WebSocketHandler signalHandler() {
    return new SignalingHandler();
  }
}
  1. 信令处理核心
public class SignalingHandler extends TextWebSocketHandler {
  private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

  @Override
  public void afterConnectionEstablished(WebSocketSession session) {
    sessions.put(session.getId(), session);
  }

  @Override
  protected void handleTextMessage(WebSocketSession session, TextMessage message) {
    try {
      JSONObject json = new JSONObject(message.getPayload());
      String target = json.getString("target"); // 目标用户ID
      
      // 转发信令给指定用户
      if (sessions.containsKey(target)) {
        sessions.get(target).sendMessage(message);
      }
    } catch (Exception e) {
      // 错误处理
    }
  }
}
  1. 房间管理(可选)
// 简单房间配对服务
@Service
public class RoomService {
  private final Map<String, String> rooms = new ConcurrentHashMap<>(); // roomId -> 用户列表

  public void joinRoom(String roomId, String userId) {
    rooms.compute(roomId, (k, v) -> v == null ? userId : v + "," + userId);
  }

  public List<String> getRoomMembers(String roomId) {
    return Arrays.asList(rooms.get(roomId).split(","));
  }
}

关键问题解决方案

  1. NAT穿透失败

    • 方案:部署TURN服务器(Coturn)
    • 修改ICE配置:
    iceServers: [
      { urls: "turn:your_domain:3478", username: "user", credential: "pass" }
    ]
    
  2. 移动端适配

    • iOS Safari:需使用iosrtc polyfill
    • Android:需HTTPS环境
  3. 媒体设备权限

    • 使用HTTPS(localhost除外)
    • 显式请求权限:navigator.mediaDevices.getUserMedia
  4. 断开重连

    // 监听连接状态
    pc.onconnectionstatechange = () => {
      if (pc.connectionState === "disconnected") {
        // 触发重新连接逻辑
      }
    };
    

部署注意事项

  1. HTTPS强制要求

    • 正式环境必须使用HTTPS(Let's Encrypt免费证书)
    • 开发环境可用localhost127.0.0.1
  2. TURN服务器部署

    # Coturn安装示例
    sudo apt-get install coturn
    echo "TURNSERVER_ENABLED=1" >> /etc/default/coturn
    
  3. 信令服务器优化

    • 添加心跳检测
    • 实现重连机制
    • 使用Redis管理会话状态
  4. 前端优化

    • 添加ICE候选超时处理(15秒)
    • 实现通话状态提示UI
    • 增加媒体设备切换功能

完整实现约需200-300行前端代码+100行后端代码,建议使用成熟库(如peerjs)简化开发,但理解底层原理对调试至关重要。