voice v1.1

This commit is contained in:
aurinex
2026-01-02 16:23:26 +05:00
parent d90ef2e535
commit a76a8b5656
3 changed files with 85 additions and 22 deletions

View File

@ -1,4 +1,6 @@
import { useVoiceRoom } from '../realtime/voice/useVoiceRoom'; import { useVoiceRoom } from '../realtime/voice/useVoiceRoom';
import { useState, useEffect } from 'react';
import { getVoiceState, subscribeVoice } from '../realtime/voice/voiceStore';
export function VoicePanel({ export function VoicePanel({
roomId, roomId,
@ -7,19 +9,26 @@ export function VoicePanel({
roomId: string; roomId: string;
username: string; username: string;
}) { }) {
const voice = useVoiceRoom(roomId, username); const [voice, setVoice] = useState(getVoiceState());
const { connect, disconnect, toggleMute } = useVoiceRoom(roomId, username);
useEffect(() => {
return subscribeVoice(() => {
setVoice({ ...getVoiceState() });
});
}, []);
return ( return (
<div> <div>
<button onClick={voice.connect} disabled={voice.connected || !username}> <button disabled={voice.connected} onClick={connect}>
Войти в голос Войти
</button> </button>
<button onClick={voice.toggleMute}> <button onClick={toggleMute}>
{voice.muted ? 'Включить микрофон' : 'Выключить микрофон'} {voice.muted ? 'Включить микрофон' : 'Выключить микрофон'}
</button> </button>
<button onClick={voice.disconnect}>Выйти</button> <button onClick={disconnect}>Выйти</button>
<ul> <ul>
{voice.participants.map((u) => ( {voice.participants.map((u) => (

View File

@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { rtcConfig } from './rtcConfig'; import { rtcConfig } from './rtcConfig';
import type { WSMessage } from './types'; import type { WSMessage } from './types';
import { setVoiceState, getVoiceState } from './voiceStore';
type PeerMap = Map<string, RTCPeerConnection>; type PeerMap = Map<string, RTCPeerConnection>;
@ -9,9 +10,9 @@ export function useVoiceRoom(roomId: string, username: string) {
const peersRef = useRef<PeerMap>(new Map()); const peersRef = useRef<PeerMap>(new Map());
const streamRef = useRef<MediaStream | null>(null); const streamRef = useRef<MediaStream | null>(null);
const [connected, setConnected] = useState(false); // const [connected, setConnected] = useState(false);
const [participants, setParticipants] = useState<string[]>([]); // const [participants, setParticipants] = useState<string[]>([]);
const [muted, setMuted] = useState(false); // const [muted, setMuted] = useState(false);
const pendingIceRef = useRef<Map<string, RTCIceCandidateInit[]>>(new Map()); const pendingIceRef = useRef<Map<string, RTCIceCandidateInit[]>>(new Map());
@ -36,27 +37,48 @@ export function useVoiceRoom(roomId: string, username: string) {
wsRef.current = ws; wsRef.current = ws;
ws.onopen = () => { ws.onopen = () => {
setConnected(true); setVoiceState({
setParticipants([username]); connected: true,
participants: [username],
});
}; };
ws.onclose = () => { ws.onclose = () => {
setVoiceState({
connected: false,
participants: [],
muted: false,
});
cleanup(); cleanup();
setConnected(false);
}; };
// ws.onclose = () => {
// cleanup();
// setConnected(false);
// };
ws.onmessage = async (ev) => { ws.onmessage = async (ev) => {
const msg: WSMessage = JSON.parse(ev.data); const msg: WSMessage = JSON.parse(ev.data);
if (msg.type === 'join' && msg.user !== username) { if (msg.type === 'join' && msg.user !== username) {
await createPeer(msg.user, false); await createPeer(msg.user, false);
setParticipants((p) => (p.includes(msg.user) ? p : [...p, msg.user])); const { participants } = getVoiceState();
if (!participants.includes(msg.user)) {
setVoiceState({
participants: [...participants, msg.user],
});
}
} }
if (msg.type === 'leave') { if (msg.type === 'leave') {
removePeer(msg.user); removePeer(msg.user);
setParticipants((p) => p.filter((u) => u !== msg.user));
setVoiceState({
participants: getVoiceState().participants.filter(
(u) => u !== msg.user,
),
});
} }
if (msg.type === 'signal') { if (msg.type === 'signal') {
@ -191,10 +213,12 @@ export function useVoiceRoom(roomId: string, username: string) {
// --- mute --- // --- mute ---
const toggleMute = () => { const toggleMute = () => {
if (!streamRef.current) return; if (!streamRef.current) return;
streamRef.current.getAudioTracks().forEach((t) => {
t.enabled = !t.enabled; const enabled = !getVoiceState().muted;
setMuted(!t.enabled);
}); streamRef.current.getAudioTracks().forEach((t) => (t.enabled = !enabled));
setVoiceState({ muted: enabled });
}; };
// --- cleanup --- // --- cleanup ---
@ -213,16 +237,16 @@ export function useVoiceRoom(roomId: string, username: string) {
const disconnect = () => { const disconnect = () => {
cleanup(); cleanup();
setParticipants([]); setVoiceState({
setConnected(false); connected: false,
participants: [],
muted: false,
});
}; };
return { return {
connect, connect,
disconnect, disconnect,
toggleMute, toggleMute,
connected,
muted,
participants,
}; };
} }

View File

@ -0,0 +1,30 @@
type VoiceState = {
connected: boolean;
participants: string[];
muted: boolean;
};
const state: VoiceState = {
connected: false,
participants: [],
muted: false,
};
const listeners = new Set<() => void>();
export function getVoiceState() {
return state;
}
export function setVoiceState(patch: Partial<VoiceState>) {
Object.assign(state, patch);
listeners.forEach((l) => l());
}
export function subscribeVoice(cb: () => void): () => void {
listeners.add(cb);
return () => {
listeners.delete(cb);
};
}