修复:

1. 本地视频流能获取到了
2. 不知道为什么偶尔能连接成功

现有问题:
1. 不稳定,连接不是每次都成功10%大约
2. 对方获取不到本地推送
This commit is contained in:
2025-07-09 14:05:12 +08:00
parent 252b29fd5e
commit 95ddd9925c
3 changed files with 362 additions and 480 deletions

View File

@@ -61,26 +61,51 @@
v-if="isMinimized"
class="minimized-content"
@mousedown="startDrag"
@dblclick="toggleMinimize"
>
<div class="remote-video-preview">
<video
v-if="callType === 'video' && hasRemoteVideo"
ref="remoteVideoPreview"
class="remote-video"
autoplay
playsinline
></video>
<div v-else class="video-placeholder">
<div class="placeholder-avatar" :style="{ background: callerInfo.color }">
<!-- 音频通话显示头像和计时器 -->
<template v-if="callType === 'audio'">
<div class="audio-call-mini">
<div class="mini-avatar" :style="{ background: callerInfo.color }">
{{ callerInfo.name.charAt(0) }}
</div>
<div class="mini-info">
<div class="mini-name">{{ callerInfo.name }}</div>
<div class="mini-timer">{{ formattedDuration }}</div>
</div>
<button class="mini-expand-btn" @click="toggleMinimize" @mousedown.stop>
<i class="fas fa-expand"></i>
</button>
</div>
</div>
</template>
<div class="mini-info">
<div class="mini-name">{{ callerInfo.name }}</div>
<div class="mini-status">{{ formattedDuration }}</div>
</div>
<!-- 视频通话显示对方视频流 -->
<template v-else>
<div class="video-call-mini">
<div class="remote-video-preview">
<video
v-if="webrtc.remoteStream.value"
ref="remoteVideoPreview"
class="remote-video"
autoplay
playsinline
:srcObject="webrtc.remoteStream.value"
></video>
<div v-else class="video-placeholder">
<div class="placeholder-avatar" :style="{ background: callerInfo.color }">
{{ callerInfo.name.charAt(0) }}
</div>
</div>
</div>
<div class="mini-info">
<div class="mini-name">{{ callerInfo.name }}</div>
<div class="mini-timer">{{ formattedDuration }}</div>
</div>
<button class="mini-expand-btn" @click="toggleMinimize" @mousedown.stop>
<i class="fas fa-expand"></i>
</button>
</div>
</template>
</div>
<!-- 视频区域 -->
@@ -88,11 +113,12 @@
<!-- 远程视频 -->
<div class="remote-video-container">
<video
v-if="hasRemoteVideo"
v-if="webrtc.remoteStream.value"
ref="remoteVideo"
class="remote-video"
autoplay
playsinline
:srcObject="webrtc.remoteStream.value"
></video>
<div v-else class="video-placeholder">
<div class="placeholder-avatar" :style="{ background: callerInfo.color }">
@@ -106,7 +132,7 @@
<!-- 本地视频 -->
<div
class="local-video-container"
v-if="callType === 'video' && showLocalVideo"
v-if="callType === 'video' && webrtc.localStream.value"
>
<video
ref="localVideo"
@@ -114,6 +140,7 @@
autoplay
playsinline
muted
:srcObject="webrtc.localStream.value"
></video>
</div>
<div
@@ -124,6 +151,12 @@
{{ userStore.currentUser?.name?.charAt(0) || 'U' }}
</div>
</div>
<!-- 通话计时器显示 -->
<div v-if="callStatus === 'ongoing'" class="call-timer-display">
<i class="fas fa-clock"></i>
<span>{{ formattedDuration }}</span>
</div>
</div>
<!-- 控制栏 -->
@@ -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();
});
</script>
@@ -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;

View File

@@ -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,
}
}

View File

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