All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
- Implemented Button component with various props for customization. - Created Modal component with header, content, and footer subcomponents. - Added Spinner component for loading indicators. - Developed Toast component for displaying notifications. - Introduced Tooltip component for contextual hints with keyboard shortcuts. - Added corresponding CSS modules for styling each component. - Updated index file to export new components. - Configured TypeScript settings for the UI package.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
"use node";
|
|
|
|
import { action } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import { AccessToken, RoomServiceClient } from "livekit-server-sdk";
|
|
|
|
/**
|
|
* Generate a LiveKit join token for a voice channel.
|
|
*
|
|
* LiveKit servers run with `room.auto_create: false` reject joins for
|
|
* rooms that don't already exist — the client gets a 404 "requested
|
|
* room does not exist" back from the /rtc/v1/validate endpoint. To
|
|
* make this deployment-agnostic, we pre-create the room via the
|
|
* LiveKit Server SDK before minting the token. When auto-create is
|
|
* enabled the `createRoom` call is idempotent (409 Conflict is
|
|
* swallowed silently), so the same code path works on both
|
|
* configurations.
|
|
*
|
|
* Requires `LIVEKIT_URL` (or the frontend's `VITE_LIVEKIT_URL` as a
|
|
* fallback) in the Convex environment so the RoomServiceClient
|
|
* can talk to the LiveKit API.
|
|
*/
|
|
export const getToken = action({
|
|
args: {
|
|
channelId: v.string(),
|
|
userId: v.string(),
|
|
username: v.string(),
|
|
},
|
|
returns: v.object({ token: v.string() }),
|
|
handler: async (_ctx, args) => {
|
|
const apiKey = process.env.LIVEKIT_API_KEY || "devkey";
|
|
const apiSecret = process.env.LIVEKIT_API_SECRET || "secret";
|
|
const livekitUrl =
|
|
process.env.LIVEKIT_URL || process.env.VITE_LIVEKIT_URL || "";
|
|
|
|
// Ensure the room exists. The LiveKit API accepts `http(s)` URLs
|
|
// for the management endpoint, but the frontend connect URL is a
|
|
// `wss://` — swap the scheme when needed.
|
|
if (livekitUrl) {
|
|
const httpUrl = livekitUrl
|
|
.replace(/^wss:\/\//i, "https://")
|
|
.replace(/^ws:\/\//i, "http://");
|
|
try {
|
|
const roomService = new RoomServiceClient(httpUrl, apiKey, apiSecret);
|
|
await roomService.createRoom({
|
|
name: args.channelId,
|
|
// Empty rooms auto-destroy after 5 minutes with no participants,
|
|
// matching LiveKit's own default so stale rooms from a crashed
|
|
// client don't pile up forever.
|
|
emptyTimeout: 5 * 60,
|
|
// 50 participants is plenty for a voice channel in this
|
|
// single-server deployment and keeps any runaway join loop
|
|
// from hitting the global limit.
|
|
maxParticipants: 50,
|
|
});
|
|
} catch (err: any) {
|
|
// 409 / "already exists" is expected when a room has already
|
|
// been created by an earlier join — swallow it and continue.
|
|
const message = String(err?.message ?? err ?? "");
|
|
const status = err?.status ?? err?.statusCode;
|
|
const alreadyExists =
|
|
status === 409 ||
|
|
/already exists/i.test(message) ||
|
|
/AlreadyExists/i.test(message);
|
|
if (!alreadyExists) {
|
|
// Non-fatal: log and fall through to token generation. If the
|
|
// real issue was misconfiguration the client will surface the
|
|
// 404 it already does.
|
|
console.warn("LiveKit createRoom failed:", message);
|
|
}
|
|
}
|
|
}
|
|
|
|
const at = new AccessToken(apiKey, apiSecret, {
|
|
identity: args.userId,
|
|
name: args.username,
|
|
ttl: "24h",
|
|
});
|
|
|
|
at.addGrant({
|
|
roomJoin: true,
|
|
room: args.channelId,
|
|
canPublish: true,
|
|
canSubscribe: true,
|
|
canPublishData: true,
|
|
});
|
|
|
|
const token = await at.toJwt();
|
|
return { token };
|
|
},
|
|
});
|