一、什么是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);});}
五、注意避坑
一定要按照流程图上的流程去做,例如:在添加候选者信息之前必须完成设置远程描述,监听对方的视频流一定要在顶部监听,如果写在方法中间可能就不会去调用监听方法