一小时教你用SpringBoot+WebSocket+WebRTC实现视频通话
- 1. 运行结果
- 2. 实现
- 2.1 后端实现
- 2.2 前端页面实现
- 3. 总结
1. 运行结果
SpringBoot+WebSocket+WebRTC实现视频通话
上述运行结果中是有声音(比较小而已)及动的画面的(画面不是静止的)。
网上关于webrtc的文档(文章)和视频也挺多的,但是用SpringBoot结合WebRTC的却屈指可数,前一段时间小编我学习了一下WebRTC的相关知识,于是用SpringBoot+WebRTC实现了一个多人的线上自习室(有画面,但是没有声音的那种,开启声音也挺简单,在js代码里设置一下即可[运行结果在最后的总结里])。最近CSDN有活动,正好把前一段时间学习的知识运用起来(下述代码只是实现了,但是其中的逻辑是存在一定问题的,所以如果读者用下述代码,切记需要改动改动哈!)。既然是WebRTC,为什么又和WebSocket扯上关系了呢?因为利用WebSocket技术来发送消息具有实时性,你看我在这端发送一个消息出去,只要另一端处于连接状态,那么就可以接收到这个消息。而如果使用的是http、https等的话,这一端你发送一个消息,另外一段需要刷新一下页面才能看到消息(当然可以搞个定时器)。结合WebSocket技术,能很快速地实现一个视频通话功能。
2. 实现
导入相关jar包的依赖,如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.10</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
上述jar包可能有一些不需要的喔!
2.1 后端实现
websocket 配置类
GetHttpSessionConfig.class
package com.example.demo.websocket2;import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {@Overridepublic void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {HttpSession httpSession = (HttpSession) request.getHttpSession();// 获取httpsession对象sec.getUserProperties().put(HttpSession.class.getName(), httpSession);}
}
ServerEndpointExporter Bean的定义 Config.class
package com.example.demo.websocket2;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration
public class Config {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
*websocket服务器类 WebSocketServer *
package com.example.demo.websocket2;import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;@Component
@ServerEndpoint(value = "/video",configurator = GetHttpSessionConfig.class)
public class WebSocketServer {//存储客户端的连接对象,每个客户端连接都会产生一个连接对象private static ConcurrentHashMap<String,WebSocketServer> map = new ConcurrentHashMap();//每个连接都会有自己的会话private Session session;private String account;@OnOpenpublic void open(Session session,EndpointConfig config){HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());String account = String.valueOf(httpSession.getAttribute("account"));map.put(account,this);this.session = session;this.account = account;}@OnClosepublic void close(){map.remove(account);}@OnErrorpublic void error(Throwable error){error.printStackTrace();}@OnMessagepublic void getMessage(String message) throws IOException {Set<Map.Entry<String, WebSocketServer>> entries = map.entrySet();for (Map.Entry<String, WebSocketServer> entry : entries) {if(!entry.getKey().equals(account)){//将消息转发到其他非自身客户端entry.getValue().send(message);}}}public void send(String message) throws IOException {if(session.isOpen()){session.getBasicRemote().sendText(message);}}public int getConnetNum(){return map.size();}
}
2.2 前端页面实现
登录界面的代码就不在这儿粘贴了,下面主要展示视频通话界面的代码(包括css样式和js代码都在的)
<html><head><title>main</title><link rel = "stylesheet" href = "https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap-theme.min.css"/></head><style>body {background: #eee;padding: 5% 0;}video {background: black;border: 1px solid gray;}.call-page {position: relative;display: block;margin: 0 auto;width: 500px;height: 500px;}#localVideo {width: 150px;height: 150px;position: absolute;top: 15px;right: 15px;}#remoteVideo {width: 500px;height: 500px;}</style><body><div id = "callPage" class = "call-page"><video id = "localVideo" autoplay></video><video id = "remoteVideo" autoplay></video><div class = "row text-center"><div class = "col-md-12"><input id = "callToUsernameInput" type = "text"placeholder = "username to call" /><button id = "callBtn" class = "btn-success btn">Call</button><button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button></div></div></div><script type="text/javascript">//our usernamevar connectedUser;//connecting to our signaling servervar conn = new WebSocket("ws://localhost:9999/video");conn.onopen = function () {console.log("Connected to the signaling server");};//when we got a message from a signaling serverconn.onmessage = function (msg) {console.log("Got message", msg.data);var data = JSON.parse(msg.data);switch(data.type) {case "login":handleLogin(data.success);break;//when somebody wants to call uscase "offer":handleOffer(data.offer, data.name);break;case "answer":handleAnswer(data.answer);break;//when a remote peer sends an ice candidate to uscase "candidate":handleCandidate(data.candidate);break;case "leave":handleLeave();break;default:break;}};conn.onerror = function (err) {console.log("Got error", err);};//alias for sending JSON encoded messagesfunction send(message) {//attach the other peer username to our messagesif (connectedUser) {message.name = connectedUser;}conn.send(JSON.stringify(message));}//******//UI selectors block//******var callPage = document.querySelector("#callPage");var callToUsernameInput = document.querySelector("#callToUsernameInput");var callBtn = document.querySelector("#callBtn");var hangUpBtn = document.querySelector("#hangUpBtn");var localVideo = document.querySelector("#localVideo");var remoteVideo = document.querySelector("#remoteVideo");var yourConn;var stream;// callPage.style.display = "none";var PeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || undefined);var RTCSessionDescription = (window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription || undefined);navigator.getUserMedia = (navigator.getUserMedia ||navigator.webkitGetUserMedia ||navigator.mozGetUserMedia ||navigator.msGetUserMedia);//**********************//Starting a peer connection//**********************//getting local video streamnavigator.getUserMedia({ video: true, audio: true }, function (myStream) {stream = myStream;//displaying local video stream on the pagelocalVideo.srcObject = stream;//using Google public stun servervar configuration = {"iceServers": []};yourConn = new PeerConnection(configuration);// setup stream listeningyourConn.addStream(stream);//when a remote user adds stream to the peer connection, we display ityourConn.onaddstream = function (e) {remoteVideo.srcObject = e.stream;};// Setup ice handlingyourConn.onicecandidate = function (event) {if (event.candidate) {send({type: "candidate",candidate: event.candidate});}};}, function (error) {console.log(error);});//initiating a callcallBtn.addEventListener("click", function () {var callToUsername = callToUsernameInput.value;if (callToUsername.length > 0) {connectedUser = callToUsername;// create an offeryourConn.createOffer(function (offer) {send({type: "offer",offer: offer});yourConn.setLocalDescription(offer);}, function (error) {alert("Error when creating an offer");});}});//when somebody sends us an offerfunction handleOffer(offer, name) {connectedUser = name;yourConn.setRemoteDescription(new RTCSessionDescription(offer));//create an answer to an offeryourConn.createAnswer(function (answer) {yourConn.setLocalDescription(answer);send({type: "answer",answer: answer});}, function (error) {alert("Error when creating an answer");});}//when we got an answer from a remote userfunction handleAnswer(answer) {yourConn.setRemoteDescription(new RTCSessionDescription(answer));}//when we got an ice candidate from a remote userfunction handleCandidate(candidate) {yourConn.addIceCandidate(new RTCIceCandidate(candidate));}//hang uphangUpBtn.addEventListener("click", function () {send({type: "leave"});handleLeave();});function handleLeave() {connectedUser = null;remoteVideo.src = null;yourConn.close();yourConn.onicecandidate = null;yourConn.onaddstream = null;}
</script></body></html>
3. 总结
上述前端代码参考来自这里:webrtc视频演示,上述代码中如果有不懂的读者可以去仔细看看这个链接里的知识,里面关于webrtc有详细的介绍及实现,不过,没有讲多人的,它只讲了一对一的,不过,前一段时间小编在参考一些大佬的实现思路及自己思考下,也实现了一个多人的,运行结果如下:
基于SpringBoot,WebSocket,WebRTC实现多人自习室功能