Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions examples/nextjs/pages/api/livekit/token.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { AccessToken } from 'livekit-server-sdk';
import type { AccessTokenOptions, VideoGrant } from 'livekit-server-sdk';
import type { TokenSourceRequestPayload, TokenSourceResponsePayload } from 'livekit-client';

type TokenSourceResponse = TokenSourceResponsePayload & {
participant_name?: string;
room_name?: string;
};

const apiKey = process.env.LK_API_KEY;
const apiSecret = process.env.LK_API_SECRET;
const serverUrl = process.env.NEXT_PUBLIC_LK_SERVER_URL || 'ws://localhost:7880';

const createToken = async (userInfo: AccessTokenOptions, grant: VideoGrant) => {
const at = new AccessToken(apiKey, apiSecret, userInfo);
Expand All @@ -13,26 +20,33 @@ const createToken = async (userInfo: AccessTokenOptions, grant: VideoGrant) => {

export default async function handleToken(req: NextApiRequest, res: NextApiResponse) {
try {
const { roomName, identity, name, metadata } = req.query;
if (!apiKey || !apiSecret) {
throw Error('LK_API_KEY and LK_API_SECRET must be set');
}

if (typeof identity !== 'string') {
throw Error('provide one (and only one) identity');
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
res.status(405).json({ error: 'Method Not Allowed. Only POST requests are supported.' });
return;
}
if (typeof roomName !== 'string') {
throw Error('provide one (and only one) roomName');

if (!req.body) {
throw Error('Request body is required');
}

if (Array.isArray(name)) {
throw Error('provide max one name');
const body = req.body as TokenSourceRequestPayload;
if (!body.room_name) {
throw Error('room_name is required');
}
if (Array.isArray(metadata)) {
throw Error('provide max one metadata string');
if (!body.participant_identity) {
throw Error('participant_identity is required');
}

// if (!userSession.isAuthenticated) {
// res.status(403).end();
// return;
// }
const roomName = body.room_name;
const identity = body.participant_identity;
const name = body.participant_name;
const metadata = body.participant_metadata;

const grant: VideoGrant = {
room: roomName,
roomJoin: true,
Expand All @@ -43,9 +57,18 @@ export default async function handleToken(req: NextApiRequest, res: NextApiRespo
};
const token = await createToken({ identity, name, metadata }, grant);

res.status(200).json({ identity, accessToken: token });
// Return response in TokenSourceResponse format (snake_case)
const response: TokenSourceResponse = {
server_url: serverUrl,
participant_token: token,
participant_name: name,
room_name: roomName,
};
res.status(200).json(response);
} catch (e) {
res.statusMessage = (e as Error).message;
res.status(500).end();
const errorMessage = (e as Error).message;
console.error('Token generation error:', errorMessage);
res.statusMessage = errorMessage;
res.status(500).json({ error: errorMessage });
}
}
45 changes: 31 additions & 14 deletions examples/nextjs/pages/audio-only.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
'use client';

import { AudioConference, LiveKitRoom, useToken } from '@livekit/components-react';
import { AudioConference, SessionProvider, useSession } from '@livekit/components-react';
import type { NextPage } from 'next';
import { generateRandomUserId } from '../lib/helper';
import { useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { TokenSource } from 'livekit-client';

const AudioExample: NextPage = () => {
const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null;
const roomName = params?.get('room') ?? 'test-room';
const [userIdentity] = useState(params?.get('user') ?? generateRandomUserId());

const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, {
userInfo: {
identity: userIdentity,
name: userIdentity,
},
const tokenSource = useMemo(() => {
return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);
}, []);
Copy link
Copy Markdown
Contributor

@1egoman 1egoman Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I think if at all possible, the examples (where it can be done) should define the TokenSource outside the component as a constant rather than in the component in a memoized fashion.

Making sure that the TokenSource doesn't change reference proved to be a challenge when Thom was porting over agent-starter-react to use these new apis, and I'd like to try to push people away from injecting local component state into TokenSources (configurable token sources should include all the metadata somebody would need in TokenSource.custom cases) unless you are doing something truly weird like agent-starter-react does to handle sandbox environments properly.


const session = useSession(tokenSource, {
roomName,
participantIdentity: userIdentity,
participantName: userIdentity,
});

useEffect(() => {
session.start({
tracks: {
microphone: { enabled: true },
},
roomConnectOptions: {
autoSubscribe: true,
},
}).catch((err) => {
console.error('Failed to start session:', err);
});
return () => {
session.end().catch((err) => {
console.error('Failed to end session:', err);
});
};
}, [session.start, session.end]);

return (
<div data-lk-theme="default">
<LiveKitRoom
video={false}
audio={true}
token={token}
serverUrl={process.env.NEXT_PUBLIC_LK_SERVER_URL}
>
<SessionProvider session={session}>
<AudioConference />
</LiveKitRoom>
</SessionProvider>
</div>
);
};
Expand Down
66 changes: 42 additions & 24 deletions examples/nextjs/pages/clubhouse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,70 @@

import {
ControlBar,
LiveKitRoom,
SessionProvider,
useSession,
RoomAudioRenderer,
RoomName,
TrackLoop,
TrackMutedIndicator,
useIsMuted,
useIsSpeaking,
useToken,
useTrackRefContext,
useTracks,
} from '@livekit/components-react';
import styles from '../styles/Clubhouse.module.scss';
import { Track } from 'livekit-client';
import { useMemo, useState } from 'react';
import { Track, TokenSource } from 'livekit-client';
import { useMemo, useState, useEffect } from 'react';
import { generateRandomUserId } from '../lib/helper';

const Clubhouse = () => {
const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null;
const roomName = params?.get('room') ?? 'test-room';
const userIdentity = params?.get('user') ?? generateRandomUserId();

const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, {
userInfo: {
identity: userIdentity,
name: userIdentity,
},
const tokenSource = useMemo(() => {
return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);
}, []);

const session = useSession(tokenSource, {
roomName,
participantIdentity: userIdentity,
participantName: userIdentity,
});

const [tryToConnect, setTryToConnect] = useState(false);
const [connected, setConnected] = useState(false);

useEffect(() => {
if (tryToConnect) {
session.start({
tracks: {
microphone: { enabled: true },
},
}).catch((err) => {
console.error('Failed to start session:', err);
});
} else {
session.end().catch((err) => {
console.error('Failed to end session:', err);
});
}
}, [tryToConnect, session.start, session.end]);

useEffect(() => {
if (session.connectionState === 'connected') {
setConnected(true);
} else {
setConnected(false);
if (session.connectionState === 'disconnected') {
setTryToConnect(false);
}
}
}, [session.connectionState]);

return (
<div data-lk-theme="default" className={styles.container}>
<LiveKitRoom
token={token}
serverUrl={process.env.NEXT_PUBLIC_LK_SERVER_URL}
connect={tryToConnect}
video={false}
audio={true}
// simulateParticipants={15}
onConnected={() => setConnected(true)}
onDisconnected={() => {
setTryToConnect(false);
setConnected(false);
}}
>
<SessionProvider session={session}>
<div style={{ display: 'grid', placeContent: 'center', height: '100%' }}>
<button
className="lk-button"
Expand All @@ -70,7 +88,7 @@ const Clubhouse = () => {
/>
<RoomAudioRenderer />
</div>
</LiveKitRoom>
</SessionProvider>
</div>
);
};
Expand Down Expand Up @@ -104,7 +122,7 @@ const CustomParticipantTile = () => {
>
<div
className={styles.avatar}
// className="z-10 grid aspect-square items-center overflow-hidden rounded-full bg-beige transition-all will-change-transform"
// className="z-10 grid aspect-square items-center overflow-hidden rounded-full bg-beige transition-all will-change-transform"
>
<img
src={`https://avatars.dicebear.com/api/avataaars/${id}.svg?mouth=default,smile,tongue&eyes=default,happy,hearts&eyebrows=default,defaultNatural,flatNatural`}
Expand Down
64 changes: 42 additions & 22 deletions examples/nextjs/pages/customize.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,75 @@
'use client';

import {
LiveKitRoom,
SessionProvider,
useSession,
ParticipantName,
TrackMutedIndicator,
RoomAudioRenderer,
isTrackReference,
useConnectionQualityIndicator,
VideoTrack,
useToken,
ControlBar,
GridLayout,
useTracks,
TrackRefContext,
} from '@livekit/components-react';
import { ConnectionQuality, Room, Track } from 'livekit-client';
import { ConnectionQuality, Room, Track, TokenSource } from 'livekit-client';
import styles from '../styles/Simple.module.css';
import myStyles from '../styles/Customize.module.css';
import type { NextPage } from 'next';
import { HTMLAttributes, useState } from 'react';
import { HTMLAttributes, useState, useMemo, useEffect } from 'react';
import { generateRandomUserId } from '../lib/helper';

const CustomizeExample: NextPage = () => {
const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null;
const roomName = params?.get('room') ?? 'test-room';
const userIdentity = params?.get('user') ?? generateRandomUserId();
const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, {
userInfo: {
identity: userIdentity,
name: userIdentity,
},
});

const [room] = useState(new Room());

const tokenSource = useMemo(() => {
return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);
}, []);

const session = useSession(tokenSource, {
roomName,
participantIdentity: userIdentity,
participantName: userIdentity,
room,
});

const [connect, setConnect] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const handleDisconnect = () => {
setConnect(false);
setIsConnected(false);
};

useEffect(() => {
if (connect) {
session.start({
tracks: {
microphone: { enabled: true },
},
}).catch((err) => {
console.error('Failed to start session:', err);
});
} else {
session.end().catch((err) => {
console.error('Failed to end session:', err);
});
}
}, [connect, session.start, session.end]);

useEffect(() => {
if (session.connectionState === 'connected') {
setIsConnected(true);
} else {
setIsConnected(false);
}
}, [session.connectionState]);

return (
<div className={styles.container} data-lk-theme="default">
<main className={styles.main}>
Expand All @@ -52,21 +81,12 @@ const CustomizeExample: NextPage = () => {
{connect ? 'Disconnect' : 'Connect'}
</button>
)}
<LiveKitRoom
room={room}
token={token}
serverUrl={process.env.NEXT_PUBLIC_LK_SERVER_URL}
connect={connect}
onConnected={() => setIsConnected(true)}
onDisconnected={handleDisconnect}
audio={true}
video={true}
>
<SessionProvider session={session}>
<RoomAudioRenderer />
{/* Render a custom Stage component once connected */}
{isConnected && <Stage />}
<ControlBar />
</LiveKitRoom>
</SessionProvider>
</main>
</div>
);
Expand Down Expand Up @@ -104,7 +124,7 @@ export function Stage() {
{/* In addition, we can still specify a style attribute and further customize the styles. */}
<ParticipantName
className={myStyles['my-participant-name']}
// style={{ color: 'blue' }}
// style={{ color: 'blue' }}
/>
{/* Custom components: Here we replace the provided <ConnectionQualityIndicator /> with our own implementation. */}
<UserDefinedConnectionQualityIndicator />
Expand Down
Loading
Loading