chore: refact webRTC relative logic and update npm module
This commit is contained in:
@@ -1,37 +1,42 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/websocket/onlineSocket'
|
||||
import { messageStore } from './message'
|
||||
|
||||
const message = messageStore()
|
||||
|
||||
export const onlineSocketStore = defineStore('onlineSocket', {
|
||||
state: () => ({
|
||||
isConnected: false,
|
||||
hasGotMessage: false,
|
||||
id: ''
|
||||
}),
|
||||
|
||||
actions: {
|
||||
connect(id) {
|
||||
this.id = id;
|
||||
if (this.isConnected === true) return
|
||||
connectWebSocket();
|
||||
this.isConnected = true;
|
||||
if (!this.hasGotMessage) {
|
||||
message.loadMessagesHistory(this.id)
|
||||
this.hasGotMessage = true
|
||||
}
|
||||
},
|
||||
disconnect() {
|
||||
setIsManualClose(true);
|
||||
disconnectWebSocket();
|
||||
this.isConnected = false;
|
||||
if (this.hasGotMessage) {
|
||||
message.saveMessagesHistory(this.id)
|
||||
}
|
||||
},
|
||||
send(message) {
|
||||
sendMessage(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
import { defineStore } from 'pinia'
|
||||
import { connectWebSocket, disconnectWebSocket, sendMessage, setIsManualClose } from '@/websocket/onlineSocket'
|
||||
import { messageStore } from './message'
|
||||
|
||||
const message = messageStore()
|
||||
|
||||
export const onlineSocketStore = defineStore('onlineSocket', {
|
||||
state: () => ({
|
||||
isConnected: false,
|
||||
hasGotMessage: false,
|
||||
id: ''
|
||||
}),
|
||||
|
||||
actions: {
|
||||
connect(id: number) {
|
||||
this.id = id;
|
||||
if (this.isConnected === true) return
|
||||
connectWebSocket();
|
||||
this.isConnected = true;
|
||||
if (!this.hasGotMessage) {
|
||||
message.loadMessagesHistory(this.id)
|
||||
this.hasGotMessage = true
|
||||
}
|
||||
},
|
||||
disconnect() {
|
||||
setIsManualClose(true);
|
||||
disconnectWebSocket();
|
||||
this.isConnected = false;
|
||||
if (this.hasGotMessage) {
|
||||
message.saveMessagesHistory(this.id)
|
||||
}
|
||||
},
|
||||
send(message: any) {
|
||||
try {
|
||||
sendMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Failed to stringify message:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
import { connectVoicesocket, disconnectVoicesocket, sendMessage, hangup } from "@/websocket/voiceSocket";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const voiceStore = defineStore("voice", {
|
||||
state: () => ({
|
||||
isConnected: false
|
||||
}),
|
||||
actions: {
|
||||
connect() {
|
||||
connectVoicesocket();
|
||||
this.isConnected = true;
|
||||
},
|
||||
disconnect() {
|
||||
disconnectVoicesocket();
|
||||
this.isConnected = false;
|
||||
},
|
||||
startCall(from, from_name, from_avatar, to) {
|
||||
if (this.isConnected) {
|
||||
const message = {
|
||||
type: "incomingcall",
|
||||
from: from,
|
||||
from_name: from_name,
|
||||
from_avatar: from_avatar,
|
||||
to: to
|
||||
}
|
||||
sendMessage(JSON.stringify(message))
|
||||
} else {
|
||||
console.log("voice socket is not connected")
|
||||
}
|
||||
},
|
||||
pickup(from,to){
|
||||
if (this.isConnected) {
|
||||
const message ={
|
||||
type: "pickup",
|
||||
from: from,
|
||||
to: to
|
||||
}
|
||||
sendMessage(JSON.stringify(message))
|
||||
} else {
|
||||
console.log("voice socket is not connected")
|
||||
}
|
||||
},
|
||||
hangup(){
|
||||
hangup()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
235
src/store/Voice.ts
Normal file
235
src/store/Voice.ts
Normal 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;
|
||||
}
|
||||
@@ -1,93 +1,115 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const onCallStore = defineStore("onCall", {
|
||||
state: () => ({
|
||||
target: {
|
||||
u_id: "",
|
||||
name: "",
|
||||
avatar: ""
|
||||
},
|
||||
panel: false,
|
||||
calling: false,
|
||||
from: false,
|
||||
status: false,
|
||||
localstream: {
|
||||
audioStream: null,
|
||||
audioElement: null
|
||||
},
|
||||
remotestream: {
|
||||
audioStream: null,
|
||||
audioElement: null
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
setTarget(u_id, u_name, u_avatar) {
|
||||
this.target = {
|
||||
u_id: u_id,
|
||||
name: u_name,
|
||||
avatar: u_avatar
|
||||
}
|
||||
},
|
||||
panelOn() {
|
||||
this.panel = true
|
||||
},
|
||||
panelOff() {
|
||||
this.panel = false
|
||||
},
|
||||
callingOn() {
|
||||
this.calling = true
|
||||
},
|
||||
callingOff() {
|
||||
this.calling = false
|
||||
},
|
||||
fromOn() {
|
||||
this.from = true
|
||||
},
|
||||
fromOff() {
|
||||
this.from = false
|
||||
},
|
||||
statusOn() {
|
||||
this.status = true
|
||||
},
|
||||
statusOff() {
|
||||
this.status = false
|
||||
},
|
||||
setLocalStream(stream) {
|
||||
this.localstream.audioStream = stream;
|
||||
if (this.localstream.audioStream) {
|
||||
this.localstream.audioElement.srcObject = stream;
|
||||
}
|
||||
},
|
||||
setLocalElement(element) {
|
||||
this.localstream.audioElement = element;
|
||||
if (this.localstream.audioStream) {
|
||||
this.localstream.audioElement.srcObject = this.localstream.audioStream;
|
||||
}
|
||||
},
|
||||
setRemoteStream(stream) {
|
||||
this.remotestream.audioStream = stream;
|
||||
if (this.remotestream.audioStream) {
|
||||
this.remotestream.audioElement.srcObject = stream;
|
||||
}
|
||||
},
|
||||
setRemoteElement(element) {
|
||||
this.remotestream.audioElement = element;
|
||||
if (this.remotestream.audioStream) {
|
||||
this.remotestream.audioElement.srcObject = this.remotestream.audioStream;
|
||||
}
|
||||
},
|
||||
resetStream() {
|
||||
// 停止本地媒体流
|
||||
if (this.localstream.audioStream) {
|
||||
this.localstream.audioStream.getTracks().forEach(track => track.stop());
|
||||
this.localstream.audioStream = null;
|
||||
}
|
||||
|
||||
// 停止远程媒体流
|
||||
if (this.remotestream.videoStream) {
|
||||
this.remotestream.videoStream.getTracks().forEach(track => track.stop());
|
||||
this.remotestream.videoStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const onCallStore = defineStore("onCall", {
|
||||
state: () => ({
|
||||
target: {
|
||||
u_id: "",
|
||||
name: "",
|
||||
avatar: ""
|
||||
},
|
||||
panel: false,
|
||||
calling: false,
|
||||
from: false,
|
||||
status: false,
|
||||
localstream: {
|
||||
audioStream: null,
|
||||
audioElement: null
|
||||
},
|
||||
remotestream: {
|
||||
audioStream: null,
|
||||
audioElement: null
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
getTarget() {
|
||||
return this.target;
|
||||
},
|
||||
setTarget(u_id:number, u_name:string, u_avatar:string) {
|
||||
this.target = {
|
||||
u_id: u_id,
|
||||
name: u_name,
|
||||
avatar: u_avatar
|
||||
}
|
||||
},
|
||||
panelOn() {
|
||||
this.panel = true
|
||||
},
|
||||
panelOff() {
|
||||
this.panel = false
|
||||
},
|
||||
callingOn() {
|
||||
this.calling = true
|
||||
},
|
||||
callingOff() {
|
||||
this.calling = false
|
||||
},
|
||||
fromOn() {
|
||||
this.from = true
|
||||
},
|
||||
fromOff() {
|
||||
this.from = false
|
||||
},
|
||||
statusOn() {
|
||||
this.status = true
|
||||
},
|
||||
statusOff() {
|
||||
this.status = false
|
||||
},
|
||||
clear(){
|
||||
this.target = {
|
||||
u_id: "",
|
||||
name: "",
|
||||
avatar: ""
|
||||
}
|
||||
this.panel = false;
|
||||
this.calling = false;
|
||||
this.from = false;
|
||||
this.status = false;
|
||||
this.localstream = {
|
||||
audioStream: null,
|
||||
audioElement: null
|
||||
}
|
||||
this.remotestream = {
|
||||
audioStream: null,
|
||||
audioElement: null
|
||||
}
|
||||
},
|
||||
setLocalStream(stream) {
|
||||
this.localstream.audioStream = stream;
|
||||
if (this.localstream.audioStream) {
|
||||
this.localstream.audioElement.srcObject = stream;
|
||||
}
|
||||
},
|
||||
setLocalElement(element) {
|
||||
this.localstream.audioElement = element;
|
||||
if (this.localstream.audioStream) {
|
||||
this.localstream.audioElement.srcObject = this.localstream.audioStream;
|
||||
}
|
||||
},
|
||||
setRemoteStream(stream) {
|
||||
this.remotestream.audioStream = stream;
|
||||
if (this.remotestream.audioStream) {
|
||||
this.remotestream.audioElement.srcObject = stream;
|
||||
}
|
||||
},
|
||||
setRemoteElement(element) {
|
||||
this.remotestream.audioElement = element;
|
||||
if (this.remotestream.audioStream) {
|
||||
this.remotestream.audioElement.srcObject = this.remotestream.audioStream;
|
||||
}
|
||||
},
|
||||
resetStream() {
|
||||
// 停止本地媒体流
|
||||
if (this.localstream.audioStream) {
|
||||
this.localstream.audioStream.getTracks().forEach(track => track.stop());
|
||||
this.localstream.audioStream = null;
|
||||
}
|
||||
|
||||
// 停止远程媒体流
|
||||
if (this.remotestream.videoStream) {
|
||||
this.remotestream.videoStream.getTracks().forEach(track => track.stop());
|
||||
this.remotestream.videoStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user