Files
nl-im-pc/src/composables/useWebRTC.js

429 lines
15 KiB
JavaScript
Raw Normal View History

2025-07-04 12:56:18 +08:00
import { ref } from "vue"
import { useChatStore } from "@/stores/chat"
import { useUserStore } from "@/stores/user"
import { callAPI } from "@/utils/request.js"
2025-07-04 12:56:18 +08:00
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()
2025-07-04 12:56:18 +08:00
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)
}
2025-07-04 12:56:18 +08:00
}
// 重连相关变量
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
2025-07-09 08:18:59 +08:00
if (peerConnection.value) {
console.log("🔄 关闭现有连接")
peerConnection.value.close()
2025-07-09 08:18:59 +08:00
}
const pc = new RTCPeerConnection(configuration)
peerConnection.value = pc
2025-07-04 12:56:18 +08:00
// 处理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) => {
2025-07-04 12:56:18 +08:00
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候选收集完成")
2025-07-04 12:56:18 +08:00
}
}
// 接收远程流 - 修复轨道处理逻辑
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("⏭️ 轨道已存在,跳过添加")
}
2025-07-04 12:56:18 +08:00
}
// 连接状态变化
pc.onconnectionstatechange = () => {
const state = pc.connectionState
console.log("🔗 连接状态:", state)
2025-07-04 12:56:18 +08:00
if (state === "connected") {
isConnected.value = true
isConnecting.value = false
reconnectAttempts = 0 // 重置重连计数器
clearTimeout(reconnectTimer)
if (chatStore.callStatus === "connecting") {
chatStore.callStatus = "ongoing"
chatStore.callConnectionStatus = "通话已连接"
}
// 连接成功后处理候选队列
processCandidateQueue()
2025-07-09 08:18:59 +08:00
} else if (state === "connecting") {
isConnecting.value = true
} else if (state === "disconnected" || state === "failed") {
isConnected.value = false
isConnecting.value = false
2025-07-09 08:18:59 +08:00
}
}
// 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
2025-07-04 12:56:18 +08:00
}
// 重启连接
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")
2025-07-04 12:56:18 +08:00
if (!peerConnection.value) {
createPeerConnection(callId, peerId)
2025-07-04 12:56:18 +08:00
}
2025-07-09 08:18:59 +08:00
try {
const offer = await peerConnection.value.createOffer()
2025-07-09 08:18:59 +08:00
await peerConnection.value.setLocalDescription(offer)
console.log("✅ Offer创建成功:", offer.type)
console.log("📋 SDP:", offer.sdp.substring(0, 100) + "...")
2025-07-04 12:56:18 +08:00
2025-07-09 08:18:59 +08:00
return offer
} catch (error) {
console.error("❌ 创建Offer失败:", error)
2025-07-09 08:18:59 +08:00
throw error
}
2025-07-04 12:56:18 +08:00
}
// 创建Answer
const createAnswer = async (offer, callId, peerId) => {
console.log("📥 处理Offer并创建Answer")
2025-07-04 12:56:18 +08:00
if (!peerConnection.value) {
createPeerConnection(callId, peerId)
2025-07-04 12:56:18 +08:00
}
2025-07-09 08:18:59 +08:00
try {
await peerConnection.value.setRemoteDescription(offer)
2025-07-09 08:18:59 +08:00
const answer = await peerConnection.value.createAnswer()
await peerConnection.value.setLocalDescription(answer)
console.log("✅ Answer创建成功:", answer.type)
console.log("📋 SDP:", answer.sdp.substring(0, 100) + "...")
2025-07-09 08:18:59 +08:00
return answer
} catch (error) {
console.error("❌ 创建Answer失败:", error)
2025-07-09 08:18:59 +08:00
throw error
}
2025-07-04 12:56:18 +08:00
}
// 设置远程Answer
2025-07-04 12:56:18 +08:00
const setRemoteAnswer = async (answer) => {
console.log("📥 设置远程Answer")
2025-07-09 08:18:59 +08:00
try {
await peerConnection.value.setRemoteDescription(answer)
console.log("✅ 远程Answer设置成功")
2025-07-09 08:18:59 +08:00
} catch (error) {
console.error("❌ 设置远程Answer失败:", error)
2025-07-09 08:18:59 +08:00
throw error
}
2025-07-04 12:56:18 +08:00
}
// 添加ICE候选
2025-07-04 12:56:18 +08:00
const addIceCandidate = async (candidate) => {
if (!peerConnection.value) {
console.log("⏳ 连接尚未建立,将候选加入队列")
iceCandidateQueue.value.push(candidate)
return
}
if (peerConnection.value.remoteDescription) {
2025-07-05 16:09:14 +08:00
try {
await peerConnection.value.addIceCandidate(candidate)
console.log("✅ ICE候选添加成功")
2025-07-05 16:09:14 +08:00
} catch (error) {
console.error("❌ 添加ICE候选失败:", error)
2025-07-05 16:09:14 +08:00
}
} else {
console.log("⏳ 等待远程描述,将候选加入队列")
iceCandidateQueue.value.push(candidate)
2025-07-04 12:56:18 +08:00
}
}
// 切换摄像头
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 ? "开启" : "关闭")
}
}
// 关闭连接 - 彻底释放资源
2025-07-04 12:56:18 +08:00
const closeConnection = () => {
console.log("🔄 关闭WebRTC连接")
2025-07-09 08:18:59 +08:00
// 停止所有媒体轨道并释放设备
2025-07-04 12:56:18 +08:00
if (localStream.value) {
console.log("📹 停止本地流轨道")
localStream.value.getTracks().forEach(track => {
console.log(`⏹ 停止轨道: ${track.kind} (${track.id})`)
track.stop() // 停止轨道以释放设备
track.enabled = false
})
2025-07-09 08:18:59 +08:00
localStream.value = null
2025-07-04 12:56:18 +08:00
}
// 清除远程流
if (remoteStream.value) {
console.log("📹 清除远程流")
remoteStream.value.getTracks().forEach(track => {
console.log(`⏹ 停止远程轨道: ${track.kind} (${track.id})`)
track.stop()
})
remoteStream.value = null
}
// 关闭对等连接
2025-07-04 12:56:18 +08:00
if (peerConnection.value) {
console.log("🔌 关闭对等连接")
2025-07-09 08:18:59 +08:00
peerConnection.value.close()
peerConnection.value = null
2025-07-04 12:56:18 +08:00
}
// 清理重连计时器
if (reconnectTimer) {
console.log("⏲ 清除重连计时器")
clearTimeout(reconnectTimer)
reconnectTimer = null
}
// 重置状态
2025-07-09 08:18:59 +08:00
isConnected.value = false
isConnecting.value = false
reconnectAttempts = 0
currentCallId = null
currentPeerId = null
// 清空候选队列
iceCandidateQueue.value = []
2025-07-04 12:56:18 +08:00
}
return {
localStream,
remoteStream,
peerConnection,
isConnected,
isConnecting,
getLocalMedia,
createPeerConnection,
2025-07-04 12:56:18 +08:00
createOffer,
createAnswer,
setRemoteAnswer,
addIceCandidate,
toggleCamera,
toggleMicrophone,
2025-07-04 12:56:18 +08:00
closeConnection,
}
2025-07-09 08:18:59 +08:00
}