一、创建 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和端口
注:以上内容仅提供参考和交流,请勿用于商业用途,如有侵权联系本人删除!