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