Files
xk-admin/apps/web-antd/src/views/business/chat/composables/useWebRTC.ts
lq 2423326951
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
fix: 聊天功能(部分)
2025-07-22 16:55:09 +08:00

905 lines
26 KiB
TypeScript
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.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: 新增方法
};
}