Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/tricky-donkeys-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@livekit/component-example-next': patch
'@livekit/components-react': patch
---

Update nextjs examples with useSession/useAgent hooks
105 changes: 105 additions & 0 deletions examples/nextjs/pages/agent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client';

import {
useAgent,
BarVisualizer,
RoomAudioRenderer,
VoiceAssistantControlBar,
SessionProvider,
useSession,
SessionEvent,
useEvents,
} from '@livekit/components-react';
import type { NextPage } from 'next';
import { useMemo, useState, useEffect } from 'react';
import { MediaDeviceFailure, TokenSource } from 'livekit-client';
import styles from '../styles/VoiceAssistant.module.scss';
import { generateRandomUserId } from '../lib/helper';

function SimpleAgent() {
const agent = useAgent();

useEffect(() => {
if (agent.state === 'failed') {
alert(`Agent error: ${agent.failureReasons.join(', ')}`);
}
}, [agent.state, agent.failureReasons]);

return (
<BarVisualizer
state={agent.state}
barCount={7}
track={agent.microphoneTrack}
style={{ width: '75vw', height: '300px' }}
/>
);
}

const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);

const AgentExample: NextPage = () => {
const params = useMemo(
() => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null),
[],
);
const roomName = useMemo(
() => params?.get('room') ?? 'test-room-' + Math.random().toFixed(5),
[params],
);
const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId());

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

const [started, setStarted] = useState(false);
useEffect(() => {
if (started) {
session.start().catch((err) => {
console.error('Failed to start session:', err);
});
} else {
session.end().catch((err) => {
console.error('Failed to end session:', err);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [started, session.start, session.end]);

useEffect(() => {
if (session.connectionState === 'disconnected') {
setStarted(false);
}
}, [session.connectionState]);

useEvents(session, SessionEvent.MediaDevicesError, (error) => {
const failure = MediaDeviceFailure.getFailure(error);
console.error(failure);
alert(
'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab',
);
}, []);

return (
<main data-lk-theme="default" className={styles.main}>
<SessionProvider session={session}>
<div className={styles.room}>
<div className={styles.inner}>
{started ? (
<SimpleAgent />
) : (
<button className="lk-button" onClick={() => setStarted(true)}>
Connect
</button>
)}
</div>
<VoiceAssistantControlBar />
<RoomAudioRenderer />
</div>
</SessionProvider>
</main>
);
};

export default AgentExample;
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 });
}
}
67 changes: 51 additions & 16 deletions examples/nextjs/pages/audio-only.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,67 @@
'use client';

import { AudioConference, LiveKitRoom, useToken } from '@livekit/components-react';
import {
AudioConference,
SessionProvider,
useSession,
SessionEvent,
useEvents,
} 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, MediaDeviceFailure } from 'livekit-client';

const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);

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

const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, {
userInfo: {
identity: userIdentity,
name: userIdentity,
},
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);
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session.start, session.end]);

useEvents(session, SessionEvent.MediaDevicesError, (error) => {
const failure = MediaDeviceFailure.getFailure(error);
console.error(failure);
alert(
'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab',
);
}, []);

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
Loading