修复:

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

现有问题:
1. 需要在挂断后关闭麦克风和摄像头
2. 没有渲染远程流推送的视频
This commit is contained in:
2025-07-10 10:38:23 +08:00
parent 95ddd9925c
commit f7e31a88e0
2 changed files with 234 additions and 18 deletions

View File

@@ -45,13 +45,31 @@
<!-- 设备状态指示 -->
<div v-if="!isMinimized" class="device-status-container">
<div v-if="callType === 'video' && !isCameraWorking" class="status-alert">
<!-- 摄像头硬件检测 -->
<div v-if="callType === 'video' && !deviceStatus.hasCamera" class="status-alert">
<i class="fas fa-video-slash"></i> 未检测到摄像头
</div>
<!-- 麦克风硬件检测 -->
<div v-if="!deviceStatus.hasMicrophone" class="status-alert">
<i class="fas fa-microphone-slash"></i> 未检测到麦克风
</div>
<!-- 本地摄像头状态 -->
<div v-if="callType === 'video' && deviceStatus.hasCamera && !deviceStatus.cameraWorking"
class="status-alert">
<i class="fas fa-video-slash"></i> 摄像头未工作
</div>
<div v-if="!isMicWorking" class="status-alert">
<!-- 本地麦克风状态 -->
<div v-if="deviceStatus.hasMicrophone && !deviceStatus.microphoneWorking"
class="status-alert">
<i class="fas fa-microphone-slash"></i> 麦克风未工作
</div>
<div v-if="!hasRemoteVideo && callType === 'video' && callStatus === 'ongoing'" class="status-alert remote-camera-off">
<!-- 远端摄像头状态 -->
<div v-if="!hasRemoteVideo && callType === 'video' && callStatus === 'ongoing'"
class="status-alert remote-camera-off">
<i class="fas fa-video-slash"></i> 对方未开启摄像头
</div>
</div>
@@ -84,12 +102,12 @@
<div class="video-call-mini">
<div class="remote-video-preview">
<video
v-if="webrtc.remoteStream.value"
v-if="webrtc.remoteStream"
ref="remoteVideoPreview"
class="remote-video"
autoplay
playsinline
:srcObject="webrtc.remoteStream.value"
:srcObject="webrtc.remoteStream"
></video>
<div v-else class="video-placeholder">
<div class="placeholder-avatar" :style="{ background: callerInfo.color }">
@@ -113,12 +131,12 @@
<!-- 远程视频 -->
<div class="remote-video-container">
<video
v-if="webrtc.remoteStream.value"
v-if="webrtc.remoteStream"
ref="remoteVideo"
class="remote-video"
autoplay
playsinline
:srcObject="webrtc.remoteStream.value"
:srcObject="webrtc.remoteStream"
></video>
<div v-else class="video-placeholder">
<div class="placeholder-avatar" :style="{ background: callerInfo.color }">
@@ -282,6 +300,14 @@ const localVideo = ref(null);
const remoteVideo = ref(null);
const remoteVideoPreview = ref(null);
// 新增:设备状态检测
const deviceStatus = reactive({
hasCamera: false,
hasMicrophone: false,
cameraWorking: false,
microphoneWorking: false,
})
// 获取当前通话状态
const callStatus = computed(() => {
return chatStore.callStatus;
@@ -533,6 +559,37 @@ const handleWindowResize = () => {
position.y = Math.max(0, Math.min(position.y, window.innerHeight - height));
};
// 远端视频轨道检测
const hasRemoteVideo = computed(() => {
if (!webrtc.remoteStream.value) return false
const tracks = webrtc.remoteStream.value.getVideoTracks()
return tracks.length > 0 && tracks.some(t => t.readyState === 'live')
})
// 监听本地流变化
watch(() => webrtc.localStream.value, (stream) => {
if (!stream) return
// 更新设备工作状态
deviceStatus.cameraWorking = stream.getVideoTracks().length > 0 &&
stream.getVideoTracks().some(t => t.readyState === 'live')
deviceStatus.microphoneWorking = stream.getAudioTracks().length > 0 &&
stream.getAudioTracks().some(t => t.readyState === 'live')
})
// 初始化设备检测
const checkDevices = async () => {
try {
const devices = await navigator.mediaDevices.enumerateDevices()
deviceStatus.hasCamera = devices.some(d => d.kind === 'videoinput' && d.deviceId !== '')
deviceStatus.hasMicrophone = devices.some(d => d.kind === 'audioinput' && d.deviceId !== '')
console.log("📷 设备检测结果:", deviceStatus)
} catch (error) {
console.error("❌ 设备检测失败:", error)
}
}
// 监听通话状态
watch(() => chatStore.callStatus, (status, oldStatus) => {
console.log("📞 VideoCallComponent 监听到通话状态变化:", oldStatus, "->", status);
@@ -597,6 +654,9 @@ onMounted(() => {
window.addEventListener('resize', handleWindowResize);
// 初始化设备检测
checkDevices();
console.log("🚀 VideoCallComponent mounted, callStatus:", chatStore.callStatus, "isIncoming:", props.isIncoming);
if (['calling', 'connecting', 'ongoing'].includes(chatStore.callStatus)) {
@@ -621,6 +681,7 @@ onUnmounted(() => {
</script>
<style scoped>
/* 样式保持不变 */
.video-call-container {
position: fixed;
z-index: 9999;
@@ -1023,6 +1084,7 @@ onUnmounted(() => {
justify-content: center;
gap: 16px;
height: 60px;
/*margin-top: 60px;*/
}
.control-btn {

View File

@@ -14,14 +14,49 @@ export function useWebRTC() {
const chatStore = useChatStore()
const configuration = {
iceServers: [{ urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }],
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" }
],
iceTransportPolicy: "all",
reconnectPolicy: {
maxAttempts: 3, // 最大重连次数
delay: 2000 // 重连延迟(ms)
}
}
// 重连相关变量
let reconnectAttempts = 0
let reconnectTimer = null
let currentCallId = null
let currentPeerId = null
// 收集ICE候选的队列
const iceCandidateQueue = ref([])
// 获取本地媒体流
const getLocalMedia = async (constraints = { audio: true, video: true }) => {
try {
console.log("🎥 开始获取本地媒体流:", constraints)
// 检测设备可用性
try {
const devices = await navigator.mediaDevices.enumerateDevices()
const hasVideo = devices.some(d => d.kind === 'videoinput' && d.deviceId !== '')
const hasAudio = devices.some(d => d.kind === 'audioinput' && d.deviceId !== '')
if (constraints.video && !hasVideo) {
console.warn("❌ 摄像头设备不可用")
constraints.video = false
}
if (constraints.audio && !hasAudio) {
console.warn("❌ 麦克风设备不可用")
constraints.audio = false
}
} catch (deviceError) {
console.warn("设备检测失败,继续尝试获取媒体:", deviceError)
}
// 如果已有流,先停止
if (localStream.value) {
localStream.value.getTracks().forEach((track) => track.stop())
@@ -64,6 +99,10 @@ export function useWebRTC() {
const createPeerConnection = (callId, peerId) => {
console.log("🔗 创建PeerConnection:", { callId, peerId })
// 保存当前通话信息用于重连
currentCallId = callId
currentPeerId = peerId
if (peerConnection.value) {
console.log("🔄 关闭现有连接")
peerConnection.value.close()
@@ -72,23 +111,48 @@ export function useWebRTC() {
const pc = new RTCPeerConnection(configuration)
peerConnection.value = pc
// 处理ICE候选队列
const processCandidateQueue = async () => {
console.log(`🔄 处理候选队列 (${iceCandidateQueue.value.length} 项)`)
while (iceCandidateQueue.value.length > 0) {
const candidate = iceCandidateQueue.value.shift()
try {
await pc.addIceCandidate(candidate)
console.log("✅ 从队列添加ICE候选成功")
} catch (error) {
console.error("❌ 添加队列ICE候选失败:", error)
}
}
}
// ICE候选事件
pc.onicecandidate = async (event) => {
if (event.candidate) {
console.log("📡 发送ICE候选")
console.log("📡 发送ICE候选:", event.candidate)
try {
await callAPI.sendCandidate(userStore.currentUser?.id, peerId, callId, event.candidate, 6)
} catch (error) {
console.error("❌ 发送ICE候选失败:", error)
}
} else {
console.log("❄️ ICE候选收集完成")
}
}
// 接收远程流
// 接收远程流 - 修复开始
pc.ontrack = (event) => {
console.log("📹 接收到远程:", event.streams[0])
remoteStream.value = event.streams[0]
console.log("📹 接收到远程轨道:", event.track.kind)
// 如果还没有远程流创建一个新的MediaStream
if (!remoteStream.value) {
remoteStream.value = new MediaStream()
}
// 将轨道添加到流中
remoteStream.value.addTrack(event.track)
console.log("✅ 远程流已更新,轨道数:", remoteStream.value.getTracks().length)
}
// 修复结束
// 连接状态变化
pc.onconnectionstatechange = () => {
@@ -98,10 +162,15 @@ export function useWebRTC() {
if (state === "connected") {
isConnected.value = true
isConnecting.value = false
reconnectAttempts = 0 // 重置重连计数器
clearTimeout(reconnectTimer)
if (chatStore.callStatus === "connecting") {
chatStore.callStatus = "ongoing"
chatStore.callConnectionStatus = "通话已连接"
}
// 连接成功后处理候选队列
processCandidateQueue()
} else if (state === "connecting") {
isConnecting.value = true
} else if (state === "disconnected" || state === "failed") {
@@ -110,6 +179,41 @@ export function useWebRTC() {
}
}
// ICE连接状态变化
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState
console.log("❄️ ICE连接状态:", state)
if (state === "failed") {
if (reconnectAttempts < configuration.reconnectPolicy.maxAttempts) {
reconnectAttempts++
console.warn(`⚠️ 连接失败,尝试重连 (${reconnectAttempts}/${configuration.reconnectPolicy.maxAttempts})`)
chatStore.callConnectionStatus = `连接失败,正在尝试重连 (${reconnectAttempts}/${configuration.reconnectPolicy.maxAttempts})`
reconnectTimer = setTimeout(() => {
restartConnection()
}, configuration.reconnectPolicy.delay)
} else {
console.error("❌ 重连次数已达上限,放弃连接")
chatStore.callStatus = "disconnected"
chatStore.callConnectionStatus = "连接失败,请稍后再试"
}
} else if (state === "connected") {
reconnectAttempts = 0
clearTimeout(reconnectTimer)
}
}
// ICE收集状态变化
pc.onicegatheringstatechange = () => {
console.log("❄️ ICE收集状态:", pc.iceGatheringState)
}
// 信令状态变化
pc.onsignalingstatechange = () => {
console.log("📶 信令状态:", pc.signalingState)
}
// 添加本地流到连接
if (localStream.value) {
console.log(" 添加本地流到PeerConnection")
@@ -121,6 +225,30 @@ export function useWebRTC() {
return pc
}
// 重启连接
const restartConnection = () => {
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)
}
}
// 创建Offer
const createOffer = async (callId, peerId) => {
console.log("📤 创建Offer")
@@ -180,13 +308,22 @@ export function useWebRTC() {
// 添加ICE候选
const addIceCandidate = async (candidate) => {
if (peerConnection.value && peerConnection.value.remoteDescription) {
if (!peerConnection.value) {
console.log("⏳ 连接尚未建立,将候选加入队列")
iceCandidateQueue.value.push(candidate)
return
}
if (peerConnection.value.remoteDescription) {
try {
await peerConnection.value.addIceCandidate(candidate)
console.log("✅ ICE候选添加成功")
} catch (error) {
console.error("❌ 添加ICE候选失败:", error)
}
} else {
console.log("⏳ 等待远程描述,将候选加入队列")
iceCandidateQueue.value.push(candidate)
}
}
@@ -216,22 +353,39 @@ export function useWebRTC() {
const closeConnection = () => {
console.log("🔄 关闭WebRTC连接")
// 停止所有媒体轨道
if (localStream.value) {
localStream.value.getTracks().forEach((track) => track.stop())
localStream.value.getTracks().forEach(track => {
track.stop()
track.enabled = false
})
localStream.value = null
}
if (remoteStream.value) {
remoteStream.value = null
}
// 关闭对等连接
if (peerConnection.value) {
peerConnection.value.close()
peerConnection.value = null
}
// 清理重连计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
// 重置状态
isConnected.value = false
isConnecting.value = false
reconnectAttempts = 0
currentCallId = null
currentPeerId = null
// 清空候选队列
iceCandidateQueue.value = []
// 清除远程流
remoteStream.value = null
}
return {