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) &amp;&amp; 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的主要内容,如果未能解决你的问题,请参考以下文章

Springboot+vue3集成使用WebSocket

Springboot+vue3集成使用WebSocket

Spring Boot - WebSocket 握手期间出错

玩转spring boot——websocket

Spring Boot之WebSocket

Spring boot + Websocket