added Audio permission
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
NEXTAUTH_SECRET=
|
NEXTAUTH_SECRET=dispatch
|
||||||
NEXTAUTH_COOKIE_PREFIX=
|
NEXTAUTH_COOKIE_PREFIX=DISPATCH
|
||||||
|
NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002
|
||||||
NEXTAUTH_URL=http://localhost:3001
|
NEXTAUTH_URL=http://localhost:3001
|
||||||
NEXT_PUBLIC_PUBLIC_URL=http://localhost:3001
|
|
||||||
NEXT_PUBLIC_HUB_URL=http://localhost:3000
|
NEXT_PUBLIC_HUB_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_PUBLIC_URL=http://localhost:3001
|
||||||
NEXT_PUBLIC_SERVICE_ID=1
|
NEXT_PUBLIC_SERVICE_ID=1
|
||||||
NEXT_PUBLIC_DISPATCH_SERVER_URL=http://localhost:3002
|
DATABASE_URL=postgresql://persistant-data:persistant-data-pw@localhost:5432/var
|
||||||
|
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Disc,
|
Disc,
|
||||||
Mic,
|
Mic,
|
||||||
PlugZap,
|
PlugZap,
|
||||||
|
ServerCrash,
|
||||||
ShieldQuestion,
|
ShieldQuestion,
|
||||||
Signal,
|
Signal,
|
||||||
SignalLow,
|
SignalLow,
|
||||||
@@ -16,8 +17,7 @@ import {
|
|||||||
import { useAudioStore } from "_store/audioStore";
|
import { useAudioStore } from "_store/audioStore";
|
||||||
import { cn } from "helpers/cn";
|
import { cn } from "helpers/cn";
|
||||||
import { ConnectionQuality } from "livekit-client";
|
import { ConnectionQuality } from "livekit-client";
|
||||||
|
import { ROOMS } from "data/livekitRooms";
|
||||||
const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"];
|
|
||||||
|
|
||||||
export const Audio = () => {
|
export const Audio = () => {
|
||||||
const connection = useConnectionStore();
|
const connection = useConnectionStore();
|
||||||
@@ -30,6 +30,7 @@ export const Audio = () => {
|
|||||||
disconnect,
|
disconnect,
|
||||||
remoteParticipants,
|
remoteParticipants,
|
||||||
room,
|
room,
|
||||||
|
message,
|
||||||
} = useAudioStore();
|
} = useAudioStore();
|
||||||
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
|
const [selectedRoom, setSelectedRoom] = useState<string>("LST_01");
|
||||||
|
|
||||||
@@ -50,16 +51,21 @@ export const Audio = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="bg-base-200 rounded-box flex items-center gap-2 p-1">
|
<div className="bg-base-200 rounded-box flex items-center gap-2 p-1">
|
||||||
|
{state === "error" && (
|
||||||
|
<div className="h-4 flex items-center">{message}</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (state === "connected") toggleTalking();
|
if (state === "connected") toggleTalking();
|
||||||
if (state === "disconnected") connect(selectedRoom);
|
if (state === "error" || state === "disconnected")
|
||||||
|
connect(selectedRoom);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"btn btn-sm btn-soft border-none hover:bg-inherit",
|
"btn btn-sm btn-soft border-none hover:bg-inherit",
|
||||||
!isTalking && "bg-transparent hover:bg-sky-400/20",
|
!isTalking && "bg-transparent hover:bg-sky-400/20",
|
||||||
isTalking && "bg-red-500 hover:bg-red-600",
|
isTalking && "bg-red-500 hover:bg-red-600",
|
||||||
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 === "connecting" &&
|
state === "connecting" &&
|
||||||
"bg-yellow-500 hover:bg-yellow-500 cursor-default",
|
"bg-yellow-500 hover:bg-yellow-500 cursor-default",
|
||||||
)}
|
)}
|
||||||
@@ -67,6 +73,7 @@ export const Audio = () => {
|
|||||||
{state === "connected" && <Mic className="w-5 h-5" />}
|
{state === "connected" && <Mic className="w-5 h-5" />}
|
||||||
{state === "disconnected" && <WifiOff className="w-5 h-5" />}
|
{state === "disconnected" && <WifiOff className="w-5 h-5" />}
|
||||||
{state === "connecting" && <PlugZap className="w-5 h-5" />}
|
{state === "connecting" && <PlugZap className="w-5 h-5" />}
|
||||||
|
{state === "error" && <ServerCrash className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{state === "connected" && (
|
{state === "connected" && (
|
||||||
@@ -112,6 +119,20 @@ export const Audio = () => {
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-ghost text-left flex items-center justify-start gap-2 relative"
|
||||||
|
onClick={() => {
|
||||||
|
disconnect();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WifiOff
|
||||||
|
className="text-error text-sm absolute left-2"
|
||||||
|
width={15}
|
||||||
|
/>
|
||||||
|
<span className="flex-1 text-center">Disconnect</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ let interval: NodeJS.Timeout;
|
|||||||
|
|
||||||
type TalkState = {
|
type TalkState = {
|
||||||
isTalking: boolean;
|
isTalking: boolean;
|
||||||
state: "connecting" | "connected" | "disconnected";
|
state: "connecting" | "connected" | "disconnected" | "error";
|
||||||
|
message: string | null;
|
||||||
connectionQuality: ConnectionQuality;
|
connectionQuality: ConnectionQuality;
|
||||||
remoteParticipants: number;
|
remoteParticipants: number;
|
||||||
toggleTalking: () => void;
|
toggleTalking: () => void;
|
||||||
@@ -21,60 +22,73 @@ type TalkState = {
|
|||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
room: Room | null;
|
room: Room | null;
|
||||||
};
|
};
|
||||||
const getToken = async (roomName: string) => {
|
const getToken = async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/livekit/token`);
|
||||||
`${process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL}/livekit/token?roomName=${roomName}`,
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.token;
|
return data.token;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAudioStore = create<TalkState>((set, get) => ({
|
export const useAudioStore = create<TalkState>((set, get) => ({
|
||||||
isTalking: false,
|
isTalking: false,
|
||||||
|
message: null,
|
||||||
state: "disconnected",
|
state: "disconnected",
|
||||||
remoteParticipants: 0,
|
remoteParticipants: 0,
|
||||||
connectionQuality: ConnectionQuality.Unknown,
|
connectionQuality: ConnectionQuality.Unknown,
|
||||||
room: null,
|
room: null,
|
||||||
toggleTalking: () => set((state) => ({ isTalking: !state.isTalking })),
|
toggleTalking: () => set((state) => ({ isTalking: !state.isTalking })),
|
||||||
connect: async (roomName) => {
|
connect: async (roomName) => {
|
||||||
const connectedRoom = get().room;
|
try {
|
||||||
if (interval) clearInterval(interval);
|
// Clean old room
|
||||||
if (connectedRoom) {
|
const connectedRoom = get().room;
|
||||||
connectedRoom.disconnect();
|
if (interval) clearInterval(interval);
|
||||||
connectedRoom.removeAllListeners();
|
if (connectedRoom) {
|
||||||
|
connectedRoom.disconnect();
|
||||||
|
connectedRoom.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ state: "connecting" });
|
||||||
|
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||||
|
if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");
|
||||||
|
|
||||||
|
const token = await getToken();
|
||||||
|
if (!token) throw new Error("Fehlende Berechtigung");
|
||||||
|
console.log("Token: ", token);
|
||||||
|
const room = new Room();
|
||||||
|
await room.prepareConnection(url, token);
|
||||||
|
room
|
||||||
|
// Connection events
|
||||||
|
.on(RoomEvent.Connected, () => {
|
||||||
|
set({ state: "connected", room, message: null });
|
||||||
|
})
|
||||||
|
.on(RoomEvent.Disconnected, () => {
|
||||||
|
set({ state: "disconnected" });
|
||||||
|
|
||||||
|
handleDisconnect();
|
||||||
|
})
|
||||||
|
.on(RoomEvent.ConnectionQualityChanged, (connectionQuality) =>
|
||||||
|
set({ connectionQuality }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track events
|
||||||
|
.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||||
|
.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||||
|
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange)
|
||||||
|
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished);
|
||||||
|
await room.connect(url, token);
|
||||||
|
interval = setInterval(() => {
|
||||||
|
set({
|
||||||
|
remoteParticipants:
|
||||||
|
room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
console.error("Error occured: ", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
set({ state: "error", message: error.message });
|
||||||
|
} else {
|
||||||
|
set({ state: "error", message: "Unknown error" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
set({ state: "connecting" });
|
|
||||||
const url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
|
||||||
if (!url) return console.error("NEXT_PUBLIC_LIVEKIT_URL not set");
|
|
||||||
const token = await getToken(roomName);
|
|
||||||
const room = new Room();
|
|
||||||
await room.prepareConnection(url, token);
|
|
||||||
room
|
|
||||||
// Connection events
|
|
||||||
.on(RoomEvent.Connected, () => {
|
|
||||||
set({ state: "connected", room });
|
|
||||||
})
|
|
||||||
.on(RoomEvent.Disconnected, () => {
|
|
||||||
set({ state: "disconnected" });
|
|
||||||
|
|
||||||
handleDisconnect();
|
|
||||||
})
|
|
||||||
.on(RoomEvent.ConnectionQualityChanged, (connectionQuality) =>
|
|
||||||
set({ connectionQuality }),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Track events
|
|
||||||
.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
|
||||||
.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
|
||||||
.on(RoomEvent.ActiveSpeakersChanged, handleActiveSpeakerChange)
|
|
||||||
.on(RoomEvent.LocalTrackUnpublished, handleLocalTrackUnpublished);
|
|
||||||
await room.connect(url, token);
|
|
||||||
interval = setInterval(() => {
|
|
||||||
set({
|
|
||||||
remoteParticipants:
|
|
||||||
room.numParticipants === 0 ? 0 : room.numParticipants - 1, // Unreliable and delayed
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
},
|
},
|
||||||
disconnect: () => {
|
disconnect: () => {
|
||||||
get().room?.disconnect();
|
get().room?.disconnect();
|
||||||
|
|||||||
44
apps/dispatch/app/api/livekit/token/route.ts
Normal file
44
apps/dispatch/app/api/livekit/token/route.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
import { ROOMS } from "data/livekitRooms";
|
||||||
|
import { AccessToken } from "livekit-server-sdk";
|
||||||
|
import { prisma } from "../../../../../../packages/database/prisma/client";
|
||||||
|
|
||||||
|
if (!process.env.LIVEKIT_API_KEY) throw new Error("LIVEKIT_API_KEY not set");
|
||||||
|
if (!process.env.LIVEKIT_API_SECRET)
|
||||||
|
throw new Error("LIVEKIT_API_SECRET not set");
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
console.log(session?.user.permissions);
|
||||||
|
if (!session)
|
||||||
|
return Response.json({ message: "Unauthorized" }, { status: 401 });
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(user);
|
||||||
|
|
||||||
|
if (!user || !user.permissions.includes("AUDIO"))
|
||||||
|
return Response.json({ message: "Missing permissions" }, { status: 401 });
|
||||||
|
|
||||||
|
const participantName = user.publicId;
|
||||||
|
|
||||||
|
const at = new AccessToken(
|
||||||
|
process.env.LIVEKIT_API_KEY,
|
||||||
|
process.env.LIVEKIT_API_SECRET,
|
||||||
|
{
|
||||||
|
identity: participantName,
|
||||||
|
// Token to expire after 10 minutes
|
||||||
|
ttl: "1d",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ROOMS.forEach((roomName) => {
|
||||||
|
at.addGrant({ roomJoin: true, room: roomName, canPublish: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await at.toJwt();
|
||||||
|
|
||||||
|
return Response.json({ token });
|
||||||
|
};
|
||||||
1
apps/dispatch/app/data/livekitRooms.ts
Normal file
1
apps/dispatch/app/data/livekitRooms.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const ROOMS = ["LST_01", "LST_02", "LST_03", "LST_04", "LST_05"];
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.0.14",
|
"@tailwindcss/postcss": "^4.0.14",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"livekit-client": "^2.9.7",
|
"livekit-client": "^2.9.7",
|
||||||
|
"livekit-server-sdk": "^2.10.2",
|
||||||
"lucide-react": "^0.482.0",
|
"lucide-react": "^0.482.0",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -28,6 +28,7 @@
|
|||||||
"@tailwindcss/postcss": "^4.0.14",
|
"@tailwindcss/postcss": "^4.0.14",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"livekit-client": "^2.9.7",
|
"livekit-client": "^2.9.7",
|
||||||
|
"livekit-server-sdk": "^2.10.2",
|
||||||
"lucide-react": "^0.482.0",
|
"lucide-react": "^0.482.0",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ enum BADGES {
|
|||||||
enum PERMISSION {
|
enum PERMISSION {
|
||||||
ADMIN_EVENT
|
ADMIN_EVENT
|
||||||
ADMIN_USER
|
ADMIN_USER
|
||||||
SUSPENDED
|
AUDIO
|
||||||
|
AUDIO_ADMIN
|
||||||
PILOT
|
PILOT
|
||||||
DISPO
|
DISPO
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user