diff --git a/src/components/VideoCallComponent.vue b/src/components/VideoCallComponent.vue index 05d651b..4e33a83 100644 --- a/src/components/VideoCallComponent.vue +++ b/src/components/VideoCallComponent.vue @@ -124,11 +124,19 @@ 对方忙线中 - - - 接听 + + + {{ isAccepting ? '接听中...' : '接听' }} - + 拒绝 @@ -209,13 +217,13 @@ const isMinimized = ref(false); const isMicOn = ref(true); const isCameraOn = ref(props.callType === 'video'); const isSpeakerOn = ref(true); -const hasLocalVideo = ref(false); const hasRemoteVideo = ref(false); const callDuration = ref(0); const callTimer = ref(null); const isCameraWorking = ref(true); const isMicWorking = ref(true); const isDragging = ref(false); +const isAccepting = ref(false); // 新增:防止重复接听 // 拖拽相关状态 const position = reactive({ x: 0, y: 0 }); @@ -281,14 +289,26 @@ const containerStyle = computed(() => { }; }); -// 初始化媒体设备 +// 初始化媒体设备 - 修复:只在需要时获取媒体 const initializeMedia = async () => { try { + // 只有在接听来电或通话已接受时才获取媒体 + if (props.isIncoming && callStatus.value === "ringing") { + console.log("来电状态,暂不获取媒体流"); + return; + } + + if (callStatus.value === "calling") { + console.log("呼出状态,暂不获取媒体流"); + return; + } + const constraints = { audio: true, video: props.callType === 'video' }; + console.log("初始化媒体设备,约束:", constraints); // 获取本地媒体流 await webrtc.getLocalMedia(constraints); @@ -361,10 +381,18 @@ const endCall = () => { // 接听通话 const acceptCall = () => { - // 确保只接听一次 - if (chatStore.callStatus === "ongoing") return; + // 防止重复点击 + if (isAccepting.value || chatStore.callStatus === "connecting" || chatStore.callStatus === "ongoing") { + return; + } - chatStore.acceptCall(); + isAccepting.value = true; + chatStore.acceptCall().finally(() => { + // 延迟重置状态,避免快速重复点击 + setTimeout(() => { + isAccepting.value = false; + }, 2000); + }); emit('accept-call'); }; @@ -436,10 +464,8 @@ const handleWindowResize = () => { watch(() => webrtc.localStream.value, (newStream) => { if (newStream && localVideo.value) { localVideo.value.srcObject = newStream; - hasLocalVideo.value = true; } else if (localVideo.value) { localVideo.value.srcObject = null; - hasLocalVideo.value = false; } }); @@ -464,15 +490,24 @@ watch(() => webrtc.remoteStream.value, (newStream) => { } }); -// 监听通话状态 +// 监听通话状态 - 修复:避免过早关闭连接 watch(() => chatStore.callStatus, (status) => { + console.log("VideoCallComponent 监听到通话状态变化:", status); + if (status === "ongoing") { // 开始计时 if (!callTimer.value) { startCallTimer(); } } else if (status === "ended" || status === "idle") { - endCall(); + // 只有在真正结束时才关闭 + console.log("通话结束,准备关闭组件"); + setTimeout(() => { + endCall(); + }, 100); // 延迟一点确保状态同步 + } else if (status === "connecting") { + // 连接中时初始化媒体 + initializeMedia(); } }); @@ -483,13 +518,18 @@ onMounted(() => { window.addEventListener('resize', handleWindowResize); - // 如果是呼出方或已接听,初始化媒体 - if (!props.isIncoming || callStatus.value === 'ongoing') { + // 修复:根据通话状态决定是否初始化媒体 + console.log("VideoCallComponent mounted, callStatus:", callStatus.value, "isIncoming:", props.isIncoming); + + // 只有在特定状态下才初始化媒体 + if (callStatus.value === 'ongoing' || callStatus.value === 'connecting') { initializeMedia(); } }); onUnmounted(() => { + console.log("VideoCallComponent unmounted"); + if (callTimer.value) clearInterval(callTimer.value); document.removeEventListener('mousemove', handleDrag); document.removeEventListener('mouseup', stopDrag); @@ -506,6 +546,7 @@ onUnmounted(() => { remoteVideoPreview.value.srcObject = null; } + // 关闭WebRTC连接 webrtc.closeConnection(); }); diff --git a/src/composables/useWebRTC.js b/src/composables/useWebRTC.js index 4893d8b..a1e6ab3 100644 --- a/src/composables/useWebRTC.js +++ b/src/composables/useWebRTC.js @@ -1,6 +1,7 @@ import { ref } from "vue" import { useChatStore } from "@/stores/chat" import { useUserStore } from "@/stores/user" +import { callAPI } from "@/utils/request.js" export function useWebRTC() { const localStream = ref(null) @@ -9,8 +10,8 @@ export function useWebRTC() { const isConnected = ref(false) const isConnecting = ref(false) - const chatStore = useChatStore() const userStore = useUserStore() + const chatStore = useChatStore() // Moved to top-level const configuration = { iceServers: [ @@ -24,22 +25,33 @@ export function useWebRTC() { const createPeerConnection = (callId, peerId) => { if (peerConnection.value) { + console.log("关闭现有的WebRTC连接") closeConnection() } + console.log("创建新的WebRTC连接") peerConnection.value = new RTCPeerConnection(configuration) - peerConnection.value.onicecandidate = (event) => { + peerConnection.value.onicecandidate = async (event) => { if (event.candidate) { console.log("发送ICE候选:", event.candidate) - // 发送ICE候选给对方 - chatStore.sendCallSignal({ - call_id: callId, - call_status: "candidate", - candidate: event.candidate, - receiver_user_id: peerId, - }) + // 只有在连接建立过程中才发送 ICE 候选 + if (chatStore.callStatus === "connecting" || chatStore.callStatus === "ongoing") { + try { + await callAPI.sendCandidate( + userStore.currentUser?.id, + peerId, + callId, + event.candidate, + 6, // 默认视频通话类型 + ) + } catch (error) { + console.error("发送ICE候选失败:", error) + } + } else { + console.log("通话状态不正确,跳过发送ICE候选:", chatStore.callStatus) + } } } @@ -57,11 +69,16 @@ export function useWebRTC() { if (state === "connected") { isConnected.value = true isConnecting.value = false - chatStore.callStatus = "ongoing" - chatStore.callConnectionStatus = "通话中" + // 只有在 connecting 状态时才切换到 ongoing + if (chatStore.callStatus === "connecting") { + chatStore.callStatus = "ongoing" + chatStore.callConnectionStatus = "通话中" + } } else if (state === "connecting") { isConnecting.value = true - chatStore.callConnectionStatus = "正在连接..." + if (chatStore.callStatus === "connecting") { + chatStore.callConnectionStatus = "正在连接..." + } } else if (state === "disconnected" || state === "failed") { isConnected.value = false isConnecting.value = false @@ -89,10 +106,12 @@ export function useWebRTC() { const getLocalMedia = async (constraints = { audio: true, video: true }) => { try { + // 如果已有本地流,先停止 if (localStream.value) { localStream.value.getTracks().forEach((track) => track.stop()) } + console.log("获取本地媒体,约束:", constraints) localStream.value = await navigator.mediaDevices.getUserMedia(constraints) console.log("获取本地媒体成功:", localStream.value) return localStream.value @@ -123,14 +142,6 @@ export function useWebRTC() { await peerConnection.value.setLocalDescription(offer) console.log("创建Offer成功:", offer) - // 发送offer给对方 - chatStore.sendCallSignal({ - call_id: callId, - call_status: "offer", - offer: offer, - receiver_user_id: peerId, - }) - return offer } catch (error) { console.error("创建Offer失败:", error) @@ -156,15 +167,6 @@ export function useWebRTC() { await peerConnection.value.setLocalDescription(answer) console.log("创建Answer成功:", answer) - - // 发送answer给对方 - chatStore.sendCallSignal({ - call_id: callId, - call_status: "answer", - answer: answer, - receiver_user_id: peerId, - }) - return answer } catch (error) { console.error("创建Answer失败:", error) diff --git a/src/stores/chat.js b/src/stores/chat.js index c7c8a32..3ae847c 100644 --- a/src/stores/chat.js +++ b/src/stores/chat.js @@ -3,6 +3,7 @@ import { ref, computed, watch } from "vue" import { chatDB } from "@/utils/db" import { useWebRTC } from "@/composables/useWebRTC" import { useUserStore } from "@/stores/user" +import { sendCallSignal, callAPI } from "@/utils/request.js" export const useChatStore = defineStore("chat", () => { const friends = ref([]) @@ -56,7 +57,7 @@ export const useChatStore = defineStore("chat", () => { callConnectionStatus.value = "" currentCall.value = null incomingCall.value = null - webrtc.closeConnection() + // 注意:这里不要立即关闭WebRTC连接,让组件自己处理 } // 清除通话超时 @@ -76,21 +77,46 @@ export const useChatStore = defineStore("chat", () => { }, duration) } - // 发送通话信令 - const sendCallSignal = (signal) => { - if (socket.value && socket.value.readyState === WebSocket.OPEN) { - const message = { - call_status: signal.call_status, - call_type: signal.call_type || (signal.receiver_user_id ? 6 : 7), // 默认视频通话 - call_id: signal.call_id, - caller_id: userStore.currentUser.id, - callee_id: signal.receiver_user_id, - data: JSON.stringify(signal.offer || signal.answer || signal.candidate || {}), + // 发送通话信令 - 使用新的专用方法 + const sendCallSignalInternal = async (signal) => { + try { + console.log("发送通话信令:", signal) + + // 确保必要参数存在 + if (!userStore.currentUser?.id) { + throw new Error("用户未登录") } - socket.value.send(JSON.stringify(message)) - console.log("发送通话信令:", message) - } else { - console.error("WebSocket未连接,无法发送通话信令") + + if (!signal.receiver_user_id) { + throw new Error("缺少接收方用户ID") + } + + if (!signal.call_id) { + throw new Error("缺少通话ID") + } + + if (!signal.call_status) { + throw new Error("缺少通话状态") + } + + // 构建请求数据 + const requestData = { + sender_user_id: userStore.currentUser.id, + receiver_user_id: signal.receiver_user_id, + message_type: signal.call_type || (signal.call_status === "invite" ? 6 : 7), // 6=视频, 7=语音 + message_content: JSON.stringify(signal.data || {}), + call_id: signal.call_id, + call_status: signal.call_status, + } + + console.log("发送通话信令请求数据:", requestData) + + const response = await sendCallSignal(requestData) + console.log("发送通话信令成功:", signal) + return response + } catch (error) { + console.error("发送通话信令失败:", error) + throw error } } @@ -330,15 +356,24 @@ export const useChatStore = defineStore("chat", () => { // 处理Offer信令 const handleOffer = async (data) => { if (incomingCall.value && incomingCall.value.callId === data.call_id) { + // 确保只处理一次 Offer + if (callStatus.value === "ongoing") { + console.log("通话已建立,忽略重复的 Offer") + return + } + try { - const offer = JSON.parse(data.data) + const offer = JSON.parse(data.content || data.message_content || "{}") console.log("收到Offer:", offer) // 设置远程Offer并创建Answer await webrtc.setRemoteOffer(offer) - const answer = await webrtc.createAnswer(offer, data.call_id, data.caller_id) + const answer = await webrtc.createAnswer(offer, data.call_id, data.sender_user_id) - console.log("发送Answer:", answer) + // 发送Answer - 使用便捷方法 + await callAPI.sendAnswer(userStore.currentUser.id, data.sender_user_id, data.call_id, answer, data.message_type) + + console.log("发送Answer成功") } catch (error) { console.error("处理Offer失败:", error) rejectCall("failed") @@ -349,8 +384,14 @@ export const useChatStore = defineStore("chat", () => { // 处理Answer信令 const handleAnswer = async (data) => { if (currentCall.value && currentCall.value.callId === data.call_id) { + // 确保只处理一次 Answer + if (callStatus.value === "ongoing") { + console.log("通话已建立,忽略重复的 Answer") + return + } + try { - const answer = JSON.parse(data.data) + const answer = JSON.parse(data.content || data.message_content || "{}") console.log("收到Answer:", answer) // 设置远程Answer @@ -417,24 +458,20 @@ export const useChatStore = defineStore("chat", () => { // 只有在当前没有通话时才处理来电 if (isInCall.value) { console.log("当前正在通话中,发送忙线状态") - sendCallSignal({ - call_status: "busy", - call_id: data.call_id, - receiver_user_id: data.caller_id, - }) + callAPI.sendBusy(userStore.currentUser.id, data.sender_user_id, data.call_id, data.message_type) // 添加未接来电消息 - const friend = friends.value.find((f) => f.id === data.caller_id) + const friend = friends.value.find((f) => f.id === data.sender_user_id) if (friend) { friend.lastMessage = "[未接来电]" chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[未接来电]", friend.unreadCount) // 添加通话记录 const callMessage = { - type: data.call_type === 6 ? "video-call" : "audio-call", + type: data.message_type === 6 ? "video-call" : "audio-call", content: "[未接来电]", time: new Date().toLocaleTimeString().slice(0, 5), - senderId: data.caller_id, + senderId: data.sender_user_id, read: true, timestamp: Date.now(), duration: 0, @@ -450,9 +487,9 @@ export const useChatStore = defineStore("chat", () => { incomingCall.value = { callId: data.call_id, - callerId: data.caller_id, - callType: data.call_type === 6 ? "video" : "audio", - content: data.data, + callerId: data.sender_user_id, + callType: data.message_type === 6 ? "video" : "audio", + content: data.content, } callStatus.value = "ringing" callError.value = null @@ -462,26 +499,37 @@ export const useChatStore = defineStore("chat", () => { } // 处理通话接受 - const handleCallAccepted = (data) => { + const handleCallAccepted = async (data) => { if (currentCall.value && currentCall.value.callId === data.call_id) { + // 防止重复处理 accepted 状态 + if (callStatus.value === "connecting" || callStatus.value === "ongoing") { + console.log("通话已在连接中,忽略重复的 accepted 信令") + return + } + callStatus.value = "connecting" callError.value = null callConnectionStatus.value = "对方已接听,正在连接..." console.log("通话已被接受,开始交换信令") - // 开始创建Offer - setTimeout(async () => { - try { - await webrtc.getLocalMedia({ - audio: true, - video: currentCall.value.callType === "video", - }) - await webrtc.createOffer(currentCall.value.callId, currentCall.value.peerId) - } catch (error) { - console.error("创建Offer失败:", error) - endCall("failed") - } - }, 100) + // 接受通话后开始创建Offer + try { + const offer = await webrtc.createOffer(currentCall.value.callId, currentCall.value.peerId) + + // 发送Offer给对方 - 使用便捷方法 + await callAPI.sendOffer( + userStore.currentUser.id, + currentCall.value.peerId, + currentCall.value.callId, + offer, + currentCall.value.callType === "video" ? 6 : 7, + ) + + console.log("发送Offer成功") + } catch (error) { + console.error("创建Offer失败:", error) + endCall("failed") + } } } @@ -573,7 +621,7 @@ export const useChatStore = defineStore("chat", () => { (incomingCall.value && incomingCall.value.callId === data.call_id) ) { try { - const candidate = JSON.parse(data.data) + const candidate = JSON.parse(data.content || data.message_content || "{}") console.log("收到ICE候选:", candidate) // 添加到WebRTC连接 @@ -653,7 +701,7 @@ export const useChatStore = defineStore("chat", () => { // 处理对方挂断 const handleCallHangup = (data) => { const callId = data?.call_id || currentCall.value?.callId || incomingCall.value?.callId - const peerId = data?.caller_id || data?.callee_id || currentCall.value?.peerId || incomingCall.value?.callerId + const peerId = data?.sender_user_id || currentCall.value?.peerId || incomingCall.value?.callerId if (callId && peerId) { // 添加通话记录 @@ -694,7 +742,7 @@ export const useChatStore = defineStore("chat", () => { // 处理对方掉线 const handleCallDisconnected = (data) => { const callId = data?.call_id || currentCall.value?.callId || incomingCall.value?.callId - const peerId = data?.caller_id || data?.callee_id || currentCall.value?.peerId || incomingCall.value?.callerId + const peerId = data?.sender_user_id || currentCall.value?.peerId || incomingCall.value?.callerId if (callId && peerId) { // 添加通话记录 @@ -735,7 +783,7 @@ export const useChatStore = defineStore("chat", () => { // 处理对方终止呼叫 const handleCallTerminated = (data) => { const callId = data.call_id - const peerId = data.caller_id || data.callee_id + const peerId = data.sender_user_id if ( (currentCall.value && currentCall.value.callId === callId) || @@ -784,6 +832,12 @@ export const useChatStore = defineStore("chat", () => { return } + // 验证用户登录状态 + if (!userStore.currentUser?.id) { + alert("用户未登录,无法发起通话") + return + } + resetCallState() const callId = Date.now().toString() + Math.random().toString(36).substr(2, 9) @@ -799,18 +853,8 @@ export const useChatStore = defineStore("chat", () => { callStatus.value = "calling" callConnectionStatus.value = "等待对方接听..." - // 获取本地媒体 - try { - await webrtc.getLocalMedia({ - audio: true, - video: type === "video", - }) - } catch (error) { - console.error("获取本地媒体失败:", error) - resetCallState() - alert("无法访问摄像头或麦克风") - return - } + // 先不获取本地媒体,等对方接听后再获取 + console.log("发起通话,等待对方接听...") // 设置30秒超时(无人接听) setCallTimeout(() => { @@ -819,12 +863,8 @@ export const useChatStore = defineStore("chat", () => { callConnectionStatus.value = "对方无人接听" console.log("呼叫超时,对方无人接听") - // 发送无人接听信号 - sendCallSignal({ - call_status: "no-answer", - call_id: callId, - receiver_user_id: peerId, - }) + // 发送无人接听信号 - 使用便捷方法 + callAPI.sendNoAnswer(userStore.currentUser.id, peerId, callId, type === "video" ? 6 : 7) // 更新好友最后消息为未接来电 friend.lastMessage = "[未接来电]" @@ -849,21 +889,46 @@ export const useChatStore = defineStore("chat", () => { } }, 30000) - // 发送通话请求 - sendCallSignal({ - call_status: "invite", - call_id: callId, - call_type: type === "video" ? 6 : 7, - receiver_user_id: peerId, - }) + // 发送通话请求 - 使用便捷方法 + try { + console.log("准备发送通话邀请:", { + senderUserId: userStore.currentUser.id, + receiverUserId: peerId, + callId: callId, + callType: type === "video" ? 6 : 7, + }) - console.log("发起通话:", currentCall.value) + await callAPI.invite(userStore.currentUser.id, peerId, callId, type === "video" ? 6 : 7) + + console.log("发起通话成功:", currentCall.value) + } catch (error) { + console.error("发起通话失败:", error) + resetCallState() + + // 提供更友好的错误提示 + let errorMessage = "发起通话失败" + if (error.response?.status === 400) { + errorMessage = "请求参数错误,请检查网络连接" + } else if (error.response?.status === 500) { + errorMessage = "服务器错误,请稍后重试" + } else if (error.message) { + errorMessage = error.message + } + + alert(errorMessage) + } } // 接听来电 const acceptCall = async () => { if (!incomingCall.value) return + // 防止重复接听 + if (callStatus.value === "connecting" || callStatus.value === "ongoing") { + console.log("通话已在进行中,忽略重复接听") + return + } + callStatus.value = "connecting" callConnectionStatus.value = "正在连接..." @@ -874,12 +939,13 @@ export const useChatStore = defineStore("chat", () => { video: incomingCall.value.callType === "video", }) - // 发送接受信号 - sendCallSignal({ - call_status: "accepted", - call_id: incomingCall.value.callId, - receiver_user_id: incomingCall.value.callerId, - }) + // 发送接受信号 - 使用便捷方法 + await callAPI.accept( + userStore.currentUser.id, + incomingCall.value.callerId, + incomingCall.value.callId, + incomingCall.value.callType === "video" ? 6 : 7, + ) currentCall.value = { callId: incomingCall.value.callId, @@ -889,7 +955,7 @@ export const useChatStore = defineStore("chat", () => { status: "connecting", } - console.log("接听通话,等待Offer...") + console.log("接听通话成功,等待信令交换...") } catch (error) { console.error("接听通话失败:", error) rejectCall("failed") @@ -897,50 +963,64 @@ export const useChatStore = defineStore("chat", () => { } // 拒绝来电 - const rejectCall = (reason = "rejected") => { + const rejectCall = async (reason = "rejected") => { if (!incomingCall.value) return - // 发送拒绝信号 - sendCallSignal({ - call_status: reason, - call_id: incomingCall.value.callId, - receiver_user_id: incomingCall.value.callerId, - }) - - // 添加通话记录 - const friend = friends.value.find((f) => f.id === incomingCall.value.callerId) - if (friend) { - const content = reason === "busy" ? "[对方忙线]" : "[已拒绝]" - - const callMessage = { - type: incomingCall.value.callType === "video" ? "video-call" : "audio-call", - content: content, - time: new Date().toLocaleTimeString().slice(0, 5), - senderId: userStore.currentUser?.id, - read: true, - timestamp: Date.now(), - duration: 0, + try { + // 发送拒绝信号 - 使用便捷方法 + if (reason === "rejected") { + await callAPI.reject( + userStore.currentUser.id, + incomingCall.value.callerId, + incomingCall.value.callId, + incomingCall.value.callType === "video" ? 6 : 7, + ) + } else { + await callAPI.sendFailed( + userStore.currentUser.id, + incomingCall.value.callerId, + incomingCall.value.callId, + incomingCall.value.callType === "video" ? 6 : 7, + ) } - chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) - // 更新最后消息 - friend.lastMessage = content - chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount) + // 添加通话记录 + const friend = friends.value.find((f) => f.id === incomingCall.value.callerId) + if (friend) { + const content = reason === "busy" ? "[对方忙线]" : "[已拒绝]" + + const callMessage = { + type: incomingCall.value.callType === "video" ? "video-call" : "audio-call", + content: content, + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: userStore.currentUser?.id, + read: true, + timestamp: Date.now(), + duration: 0, + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = content + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount) + } + + resetCallState() + callConnectionStatus.value = reason === "busy" ? "对方忙线" : "已拒绝" + + // 5秒后清除状态 + setTimeout(() => { + callConnectionStatus.value = "" + }, 5000) + + console.log("拒绝通话成功") + } catch (error) { + console.error("拒绝通话失败:", error) } - - resetCallState() - callConnectionStatus.value = reason === "busy" ? "对方忙线" : "已拒绝" - - // 5秒后清除状态 - setTimeout(() => { - callConnectionStatus.value = "" - }, 5000) - - console.log("拒绝通话") } // 结束通话 - const endCall = (reason = "ended") => { + const endCall = async (reason = "ended") => { const callId = currentCall.value?.callId || incomingCall.value?.callId const peerId = currentCall.value?.peerId || incomingCall.value?.callerId const callTypeVal = currentCall.value?.callType || incomingCall.value?.callType @@ -950,55 +1030,56 @@ export const useChatStore = defineStore("chat", () => { return } - // 发送结束信号 - sendCallSignal({ - call_status: reason, - call_id: callId, - receiver_user_id: peerId, - }) + try { + // 发送结束信号 - 使用便捷方法 + await callAPI.end(userStore.currentUser.id, peerId, callId, callTypeVal === "video" ? 6 : 7) - // 添加通话记录 - const friend = friends.value.find((f) => f.id === peerId) - if (friend) { - const duration = - callStatus.value === "ongoing" - ? Math.floor((Date.now() - (currentCall.value?.startTime || incomingCall.value?.startTime)) / 1000) - : 0 + // 添加通话记录 + const friend = friends.value.find((f) => f.id === peerId) + if (friend) { + const duration = + callStatus.value === "ongoing" + ? Math.floor((Date.now() - (currentCall.value?.startTime || incomingCall.value?.startTime)) / 1000) + : 0 - let content = "" - if (reason === "no-answer") { - content = "[未接来电]" - } else if (duration > 0) { - content = `[${callTypeVal === "video" ? "视频" : "语音"}通话] ${Math.floor(duration / 60)}分${duration % 60}秒` - } else { - content = "[通话结束]" + let content = "" + if (reason === "no-answer") { + content = "[未接来电]" + } else if (duration > 0) { + content = `[${callTypeVal === "video" ? "视频" : "语音"}通话] ${Math.floor(duration / 60)}分${duration % 60}秒` + } else { + content = "[通话结束]" + } + + const callMessage = { + type: callTypeVal === "video" ? "video-call" : "audio-call", + content: content, + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: userStore.currentUser?.id, + read: true, + timestamp: Date.now(), + duration: duration, + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = content + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount) } - const callMessage = { - type: callTypeVal === "video" ? "video-call" : "audio-call", - content: content, - time: new Date().toLocaleTimeString().slice(0, 5), - senderId: userStore.currentUser?.id, - read: true, - timestamp: Date.now(), - duration: duration, - } - chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + resetCallState() + callConnectionStatus.value = reason === "no-answer" ? "对方无人接听" : "通话结束" - // 更新最后消息 - friend.lastMessage = content - chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount) + // 5秒后清除状态 + setTimeout(() => { + callConnectionStatus.value = "" + }, 5000) + + console.log("结束通话成功") + } catch (error) { + console.error("结束通话失败:", error) + resetCallState() } - - resetCallState() - callConnectionStatus.value = reason === "no-answer" ? "对方无人接听" : "通话结束" - - // 5秒后清除状态 - setTimeout(() => { - callConnectionStatus.value = "" - }, 5000) - - console.log("结束通话") } return { @@ -1032,7 +1113,7 @@ export const useChatStore = defineStore("chat", () => { rejectCall, endCall, resetCallState, - sendCallSignal, + sendCallSignal: sendCallSignalInternal, clearCallTimeout, } }) diff --git a/src/utils/request.js b/src/utils/request.js index de69008..6b99e33 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -19,7 +19,7 @@ service.interceptors.request.use( (error) => { console.error("Request error:", error) return Promise.reject(error) - } + }, ) // 响应拦截器 @@ -59,7 +59,7 @@ service.interceptors.response.use( message.error("网络请求失败") return Promise.reject(error) - } + }, ) // CORS错误的备用处理方案 @@ -107,7 +107,7 @@ export const upload = (url, file) => { }) } -// 发送消息API(支持普通消息和通话信令) +// 发送普通消息API export const sendMessage = (data) => { const apiUrl = "/api/send-to-user" @@ -121,7 +121,7 @@ export const sendMessage = (data) => { "medical-record": 5, "video-call": 6, "audio-call": 7, - file: 8 + file: 8, } const requestData = { @@ -130,7 +130,7 @@ export const sendMessage = (data) => { message_type: messageTypeMap[data.type] || 0, message_content: data.content || "", // 添加来源标记 - isFromHttp: true + isFromHttp: true, } // 如果包含通话信令,添加call_id和call_status @@ -161,4 +161,182 @@ export const sendMessage = (data) => { }) } -export default service \ No newline at end of file +// 专门的通话信令发送方法 +export const sendCallSignal = (signalData) => { + const apiUrl = "/api/send-to-user" + + // 验证必要参数 + if (!signalData.sender_user_id) { + throw new Error("缺少发送方用户ID") + } + if (!signalData.receiver_user_id) { + throw new Error("缺少接收方用户ID") + } + if (!signalData.call_id) { + throw new Error("缺少通话ID") + } + if (!signalData.call_status) { + throw new Error("缺少通话状态") + } + + // 构建通话信令请求数据 + const requestData = { + sender_user_id: signalData.sender_user_id, + receiver_user_id: signalData.receiver_user_id, + message_type: signalData.message_type || 6, // 默认视频通话 + message_content: signalData.message_content || JSON.stringify(signalData.data || {}), + call_id: signalData.call_id, + call_status: signalData.call_status, + // 添加来源标记 + isFromHttp: true, + } + + console.log("发送通话信令到API:", apiUrl, requestData) + + return axios + .post(apiUrl, requestData, { + headers: { + "Content-Type": "application/json", + }, + timeout: 15000, + withCredentials: false, + }) + .then((response) => { + console.log("通话信令发送成功:", response.data) + return response.data + }) + .catch((error) => { + console.error("通话信令发送失败:", error) + + // 提供更详细的错误信息 + if (error.response) { + console.error("响应错误:", error.response.data) + console.error("响应状态:", error.response.status) + console.error("响应头:", error.response.headers) + } else if (error.request) { + console.error("请求错误:", error.request) + } else { + console.error("配置错误:", error.message) + } + + throw error + }) +} + +// 通话相关的便捷方法 +export const callAPI = { + // 发起通话邀请 + invite: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "invite", + message_type: callType, // 6=视频, 7=语音 + }) + }, + + // 接受通话 + accept: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "accepted", + message_type: callType, + }) + }, + + // 拒绝通话 + reject: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "rejected", + message_type: callType, + }) + }, + + // 结束通话 + end: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "ended", + message_type: callType, + }) + }, + + // 发送WebRTC Offer + sendOffer: (senderUserId, receiverUserId, callId, offerData, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "offer", + message_type: callType, + data: offerData, + }) + }, + + // 发送WebRTC Answer + sendAnswer: (senderUserId, receiverUserId, callId, answerData, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "answer", + message_type: callType, + data: answerData, + }) + }, + + // 发送ICE候选 + sendCandidate: (senderUserId, receiverUserId, callId, candidateData, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "candidate", + message_type: callType, + data: candidateData, + }) + }, + + // 发送忙线状态 + sendBusy: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "busy", + message_type: callType, + }) + }, + + // 发送无人接听 + sendNoAnswer: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "no-answer", + message_type: callType, + }) + }, + + // 发送通话失败 + sendFailed: (senderUserId, receiverUserId, callId, callType = 6) => { + return sendCallSignal({ + sender_user_id: senderUserId, + receiver_user_id: receiverUserId, + call_id: callId, + call_status: "failed", + message_type: callType, + }) + }, +} + +export default service