completed Audio frontend

This commit is contained in:
PxlLoewe
2025-03-17 00:41:07 -07:00
parent e722adaf8e
commit 9a7049ec44
28 changed files with 252 additions and 889 deletions

View File

@@ -1,32 +0,0 @@
"use client";
import { useTalkStore } from "../../_store/useTalkStore";
export const ToggleTalkButton = () => {
const { isTalking, toggleTalking } = useTalkStore();
return (
<button
onClick={toggleTalking}
className={`${
isTalking
? "bg-red-500 hover:bg-red-600"
: "bg-transparent hover:bg-neutral-300"
}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z"
/>
</svg>
</button>
);
};

View File

@@ -1,11 +1,10 @@
"use client";
import { ToggleTalkButton } from "../ToggleTalkButton";
import Link from "next/link";
import { Connection } from "../Connection";
import { Connection } from "./_components/Connection";
import { ThemeSwap } from "./_components/ThemeSwap";
import { useState } from "react";
import { Audio } from "./_components/Audio";
import { useState } from "react";
export default function Navbar() {
const [isDark, setIsDark] = useState(false);
@@ -25,14 +24,7 @@ export default function Navbar() {
<a className="btn btn-ghost text-xl">VAR Leitstelle V2</a>
</div>
<div className="flex gap-2">
<ul className="menu menu-horizontal bg-base-200 rounded-box flex items-center gap-2">
<li>
<ToggleTalkButton />
</li>
<li>
<Audio />
</li>
</ul>
<Audio />
</div>
<div className="flex gap-6">
<ThemeSwap isDark={isDark} toggleTheme={toggleTheme} />

View File

@@ -1,159 +1,111 @@
"use client";
import { useEffect, useState } from "react";
import { useConnectionStore } from "_store/connectionStore";
import {
LocalParticipant,
LocalTrackPublication,
Participant,
RemoteParticipant,
RemoteTrack,
RemoteTrackPublication,
Room,
RoomEvent,
Track,
VideoPresets,
} from "livekit-client";
import { connectionStore } from "../../../../_store/connectionStore";
Circle,
Disc,
Mic,
PlugZap,
ShieldQuestion,
Signal,
SignalLow,
SignalMedium,
WifiOff,
ZapOff,
} from "lucide-react";
import { useAudioStore } from "_store/audioStore";
import { cn } from "helpers/cn";
import { ConnectionQuality } from "livekit-client";
const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"];
export const Audio = () => {
const connection = connectionStore();
const [token, setToken] = useState("");
const [room, setRoom] = useState<Room | null>(null);
useEffect(() => {
const fetchToken = async () => {
const response = await fetch(
`${process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL}/livekit/token`,
);
const data = await response.json();
setToken(data.token);
};
fetchToken();
}, []);
const connection = useConnectionStore();
const {
isTalking,
toggleTalking,
connect,
state,
connectionQuality,
disconnect,
remoteParticipants,
room,
} = useAudioStore();
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
useEffect(() => {
const joinRoom = async () => {
if (!connection.isConnected) return;
if (!token) return;
if (!process.env.NEXT_PUBLIC_LIVEKIT_URL)
return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");
console.log("Connecting to room", {
token,
url: process.env.NEXT_PUBLIC_LIVEKIT_URL,
});
const url = "ws://localhost:7880";
/* const token =
"eyJhbGciOiJIUzI1NiJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6InF1aWNrc3RhcnQtcm9vbSJ9LCJpc3MiOiJBUElBbnNHZHRkWXAySG8iLCJleHAiOjE3NDIxNjAwOTIsIm5iZiI6MCwic3ViIjoicXVpY2tzdGFydC11c2VybmFtZSJ9.ih7my6oXby6yYBfzt5LXHKN5WZU9exIqS8CdpRJRLQI"; */
console.log("Connecting to room", { token, url });
const room = new Room({
// automatically manage subscribed video quality
adaptiveStream: true,
// optimize publishing bandwidth and CPU for published tracks
dynacast: true,
// default capture settings
videoCaptureDefaults: {
resolution: VideoPresets.h720.resolution,
},
});
// pre-warm connection, this can be called as early as your page is loaded
room.prepareConnection(url, token);
// set up event listeners
room
.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange)
.on(RoomEvent.Disconnected, handleDisconnect)
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished);
// connect to room
await room.connect(url, token);
console.log("connected to room", room.name);
// publish local camera and mic tracks
await room.localParticipant.enableCameraAndMicrophone();
function handleTrackSubscribed(
track: RemoteTrack,
publication: RemoteTrackPublication,
participant: RemoteParticipant,
) {
if (
track.kind === Track.Kind.Video ||
track.kind === Track.Kind.Audio
) {
// attach it to a new HTMLVideoElement or HTMLAudioElement
const element = track.attach();
}
}
function handleTrackUnsubscribed(
track: RemoteTrack,
publication: RemoteTrackPublication,
participant: RemoteParticipant,
) {
// remove tracks from all attached elements
track.detach();
}
function handleLocalTrackUnpublished(
publication: LocalTrackPublication,
participant: LocalParticipant,
) {
// when local tracks are ended, update UI to remove them from rendering
publication.track?.detach();
}
function handleActiveSpeakerChange(speakers: Participant[]) {
// show UI indicators when participant is speaking
}
function handleDisconnect() {
console.log("disconnected from room");
}
setRoom(room);
if (state === "connected") return;
connect(selectedRoom);
};
joinRoom();
return () => {
room?.disconnect();
disconnect();
};
}, [token, connection.isConnected]);
}, [connection.isConnected]);
return (
<>
<details className="dropdown">
<summary className="dropdown flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
/>
</svg>
<div className="badge badge-soft badge-success">1</div>
</summary>
<ul className="menu dropdown-content bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<li>
<a>Rufgruppe 1</a>
</li>
<li>
<a>Rufgruppe 2</a>
</li>
</ul>
</details>
<div className="bg-base-200 rounded-box flex items-center gap-2 p-3">
<button
onClick={() => {
if (state === "connected") toggleTalking();
if (state === "disconnected") connect(selectedRoom);
}}
className={cn(
"btn btn-sm btn-soft border-none hover:bg-inherit",
!isTalking && "bg-transparent hover:bg-sky-400/20",
isTalking && "bg-red-500 hover:bg-red-600",
state === "disconnected" && "bg-red-500 hover:bg-red-500",
state === "connecting" &&
"bg-yellow-500 hover:bg-yellow-500 cursor-default",
)}
>
{state === "connected" && <Mic />}
{state === "disconnected" && <WifiOff />}
{state === "connecting" && <PlugZap />}
</button>
{state === "connected" && (
<details className="dropdown">
<summary className="dropdown btn btn-ghost flex items-center gap-1">
{connectionQuality === ConnectionQuality.Excellent && <Signal />}
{connectionQuality === ConnectionQuality.Good && <SignalMedium />}
{connectionQuality === ConnectionQuality.Poor && <SignalLow />}
{connectionQuality === ConnectionQuality.Lost && <ZapOff />}
{connectionQuality === ConnectionQuality.Unknown && (
<ShieldQuestion />
)}
<div className="badge badge-soft badge-success">
{remoteParticipants}
</div>
</summary>
<ul className="menu dropdown-content bg-base-100 rounded-box z-[99999999] w-52 p-2 shadow-sm">
{ROOMS.map((r) => (
<li key={r}>
<button
className="btn btn-sm btn-ghost text-left"
onClick={() => {
if (selectedRoom === r) return;
setSelectedRoom(r);
connect(r);
}}
>
{room?.name === r && (
<Disc className="text-success text-sm" width={15} />
)}
{r}
</button>
</li>
))}
</ul>
</details>
)}
</div>
</>
);
};

View File

@@ -1,12 +1,12 @@
"use client";
import { useSession } from "next-auth/react";
import { connectionStore } from "../../_store/connectionStore";
import { useConnectionStore } from "../../../../_store/connectionStore";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
export const ConnectionBtn = () => {
const modalRef = useRef<HTMLDialogElement>(null);
const connection = connectionStore((state) => state);
const connection = useConnectionStore((state) => state);
const [form, setForm] = useState({
logoffTime: "",
selectedZone: "LST_01",