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
核心组件功能
-
前端Vue
- 媒体设备访问:通过
getUserMedia
获取音视频流 RTCPeerConnection
:核心WebRTC对象,处理连接建立RTCSessionDescription
:交换SDP(媒体配置信息)RTCIceCandidate
:处理NAT穿透的ICE候选- WebSocket:与信令服务器实时通信
- 媒体设备访问:通过
-
后端Spring Boot
- 信令服务器:使用WebSocket转发SDP/ICE消息
- STUN/TURN服务配置(可选):穿透复杂网络环境
- 房间管理:用户配对逻辑
-
WebRTC服务
- STUN服务器(免费):
stun.l.google.com:19302
- TURN服务器(需自建):处理对称NAT穿透
- STUN服务器(免费):
详细实现步骤
一、前端Vue实现(关键代码)
- 初始化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];
};
- 信令交换
// 发起方创建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实现
- 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();
}
}
- 信令处理核心
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) {
// 错误处理
}
}
}
- 房间管理(可选)
// 简单房间配对服务
@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(","));
}
}
关键问题解决方案
-
NAT穿透失败
- 方案:部署TURN服务器(Coturn)
- 修改ICE配置:
iceServers: [ { urls: "turn:your_domain:3478", username: "user", credential: "pass" } ]
-
移动端适配
- iOS Safari:需使用
iosrtc
polyfill - Android:需HTTPS环境
- iOS Safari:需使用
-
媒体设备权限
- 使用HTTPS(localhost除外)
- 显式请求权限:
navigator.mediaDevices.getUserMedia
-
断开重连
// 监听连接状态 pc.onconnectionstatechange = () => { if (pc.connectionState === "disconnected") { // 触发重新连接逻辑 } };
部署注意事项
-
HTTPS强制要求
- 正式环境必须使用HTTPS(Let's Encrypt免费证书)
- 开发环境可用
localhost
或127.0.0.1
-
TURN服务器部署
# Coturn安装示例 sudo apt-get install coturn echo "TURNSERVER_ENABLED=1" >> /etc/default/coturn
-
信令服务器优化
- 添加心跳检测
- 实现重连机制
- 使用Redis管理会话状态
-
前端优化
- 添加ICE候选超时处理(15秒)
- 实现通话状态提示UI
- 增加媒体设备切换功能
完整实现约需200-300行前端代码+100行后端代码,建议使用成熟库(如peerjs
)简化开发,但理解底层原理对调试至关重要。