Files
var-monorepo/apps/dispatch/app/_components/Audio/Audio.tsx
2026-01-16 00:03:33 +01:00

223 lines
6.9 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
import {
Disc,
Mic,
PlugZap,
ServerCrash,
ShieldQuestion,
Signal,
SignalLow,
SignalMedium,
WifiOff,
ZapOff,
} from "lucide-react";
import { useAudioStore } from "_store/audioStore";
import { cn } from "@repo/shared-components";
import { ConnectionQuality } from "livekit-client";
import { ROOMS } from "_data/livekitRooms";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { useSession } from "next-auth/react";
import { useSounds } from "_components/Audio/useSounds";
export const Audio = () => {
const {
speakingParticipants,
resetSpeakingParticipants,
isTalking,
toggleTalking,
transmitBlocked,
connect,
state,
connectionQuality,
disconnect,
remoteParticipants,
room,
message,
removeMessage,
} = useAudioStore();
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
useSounds({
isReceiving: speakingParticipants.length > 0,
isTransmitting: isTalking,
unpausedTracks: speakingParticipants,
transmitBlocked,
});
const { selectedStation, status: pilotState } = usePilotConnectionStore((state) => state);
const { selectedZone, status: dispatcherState } = useDispatchConnectionStore((state) => state);
const session = useSession();
const [isReceivingBlick, setIsReceivingBlick] = useState(false);
const [recentSpeakers, setRecentSpeakers] = useState<typeof speakingParticipants>([]);
useEffect(() => {
if (speakingParticipants.length > 0) {
setRecentSpeakers(speakingParticipants);
} else if (recentSpeakers.length > 0) {
const timeout = setTimeout(() => {
setRecentSpeakers([]);
}, 10000);
return () => clearTimeout(timeout);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [speakingParticipants]);
useEffect(() => {
if (speakingParticipants.length > 0) {
setIsReceivingBlick(true);
const timeout = setInterval(() => {
setIsReceivingBlick((s) => !s);
}, 1000);
return () => {
clearTimeout(timeout);
setIsReceivingBlick(false);
};
}
}, [setIsReceivingBlick, speakingParticipants]);
useEffect(() => {
if (message && state !== "error") {
const timeout = setTimeout(() => {
removeMessage();
}, 10000);
return () => clearTimeout(timeout);
}
}, [message, removeMessage, state]);
const displayedSpeakers = speakingParticipants.length > 0 ? speakingParticipants : recentSpeakers;
const canStopOtherSpeakers = dispatcherState === "connected";
const role =
(dispatcherState === "connected" && selectedZone) ||
(pilotState == "connected" && selectedStation?.bosCallsignShort) ||
session.data?.user?.publicId;
return (
<>
<div className="bg-base-200 rounded-box flex items-center gap-2 p-1">
{message && (
<div
className="tooltip tooltip-left tooltip-warning font-semibold"
data-tip="Nachricht entfernen"
>
<button
className={cn("btn btn-sm btn-ghost border-warning bg-transparent")}
onClick={() => {
removeMessage();
}}
>
{message}
</button>
</div>
)}
{displayedSpeakers.length > 0 && (
<div
className={cn(
"tooltip-left tooltip-error font-semibold",
canStopOtherSpeakers && speakingParticipants.length > 0 && "tooltip",
)}
data-tip="Funkspruch unterbrechen"
>
<button
className={cn(
"btn btn-sm btn-soft border bg-transparent",
canStopOtherSpeakers && speakingParticipants.length > 0 && "hover:bg-error",
speakingParticipants.length > 0 && "hover:bg-errorborder",
isReceivingBlick && "border-warning",
)}
onClick={() => {
if (!canStopOtherSpeakers) return;
const payload = JSON.stringify({
by: role,
});
resetSpeakingParticipants("dich");
speakingParticipants.forEach(async (p) => {
await room?.localParticipant.performRpc({
destinationIdentity: p.identity,
method: "force-mute",
payload,
});
});
}}
>
{displayedSpeakers.map((p) => p.attributes.role).join(", ") || ""}
</button>
</div>
)}
<button
onClick={() => {
if (state === "connected") toggleTalking();
if (!role) return;
if (state === "error" || state === "disconnected") connect(selectedRoom, role);
}}
className={cn(
"btn btn-sm btn-soft border-none hover:bg-inherit",
!isTalking && "bg-transparent hover:bg-sky-400/20",
isTalking && "bg-green-700 hover:bg-green-600",
transmitBlocked && "bg-yellow-500 hover:bg-yellow-500",
state === "disconnected" && "bg-red-500 hover:bg-red-500",
state === "error" && "bg-red-500 hover:bg-red-500",
state === "connecting" && "cursor-default bg-yellow-500 hover:bg-yellow-500",
)}
>
{state === "connected" && <Mic className="h-5 w-5" />}
{state === "disconnected" && <WifiOff className="h-5 w-5" />}
{state === "connecting" && <PlugZap className="h-5 w-5" />}
{state === "error" && <ServerCrash className="h-5 w-5" />}
</button>
{state === "connected" && (
<details className="dropdown relative z-[1050]">
<summary className="dropdown btn btn-ghost flex items-center gap-1">
{connectionQuality === ConnectionQuality.Excellent && <Signal className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Good && <SignalMedium className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Poor && <SignalLow className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Lost && <ZapOff className="h-5 w-5" />}
{connectionQuality === ConnectionQuality.Unknown && (
<ShieldQuestion className="h-5 w-5" />
)}
<div className="badge badge-sm badge-soft badge-success">{remoteParticipants}</div>
</summary>
<ul className="menu dropdown-content bg-base-200 rounded-box z-[1050] w-52 p-2 shadow-sm">
{ROOMS.map((r) => (
<li key={r}>
<button
className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
onClick={() => {
if (!role) return;
if (selectedRoom === r) return;
setSelectedRoom(r);
connect(r, role);
}}
>
{room?.name === r && (
<Disc className="text-success absolute left-2 text-sm" width={15} />
)}
<span className="flex-1 text-center">{r}</span>
</button>
</li>
))}
<li>
<button
className="btn btn-sm btn-ghost relative flex items-center justify-start gap-2 text-left"
onClick={() => {
disconnect();
}}
>
<WifiOff className="text-error absolute left-2 text-sm" width={15} />
<span className="flex-1 text-center">Disconnect</span>
</button>
</li>
</ul>
</details>
)}
</div>
</>
);
};