Update environment configuration for production, refactor WebRTC components, and enhance chat functionality. Replace deprecated SessionUsersPanel with SessionPage, integrate chat history loading, and improve audio/video toggle handling. Remove unused SessionUsersPanel2 component and update related socket event handling in the server.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../lib/api";
|
||||
import type { ChatMessage } from "../lib/webrtc";
|
||||
|
||||
interface ChatHistoryResponse {
|
||||
success: boolean;
|
||||
messages: ChatMessage[];
|
||||
count: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const useChatHistory = (sessionId: string | undefined, enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: ["chat-history", sessionId],
|
||||
queryFn: async () => {
|
||||
if (!sessionId) {
|
||||
throw new Error("Session ID is required");
|
||||
}
|
||||
|
||||
const response = await api
|
||||
.get(`sessions/${sessionId}/messages`)
|
||||
.json<ChatHistoryResponse>();
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || "Failed to load chat history");
|
||||
}
|
||||
|
||||
return response.messages;
|
||||
},
|
||||
enabled: enabled && !!sessionId,
|
||||
staleTime: 1000 * 60 * 5, // 5 минут - история считается актуальной
|
||||
gcTime: 1000 * 60 * 30, // 30 минут в кэше
|
||||
refetchOnWindowFocus: false, // Не перезагружать при фокусе
|
||||
refetchOnReconnect: false, // Не перезагружать при реконнекте
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
interface UseVoiceActivityOptions {
|
||||
threshold?: number; // Порог громкости (0-100)
|
||||
smoothingTimeConstant?: number; // Сглаживание (0-1)
|
||||
fftSize?: number; // Размер FFT для анализа
|
||||
debounceTime?: number; // Время задержки выключения индикатора (ms)
|
||||
}
|
||||
|
||||
/**
|
||||
* Хук для определения голосовой активности в MediaStream
|
||||
* @param stream - MediaStream для анализа
|
||||
* @param options - Опции для настройки детекции
|
||||
* @returns объект с isSpeaking и audioLevel
|
||||
*/
|
||||
export function useVoiceActivity(
|
||||
stream: MediaStream | null | undefined,
|
||||
options: UseVoiceActivityOptions = {}
|
||||
): { isSpeaking: boolean; audioLevel: number } {
|
||||
const {
|
||||
threshold = 6, // Низкий порог для непрерывной речи (ловит тихие паузы между словами)
|
||||
smoothingTimeConstant = 0.8, // Высокое сглаживание для стабильности
|
||||
fftSize = 2048, // Больший размер для лучшей точности голоса
|
||||
debounceTime = 1000, // 1 секунда задержки выключения
|
||||
} = options;
|
||||
|
||||
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||
const [audioLevel, setAudioLevel] = useState(0);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const analyserRef = useRef<AnalyserNode | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
const lastSpeakingTimeRef = useRef<number>(0);
|
||||
const speakingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stream) {
|
||||
setIsSpeaking(false);
|
||||
setAudioLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
if (audioTracks.length === 0) {
|
||||
setIsSpeaking(false);
|
||||
setAudioLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что аудио трек активен
|
||||
const audioTrack = audioTracks[0];
|
||||
if (!audioTrack.enabled) {
|
||||
setIsSpeaking(false);
|
||||
setAudioLevel(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаем AudioContext
|
||||
const audioContext = new AudioContext();
|
||||
audioContextRef.current = audioContext;
|
||||
|
||||
// Создаем источник из MediaStream
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
// Создаем анализатор
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = fftSize;
|
||||
analyser.smoothingTimeConstant = smoothingTimeConstant;
|
||||
analyserRef.current = analyser;
|
||||
|
||||
// Подключаем источник к анализатору
|
||||
source.connect(analyser);
|
||||
|
||||
// Буфер для данных
|
||||
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
|
||||
// Счетчик для логирования (не логируем каждый кадр)
|
||||
let frameCount = 0;
|
||||
|
||||
// Функция для проверки активности
|
||||
const checkVoiceActivity = () => {
|
||||
if (!analyserRef.current) return;
|
||||
|
||||
// Получаем waveform (временные данные)
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
|
||||
// Вычисляем RMS (Root Mean Square) - эффективную громкость
|
||||
// dataArray содержит значения от 0 до 255, где 128 = тишина
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
const normalized = (dataArray[i] - 128) / 128; // Нормализуем к диапазону -1 до 1
|
||||
sumSquares += normalized * normalized;
|
||||
}
|
||||
const rms = Math.sqrt(sumSquares / dataArray.length);
|
||||
|
||||
// Преобразуем RMS в проценты (калибровка под Chrome)
|
||||
// RMS обычно в диапазоне 0.0-0.3 (тишина-громкая речь)
|
||||
// Используем нелинейную кривую для лучшего соответствия Chrome
|
||||
const audioLevel = Math.min(100, Math.pow(rms * 100, 1.2));
|
||||
|
||||
// Обновляем уровень громкости постоянно
|
||||
setAudioLevel(audioLevel);
|
||||
|
||||
// Проверяем, превышен ли порог
|
||||
const isActive = audioLevel > threshold;
|
||||
|
||||
if (isActive) {
|
||||
// Если человек говорит сейчас - обновляем время и включаем индикатор
|
||||
lastSpeakingTimeRef.current = Date.now();
|
||||
setIsSpeaking(true);
|
||||
|
||||
// Отменяем предыдущий таймер выключения (если был)
|
||||
if (speakingTimeoutRef.current) {
|
||||
clearTimeout(speakingTimeoutRef.current);
|
||||
speakingTimeoutRef.current = null;
|
||||
}
|
||||
} else {
|
||||
// Если сейчас тишина, проверяем прошло ли debounceTime с последней активности
|
||||
const timeSinceLastSpeaking =
|
||||
Date.now() - lastSpeakingTimeRef.current;
|
||||
|
||||
if (timeSinceLastSpeaking >= debounceTime) {
|
||||
// Прошло достаточно времени - выключаем индикатор
|
||||
setIsSpeaking(false);
|
||||
} else if (!speakingTimeoutRef.current) {
|
||||
// Ставим таймер на выключение через оставшееся время
|
||||
const remainingTime = debounceTime - timeSinceLastSpeaking;
|
||||
speakingTimeoutRef.current = setTimeout(() => {
|
||||
setIsSpeaking(false);
|
||||
speakingTimeoutRef.current = null;
|
||||
}, remainingTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Логируем каждые 30 кадров (~500ms при 60fps)
|
||||
frameCount++;
|
||||
if (frameCount % 30 === 0) {
|
||||
console.log(
|
||||
`[VoiceActivity] Level: ${audioLevel.toFixed(
|
||||
1
|
||||
)}% | RMS: ${rms.toFixed(
|
||||
4
|
||||
)} | Threshold: ${threshold}% | Speaking: ${
|
||||
isActive ? "🟢 YES" : "⚪ NO"
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// Запланировать следующую проверку
|
||||
animationFrameRef.current = requestAnimationFrame(checkVoiceActivity);
|
||||
};
|
||||
|
||||
// Запускаем проверку
|
||||
checkVoiceActivity();
|
||||
|
||||
console.log(
|
||||
`[useVoiceActivity] Started voice activity detection - Threshold: ${threshold}, FFT: ${fftSize}, Smoothing: ${smoothingTimeConstant}, Debounce: ${debounceTime}ms`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useVoiceActivity] Error setting up voice detection:",
|
||||
error
|
||||
);
|
||||
setIsSpeaking(false);
|
||||
setAudioLevel(0);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (animationFrameRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = null;
|
||||
}
|
||||
|
||||
if (speakingTimeoutRef.current !== null) {
|
||||
clearTimeout(speakingTimeoutRef.current);
|
||||
speakingTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (analyserRef.current) {
|
||||
analyserRef.current.disconnect();
|
||||
analyserRef.current = null;
|
||||
}
|
||||
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.close();
|
||||
audioContextRef.current = null;
|
||||
}
|
||||
|
||||
setIsSpeaking(false);
|
||||
setAudioLevel(0);
|
||||
console.log("[useVoiceActivity] Cleaned up voice activity detection");
|
||||
};
|
||||
}, [stream, threshold, smoothingTimeConstant, fftSize, debounceTime]);
|
||||
|
||||
return { isSpeaking, audioLevel };
|
||||
}
|
||||
@@ -98,15 +98,33 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
onParticipantLeft: (participantId) => {
|
||||
setParticipants((prev) => prev.filter((p) => p.id !== participantId));
|
||||
},
|
||||
onParticipantAudioToggle: (participantId, isEnabled) => {
|
||||
console.log(`[useWebRTC] Audio toggle for ${participantId}: ${isEnabled}`);
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === participantId ? { ...p, isMuted: !isEnabled } : p
|
||||
)
|
||||
);
|
||||
},
|
||||
onParticipantVideoToggle: (participantId, isEnabled) => {
|
||||
console.log(`[useWebRTC] Video toggle for ${participantId}: ${isEnabled}`);
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === participantId ? { ...p, isVideoOff: !isEnabled } : p
|
||||
)
|
||||
);
|
||||
},
|
||||
onParticipantSpeakingChange: (participantId, isSpeaking) => {
|
||||
setParticipants((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === participantId ? { ...p, isSpeaking } : p
|
||||
)
|
||||
);
|
||||
},
|
||||
onChatMessage: (message) => {
|
||||
console.log("[useWebRTC] onChatMessage called:", message);
|
||||
setChatMessages((prev) => [...prev, message]);
|
||||
},
|
||||
onDataChannelOpen: () => {
|
||||
// DataChannel opened
|
||||
},
|
||||
onDataChannelClose: () => {
|
||||
// DataChannel closed
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("[useWebRTC] Error:", error);
|
||||
},
|
||||
@@ -202,6 +220,11 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
setParticipants([]);
|
||||
};
|
||||
|
||||
const updateSpeakingState = (isSpeaking: boolean) => {
|
||||
if (!webrtcServiceInstance) return;
|
||||
webrtcServiceInstance.updateSpeakingState(isSpeaking);
|
||||
};
|
||||
|
||||
return {
|
||||
localStream,
|
||||
participants,
|
||||
@@ -214,6 +237,7 @@ export const useWebRTC = (roomId?: string, autoJoin = false) => {
|
||||
toggleAudio,
|
||||
toggleVideo,
|
||||
sendMessage,
|
||||
updateSpeakingState,
|
||||
joinRoom,
|
||||
leaveRoom,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user