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

server/2024/10/19 13:23:14/

一、什么是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/server/124394.html

相关文章

nvm以及npm源配置

配置 NVM 和 NPM 使用镜像源 接上一篇。国内使用会遇到网络连接问题。为了解决这个问题,我们可以配置 NVM 和 NPM 使用腾讯的源。 配置 NVM 源 首先,我们需要配置 NVM 源。可以使用以下命令: export NVM_NODEJS_ORG_MIRRORhttps://mirrors.…

饿了么 表单 回填后 无法更新 问题

原因就在于 1.后端返回的数据和原始的页面中的数据 对不上 例如 后端返回的是 1 和 0 但是页面中是 true 和false babel值是对的 但是value的值是错误 的 就是导致 无法选择 是否在页面初始化的时候 把 data中的 表单的 数据 置为空了 例如 data 中的 数据是 list:{ a:‘asdfs…

WPF入门教学二十四 WPF性能优化

WPF(Windows Presentation Foundation)是一个功能丰富的UI框架,但这也意味着如果不注意性能优化,应用程序可能会变得缓慢或响应迟缓。以下是一些WPF性能优化的技巧和建议: 1. 减少布局计算 布局计算是WPF中非常消耗资…

Java安全最佳实践:防御常见网络攻击

Java安全最佳实践:防御常见网络攻击 大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿! 在当今的网络环境中,应用程序面临着各种安全威胁。Java作为一种广泛使用的编程语言&…

第十七节 鼠标的操作与相应

知识点 -event代表鼠标事件类型 -EVENT_LBUTTONDOWN鼠标左键按下 -EVENT_LBUTTONUP鼠标左键抬起 -EVENT_LBUTTONMOVE鼠标及移动 Point sp(-1, -1); Point ep(-1, -1); Mat temp; static void on_draw(int event, int x, int y, int flags, void* userdata) { Mat imag…

追梦无Bug的软件世界

追梦无Bug的软件世界:测试人员的视角与探索 我有一个梦想,今天我们共同承载着一个愿景:创造一个没有Bug的软件世界。 我梦想有一天,用户将享受到完全无Bug的软件体验,用户不再因为软件中的Bug而感到困扰和沮丧。 我梦…

哈希知识点总结:哈希、哈希表、位图、布隆过滤器

目录 哈希 哈希表 哈希常用方法 1、直接定址法 2、存留余数法 哈希冲突 哈希冲突的解决办法 1、闭散列:开放定址法 (1)线性探测法 (2)二次探测法 2、开散列 哈希桶 / 拉链法 哈希的运用 位图 set操作 …

微信小程序操作蓝牙

主要流程: 1.初始化蓝牙适配器openBluetoothAdapter,如果不成功就onBluetoothAdapterStateChange监听蓝牙适配器状态变化事件 2.startBluetoothDevicesDiscovery开始搜寻附近的蓝牙外围设备 3.onBluetoothDeviceFound监听寻找到新设备的事件,…