webrtc实现视频会议
学习链接:webrtc实现视频会议,web多人视频通话,websocket通信
,全部的代码:包括java的后端,vue的webrtc客户端前端代码,放在了微信收藏中
vite配置https、nginx配置ssl、openssl本地搭建
示例
客户端代码
client.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
}
.video-wrapper {
video {
width: 400px;
height: 300px;
border: 1px dashed black;
}
}
</style>
</head>
<body>
<div class="video-wrapper">
<video id="localVideo"></video>
<video id="remoteVideo"></video>
</div>
<div>
目标用户id: <input type="text" id="targetIdIpt" value="2">
<br />
<button id="startCallBtn" onclick="startCall">发起通话</button></button>
</div>
</body>
<script>
let localVideo = document.querySelector('#localVideo')
let remoteVideo = document.querySelector('#remoteVideo')
let targetIdIpt = document.querySelector('#targetIdIpt')
const params = location.href.substring(location.href.indexOf('?') + 1)
const _params = {}
params.split('&').forEach(item => {
const t = item.split('=')
_params[t[0]] = t[1]
})
const { id: currUserId } = _params
startCallBtn.addEventListener('click', startCall)
let localStream = null
let socket = null
let peer = null
async function startCamera() {
localStream = await navigator.mediaDevices.getUserMedia({ video:true, /* audio:true */ })
localVideo.srcObject = localStream
localVideo.play()
}
// 开启摄像头
startCamera()
function initWs() {
socket = new WebSocket('ws://localhost:9095/ws/' + currUserId)
socket.onopen = () => {
console.log('连接建立');
}
socket.onmessage = (e) => {
console.log('收到消息: ', e.data);
let {code, data} = JSON.parse(e.data)
console.log('收到消息的code', code);
if (code == 'connect_success') {
console.log('连接成功');
} else if (code == 'offer') {
acceptCall(data)
} else if (code == 'icecandidate') {
console.log('收到icecandidate消息',peer,data)
let {candidate} = data
peer.addIceCandidate(candidate)
} else if (code == 'answer') {
console.log('对方已同意', data);
acceptAnswer(data)
}
}
socket.onerror = (err) => {
console.log('连接发生错误');
}
socket.onclose = () => {
console.log('连接关闭');
}
}
// 初始化websocket
initWs()
// 发起通话
async function startCall() {
peer = new RTCPeerConnection({})
localStream.getTracks().forEach(track => {
peer.addTrack(track, localStream)
})
peer.addEventListener('track', e => {
console.log('发起方ontrack触发', e);
remoteVideo.srcObject = e.streams[0]
remoteVideo.play()
})
// 这个事件要被触发的前提是, 要先执行上面的把音视频流的轨道给到peer。然后 调用peer.setLocalDescription时会多次连续触发icecandidate事件
peer.addEventListener('icecandidate', e => {
// RTCPeerConnectionIceEvent {isTrusted: true, candidate: RTCIceCandidate, type: 'icecandidate', target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}
console.log('发起方 icecandidate触发', e);
if (e.candidate) {
// e.candidate RTCIceCandidate {candidate: 'candidate:1764731264 1 udp 2122260223 192.168.134.…760 typ host generation 0 ufrag KPfd network-id 1', sdpMid: '1', sdpMLineIndex: 1, foundation: '1764731264', component: 'rtp', …}
console.log('发起方 e.candidate', e.candidate);
// 内网穿透
const message = {
code: 'icecandidate',
data: {
targetId: targetIdIpt.value,
candidate: e.candidate
}
}
socket.send(JSON.stringify(message))
}
})
sendOffer()
}
async function sendOffer() {
// RTCSessionDescription {type: 'offer', sdp: 'v=0\r\no=- 8833742509318497131 2 IN IP4 127.0.0.1\r\ns…770 msid:- 8e3b686c-7779-48a1-8714-8a7a0caf0136\r\n'}
let offer = await peer.createOffer()
console.log('创建了offer', offer);
// 设置offer, 会连续触发多次 peer 的 icecandidat 事件, 最后1次的candidate是null
peer.setLocalDescription(offer)
const message = {
code: 'offer',
data: {
targetId: targetIdIpt.value,
offer: offer
}
}
socket.send(JSON.stringify(message))
}
function acceptAnswer({fromId, answer}) {
console.log('设置answer', answer);
peer.setRemoteDescription(answer)
}
function acceptCall({fromId, offer}) {
console.log('接受通话请求', localStream);
peer = new RTCPeerConnection({})
localStream.getTracks().forEach(track => {
peer.addTrack(track, localStream)
})
peer.addEventListener('track', e => {
console.log('接收方 ontrack触发', e);
remoteVideo.srcObject = e.streams[0]
remoteVideo.play()
})
// 接收方获得offer {type: 'offer', sdp: 'v=0\r\no=- 1381205837170471750 2 IN IP4 127.0.0.1\r\ns…8adcbdb976 87647395-dbcb-4db4-b105-83145f853af1\r\n'}
console.log('接收方获得offer', offer);
peer.addEventListener('icecandidate', e => {
console.log('接收方 icecandidate触发', e);
if (e.candidate) {
console.log('接收方 e.candidate', e.candidate);
// 内网穿透
const message = {
code: 'icecandidate',
data: {
targetId: fromId,
candidate: e.candidate
}
}
socket.send(JSON.stringify(message))
}
})
// 设置远程发过来的offer, 会触发 peer 的 track事件 监听函数
peer.setRemoteDescription(offer)
sendAnswer(fromId)
}
async function sendAnswer(fromId) {
let answer = await peer.createAnswer()
peer.setLocalDescription(answer)
const message = {
code: 'answer',
data: {
targetId: fromId,
answer: answer
}
}
socket.send(JSON.stringify(message))
}
</script>
</html>
服务端代码
SignalWsServer
就是将消息转发给目标对象
@Slf4j
@Component
@ServerEndpoint("/ws/{id}")
public class SignalWsServer {
private static ConcurrentHashMap<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
private Session session;
private String id;
@OnOpen
public void onOpen(@PathParam("id") String id, Session session) {
log.info("连接::onOpen->{}, {}", id, session);
SESSION_MAP.put(id, session);
this.id = id;
this.session = session;
this.session.getUserProperties().put("id", id);
try {
this.session.getBasicRemote().sendText(
JsonUtil.obj2Json(MapBuilder.newHashMap().put("code", "connect_success").build())
);
} catch (IOException e) {
e.printStackTrace();
}
}
@OnMessage
public void onMessage(String msg) throws Exception {
log.info("收到客户端 【{}】 的消息::{}", id, msg);
JSONObject jsonObject = JSON.parseObject(msg);
String code = String.valueOf(jsonObject.get("code"));
if ("offer".equals(code)) {
JSONObject data = jsonObject.getJSONObject("data");
String targetId = data.getString("targetId");
Object offer = data.get("offer");
Session session = SESSION_MAP.get(targetId);
if (session == null) {
log.error("目标用户不存在");
return;
}
Map<String, Object> obj = MapBuilder.newHashMap()
.put("code", "offer")
.put("data", MapBuilder.newHashMap().put("fromId", id).put("offer", offer).build())
.build();
sendMsg(session, obj);
} else if ("icecandidate".equals(code)) {
JSONObject data = jsonObject.getJSONObject("data");
String targetId = data.getString("targetId");
Object candidate = data.get("candidate");
Session session = SESSION_MAP.get(targetId);
if (session == null) {
log.error("目标用户不存在");
return;
}
Map<String, Object> obj = MapBuilder.newHashMap()
.put("code", "icecandidate")
.put("data", MapBuilder.newHashMap().put("fromId", id).put("candidate", candidate).build())
.build();
sendMsg(session, obj);
} else if ("answer".equals(code)) {
JSONObject data = jsonObject.getJSONObject("data");
String targetId = data.getString("targetId");
Object answer = data.get("answer");
Session session = SESSION_MAP.get(targetId);
if (session == null) {
log.error("目标用户不存在");
return;
}
Map<String, Object> obj = MapBuilder.newHashMap()
.put("code", "answer")
.put("data", MapBuilder.newHashMap().put("fromId", id).put("answer", answer).build())
.build();
sendMsg(session, obj);
}
}
private void sendMsg(Session session, Object obj) {
try {
session.getBasicRemote().sendText(JsonUtil.obj2Json(obj));
} catch (IOException e) {
e.printStackTrace();
}
}
@OnClose
public void onClose() {
log.info("关闭::onClose->{}",session);
String id = session.getUserProperties().get("id").toString();
SESSION_MAP.remove(id);
}
@OnError
public void onError(Throwable ex) throws IOException {
log.info("发生错误::onError->{}", ex);
if (this.session.isOpen()) {
session.close();
}
}
}
WsConfig
@Configuration
public class WsConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}