feat(voice): голосовой интерфейс на LiveKit (Фаза 3, клиент)
- `livekit-client` подключён как ленивый чанк: SDK грузится только при входе в комнату, стартовый бандл не вырос; - `src/lib/livekit.ts` — единственное место знания о SDK: подключение, дорожки микрофона/камеры/экрана, снимки участников (речь, флаги, качество связи), выбор устройств на лету, разбор exp токена; - `src/stores/voiceConnection.ts` — сессия голоса: статусы idle/connecting/connected/reconnecting/error, продление токена за минуту до истечения с переподключением и повторным применением флагов, серверный мьют и глушение, громкость участников, перемещение, подсказка «говорите с выключенным микрофоном» не чаще раза в 5 с; - UI: состав голосовой комнаты в сайдбаре (речь, флаги, качество), панель комнаты с плитками и модерацией, меню участника (личная громкость, мьют, перемещение), drag&drop между голосовыми комнатами, кнопки панели пользователя управляют реальной сессией, при выключенном голосе — понятная подсказка; - 368 тестов Vitest (было 330), check/lint/test/build зелёные.
This commit is contained in:
@@ -45,6 +45,8 @@ function VoiceTile({
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
||||
const volume = useVoiceStore((state) => state.participantVolumes[occupant.userId] ?? 100);
|
||||
// Общая громкость звука умножается на личную громкость участника.
|
||||
const outputVolume = useVoiceStore((state) => state.outputVolume);
|
||||
const deafened = useVoiceConnectionStore((state) => state.deafened);
|
||||
const media = occupant.participant;
|
||||
const video = media?.screen ?? media?.video;
|
||||
@@ -63,6 +65,14 @@ function VoiceTile({
|
||||
data-speaking={occupant.isSpeaking}
|
||||
draggable={permissions.move && !occupant.isLocal}
|
||||
onDragStart={onDragStart}
|
||||
onContextMenu={(event) => {
|
||||
// ПКМ по плитке открывает меню участника (AGENT.md 7.14).
|
||||
if (occupant.isLocal) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setMenuOpen(true);
|
||||
}}
|
||||
className={`flex flex-col gap-2 rounded-[var(--radius-lg)] border bg-surface-1 p-3 transition-colors duration-[var(--duration-fast)] ${
|
||||
occupant.isSpeaking ? 'border-success/70' : 'border-border/60'
|
||||
}`}
|
||||
@@ -81,7 +91,11 @@ function VoiceTile({
|
||||
<RemoteVideo track={video} muted={occupant.isLocal} />
|
||||
)}
|
||||
{media?.audio === undefined ? null : (
|
||||
<RemoteAudio track={media.audio} volume={volume} muted={deafened} />
|
||||
<RemoteAudio
|
||||
track={media.audio}
|
||||
volume={(volume * outputVolume) / 100}
|
||||
muted={deafened}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user