1. 本地视频流能获取到了 2. 找到了视频连接错误的原因:挂断后本地麦克风和视频并没有关闭 现有问题: 1. 需要在挂断后关闭麦克风和摄像头 2. 没有渲染远程流推送的视频
429 lines
15 KiB
JavaScript
429 lines
15 KiB
JavaScript
import { ref } from "vue"
|
||
import { useChatStore } from "@/stores/chat"
|
||
import { useUserStore } from "@/stores/user"
|
||
import { callAPI } from "@/utils/request.js"
|
||
|
||
export function useWebRTC() {
|
||
const localStream = ref(null)
|
||
const remoteStream = ref(null)
|
||
const peerConnection = ref(null)
|
||
const isConnected = ref(false)
|
||
const isConnecting = 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" }
|
||
],
|
||
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())
|
||
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 })
|
||
|
||
// 保存当前通话信息用于重连
|
||
currentCallId = callId
|
||
currentPeerId = peerId
|
||
|
||
if (peerConnection.value) {
|
||
console.log("🔄 关闭现有连接")
|
||
peerConnection.value.close()
|
||
}
|
||
|
||
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候选:", 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.track.kind, "id:", event.track.id)
|
||
|
||
// 如果还没有远程流,创建一个新的MediaStream
|
||
if (!remoteStream.value) {
|
||
console.log("🆕 创建新的远程流")
|
||
remoteStream.value = new MediaStream()
|
||
}
|
||
|
||
// 确保轨道尚未添加
|
||
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)
|
||
} else {
|
||
console.log("⏭️ 轨道已存在,跳过添加")
|
||
}
|
||
}
|
||
|
||
// 连接状态变化
|
||
pc.onconnectionstatechange = () => {
|
||
const state = pc.connectionState
|
||
console.log("🔗 连接状态:", state)
|
||
|
||
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") {
|
||
isConnected.value = false
|
||
isConnecting.value = false
|
||
}
|
||
}
|
||
|
||
// 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")
|
||
localStream.value.getTracks().forEach((track) => {
|
||
// 确保只添加活跃的轨道
|
||
if (track.readyState === 'live') {
|
||
pc.addTrack(track, localStream.value)
|
||
console.log(`✅ 添加 ${track.kind} 轨道到连接`)
|
||
}
|
||
})
|
||
}
|
||
|
||
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")
|
||
|
||
if (!peerConnection.value) {
|
||
createPeerConnection(callId, peerId)
|
||
}
|
||
|
||
try {
|
||
const offer = await peerConnection.value.createOffer()
|
||
await peerConnection.value.setLocalDescription(offer)
|
||
|
||
console.log("✅ Offer创建成功:", offer.type)
|
||
console.log("📋 SDP:", offer.sdp.substring(0, 100) + "...")
|
||
|
||
return offer
|
||
} catch (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 {
|
||
await peerConnection.value.setRemoteDescription(offer)
|
||
const answer = await peerConnection.value.createAnswer()
|
||
await peerConnection.value.setLocalDescription(answer)
|
||
|
||
console.log("✅ Answer创建成功:", answer.type)
|
||
console.log("📋 SDP:", answer.sdp.substring(0, 100) + "...")
|
||
|
||
return answer
|
||
} catch (error) {
|
||
console.error("❌ 创建Answer失败:", error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 设置远程Answer
|
||
const setRemoteAnswer = async (answer) => {
|
||
console.log("📥 设置远程Answer")
|
||
try {
|
||
await peerConnection.value.setRemoteDescription(answer)
|
||
console.log("✅ 远程Answer设置成功")
|
||
} catch (error) {
|
||
console.error("❌ 设置远程Answer失败:", error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 添加ICE候选
|
||
const addIceCandidate = async (candidate) => {
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 切换摄像头
|
||
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连接")
|
||
|
||
// 停止所有媒体轨道并释放设备
|
||
if (localStream.value) {
|
||
console.log("📹 停止本地流轨道")
|
||
localStream.value.getTracks().forEach(track => {
|
||
console.log(`⏹ 停止轨道: ${track.kind} (${track.id})`)
|
||
track.stop() // 停止轨道以释放设备
|
||
track.enabled = false
|
||
})
|
||
localStream.value = null
|
||
}
|
||
|
||
// 清除远程流
|
||
if (remoteStream.value) {
|
||
console.log("📹 清除远程流")
|
||
remoteStream.value.getTracks().forEach(track => {
|
||
console.log(`⏹ 停止远程轨道: ${track.kind} (${track.id})`)
|
||
track.stop()
|
||
})
|
||
remoteStream.value = null
|
||
}
|
||
|
||
// 关闭对等连接
|
||
if (peerConnection.value) {
|
||
console.log("🔌 关闭对等连接")
|
||
peerConnection.value.close()
|
||
peerConnection.value = null
|
||
}
|
||
|
||
// 清理重连计时器
|
||
if (reconnectTimer) {
|
||
console.log("⏲ 清除重连计时器")
|
||
clearTimeout(reconnectTimer)
|
||
reconnectTimer = null
|
||
}
|
||
|
||
// 重置状态
|
||
isConnected.value = false
|
||
isConnecting.value = false
|
||
reconnectAttempts = 0
|
||
currentCallId = null
|
||
currentPeerId = null
|
||
|
||
// 清空候选队列
|
||
iceCandidateQueue.value = []
|
||
}
|
||
|
||
return {
|
||
localStream,
|
||
remoteStream,
|
||
peerConnection,
|
||
isConnected,
|
||
isConnecting,
|
||
getLocalMedia,
|
||
createPeerConnection,
|
||
createOffer,
|
||
createAnswer,
|
||
setRemoteAnswer,
|
||
addIceCandidate,
|
||
toggleCamera,
|
||
toggleMicrophone,
|
||
closeConnection,
|
||
}
|
||
}
|