当前位置: 首页 > news >正文

局域网中实现一对一视频聊天(附源码)

一、什么是webRTC

WebRTC(Web Real-Time Communication)是一项支持网页浏览器进行实时语音对话或视频对话的API技术。它允许直接在浏览器中实现点对点(Peer-to-Peer,P2P)的通信,而无需任何插件或第三方软件。WebRTC 旨在提供通用的实时通信能力,使得开发者能够在网络应用中构建丰富的实时通信功能,如视频会议、直播、即时消息等。

二、webRTC基本流程

  • 媒体捕获: 使用 navigator.mediaDevices.getUserMedia 捕获音视频流。

  • 信令交换: 通过信令服务器交换 Offer、Answer 和 ICE 候选信息。

  • 连接建立: 使用 STUN/TURN 服务器进行 ICE 候选交换,以建立 P2P 连接。

  • 媒体传输: 一旦连接建立,就可以开始传输音视频数据和任意数据。

三、基本流程图

四、源码

1.发送者

//发送视频邀请
const postoffer = async () => {	try {// 获取本地媒体流const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio:true });// 获取用户 IDconst senderId = userstore.userData.id;const receiverId = sendData.value.receiver_id; // 假设已经在 sendData 中设置了receiver_id// 调用 forwardOffer 函数await forwardOffer(localStream, senderId, receiverId);} catch (error) {console.error("Error during getting user media or sending offer: ", error);}
};//forwardOffer方法export async function forwardOffer(localStream, senderId, receiverId) {const peerConnectionStore = usePeerConnectionStore();// 创建 RTCPeerConnection 实例peerConnectionStore.createPeerConnection();// 添加本地媒体流的轨道到 RTCPeerConnectionlocalStream.getTracks().forEach(track => {peerConnectionStore.peerConnection.addTrack(track, localStream);});// 创建 offerconst offer = await peerConnectionStore.peerConnection.createOffer();await peerConnectionStore.peerConnection.setLocalDescription(offer);// 构建 offer 数据对象const offerData = {type: 5,sdp: offer.sdp,sender_id: senderId,receiver_id: receiverId,content: '发起视频通话',time: new Date().toISOString(),seq_id: uuidv4(),content_type: 1,};// 通过 WebSocket 发送 offer 数据websocketSend(offerData);// 监听 ICE 候选者事件peerConnectionStore.peerConnection.onicecandidate = async (event) => {if (event.candidate) {// 如果有新的候选者,通过 WebSocket 发送给远程对等端const candidateData = {type: 7, // 假设 6 代表 ICE 候选者类型candidate: event.candidate.candidate,sdpMid: event.candidate.sdpMid,sdpMLineIndex: event.candidate.sdpMLineIndex,sender_id: senderId,receiver_id: receiverId,};websocketSend(candidateData);} else {// 所有 ICE 候选者都已经发送完毕console.log('ICE发送完毕');}};}//发送者收到接收者发送的anwserexport function setRemoteDescription1(answerData) {const peerConnectionStore = usePeerConnectionStore();// 设置远程描述peerConnectionStore.peerConnection.setRemoteDescription(new RTCSessionDescription({type: answerData.type,sdp: answerData.sdp})).then(() => {// 设置远程描述成功后//将pinia管理的远程描述的状态设置为truepeerConnectionStore.remoteDescriptionSet = true;// 监听远程媒体流peerConnectionStore.peerConnection.ontrack = (event) => {const remoteVideoElement = document.createElement('video');remoteVideoElement.srcObject = event.streams[0];remoteVideoElement.autoplay = true;remoteVideoElement.playsinline = true; // 避免新窗口播放document.body.appendChild(remoteVideoElement);};}).catch(error => {console.error("Error setting remote description: ", error);});
}

 2.接收者

async function handleOffer(offerData) {const peerConnectionStore = usePeerConnectionStore();// 创建 RTCPeerConnection 实例peerConnectionStore.createPeerConnection();// 监听远程媒体流peerConnectionStore.peerConnection.ontrack = (event) => {console.log("监听到接收者的媒体流", event);// 获取媒体流中的所有轨道const tracks = event.streams[0].getTracks();// 检查是否有视频轨道const hasVideoTrack = tracks.some(track => track.kind === 'video');if (hasVideoTrack) {console.log('这个媒体流中有视频流。');} else {console.log('这个媒体流中没有视频流。');return; // 如果没有视频轨道,就不继续执行}console.log("创建视频元素");// 创建视频元素并设置样式let videoElement = document.createElement('video');videoElement.style.position = 'fixed';videoElement.style.top = '50%';videoElement.style.left = '50%';videoElement.style.transform = 'translate(-50%, -50%)';videoElement.style.width = '100%';videoElement.style.height = '100%';videoElement.style.zIndex = 9999; // 确保视频在最上层videoElement.controls = false; // 不显示视频控件videoElement.autoplay = true; // 确保浏览器允许自动播放console.log("创建视频元素1");videoElement.srcObject = event.streams[0]; // 将远程媒体流绑定到视频元素console.log("创建视频元素2");document.body.appendChild(videoElement); // 视频准备就绪后添加到页面中console.log("创建视频元素3");// videoElement.play(); // 元数据加载完成后开始播放console.log("创建视频元素4");// 监听视频是否准备就绪videoElement.addEventListener('loadedmetadata', () => {console.log("视频元数据已加载,可以播放");console.log("接收媒体流成功并开始播放"); // 移动日志到这里});videoElement.addEventListener('error', (error) => {console.error('视频播放出错:', error);});};console.log("创建anwser实例成功")// 设置远程描述peerConnectionStore.peerConnection.setRemoteDescription(new RTCSessionDescription({type:offerData.type,sdp: offerData.sdp})).then(() => {console.log("设置远程描述")// 设置远程描述成功后peerConnectionStore.remoteDescriptionSet = true;console.log("设置远程描述成功")}).catch(error => {console.error("Error setting remote description: ", error);});try {// 获取本地媒体流const localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });// 获取用户 IDconst senderId = userstore.userData.id;const receiverId = offerData.sender_id; // 假设已经在 sendData 中设置了 receiver_idconsole.log("获取用户id成功")// 调用 forwardOffer 函数await  forwardOffer2(localStream, senderId, receiverId, offerData);} catch (error) {console.error("Error during getting user media or sending offer: ", error);}}//forwardOffer2方法export async function forwardOffer2(localStream, senderId, receiverId, offerData) {// 创建一个新的 RTCPeerConnection 实例,不提供 ICE 服务器
const peerConnectionStore = usePeerConnectionStore();// 添加本地媒体流的轨道到 RTCPeerConnectionlocalStream.getTracks().forEach(track => {peerConnectionStore.peerConnection.addTrack(track, localStream);});console.log("本地媒体流添加本地轨道")try {// 创建 answerconst answer = await peerConnectionStore.peerConnection.createAnswer();await peerConnectionStore.peerConnection.setLocalDescription(answer);// 发送 answer 回 Aconst answerData = {type: 6,sdp: answer.sdp,sender_id: offerData.receiver_id, // B 的 IDreceiver_id: offerData.sender_id, // A 的 IDtime: new Date().toISOString(),seq_id: uuidv4(),content_type: 1, // 假设 1 代表视频通话回应};websocketSend(answerData);// 监听 icecandidate 事件以处理远程 ICE 候选者peerConnectionStore.peerConnection.onicecandidate = async (event) => {if (event.candidate) {try {// 如果有新的候选者,通过 WebSocket 发送给远程对等端const candidateData = {type: 7, // 假设 7 代表 ICE 候选者类型candidate: event.candidate.candidate,sdpMid: event.candidate.sdpMid,sdpMLineIndex: event.candidate.sdpMLineIndex,sender_id: senderId,receiver_id: receiverId,};console.log(candidateData,"这里!!!!!!!!!!!!!!")websocketSend(candidateData);console.log("ICE2发送完毕")} catch (error) {console.error('ICE发送出错:', error);}}};// 监听远程媒体流peerConnectionStore.peerConnection.ontrack = (event) => {console.log("监听到接收者的媒体流", event);// 获取媒体流中的所有轨道const tracks = event.streams[0].getTracks();// 检查是否有视频轨道const hasVideoTrack = tracks.some(track => track.kind === 'video');if (hasVideoTrack) {console.log('这个媒体流中有视频流。');} else {console.log('这个媒体流中没有视频流。');return; // 如果没有视频轨道,就不继续执行}console.log("创建视频元素");// 创建视频元素并设置样式let videoElement = document.createElement('video');videoElement.style.position = 'fixed';videoElement.style.top = '50%';videoElement.style.left = '50%';videoElement.style.transform = 'translate(-50%, -50%)';videoElement.style.width = '100%';videoElement.style.height = '100%';videoElement.style.zIndex = 9999; // 确保视频在最上层videoElement.controls = false; // 不显示视频控件videoElement.autoplay = true; // 确保浏览器允许自动播放console.log("创建视频元素1");videoElement.srcObject = event.streams[0]; // 将远程媒体流绑定到视频元素console.log("创建视频元素2");document.body.appendChild(videoElement); // 视频准备就绪后添加到页面中console.log("创建视频元素3");// videoElement.play(); // 元数据加载完成后开始播放console.log("创建视频元素4");// 监听视频是否准备就绪videoElement.addEventListener('loadedmetadata', () => {console.log("视频元数据已加载,可以播放");console.log("接收媒体流成功并开始播放"); // 移动日志到这里});videoElement.addEventListener('error', (error) => {console.error('视频播放出错:', error);});};} catch (error) {console.error("Error handling offer: ", error);}}

 3.双方都收到candidate信息

	function onRemoteIceCandidate(candidateData) {const peerConnectionStore = usePeerConnectionStore();// 确保 peerConnection 已经被创建if (!peerConnectionStore.peerConnection) {peerConnectionStore.createPeerConnection();}// 确保远程描述已经被设置if (peerConnectionStore.remoteDescriptionSet) {const candidate = new RTCIceCandidate({candidate: candidateData.candidate,sdpMid: candidateData.sdpMid,sdpMLineIndex: candidateData.sdpMLineIndex,});peerConnectionStore.peerConnection.addIceCandidate(candidate).then(() => {console.log('远程 ICE 候选者已添加');}).catch((error) => {console.error('添加远程 ICE 候选者失败:', error);});} else {console.error('远程描述尚未设置,无法添加 ICE 候选者');}}//添加候选者方法function addIceCandidate(candidateData) {const peerConnectionStore = usePeerConnectionStore();const candidate = new RTCIceCandidate({candidate: candidateData.candidate,sdpMid: candidateData.sdpMid,sdpMLineIndex: candidateData.sdpMLineIndex,});peerConnectionStore.peerConnection.addIceCandidate(candidate).then(() => {console.log('远程 ICE 候选者已添加');}).catch((error) => {console.error('添加远程 ICE 候选者失败:', error);});}

五、注意避坑

一定要按照流程图上的流程去做,例如:在添加候选者信息之前必须完成设置远程描述,监听对方的视频流一定要在顶部监听,如果写在方法中间可能就不会去调用监听方法

相关文章:

  • Shp2pb:Shapefile转Protocol Buffers的高效工具
  • 直线模组降噪攻略
  • 【专题】2024年中国白酒行业数字化转型研究报告合集PDF分享(附原数据表)
  • 高性能计算应用优化实践之WRF
  • 深度学习04:无监督学习
  • Golang | Leetcode Golang题解之第433题最小基因变化
  • MongoDB 双活集群在运营商的实践
  • postgresql gcc编译选项解释
  • jinaai/jina-embeddings-v2-base-zh向量模型报错解决
  • 从 0 到 1:互联网产品经理核心技能全解析
  • php thinkphp 小程序发送订阅模板消息通知
  • 如何给多台Linux机器设置时间同步
  • Power Platform开发小技巧,一天一个APP, 如何快速搭建二维码识别器
  • 深度学习:(五)初识神经网络
  • Git 提交规范
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • Apache Pulsar 2.1 重磅发布
  • axios请求、和返回数据拦截,统一请求报错提示_012
  • C++类的相互关联
  • CSS魔法堂:Absolute Positioning就这个样
  • C语言笔记(第一章:C语言编程)
  • DOM的那些事
  • Elasticsearch 参考指南(升级前重新索引)
  • Java|序列化异常StreamCorruptedException的解决方法
  • JS基础之数据类型、对象、原型、原型链、继承
  • MySQL常见的两种存储引擎:MyISAM与InnoDB的爱恨情仇
  • use Google search engine
  • 从setTimeout-setInterval看JS线程
  • 从重复到重用
  • 如何使用 JavaScript 解析 URL
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • 回归生活:清理微信公众号
  • ​力扣解法汇总946-验证栈序列
  • #Datawhale AI夏令营第4期#AIGC方向 文生图 Task2
  • #pragma multi_compile #pragma shader_feature
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • (1)bark-ml
  • (2024)docker-compose实战 (8)部署LAMP项目(最终版)
  • (备份) esp32 GPIO
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (附源码)计算机毕业设计ssm-Java网名推荐系统
  • (论文阅读30/100)Convolutional Pose Machines
  • (一)十分简易快速 自己训练样本 opencv级联haar分类器 车牌识别
  • **《Linux/Unix系统编程手册》读书笔记24章**
  • .bat批处理(六):替换字符串中匹配的子串
  • .Net Attribute详解(上)-Attribute本质以及一个简单示例
  • .NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
  • .NET Framework、.NET Core 、 .NET 5、.NET 6和.NET 7 和.NET8 简介及区别
  • .net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?
  • .net 桌面开发 运行一阵子就自动关闭_聊城旋转门家用价格大约是多少,全自动旋转门,期待合作...
  • [100天算法】-目标和(day 79)
  • [Android] 修改设备访问权限
  • [AR Foundation] 人脸检测的流程
  • [C#]OpenCvSharp结合yolov8-face实现L2CS-Net眼睛注视方向估计或者人脸朝向估计