修正语音webrtc模式
This commit is contained in:
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user