This commit is contained in:
PxlLoewe
2025-07-25 21:30:03 -07:00
parent 1da23c3412
commit 2abdbf168f
2 changed files with 62 additions and 40 deletions

View File

@@ -25,6 +25,7 @@ import { useSounds } from "_components/Audio/useSounds";
export const Audio = () => { export const Audio = () => {
const { const {
speakingParticipants, speakingParticipants,
resetSpeakingParticipants,
isTalking, isTalking,
toggleTalking, toggleTalking,
transmitBlocked, transmitBlocked,
@@ -104,7 +105,7 @@ export const Audio = () => {
data-tip="Nachricht entfernen" data-tip="Nachricht entfernen"
> >
<button <button
className={cn("btn btn-sm btn-ghost border-warning bg-transparent ")} className={cn("btn btn-sm btn-ghost border-warning bg-transparent")}
onClick={() => { onClick={() => {
removeMessage(); removeMessage();
}} }}
@@ -123,9 +124,9 @@ export const Audio = () => {
> >
<button <button
className={cn( className={cn(
"btn btn-sm btn-soft bg-transparent border", "btn btn-sm btn-soft border bg-transparent",
canStopOtherSpeakers && speakingParticipants.length > 0 && "hover:bg-error", canStopOtherSpeakers && speakingParticipants.length > 0 && "hover:bg-error",
speakingParticipants.length > 0 && " hover:bg-errorborder", speakingParticipants.length > 0 && "hover:bg-errorborder",
isReceivingBlick && "border-warning", isReceivingBlick && "border-warning",
)} )}
onClick={() => { onClick={() => {
@@ -133,6 +134,7 @@ export const Audio = () => {
const payload = JSON.stringify({ const payload = JSON.stringify({
by: role, by: role,
}); });
resetSpeakingParticipants("dich");
speakingParticipants.forEach(async (p) => { speakingParticipants.forEach(async (p) => {
await room?.localParticipant.performRpc({ await room?.localParticipant.performRpc({
destinationIdentity: p.identity, destinationIdentity: p.identity,
@@ -159,24 +161,24 @@ export const Audio = () => {
transmitBlocked && "bg-yellow-500 hover:bg-yellow-500", transmitBlocked && "bg-yellow-500 hover:bg-yellow-500",
state === "disconnected" && "bg-red-500 hover:bg-red-500", state === "disconnected" && "bg-red-500 hover:bg-red-500",
state === "error" && "bg-red-500 hover:bg-red-500", state === "error" && "bg-red-500 hover:bg-red-500",
state === "connecting" && "bg-yellow-500 hover:bg-yellow-500 cursor-default", state === "connecting" && "cursor-default bg-yellow-500 hover:bg-yellow-500",
)} )}
> >
{state === "connected" && <Mic className="w-5 h-5" />} {state === "connected" && <Mic className="h-5 w-5" />}
{state === "disconnected" && <WifiOff className="w-5 h-5" />} {state === "disconnected" && <WifiOff className="h-5 w-5" />}
{state === "connecting" && <PlugZap className="w-5 h-5" />} {state === "connecting" && <PlugZap className="h-5 w-5" />}
{state === "error" && <ServerCrash className="w-5 h-5" />} {state === "error" && <ServerCrash className="h-5 w-5" />}
</button> </button>
{state === "connected" && ( {state === "connected" && (
<details className="dropdown relative z-[1050]"> <details className="dropdown relative z-[1050]">
<summary className="dropdown btn btn-ghost flex items-center gap-1"> <summary className="dropdown btn btn-ghost flex items-center gap-1">
{connectionQuality === ConnectionQuality.Excellent && <Signal className="w-5 h-5" />} {connectionQuality === ConnectionQuality.Excellent && <Signal className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Good && <SignalMedium className="w-5 h-5" />} {connectionQuality === ConnectionQuality.Good && <SignalMedium className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Poor && <SignalLow className="w-5 h-5" />} {connectionQuality === ConnectionQuality.Poor && <SignalLow className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Lost && <ZapOff className="w-5 h-5" />} {connectionQuality === ConnectionQuality.Lost && <ZapOff className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Unknown && ( {connectionQuality === ConnectionQuality.Unknown && (
<ShieldQuestion className="w-5 h-5" /> <ShieldQuestion className="h-5 w-5" />
)} )}
<div className="badge badge-sm badge-soft badge-success">{remoteParticipants}</div> <div className="badge badge-sm badge-soft badge-success">{remoteParticipants}</div>
</summary> </summary>
@@ -184,7 +186,7 @@ export const Audio = () => {
{ROOMS.map((r) => ( {ROOMS.map((r) => (
<li key={r}> <li key={r}>
<button <button
className="btn btn-sm btn-ghost text-left flex items-center justify-start gap-2 relative" className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
onClick={() => { onClick={() => {
if (!role) return; if (!role) return;
if (selectedRoom === r) return; if (selectedRoom === r) return;
@@ -193,7 +195,7 @@ export const Audio = () => {
}} }}
> >
{room?.name === r && ( {room?.name === r && (
<Disc className="text-success text-sm absolute left-2" width={15} /> <Disc className="text-success absolute left-2 text-sm" width={15} />
)} )}
<span className="flex-1 text-center">{r}</span> <span className="flex-1 text-center">{r}</span>
</button> </button>
@@ -201,12 +203,12 @@ export const Audio = () => {
))} ))}
<li> <li>
<button <button
className="btn btn-sm btn-ghost text-left flex items-center justify-start gap-2 relative" className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
onClick={() => { onClick={() => {
disconnect(); disconnect();
}} }}
> >
<WifiOff className="text-error text-sm absolute left-2" width={15} /> <WifiOff className="text-error absolute left-2 text-sm" width={15} />
<span className="flex-1 text-center">Disconnect</span> <span className="flex-1 text-center">Disconnect</span>
</button> </button>
</li> </li>

View File

@@ -25,28 +25,29 @@ import { usePilotConnectionStore } from "_store/pilot/connectionStore";
let interval: NodeJS.Timeout; let interval: NodeJS.Timeout;
type TalkState = { type TalkState = {
addSpeakingParticipant: (participant: Participant) => void;
connect: (roomName: string, role: string) => void;
connectionQuality: ConnectionQuality;
disconnect: () => void;
isTalking: boolean;
localRadioTrack: LocalTrackPublication | undefined;
message: string | null;
removeMessage: () => void;
removeSpeakingParticipant: (speakingParticipants: Participant) => void;
remoteParticipants: number;
resetSpeakingParticipants: (source: string) => void;
room: Room | null;
setSettings: (settings: Partial<TalkState["settings"]>) => void;
settings: { settings: {
micDeviceId: string | null; micDeviceId: string | null;
micVolume: number; micVolume: number;
radioVolume: number; radioVolume: number;
dmeVolume: number; dmeVolume: number;
}; };
isTalking: boolean;
transmitBlocked: boolean;
removeMessage: () => void;
state: "connecting" | "connected" | "disconnected" | "error";
message: string | null;
connectionQuality: ConnectionQuality;
remoteParticipants: number;
toggleTalking: () => void;
setSettings: (settings: Partial<TalkState["settings"]>) => void;
connect: (roomName: string, role: string) => void;
disconnect: () => void;
speakingParticipants: Participant[]; speakingParticipants: Participant[];
addSpeakingParticipant: (participant: Participant) => void; state: "connecting" | "connected" | "disconnected" | "error";
removeSpeakingParticipant: (speakingParticipants: Participant) => void; toggleTalking: () => void;
room: Room | null; transmitBlocked: boolean;
localRadioTrack: LocalTrackPublication | undefined;
}; };
const getToken = async (roomName: string) => { const getToken = async (roomName: string) => {
const response = await axios.get(`/api/livekit-token?roomName=${roomName}`); const response = await axios.get(`/api/livekit-token?roomName=${roomName}`);
@@ -71,6 +72,15 @@ export const useAudioStore = create<TalkState>((set, get) => ({
remoteParticipants: 0, remoteParticipants: 0,
connectionQuality: ConnectionQuality.Unknown, connectionQuality: ConnectionQuality.Unknown,
room: null, room: null,
resetSpeakingParticipants: (source: string) => {
set({
speakingParticipants: [],
isTalking: false,
transmitBlocked: false,
message: `Ruf beendet durch ${source || "eine unsichtbare Macht"}`,
});
get().room?.localParticipant.setMicrophoneEnabled(false);
},
addSpeakingParticipant: (participant) => { addSpeakingParticipant: (participant) => {
set((state) => { set((state) => {
if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) { if (!state.speakingParticipants.some((p) => p.identity === participant.identity)) {
@@ -201,10 +211,15 @@ export const useAudioStore = create<TalkState>((set, get) => ({
set({ localRadioTrack: publishedTrack }); set({ localRadioTrack: publishedTrack });
set({ state: "connected", room, message: null }); set({ state: "connected", room, isTalking: false, message: null });
}) })
.on(RoomEvent.Disconnected, () => { .on(RoomEvent.Disconnected, () => {
set({ state: "disconnected", speakingParticipants: [], transmitBlocked: false }); set({
state: "disconnected",
speakingParticipants: [],
transmitBlocked: false,
isTalking: false,
});
handleDisconnect(); handleDisconnect();
}) })
@@ -223,17 +238,22 @@ export const useAudioStore = create<TalkState>((set, get) => ({
room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => { room.registerRpcMethod("force-mute", async (data: RpcInvocationData) => {
const { by } = JSON.parse(data.payload); const { by } = JSON.parse(data.payload);
room.localParticipant.setMicrophoneEnabled(false); get().resetSpeakingParticipants(by);
useAudioStore.setState({ return "OK";
isTalking: false,
message: `Ruf beendet durch ${by || "eine unsichtbare Macht"}`,
});
return `Hello, ${data.callerIdentity}!`;
}); });
interval = setInterval(() => { interval = setInterval(() => {
// Filter forgotten participants
const oldSpeakingParticipants = get().speakingParticipants;
const speakingParticipants = oldSpeakingParticipants.filter((oP) => {
return Array.from(room.remoteParticipants.values()).find(
(p) => p.identity === oP.identity,
);
});
set({ set({
remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed remoteParticipants: room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed
speakingParticipants,
}); });
}, 500); }, 500);
} catch (error: Error | unknown) { } catch (error: Error | unknown) {