Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
905 lines
26 KiB
TypeScript
905 lines
26 KiB
TypeScript
import { ref } from 'vue';
|
||
|
||
import { useChatStore } from '../stores/chat';
|
||
import { useUserStore } from '../stores/user';
|
||
import { callAPI } from '../utils/request.ts';
|
||
|
||
export function useWebRTC() {
|
||
const localStream = ref(null);
|
||
const remoteStream = ref(null);
|
||
const peerConnection = ref(null);
|
||
const isConnected = ref(false);
|
||
const isConnecting = ref(false);
|
||
|
||
// ICE候选缓存和状态管理
|
||
const pendingIceCandidates = ref([]);
|
||
const isRemoteDescriptionSet = ref(false);
|
||
const connectionAttempts = ref(0);
|
||
const maxConnectionAttempts = 3;
|
||
|
||
// 修复问题1: 添加视频元素回调注册机制
|
||
const videoElementCallbacks = ref(new Set());
|
||
|
||
const userStore = useUserStore();
|
||
const chatStore = useChatStore();
|
||
|
||
// 改进ICE服务器配置
|
||
const configuration = {
|
||
iceServers: [
|
||
{ urls: 'stun:stun.l.google.com:19302' },
|
||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||
{ urls: 'stun:stun2.l.google.com:19302' },
|
||
{ urls: 'stun:stun3.l.google.com:19302' },
|
||
{ urls: 'stun:stun4.l.google.com:19302' },
|
||
],
|
||
iceTransportPolicy: 'all',
|
||
iceCandidatePoolSize: 10,
|
||
bundlePolicy: 'max-bundle',
|
||
rtcpMuxPolicy: 'require',
|
||
};
|
||
|
||
// 时间戳日志函数
|
||
const logWithTime = (level, message, ...args) => {
|
||
const timestamp = new Date().toLocaleTimeString('zh-CN', {
|
||
hour12: false,
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
fractionalSecondDigits: 3,
|
||
});
|
||
|
||
switch (level) {
|
||
case 'error': {
|
||
console.error(`[${timestamp}] ${message}`, ...args);
|
||
break;
|
||
}
|
||
case 'log': {
|
||
console.log(`[${timestamp}] ${message}`, ...args);
|
||
break;
|
||
}
|
||
case 'warn': {
|
||
console.warn(`[${timestamp}] ${message}`, ...args);
|
||
break;
|
||
}
|
||
default: {
|
||
console.log(`[${timestamp}] ${message}`, ...args);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 获取本地媒体流
|
||
const getLocalMedia = async (constraints = { audio: true, video: true }) => {
|
||
try {
|
||
logWithTime('log', '🎥 开始获取本地媒体流', constraints);
|
||
|
||
if (localStream.value) {
|
||
logWithTime('log', '🔄 停止现有本地媒体流');
|
||
localStream.value.getTracks().forEach((track) => {
|
||
track.stop();
|
||
logWithTime('log', `停止轨道: ${track.kind} - ${track.label}`);
|
||
});
|
||
localStream.value = null;
|
||
}
|
||
|
||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||
localStream.value = stream;
|
||
logWithTime(
|
||
'log',
|
||
'✅ 本地媒体流获取成功',
|
||
stream.getTracks().map((t) => `${t.kind}:${t.label}`),
|
||
);
|
||
return stream;
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 获取本地媒体流失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// 处理缓存的ICE候选
|
||
const processPendingIceCandidates = async () => {
|
||
if (
|
||
!peerConnection.value ||
|
||
!isRemoteDescriptionSet.value ||
|
||
pendingIceCandidates.value.length === 0
|
||
) {
|
||
logWithTime(
|
||
'log',
|
||
`🧊 跳过处理ICE候选 - PC:${!!peerConnection.value}, RD:${isRemoteDescriptionSet.value}, 候选数:${pendingIceCandidates.value.length}`,
|
||
);
|
||
return;
|
||
}
|
||
|
||
logWithTime(
|
||
'log',
|
||
`🧊 开始处理 ${pendingIceCandidates.value.length} 个缓存的ICE候选`,
|
||
);
|
||
|
||
const candidates = [...pendingIceCandidates.value];
|
||
pendingIceCandidates.value = [];
|
||
|
||
for (const candidate of candidates) {
|
||
try {
|
||
if (
|
||
peerConnection.value &&
|
||
peerConnection.value.connectionState !== 'closed'
|
||
) {
|
||
const iceCandidate = candidate.candidate
|
||
? candidate
|
||
: new RTCIceCandidate(candidate);
|
||
await peerConnection.value.addIceCandidate(iceCandidate);
|
||
logWithTime(
|
||
'log',
|
||
'✅ 缓存ICE候选添加成功:',
|
||
`${iceCandidate.candidate?.slice(0, 50)}...`,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 缓存ICE候选添加失败:', error);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 修复问题1: 注册视频元素更新回调
|
||
const registerVideoCallback = (callback) => {
|
||
videoElementCallbacks.value.add(callback);
|
||
logWithTime(
|
||
'log',
|
||
'📹 注册视频元素回调,当前回调数量:',
|
||
videoElementCallbacks.value.size,
|
||
);
|
||
|
||
// 如果已经有远程流,立即调用回调
|
||
if (remoteStream.value) {
|
||
logWithTime('log', '📹 立即调用新注册的回调,因为远程流已存在');
|
||
try {
|
||
callback(remoteStream.value);
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 调用视频回调失败:', error);
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
videoElementCallbacks.value.delete(callback);
|
||
logWithTime('log', '📹 取消注册视频元素回调');
|
||
};
|
||
};
|
||
|
||
// 修复问题1: 通知所有注册的视频元素更新
|
||
const notifyVideoElementsUpdate = (stream) => {
|
||
logWithTime(
|
||
'log',
|
||
'📹 通知所有视频元素更新,回调数量:',
|
||
videoElementCallbacks.value.size,
|
||
);
|
||
|
||
videoElementCallbacks.value.forEach((callback) => {
|
||
try {
|
||
callback(stream);
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 调用视频回调失败:', error);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 修复问题1: 改进远程轨道处理,直接通知组件更新
|
||
const setupRemoteTrackHandling = (pc) => {
|
||
pc.ontrack = (event) => {
|
||
logWithTime('log', '📹 接收到远程轨道事件:', {
|
||
streams: event.streams?.length,
|
||
track: event.track?.kind,
|
||
trackId: event.track?.id,
|
||
trackState: event.track?.readyState,
|
||
trackEnabled: event.track?.enabled,
|
||
trackMuted: event.track?.muted,
|
||
});
|
||
|
||
if (event.streams && event.streams.length > 0) {
|
||
const stream = event.streams[0];
|
||
const tracks = stream.getTracks();
|
||
|
||
if (tracks.length === 0) {
|
||
logWithTime('warn', '⚠️ 远程流没有轨道');
|
||
return;
|
||
}
|
||
|
||
logWithTime('log', '✅ 设置远程流:', {
|
||
id: stream.id,
|
||
tracks: tracks.map(
|
||
(t) => `${t.kind}:${t.id}:${t.readyState}:${t.enabled}:${t.muted}`,
|
||
),
|
||
active: stream.active,
|
||
});
|
||
|
||
// 清理旧的远程流
|
||
if (remoteStream.value && remoteStream.value.id !== stream.id) {
|
||
logWithTime('log', '🔄 清理旧的远程流');
|
||
remoteStream.value.getTracks().forEach((track) => {
|
||
track.stop();
|
||
logWithTime('log', `停止旧远程轨道: ${track.kind}`);
|
||
});
|
||
}
|
||
|
||
// 设置新的远程流
|
||
remoteStream.value = stream;
|
||
|
||
// 修复问题1: 直接通知所有注册的视频元素
|
||
notifyVideoElementsUpdate(stream);
|
||
|
||
// 监听流和轨道的状态变化
|
||
stream.onactive = () => {
|
||
logWithTime('log', '📹 远程流变为活跃状态');
|
||
notifyVideoElementsUpdate(stream);
|
||
};
|
||
|
||
stream.oninactive = () => {
|
||
logWithTime('warn', '⚠️ 远程流变为非活跃状态');
|
||
};
|
||
|
||
stream.onaddtrack = (trackEvent) => {
|
||
logWithTime('log', '📹 远程流添加新轨道:', trackEvent.track.kind);
|
||
setupTrackListeners(trackEvent.track);
|
||
notifyVideoElementsUpdate(stream);
|
||
};
|
||
|
||
stream.onremovetrack = (trackEvent) => {
|
||
logWithTime('warn', '⚠️ 远程流移除轨道:', trackEvent.track.kind);
|
||
};
|
||
|
||
// 为每个轨道设置监听器
|
||
tracks.forEach((track, index) => {
|
||
setupTrackListeners(track, index);
|
||
});
|
||
|
||
// 延迟通知,确保DOM已更新
|
||
setTimeout(() => {
|
||
notifyVideoElementsUpdate(stream);
|
||
}, 100);
|
||
|
||
setTimeout(() => {
|
||
notifyVideoElementsUpdate(stream);
|
||
}, 500);
|
||
} else if (event.track) {
|
||
// 如果没有流,尝试从轨道创建流
|
||
logWithTime('log', '🔄 从轨道创建远程流');
|
||
const newStream = new MediaStream([event.track]);
|
||
remoteStream.value = newStream;
|
||
setupTrackListeners(event.track);
|
||
|
||
logWithTime('log', '✅ 从轨道创建的流:', {
|
||
id: newStream.id,
|
||
tracks: newStream.getTracks().map((t) => `${t.kind}:${t.id}`),
|
||
});
|
||
|
||
// 通知视频元素更新
|
||
notifyVideoElementsUpdate(newStream);
|
||
|
||
setTimeout(() => {
|
||
notifyVideoElementsUpdate(newStream);
|
||
}, 100);
|
||
} else {
|
||
logWithTime('warn', '⚠️ ontrack事件没有接收到有效的媒体流或轨道');
|
||
}
|
||
};
|
||
};
|
||
|
||
// 为轨道设置完整的状态监听
|
||
const setupTrackListeners = (track, index = 0) => {
|
||
logWithTime(
|
||
'log',
|
||
`设置轨道${index}监听器: ${track.kind} - ${track.readyState} - ${track.enabled}`,
|
||
);
|
||
|
||
track.addEventListener('ended', () => {
|
||
logWithTime('warn', `远程轨道结束: ${track.kind} - ${track.id}`);
|
||
// 轨道结束时尝试重新协商
|
||
if (
|
||
peerConnection.value &&
|
||
peerConnection.value.connectionState === 'connected'
|
||
) {
|
||
logWithTime('log', '轨道结束,可能需要重新协商');
|
||
}
|
||
});
|
||
|
||
track.onmute = () => {
|
||
logWithTime('warn', `远程轨道静音: ${track.kind} - ${track.id}`);
|
||
};
|
||
|
||
track.onunmute = () => {
|
||
logWithTime('log', `远程轨道取消静音: ${track.kind} - ${track.id}`);
|
||
// 取消静音时触发视频更新
|
||
if (track.kind === 'video' && remoteStream.value) {
|
||
notifyVideoElementsUpdate(remoteStream.value);
|
||
}
|
||
};
|
||
|
||
// 确保轨道是启用的
|
||
if (!track.enabled) {
|
||
logWithTime('warn', `⚠️ 远程轨道未启用,尝试启用: ${track.kind}`);
|
||
track.enabled = true;
|
||
}
|
||
|
||
// 定期检查轨道状态
|
||
const checkInterval = setInterval(() => {
|
||
if (track.readyState === 'ended') {
|
||
clearInterval(checkInterval);
|
||
return;
|
||
}
|
||
|
||
if (
|
||
track.kind === 'video' &&
|
||
track.readyState === 'live' &&
|
||
track.enabled &&
|
||
!track.muted && // 视频轨道状态良好,确保视频元素正在播放
|
||
remoteStream.value
|
||
) {
|
||
notifyVideoElementsUpdate(remoteStream.value);
|
||
}
|
||
}, 5000);
|
||
|
||
// 清理定时器
|
||
track.addEventListener('ended', () => {
|
||
clearInterval(checkInterval);
|
||
});
|
||
};
|
||
|
||
// 连接重试机制
|
||
const attemptReconnection = async (callId, peerId) => {
|
||
if (connectionAttempts.value >= maxConnectionAttempts) {
|
||
logWithTime('error', '❌ 达到最大重连次数,停止重试');
|
||
chatStore.callStatus = 'failed';
|
||
return false;
|
||
}
|
||
|
||
connectionAttempts.value++;
|
||
logWithTime(
|
||
'log',
|
||
`🔄 尝试重新连接 (${connectionAttempts.value}/${maxConnectionAttempts})`,
|
||
);
|
||
|
||
try {
|
||
// 清理现有连接
|
||
cleanupPeerConnection();
|
||
|
||
// 重新创建连接
|
||
createPeerConnection(callId, peerId);
|
||
|
||
// 重置状态
|
||
isRemoteDescriptionSet.value = false;
|
||
pendingIceCandidates.value = [];
|
||
|
||
return true;
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 重连失败:', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 改进PeerConnection创建逻辑
|
||
const createPeerConnection = (callId, peerId) => {
|
||
logWithTime('log', '🔗 创建PeerConnection', { callId, peerId });
|
||
|
||
if (peerConnection.value) {
|
||
logWithTime('log', '🔄 关闭现有PeerConnection');
|
||
cleanupPeerConnection();
|
||
}
|
||
|
||
// 重置状态
|
||
isConnected.value = false;
|
||
isConnecting.value = false;
|
||
isRemoteDescriptionSet.value = false;
|
||
pendingIceCandidates.value = [];
|
||
|
||
const pc = new RTCPeerConnection(configuration);
|
||
peerConnection.value = pc;
|
||
|
||
// 添加本地流到连接
|
||
if (localStream.value) {
|
||
logWithTime('log', '📤 添加本地流轨道到PeerConnection');
|
||
localStream.value.getTracks().forEach((track) => {
|
||
logWithTime('log', `添加轨道: ${track.kind} - ${track.label}`);
|
||
pc.addTrack(track, localStream.value);
|
||
});
|
||
}
|
||
|
||
// ICE候选事件处理
|
||
pc.onicecandidate = async (event) => {
|
||
if (event.candidate) {
|
||
logWithTime(
|
||
'log',
|
||
'📡 发送ICE候选:',
|
||
`${event.candidate.candidate?.slice(0, 50)}...`,
|
||
);
|
||
try {
|
||
await callAPI.sendCandidate(
|
||
userStore.currentUser?.doctor_id,
|
||
peerId,
|
||
callId,
|
||
event.candidate,
|
||
6,
|
||
);
|
||
logWithTime('log', '✅ ICE候选发送成功');
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 发送ICE候选失败:', error);
|
||
}
|
||
} else {
|
||
logWithTime('log', '📡 ICE候选收集完成');
|
||
}
|
||
};
|
||
|
||
// 设置改进的远程轨道处理
|
||
setupRemoteTrackHandling(pc);
|
||
|
||
// 连接状态监听
|
||
pc.onconnectionstatechange = () => {
|
||
const state = pc.connectionState;
|
||
logWithTime('log', '🔗 连接状态变化:', state);
|
||
|
||
switch (state) {
|
||
case 'closed': {
|
||
isConnected.value = false;
|
||
isConnecting.value = false;
|
||
logWithTime('log', '🔒 WebRTC连接已关闭');
|
||
break;
|
||
}
|
||
|
||
case 'connected': {
|
||
isConnected.value = true;
|
||
isConnecting.value = false;
|
||
connectionAttempts.value = 0; // 重置重试计数
|
||
chatStore.callStatus = 'ongoing';
|
||
chatStore.callConnectionStatus = '通话已连接';
|
||
logWithTime('log', '✅ WebRTC连接已建立');
|
||
break;
|
||
}
|
||
|
||
case 'connecting': {
|
||
isConnecting.value = true;
|
||
chatStore.callConnectionStatus = '正在连接...';
|
||
logWithTime('log', '🔄 WebRTC正在连接');
|
||
break;
|
||
}
|
||
|
||
case 'disconnected': {
|
||
isConnected.value = false;
|
||
isConnecting.value = false;
|
||
chatStore.callStatus = 'disconnected';
|
||
chatStore.callConnectionStatus = '连接已断开';
|
||
logWithTime('warn', '⚠️ WebRTC连接断开');
|
||
|
||
// 连接断开时尝试重连
|
||
setTimeout(() => {
|
||
if (chatStore.callStatus === 'disconnected') {
|
||
attemptReconnection(callId, peerId);
|
||
}
|
||
}, 2000);
|
||
break;
|
||
}
|
||
|
||
case 'failed': {
|
||
isConnected.value = false;
|
||
isConnecting.value = false;
|
||
chatStore.callStatus = 'failed';
|
||
chatStore.callConnectionStatus = '连接失败';
|
||
logWithTime('error', '❌ WebRTC连接失败');
|
||
|
||
// 连接失败时尝试重连
|
||
setTimeout(() => {
|
||
if (chatStore.callStatus === 'failed') {
|
||
attemptReconnection(callId, peerId);
|
||
}
|
||
}, 3000);
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
|
||
// ICE连接状态监听和重连逻辑
|
||
pc.oniceconnectionstatechange = () => {
|
||
const iceState = pc.iceConnectionState;
|
||
logWithTime('log', '🧊 ICE连接状态变化:', iceState);
|
||
|
||
switch (iceState) {
|
||
case 'checking': {
|
||
logWithTime('log', '🧊 ICE正在检查连接');
|
||
break;
|
||
}
|
||
case 'closed': {
|
||
logWithTime('log', '🔒 ICE连接已关闭');
|
||
break;
|
||
}
|
||
case 'completed': {
|
||
logWithTime('log', '✅ ICE连接完成');
|
||
break;
|
||
}
|
||
case 'connected': {
|
||
logWithTime('log', '✅ ICE连接成功');
|
||
connectionAttempts.value = 0; // 重置重试计数
|
||
break;
|
||
}
|
||
case 'disconnected': {
|
||
logWithTime('warn', '⚠️ ICE连接断开');
|
||
|
||
// ICE断开时等待一段时间看是否能恢复
|
||
setTimeout(() => {
|
||
if (pc.iceConnectionState === 'disconnected') {
|
||
logWithTime('log', '🔄 ICE连接断开超时,尝试重连');
|
||
attemptReconnection(callId, peerId);
|
||
}
|
||
}, 5000);
|
||
break;
|
||
}
|
||
case 'failed': {
|
||
logWithTime('error', '❌ ICE连接失败');
|
||
chatStore.callConnectionStatus = '网络连接失败';
|
||
|
||
// ICE连接失败时重新协商
|
||
setTimeout(() => {
|
||
if (pc.iceConnectionState === 'failed') {
|
||
logWithTime('log', '🔄 ICE连接失败,尝试重新协商');
|
||
// 通知需要重新协商
|
||
const event = new CustomEvent('iceConnectionFailed', {
|
||
detail: { callId, peerId },
|
||
});
|
||
window.dispatchEvent(event);
|
||
}
|
||
}, 2000);
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
|
||
// ICE收集状态监听
|
||
pc.onicegatheringstatechange = () => {
|
||
logWithTime('log', '🧊 ICE收集状态:', pc.iceGatheringState);
|
||
};
|
||
|
||
// 信令状态监听
|
||
pc.onsignalingstatechange = () => {
|
||
logWithTime('log', '📡 信令状态:', pc.signalingState);
|
||
};
|
||
|
||
return pc;
|
||
};
|
||
|
||
// 清理PeerConnection
|
||
const cleanupPeerConnection = () => {
|
||
if (!peerConnection.value) return;
|
||
|
||
try {
|
||
const senders = peerConnection.value.getSenders();
|
||
senders.forEach((sender) => {
|
||
if (sender.track) {
|
||
logWithTime('log', `停止发送器轨道: ${sender.track.kind}`);
|
||
sender.track.stop();
|
||
}
|
||
});
|
||
|
||
const receivers = peerConnection.value.getReceivers();
|
||
receivers.forEach((receiver) => {
|
||
if (receiver.track) {
|
||
logWithTime('log', `停止接收器轨道: ${receiver.track.kind}`);
|
||
receiver.track.stop();
|
||
}
|
||
});
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 清理发送器/接收器时出错:', error);
|
||
}
|
||
|
||
// 清理事件监听器
|
||
peerConnection.value.onicecandidate = null;
|
||
peerConnection.value.ontrack = null;
|
||
peerConnection.value.onconnectionstatechange = null;
|
||
peerConnection.value.oniceconnectionstatechange = null;
|
||
peerConnection.value.onicegatheringstatechange = null;
|
||
peerConnection.value.onsignalingstatechange = null;
|
||
peerConnection.value.ondatachannel = null;
|
||
|
||
peerConnection.value.close();
|
||
peerConnection.value = null;
|
||
};
|
||
|
||
// 创建Offer
|
||
const createOffer = async (callId, peerId) => {
|
||
logWithTime('log', '📤 创建Offer', { callId, peerId });
|
||
|
||
if (!peerConnection.value) {
|
||
logWithTime('log', '🔗 PeerConnection不存在,先创建');
|
||
createPeerConnection(callId, peerId);
|
||
}
|
||
|
||
try {
|
||
const offerOptions = {
|
||
offerToReceiveAudio: true,
|
||
offerToReceiveVideo: true,
|
||
voiceActivityDetection: true,
|
||
iceRestart: connectionAttempts.value > 0, // 重连时启用ICE重启
|
||
};
|
||
|
||
const offer = await peerConnection.value.createOffer(offerOptions);
|
||
await peerConnection.value.setLocalDescription(offer);
|
||
|
||
logWithTime('log', '✅ Offer创建成功:', {
|
||
type: offer.type,
|
||
sdpLength: offer.sdp.length,
|
||
hasAudio: offer.sdp.includes('m=audio'),
|
||
hasVideo: offer.sdp.includes('m=video'),
|
||
iceRestart: offerOptions.iceRestart,
|
||
});
|
||
|
||
return offer;
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 创建Offer失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// 创建Answer
|
||
const createAnswer = async (offer, callId, peerId) => {
|
||
logWithTime('log', '📥 处理Offer并创建Answer');
|
||
|
||
if (!peerConnection.value) {
|
||
logWithTime('log', '🔗 PeerConnection不存在,先创建');
|
||
createPeerConnection(callId, peerId);
|
||
}
|
||
|
||
try {
|
||
logWithTime('log', '📥 设置远程Offer');
|
||
const remoteDesc = new RTCSessionDescription(offer);
|
||
await peerConnection.value.setRemoteDescription(remoteDesc);
|
||
|
||
isRemoteDescriptionSet.value = true;
|
||
logWithTime('log', '✅ 远程描述设置完成,开始处理缓存的ICE候选');
|
||
|
||
await processPendingIceCandidates();
|
||
|
||
const answer = await peerConnection.value.createAnswer();
|
||
await peerConnection.value.setLocalDescription(answer);
|
||
|
||
logWithTime('log', '✅ Answer创建成功:', {
|
||
type: answer.type,
|
||
sdpLength: answer.sdp.length,
|
||
hasAudio: answer.sdp.includes('m=audio'),
|
||
hasVideo: answer.sdp.includes('m=video'),
|
||
});
|
||
|
||
return answer;
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 创建Answer失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// 设置远程Answer
|
||
const setRemoteAnswer = async (answer) => {
|
||
logWithTime('log', '📥 设置远程Answer');
|
||
|
||
if (!peerConnection.value) {
|
||
logWithTime('error', '❌ PeerConnection不存在');
|
||
throw new Error('PeerConnection不存在');
|
||
}
|
||
|
||
try {
|
||
const remoteDesc = new RTCSessionDescription(answer);
|
||
await peerConnection.value.setRemoteDescription(remoteDesc);
|
||
|
||
isRemoteDescriptionSet.value = true;
|
||
logWithTime('log', '✅ 远程Answer设置成功,开始处理缓存的ICE候选');
|
||
|
||
await processPendingIceCandidates();
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 设置远程Answer失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// ICE候选添加逻辑
|
||
const addIceCandidate = async (candidate) => {
|
||
logWithTime(
|
||
'log',
|
||
'🧊 收到ICE候选:',
|
||
`${candidate.candidate?.slice(0, 50)}...`,
|
||
);
|
||
|
||
if (!candidate || !candidate.candidate) {
|
||
logWithTime('warn', '⚠️ 无效的ICE候选:', candidate);
|
||
return false;
|
||
}
|
||
|
||
if (!peerConnection.value) {
|
||
logWithTime('warn', '⚠️ PeerConnection不存在,缓存ICE候选');
|
||
pendingIceCandidates.value.push(candidate);
|
||
return false;
|
||
}
|
||
|
||
if (
|
||
peerConnection.value.connectionState === 'closed' ||
|
||
peerConnection.value.connectionState === 'failed'
|
||
) {
|
||
logWithTime(
|
||
'warn',
|
||
'⚠️ PeerConnection状态异常,缓存ICE候选:',
|
||
peerConnection.value.connectionState,
|
||
);
|
||
pendingIceCandidates.value.push(candidate);
|
||
return false;
|
||
}
|
||
|
||
if (!isRemoteDescriptionSet.value) {
|
||
logWithTime('warn', '⚠️ 远程描述未设置,缓存ICE候选');
|
||
pendingIceCandidates.value.push(candidate);
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const iceCandidate = candidate.candidate
|
||
? candidate
|
||
: new RTCIceCandidate(candidate);
|
||
await peerConnection.value.addIceCandidate(iceCandidate);
|
||
logWithTime('log', '✅ ICE候选添加成功');
|
||
return true;
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 添加ICE候选失败:', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 切换摄像头
|
||
const toggleCamera = (enable) => {
|
||
if (!localStream.value) {
|
||
logWithTime('warn', '⚠️ 本地流不存在,无法切换摄像头');
|
||
return;
|
||
}
|
||
|
||
const videoTracks = localStream.value.getVideoTracks();
|
||
if (videoTracks.length > 0) {
|
||
videoTracks.forEach((track) => {
|
||
track.enabled = enable;
|
||
logWithTime(
|
||
'log',
|
||
`📹 摄像头${enable ? '开启' : '关闭'}: ${track.label}`,
|
||
);
|
||
});
|
||
} else {
|
||
logWithTime('warn', '⚠️ 没有找到视频轨道');
|
||
}
|
||
};
|
||
|
||
// 切换麦克风
|
||
const toggleMicrophone = (enable) => {
|
||
if (!localStream.value) {
|
||
logWithTime('warn', '⚠️ 本地流不存在,无法切换麦克风');
|
||
return;
|
||
}
|
||
|
||
const audioTracks = localStream.value.getAudioTracks();
|
||
if (audioTracks.length > 0) {
|
||
audioTracks.forEach((track) => {
|
||
track.enabled = enable;
|
||
logWithTime(
|
||
'log',
|
||
`🎤 麦克风${enable ? '开启' : '关闭'}: ${track.label}`,
|
||
);
|
||
});
|
||
} else {
|
||
logWithTime('warn', '⚠️ 没有找到音频轨道');
|
||
}
|
||
};
|
||
|
||
// 释放媒体资源
|
||
const releaseMediaResources = () => {
|
||
logWithTime('log', '🔄 开始释放媒体资源');
|
||
|
||
if (localStream.value) {
|
||
logWithTime('log', '🔄 释放本地媒体流');
|
||
localStream.value.getTracks().forEach((track) => {
|
||
logWithTime(
|
||
'log',
|
||
`停止本地轨道: ${track.kind} - ${track.label} - 状态: ${track.readyState}`,
|
||
);
|
||
track.stop();
|
||
|
||
setTimeout(() => {
|
||
if (track.readyState === 'ended') {
|
||
logWithTime('log', `✅ 轨道已完全停止: ${track.kind}`);
|
||
} else {
|
||
logWithTime(
|
||
'warn',
|
||
`⚠️ 轨道未完全停止: ${track.kind} - ${track.readyState}`,
|
||
);
|
||
try {
|
||
track.stop();
|
||
} catch (error) {
|
||
logWithTime('error', '强制停止轨道失败:', error);
|
||
}
|
||
}
|
||
}, 200);
|
||
});
|
||
localStream.value = null;
|
||
}
|
||
|
||
if (remoteStream.value) {
|
||
logWithTime('log', '🔄 释放远程媒体流');
|
||
remoteStream.value.getTracks().forEach((track) => {
|
||
logWithTime('log', `停止远程轨道: ${track.kind}`);
|
||
track.stop();
|
||
});
|
||
remoteStream.value = null;
|
||
|
||
// 通知视频元素清理
|
||
notifyVideoElementsUpdate(null);
|
||
}
|
||
|
||
logWithTime('log', '✅ 媒体资源释放完成');
|
||
};
|
||
|
||
// 关闭连接
|
||
const closeConnection = () => {
|
||
logWithTime('log', '🔄 开始关闭WebRTC连接');
|
||
|
||
pendingIceCandidates.value = [];
|
||
isRemoteDescriptionSet.value = false;
|
||
connectionAttempts.value = 0;
|
||
logWithTime('log', '🧊 清空ICE候选缓存和状态');
|
||
|
||
releaseMediaResources();
|
||
cleanupPeerConnection();
|
||
|
||
isConnected.value = false;
|
||
isConnecting.value = false;
|
||
|
||
logWithTime('log', '✅ WebRTC连接已完全关闭,所有资源已释放');
|
||
};
|
||
|
||
// 强制清理所有资源
|
||
const forceCleanup = () => {
|
||
logWithTime('log', '🚨 强制清理所有WebRTC资源');
|
||
|
||
try {
|
||
pendingIceCandidates.value = [];
|
||
isRemoteDescriptionSet.value = false;
|
||
connectionAttempts.value = 0;
|
||
|
||
// 清理所有回调
|
||
videoElementCallbacks.value.clear();
|
||
|
||
closeConnection();
|
||
|
||
setTimeout(() => {
|
||
localStream.value = null;
|
||
remoteStream.value = null;
|
||
peerConnection.value = null;
|
||
isConnected.value = false;
|
||
isConnecting.value = false;
|
||
|
||
logWithTime('log', '✅ 强制清理完成');
|
||
}, 100);
|
||
} catch (error) {
|
||
logWithTime('error', '❌ 强制清理时出错:', error);
|
||
}
|
||
};
|
||
|
||
return {
|
||
localStream,
|
||
remoteStream,
|
||
peerConnection,
|
||
isConnected,
|
||
isConnecting,
|
||
pendingIceCandidates,
|
||
isRemoteDescriptionSet,
|
||
connectionAttempts,
|
||
getLocalMedia,
|
||
createPeerConnection,
|
||
createOffer,
|
||
createAnswer,
|
||
setRemoteAnswer,
|
||
addIceCandidate,
|
||
toggleCamera,
|
||
toggleMicrophone,
|
||
closeConnection,
|
||
releaseMediaResources,
|
||
forceCleanup,
|
||
attemptReconnection,
|
||
registerVideoCallback, // 修复问题1: 新增方法
|
||
};
|
||
}
|