加载中...

Spring Boot WebSocket + WebRTC 实现点对点视频通话功能Demo

Spring Boot WebSocket + WebRTC 实现点对点视频通话功能Demo

一、创建 SpringBoot 项目

1.1、创建一个空项目:传送门

1.2、添加 websocket 引用

  1. <!-- websocket -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-websocket</artifactId>
  5. </dependency>

1.3、添加 WebSocketConfig 配置文件

  1. package com.example.demo.conf;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  5. @Configuration
  6. public class WebSocketConfig {
  7. /**
  8. * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
  9. */
  10. @Bean
  11. public ServerEndpointExporter serverEndpointExporter() {
  12. return new ServerEndpointExporter();
  13. }
  14. }

1.4、添加 WebSocketServer 核心代码

  1. package com.example.demo.socket;
  2. import org.springframework.context.annotation.Scope;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.util.StringUtils;
  5. import javax.websocket.*;
  6. import javax.websocket.server.PathParam;
  7. import javax.websocket.server.ServerEndpoint;
  8. import java.io.IOException;
  9. import java.util.Enumeration;
  10. import java.util.concurrent.ConcurrentHashMap;
  11. @ServerEndpoint("/msgServer/{userId}")
  12. @Component
  13. @Scope("prototype")
  14. public class WebSocketServer {
  15. /**
  16. * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
  17. */
  18. private static int onlineCount = 0;
  19. /**
  20. * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
  21. */
  22. private static ConcurrentHashMap<String, Session> webSocketMap = new ConcurrentHashMap<>();
  23. /**
  24. * 与某个客户端的连接会话,需要通过它来给客户端发送数据
  25. */
  26. private Session session;
  27. /**
  28. * 接收userId
  29. */
  30. private String userId = "";
  31. @OnOpen
  32. public void onOpen(Session session, @PathParam("userId") String userId) {
  33. this.session = session;
  34. this.userId = userId;
  35. /**
  36. * 连接被打开:向socket-map中添加session
  37. */
  38. webSocketMap.put(userId, session);
  39. System.out.println(userId + " - 连接建立成功...");
  40. }
  41. @OnMessage
  42. public void onMessage(String message, Session session) {
  43. try {
  44. this.sendMessage(message);
  45. } catch (IOException e) {
  46. e.printStackTrace();
  47. }
  48. }
  49. @OnError
  50. public void onError(Session session, Throwable error) {
  51. System.out.println("连接异常...");
  52. error.printStackTrace();
  53. }
  54. @OnClose
  55. public void onClose() {
  56. System.out.println("连接关闭");
  57. }
  58. /**
  59. * 实现服务器主动推送
  60. */
  61. public void sendMessage(String message) throws IOException {
  62. if (message.equals("心跳")) {
  63. this.session.getBasicRemote().sendText(message);
  64. }
  65. Enumeration<String> keys = webSocketMap.keys();
  66. while (keys.hasMoreElements()) {
  67. String key = keys.nextElement();
  68. if (key.equals(this.userId)) {
  69. System.err.println("my id " + key);
  70. continue;
  71. }
  72. if (webSocketMap.get(key) == null) {
  73. webSocketMap.remove(key);
  74. System.err.println(key + " : null");
  75. continue;
  76. }
  77. Session sessionValue = webSocketMap.get(key);
  78. if (sessionValue.isOpen()) {
  79. System.out.println("发消息给: " + key + " ,message: " + message);
  80. sessionValue.getBasicRemote().sendText(message);
  81. } else {
  82. System.err.println(key + ": not open");
  83. sessionValue.close();
  84. webSocketMap.remove(key);
  85. }
  86. }
  87. }
  88. /**
  89. * 发送自定义消息
  90. */
  91. public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
  92. System.out.println("发送消息到:" + userId + ",内容:" + message);
  93. if (!StringUtils.isEmpty(userId) && webSocketMap.containsKey(userId)) {
  94. webSocketMap.get(userId).getBasicRemote().sendText(message);
  95. //webSocketServer.sendMessage(message);
  96. } else {
  97. System.out.println("用户" + userId + ",不在线!");
  98. }
  99. }
  100. public static synchronized int getOnlineCount() {
  101. return onlineCount;
  102. }
  103. public static synchronized void addOnlineCount() {
  104. WebSocketServer.onlineCount++;
  105. }
  106. public static synchronized void subOnlineCount() {
  107. WebSocketServer.onlineCount--;
  108. }
  109. }

二、编写测试HTML

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>RTC视频通话测试页面</title>
  5. <meta charset="UTF-8"> <!-- for HTML5 -->
  6. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  7. <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
  8. </head>
  9. <body>
  10. <style type="text/css">
  11. body {
  12. background: #000;
  13. }
  14. button {
  15. height: 40px;
  16. line-height: 40px;
  17. width: auto;
  18. padding: 0 15px;
  19. background: #ccc;
  20. border: none;
  21. border-radius: 10px;
  22. margin-bottom: 10px;
  23. overflow: hidden;
  24. }
  25. .wrap {
  26. width: 100vw;
  27. height: 100vh;
  28. display: flex;
  29. justify-content: center;
  30. align-items: center;
  31. }
  32. .video-box {
  33. border-radius: 20px;
  34. background: pink;
  35. position: relative;
  36. width: 800px;
  37. height: 600px;
  38. overflow: hidden;
  39. }
  40. .remote-video {
  41. width: 800px;
  42. height: 600px;
  43. border: 1px solid black;
  44. overflow: hidden;
  45. }
  46. .local-video {
  47. width: 320px;
  48. height: 240px;
  49. position: absolute;
  50. right: 0;
  51. bottom: 0;
  52. border-radius: 20px 0 0 0;
  53. overflow: hidden;
  54. }
  55. video {
  56. width: 100%;
  57. height: 100%;
  58. }
  59. </style>
  60. <div class="wrap">
  61. <div>
  62. <div>
  63. <button type="button" onclick="startVideo();">开启本机摄像和音频</button>
  64. <button type="button" onclick="connect();">建立连接</button>
  65. <button type="button" onclick="hangUp();">挂断</button>
  66. <button type="button" onclick="refreshPage();">刷新页面</button>
  67. </div>
  68. <div class="video-box">
  69. <div class="local-video">
  70. <video id="local-video" autoplay style=""></video>
  71. </div>
  72. <div class="remote-video">
  73. <video id="remote-video" autoplay></video>
  74. </div>
  75. </div>
  76. </div>
  77. </div>
  78. <script>
  79. // ===================以下是socket=======================
  80. var user = Math.round(Math.random() * 1000) + ""
  81. var socketUrl = "ws://localhost:8080/msgServer/" + user;
  82. var socket = null
  83. var socketRead = false
  84. window.onload = function() {
  85. socket = new WebSocket(socketUrl)
  86. socket.onopen = function() {
  87. console.log("成功连接到服务器...")
  88. socketRead = true
  89. }
  90. socket.onclose = function(e) {
  91. console.log(''与服务器连接关闭: '' + e.code)
  92. socketRead = false
  93. }
  94. socket.onmessage = function(res) {
  95. var evt = JSON.parse(res.data)
  96. console.log(evt)
  97. if (evt.type === ''offer'') {
  98. console.log("接收到offer,设置offer,发送answer....")
  99. onOffer(evt);
  100. } else if (evt.type === ''answer'' && peerStarted) {
  101. console.log(''接收到answer,设置answer SDP'');
  102. onAnswer(evt);
  103. } else if (evt.type === ''candidate'' && peerStarted) {
  104. console.log(''接收到ICE候选者..'');
  105. onCandidate(evt);
  106. } else if (evt.type === ''bye'' && peerStarted) {
  107. console.log("WebRTC通信断开");
  108. stop();
  109. }
  110. }
  111. }
  112. // ===================以上是socket=======================
  113. var localVideo = document.getElementById(''local-video'');
  114. var remoteVideo = document.getElementById(''remote-video'');
  115. var localStream = null;
  116. var peerConnection = null;
  117. var peerStarted = false;
  118. var mediaConstraints = {
  119. ''mandatory'': {
  120. ''OfferToReceiveAudio'': false,
  121. ''OfferToReceiveVideo'': true
  122. }
  123. };
  124. //----------------------交换信息 -----------------------
  125. function onOffer(evt) {
  126. console.log("接收到offer...")
  127. console.log(evt);
  128. setOffer(evt);
  129. sendAnswer(evt);
  130. peerStarted = true
  131. }
  132. function onAnswer(evt) {
  133. console.log("接收到Answer...")
  134. console.log(evt);
  135. setAnswer(evt);
  136. }
  137. function onCandidate(evt) {
  138. var candidate = new RTCIceCandidate({
  139. sdpMLineIndex: evt.sdpMLineIndex,
  140. sdpMid: evt.sdpMid,
  141. candidate: evt.candidate
  142. });
  143. console.log("接收到Candidate...")
  144. console.log(candidate);
  145. peerConnection.addIceCandidate(candidate);
  146. }
  147. function sendSDP(sdp) {
  148. var text = JSON.stringify(sdp);
  149. console.log(''发送sdp.....'')
  150. console.log(text); // "type":"offer"....
  151. // textForSendSDP.value = text;
  152. // 通过socket发送sdp
  153. socket.send(text)
  154. }
  155. function sendCandidate(candidate) {
  156. var text = JSON.stringify(candidate);
  157. console.log(text); // "type":"candidate","sdpMLineIndex":0,"sdpMid":"0","candidate":"....
  158. socket.send(text) // socket发送
  159. }
  160. //---------------------- 视频处理 -----------------------
  161. function startVideo() {
  162. navigator.webkitGetUserMedia({
  163. video: true,
  164. audio: true
  165. },
  166. function(stream) { //success
  167. localStream = stream;
  168. localVideo.srcObject = stream;
  169. //localVideo.src = window.URL.createObjectURL(stream);
  170. localVideo.play();
  171. localVideo.volume = 0;
  172. },
  173. function(error) { //error
  174. console.error(''发生了一个错误: [错误代码:'' + error.code + '']'');
  175. return;
  176. });
  177. }
  178. function refreshPage() {
  179. location.reload();
  180. }
  181. //---------------------- 处理连接 -----------------------
  182. function prepareNewConnection() {
  183. var pc_config = {
  184. "iceServers": []
  185. };
  186. var peer = null;
  187. try {
  188. peer = new webkitRTCPeerConnection(pc_config);
  189. } catch (e) {
  190. console.log("建立连接失败,错误:" + e.message);
  191. }
  192. // 发送所有ICE候选者给对方
  193. peer.onicecandidate = function(evt) {
  194. if (evt.candidate) {
  195. console.log(evt.candidate);
  196. sendCandidate({
  197. type: "candidate",
  198. sdpMLineIndex: evt.candidate.sdpMLineIndex,
  199. sdpMid: evt.candidate.sdpMid,
  200. candidate: evt.candidate.candidate
  201. });
  202. }
  203. };
  204. console.log(''添加本地视频流...'');
  205. peer.addStream(localStream);
  206. peer.addEventListener("addstream", onRemoteStreamAdded, false);
  207. peer.addEventListener("removestream", onRemoteStreamRemoved, false);
  208. // 当接收到远程视频流时,使用本地video元素进行显示
  209. function onRemoteStreamAdded(event) {
  210. console.log("添加远程视频流");
  211. // remoteVideo.src = window.URL.createObjectURL(event.stream);
  212. remoteVideo.srcObject = event.stream;
  213. }
  214. // 当远程结束通信时,取消本地video元素中的显示
  215. function onRemoteStreamRemoved(event) {
  216. console.log("移除远程视频流");
  217. remoteVideo.src = "";
  218. }
  219. return peer;
  220. }
  221. function sendOffer() {
  222. peerConnection = prepareNewConnection();
  223. peerConnection.createOffer(function(sessionDescription) { //成功时调用
  224. peerConnection.setLocalDescription(sessionDescription);
  225. console.log("发送: SDP");
  226. console.log(sessionDescription);
  227. sendSDP(sessionDescription);
  228. }, function(err) { //失败时调用
  229. console.log("创建Offer失败");
  230. }, mediaConstraints);
  231. }
  232. function setOffer(evt) {
  233. if (peerConnection) {
  234. console.error(''peerConnection已存在!'');
  235. return;
  236. }
  237. peerConnection = prepareNewConnection();
  238. peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  239. }
  240. function sendAnswer(evt) {
  241. console.log(''发送Answer,创建远程会话描述...'');
  242. if (!peerConnection) {
  243. console.error(''peerConnection不存在!'');
  244. return;
  245. }
  246. peerConnection.createAnswer(function(sessionDescription) { //成功时
  247. peerConnection.setLocalDescription(sessionDescription);
  248. console.log("发送: SDP");
  249. console.log(sessionDescription);
  250. sendSDP(sessionDescription);
  251. }, function() { //失败时
  252. console.log("创建Answer失败");
  253. }, mediaConstraints);
  254. }
  255. function setAnswer(evt) {
  256. if (!peerConnection) {
  257. console.error(''peerConnection不存在!'');
  258. return;
  259. }
  260. peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  261. }
  262. //-------- 处理用户UI事件 -----
  263. // 开始建立连接
  264. function connect() {
  265. if (!localStream) {
  266. alert("请首先捕获本地视频数据.");
  267. } else if (peerStarted || !socketRead) {
  268. alert("请刷新页面后重试.");
  269. } else {
  270. sendOffer();
  271. peerStarted = true;
  272. }
  273. }
  274. // 停止连接
  275. function hangUp() {
  276. console.log("挂断.");
  277. stop();
  278. }
  279. function stop() {
  280. peerConnection.close();
  281. peerConnection = null;
  282. peerStarted = false;
  283. }
  284. </script>
  285. </body>
  286. </html>

三、本地打开测试

因为打开摄像头就露脸了,所以就初始化截个图吧

New Image

四、搭建STUN和TURN服务:传送门

五、更改 html 配置

  1. var pc_config = {
  2.                     "iceServers": [{
  3.                         url: "stun:ip:端口"
  4.                     }, {
  5.                         url: "turn:ip:端口",
  6.                         credential: "kurento",
  7.                         username: "kurento"
  8.                     }]
  9.                 };

注:如果想要非局域网测试,需要把 STUN、TURN 和 websocket服务 要部署到公网环境,然后记得更改 html内的 websocket ip和端口

注:以上内容仅提供参考和交流,请勿用于商业用途,如有侵权联系本人删除!