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

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

一、什么是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.kler.cn/a/317541.html

相关文章:

  • vscode远程连接服务器并启用tmux挂载进程
  • 机器学习总结
  • 深度学习——优化算法、激活函数、归一化、正则化
  • LeetCode【0035】搜索插入位置
  • 文件输入输出——NOI
  • 实现 MVC 模式
  • prober found high clock drift,Linux服务器时间不能自动同步,导致服务器时间漂移解决办法。
  • Maven的详细解读和配置
  • Linux 常用命令(待更新)
  • 安卓学习资源推荐
  • Java-数据结构-优先级队列(堆)-(二) (゚▽゚*)
  • Fyne ( go跨平台GUI )中文文档-绘图和动画(三)
  • 鸿蒙OpenHarmony【轻量系统内核通信机制(消息队列)】子系统开发
  • UDP Socket聊天室(Java)
  • Leetcode—329. 矩阵中的最长递增路径【困难】
  • dbt snapshot命令及应用示例
  • 基于BeagleBone Black的网页LED控制功能(Flask+gpiod)
  • 【CSS】字体文本
  • SQL_UNION
  • 【Linux】系统字符集无法修改,单独修改vim后的文件字符集
  • 爬虫技术初步自学
  • FreeRTOS学习——Systick中断、SVC中断、PendSV中断
  • 反转字符串 II--力扣541
  • k8s介绍-搭建k8s
  • Redis数据结构之哈希表
  • 【QT】QSS基础