优化webrtc连接

This commit is contained in:
mtvpls
2025-12-11 20:53:05 +08:00
parent eb2898e5c1
commit 36bb393ae9
2 changed files with 162 additions and 23 deletions

View File

@@ -42,6 +42,7 @@ export function useVoiceChat({
const audioContextRef = useRef<AudioContext | null>(null);
const remoteAudioElementsRef = useRef<Map<string, HTMLAudioElement>>(new Map());
const nextPlayTimeRef = useRef<Map<string, number>>(new Map()); // 跟踪每个用户的下一个播放时间
const disconnectionTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map()); // 跟踪连接断开的定时器
// 服务器中转相关
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
@@ -50,8 +51,15 @@ export function useVoiceChat({
const switchToServerRelayRef = useRef<(() => void) | null>(null);
const playRemoteStreamRef = useRef<((peerId: string, stream: MediaStream) => void) | null>(null);
// ICE服务器配置使用免费的STUN服务器
// ICE服务器配置使用多个免费的STUN服务器作为备份
const iceServers = [
// Cloudflare STUN 服务器(主选,全球 CDN
{ urls: 'stun:stun.cloudflare.com:3478' },
// Numb/Viagenie备选老牌稳定服务
{ urls: 'stun:stun.numb.viagenie.ca:3478' },
// Annatel备选
{ urls: 'stun:stun.annatel.net:3478' },
// Google STUN 服务器(最后备选)
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
];
@@ -76,7 +84,7 @@ export function useVoiceChat({
}
}, []);
// 停止本地流
// 停止本地流(完全停止并释放麦克风)
const stopLocalStream = useCallback(() => {
if (localStreamRef.current) {
localStreamRef.current.getTracks().forEach(track => track.stop());
@@ -85,6 +93,26 @@ export function useVoiceChat({
}
}, []);
// 禁用本地音频轨道(静音,但不释放麦克风)
const muteLocalStream = useCallback(() => {
if (localStreamRef.current) {
localStreamRef.current.getTracks().forEach(track => {
track.enabled = false;
});
console.log('[VoiceChat] Muted local stream');
}
}, []);
// 启用本地音频轨道(取消静音)
const unmuteLocalStream = useCallback(() => {
if (localStreamRef.current) {
localStreamRef.current.getTracks().forEach(track => {
track.enabled = true;
});
console.log('[VoiceChat] Unmuted local stream');
}
}, []);
// ==================== WebRTC P2P 逻辑 ====================
// 创建 RTCPeerConnection
@@ -113,16 +141,98 @@ export function useVoiceChat({
}
};
// 连接状态变化
// ICE 连接状态变化 - 更准确地反映连接质量
pc.oniceconnectionstatechange = () => {
console.log('[VoiceChat] ICE connection state with', peerId, ':', pc.iceConnectionState);
if (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
// 连接成功,清除断开定时器
const timer = disconnectionTimersRef.current.get(peerId);
if (timer) {
clearTimeout(timer);
disconnectionTimersRef.current.delete(peerId);
}
setIsConnected(true);
setIsConnecting(false);
} else if (pc.iceConnectionState === 'disconnected') {
// 连接断开但给它5秒恢复时间
console.log('[VoiceChat] ICE disconnected for', peerId, ', waiting for recovery...');
// 清除之前的定时器(如果有)
const existingTimer = disconnectionTimersRef.current.get(peerId);
if (existingTimer) {
clearTimeout(existingTimer);
}
// 设置新的定时器
const timer = setTimeout(() => {
console.log('[VoiceChat] ICE connection recovery timeout for', peerId);
// 5秒后仍未恢复检查是否所有连接都断开了
if (strategy === 'webrtc-fallback' && pc.iceConnectionState === 'disconnected') {
// 检查是否还有其他活跃的连接
let hasActiveConnection = false;
peerConnectionsRef.current.forEach((otherPc, otherPeerId) => {
if (otherPeerId !== peerId) {
const state = otherPc.iceConnectionState;
if (state === 'connected' || state === 'completed' || state === 'checking') {
hasActiveConnection = true;
}
}
});
// 只有当所有连接都断开时才切换到服务器中转
if (!hasActiveConnection) {
console.log('[VoiceChat] All ICE connections failed, switching to server relay');
switchToServerRelayRef.current?.();
} else {
console.log('[VoiceChat] Other connections still active, not switching to server relay');
// 只关闭这个失败的连接
pc.close();
peerConnectionsRef.current.delete(peerId);
}
}
disconnectionTimersRef.current.delete(peerId);
}, 5000); // 给5秒恢复时间
disconnectionTimersRef.current.set(peerId, timer);
} else if (pc.iceConnectionState === 'failed') {
// ICE 连接彻底失败
console.log('[VoiceChat] ICE connection failed for', peerId);
if (strategy === 'webrtc-fallback') {
// 检查是否还有其他活跃的连接
let hasActiveConnection = false;
peerConnectionsRef.current.forEach((otherPc, otherPeerId) => {
if (otherPeerId !== peerId) {
const state = otherPc.iceConnectionState;
if (state === 'connected' || state === 'completed' || state === 'checking') {
hasActiveConnection = true;
}
}
});
if (!hasActiveConnection) {
console.log('[VoiceChat] All ICE connections failed, switching to server relay');
switchToServerRelayRef.current?.();
} else {
console.log('[VoiceChat] Other connections still active, not switching to server relay');
// 只关闭这个失败的连接
pc.close();
peerConnectionsRef.current.delete(peerId);
}
}
}
};
// 连接状态变化 - 作为辅助监控
pc.onconnectionstatechange = () => {
console.log('[VoiceChat] Connection state with', peerId, ':', pc.connectionState);
if (pc.connectionState === 'connected') {
setIsConnected(true);
setIsConnecting(false);
} else if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
// WebRTC连接失败如果策略允许切换到服务器中转
} else if (pc.connectionState === 'failed') {
// 只在 failed 状态时切换,不在 disconnected 时切换
if (strategy === 'webrtc-fallback') {
console.log('[VoiceChat] WebRTC failed, falling back to server relay');
console.log('[VoiceChat] Connection failed, falling back to server relay');
switchToServerRelayRef.current?.();
}
}
@@ -164,6 +274,12 @@ export function useVoiceChat({
// 清理WebRTC连接
const cleanupWebRTC = useCallback(() => {
// 清除所有断开定时器
disconnectionTimersRef.current.forEach((timer) => {
clearTimeout(timer);
});
disconnectionTimersRef.current.clear();
// 关闭所有peer connections
peerConnectionsRef.current.forEach((pc, peerId) => {
pc.close();
@@ -382,7 +498,19 @@ export function useVoiceChat({
// 启动服务器中转
if (isMicEnabled && localStreamRef.current) {
startServerRelay();
// 确保音频轨道是启用的
const tracks = localStreamRef.current.getTracks();
const hasEnabledTrack = tracks.some(track => track.enabled && track.readyState === 'live');
if (hasEnabledTrack) {
startServerRelay();
} else {
console.error('[VoiceChat] Cannot start server relay - no enabled audio tracks');
setError('服务器中转启动失败:麦克风未启用');
}
} else {
console.error('[VoiceChat] Cannot start server relay - mic disabled or no stream');
setError('服务器中转启动失败:麦克风未开启');
}
}, [isMicEnabled, cleanupWebRTC, startServerRelay]);
@@ -475,6 +603,19 @@ export function useVoiceChat({
if (isMicEnabled) {
// 开启麦克风
// 检查是否已经有本地流(可能只是被静音了)
if (localStreamRef.current) {
// 已有本地流,只需取消静音
console.log('[VoiceChat] Unmuting existing local stream');
unmuteLocalStream();
// 重新启动服务器中转(如果需要)
if (strategy === 'server-only' || peerConnectionsRef.current.size === 0) {
startServerRelay();
}
return;
}
// 没有本地流,需要重新获取
setIsConnecting(true);
setError(null);
@@ -511,18 +652,15 @@ export function useVoiceChat({
setIsConnecting(false);
});
} else {
// 关闭麦克风
stopLocalStream();
cleanupWebRTC();
// 关闭麦克风 - 只静音,不断开连接
muteLocalStream();
// 停止服务器中转(因为不需要发送音频了)
stopServerRelay();
}
return () => {
if (!isMicEnabled) {
cleanup();
}
};
}, [isMicEnabled, socket, roomId, strategy, members, getLocalStream, stopLocalStream, cleanupWebRTC, stopServerRelay, startServerRelay, cleanup, initiateConnection]);
// 不需要 cleanup因为我们希望保持连接
// cleanup 只在房间变化时调用
}, [isMicEnabled, socket, roomId, strategy, members, getLocalStream, muteLocalStream, unmuteLocalStream, stopServerRelay, startServerRelay, initiateConnection]);
// 监听喇叭状态变化
useEffect(() => {

View File

@@ -212,13 +212,14 @@ class WatchRoomSocketManager {
// eslint-disable-next-line no-console
console.warn('[WatchRoom] Heartbeat timeout detected, last response was', timeSinceLastResponse, 'ms ago');
// 尝试重连
if (this.socket) {
this.socket.disconnect();
this.socket.connect();
// 重置心跳响应时间,避免重复触发
this.lastHeartbeatResponse = Date.now();
}
// 不要强制断开连接,让 Socket.IO 的自动重连机制处理
// Socket.IO 会自动检测连接问题并尝试重连
// 只记录警告,不主动断开
// eslint-disable-next-line no-console
console.warn('[WatchRoom] Waiting for Socket.IO auto-reconnect mechanism');
// 重置心跳响应时间,避免重复触发警告
this.lastHeartbeatResponse = Date.now();
}
}, 3000);
}