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

news2024/11/16 17:27:11

一、什么是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 });
    // 获取用户 ID
    const 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();
 // 添加本地媒体流的轨道到 RTCPeerConnection
 localStream.getTracks().forEach(track => {
   peerConnectionStore.peerConnection.addTrack(track, localStream);
 });
   // 创建 offer
   const 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发送完毕');
      }
    };
 }

//发送者收到接收者发送的anwser
 export function setRemoteDescription1(answerData) {
	 const peerConnectionStore = usePeerConnectionStore();
  // 设置远程描述
  peerConnectionStore.peerConnection.setRemoteDescription(new RTCSessionDescription({
    type: answerData.type,
    sdp: answerData.sdp
  })).then(() => {
	  // 设置远程描述成功后
      //将pinia管理的远程描述的状态设置为true
	  peerConnectionStore.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 });
	    // 获取用户 ID
	    const senderId = userstore.userData.id;
	    const receiverId = offerData.sender_id; // 假设已经在 sendData 中设置了 receiver_id
	console.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();
 
   // 添加本地媒体流的轨道到 RTCPeerConnection
   localStream.getTracks().forEach(track => {
     peerConnectionStore.peerConnection.addTrack(track, localStream);
   });
   console.log("本地媒体流添加本地轨道")

 
   try {
	
     // 创建 answer
     const answer = await peerConnectionStore.peerConnection.createAnswer();
     await peerConnectionStore.peerConnection.setLocalDescription(answer);
	
 
     // 发送 answer 回 A
     const answerData = {
       type: 6,
       sdp: answer.sdp,
       sender_id: offerData.receiver_id, // B 的 ID
       receiver_id: offerData.sender_id, // A 的 ID
       time: 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);
	    });
	}

五、注意避坑

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

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

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

相关文章

不可思议的转折,这部韩剧在口碑上实现逆袭

今天,推荐一下韩国版的《纸钞屋》,第一季豆瓣从9.4分滑到6.9分。第二季的回归却让这部剧迎来了“翻身仗”,目前豆瓣已飙升至8.4。 对比第一季,第二季不仅在剧情反转和人物刻画上有了明显的提升,还引入了《黑暗荣耀》中…

浅谈Agent智能体

Agent智能体无疑是24年最为火爆的话题之一,那么什么是Agent智能体?有什么作用?为什么需要Agent智能体? 用下边一张图简单说明一下 每日进步一点点

Python制作进度条,18种方式全网最全!(不全去你家扫厕所!)

想象一下,你的程序在执行复杂任务时,不再是冷冰冰的等待光标,而是伴随着色彩斑斓、动态变化的进度条,不仅让等待变得有趣,更让用户对你的作品刮目相看。从基础的文本进度条到高级的图形界面进度条,从简单的…

小程序兼容问题

【微信小程序】安卓兼容问题,scroll-view上拉导致input输入框上移 引用:https://blog.csdn.net/krico233/article/details/127491690 当一个scroll-view占据全屏高度(100vh)并包含input表单时,输入框聚焦会导致光标上移但输入框本身位置不变…

【C语言】数组(上)

【C语言】数组 1、数组的概念2、一维数组的创建和初始化2.1数组创建2.2数组的初始化2.3数组的类型 3、一维数组的使用3.1数组下标3.2 数组元素打印3.3数组的输入 4、一维数组在内存中的存储5、sizeof计算数组元素个数 1、数组的概念 数组是一组相同类型元素的组合,…

【RabbitMQ】面试题

在本篇文章中,主要是介绍RabbitMQ一些常见的面试题。对于前几篇文章的代码,都已经在码云中给出,链接是mq-test: 学习RabbitMQ的一些简单案例 (gitee.com),如果存在问题的话欢迎各位提出,望共同进步。 MQ的作用以及应用…

快速上手Make Sense:在线标注数据集的强大工具

链接: Makesense汉化版本 Makesense英文版 随着深度学习在计算机视觉领域的广泛应用,数据集标注成为了一项重要的任务。Make Sense正是一个为图像数据集提供标注功能的在线工具。其易用性和强大的功能使得它在众多标注工具中脱颖而出。本文将为你详细介绍…

找不到msvcr100.dll怎么解决?总结6个有效的解决方法

在使用计算机的过程中,我们经常会遇到一些错误提示,其中之一就是“msvcr100.dll丢失”。这个问题可能会让我们感到困惑和无助,但是不用担心,本文将为大家介绍六种实用的解决方法,帮助你轻松解决这个问题。 一&#xff…

raylib实现生产者消费者模型增加缓冲提高帧率

原来增加了四叉树导致帧率下降 后来学了生产者消费者模型&#xff0c;尝试追加缓冲池&#xff0c;剥离主函数查找需要更新的数据 帧率上升稳定到60帧 多了10 帧 中间工程主要是探索数据结构体怎么安排 // 参考自 https://zhuanlan.zhihu.com/p/693482704 #include <stdio.…

C语言-进程

一,进程的基本认识 1,进程的简介 进程描述是一个程序执行过程。当程序执行后&#xff0c;执行过程开始&#xff0c;则进程产生&#xff1b;执行过程结束&#xff0c;则进程也就结束了.进程和我们普通电费程序最大的区别就是,进程是动态的,他是一个过程,而程序是静态的. 2,进程…

永辉超市自救三部曲:靠名创优品复制胖东来?如何避免另一个苏宁易购?

《港湾商业观察》施子夫 王璐 从潮流产品新锐向大型商超迈入&#xff0c;没有人想到名创优品(09896.HK&#xff1b;MNSO.US)会成为永辉超市&#xff08;601933.SH&#xff09;的第一大股东。 近63亿元的收购价让两家本就知名度颇高的企业在2024年的商业江湖中更加瞩目。然而…

​极狐阿尔法 S5安全至上,北汽极狐打造移动防护堡垒

在新能源汽车的广阔舞台上&#xff0c;北汽极狐以其卓越的品质和创新的技术&#xff0c;不断书写着辉煌篇章。其中&#xff0c;极狐阿尔法 S5更是以其强大的性能、豪华的配置和亲民的价格&#xff0c;成为了众多消费者关注的焦点。 北汽极狐的品质追求 北汽极狐一直以来都将品…

【蓝牙小知识集锦!】禁止电脑连接蓝牙如何操作?一分钟教你5种小妙招!

禁止电脑连接蓝牙如何操作&#xff1f; 在回答如何禁止电脑连接蓝牙这个问题之前&#xff0c;我们要先了解&#xff0c;企业为啥要禁止蓝牙&#xff1f;原因是什么&#xff1f; 一、禁止蓝牙连接的原因 它可以涉及多个方面&#xff0c;主要包括安全性、效率、能源管理以及避免…

matlab r2024a、matlab R2024b保姆级安装教程

​ 1.安装步骤 右键【setup.exe】以【管理员身份运行】 点击【高级选项】-【我有文件安装密钥】 点击【是】-【下一步】 输入密钥【21471-07182-41807-00726-32378-34241-61866-60308-44209-03650-51035-48216-24734-36781-57695-35731-64525-44540-57877-31100-06573-50736-…

【论文速看】DL最新进展20240927-目标检测、Transformer

目录 【目标检测】【Transformer】 【目标检测】 [2024小目标检测] A DeNoising FPN With Transformer R-CNN for Tiny Object Detection 论文链接&#xff1a;https://arxiv.org/abs/2406.05755 代码链接&#xff1a;https://github.com/hoiliu-0801/DNTR 尽管计算机视觉领域…

信息学奥赛复赛复习05-CSP-J2020-01优秀的拆分-对数函数、自然对数、以2为底的对数、幂函数、打表

PDF文档回复:20240927 1 2020 CSP-J 题目1 优秀的拆分 [题目描述] 一般来说&#xff0c;一个正整数可以拆分成若干个正整数的和 例如&#xff0c;11&#xff0c;101234 等。对于正整数 n的一种特定拆分&#xff0c;我们称它为“优秀的”&#xff0c;当且仅当在这种拆分下&am…

【Redis】安装redis-plus-plus

目录 安装redis-plus-plus 安装hiredis 安装redis-plus-plus本体 具体步骤 ​编辑编写一个hello程序 安装redis-plus-plus C操作redis的第三方库有很多&#xff0c;咱们此处使用redis-plus-plus&#xff0c;安装链接如下&#xff1a; GitHub - sewenew/redis-plus-plus: …

gitee windows/linux配置使用

1、安装git工具 地址&#xff1a;git工具安装地址 1.2在gitee上创建仓库 在浏览器中打开Gitee网站&#xff0c;并登录到您的账户。点击页面右上方的加号图标&#xff0c;然后选择“新建仓库”。输入仓库的名称、描述和其他相关信息&#xff0c;然后点击“创建仓库”按钮。添…

大势Inside | “郧县人”重大考古成果写入人教版初中历史教科书

近日&#xff0c;发掘于湖北十堰郧阳的“郧县人”考古成果被写入2024年秋人教版历史教科书&#xff08;七年级上册&#xff09;第一课“远古时期的人类活动”&#xff0c;与闻名中外的“元谋人”、“蓝田人”、“北京人”、“山顶洞人”并列。 人教版七年级上册中国历史教科书 …

基于SSM的图书管理管理系统的设计与实现 (含源码+sql+视频导入教程)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSM的图书管理管理系统4拥有两种角色&#xff0c;用户可以浏览评论图书、登录注册&#xff0c;管理员可以进行图书馆管理、用户管理、分类管理等功能 1.1 背景描述 图书书店销售管理…