Spring Boot集成websocket实现webrtc功能

1.什么是webrtc?

WebRTC 是 Web 实时通信(Real-Time Communication)的缩写,它既是 API 也是协议。WebRTC 协议是两个 WebRTC Agent 协商双向安全实时通信的一组规则。开发人员可以通过 WebRTC API 使用 WebRTC 协议。目前 WebRTC API 仅有 JavaScript 版本。 可以用 HTTP 和 Fetch API 之间的关系作为类比。WebRTC 协议就是 HTTP,而 WebRTC API 就是 Fetch API。 除了 JavaScript 语言,WebRTC 协议也可以在其他 API 和语言中使用。你还可以找到 WebRTC 的服务器和特定领域的工具。所有这些实现都使用 WebRTC 协议,以便它们可以彼此交互。 WebRTC 协议由 IETF 工作组在rtcweb中维护。WebRTC API 的 W3C 文档在webrtc。

WebSocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

webrtc架构

architecture

2.代码工程

实验目标

实现视频通话功能

pom.xml

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>springboot-demo</artifactId><groupId>com.et</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>WebRTC</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies>
</project>

controller

package com.et.webrtc.controller;import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;import java.util.HashMap;
import java.util.Map;@RestController
public class HelloWorldController {@RequestMapping("/hello")public Map<String, Object> showHelloWorld(){Map<String, Object> map = new HashMap<>();map.put("msg", "HelloWorld");return map;}/*** WebRTC + WebSocket*/@RequestMapping("webrtc/{username}.html")public ModelAndView socketChartPage(@PathVariable String username) {ModelAndView modelAndView = new ModelAndView();modelAndView.setViewName("webrtc.html");modelAndView.addObject("username",username);return modelAndView;}
}

config

package com.et.webrtc.config;import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** WebRTC + WebSocket*/
@Slf4j
@Component
@ServerEndpoint(value = "/webrtc/{username}")
public class WebRtcWSServer {/*** 连接集合*/private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("username") String username, @PathParam("publicKey") String publicKey) {sessionMap.put(username, session);}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(Session session) {for (Map.Entry<String, Session> entry : sessionMap.entrySet()) {if (entry.getValue() == session) {sessionMap.remove(entry.getKey());break;}}}/*** 发生错误时调用*/@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}/*** 服务器接收到客户端消息时调用的方法*/@OnMessagepublic void onMessage(String message, Session session) {try{//jacksonObjectMapper mapper = new ObjectMapper();mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);//JSON字符串转 HashMapHashMap hashMap = mapper.readValue(message, HashMap.class);//消息类型String type = (String) hashMap.get("type");//to userString toUser = (String) hashMap.get("toUser");Session toUserSession = sessionMap.get(toUser);String fromUser = (String) hashMap.get("fromUser");//msgString msg = (String) hashMap.get("msg");//sdpString sdp = (String) hashMap.get("sdp");//iceMap iceCandidate  = (Map) hashMap.get("iceCandidate");HashMap<String, Object> map = new HashMap<>();map.put("type",type);//呼叫的用户不在线if(toUserSession == null){toUserSession = session;map.put("type","call_back");map.put("fromUser","系统消息");map.put("msg","Sorry,呼叫的用户不在线!");send(toUserSession,mapper.writeValueAsString(map));return;}//对方挂断if ("hangup".equals(type)) {map.put("fromUser",fromUser);map.put("msg","对方挂断!");}//视频通话请求if ("call_start".equals(type)) {map.put("fromUser",fromUser);map.put("msg","1");}//视频通话请求回应if ("call_back".equals(type)) {map.put("fromUser",toUser);map.put("msg",msg);}//offerif ("offer".equals(type)) {map.put("fromUser",toUser);map.put("sdp",sdp);}//answerif ("answer".equals(type)) {map.put("fromUser",toUser);map.put("sdp",sdp);}//iceif ("_ice".equals(type)) {map.put("fromUser",toUser);map.put("iceCandidate",iceCandidate);}send(toUserSession,mapper.writeValueAsString(map));}catch(Exception e){e.printStackTrace();}}/*** 封装一个send方法,发送消息到前端*/private void send(Session session, String message) {try {System.out.println(message);session.getBasicRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}
}
package com.et.webrtc.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration
@EnableWebSocket
public class WebSocketConfiguration {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}

前端页面

<!DOCTYPE>
<!--解决idea thymeleaf 表达式模板报红波浪线-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>WebRTC + WebSocket</title><meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"><style>html,body{margin: 0;padding: 0;}#main{position: absolute;width: 370px;height: 550px;}#localVideo{position: absolute;background: #757474;top: 10px;right: 10px;width: 100px;height: 150px;z-index: 2;}#remoteVideo{position: absolute;top: 0px;left: 0px;width: 100%;height: 100%;background: #222;}#buttons{z-index: 3;bottom: 20px;left: 90px;position: absolute;}#toUser{border: 1px solid #ccc;padding: 7px 0px;border-radius: 5px;padding-left: 5px;margin-bottom: 5px;}#toUser:focus{border-color: #66afe9;outline: 0;-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}#call{width: 70px;height: 35px;background-color: #00BB00;border: none;margin-right: 25px;color: white;border-radius: 5px;}#hangup{width:70px;height:35px;background-color:#FF5151;border:none;color:white;border-radius: 5px;}</style>
</head>
<body>
<div id="main"><video id="remoteVideo" playsinline autoplay></video><video id="localVideo" playsinline autoplay muted></video><div id="buttons"><input id="toUser" placeholder="输入在线好友账号"/><br/><button id="call">视频通话</button><button id="hangup">挂断</button></div>
</div>
</body>
<!-- 可引可不引 -->
<!--<script th:src="@{/js/adapter-2021.js}"></script>-->
<script type="text/javascript" th:inline="javascript">let username = /*[[${username}]]*/'';let localVideo = document.getElementById('localVideo');let remoteVideo = document.getElementById('remoteVideo');let websocket = null;let peer = null;WebSocketInit();ButtonFunInit();/* WebSocket */function WebSocketInit(){//判断当前浏览器是否支持WebSocketif ('WebSocket' in window) {websocket = new WebSocket("wss://192.168.0.104/webrtc/"+username);} else {alert("当前浏览器不支持WebSocket!");}//连接发生错误的回调方法websocket.onerror = function (e) {alert("WebSocket连接发生错误!");};//连接关闭的回调方法websocket.onclose = function () {console.error("WebSocket连接关闭");};//连接成功建立的回调方法websocket.onopen = function () {console.log("WebSocket连接成功");};//接收到消息的回调方法websocket.onmessage = async function (event) {let { type, fromUser, msg, sdp, iceCandidate } = JSON.parse(event.data.replace(/\n/g,"\\n").replace(/\r/g,"\\r"));console.log(type);if (type === 'hangup') {console.log(msg);document.getElementById('hangup').click();return;}if (type === 'call_start') {let msg = "0"if(confirm(fromUser + "发起视频通话,确定接听吗")==true){document.getElementById('toUser').value = fromUser;WebRTCInit();msg = "1"}websocket.send(JSON.stringify({type:"call_back",toUser:fromUser,fromUser:username,msg:msg}));return;}if (type === 'call_back') {if(msg === "1"){console.log(document.getElementById('toUser').value + "同意视频通话");//创建本地视频并发送offerlet stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })localVideo.srcObject = stream;stream.getTracks().forEach(track => {peer.addTrack(track, stream);});let offer = await peer.createOffer();await peer.setLocalDescription(offer);let newOffer = offer.toJSON();newOffer["fromUser"] = username;newOffer["toUser"] = document.getElementById('toUser').value;websocket.send(JSON.stringify(newOffer));}else if(msg === "0"){alert(document.getElementById('toUser').value + "拒绝视频通话");document.getElementById('hangup').click();}else{alert(msg);document.getElementById('hangup').click();}return;}if (type === 'offer') {let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });localVideo.srcObject = stream;stream.getTracks().forEach(track => {peer.addTrack(track, stream);});await peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));let answer = await peer.createAnswer();let newAnswer = answer.toJSON();newAnswer["fromUser"] = username;newAnswer["toUser"] = document.getElementById('toUser').value;websocket.send(JSON.stringify(newAnswer));await peer.setLocalDescription(answer);return;}if (type === 'answer') {peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));return;}if (type === '_ice') {peer.addIceCandidate(iceCandidate);return;}}}/* WebRTC */function WebRTCInit(){peer = new RTCPeerConnection();//icepeer.onicecandidate = function (e) {if (e.candidate) {websocket.send(JSON.stringify({type: '_ice',toUser:document.getElementById('toUser').value,fromUser:username,iceCandidate: e.candidate}));}};//trackpeer.ontrack = function (e) {if (e && e.streams) {remoteVideo.srcObject = e.streams[0];}};}/* 按钮事件 */function ButtonFunInit(){//视频通话document.getElementById('call').onclick = function (e){document.getElementById('toUser').style.visibility = 'hidden';let toUser = document.getElementById('toUser').value;if(!toUser){alert("请先指定好友账号,再发起视频通话!");return;}if(peer == null){WebRTCInit();}websocket.send(JSON.stringify({type:"call_start",fromUser:username,toUser:toUser,}));}//挂断document.getElementById('hangup').onclick = function (e){document.getElementById('toUser').style.visibility = 'unset';if(localVideo.srcObject){const videoTracks = localVideo.srcObject.getVideoTracks();videoTracks.forEach(videoTrack => {videoTrack.stop();localVideo.srcObject.removeTrack(videoTrack);});}if(remoteVideo.srcObject){const videoTracks = remoteVideo.srcObject.getVideoTracks();videoTracks.forEach(videoTrack => {videoTrack.stop();remoteVideo.srcObject.removeTrack(videoTrack);});//挂断同时,通知对方websocket.send(JSON.stringify({type:"hangup",fromUser:username,toUser:document.getElementById('toUser').value,}));}if(peer){peer.ontrack = null;peer.onremovetrack = null;peer.onremovestream = null;peer.onicecandidate = null;peer.oniceconnectionstatechange = null;peer.onsignalingstatechange = null;peer.onicegatheringstatechange = null;peer.onnegotiationneeded = null;peer.close();peer = null;}localVideo.srcObject = null;remoteVideo.srcObject = null;}}
</script>
</html>

DemoAppliciation.java

package com.et.webrtc;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

  • GitHub - Harries/springboot-demo: a simple springboot demo with some components for example: redis,solr,rockmq and so on.

3.测试

启动Spring Boot应用

测试视频通话

前置条件:必须是https协议,不然无法打开视频和语音权限

  • 笔记本:https://192.168.0.104/webrtc/2.html
  • 手机:https://192.168.0.104/webrtc/1.html

输入对方id,进行视屏通话

4.引用

  • 是什么,为什么,如何使用 | 给好奇者的WebRTC
  • https://www.cnblogs.com/huanzi-qch/p/15716286.html
  • Spring Boot集成websocket实现webrtc功能 | Harries Blog™

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/29315.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPF学习(4)--SCICHART学习

一、项目创建过程 1.下载SCICHART插件 2.选中第一个&#xff0c;确保引用中有我们要用的 二、示例代码 1.前端代码 <Window x:Class"SciChart.Examples.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"h…

centos 7无需token编译安装freeswitch 1.10.11 ——筑梦之路

准备工作 安装编译工具和依赖包 yum update -y sudo yum install epel-release vim tcpdump net-tools.x86_64 -y sudo yum install gcc-c sqlite-devel zlib-devel libcurl-devel pcre-devel speex-devel ldns-devel libedit-devel openssl-devel git -y yum install yasm n…

DFS 迷宫

个人感觉DFS没有递归那么烧脑 简单迷宫 如何接受二维数组 先构建A[MAXN][MAXN]&#xff0c;人后二重循环 #include <iostream> #include <vector> #include <cmath> #include <string> #include <cstring> using namespace std; const int N…

微型操作系统内核源码详解系列五(2):cm3下栈的初始化

系列一&#xff1a;微型操作系统内核源码详解系列一&#xff1a;rtos内核源码概论篇&#xff08;以freertos为例&#xff09;-CSDN博客 系列二&#xff1a;微型操作系统内核源码详解系列二&#xff1a;数据结构和对象篇&#xff08;以freertos为例&#xff09;-CSDN博客 系列…

windows反弹shell的方法

什么是正向shell和反向shell 首先说&#xff0c;正向shell是控制端主动连接被控制端&#xff0c;通过目标主机开放一个监听端口等待其他主机访问&#xff0c;从而获得对目标主机的shell访问&#xff0c;优点是控制端可以整个控制目标主机&#xff0c;但缺点会受到防火墙的连&a…

手机usb共享网络电脑没反应的方法

适用于win10电脑&#xff0c;安卓手机上可以 开启usb网络共享选择&#xff0c;如果选择后一直跳&#xff0c;让重复选择usb选项的话&#xff0c;就开启 开发者模式&#xff0c;进到 开发者模式 里设置 默认usb 共享网络 选项 &#xff0c;就不会一直跳让你选。 1.先用数据线 连…

如何通过Appium连接真机调试

1、打开appium&#xff0c;点击启动appium服务器&#xff08;如图1&#xff09; 2、appium启动成功后&#xff0c;点击放大镜启动检查会话&#xff08;如图2&#xff09; 3、填写真机设备信息和APP的package、activity,点击启动会话&#xff08;如图3&#xff09; 4、打开运行A…

数据结构-十大排序算法集合(四万字精讲集合)

前言 1&#xff0c;数据结构排序篇章是一个大的工程&#xff0c;这里是一个总结篇章&#xff0c;配备动图和过程详解&#xff0c;从难到易逐步解析。 2&#xff0c;这里我们详细分析几个具备教学意义和实际使用意义的排序&#xff1a; 冒泡排序&#xff0c;选择排序&#xff0c…

Swift Combine — Subject Publishers(PassthroughSubject CurrentValueSubject)

本文主要介绍一下Subject&#xff0c;Subject 本身也是一个 Publisher&#xff0c;其定义如下&#xff1a; public protocol Subject<Output, Failure> : AnyObject, Publisher {func send(_ value: Self.Output)func send(completion: Subscribers.Completion<Self.…

【漏洞复现】泛微OA E-Cology ln.FileDownload 任意文件读取漏洞

免责声明&#xff1a; 本文内容旨在提供有关特定漏洞或安全漏洞的信息&#xff0c;以帮助用户更好地了解可能存在的风险。公布此类信息的目的在于促进网络安全意识和技术进步&#xff0c;并非出于任何恶意目的。阅读者应该明白&#xff0c;在利用本文提到的漏洞信息或进行相关测…

JAVA开发 选择本地的文件,控制台输出选择的文件名

打开文件选择器对话框&#xff0c;控制台输出选择的文件 1、展示效果2、实现代码3、JFileChooser类 1、展示效果 2、实现代码 import javax.swing.*; import java.io.*; import java.text.SimpleDateFormat; import java.util.Date;public class GenerateCompress {public sta…

强大的api管理系统Storm Core API_V1.1免授权源码

强大的api管理系统Storm Core API_V1.1免授权源码&#xff0c;带用户key和ip白名单功能 可设置付费操作等 更新日志 此次更新功能比较多 1.完善个人中心页面 2.完善注册登录页面 3.完善key功能 4.增加ip白名单功能 5.以及一些其他小小的美化 6.模板dxx的图片可单个自定…

AWS——01篇(AWS入门 以及 AWS之EC2实例及简单实用)AWS

AWS——01篇&#xff08;AWS入门 以及 AWS之EC2实例及简单实用&#xff09; 1. 前言 2. 创建AWS账户 3. EC2 3.1 启动 EC2 新实例 3.1.1 入口 3.1.2 设置名称 选择服务 3.1.3 创建密钥对 3.1.4 网络设置——安全组 3.1.4.1 初始设置 3.1.4.2 添加安全组规则&#xff08;开放新…

【Linux】MySQL下载与安装

1. 下载压缩包 官网下载 2. 解压 上传并解压好放在指定位置 创建soft文件夹 mkdir /soft上传文件&#xff0c;在该目录下再创建一个mysql文件夹&#xff0c;将安装包解压到新文件夹中 mkdir /soft/mysql-8.3.0 tar -xvf mysql-8.3.0-1.el9.x86_64.rpm-bundle.tar -C mysq…

C语言 | Leetcode C语言题解之第162题寻找峰值

题目&#xff1a; 题解&#xff1a; int findPeakElement(int* nums, int numsSize) {int ls_max0;for(int i1;i<numsSize;i){if(nums[ls_max]>nums[i]);else{ls_maxi;}}return ls_max; }

2024上海初中生古诗文大会倒计时4个月:单选题真题和独家解析

现在距离2024年初中生古诗文大会还有4个多月时间&#xff0c;我们继续来看10道选择题真题和详细解析&#xff0c;以下题目截取自我独家制作的在线真题集&#xff0c;都是来自于历届真题&#xff0c;去重、合并后&#xff0c;每道题都有参考答案和解析。 为帮助孩子自测和练习&…

演示:WPF开发的Diagram自动化流程图应用

一、目的&#xff1a;演示Diaram应用功能 二、预览 三、功能列表 功能模块 通用测试 流程图 仪器仪表 机器人 网络通信测试 PLC测试 轮毂生产线流程测试 图像处理 目标检测 绘图 思维导图 图表 流程图功能 模板管理 工程管理 模块许可管理 工具栏 开始 停止 删除 清除 …

RabbitMQ实践——对不可路由的消息的处理方法

大纲 无备用&#xff08;Alternate&#xff09;交换器构建交换器、队列和绑定关系测试代码抛弃不可路由消息返还不可路由消息 有备用&#xff08;Alternate&#xff09;交换器创建带备用&#xff08;Alternate&#xff09;交换器的交换器mandatory非mandatory 总结 在实际工作中…

【DevOps】Elasticsearch 数据跨集群同步方案

目录 1、Elasticsearch Cross-Cluster Replication (CCR) 1.1、优点 1.2、缺点 1.3、步骤 1.4、示例 2. Logstash 或其他 ETL 工具 2.1、优点 2.2、缺点 2.3、步骤 3. Apache Kafka 或 RabbitMQ 3.1、优点 3.2、缺点 3.3、步骤 4、使用 Reindex API 进行跨集群同…

【LeetCode最详尽解答】42-接雨水 Trapping-Rain-Water

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; 链接: 42-接雨水 直觉 通过可视化图形来解决这个问题会更容易理解和解决。 给定输入: height [0,1,…