如何使用 WebRTC 和 Spring Boot 构建实时音视频通话

了解如何使用 WebRTC、信令和 STUN/TURN 服务器在 Spring Boot 应用程序中启用点对点实时通信。

为什么选择 WebRTC + Spring Boot?

WebRTC 可在浏览器之间直接进行实时音频/视频流传输,延迟低,无需插件。但 WebRTC 需要一个信令服务器来交换连接设置数据(SDP、ICE candidates)。而 Spring Boot 可以使用 WebSocket 构建一个出色的信令服务器!

高级架构

  • 两个客户端打开浏览器页面。
  • 通过 WebSocket 连接到 Spring Boot 服务器。
  • 播放音频/视频轨道。
  • 使用 WebSocket 信令交换 SDP offers/answers 和 ICE candidates。
  • WebRTC 建立点对点连接。
  • 客户端直接流式传输音频/视频,服务器只充当协调者。
Client A — WebSocket Signalling — Spring Boot — WebSocket Signalling — Client B
  ↘ Peer-to-peer Media using WebRTC (audio/video)

服务器设置:Spring Boot + WebSocket

添加依赖项(Maven):

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocket 配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/signal")
            .setAllowedOrigins("*")
            .withSockJS();
  }
}

信令控制器:

@Controller
public class SignalingController {

  @MessageMapping("/signal")
  @SendTo("/topic/signals")
  public SignalingMessage signaling(SignalingMessage message) {
    return message;
  }
}

信令消息的DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignalingMessage {
    private String from;
    private String to;
    private String type; // "offer", "answer", "candidate"
    private String sdp; // for offer/answer
    private String candidate; // for ICE
}

客户端:JavaScript 代码

HTML:

<!DOCTYPE html>
<html>
<head>
  <title>WebRTC Call</title>
</head>
<body>
  <video id="localVideo" autoplay muted></video>
  <video id="remoteVideo" autoplay></video>
  <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.1/dist/sockjs.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/stompjs/lib/stomp.min.js"></script>
  <script src="webrtc.js"></script>
</body>
</html>

webrtc.js:

let localStream, peerConnection;
const config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };

// 连接到信令
const socket = new SockJS('/signal');
const stomp = Stomp.over(socket);
stomp.connect({}, () => {
  stomp.subscribe('/topic/signals', onSignal);
});

// 从摄像机/麦克风获取媒体
navigator.mediaDevices.getUserMedia({ audio: true, video: true })
  .then(stream => {
    localStream = stream;
    document.getElementById('localVideo').srcObject = stream;
    createPeerConnection();
  });

function createPeerConnection() {
  peerConnection = new RTCPeerConnection(config);

  // 将本地轨道添加到连接
  localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));

  peerConnection.ontrack = event => {
    document.getElementById('remoteVideo').srcObject = event.streams[0];
  };

  peerConnection.onicecandidate = event => {
    if (event.candidate) {
      stomp.send("/app/signal", {}, JSON.stringify({
        type: "candidate",
        candidate: JSON.stringify(event.candidate)
      }));
    }
  };

  // 创建 offer 并发送
  peerConnection.createOffer()
    .then(offer => peerConnection.setLocalDescription(offer))
    .then(() => {
      stomp.send("/app/signal", {}, JSON.stringify({
        type: "offer",
        sdp: peerConnection.localDescription
      }));
    });
}

function onSignal(message) {
  const msg = JSON.parse(message.body);
  switch (msg.type) {
    case "offer":
      handleOffer(msg);
      break;
    case "answer":
      peerConnection.setRemoteDescription(new RTCSessionDescription(msg.sdp));
      break;
    case "candidate":
      peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.candidate)));
      break;
  }
}

function handleOffer(msg) {
  createPeerConnection();
  peerConnection.setRemoteDescription(new RTCSessionDescription(msg.sdp))
    .then(() => peerConnection.createAnswer())
    .then(answer => peerConnection.setLocalDescription(answer))
    .then(() => {
      stomp.send("/app/signal", {}, JSON.stringify({
        type: "answer",
        sdp: peerConnection.localDescription
      }));
    });
}

其它

媒体:STUN/TURN 服务器

  • STUN 服务器支持 NAT 穿越。在 RTCPeerConnection 配置中使用像 Google 这样的公共服务器。
  • 穿越 NAT/防火墙需要 TURN 服务器。在 iceServers 配置中添加 TURN 服务器详细信息。

高级增强功能

  • 使用TURN 服务器在受限网络之间实现可靠通信。
  • 实施信令验证,防止未经授权的连接。
  • 使用 getStats() 跟踪呼叫统计信息。
  • 通过网状或 SFU 结构添加群组呼叫支持。
  • 使用 TLS (wss://) 进行部署,以确保信令安全。

常见陷阱和提示

  • 在开始 .createOffer() 之前,确保两个对等方都加载了客户端和信令。
  • 浏览器行为不同;使用 onicecandidate 事件。
  • 切记将本地视频元素静音以避免回声。
  • 通过 peerConnection.close() 和信令处理干净的断开连接。

小结

本文仅用 Spring Boot 和浏览器原生 WebRTC 构建了一个简单的音频/视频通话系统。服务器只处理信令,而浏览器对等方则以 P2P 方式连接。这种设计具有良好的扩展性,并避免了服务器媒体处理。

在此基础上进行扩展,可以在 Java 技术栈上构建聊天应用程序、远程医疗服务或协作平台。

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/60013.html

(0)

相关推荐

发表回复

登录后才能评论