voice v1.1
This commit is contained in:
@ -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) => (
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/renderer/realtime/voice/voiceStore.ts
Normal file
30
src/renderer/realtime/voice/voiceStore.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user