修正语音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
@@ -32,6 +32,7 @@ export default function ChatFloatingWindow() {
roomId: watchRoom?.currentRoom?.id || null, roomId: watchRoom?.currentRoom?.id || null,
isMicEnabled, isMicEnabled,
isSpeakerEnabled, isSpeakerEnabled,
members: watchRoom?.members || [],
}); });
// 当房间变化时重置状态 // 当房间变化时重置状态
+108 -42
View File
@@ -3,12 +3,14 @@
import { useEffect, useRef, useCallback, useState } from 'react'; import { useEffect, useRef, useCallback, useState } from 'react';
import type { WatchRoomSocket } from '@/lib/watch-room-socket'; import type { WatchRoomSocket } from '@/lib/watch-room-socket';
import type { Member } from '@/types/watch-room';
interface UseVoiceChatOptions { interface UseVoiceChatOptions {
socket: WatchRoomSocket | null; socket: WatchRoomSocket | null;
roomId: string | null; roomId: string | null;
isMicEnabled: boolean; isMicEnabled: boolean;
isSpeakerEnabled: boolean; isSpeakerEnabled: boolean;
members: Member[];
} }
// 语音聊天策略类型 // 语音聊天策略类型
@@ -26,6 +28,7 @@ export function useVoiceChat({
roomId, roomId,
isMicEnabled, isMicEnabled,
isSpeakerEnabled, isSpeakerEnabled,
members,
}: UseVoiceChatOptions) { }: UseVoiceChatOptions) {
const [isConnecting, setIsConnecting] = useState(false); const [isConnecting, setIsConnecting] = useState(false);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
@@ -42,6 +45,10 @@ export function useVoiceChat({
// 服务器中转相关 // 服务器中转相关
const mediaRecorderRef = useRef<MediaRecorder | null>(null); 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服务器) // ICE服务器配置(使用免费的STUN服务器)
const iceServers = [ const iceServers = [
{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:19302' },
@@ -86,7 +93,6 @@ export function useVoiceChat({
// ICE候选收集 // ICE候选收集
pc.onicecandidate = (event) => { pc.onicecandidate = (event) => {
if (event.candidate && socket) { if (event.candidate && socket) {
console.log('[VoiceChat] Sending ICE candidate to', peerId);
socket.emit('voice:ice', { socket.emit('voice:ice', {
targetUserId: peerId, targetUserId: peerId,
candidate: event.candidate.toJSON(), candidate: event.candidate.toJSON(),
@@ -102,7 +108,7 @@ export function useVoiceChat({
// 创建音频元素播放远程流 // 创建音频元素播放远程流
if (isSpeakerEnabled) { if (isSpeakerEnabled) {
playRemoteStream(peerId, remoteStream); playRemoteStreamRef.current?.(peerId, remoteStream);
} }
}; };
@@ -116,7 +122,7 @@ export function useVoiceChat({
// WebRTC连接失败,如果策略允许,切换到服务器中转 // WebRTC连接失败,如果策略允许,切换到服务器中转
if (strategy === 'webrtc-fallback') { if (strategy === 'webrtc-fallback') {
console.log('[VoiceChat] WebRTC failed, falling back to server relay'); console.log('[VoiceChat] WebRTC failed, falling back to server relay');
switchToServerRelay(); switchToServerRelayRef.current?.();
} }
} }
}; };
@@ -136,6 +142,11 @@ export function useVoiceChat({
audio.srcObject = stream; audio.srcObject = stream;
}, []); }, []);
// 将播放函数存储到ref中,供createPeerConnection使用
useEffect(() => {
playRemoteStreamRef.current = playRemoteStream;
}, [playRemoteStream]);
// 停止播放远程音频流 // 停止播放远程音频流
const stopRemoteStream = useCallback((peerId: string) => { const stopRemoteStream = useCallback((peerId: string) => {
const audio = remoteAudioElementsRef.current.get(peerId); const audio = remoteAudioElementsRef.current.get(peerId);
@@ -147,6 +158,18 @@ export function useVoiceChat({
remoteStreamsRef.current.delete(peerId); 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) // 向对等端发起连接(创建offer)
const initiateConnection = useCallback(async (peerId: string) => { const initiateConnection = useCallback(async (peerId: string) => {
if (!socket || !localStreamRef.current) return; if (!socket || !localStreamRef.current) return;
@@ -178,17 +201,22 @@ export function useVoiceChat({
// 处理接收到的offer // 处理接收到的offer
const handleOffer = useCallback(async (data: { userId: string; offer: RTCSessionDescriptionInit }) => { 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); console.log('[VoiceChat] Received offer from', data.userId);
const pc = createPeerConnection(data.userId); const pc = createPeerConnection(data.userId);
// 添加本地流 // 如果有本地流,添加音频轨道
localStreamRef.current.getTracks().forEach(track => { if (localStreamRef.current) {
if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => {
pc.addTrack(track, localStreamRef.current); 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 { try {
await pc.setRemoteDescription(new RTCSessionDescription(data.offer)); await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
@@ -220,7 +248,6 @@ export function useVoiceChat({
// 处理接收到的ICE候选 // 处理接收到的ICE候选
const handleIceCandidate = useCallback(async (data: { userId: string; candidate: RTCIceCandidateInit }) => { 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); const pc = peerConnectionsRef.current.get(data.userId);
if (!pc) return; 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(() => { const startServerRelay = useCallback(() => {
if (!socket || !localStreamRef.current) { 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数据 // 播放服务器中转的音频 - 使用Web Audio API播放PCM数据
const playServerRelayAudio = useCallback(async (userId: string, audioData: number[], sampleRate: number = 16000) => { const playServerRelayAudio = useCallback(async (userId: string, audioData: number[], sampleRate: number = 16000) => {
if (!isSpeakerEnabled) return; 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(() => { const cleanup = useCallback(() => {
stopLocalStream(); stopLocalStream();
@@ -412,10 +432,23 @@ export function useVoiceChat({
// 仅使用服务器中转 // 仅使用服务器中转
startServerRelay(); startServerRelay();
} else { } else {
// 使用WebRTC,失败时自动切换到服务器中转 // 使用WebRTC P2P连接
// 这里需要获取房间内其他成员列表,然后向每个成员发起连接 console.log('[VoiceChat] WebRTC mode - initiating peer connections');
// 这部分逻辑需要在WatchRoomProvider中获取members列表
console.log('[VoiceChat] WebRTC mode, waiting for 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); setIsConnecting(false);
@@ -435,7 +468,7 @@ export function useVoiceChat({
cleanup(); cleanup();
} }
}; };
}, [isMicEnabled, socket, roomId, strategy, getLocalStream, stopLocalStream, cleanupWebRTC, stopServerRelay, startServerRelay, cleanup]); }, [isMicEnabled, socket, roomId, strategy, members, getLocalStream, stopLocalStream, cleanupWebRTC, stopServerRelay, startServerRelay, cleanup, initiateConnection]);
// 监听喇叭状态变化 // 监听喇叭状态变化
useEffect(() => { useEffect(() => {
@@ -470,6 +503,12 @@ export function useVoiceChat({
socket.on('voice:answer', handleAnswer); socket.on('voice:answer', handleAnswer);
socket.on('voice:ice', handleIceCandidate); 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 }) => { const handleAudioChunk = (data: { userId: string; audioData: number[]; sampleRate?: number }) => {
if (strategy === 'server-only' || !peerConnectionsRef.current.has(data.userId)) { if (strategy === 'server-only' || !peerConnectionsRef.current.has(data.userId)) {
@@ -484,10 +523,37 @@ export function useVoiceChat({
socket.off('voice:offer', handleOffer); socket.off('voice:offer', handleOffer);
socket.off('voice:answer', handleAnswer); socket.off('voice:answer', handleAnswer);
socket.off('voice:ice', handleIceCandidate); socket.off('voice:ice', handleIceCandidate);
socket.off('voice:mic-enabled');
socket.off('voice:audio-chunk', handleAudioChunk); socket.off('voice:audio-chunk', handleAudioChunk);
}; };
}, [socket, strategy, handleOffer, handleAnswer, handleIceCandidate, playServerRelayAudio]); }, [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(() => { useEffect(() => {
return () => { return () => {