Files
nl-im-pc/src/composables/useWebRTC.js
liqi 355a26051d 修复:
1. 本地视频流能获取到了
2. 找到了视频连接错误的原因:挂断后本地麦克风和视频并没有关闭

现有问题:
1. 需要在挂断后关闭麦克风和摄像头
2. 没有渲染远程流推送的视频
2025-07-11 08:15:38 +08:00

429 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}