chore: refact webRTC relative logic and update npm module

This commit is contained in:
2026-03-17 18:10:30 +08:00
parent 6f205d2408
commit 08ae7414d0
16 changed files with 6503 additions and 6241 deletions

235
src/store/Voice.ts Normal file
View File

@@ -0,0 +1,235 @@
import { sendMessage } from "@/websocket/onlineSocket";
import { onCallStore } from "@/store/VoiceTarget";
import { ref } from "vue";
const iceserver = {
iceServers: [
{
urls: process.env.STUN_URL,
},
{
urls: process.env.TURN_URL,
username: process.env.TURN_USERNAME,
credential: process.env.TURN_CREDENTIAL,
},
],
};
interface message {
cmd: "VOICE_ICE_CANDIDATE" | "VOICE_SDP_OFFER" | "VOICE_SDP_ANSWER",
from: number,
to: number,
content: any,
}
const oncall = onCallStore();
const RTCpeerConnection = ref(null);
const localstream = ref(null);
const remotestream = ref<MediaStream | null>(null);
// 本地候选:为了避免“绑定 onicecandidate 太晚”导致丢失,统一先缓存,等 remoteDescription 就绪后再发送
const pendingLocalCandidates = ref<any[]>([]);
// 远端候选:避免远端 candidate 早到但 remoteDescription 未 set 导致 addIceCandidate 失败
const pendingRemoteCandidates = ref<any[]>([]);
// 当前会话对端信息(用于发送 ICE 时带上 from/to
const currentFrom = ref<number | null>(null);
const currentTo = ref<number | null>(null);
const canSendIceNow = () => {
const pc: any = RTCpeerConnection.value;
return (
!!pc &&
!!pc.localDescription &&
!!pc.remoteDescription &&
currentFrom.value !== null &&
currentTo.value !== null
);
};
const flushLocalCandidatesIfReady = () => {
if (!canSendIceNow()) return;
if (pendingLocalCandidates.value.length === 0) return;
pendingLocalCandidates.value.forEach((candidate) => {
const message: message = {
cmd: "VOICE_ICE_CANDIDATE",
from: currentFrom.value as number,
to: currentTo.value as number,
content: candidate,
};
sendMessage(message);
});
pendingLocalCandidates.value = [];
};
const flushRemoteCandidatesIfReady = async () => {
const pc: any = RTCpeerConnection.value;
if (!pc || !pc.remoteDescription) return;
if (pendingRemoteCandidates.value.length === 0) return;
for (const c of pendingRemoteCandidates.value) {
await pc.addIceCandidate(new RTCIceCandidate(c));
}
pendingRemoteCandidates.value = [];
};
const getlocalStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log("获取本地音频流成功");
return stream;
// // 获取音频和视频轨道
// const audioTrack = stream.getAudioTracks()[0];
// // 将轨道添加到 RTCPeerConnection
// peerConnection.addTrack(audioTrack, stream);
};
const closeLocalStream = (stream: MediaStream) => {
stream.getTracks().forEach(track => track.stop());
}
export const initRTCconnection = async () => {
RTCpeerConnection.value = new RTCPeerConnection(iceserver);
localstream.value = await getlocalStream();
RTCpeerConnection.value.addTrack(localstream.value.getAudioTracks()[0], localstream.value);
// 必须尽早绑定,否则可能在 setLocalDescription 后就把 candidate 产完了
RTCpeerConnection.value.onicecandidate = (event: { candidate: any }) => {
if (!event.candidate) return;
pendingLocalCandidates.value.push(event.candidate);
flushLocalCandidatesIfReady();
};
// 远端音频:收到 track 后把 MediaStream 绑定到 UI 的 <audio>
RTCpeerConnection.value.ontrack = (event: RTCTrackEvent) => {
// 优先用浏览器提供的 streams[0];否则手动聚合 track
let stream = event.streams && event.streams[0] ? event.streams[0] : null;
if (!stream) {
if (!remotestream.value) remotestream.value = new MediaStream();
if (event.track) remotestream.value.addTrack(event.track);
stream = remotestream.value;
}
if (stream) {
oncall.setRemoteStream(stream);
}
};
};
export const sendOffer = async (from: number,to: number) => {
if (!RTCpeerConnection.value) {
await initRTCconnection();
};
currentFrom.value = from;
currentTo.value = to;
const offer = await RTCpeerConnection.value.createOffer();
await RTCpeerConnection.value.setLocalDescription(offer);
const message: message = {
cmd: "VOICE_SDP_OFFER",
from: from,
to: to,
content: offer
}
sendMessage(message);
}
export const handleOffer = async (offer: any, from: number, to: number) => {
if (!RTCpeerConnection.value) {
await initRTCconnection();
}
// 这里是“我收到对方 offer”因此我这端的 from/to 应该是 (to -> from)
currentFrom.value = to;
currentTo.value = from;
await RTCpeerConnection.value.setRemoteDescription(new RTCSessionDescription(offer));
await flushRemoteCandidatesIfReady();
const answer = await RTCpeerConnection.value.createAnswer();
await RTCpeerConnection.value.setLocalDescription(answer);
const message: message = {
cmd: "VOICE_SDP_ANSWER",
from: to,
to: from,
content: answer
}
sendMessage(message);
// remoteDescription + localDescription 都已具备,允许发送本地候选
flushLocalCandidatesIfReady();
}
export const handleAnswer = async (answer: any, from: number, to: number) => {
if (!RTCpeerConnection.value)
{
console.error("connection lost");
return;
}
// 这里是“我收到对方 answer”因此我这端的 from/to 应该是 (to -> from)
currentFrom.value = to;
currentTo.value = from;
await RTCpeerConnection.value.setRemoteDescription(new RTCSessionDescription(answer));
await flushRemoteCandidatesIfReady();
flushLocalCandidatesIfReady();
}
export const handleCandidate = async (candidate: any) => {
if (!RTCpeerConnection.value) return;
// candidate 可能早到remoteDescription 未 set 时直接 add 会失败,先缓存
if (!RTCpeerConnection.value.remoteDescription) {
pendingRemoteCandidates.value.push(candidate);
return;
}
await RTCpeerConnection.value.addIceCandidate(new RTCIceCandidate(candidate));
}
export const hangupCall = (from: number, to: number) => {
if(from === null || to === null) return;
if (RTCpeerConnection.value) {
RTCpeerConnection.value.close();
RTCpeerConnection.value = null;
const msg = {
cmd: "VOICE_CALL_END",
from: from,
to: to,
time: new Date().toLocaleString()
}
sendMessage(msg);
oncall.clear();
closeLocalStream(localstream.value);
remotestream.value = null;
pendingLocalCandidates.value = [];
pendingRemoteCandidates.value = [];
currentFrom.value = null;
currentTo.value = null;
}
}
export const denyCall = (from: number, to: number) => {
if(from === null || to === null) return;
const msg = {
cmd: "VOICE_CALL_DENY",
from: from,
to: to,
time: new Date().toLocaleString()
}
sendMessage(msg);
oncall.clear();
remotestream.value = null;
pendingLocalCandidates.value = [];
pendingRemoteCandidates.value = [];
currentFrom.value = null;
currentTo.value = null;
}
export const closeConnection = () => {
if (RTCpeerConnection.value) {
RTCpeerConnection.value.close();
RTCpeerConnection.value = null;
}
closeLocalStream(localstream.value);
remotestream.value = null;
pendingLocalCandidates.value = [];
pendingRemoteCandidates.value = [];
currentFrom.value = null;
currentTo.value = null;
}