修复:

1. 本地视频流能获取到了
2. 找到了视频连接错误的原因:挂断后本地麦克风和视频并没有关闭

现有问题:
1. 需要在挂断后关闭麦克风和摄像头
2. 没有渲染远程流推送的视频
This commit is contained in:
2025-07-14 11:17:41 +08:00
parent 355a26051d
commit 32e676e320
4 changed files with 383 additions and 125 deletions

View File

@@ -399,7 +399,7 @@ const containerStyle = computed(() => {
};
});
// 初始化媒体设备
// 初始化媒体设备 - 优化版本
const initializeMedia = async () => {
try {
console.log("🎥 初始化媒体设备,当前状态:", callStatus.value);
@@ -414,13 +414,26 @@ const initializeMedia = async () => {
console.log("🎬 获取媒体约束:", constraints);
await webrtc.getLocalMedia(constraints);
console.log("✅ 媒体获取成功");
// 更新设备工作状态
if (webrtc.localStream.value) {
deviceStatus.cameraWorking = webrtc.localStream.value.getVideoTracks().length > 0 &&
webrtc.localStream.value.getVideoTracks().some(t => t.readyState === 'live');
deviceStatus.microphoneWorking = webrtc.localStream.value.getAudioTracks().length > 0 &&
webrtc.localStream.value.getAudioTracks().some(t => t.readyState === 'live');
}
console.log("✅ 媒体获取成功", {
camera: deviceStatus.cameraWorking,
microphone: deviceStatus.microphoneWorking
});
} else {
console.log("⏭️ 当前状态不需要获取媒体:", callStatus.value);
}
} catch (error) {
console.warn('⚠️ 无法访问媒体设备:', error);
console.error(' 无法访问媒体设备:', error);
showFriendlyNotification('媒体设备访问失败,请检查权限设置', 3000);
}
};
@@ -459,9 +472,16 @@ const toggleMinimize = () => {
position.y = Math.max(0, Math.min(position.y, window.innerHeight - height));
};
// 修复1确保在结束通话前关闭WebRTC连接
const endCall = () => {
// 先关闭WebRTC连接
webrtc.closeConnection();
// 再停止计时器和更新UI
stopCallTimer();
isVisible.value = false;
// 更新状态
chatStore.endCall();
emit('end-call');
};
@@ -503,6 +523,10 @@ const rejectCall = () => {
}
console.log("❌ 用户点击拒绝按钮")
// 修复2拒绝通话时也关闭WebRTC连接
webrtc.closeConnection();
isVisible.value = false;
chatStore.rejectCall();
emit('reject-call');
@@ -566,17 +590,33 @@ const hasRemoteVideo = computed(() => {
return tracks.length > 0 && tracks.some(t => t.readyState === 'live')
})
// 监听本地流变化
watch(() => webrtc.localStream.value, (stream) => {
if (!stream) return
// 监听本地流变化 - 优化版本
watch(() => webrtc.localStream.value, (stream, oldStream) => {
console.log('🔄 本地流变化:', { newStream: !!stream, oldStream: !!oldStream });
// 更新设备工作状态
deviceStatus.cameraWorking = stream.getVideoTracks().length > 0 &&
stream.getVideoTracks().some(t => t.readyState === 'live')
if (stream) {
// 更新设备工作状态
const videoTracks = stream.getVideoTracks();
const audioTracks = stream.getAudioTracks();
deviceStatus.microphoneWorking = stream.getAudioTracks().length > 0 &&
stream.getAudioTracks().some(t => t.readyState === 'live')
})
deviceStatus.cameraWorking = videoTracks.length > 0 &&
videoTracks.some(t => t.readyState === 'live');
deviceStatus.microphoneWorking = audioTracks.length > 0 &&
audioTracks.some(t => t.readyState === 'live');
console.log('📱 设备状态更新:', {
camera: deviceStatus.cameraWorking,
microphone: deviceStatus.microphoneWorking,
videoTracks: videoTracks.length,
audioTracks: audioTracks.length
});
} else {
deviceStatus.cameraWorking = false;
deviceStatus.microphoneWorking = false;
console.log('📱 设备状态重置: 无媒体流');
}
}, { immediate: true })
// 初始化设备检测
const checkDevices = async () => {
@@ -590,46 +630,78 @@ const checkDevices = async () => {
}
}
// 监听通话状态
watch(() => chatStore.callStatus, (status, oldStatus) => {
// 监听通话状态 - 优化版本
watch(() => chatStore.callStatus, async (status, oldStatus) => {
console.log("📞 VideoCallComponent 监听到通话状态变化:", oldStatus, "->", status);
if (status === "calling") {
console.log("📞 发起通话,初始化媒体设备")
initializeMedia();
} else if (status === "ongoing") {
if (!callTimer.value) {
console.log("⏰ 开始通话计时")
startCallTimer();
try {
switch (status) {
case "calling":
case "connecting":
console.log("📞 发起通话,初始化媒体设备")
await initializeMedia();
if (status === "connecting") {
showFriendlyNotification("正在建立连接...", 2000);
}
break;
case "ongoing":
// 确保媒体流正常
if (!webrtc.localStream.value) {
console.warn('⚠️ 通话进行中但无本地流,重新初始化');
await initializeMedia();
}
if (!callTimer.value) {
console.log("⏰ 开始通话计时")
startCallTimer();
}
showFriendlyNotification("通话已连接", 2000);
break;
case "ended":
case "idle":
console.log("📞 通话结束,准备关闭组件");
stopCallTimer();
// 延迟关闭以显示状态信息
setTimeout(() => {
if (chatStore.callStatus === status) {
endCall();
}
}, 1000);
break;
case "failed":
console.log("❌ 通话失败")
showFriendlyNotification("连接失败", 3000);
setTimeout(() => {
if (chatStore.callStatus === status) {
endCall();
}
}, 3000);
break;
case "hangup":
console.log("📞 对方已挂断3秒后关闭组件")
showFriendlyNotification("对方已挂断", 3000);
setTimeout(() => {
if (chatStore.callStatus === status) {
endCall();
}
}, 3000);
break;
case "disconnected":
console.log("🌐 网络连接已断开3秒后关闭组件")
showFriendlyNotification("网络连接已断开", 3000);
setTimeout(() => {
if (chatStore.callStatus === status) {
endCall();
}
}, 3000);
break;
}
showFriendlyNotification("通话已连接", 2000);
} else if (status === "ended" || status === "idle") {
console.log("📞 通话结束,准备关闭组件");
stopCallTimer();
setTimeout(() => {
endCall();
}, 100);
} else if (status === "connecting") {
console.log("🔗 通话连接中")
showFriendlyNotification("正在建立连接...", 2000);
} else if (status === "failed") {
console.log("❌ 通话失败")
showFriendlyNotification("连接失败", 3000);
setTimeout(() => {
endCall();
}, 3000);
} else if (status === "hangup") {
console.log("📞 对方已挂断3秒后关闭组件")
showFriendlyNotification("对方已挂断", 3000);
setTimeout(() => {
endCall();
}, 3000);
} else if (status === "disconnected") {
console.log("🌐 网络连接已断开3秒后关闭组件")
showFriendlyNotification("网络连接已断开", 3000);
setTimeout(() => {
endCall();
}, 3000);
} catch (error) {
console.error('❌ 处理通话状态变化失败:', error);
}
});
@@ -648,35 +720,61 @@ watch(() => chatStore.callConnectionStatus, (newStatus, oldStatus) => {
}
});
onMounted(() => {
position.x = Math.max(0, (window.innerWidth - 500) / 2);
position.y = Math.max(0, (window.innerHeight - 380) / 2);
onMounted(async () => {
console.log('🚀 VideoCallComponent 组件挂载');
window.addEventListener('resize', handleWindowResize);
try {
// 初始化组件位置
position.x = Math.max(0, (window.innerWidth - 500) / 2);
position.y = Math.max(0, (window.innerHeight - 380) / 2);
console.log('📍 组件位置初始化:', position);
// 初始化设备检测
checkDevices();
// 添加窗口大小变化监听
window.addEventListener('resize', handleWindowResize);
console.log("🚀 VideoCallComponent mounted, callStatus:", chatStore.callStatus, "isIncoming:", props.isIncoming);
// 初始化设备检测
await checkDevices();
if (['calling', 'connecting', 'ongoing'].includes(chatStore.callStatus)) {
initializeMedia();
console.log("🚀 VideoCallComponent mounted, callStatus:", chatStore.callStatus, "isIncoming:", props.isIncoming);
// 如果当前状态需要媒体,立即初始化
if (['calling', 'connecting', 'ongoing'].includes(chatStore.callStatus)) {
console.log('🎥 组件挂载时初始化媒体');
await initializeMedia();
}
console.log('✅ VideoCallComponent 组件挂载完成');
} catch (error) {
console.error('❌ VideoCallComponent 组件挂载失败:', error);
showFriendlyNotification('组件初始化失败,请刷新页面重试', 3000);
}
});
onUnmounted(() => {
console.log("🔚 VideoCallComponent unmounted");
console.log('🔄 VideoCallComponent 组件卸载');
stopCallTimer();
if (friendlyMessageTimer.value) {
clearTimeout(friendlyMessageTimer.value);
try {
// 修复3确保组件卸载时关闭WebRTC连接
webrtc.closeConnection();
// 停止计时器
stopCallTimer();
if (friendlyMessageTimer.value) {
clearTimeout(friendlyMessageTimer.value);
friendlyMessageTimer.value = null;
}
console.log('⏲ 计时器已清除');
// 清除事件监听
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', stopDrag);
window.removeEventListener('resize', handleWindowResize);
console.log('🎧 事件监听器已清除');
console.log('✅ VideoCallComponent 组件卸载完成');
} catch (error) {
console.error('❌ VideoCallComponent 组件卸载时出错:', error);
}
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', stopDrag);
window.removeEventListener('resize', handleWindowResize);
webrtc.closeConnection();
});
</script>
@@ -1162,7 +1260,7 @@ onUnmounted(() => {
justify-content: center;
font-size: 14px;
cursor: pointer;
border: none;
border: null;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

View File

@@ -9,14 +9,16 @@ export function useWebRTC() {
const peerConnection = ref(null)
const isConnected = ref(false)
const isConnecting = ref(false)
const mediaStreamManager = ref(null) // 媒体流管理器
const lastError = ref(null) // 最后一个错误
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:stun.l.google.com:19302"},
{urls: "stun:stun1.l.google.com:19302"}
],
iceTransportPolicy: "all",
reconnectPolicy: {
@@ -30,16 +32,37 @@ export function useWebRTC() {
let reconnectTimer = null
let currentCallId = null
let currentPeerId = null
let isGettingMedia = false // 防止重复获取媒体流
// 收集ICE候选的队列
const iceCandidateQueue = ref([])
// 获取本地媒体流
const getLocalMedia = async (constraints = { audio: true, video: true }) => {
// 获取本地媒体流 - 优化版本
const getLocalMedia = async (constraints = {audio: true, video: true}) => {
// 防止重复获取
if (isGettingMedia) {
console.log("⏳ 正在获取媒体流,跳过重复请求")
return localStream.value
}
// 如果已有相同约束的流,直接返回
if (localStream.value && mediaStreamManager.value) {
const currentConstraints = mediaStreamManager.value
if (currentConstraints.audio === constraints.audio &&
currentConstraints.video === constraints.video) {
console.log("✅ 使用现有媒体流")
return localStream.value
}
}
isGettingMedia = true
lastError.value = null
try {
console.log("🎥 开始获取本地媒体流:", constraints)
// 检测设备可用性
// 检测设备可用性和权限
let finalConstraints = {...constraints}
try {
const devices = await navigator.mediaDevices.enumerateDevices()
const hasVideo = devices.some(d => d.kind === 'videoinput' && d.deviceId !== '')
@@ -47,24 +70,53 @@ export function useWebRTC() {
if (constraints.video && !hasVideo) {
console.warn("❌ 摄像头设备不可用")
constraints.video = false
finalConstraints.video = false
}
if (constraints.audio && !hasAudio) {
console.warn("❌ 麦克风设备不可用")
constraints.audio = false
finalConstraints.audio = false
}
// 检查权限状态
if (navigator.permissions) {
try {
const cameraPermission = await navigator.permissions.query({name: 'camera'})
const micPermission = await navigator.permissions.query({name: 'microphone'})
if (finalConstraints.video && cameraPermission.state === 'denied') {
console.warn("❌ 摄像头权限被拒绝")
finalConstraints.video = false
}
if (finalConstraints.audio && micPermission.state === 'denied') {
console.warn("❌ 麦克风权限被拒绝")
finalConstraints.audio = false
}
} catch (permError) {
console.warn("权限检查失败:", permError)
}
}
} catch (deviceError) {
console.warn("设备检测失败,继续尝试获取媒体:", deviceError)
}
// 如果没有可用设备,抛出错误
if (!finalConstraints.audio && !finalConstraints.video) {
throw new Error('没有可用的音视频设备或权限被拒绝')
}
// 如果已有流,先停止
if (localStream.value) {
localStream.value.getTracks().forEach((track) => track.stop())
console.log("🔄 停止现有媒体流")
localStream.value.getTracks().forEach((track) => {
track.stop()
console.log(`⏹ 停止轨道: ${track.kind}`)
})
localStream.value = null
}
const stream = await navigator.mediaDevices.getUserMedia(constraints)
const stream = await navigator.mediaDevices.getUserMedia(finalConstraints)
localStream.value = stream
mediaStreamManager.value = finalConstraints
console.log("✅ 本地媒体流获取成功:", {
streamId: stream.id,
@@ -72,32 +124,63 @@ export function useWebRTC() {
audioTracks: stream.getAudioTracks().length,
videoEnabled: stream.getVideoTracks()[0]?.enabled,
audioEnabled: stream.getAudioTracks()[0]?.enabled,
constraints: finalConstraints
})
// 如果PeerConnection已存在添加新的轨道
if (peerConnection.value && peerConnection.value.connectionState !== 'closed') {
console.log(" 将新获取的本地流添加到现有PeerConnection")
stream.getTracks().forEach((track) => {
if (track.readyState === 'live') {
// 检查是否已经添加了相同类型的轨道
const existingSender = peerConnection.value.getSenders().find(sender =>
sender.track && sender.track.kind === track.kind
)
if (existingSender) {
// 替换现有轨道
console.log(`🔄 替换现有 ${track.kind} 轨道`)
existingSender.replaceTrack(track).catch(error => {
console.error(`❌ 替换 ${track.kind} 轨道失败:`, error)
})
} else {
// 添加新轨道
peerConnection.value.addTrack(track, stream)
console.log(`✅ 添加新的 ${track.kind} 轨道到PeerConnection`)
}
}
})
}
return stream
} catch (error) {
console.error("❌ 获取本地媒体流失败:", error)
lastError.value = error
// 如果视频失败,尝试只获取音频
if (constraints.video) {
if (constraints.video && !constraints.audio === false) {
try {
console.log("🎵 尝试仅获取音频流")
const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false })
const audioStream = await navigator.mediaDevices.getUserMedia({audio: true, video: false})
localStream.value = audioStream
mediaStreamManager.value = {audio: true, video: false}
console.log("✅ 音频流获取成功")
return audioStream
} catch (audioError) {
console.error("❌ 音频流也获取失败:", audioError)
lastError.value = audioError
}
}
throw error
} finally {
isGettingMedia = false
}
}
// 创建PeerConnection
const createPeerConnection = (callId, peerId) => {
console.log("🔗 创建PeerConnection:", { callId, peerId })
console.log("🔗 创建PeerConnection:", {callId, peerId})
// 保存当前通话信息用于重连
currentCallId = callId
@@ -141,24 +224,37 @@ export function useWebRTC() {
// 接收远程流 - 修复轨道处理逻辑
pc.ontrack = (event) => {
console.log("📹 接收到远程轨道:", event.track.kind, "id:", event.track.id)
console.log("📹 接收到远程轨道:", event.track.kind, "id:", event.track.id);
// 如果还没有远程流创建一个新的MediaStream
// 确保流对象存在
if (!remoteStream.value) {
console.log("🆕 创建新的远程流")
remoteStream.value = new MediaStream()
console.log("🆕 创建新的远程流");
remoteStream.value = new MediaStream();
}
// 确保轨道尚未添加
const existingTrack = remoteStream.value.getTracks().find(t => t.id === event.track.id)
// 检查轨道是否已存在
const existingTrack = remoteStream.value.getTracks().find(t => t.id === event.track.id);
if (!existingTrack) {
remoteStream.value.addTrack(event.track)
console.log("✅ 添加远程轨道成功,轨道数:", remoteStream.value.getTracks().length)
console.log("✅ 添加远程轨道成功:", remoteStream.value)
remoteStream.value.addTrack(event.track);
console.log("✅ 添加远程轨道成功,轨道数:", remoteStream.value.getTracks().length);
} else {
console.log("⏭️ 轨道已存在,跳过添加")
console.log("⏭️ 轨道已存在,跳过添加");
}
}
};
// 添加 onnegotiationneeded 和重启逻辑
pc.onnegotiationneeded = async () => {
try {
// 这里添加你的信令交换逻辑offer/answer
console.log("🚀 重新协商连接...");
} catch (err) {
console.error("协商失败:", err);
// 重启轨道
remoteStream.value.getTracks().forEach(track => track.stop());
remoteStream.value = null;
}
};
// 连接状态变化
pc.onconnectionstatechange = () => {
@@ -185,7 +281,7 @@ export function useWebRTC() {
}
}
// ICE连接状态变化
// ICE连接状态变化 - 优化版本
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState
console.log("❄️ ICE连接状态:", state)
@@ -196,8 +292,17 @@ export function useWebRTC() {
console.warn(`⚠️ 连接失败,尝试重连 (${reconnectAttempts}/${configuration.reconnectPolicy.maxAttempts})`)
chatStore.callConnectionStatus = `连接失败,正在尝试重连 (${reconnectAttempts}/${configuration.reconnectPolicy.maxAttempts})`
reconnectTimer = setTimeout(() => {
restartConnection()
// 清除之前的重连计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer)
}
reconnectTimer = setTimeout(async () => {
try {
await restartConnection()
} catch (error) {
console.error("❌ 自动重连失败:", error)
}
}, configuration.reconnectPolicy.delay)
} else {
console.error("❌ 重连次数已达上限,放弃连接")
@@ -227,7 +332,7 @@ export function useWebRTC() {
// 确保只添加活跃的轨道
if (track.readyState === 'live') {
pc.addTrack(track, localStream.value)
console.log(`✅ 添加 ${track.kind} 轨道到连接`)
console.log(`✅ 添加本地 ${track.kind} 轨道到连接`)
}
})
}
@@ -235,27 +340,58 @@ export function useWebRTC() {
return pc
}
// 重启连接
const restartConnection = () => {
// 重启连接 - 优化版本
const restartConnection = async () => {
if (!currentCallId || !currentPeerId) {
console.error("❌ 无法重启连接: 缺少callId或peerId")
return
}
console.log("🔄 尝试重启WebRTC连接")
closeConnection()
createPeerConnection(currentCallId, currentPeerId)
if (chatStore.isCaller) {
createOffer(currentCallId, currentPeerId).then(offer => {
// 重新发送offer逻辑
callAPI.sendOffer(userStore.currentUser?.id, currentPeerId, currentCallId, offer, 1)
console.log("📤 重新发送Offer")
})
} else {
// 被叫方请求对方重新发送offer
console.log("📤 请求对方重新发送Offer")
callAPI.requestResendOffer(currentCallId, currentPeerId)
try {
// 先关闭现有连接但保留媒体流
if (peerConnection.value) {
peerConnection.value.close()
peerConnection.value = null
}
// 清空候选队列
iceCandidateQueue.value = []
// 重新获取媒体流(如果需要)
if (!localStream.value || localStream.value.getTracks().length === 0) {
console.log("🎥 重连时重新获取媒体流")
const constraints = mediaStreamManager.value || {audio: true, video: true}
await getLocalMedia(constraints)
}
// 创建新的连接
createPeerConnection(currentCallId, currentPeerId)
if (chatStore.isCaller) {
try {
const offer = await createOffer(currentCallId, currentPeerId)
await callAPI.sendOffer(userStore.currentUser?.id, currentPeerId, currentCallId, offer, 1)
console.log("📤 重新发送Offer成功")
} catch (error) {
console.error("❌ 重新发送Offer失败:", error)
throw error
}
} else {
// 被叫方请求对方重新发送offer
console.log("📤 请求对方重新发送Offer")
try {
await callAPI.requestResendOffer(currentCallId, currentPeerId)
} catch (error) {
console.error("❌ 请求重新发送Offer失败:", error)
}
}
} catch (error) {
console.error("❌ 重启连接失败:", error)
// 如果重连失败,更新状态
chatStore.callStatus = "failed"
chatStore.callConnectionStatus = "重连失败,请稍后再试"
}
}
@@ -312,6 +448,7 @@ export function useWebRTC() {
console.log("✅ 远程Answer设置成功")
} catch (error) {
console.error("❌ 设置远程Answer失败:", error)
console.error("❌ 设置远程Answer失败-对象:", peerConnection.value)
throw error
}
}
@@ -359,27 +496,32 @@ export function useWebRTC() {
}
}
// 关闭连接 - 彻底释放资源
const closeConnection = () => {
console.log("🔄 关闭WebRTC连接")
// 关闭连接 - 彻底释放资源 - 优化版本
const closeConnection = (keepLocalStream = false) => {
console.log("🔄 关闭WebRTC连接", keepLocalStream ? "(保留本地流)" : "(完全关闭)")
// 停止所有媒体轨道并释放设备
// 完全重置状态
const fullReset = !keepLocalStream;
// 停止所有媒体轨道
if (localStream.value) {
console.log("📹 停止本地流轨道")
localStream.value.getTracks().forEach(track => {
console.log(`⏹ 停止轨道: ${track.kind} (${track.id})`)
track.stop() // 停止轨道以释放设备
track.stop() // 确保释放设备
track.enabled = false
})
localStream.value = null
if (fullReset) {
localStream.value = null
mediaStreamManager.value = null
}
}
// 清除远程流
if (remoteStream.value) {
console.log("📹 清除远程流")
remoteStream.value.getTracks().forEach(track => {
console.log(`⏹ 停止远程轨道: ${track.kind} (${track.id})`)
track.stop()
remoteStream.value.removeTrack(track)
})
remoteStream.value = null
}
@@ -387,7 +529,20 @@ export function useWebRTC() {
// 关闭对等连接
if (peerConnection.value) {
console.log("🔌 关闭对等连接")
peerConnection.value.close()
try {
// 移除所有事件监听器
pc.onicecandidate = null;
pc.ontrack = null;
pc.onnegotiationneeded = null;
pc.onconnectionstatechange = null;
pc.oniceconnectionstatechange = null;
pc.onicegatheringstatechange = null;
pc.onsignalingstatechange = null;
peerConnection.value.close()
} catch (error) {
console.warn("关闭连接时出错:", error)
}
peerConnection.value = null
}
@@ -401,12 +556,15 @@ export function useWebRTC() {
// 重置状态
isConnected.value = false
isConnecting.value = false
reconnectAttempts = 0
currentCallId = null
currentPeerId = null
// 清空候选队列
iceCandidateQueue.value = []
if (fullReset) {
reconnectAttempts = 0
currentCallId = null
currentPeerId = null
isGettingMedia = false
lastError.value = null
iceCandidateQueue.value = [] // 清空候选队列
}
}
return {

View File

@@ -33,6 +33,7 @@ export const useChatStore = defineStore("chat", () => {
const isDisconnected = computed(() => callStatus.value === "disconnected")
const isHangup = computed(() => callStatus.value === "hangup")
// 监听通话状态变化
watch(
() => callStatus.value,
@@ -920,6 +921,7 @@ export const useChatStore = defineStore("chat", () => {
// 接听来电
const acceptCall = async () => {
// if (isConnecting) return;
if (!incomingCall.value) {
console.log("没有来电,无法接听")
return