Skip to content

Commit 7790393

Browse files
pblazej1egoman
andauthored
Update examples with useSession/useAgent hooks (#1242)
Co-authored-by: Ryan Gaus <[email protected]>
1 parent 35a44b0 commit 7790393

File tree

14 files changed

+568
-294
lines changed

14 files changed

+568
-294
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@livekit/component-example-next': patch
3+
'@livekit/components-react': patch
4+
---
5+
6+
Update nextjs examples with useSession/useAgent hooks

examples/nextjs/pages/agent.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client';
2+
3+
import {
4+
useAgent,
5+
BarVisualizer,
6+
RoomAudioRenderer,
7+
VoiceAssistantControlBar,
8+
SessionProvider,
9+
useSession,
10+
SessionEvent,
11+
useEvents,
12+
} from '@livekit/components-react';
13+
import type { NextPage } from 'next';
14+
import { useMemo, useState, useEffect } from 'react';
15+
import { MediaDeviceFailure, TokenSource } from 'livekit-client';
16+
import styles from '../styles/VoiceAssistant.module.scss';
17+
import { generateRandomUserId } from '../lib/helper';
18+
19+
function SimpleAgent() {
20+
const agent = useAgent();
21+
22+
useEffect(() => {
23+
if (agent.state === 'failed') {
24+
alert(`Agent error: ${agent.failureReasons.join(', ')}`);
25+
}
26+
}, [agent.state, agent.failureReasons]);
27+
28+
return (
29+
<BarVisualizer
30+
state={agent.state}
31+
barCount={7}
32+
track={agent.microphoneTrack}
33+
style={{ width: '75vw', height: '300px' }}
34+
/>
35+
);
36+
}
37+
38+
const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);
39+
40+
const AgentExample: NextPage = () => {
41+
const params = useMemo(
42+
() => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null),
43+
[],
44+
);
45+
const roomName = useMemo(
46+
() => params?.get('room') ?? 'test-room-' + Math.random().toFixed(5),
47+
[params],
48+
);
49+
const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId());
50+
51+
const session = useSession(tokenSource, {
52+
roomName,
53+
participantIdentity: userIdentity,
54+
});
55+
56+
const [started, setStarted] = useState(false);
57+
useEffect(() => {
58+
if (started) {
59+
session.start().catch((err) => {
60+
console.error('Failed to start session:', err);
61+
});
62+
} else {
63+
session.end().catch((err) => {
64+
console.error('Failed to end session:', err);
65+
});
66+
}
67+
// eslint-disable-next-line react-hooks/exhaustive-deps
68+
}, [started, session.start, session.end]);
69+
70+
useEffect(() => {
71+
if (session.connectionState === 'disconnected') {
72+
setStarted(false);
73+
}
74+
}, [session.connectionState]);
75+
76+
useEvents(session, SessionEvent.MediaDevicesError, (error) => {
77+
const failure = MediaDeviceFailure.getFailure(error);
78+
console.error(failure);
79+
alert(
80+
'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab',
81+
);
82+
}, []);
83+
84+
return (
85+
<main data-lk-theme="default" className={styles.main}>
86+
<SessionProvider session={session}>
87+
<div className={styles.room}>
88+
<div className={styles.inner}>
89+
{started ? (
90+
<SimpleAgent />
91+
) : (
92+
<button className="lk-button" onClick={() => setStarted(true)}>
93+
Connect
94+
</button>
95+
)}
96+
</div>
97+
<VoiceAssistantControlBar />
98+
<RoomAudioRenderer />
99+
</div>
100+
</SessionProvider>
101+
</main>
102+
);
103+
};
104+
105+
export default AgentExample;
Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
22
import { AccessToken } from 'livekit-server-sdk';
33
import type { AccessTokenOptions, VideoGrant } from 'livekit-server-sdk';
4+
import type { TokenSourceRequestPayload, TokenSourceResponsePayload } from 'livekit-client';
5+
6+
type TokenSourceResponse = TokenSourceResponsePayload & {
7+
participant_name?: string;
8+
room_name?: string;
9+
};
410

511
const apiKey = process.env.LK_API_KEY;
612
const apiSecret = process.env.LK_API_SECRET;
13+
const serverUrl = process.env.NEXT_PUBLIC_LK_SERVER_URL || 'ws://localhost:7880';
714

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

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

18-
if (typeof identity !== 'string') {
19-
throw Error('provide one (and only one) identity');
27+
if (req.method !== 'POST') {
28+
res.setHeader('Allow', 'POST');
29+
res.status(405).json({ error: 'Method Not Allowed. Only POST requests are supported.' });
30+
return;
2031
}
21-
if (typeof roomName !== 'string') {
22-
throw Error('provide one (and only one) roomName');
32+
33+
if (!req.body) {
34+
throw Error('Request body is required');
2335
}
2436

25-
if (Array.isArray(name)) {
26-
throw Error('provide max one name');
37+
const body = req.body as TokenSourceRequestPayload;
38+
if (!body.room_name) {
39+
throw Error('room_name is required');
2740
}
28-
if (Array.isArray(metadata)) {
29-
throw Error('provide max one metadata string');
41+
if (!body.participant_identity) {
42+
throw Error('participant_identity is required');
3043
}
3144

32-
// if (!userSession.isAuthenticated) {
33-
// res.status(403).end();
34-
// return;
35-
// }
45+
const roomName = body.room_name;
46+
const identity = body.participant_identity;
47+
const name = body.participant_name;
48+
const metadata = body.participant_metadata;
49+
3650
const grant: VideoGrant = {
3751
room: roomName,
3852
roomJoin: true,
@@ -43,9 +57,18 @@ export default async function handleToken(req: NextApiRequest, res: NextApiRespo
4357
};
4458
const token = await createToken({ identity, name, metadata }, grant);
4559

46-
res.status(200).json({ identity, accessToken: token });
60+
// Return response in TokenSourceResponse format (snake_case)
61+
const response: TokenSourceResponse = {
62+
server_url: serverUrl,
63+
participant_token: token,
64+
participant_name: name,
65+
room_name: roomName,
66+
};
67+
res.status(200).json(response);
4768
} catch (e) {
48-
res.statusMessage = (e as Error).message;
49-
res.status(500).end();
69+
const errorMessage = (e as Error).message;
70+
console.error('Token generation error:', errorMessage);
71+
res.statusMessage = errorMessage;
72+
res.status(500).json({ error: errorMessage });
5073
}
5174
}

examples/nextjs/pages/audio-only.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,67 @@
11
'use client';
22

3-
import { AudioConference, LiveKitRoom, useToken } from '@livekit/components-react';
3+
import {
4+
AudioConference,
5+
SessionProvider,
6+
useSession,
7+
SessionEvent,
8+
useEvents,
9+
} from '@livekit/components-react';
410
import type { NextPage } from 'next';
511
import { generateRandomUserId } from '../lib/helper';
6-
import { useState } from 'react';
12+
import { useMemo, useState, useEffect } from 'react';
13+
import { TokenSource, MediaDeviceFailure } from 'livekit-client';
14+
15+
const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!);
716

817
const AudioExample: NextPage = () => {
9-
const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null;
18+
const params = useMemo(
19+
() => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null),
20+
[],
21+
);
1022
const roomName = params?.get('room') ?? 'test-room';
11-
const [userIdentity] = useState(params?.get('user') ?? generateRandomUserId());
23+
const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId());
1224

13-
const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, {
14-
userInfo: {
15-
identity: userIdentity,
16-
name: userIdentity,
17-
},
25+
const session = useSession(tokenSource, {
26+
roomName,
27+
participantIdentity: userIdentity,
28+
participantName: userIdentity,
1829
});
1930

31+
useEffect(() => {
32+
session
33+
.start({
34+
tracks: {
35+
microphone: { enabled: true },
36+
},
37+
roomConnectOptions: {
38+
autoSubscribe: true,
39+
},
40+
})
41+
.catch((err) => {
42+
console.error('Failed to start session:', err);
43+
});
44+
return () => {
45+
session.end().catch((err) => {
46+
console.error('Failed to end session:', err);
47+
});
48+
};
49+
// eslint-disable-next-line react-hooks/exhaustive-deps
50+
}, [session.start, session.end]);
51+
52+
useEvents(session, SessionEvent.MediaDevicesError, (error) => {
53+
const failure = MediaDeviceFailure.getFailure(error);
54+
console.error(failure);
55+
alert(
56+
'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab',
57+
);
58+
}, []);
59+
2060
return (
2161
<div data-lk-theme="default">
22-
<LiveKitRoom
23-
video={false}
24-
audio={true}
25-
token={token}
26-
serverUrl={process.env.NEXT_PUBLIC_LK_SERVER_URL}
27-
>
62+
<SessionProvider session={session}>
2863
<AudioConference />
29-
</LiveKitRoom>
64+
</SessionProvider>
3065
</div>
3166
);
3267
};

0 commit comments

Comments
 (0)