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

ops/2024/10/19 7:30:12/

一、什么是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);});}

五、注意避坑

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


http://www.ppmy.cn/ops/118154.html

相关文章

每天学习一个技术栈 ——【Django Channels】篇(2)

前篇:每天学习一个技术栈 ——【Django Channels】篇(1)-CSDN博客 四、结合Celery实现异步任务 在本节中,我们将介绍如何使用Celery处理实时聊天应用中的异步任务。Celery能够帮助我们将耗时的任务(如保存聊天记录&a…

科技赋能环保:静电与光解技术在油烟净化中的卓越应用

我最近分析了餐饮市场的油烟净化器等产品报告,解决了餐饮业厨房油腻的难题,更加方便了在餐饮业和商业场所有需求的小伙伴们。 随着环保政策的不断升级,餐饮行业的油烟治理成为重要课题。油烟净化器的技术革新不仅提升了净化效率,…

Chainlit集成LlamaIndex实现知识库高级检索(从小到大递归检索器)

检索原理 从小到大的检索是指我们在切割文档时可以同时设置多个不同的chunk_size的颗粒度,比如我们可以同时设置chunk_size为128,256,512即按这三个不同的颗粒度对同时对所有文档都切割一遍。利用LlamaIndex中的RecursiveRetriever递归检索器…

PHP爬虫:获取商品销量详情API的利器

在电子商务时代,商品的销量数据对于商家来说至关重要。它不仅能够帮助商家了解市场动态,还能够指导库存管理和营销策略。PHP作为一种流行的服务器端脚本语言,结合其强大的HTTP请求处理能力,可以有效地用于编写爬虫程序&#xff0c…

python/requests库的使用/爬虫基础工具/

requests 是一个 Python 库,它允许你发送 HTTP 请求。这个库需要单独安装,因为它不是 Python 标准库的一部分 1.让我们安装requests 在控制台运行 pip install requests 使用 requests 发送请求 1.GET 请求: import requestsresponse …

S1_02_第一章_计算机网络概述

1、什么是计算机网络 那么,到底什么是计算机网络呢?用通信设备和线路将处于不同地理位置、操作相对独立的多 台计算机连接起来,并配置相应的系统和应用软件,在原本各自独立的计算机之间实现软硬件资源 共享和信息传递等形成的系统就是计算机网络。 1.1、计算机网络的功能 1&…

Ubuntu 离线安装 docker

1、下载离线包,网址:https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/ 离线安装docker需要下载3个包,containerd.io ,docker-ce-cli,docker-ce 2、下载完毕后拷贝到ubuntu上用 dpkg 命令安装&am…

Python爬虫爬取王者荣耀英雄信息并保存到图数据库

爬取信息说明 英雄名称英雄类型英雄包含的所有皮肤名称 创建英雄类型节点 王者荣耀官方给出的英雄类型是以下几种: 直接准备好英雄类型词典 hero_type_dict [战士, 法师, 坦克, 刺客, 射手, 辅助 ]添加到图数据库中 def create_hero_type_node():for hero_ty…