基本介绍
使用websocket来 WebRTC 建立连接时的 数据的传递和交换。
WebRTC 建立连接时,通常需要按照以下顺序执行一些步骤:
1.创建本地 PeerConnection 对象:使用 RTCPeerConnection 构造函数创建本地的 PeerConnection 对象,该对象用于管理 WebRTC 连接。
2.添加本地媒体流:通过调用 getUserMedia 方法获取本地的音视频流,并将其添加到 PeerConnection 对象中。这样可以将本地的音视频数据发送给远程对等方。
3.创建和设置本地 SDP:使用 createOffer 方法创建本地的 Session Description Protocol (SDP),描述本地对等方的音视频设置和网络信息。然后,通过调用 setLocalDescription 方法将本地 SDP 设置为本地 PeerConnection 对象的本地描述。
4.发送本地 SDP:将本地 SDP 发送给远程对等方,可以使用信令服务器或其他通信方式发送。
5.接收远程 SDP:从远程对等方接收远程 SDP,可以通过信令服务器或其他通信方式接收。
6.设置远程 SDP:使用接收到的远程 SDP,调用 PeerConnection 对象的 setRemoteDescription 方法将其设置为远程描述。
7.创建和设置本地 ICE 候选项:使用 onicecandidate 事件监听 PeerConnection 对象的 ICE 候选项生成,在生成候选项后,通过信令服务器或其他通信方式将其发送给远程对等方。
8.接收和添加远程 ICE 候选项:从远程对等方接收到 ICE 候选项后,调用 addIceCandidate 方法将其添加到本地 PeerConnection 对象中。
9.连接建立:一旦本地和远程的 SDP 和 ICE 候选项都设置好并添加完毕,连接就会建立起来。此时,音视频流可以在本地和远程对等方之间进行传输。
windwos+局域网直连的环境,测试过程的问题
1.http协议下安全性原因导致无法调用摄像头和麦克风
chrome://flags/ 配置安全策略 或者 配置本地的https环境
2. 开启了防火墙,webRTC连接失败,
windows防火墙-高级设置-入站规则-新建规则-端口 ,udp
UDP: 32355-65535 放行
为了方便测试 直接关闭防火墙也行。
效果如下 局域网0延迟:
如下demo级别的代码,复制运行就能直接测试,简单修改后,可实现基于webrtc的 共享桌面、视频录制等功能。
html代码
<!DOCTYPE>
<html>
<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="myid" />
<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">
function generateRandomLetters(length) {
let result = '';
const characters = 'abcdefghijklmnopqrstuvwxyz'; // 字母表
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
const randomLetter = characters[randomIndex];
result += randomLetter;
}
return result;
}
let username = generateRandomLetters(2);
document.getElementById('myid').value = username;
let localVideo = document.getElementById('localVideo');
let remoteVideo = document.getElementById('remoteVideo');
let websocket = null;
let peer = null;
let candidate = null;
/* WebSocket */
function WebSocketInit() {
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://192.168.31.14:8181/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, fromUser, msg, sdp, iceCandidate);
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;
console.log(peer);
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;
}
}
WebSocketInit();
ButtonFunInit();
</script>
</html>
websocket java代码
package com.coco.boot.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
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();
}
}
}
springboot 相关依赖和配置
// 1.pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
// 2.启用功能
@EnableWebSocket
public class CocoBootApplication
3.config
package com.coco.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
主要参考:
WebRTC + WebSocket 实现视频通话
WebRTC穿透服务器防火墙配置问题