From 95ddd9925cd2e39fc4f4ccd375ee46e3ce8e3c85 Mon Sep 17 00:00:00 2001 From: liqi Date: Wed, 9 Jul 2025 14:05:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=201.=20=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E8=A7=86=E9=A2=91=E6=B5=81=E8=83=BD=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=88=B0=E4=BA=86=202.=20=E4=B8=8D=E7=9F=A5=E9=81=93=E4=B8=BA?= =?UTF-8?q?=E4=BB=80=E4=B9=88=E5=81=B6=E5=B0=94=E8=83=BD=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现有问题: 1. 不稳定,连接不是每次都成功10%大约 2. 对方获取不到本地推送 --- src/components/VideoCallComponent.vue | 341 ++++++++++--------- src/composables/useWebRTC.js | 450 ++++++++------------------ src/stores/chat.js | 51 ++- 3 files changed, 362 insertions(+), 480 deletions(-) diff --git a/src/components/VideoCallComponent.vue b/src/components/VideoCallComponent.vue index f6dd33e..ea1f880 100644 --- a/src/components/VideoCallComponent.vue +++ b/src/components/VideoCallComponent.vue @@ -61,26 +61,51 @@ v-if="isMinimized" class="minimized-content" @mousedown="startDrag" + @dblclick="toggleMinimize" > -
- -
-
+ + -
-
{{ callerInfo.name }}
-
{{ formattedDuration }}
-
+ +
@@ -88,11 +113,12 @@
@@ -106,7 +132,7 @@
+ + +
+ + {{ formattedDuration }} +
@@ -229,12 +262,10 @@ const isMinimized = ref(false); const isMicOn = ref(true); const isCameraOn = ref(props.callType === 'video'); const isSpeakerOn = ref(true); -const hasRemoteVideo = ref(false); const callDuration = ref(0); const callTimer = ref(null); const isDragging = ref(false); const isAccepting = ref(false); -const showLocalVideo = ref(false); // 友好提示相关 const showFriendlyMessage = ref(false); @@ -256,12 +287,6 @@ const callStatus = computed(() => { return chatStore.callStatus; }); -// 本地媒体状态 -const hasLocalVideo = computed(() => webrtc.hasLocalVideo.value); -const hasLocalAudio = computed(() => webrtc.hasLocalAudio.value); -const isCameraWorking = computed(() => hasLocalVideo.value && isCameraOn.value); -const isMicWorking = computed(() => hasLocalAudio.value && isMicOn.value); - // 连接状态文本 const connectionText = computed(() => { if (chatStore.callConnectionStatus) { @@ -285,7 +310,7 @@ const connectionText = computed(() => { } if (callStatus.value === "ongoing") { - return `通话中 ${formattedDuration.value}`; + return `通话中`; } if (callStatus.value === "connecting") { @@ -317,14 +342,14 @@ const startCallTimer = () => { callTimer.value = setInterval(() => { callDuration.value += 1; }, 1000); - console.log("通话计时器已启动"); + console.log("⏰ 通话计时器已启动"); }; const stopCallTimer = () => { if (callTimer.value) { clearInterval(callTimer.value); callTimer.value = null; - console.log("通话计时器已停止"); + console.log("⏰ 通话计时器已停止"); } }; @@ -337,81 +362,52 @@ const formattedDuration = computed(() => { // 计算容器样式 const containerStyle = computed(() => { + const width = isMinimized.value ? (props.callType === 'audio' ? '250px' : '200px') : '500px'; + const height = isMinimized.value ? '60px' : '380px'; + return { left: `${position.x}px`, top: `${position.y}px`, - width: isMinimized.value ? '200px' : '500px', - height: isMinimized.value ? '60px' : '380px' + width: width, + height: height }; }); // 初始化媒体设备 const initializeMedia = async () => { try { - if (callStatus.value === "calling" || callStatus.value === "connecting" || callStatus.value === "ongoing") { + console.log("🎥 初始化媒体设备,当前状态:", callStatus.value); + + if (['calling', 'connecting', 'ongoing'].includes(callStatus.value) || + (props.isIncoming && callStatus.value === 'ringing')) { + const constraints = { audio: true, video: props.callType === 'video' }; - console.log("初始化媒体设备,约束:", constraints); + console.log("🎬 获取媒体约束:", constraints); await webrtc.getLocalMedia(constraints); + console.log("✅ 媒体获取成功"); - // 立即设置本地视频 - await nextTick(); - updateLocalVideo(); - - } else if (props.isIncoming && callStatus.value === "ringing") { - console.log("来电状态,暂不获取媒体流"); - return; } else { - console.log("当前状态不需要获取媒体:", callStatus.value); + console.log("⏭️ 当前状态不需要获取媒体:", callStatus.value); } } catch (error) { - console.warn('无法访问媒体设备,但继续通话流程:', error); - } -}; - -// 更新本地视频显示 -const updateLocalVideo = async () => { - await nextTick(); - if (webrtc.localStream.value && localVideo.value) { - console.log("设置本地视频流到video元素"); - localVideo.value.srcObject = webrtc.localStream.value; - showLocalVideo.value = props.callType === 'video' && webrtc.hasLocalVideo.value; - console.log("本地视频显示状态:", showLocalVideo.value); - } else { - console.log("本地视频流或元素不存在:", { - stream: !!webrtc.localStream.value, - element: !!localVideo.value - }); - showLocalVideo.value = false; + console.warn('⚠️ 无法访问媒体设备:', error); } }; // 控制功能 const toggleMic = () => { isMicOn.value = !isMicOn.value; - if (webrtc.localStream.value) { - webrtc.localStream.value.getAudioTracks().forEach(track => { - track.enabled = isMicOn.value; - }); - } + webrtc.toggleMicrophone(isMicOn.value); }; const toggleCamera = async () => { if (props.callType === 'video') { isCameraOn.value = !isCameraOn.value; - - try { - await webrtc.toggleVideoStream(isCameraOn.value); - await nextTick(); - updateLocalVideo(); - console.log("摄像头切换成功:", isCameraOn.value); - } catch (error) { - console.error("摄像头切换失败:", error); - isCameraOn.value = !isCameraOn.value; - } + webrtc.toggleCamera(isCameraOn.value); } }; @@ -420,52 +416,55 @@ const toggleSpeaker = () => { if (remoteVideo.value) { remoteVideo.value.muted = !isSpeakerOn.value; } + if (remoteVideoPreview.value) { + remoteVideoPreview.value.muted = !isSpeakerOn.value; + } + console.log("🔊 扬声器状态:", isSpeakerOn.value); }; const toggleMinimize = () => { isMinimized.value = !isMinimized.value; + console.log("📐 切换最小化状态:", isMinimized.value); - if (isMinimized.value) { - position.x = Math.max(0, Math.min(position.x, window.innerWidth - 200)); - position.y = Math.max(0, Math.min(position.y, window.innerHeight - 60)); - } else { - position.x = Math.max(0, Math.min(position.x, window.innerWidth - 500)); - position.y = Math.max(0, Math.min(position.y, window.innerHeight - 380)); - } + const width = isMinimized.value ? (props.callType === 'audio' ? 250 : 200) : 500; + const height = isMinimized.value ? 60 : 380; + + position.x = Math.max(0, Math.min(position.x, window.innerWidth - width)); + position.y = Math.max(0, Math.min(position.y, window.innerHeight - height)); }; const endCall = () => { stopCallTimer(); - - if (localVideo.value) localVideo.value.srcObject = null; - if (remoteVideo.value) remoteVideo.value.srcObject = null; - if (remoteVideoPreview.value) remoteVideoPreview.value.srcObject = null; - isVisible.value = false; chatStore.endCall(); emit('end-call'); }; // 接听通话 -const acceptCall = () => { - if (isAccepting.value || chatStore.callStatus === "connecting" || chatStore.callStatus === "ongoing") { - console.log("已在处理接听或通话中,忽略重复点击") +const acceptCall = async () => { + if (isAccepting.value || ['connecting', 'ongoing'].includes(chatStore.callStatus)) { + console.log("⚠️ 已在处理接听或通话中,忽略重复点击") return; } - console.log("用户点击接听按钮") + console.log("📞 用户点击接听按钮") isAccepting.value = true; - chatStore.acceptCall().then(() => { - console.log("接听请求已发送") + try { + // 先获取媒体流 + await initializeMedia(); + + // 然后接听通话 + await chatStore.acceptCall(); + console.log("✅ 接听请求已发送") showFriendlyNotification("正在接听通话...", 2000); - }).catch((error) => { - console.error("接听失败:", error) - }).finally(() => { + } catch (error) { + console.error("❌ 接听失败:", error) + } finally { setTimeout(() => { isAccepting.value = false; }, 3000); - }); + } emit('accept-call'); }; @@ -473,11 +472,11 @@ const acceptCall = () => { // 拒绝通话 const rejectCall = () => { if (chatStore.callStatus === "idle" || isAccepting.value) { - console.log("通话已结束或正在接听,忽略拒绝操作") + console.log("⚠️ 通话已结束或正在接听,忽略拒绝操作") return; } - console.log("用户点击拒绝按钮") + console.log("❌ 用户点击拒绝按钮") isVisible.value = false; chatStore.rejectCall(); emit('reject-call'); @@ -485,7 +484,9 @@ const rejectCall = () => { // 拖拽功能 const startDrag = (event) => { - if (event.target.closest('.action-btn') || event.target.closest('.control-btn')) { + if (event.target.closest('.action-btn') || + event.target.closest('.control-btn') || + event.target.closest('.mini-expand-btn')) { return; } @@ -510,7 +511,7 @@ const handleDrag = (event) => { startPosition.x = event.clientX; startPosition.y = event.clientY; - const width = isMinimized.value ? 200 : 500; + const width = isMinimized.value ? (props.callType === 'audio' ? 250 : 200) : 500; const height = isMinimized.value ? 60 : 380; position.x = Math.max(0, Math.min(position.x, window.innerWidth - width)); @@ -525,81 +526,49 @@ const stopDrag = () => { // 窗口大小变化处理 const handleWindowResize = () => { - const width = isMinimized.value ? 200 : 500; + const width = isMinimized.value ? (props.callType === 'audio' ? 250 : 200) : 500; const height = isMinimized.value ? 60 : 380; position.x = Math.max(0, Math.min(position.x, window.innerWidth - width)); position.y = Math.max(0, Math.min(position.y, window.innerHeight - height)); }; -// 监听本地流变化 -watch(() => webrtc.localStream.value, async (newStream) => { - console.log("本地流变化:", !!newStream); - await nextTick(); - updateLocalVideo(); -}); - -// 监听远程流变化 -watch(() => webrtc.remoteStream.value, (newStream) => { - console.log("远程流变化:", !!newStream); - if (newStream) { - if (remoteVideo.value) { - remoteVideo.value.srcObject = newStream; - } - if (remoteVideoPreview.value) { - remoteVideoPreview.value.srcObject = newStream; - } - hasRemoteVideo.value = true; - } else { - if (remoteVideo.value) { - remoteVideo.value.srcObject = null; - } - if (remoteVideoPreview.value) { - remoteVideoPreview.value.srcObject = null; - } - hasRemoteVideo.value = false; - } -}); - // 监听通话状态 watch(() => chatStore.callStatus, (status, oldStatus) => { - console.log("VideoCallComponent 监听到通话状态变化:", oldStatus, "->", status); + console.log("📞 VideoCallComponent 监听到通话状态变化:", oldStatus, "->", status); if (status === "calling") { - console.log("发起通话,初始化媒体设备") + console.log("📞 发起通话,初始化媒体设备") initializeMedia(); } else if (status === "ongoing") { if (!callTimer.value) { - console.log("开始通话计时") + console.log("⏰ 开始通话计时") startCallTimer(); } showFriendlyNotification("通话已连接", 2000); } else if (status === "ended" || status === "idle") { - console.log("通话结束,准备关闭组件"); + console.log("📞 通话结束,准备关闭组件"); stopCallTimer(); setTimeout(() => { endCall(); }, 100); } else if (status === "connecting") { - console.log("通话连接中,确保媒体设备已初始化") - if (!webrtc.localStream.value) { - initializeMedia(); - } + console.log("🔗 通话连接中") showFriendlyNotification("正在建立连接...", 2000); } else if (status === "failed") { - console.log("通话失败") + console.log("❌ 通话失败") showFriendlyNotification("连接失败", 3000); setTimeout(() => { endCall(); }, 3000); } else if (status === "hangup") { - console.log("对方已挂断,3秒后关闭组件") + console.log("📞 对方已挂断,3秒后关闭组件") showFriendlyNotification("对方已挂断", 3000); setTimeout(() => { endCall(); }, 3000); } else if (status === "disconnected") { - console.log("网络连接已断开,3秒后关闭组件") + console.log("🌐 网络连接已断开,3秒后关闭组件") showFriendlyNotification("网络连接已断开", 3000); setTimeout(() => { endCall(); @@ -628,7 +597,7 @@ onMounted(() => { window.addEventListener('resize', handleWindowResize); - console.log("VideoCallComponent mounted, callStatus:", chatStore.callStatus, "isIncoming:", props.isIncoming); + console.log("🚀 VideoCallComponent mounted, callStatus:", chatStore.callStatus, "isIncoming:", props.isIncoming); if (['calling', 'connecting', 'ongoing'].includes(chatStore.callStatus)) { initializeMedia(); @@ -636,7 +605,7 @@ onMounted(() => { }); onUnmounted(() => { - console.log("VideoCallComponent unmounted"); + console.log("🔚 VideoCallComponent unmounted"); stopCallTimer(); if (friendlyMessageTimer.value) { @@ -647,16 +616,6 @@ onUnmounted(() => { document.removeEventListener('mouseup', stopDrag); window.removeEventListener('resize', handleWindowResize); - if (localVideo.value) { - localVideo.value.srcObject = null; - } - if (remoteVideo.value) { - remoteVideo.value.srcObject = null; - } - if (remoteVideoPreview.value) { - remoteVideoPreview.value.srcObject = null; - } - webrtc.closeConnection(); }); @@ -831,15 +790,44 @@ onUnmounted(() => { height: 100%; background: linear-gradient(135deg, #4361ee, #3f37c9); color: white; - padding: 0 10px; cursor: move; } +.audio-call-mini { + display: flex; + align-items: center; + width: 100%; + padding: 0 12px; + gap: 10px; +} + +.video-call-mini { + display: flex; + align-items: center; + width: 100%; + padding: 0 10px; + gap: 8px; +} + +.mini-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: white; + font-size: 16px; + flex-shrink: 0; +} + .remote-video-preview { width: 40px; height: 40px; border-radius: 8px; overflow: hidden; + flex-shrink: 0; } .remote-video { @@ -872,7 +860,9 @@ onUnmounted(() => { .mini-info { flex: 1; min-width: 0; - padding: 0 10px; + display: flex; + flex-direction: column; + gap: 2px; } .mini-name { @@ -883,9 +873,30 @@ onUnmounted(() => { text-overflow: ellipsis; } -.mini-status { - font-size: 11px; - opacity: 0.8; +.mini-timer { + font-size: 12px; + opacity: 0.9; + font-family: 'Courier New', monospace; +} + +.mini-expand-btn { + width: 24px; + height: 24px; + border: none; + border-radius: 4px; + background: rgba(255, 255, 255, 0.2); + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.mini-expand-btn:hover { + background: rgba(255, 255, 255, 0.3); } .video-area { @@ -943,6 +954,22 @@ onUnmounted(() => { opacity: 0.8; } +.call-timer-display { + position: absolute; + top: 16px; + left: 16px; + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 8px 12px; + border-radius: 20px; + font-size: 14px; + font-family: 'Courier New', monospace; + display: flex; + align-items: center; + gap: 6px; + backdrop-filter: blur(10px); +} + .local-video-container { position: absolute; bottom: 16px; diff --git a/src/composables/useWebRTC.js b/src/composables/useWebRTC.js index e5dfd09..2e575e1 100644 --- a/src/composables/useWebRTC.js +++ b/src/composables/useWebRTC.js @@ -9,62 +9,91 @@ export function useWebRTC() { const peerConnection = ref(null) const isConnected = ref(false) const isConnecting = ref(false) - const hasLocalVideo = ref(false) - const hasLocalAudio = ref(false) const userStore = useUserStore() const chatStore = useChatStore() const configuration = { - iceServers: [ - { urls: "stun:stun.l.google.com:19302" }, - { urls: "stun:stun1.l.google.com:19302" }, - { urls: "stun:stun2.l.google.com:19302" }, - { urls: "stun:stun3.l.google.com:19302" }, - { urls: "stun:stun4.l.google.com:19302" }, - ], + iceServers: [{ urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }], } + // 获取本地媒体流 + const getLocalMedia = async (constraints = { audio: true, video: true }) => { + try { + console.log("🎥 开始获取本地媒体流:", constraints) + + // 如果已有流,先停止 + if (localStream.value) { + localStream.value.getTracks().forEach((track) => track.stop()) + localStream.value = null + } + + const stream = await navigator.mediaDevices.getUserMedia(constraints) + localStream.value = stream + + console.log("✅ 本地媒体流获取成功:", { + streamId: stream.id, + videoTracks: stream.getVideoTracks().length, + audioTracks: stream.getAudioTracks().length, + videoEnabled: stream.getVideoTracks()[0]?.enabled, + audioEnabled: stream.getAudioTracks()[0]?.enabled, + }) + + return stream + } catch (error) { + console.error("❌ 获取本地媒体流失败:", error) + + // 如果视频失败,尝试只获取音频 + if (constraints.video) { + try { + console.log("🎵 尝试仅获取音频流") + const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }) + localStream.value = audioStream + console.log("✅ 音频流获取成功") + return audioStream + } catch (audioError) { + console.error("❌ 音频流也获取失败:", audioError) + } + } + + throw error + } + } + + // 创建PeerConnection const createPeerConnection = (callId, peerId) => { + console.log("🔗 创建PeerConnection:", { callId, peerId }) + if (peerConnection.value) { - console.log("关闭现有的WebRTC连接") - closeConnection() + console.log("🔄 关闭现有连接") + peerConnection.value.close() } - console.log("创建新的WebRTC连接") - peerConnection.value = new RTCPeerConnection(configuration) + const pc = new RTCPeerConnection(configuration) + peerConnection.value = pc - peerConnection.value.onicecandidate = async (event) => { + // ICE候选事件 + pc.onicecandidate = async (event) => { if (event.candidate) { - console.log("发送ICE候选:", event.candidate) - - 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) - } + console.log("📡 发送ICE候选") + try { + await callAPI.sendCandidate(userStore.currentUser?.id, peerId, callId, event.candidate, 6) + } catch (error) { + console.error("❌ 发送ICE候选失败:", error) } } } - peerConnection.value.ontrack = (event) => { - if (event.streams && event.streams[0]) { - console.log("接收到远程流:", event.streams[0]) - remoteStream.value = event.streams[0] - - const videoTracks = event.streams[0].getVideoTracks() - const audioTracks = event.streams[0].getAudioTracks() - console.log("远程流包含:", { - video: videoTracks.length, - audio: audioTracks.length, - }) - } + // 接收远程流 + pc.ontrack = (event) => { + console.log("📹 接收到远程流:", event.streams[0]) + remoteStream.value = event.streams[0] } - peerConnection.value.onconnectionstatechange = () => { - const state = peerConnection.value?.connectionState - console.log("WebRTC连接状态变化:", state) + // 连接状态变化 + pc.onconnectionstatechange = () => { + const state = pc.connectionState + console.log("🔗 连接状态:", state) if (state === "connected") { isConnected.value = true @@ -72,352 +101,137 @@ export function useWebRTC() { if (chatStore.callStatus === "connecting") { chatStore.callStatus = "ongoing" chatStore.callConnectionStatus = "通话已连接" - console.log("WebRTC连接已建立,通话开始") } } else if (state === "connecting") { isConnecting.value = true - if (chatStore.callStatus === "connecting") { - chatStore.callConnectionStatus = "正在建立连接..." - } - } else if (state === "disconnected") { - console.log("WebRTC连接断开") + } else if (state === "disconnected" || state === "failed") { isConnected.value = false isConnecting.value = false - if (chatStore.callStatus === "ongoing") { - chatStore.callStatus = "disconnected" - chatStore.callConnectionStatus = "连接已断开" - } - } else if (state === "failed") { - console.log("WebRTC连接失败") - isConnected.value = false - isConnecting.value = false - if (chatStore.callStatus === "connecting" || chatStore.callStatus === "ongoing") { - chatStore.callStatus = "failed" - chatStore.callConnectionStatus = "连接失败" - } } } - peerConnection.value.oniceconnectionstatechange = () => { - const state = peerConnection.value?.iceConnectionState - console.log("ICE连接状态变化:", state) - - if (state === "connected" || state === "completed") { - console.log("ICE连接成功") - isConnected.value = true - if (chatStore.callStatus === "connecting") { - chatStore.callStatus = "ongoing" - chatStore.callConnectionStatus = "通话中" - } - } else if (state === "failed") { - console.log("ICE连接失败") - if (chatStore.callStatus === "ongoing" || chatStore.callStatus === "connecting") { - chatStore.callStatus = "failed" - chatStore.callConnectionStatus = "网络连接失败" - } - } else if (state === "disconnected") { - console.log("ICE连接断开") - if (chatStore.callStatus === "ongoing") { - chatStore.callStatus = "disconnected" - chatStore.callConnectionStatus = "网络连接已断开" - } - } + // 添加本地流到连接 + if (localStream.value) { + console.log("➕ 添加本地流到PeerConnection") + localStream.value.getTracks().forEach((track) => { + pc.addTrack(track, localStream.value) + }) } - return peerConnection.value - } - - const getLocalMedia = async (constraints = { audio: true, video: true }) => { - try { - // 如果已有本地流,先停止 - if (localStream.value) { - localStream.value.getTracks().forEach((track) => track.stop()) - localStream.value = null - hasLocalVideo.value = false - hasLocalAudio.value = false - } - - console.log("获取本地媒体,约束:", constraints) - - try { - localStream.value = await navigator.mediaDevices.getUserMedia(constraints) - console.log("获取本地媒体成功:", localStream.value) - - // 检查实际获取到的轨道 - const videoTracks = localStream.value.getVideoTracks() - const audioTracks = localStream.value.getAudioTracks() - - hasLocalVideo.value = videoTracks.length > 0 && videoTracks[0].enabled - hasLocalAudio.value = audioTracks.length > 0 && audioTracks[0].enabled - - console.log("本地媒体状态:", { - hasVideo: hasLocalVideo.value, - hasAudio: hasLocalAudio.value, - videoTracks: videoTracks.length, - audioTracks: audioTracks.length, - }) - - return localStream.value - } catch (mediaError) { - console.warn("获取完整媒体失败,尝试仅获取音频:", mediaError) - - if (constraints.video) { - try { - localStream.value = await navigator.mediaDevices.getUserMedia({ audio: constraints.audio, video: false }) - console.log("仅获取音频成功:", localStream.value) - - const audioTracks = localStream.value.getAudioTracks() - hasLocalVideo.value = false - hasLocalAudio.value = audioTracks.length > 0 && audioTracks[0].enabled - - console.log("本地媒体状态(仅音频):", { hasVideo: hasLocalVideo.value, hasAudio: hasLocalAudio.value }) - - return localStream.value - } catch (audioError) { - console.warn("获取音频也失败:", audioError) - hasLocalVideo.value = false - hasLocalAudio.value = false - return null - } - } else { - hasLocalVideo.value = false - hasLocalAudio.value = false - return null - } - } - } catch (error) { - console.error("获取本地媒体完全失败:", error) - hasLocalVideo.value = false - hasLocalAudio.value = false - return null - } - } - - // 重新获取视频流(用于摄像头开关) - const toggleVideoStream = async (enable) => { - try { - if (enable) { - console.log("重新获取视频流") - const newStream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: true, - }) - - // 停止旧的流 - if (localStream.value) { - localStream.value.getTracks().forEach((track) => track.stop()) - } - - localStream.value = newStream - hasLocalVideo.value = true - hasLocalAudio.value = true - - // 如果有PeerConnection,替换轨道 - if (peerConnection.value) { - const videoTrack = newStream.getVideoTracks()[0] - const audioTrack = newStream.getAudioTracks()[0] - - const senders = peerConnection.value.getSenders() - - // 替换视频轨道 - const videoSender = senders.find((sender) => sender.track && sender.track.kind === "video") - if (videoSender && videoTrack) { - await videoSender.replaceTrack(videoTrack) - console.log("替换视频轨道成功") - } else if (videoTrack) { - peerConnection.value.addTrack(videoTrack, newStream) - console.log("添加新视频轨道") - } - - // 替换音频轨道 - const audioSender = senders.find((sender) => sender.track && sender.track.kind === "audio") - if (audioSender && audioTrack) { - await audioSender.replaceTrack(audioTrack) - console.log("替换音频轨道成功") - } else if (audioTrack) { - peerConnection.value.addTrack(audioTrack, newStream) - console.log("添加新音频轨道") - } - } - - return newStream - } else { - // 关闭摄像头:只保留音频 - if (localStream.value) { - const videoTracks = localStream.value.getVideoTracks() - videoTracks.forEach((track) => { - track.stop() - localStream.value.removeTrack(track) - }) - hasLocalVideo.value = false - - // 如果有PeerConnection,移除视频轨道 - if (peerConnection.value) { - const senders = peerConnection.value.getSenders() - const videoSender = senders.find((sender) => sender.track && sender.track.kind === "video") - if (videoSender) { - await videoSender.replaceTrack(null) - console.log("移除视频轨道") - } - } - } - return localStream.value - } - } catch (error) { - console.error("切换视频流失败:", error) - return localStream.value - } + return pc } + // 创建Offer const createOffer = async (callId, peerId) => { + console.log("📤 创建Offer") + if (!peerConnection.value) { createPeerConnection(callId, peerId) } - // 确保本地流已添加到连接 - if (localStream.value) { - console.log("添加本地流到PeerConnection") - const existingSenders = peerConnection.value.getSenders() - - localStream.value.getTracks().forEach((track) => { - // 检查是否已经添加了这个轨道 - const existingSender = existingSenders.find((sender) => sender.track === track) - if (!existingSender) { - console.log("添加轨道到PeerConnection:", track.kind, track.enabled) - peerConnection.value.addTrack(track, localStream.value) - } else { - console.log("轨道已存在,跳过添加:", track.kind) - } - }) - } else { - console.warn("本地流不存在,无法添加到PeerConnection") - } - try { - console.log("开始创建Offer") - const offer = await peerConnection.value.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }) - - console.log("设置本地描述") + const offer = await peerConnection.value.createOffer() await peerConnection.value.setLocalDescription(offer) - console.log("创建Offer成功:", offer) + + console.log("✅ Offer创建成功:", offer.type) + console.log("📋 SDP:", offer.sdp.substring(0, 100) + "...") return offer } catch (error) { - console.error("创建Offer失败:", error) + console.error("❌ 创建Offer失败:", error) throw error } } + // 创建Answer const createAnswer = async (offer, callId, peerId) => { + console.log("📥 处理Offer并创建Answer") + if (!peerConnection.value) { createPeerConnection(callId, peerId) } try { - console.log("设置远程Offer描述") - await peerConnection.value.setRemoteDescription(new RTCSessionDescription(offer)) - - // 确保本地流已添加到连接 - if (localStream.value) { - console.log("添加本地流到PeerConnection") - const existingSenders = peerConnection.value.getSenders() - - localStream.value.getTracks().forEach((track) => { - const existingSender = existingSenders.find((sender) => sender.track === track) - if (!existingSender) { - console.log("添加轨道到PeerConnection:", track.kind, track.enabled) - peerConnection.value.addTrack(track, localStream.value) - } else { - console.log("轨道已存在,跳过添加:", track.kind) - } - }) - } else { - console.warn("本地流不存在,无法添加到PeerConnection") - } - - console.log("开始创建Answer") + await peerConnection.value.setRemoteDescription(offer) const answer = await peerConnection.value.createAnswer() - - console.log("设置本地Answer描述") await peerConnection.value.setLocalDescription(answer) - console.log("创建Answer成功:", answer) + + console.log("✅ Answer创建成功:", answer.type) + console.log("📋 SDP:", answer.sdp.substring(0, 100) + "...") return answer } catch (error) { - console.error("创建Answer失败:", error) + console.error("❌ 创建Answer失败:", error) throw error } } + // 设置远程Answer const setRemoteAnswer = async (answer) => { + console.log("📥 设置远程Answer") try { - if (!peerConnection.value) { - throw new Error("PeerConnection 未初始化") - } - console.log("设置远程Answer描述") - await peerConnection.value.setRemoteDescription(new RTCSessionDescription(answer)) - console.log("设置远程Answer成功") + await peerConnection.value.setRemoteDescription(answer) + console.log("✅ 远程Answer设置成功") } catch (error) { - console.error("设置远程Answer失败:", error) - throw error - } - } - - const setRemoteOffer = async (offer) => { - try { - if (!peerConnection.value) { - throw new Error("PeerConnection 未初始化") - } - console.log("设置远程Offer描述") - await peerConnection.value.setRemoteDescription(new RTCSessionDescription(offer)) - console.log("设置远程Offer成功") - } catch (error) { - console.error("设置远程Offer失败:", error) + console.error("❌ 设置远程Answer失败:", error) throw error } } + // 添加ICE候选 const addIceCandidate = async (candidate) => { if (peerConnection.value && peerConnection.value.remoteDescription) { try { - console.log("添加ICE候选:", candidate) - await peerConnection.value.addIceCandidate(new RTCIceCandidate(candidate)) - console.log("添加ICE候选成功") + await peerConnection.value.addIceCandidate(candidate) + console.log("✅ ICE候选添加成功") } catch (error) { - console.error("添加ICE候选失败:", error) + console.error("❌ 添加ICE候选失败:", error) } - } else { - console.warn("无法添加ICE候选,连接未准备好") } } + // 切换摄像头 + const toggleCamera = async (enable) => { + if (!localStream.value) return + + const videoTrack = localStream.value.getVideoTracks()[0] + if (videoTrack) { + videoTrack.enabled = enable + console.log("📹 摄像头状态:", enable ? "开启" : "关闭") + } + } + + // 切换麦克风 + const toggleMicrophone = async (enable) => { + if (!localStream.value) return + + const audioTrack = localStream.value.getAudioTracks()[0] + if (audioTrack) { + audioTrack.enabled = enable + console.log("🎤 麦克风状态:", enable ? "开启" : "关闭") + } + } + + // 关闭连接 const closeConnection = () => { - console.log("关闭WebRTC连接") + console.log("🔄 关闭WebRTC连接") if (localStream.value) { - localStream.value.getTracks().forEach((track) => { - track.stop() - }) + localStream.value.getTracks().forEach((track) => track.stop()) localStream.value = null } + if (remoteStream.value) { + remoteStream.value = null + } + if (peerConnection.value) { - peerConnection.value.onicecandidate = null - peerConnection.value.ontrack = null - peerConnection.value.onconnectionstatechange = null - peerConnection.value.oniceconnectionstatechange = null peerConnection.value.close() peerConnection.value = null } - remoteStream.value = null isConnected.value = false isConnecting.value = false - hasLocalVideo.value = false - hasLocalAudio.value = false } return { @@ -426,16 +240,14 @@ export function useWebRTC() { peerConnection, isConnected, isConnecting, - hasLocalVideo, - hasLocalAudio, - createPeerConnection, getLocalMedia, - toggleVideoStream, + createPeerConnection, createOffer, createAnswer, setRemoteAnswer, - setRemoteOffer, addIceCandidate, + toggleCamera, + toggleMicrophone, closeConnection, } } diff --git a/src/stores/chat.js b/src/stores/chat.js index d44cdde..f025537 100644 --- a/src/stores/chat.js +++ b/src/stores/chat.js @@ -334,7 +334,15 @@ export const useChatStore = defineStore("chat", () => { try { const offer = JSON.parse(data.content || data.message_content || "{}") - console.log("收到Offer:", offer) + + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") + console.log("📥 收到对方 OFFER 信令:") + console.log("📋 Offer 类型:", offer.type) + console.log("📋 Offer SDP (前200字符):", offer.sdp ? offer.sdp.substring(0, 200) + "..." : "无SDP") + console.log("📋 通话ID:", data.call_id) + console.log("📋 发送方:", data.sender_user_id) + console.log("📋 完整 Offer 对象:", offer) + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") if (!webrtc.peerConnection.value) { console.log("创建 WebRTC 连接以处理 Offer") @@ -343,9 +351,18 @@ export const useChatStore = defineStore("chat", () => { const answer = await webrtc.createAnswer(offer, data.call_id, data.sender_user_id) + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") + console.log("📤 准备发送 ANSWER 信令:") + console.log("📋 Answer 类型:", answer.type) + console.log("📋 Answer SDP (前200字符):", answer.sdp ? answer.sdp.substring(0, 200) + "..." : "无SDP") + console.log("📋 通话ID:", data.call_id) + console.log("📋 接收方:", data.sender_user_id) + console.log("📋 完整 Answer 对象:", answer) + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") + await callAPI.sendAnswer(userStore.currentUser.id, data.sender_user_id, data.call_id, answer, data.message_type) - console.log("发送Answer成功") + console.log("✅ 发送Answer成功") } catch (error) { console.error("处理Offer失败:", error) rejectCall("failed") @@ -363,12 +380,22 @@ export const useChatStore = defineStore("chat", () => { try { const answer = JSON.parse(data.content || data.message_content || "{}") - console.log("收到Answer:", answer) + + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") + console.log("📥 收到对方 ANSWER 信令:") + console.log("📋 Answer 类型:", answer.type) + console.log("📋 Answer SDP (前200字符):", answer.sdp ? answer.sdp.substring(0, 200) + "..." : "无SDP") + console.log("📋 通话ID:", data.call_id) + console.log("📋 发送方:", data.sender_user_id) + console.log("📋 完整 Answer 对象:", answer) + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") await webrtc.setRemoteAnswer(answer) callStatus.value = "connecting" callConnectionStatus.value = "正在建立连接..." + + console.log("✅ Answer处理成功,开始建立连接") } catch (error) { console.error("处理Answer失败:", error) endCall("failed") @@ -481,6 +508,13 @@ export const useChatStore = defineStore("chat", () => { callConnectionStatus.value = "对方已接听,正在连接..." console.log("通话已被接受,开始交换信令") + // 显示友好提示 + setTimeout(() => { + if (callStatus.value === "connecting") { + callConnectionStatus.value = "对方已接听,正在建立连接..." + } + }, 1000) + try { if (!webrtc.localStream.value) { console.log("获取本地媒体用于创建Offer") @@ -492,6 +526,15 @@ export const useChatStore = defineStore("chat", () => { const offer = await webrtc.createOffer(currentCall.value.callId, currentCall.value.peerId) + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") + console.log("📤 准备发送 OFFER 信令:") + console.log("📋 Offer 类型:", offer.type) + console.log("📋 Offer SDP (前200字符):", offer.sdp ? offer.sdp.substring(0, 200) + "..." : "无SDP") + console.log("📋 通话ID:", currentCall.value.callId) + console.log("📋 接收方:", currentCall.value.peerId) + console.log("📋 完整 Offer 对象:", offer) + console.log("🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯🎯") + await callAPI.sendOffer( userStore.currentUser.id, currentCall.value.peerId, @@ -500,7 +543,7 @@ export const useChatStore = defineStore("chat", () => { currentCall.value.callType === "video" ? 6 : 7, ) - console.log("发送Offer成功") + console.log("✅ 发送Offer成功") } catch (error) { console.error("创建Offer失败:", error) endCall("failed")