Spring Boot集成websocket实现webrtc功能

HBLOGA 2024-10-05 11:03:01 阅读 84

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

<code><?xml version="1.0" encoding="UTF-8"?>code>

<project xmlns="http://maven.apache.org/POM/4.0.0"code>

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"code>

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">code>

<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<>();

/**

* 连接建立成功调用的方法

*/

@OnOpen

public void onOpen(Session session, @PathParam("username") String username, @PathParam("publicKey") String publicKey) {

sessionMap.put(username, session);

}

/**

* 连接关闭调用的方法

*/

@OnClose

public void onClose(Session session) {

for (Map.Entry<String, Session> entry : sessionMap.entrySet()) {

if (entry.getValue() == session) {

sessionMap.remove(entry.getKey());

break;

}

}

}

/**

* 发生错误时调用

*/

@OnError

public void onError(Session session, Throwable error) {

error.printStackTrace();

}

/**

* 服务器接收到客户端消息时调用的方法

*/

@OnMessage

public void onMessage(String message, Session session) {

try{

//jackson

ObjectMapper mapper = new ObjectMapper();

mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

//JSON字符串转 HashMap

HashMap hashMap = mapper.readValue(message, HashMap.class);

//消息类型

String type = (String) hashMap.get("type");

//to user

String toUser = (String) hashMap.get("toUser");

Session toUserSession = sessionMap.get(toUser);

String fromUser = (String) hashMap.get("fromUser");

//msg

String msg = (String) hashMap.get("msg");

//sdp

String sdp = (String) hashMap.get("sdp");

//ice

Map 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);

}

//offer

if ("offer".equals(type)) {

map.put("fromUser",toUser);

map.put("sdp",sdp);

}

//answer

if ("answer".equals(type)) {

map.put("fromUser",toUser);

map.put("sdp",sdp);

}

//ice

if ("_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 {

@Bean

public ServerEndpointExporter serverEndpointExporter() {

return new ServerEndpointExporter();

}

}

前端页面

<!DOCTYPE>

<!--解决idea thymeleaf 表达式模板报红波浪线-->

<!--suppress ALL -->

<html xmlns:th="http://www.thymeleaf.org">code>

<head>

<meta charset="UTF-8">code>

<title>WebRTC + WebSocket</title>

<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">code>

<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">code>

<video id="remoteVideo" playsinline autoplay></video>code>

<video id="localVideo" playsinline autoplay muted></video>code>

<div id="buttons">code>

<input id="toUser" placeholder="输入在线好友账号"/><br/>code>

<button id="call">视频通话</button>code>

<button id="hangup">挂断</button>code>

</div>

</div>

</body>

<!-- 可引可不引 -->

<!--<script th:src="@{/js/adapter-2021.js}"></script>-->code>

<script type="text/javascript" th:inline="javascript">code>

let username = /*[[${username}]]*/'';

let localVideo = document.getElementById('localVideo');

let remoteVideo = document.getElementById('remoteVideo');

let websocket = null;

let peer = null;

WebSocketInit();

ButtonFunInit();

/* WebSocket */

function WebSocketInit(){

//判断当前浏览器是否支持WebSocket

if ('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 + "同意视频通话");

//创建本地视频并发送offer

let 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();

//ice

peer.onicecandidate = function (e) {

if (e.candidate) {

websocket.send(JSON.stringify({

type: '_ice',

toUser:document.getElementById('toUser').value,

fromUser:username,

iceCandidate: e.candidate

}));

}

};

//track

peer.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.引用

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。