修复:
1. 本地视频流能获取到了 2. 找到了视频连接错误的原因:挂断后本地麦克风和视频并没有关闭 现有问题: 1. 需要在挂断后关闭麦克风和摄像头 2. 没有渲染远程流推送的视频
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user