修正语音webrtc模式

This commit is contained in:
mtvpls
2025-12-09 01:26:30 +08:00
parent 59e1f9f1c6
commit c4c4843aa9
2 changed files with 109 additions and 42 deletions

View File

@@ -32,6 +32,7 @@ export default function ChatFloatingWindow() {
roomId: watchRoom?.currentRoom?.id || null,
isMicEnabled,
isSpeakerEnabled,
members: watchRoom?.members || [],
});
// 当房间变化时重置状态

View File

@@ -3,12 +3,14 @@
import { useEffect, useRef, useCallback, useState } from 'react';
import type { WatchRoomSocket } from '@/lib/watch-room-socket';
import type { Member } from '@/types/watch-room';
interface UseVoiceChatOptions {
socket: WatchRoomSocket | null;
roomId: string | null;
isMicEnabled: boolean;
isSpeakerEnabled: boolean;
members: Member[];
}
// 语音聊天策略类型
@@ -26,6 +28,7 @@ export function useVoiceChat({
roomId,
isMicEnabled,
isSpeakerEnabled,
members,
}: UseVoiceChatOptions) {
const [isConnecting, setIsConnecting] = useState(false);
const [isConnected, setIsConnected] = useState(false);
@@ -42,6 +45,10 @@ export function useVoiceChat({
// 服务器中转相关
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
// 使用ref存储回退函数避免循环依赖
const switchToServerRelayRef = useRef<(() => void) | null>(null);
const playRemoteStreamRef = useRef<((peerId: string, stream: MediaStream) => void) | null>(null);
// ICE服务器配置使用免费的STUN服务器
const iceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
@@ -86,7 +93,6 @@ export function useVoiceChat({
// ICE候选收集
pc.onicecandidate = (event) => {
if (event.candidate && socket) {
console.log('[VoiceChat] Sending ICE candidate to', peerId);
socket.emit('voice:ice', {
targetUserId: peerId,
candidate: event.candidate.toJSON(),
@@ -102,7 +108,7 @@ export function useVoiceChat({
// 创建音频元素播放远程流
if (isSpeakerEnabled) {
playRemoteStream(peerId, remoteStream);
playRemoteStreamRef.current?.(peerId, remoteStream);
}
};
@@ -116,7 +122,7 @@ export function useVoiceChat({
// WebRTC连接失败如果策略允许切换到服务器中转
if (strategy === 'webrtc-fallback') {
console.log('[VoiceChat] WebRTC failed, falling back to server relay');
switchToServerRelay();
switchToServerRelayRef.current?.();
}
}
};
@@ -136,6 +142,11 @@ export function useVoiceChat({
audio.srcObject = stream;
}, []);
// 将播放函数存储到ref中供createPeerConnection使用
useEffect(() => {
playRemoteStreamRef.current = playRemoteStream;
}, [playRemoteStream]);
// 停止播放远程音频流
const stopRemoteStream = useCallback((peerId: string) => {
const audio = remoteAudioElementsRef.current.get(peerId);
@@ -147,6 +158,18 @@ export function useVoiceChat({
remoteStreamsRef.current.delete(peerId);
}, []);
// 清理WebRTC连接
const cleanupWebRTC = useCallback(() => {
// 关闭所有peer connections
peerConnectionsRef.current.forEach((pc, peerId) => {
pc.close();
stopRemoteStream(peerId);
});
peerConnectionsRef.current.clear();
console.log('[VoiceChat] WebRTC cleaned up');
}, [stopRemoteStream]);
// 向对等端发起连接创建offer
const initiateConnection = useCallback(async (peerId: string) => {
if (!socket || !localStreamRef.current) return;
@@ -178,17 +201,22 @@ export function useVoiceChat({
// 处理接收到的offer
const handleOffer = useCallback(async (data: { userId: string; offer: RTCSessionDescriptionInit }) => {
if (!socket || !localStreamRef.current) return;
if (!socket) return;
console.log('[VoiceChat] Received offer from', data.userId);
const pc = createPeerConnection(data.userId);
// 添加本地流
localStreamRef.current.getTracks().forEach(track => {
if (localStreamRef.current) {
pc.addTrack(track, localStreamRef.current);
}
});
// 如果有本地流,添加音频轨道
if (localStreamRef.current) {
localStreamRef.current.getTracks().forEach(track => {
if (localStreamRef.current) {
pc.addTrack(track, localStreamRef.current);
console.log('[VoiceChat] Added local track to answer');
}
});
} else {
console.log('[VoiceChat] No local stream, creating answer without sending audio');
}
try {
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
@@ -220,7 +248,6 @@ export function useVoiceChat({
// 处理接收到的ICE候选
const handleIceCandidate = useCallback(async (data: { userId: string; candidate: RTCIceCandidateInit }) => {
console.log('[VoiceChat] Received ICE candidate from', data.userId);
const pc = peerConnectionsRef.current.get(data.userId);
if (!pc) return;
@@ -233,20 +260,6 @@ export function useVoiceChat({
// ==================== 服务器中转逻辑 ====================
// 切换到服务器中转模式
const switchToServerRelay = useCallback(async () => {
console.log('[VoiceChat] Switching to server relay mode');
setError('P2P连接失败切换到服务器中转模式');
// 清理WebRTC连接
cleanupWebRTC();
// 启动服务器中转
if (isMicEnabled && localStreamRef.current) {
startServerRelay();
}
}, [isMicEnabled]);
// 启动服务器中转
const startServerRelay = useCallback(() => {
if (!socket || !localStreamRef.current) {
@@ -323,6 +336,25 @@ export function useVoiceChat({
}
}, []);
// 切换到服务器中转模式
const switchToServerRelay = useCallback(async () => {
console.log('[VoiceChat] Switching to server relay mode');
setError('P2P连接失败切换到服务器中转模式');
// 清理WebRTC连接
cleanupWebRTC();
// 启动服务器中转
if (isMicEnabled && localStreamRef.current) {
startServerRelay();
}
}, [isMicEnabled, cleanupWebRTC, startServerRelay]);
// 将回退函数存储到ref中供createPeerConnection使用
useEffect(() => {
switchToServerRelayRef.current = switchToServerRelay;
}, [switchToServerRelay]);
// 播放服务器中转的音频 - 使用Web Audio API播放PCM数据
const playServerRelayAudio = useCallback(async (userId: string, audioData: number[], sampleRate: number = 16000) => {
if (!isSpeakerEnabled) return;
@@ -363,18 +395,6 @@ export function useVoiceChat({
// ==================== 清理函数 ====================
// 清理WebRTC连接
const cleanupWebRTC = useCallback(() => {
// 关闭所有peer connections
peerConnectionsRef.current.forEach((pc, peerId) => {
pc.close();
stopRemoteStream(peerId);
});
peerConnectionsRef.current.clear();
console.log('[VoiceChat] WebRTC cleaned up');
}, [stopRemoteStream]);
// 清理所有连接
const cleanup = useCallback(() => {
stopLocalStream();
@@ -412,10 +432,23 @@ export function useVoiceChat({
// 仅使用服务器中转
startServerRelay();
} else {
// 使用WebRTC,失败时自动切换到服务器中转
// 这里需要获取房间内其他成员列表,然后向每个成员发起连接
// 这部分逻辑需要在WatchRoomProvider中获取members列表
console.log('[VoiceChat] WebRTC mode, waiting for peer connections');
// 使用WebRTC P2P连接
console.log('[VoiceChat] WebRTC mode - initiating peer connections');
// 向房间内的其他成员发起连接
const otherMembers = members.filter(m => m.id !== socket.id);
console.log('[VoiceChat] Found', otherMembers.length, 'other members, initiating connections');
if (otherMembers.length > 0) {
otherMembers.forEach(member => {
console.log('[VoiceChat] Initiating connection to', member.name, member.id);
initiateConnection(member.id);
});
} else {
// 如果没有其他成员,先启动服务器中转作为后备
console.log('[VoiceChat] No other members, using server relay as fallback');
startServerRelay();
}
}
setIsConnecting(false);
@@ -435,7 +468,7 @@ export function useVoiceChat({
cleanup();
}
};
}, [isMicEnabled, socket, roomId, strategy, getLocalStream, stopLocalStream, cleanupWebRTC, stopServerRelay, startServerRelay, cleanup]);
}, [isMicEnabled, socket, roomId, strategy, members, getLocalStream, stopLocalStream, cleanupWebRTC, stopServerRelay, startServerRelay, cleanup, initiateConnection]);
// 监听喇叭状态变化
useEffect(() => {
@@ -470,6 +503,12 @@ export function useVoiceChat({
socket.on('voice:answer', handleAnswer);
socket.on('voice:ice', handleIceCandidate);
// 监听其他用户开启麦克风的通知
socket.on('voice:mic-enabled', (data: { userId: string }) => {
console.log('[VoiceChat] User', data.userId, 'enabled microphone');
// 其他用户开启了麦克风我们不需要做任何事等待接收他们的offer即可
});
// 服务器中转事件
const handleAudioChunk = (data: { userId: string; audioData: number[]; sampleRate?: number }) => {
if (strategy === 'server-only' || !peerConnectionsRef.current.has(data.userId)) {
@@ -484,10 +523,37 @@ export function useVoiceChat({
socket.off('voice:offer', handleOffer);
socket.off('voice:answer', handleAnswer);
socket.off('voice:ice', handleIceCandidate);
socket.off('voice:mic-enabled');
socket.off('voice:audio-chunk', handleAudioChunk);
};
}, [socket, strategy, handleOffer, handleAnswer, handleIceCandidate, playServerRelayAudio]);
// 监听房间成员变化 - 处理新成员加入的情况
useEffect(() => {
// 只在WebRTC模式、麦克风开启、有本地流的情况下才处理
if (strategy !== 'webrtc-fallback' || !isMicEnabled || !localStreamRef.current || !socket) {
return;
}
// 检查是否有新成员加入
const currentPeerIds = Array.from(peerConnectionsRef.current.keys());
const memberIds = members.filter(m => m.id !== socket.id).map(m => m.id);
// 找出新加入的成员在memberIds中但不在currentPeerIds中
const newMemberIds = memberIds.filter(id => !currentPeerIds.includes(id));
if (newMemberIds.length > 0) {
console.log('[VoiceChat] New members joined, initiating connections:', newMemberIds);
newMemberIds.forEach(memberId => {
const member = members.find(m => m.id === memberId);
if (member) {
console.log('[VoiceChat] Initiating connection to new member:', member.name, member.id);
initiateConnection(member.id);
}
});
}
}, [members, strategy, isMicEnabled, socket, initiateConnection]);
// 房间变化时清理
useEffect(() => {
return () => {