Spring Boot WebSocket + WebRTC 实现点对点视频通话功能Demo
Posted JAVA·D·WangJing
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot WebSocket + WebRTC 实现点对点视频通话功能Demo相关的知识,希望对你有一定的参考价值。
一、创建 SpringBoot 项目
1.1、创建一个空项目:传送门
1.2、添加 websocket 引用
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
1.3、添加 WebSocketConfig 配置文件
package com.example.demo.conf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig
/**
* 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter()
return new ServerEndpointExporter();
1.4、添加 WebSocketServer 核心代码
package com.example.demo.socket;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Enumeration;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/msgServer/userId")
@Component
@Scope("prototype")
public class WebSocketServer
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static ConcurrentHashMap<String, Session> webSocketMap = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId)
this.session = session;
this.userId = userId;
/**
* 连接被打开:向socket-map中添加session
*/
webSocketMap.put(userId, session);
System.out.println(userId + " - 连接建立成功...");
@OnMessage
public void onMessage(String message, Session session)
try
this.sendMessage(message);
catch (IOException e)
e.printStackTrace();
@OnError
public void onError(Session session, Throwable error)
System.out.println("连接异常...");
error.printStackTrace();
@OnClose
public void onClose()
System.out.println("连接关闭");
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException
if (message.equals("心跳"))
this.session.getBasicRemote().sendText(message);
Enumeration<String> keys = webSocketMap.keys();
while (keys.hasMoreElements())
String key = keys.nextElement();
if (key.equals(this.userId))
System.err.println("my id " + key);
continue;
if (webSocketMap.get(key) == null)
webSocketMap.remove(key);
System.err.println(key + " : null");
continue;
Session sessionValue = webSocketMap.get(key);
if (sessionValue.isOpen())
System.out.println("发消息给: " + key + " ,message: " + message);
sessionValue.getBasicRemote().sendText(message);
else
System.err.println(key + ": not open");
sessionValue.close();
webSocketMap.remove(key);
/**
* 发送自定义消息
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException
System.out.println("发送消息到:" + userId + ",内容:" + message);
if (!StringUtils.isEmpty(userId) && webSocketMap.containsKey(userId))
webSocketMap.get(userId).getBasicRemote().sendText(message);
//webSocketServer.sendMessage(message);
else
System.out.println("用户" + userId + ",不在线!");
public static synchronized int getOnlineCount()
return onlineCount;
public static synchronized void addOnlineCount()
WebSocketServer.onlineCount++;
public static synchronized void subOnlineCount()
WebSocketServer.onlineCount--;
二、编写测试html
<!DOCTYPE html>
<html>
<head>
<title>RTC视频通话测试页面</title>
<meta charset="UTF-8"> <!-- for HTML5 -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
</head>
<body>
<style type="text/css">
body
background: #000;
button
height: 40px;
line-height: 40px;
width: auto;
padding: 0 15px;
background: #ccc;
border: none;
border-radius: 10px;
margin-bottom: 10px;
overflow: hidden;
.wrap
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.video-box
border-radius: 20px;
background: pink;
position: relative;
width: 800px;
height: 600px;
overflow: hidden;
.remote-video
width: 800px;
height: 600px;
border: 1px solid black;
overflow: hidden;
.local-video
width: 320px;
height: 240px;
position: absolute;
right: 0;
bottom: 0;
border-radius: 20px 0 0 0;
overflow: hidden;
video
width: 100%;
height: 100%;
</style>
<div class="wrap">
<div>
<div>
<button type="button" onclick="startVideo();">开启本机摄像和音频</button>
<button type="button" onclick="connect();">建立连接</button>
<button type="button" onclick="hangUp();">挂断</button>
<button type="button" onclick="refreshPage();">刷新页面</button>
</div>
<div class="video-box">
<div class="local-video">
<video id="local-video" autoplay style=""></video>
</div>
<div class="remote-video">
<video id="remote-video" autoplay></video>
</div>
</div>
</div>
</div>
<script>
// ===================以下是socket=======================
var user = Math.round(Math.random() * 1000) + ""
var socketUrl = "ws://localhost:8080/msgServer/" + user;
var socket = null
var socketRead = false
window.onload = function()
socket = new WebSocket(socketUrl)
socket.onopen = function()
console.log("成功连接到服务器...")
socketRead = true
socket.onclose = function(e)
console.log('与服务器连接关闭: ' + e.code)
socketRead = false
socket.onmessage = function(res)
var evt = JSON.parse(res.data)
console.log(evt)
if (evt.type === 'offer')
console.log("接收到offer,设置offer,发送answer....")
onOffer(evt);
else if (evt.type === 'answer' && peerStarted)
console.log('接收到answer,设置answer SDP');
onAnswer(evt);
else if (evt.type === 'candidate' && peerStarted)
console.log('接收到ICE候选者..');
onCandidate(evt);
else if (evt.type === 'bye' && peerStarted)
console.log("WebRTC通信断开");
stop();
// ===================以上是socket=======================
var localVideo = document.getElementById('local-video');
var remoteVideo = document.getElementById('remote-video');
var localStream = null;
var peerConnection = null;
var peerStarted = false;
var mediaConstraints =
'mandatory':
'OfferToReceiveAudio': false,
'OfferToReceiveVideo': true
;
//----------------------交换信息 -----------------------
function onOffer(evt)
console.log("接收到offer...")
console.log(evt);
setOffer(evt);
sendAnswer(evt);
peerStarted = true
function onAnswer(evt)
console.log("接收到Answer...")
console.log(evt);
setAnswer(evt);
function onCandidate(evt)
var candidate = new RTCIceCandidate(
sdpMLineIndex: evt.sdpMLineIndex,
sdpMid: evt.sdpMid,
candidate: evt.candidate
);
console.log("接收到Candidate...")
console.log(candidate);
peerConnection.addIceCandidate(candidate);
function sendSDP(sdp)
var text = JSON.stringify(sdp);
console.log('发送sdp.....')
console.log(text); // "type":"offer"....
// textForSendSDP.value = text;
// 通过socket发送sdp
socket.send(text)
function sendCandidate(candidate)
var text = JSON.stringify(candidate);
console.log(text); // "type":"candidate","sdpMLineIndex":0,"sdpMid":"0","candidate":"....
socket.send(text) // socket发送
//---------------------- 视频处理 -----------------------
function startVideo()
navigator.webkitGetUserMedia(
video: true,
audio: true
,
function(stream) //success
localStream = stream;
localVideo.srcObject = stream;
//localVideo.src = window.URL.createObjectURL(stream);
localVideo.play();
localVideo.volume = 0;
,
function(error) //error
console.error('发生了一个错误: [错误代码:' + error.code + ']');
return;
);
function refreshPage()
location.reload();
//---------------------- 处理连接 -----------------------
function prepareNewConnection()
var pc_config =
"iceServers": []
;
var peer = null;
try
peer = new webkitRTCPeerConnection(pc_config);
catch (e)
console.log("建立连接失败,错误:" + e.message);
// 发送所有ICE候选者给对方
peer.onicecandidate = function(evt)
if (evt.candidate)
console.log(evt.candidate);
sendCandidate(
type: "candidate",
sdpMLineIndex: evt.candidate.sdpMLineIndex,
sdpMid: evt.candidate.sdpMid,
candidate: evt.candidate.candidate
);
;
console.log('添加本地视频流...');
peer.addStream(localStream);
peer.addEventListener("addstream", onRemoteStreamAdded, false);
peer.addEventListener("removestream", onRemoteStreamRemoved, false);
// 当接收到远程视频流时,使用本地video元素进行显示
function onRemoteStreamAdded(event)
console.log("添加远程视频流");
// remoteVideo.src = window.URL.createObjectURL(event.stream);
remoteVideo.srcObject = event.stream;
// 当远程结束通信时,取消本地video元素中的显示
function onRemoteStreamRemoved(event)
console.log("移除远程视频流");
remoteVideo.src = "";
return peer;
function sendOffer()
peerConnection = prepareNewConnection();
peerConnection.createOffer(function(sessionDescription) //成功时调用
peerConnection.setLocalDescription(sessionDescription);
console.log("发送: SDP");
console.log(sessionDescription);
sendSDP(sessionDescription);
, function(err) //失败时调用
console.log("创建Offer失败");
, mediaConstraints);
function setOffer(evt)
if (peerConnection)
console.error('peerConnection已存在!');
return;
peerConnection = prepareNewConnection();
peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
function sendAnswer(evt)
console.log('发送Answer,创建远程会话描述...');
if (!peerConnection)
console.error('peerConnection不存在!');
return;
peerConnection.createAnswer(function(sessionDescription) //成功时
peerConnection.setLocalDescription(sessionDescription);
console.log("发送: SDP");
console.log(sessionDescription);
sendSDP(sessionDescription);
, function() //失败时
console.log("创建Answer失败");
, mediaConstraints);
function setAnswer(evt)
if (!peerConnection)
console.error('peerConnection不存在!');
return;
peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
//-------- 处理用户UI事件 -----
// 开始建立连接
function connect()
if (!localStream)
alert("请首先捕获本地视频数据.");
else if (peerStarted || !socketRead)
alert("请刷新页面后重试.");
else
sendOffer();
peerStarted = true;
// 停止连接
function hangUp()
console.log("挂断.");
stop();
function stop()
peerConnection.close();
peerConnection = null;
peerStarted = false;
</script>
</body>
</html>
三、本地打开测试
因为打开摄像头就露脸了,所以就初始化截个图吧
四、搭建STUN和TURN服务:传送门
五、更改 html 配置
var pc_config =
"iceServers": [
url: "stun:ip:端口"
,
url: "turn:ip:端口",
credential: "kurento",
username: "kurento"
]
;
注:如果想要非局域网测试,需要把 STUN、TURN 和 websocket服务 要部署到公网环境,然后记得更改 html内的 websocket ip和端口
注:以上内容仅提供参考和交流,请勿用于商业用途,如有侵权联系本人删除!
在 Spring-boot 中路由 websocket 目标
【中文标题】在 Spring-boot 中路由 websocket 目标【英文标题】:Routing websocket destination in Spring-boot 【发布时间】:2018-08-25 12:34:10 【问题描述】:拥有原始的 websocket 实现:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
registry.addHandler(new MessageHandler(), "/websocket")
.setAllowedOrigins("*")
.addInterceptors();;
处理程序:
public class MessageHandler extends TextWebSocketHandler
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception
// The WebSocket has been closed
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception
String auth = (String) session.getAttributes().get("auth");
System.out.println(auth);
session.sendMessage(new TextMessage("You are now connected to the server. This is the first message."));
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception
// A message has been received
websocket 客户端使用/websocket
url 例如ws://localhost:8080/websocket
连接到服务器(握手等)
但是,既然已经建立了连接,有没有办法路由消息?假设我有一个提供聊天和一些弹出功能的应用程序(为简单起见,假设用户向应用程序中的所有朋友发送弹出消息和一些弹出窗口)。
当然我想将聊天消息路由到/chat
并弹出到/popup
。
实现此目的的一种方法是将 json 消息发送到服务器并在那里解析它,例如:
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception
String path = getRouteFromJsonMessage(textMessage);
if( ! "".equals(path) && path.equals("chat")
....
if( ! "".equals(path) && path.equals("popup")
....
但这似乎太慢了,每条消息都解析 json。还有其他更好的方法来实现路由吗?
感谢您的帮助!
【问题讨论】:
顺便说一句,阅读更短更快捷:if ("chat".equals(path))
而不是 if( ! "".equals(path) && path.equals("chat")
@MarkusPscheidt 谢谢,我只是将代码添加到问题中只是为了演示,没有考虑。
【参考方案1】:
你为什么不注册两个不同的 MessageHandlers
public class WebSocketConfig implements WebSocketConfigurer
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
registry.addHandler(new ChatMessageHandler(), "/chat")
.setAllowedOrigins("*")
.addInterceptors()
.addHandler(new PopUpHandler(), "/popup") //etc;
【讨论】:
在这种情况下不会创建 2 个 Web 套接字连接吗?假设该应用程序有百万用户已连接,在这种情况下,您必须保存 2 百万 Web 套接字连接而不是 1 百万。还是我错了?以上是关于Spring Boot WebSocket + WebRTC 实现点对点视频通话功能Demo的主要内容,如果未能解决你的问题,请参考以下文章