@@ -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")