【前后端的那些事】webrtc入门demo(代码)

文章目录

  • 前端代码
    • api
    • vue界面
  • 后端
    • model
    • websocket
    • config
    • resource

龙年到了,先祝福各位龙年快乐,事业有成!

最近在搞webrtc,想到【前后端的那些事】好久都没有更新了,所以打算先把最近编写的小demo发出来。

p2p webrtc的demo在编写的时需要编写人员以不同的客户端角度出发编写代码,因此对编码造成一定的障碍,详细的介绍文章不是特别好写,所以我打算先把demo代码先分享出来,后续再进一步整理

效果

在这里插入图片描述

前端代码

api

/src/api/webrtc.ts

export const SIGNAL_TYPE_JOIN = "join";
export const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
export const SIGNAL_TYPE_LEAVE = "leave";
export const SIGNAL_TYPE_NEW_PEER = "new-peer";
export const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
export const SIGNAL_TYPE_OFFER = "offer";
export const SIGNAL_TYPE_ANSWER = "answer";
export const SIGNAL_TYPE_CANDIDATE = "candidate";export class Message {userId: string;roomId: string;remoteUserId: string;data: any;cmd: string;constructor() {this.roomId = "1";}
}export default {SIGNAL_TYPE_JOIN,SIGNAL_TYPE_RESP_JOIN,SIGNAL_TYPE_LEAVE,SIGNAL_TYPE_NEW_PEER,SIGNAL_TYPE_PEER_LEAVE,SIGNAL_TYPE_OFFER,SIGNAL_TYPE_ANSWER,SIGNAL_TYPE_CANDIDATE
}

vue界面

/src/views/welecome/index.vue

<script setup lang="ts">
import { ref, onMounted } from "vue";
import {Message,SIGNAL_TYPE_JOIN,SIGNAL_TYPE_NEW_PEER,SIGNAL_TYPE_RESP_JOIN,SIGNAL_TYPE_OFFER,SIGNAL_TYPE_ANSWER,SIGNAL_TYPE_CANDIDATE
} from "@/api/webrtc";// 链接websocket
const userId = ref<string>(Math.random().toString(36).substr(2));
const remoteUserId = ref<string>();
const ws = new WebSocket("ws://localhost:1000/ws/" + userId.value);const localVideo = ref<HTMLVideoElement>();
const localStream = ref<MediaStream>();const remoteVideo = ref<HTMLVideoElement>();
const remoteStream = ref<MediaStream>();const pc = ref<RTCPeerConnection>();onMounted(() => {localVideo.value = document.querySelector("#localVideo");remoteVideo.value = document.querySelector("#remoteVideo");
})ws.onopen = (ev: Event) => {console.log("连接成功 userId = " + userId.value);
}ws.onmessage = (ev: MessageEvent) => {const data = JSON.parse(ev.data);if (data.cmd === SIGNAL_TYPE_NEW_PEER) {handleNewPeer(data);} else if (data.cmd === SIGNAL_TYPE_RESP_JOIN) {handleRespJoin(data);} else if (data.cmd === SIGNAL_TYPE_OFFER) {handleRemoteOffer(data);} else if (data.cmd === SIGNAL_TYPE_ANSWER) {handleRemoteAnswer(data);} else if (data.cmd === SIGNAL_TYPE_CANDIDATE) {handleRemoteCandidate(data);}
}ws.onclose = (ev) => {console.log("连接关闭 userId = " + userId.value);
}const handleRemoteCandidate = (msg : Message) => {console.log("handleRemoteCandidate...");// 保存远程cadidatepc.value.addIceCandidate(msg.data);
}/*** 处理远端发送来的answer */
const handleRemoteAnswer = (msg : Message) => {console.log("handleRemoteAnswer...");// 保存远端发送的answer(offer)pc.value.setRemoteDescription(msg.data);
}/*** 处理对端发送过来的offer, 并且发送answer(offer) * @param msg */
const handleRemoteOffer = async (msg : Message) => {console.log("handleRemoteOffer...");// 存储对端的offerpc.value.setRemoteDescription(msg.data);// 创建自己的offer(answer)const answer = await pc.value.createAnswer();// 保存本地offerpc.value.setLocalDescription(answer);// 转发answerconst answerMsg = new Message();answerMsg.userId = userId.value;answerMsg.remoteUserId = remoteUserId.value;answerMsg.cmd = SIGNAL_TYPE_ANSWER;answerMsg.data = answer;console.log("发送answer...");ws.send(JSON.stringify(answerMsg));
}/**   * 创建offer,设置本地offer并且发送给对端 */
const handleNewPeer = async (msg : Message) => {console.log("handleNewPeer...");// 存储对端用户idremoteUserId.value = msg.remoteUserId;// todo:// 创建offerconst offer = await pc.value.createOffer()// 本地存储offerpc.value.setLocalDescription(offer);// 转发offerconst offerMsg = new Message();offerMsg.userId = userId.value;offerMsg.remoteUserId = remoteUserId.value;offerMsg.data = offer;offerMsg.cmd = SIGNAL_TYPE_OFFER;console.log("发送offer...");ws.send(JSON.stringify(offerMsg));
}const handleRespJoin = (msg: Message) => {console.log("handleRespJoin...");console.log(msg);remoteUserId.value = msg.remoteUserId;
}const join = async () => {// 初始化视频流console.log(navigator.mediaDevices);const stream = await navigator.mediaDevices.getUserMedia({audio: true,video: true})localVideo.value!.srcObject = streamlocalVideo.value!.play()localStream.value = stream;// 创建pccreatePeerConn();// 加入房间doJoin();
}const doJoin = () => {// 创建信息对象const message = new Message();message.cmd = SIGNAL_TYPE_JOIN;message.userId = userId.value;const msg = JSON.stringify(message);// send messagews.send(msg);
}/*** 创建peerConnection*/
const createPeerConn = () => {pc.value = new RTCPeerConnection();// 将本地流的控制权交给pc// const tracks = localStream.value.getTracks()// for (const track of tracks) {//   pc.value.addTrack(track); // } localStream.value.getTracks().forEach(track => {pc.value.addTrack(track, localStream.value);});pc.value.onicecandidate = (event : RTCPeerConnectionIceEvent) => {if (event.candidate) {// 发送candidateconst msg = new Message();msg.data = event.candidate;msg.userId = userId.value;msg.remoteUserId = remoteUserId.value;msg.cmd = SIGNAL_TYPE_CANDIDATE;console.log("onicecandidate...");console.log(msg);ws.send(JSON.stringify(msg));} else {console.log('candidate is null');}}pc.value.ontrack = (event: RTCTrackEvent) => {console.log("handleRemoteStream add...");// 添加远程的streamremoteVideo.value.srcObject = event.streams[0];remoteStream.value = event.streams[0];}pc.value.onconnectionstatechange = () => {if(pc != null) {console.info("ConnectionState -> " + pc.value.connectionState);}};pc.value.oniceconnectionstatechange = () => {if(pc != null) {console.info("IceConnectionState -> " + pc.value.iceConnectionState);}
}
}
</script><template><el-button @click="join">加入</el-button><div id="videos"><video id="localVideo" autoplay muted playsinline>本地窗口</video><video id="remoteVideo" autoplay playsinline>远端窗口</video></div>
</template>

后端

model

Client.java

import lombok.Data;import javax.websocket.Session;@Data
public class Client {private String userId;private String roomId;private Session session;
}

Message.java

import lombok.Data;@Data
public class Message {private String userId;private String remoteUserId;private Object data;private String roomId;private String cmd;@Overridepublic String toString() {return "Message{" +"userId='" + userId + '\'' +", remoteUserId='" + remoteUserId + '\'' +", roomId='" + roomId + '\'' +", cmd='" + cmd + '\'' +'}';}
}

Constant.java

public interface Constant {String SIGNAL_TYPE_JOIN = "join";String SIGNAL_TYPE_RESP_JOIN = "resp-join";String SIGNAL_TYPE_LEAVE = "leave";String SIGNAL_TYPE_NEW_PEER = "new-peer";String SIGNAL_TYPE_PEER_LEAVE = "peer-leave";String SIGNAL_TYPE_OFFER = "offer";String SIGNAL_TYPE_ANSWER = "answer";String SIGNAL_TYPE_CANDIDATE = "candidate";
}

websocket

WebSocket.java

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fgbg.webrtc.model.Client;
import com.fgbg.webrtc.model.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;import static com.fgbg.webrtc.model.Constant.*;@Component
@Slf4j
@ServerEndpoint("/ws/{userId}")
public class WebSocket {//与某个客户端的连接会话,需要通过它来给客户端发送数据private Client client;// 存储用户private static Map<String, Client> clientMap = new ConcurrentHashMap<>();// 存储房间private static Map<String, Set<String>> roomMap = new ConcurrentHashMap<>();// 为了简化逻辑, 只有一个房间->1号房间static {roomMap.put("1", new HashSet<String>());}private ObjectMapper objectMapper = new ObjectMapper();@OnOpenpublic void onOpen(Session session, @PathParam(value="userId")String userId) {log.info("userId = " + userId + " 加入房间1");Client client = new Client();client.setRoomId("1");client.setSession(session);client.setUserId(userId);this.client = client;clientMap.put(userId, client);}@OnClosepublic void onClose() {String userId = client.getUserId();clientMap.remove(userId);roomMap.get("1").remove(userId);log.info("userId = " + userId + " 退出房间1");}@OnMessagepublic void onMessage(String message) throws JsonProcessingException {// 反序列化messagelog.info("userId = " + client.getUserId() + " 收到消息");Message msg = objectMapper.readValue(message, Message.class);switch (msg.getCmd()) {case SIGNAL_TYPE_JOIN:handleJoin(message, msg);break;case SIGNAL_TYPE_OFFER:handleOffer(message, msg);break;case SIGNAL_TYPE_ANSWER:handleAnswer(message, msg);break;case SIGNAL_TYPE_CANDIDATE:handleCandidate(message, msg);break;}}/*** 转发candidate* @param message* @param msg*/private void handleCandidate(String message, Message msg) throws JsonProcessingException {System.out.println("handleCandidate msg = " + msg);String remoteId = msg.getRemoteUserId();sendMsgByUserId(msg, remoteId);}/*** 转发answer* @param message* @param msg*/private void handleAnswer(String message, Message msg) throws JsonProcessingException {System.out.println("handleAnswer msg = " + msg);String remoteId = msg.getRemoteUserId();sendMsgByUserId(msg, remoteId);}/*** 转发offer* @param message* @param msg*/private void handleOffer(String message, Message msg) throws JsonProcessingException {System.out.println("handleOffer msg = " + msg);String remoteId = msg.getRemoteUserId();sendMsgByUserId(msg, remoteId);}/*** 处理加入房间逻辑* @param message* @param msg*/private void handleJoin(String message, Message msg) throws JsonProcessingException {String roomId = msg.getRoomId();String userId = msg.getUserId();System.out.println("userId = " + msg.getUserId() + " join 房间" + roomId);// 添加到房间内Set<String> room = roomMap.get(roomId);room.add(userId);if (room.size() == 2) {String remoteId = null;for (String id : room) {if (!id.equals(userId)) {remoteId = id;}}// 通知两个客户端// resp-joinMessage respJoinMsg = new Message();respJoinMsg.setUserId(userId);respJoinMsg.setRemoteUserId(remoteId);respJoinMsg.setCmd(SIGNAL_TYPE_RESP_JOIN);sendMsgByUserId(respJoinMsg, userId);// new-peerMessage newPeerMsg = new Message();newPeerMsg.setUserId(remoteId);newPeerMsg.setRemoteUserId(userId);newPeerMsg.setCmd(SIGNAL_TYPE_NEW_PEER);sendMsgByUserId(newPeerMsg, remoteId);}else if (room.size() > 2) {log.error("房间号" + roomId + " 人数过多");return;}}/*** 根据远端用户id, 转发信息*/private void sendMsgByUserId(Message msg, String remoteId) throws JsonProcessingException {Client client = clientMap.get(remoteId);client.getSession().getAsyncRemote().sendText(objectMapper.writeValueAsString(msg));System.out.println("信息转发: " + msg);}
}

config

WebSocketConfig.java

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*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}

resource

application.yml

server:port: 1000

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

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

相关文章

for循环的多重跳出

for的多重跳出 1.前言2.标签使用3.使用异常的方式 本文在jdk17中测试通过 1.前言 前段时间面试时&#xff0c;面试官问我多重for循环如何跳出&#xff0c;我懵了&#xff0c;今天特别的研究了一下 本文主要说的不是continue与break&#xff0c;而是少用的另类操作 1.continue:…

数据结构——5.4 树、森林

5.4 树、森林 概念 树的存储结构 双亲表示法 孩子表示法 孩子兄弟表示法&#xff08;二叉树表示法&#xff09;&#xff1a; 二叉树每个结点有三个变量 ① 二叉树结点值&#xff1a;原树结点的值 ② 二叉树左孩子&#xff1a;原树结点的最左孩子 ③ 二叉树右孩子&#xff1a…

计算机网络——04接入网和物理媒体

接入网和物理媒体 接入网络和物理媒体 怎样将端系统和边缘路由器连接&#xff1f; 住宅接入网络单位接入网络&#xff08;学校、公司&#xff09;无线接入网络 住宅接入&#xff1a;modem 将上网数据调制加载到音频信号上&#xff0c;在电话线上传输&#xff0c;在局端将其…

【C语言|数据结构】数据结构顺序表

目录 一、数据结构 1.1概念 1.2总结 1.3为什么需要数据结构&#xff1f; 二、顺序表 1.顺序表的概念及结构 1.1线性表 2.顺序表分类 2.1顺序表和数组的区别 2.2顺序表的分类 2.2.1静态顺序表 2.2.1.1概念 2.2.1.2缺陷 2.2.2动态顺序表 三、动态顺序表的实现 3.1新…

如何部署一个高可用的 Linux 集群?

部署一个高可用的 Linux 集群需要经过多个步骤和考虑因素。以下是一个简要的指南&#xff0c;帮助您了解如何部署一个高可用的 Linux 集群&#xff1a; 确定需求和目标&#xff1a;在开始部署之前&#xff0c;您需要明确高可用性的定义和目标。对于一些组织而言&#xff0c;高…

计算机设计大赛 深度学习 机器视觉 车位识别车道线检测 - python opencv

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 深度学习 机器视觉 车位识别车道线检测 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) …

EMC学习笔记(二十三)降低EMI的PCB设计指南(三)

双层板电源分配 1.单点与多点分布2.星型分布3.创建网格平面4.旁路和磁珠5.将噪声保持在芯片附近 tips&#xff1a;资料主要来自网络&#xff0c;仅供学习使用。 1.单点与多点分布 在一个真正的单点配电系统中&#xff0c;每个有源元件都有自己独立的电源和地&#xff0c;这些…

【React】如何使antd禁用状态的表单输入组件响应点击事件?

最近遇到一个需求&#xff0c;需要在<Input.textarea>组件中&#xff0c;设置属性disabled为true&#xff0c;使textarea响应点击事件&#xff0c;但直接绑定onClick并不会在禁用状态下被响应。 解决方法1 之后尝试了很多方法&#xff0c;比如设置csspointer-events:no…

【Dubbo源码二:Dubbo服务导出】

入口 Dubbo服务导出的入口&#xff1a;服务导出是在DubboBootstrapApplicationListener在监听到ApplicationContextEvent的ContextRefreshedEvent事件后&#xff0c;会触发dubboBootstrap.start(), 在这个方法中最后会导出Dubbo服务 DubboBootstrapApplicationListener Dub…

无人机飞控算法原理基础研究,多旋翼无人机的飞行控制算法理论详解,无人机飞控软件架构设计

多旋翼无人机的飞行控制算法主要涉及到自动控制器、捷联式惯性导航系统、卡尔曼滤波算法和飞行控制PID算法等部分。 自动控制器是无人机飞行控制的核心部分&#xff0c;它负责接收来自无人机传感器和其他系统的信息&#xff0c;并根据预设的算法和逻辑&#xff0c;对无人机的姿…

【基础】比较器 - 振荡来自何处?

比较器是一个简单的概念-在输入端对两个电压进行比较。输出为高或者低。因此&#xff0c;在转换的过程中为什么存在振荡。 当转换电平缓慢改变的时候&#xff0c;这个现象经常会发生。常常是由于输入信号存在噪声&#xff0c;因此在转换电平附近的轻微波动会引起输出端的振荡。…

基于深度学习算法的轴承故障自主分类

1. 要求 轴承有3种故障&#xff1a;外圈故障&#xff0c;内圈故障&#xff0c;滚珠故障&#xff0c;外加正常的工作状态。如表1所示&#xff0c;结合轴承的3种直径&#xff08;直径1,直径2,直径3&#xff09;&#xff0c;轴承的工作状态有10类&#xff1a; 表1 轴承故障类别 外…

单片机学习路线(简单介绍)

学习单片机对于电子爱好者和未来的嵌入式系统工程师来说是一段激动人心的旅程。单片机因其强大的功能、灵活性以及在各种智能设备中的广泛应用&#xff0c;成为了电子和计算机科学领域一个不可或缺的组成部分。如果你对如何开始这段旅程感到好奇&#xff0c;那么你来对地方了。…

计算机算术

计算机算术 数据是什么 数据是各种各样的信息&#xff0c;如数字、文本、计算机程序、音乐、图像、符号等等&#xff0c;实际上&#xff0c;信息可以是能够被计算机存储和处理的任何事物。 位与字节 计算机中存储和处理信息的最小单位是位&#xff08;Binary digit比特&#x…

[动态规划]判断整除

题目 一个给定的正整数序列&#xff0c;在每个数之前都插入号或-号后计算它们的和。比如序列&#xff1a;1、2、4共有8种可能的序列&#xff1a; (1) (2) (4) 7 (1) (2) (-4) -1 (1) (-2) (4) 3 (1) (-2) (-4) -5 (-1) (2) (4) 5 (-1) (2) (-4) -3 (…

Open CASCADE学习|保存为STL文件

STL (Stereolithography) 文件是一种广泛用于3D打印和计算机辅助设计 (CAD) 领域的文件格式。它描述了一个三维模型的表面而不包含颜色、材质或其他非几何信息。STL文件通常用于3D打印过程中&#xff0c;因为它们仅包含构建物体所需的位置信息。 由于STL文件只包含表面信息&am…

【开源项目阅读】Java爬虫抓取豆瓣图书信息

原项目链接 Java爬虫抓取豆瓣图书信息 本地运行 运行过程 另建项目&#xff0c;把四个源代码文件拷贝到自己的包下面 在代码爆红处按ALTENTER自动导入maven依赖 直接运行Main.main方法&#xff0c;启动项目 运行结果 在本地磁盘上生成三个xml文件 其中的内容即位爬取…

论文阅读-CARD:一种针对复制元数据服务器集群的拥塞感知请求调度方案

论文名称&#xff1a;CARD: A Congestion-Aware Request Dispatching Scheme for Replicated Metadata Server Cluster 摘要 复制元数据服务器集群&#xff08;RMSC&#xff09;在分布式文件系统中非常高效&#xff0c;同时面对数据驱动的场景&#xff08;例如&#xff0c;大…

ECMAScript Modules规范的示例详解

ECMAScript Modules&#xff08;ESM&#xff09;是JavaScript中用于模块化开发的规范&#xff0c;它允许开发者将代码分割成多个独立的文件&#xff0c;以提高代码的可维护性和可重用性。下面是一个ECMAScript Modules规范的示例详解&#xff1a; 创建模块 1.1 导出变量 在一个…

大数据Flume--入门

文章目录 FlumeFlume 定义Flume 基础架构AgentSourceSinkChannelEvent Flume 安装部署安装地址安装部署 Flume 入门案例监控端口数据官方案例实时监控单个追加文件实时监控目录下多个新文件实时监控目录下的多个追加文件 Flume Flume 定义 Flume 是 Cloudera 提供的一个高可用…