From f5143188ba2d3272342b08142ae06c56d67d861a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:21:45 +0100 Subject: [PATCH 01/19] Use session --- examples/nextjs/pages/audio-only.tsx | 41 ++++++++----- examples/nextjs/pages/clubhouse.tsx | 62 +++++++++++-------- examples/nextjs/pages/customize.tsx | 60 ++++++++++++------- examples/nextjs/pages/e2ee.tsx | 59 +++++++++++-------- examples/nextjs/pages/minimal.tsx | 52 ++++++++-------- examples/nextjs/pages/processors.tsx | 51 ++++++++++------ examples/nextjs/pages/simple.tsx | 55 ++++++++++------- examples/nextjs/pages/voice-assistant.tsx | 72 ++++++++++++----------- packages/react/src/hooks/useSession.ts | 31 ++++++---- 9 files changed, 288 insertions(+), 195 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 803eadfde..e91db0aef 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -1,32 +1,45 @@ '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!); + }, []); + + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, }); + useEffect(() => { + session.start({ + tracks: { + microphone: { enabled: true }, + }, + roomConnectOptions: { + autoSubscribe: true, + }, + }); + return () => { + session.end(); + }; + }, [session]); + return (
- + - +
); }; diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index a35d8a187..a077e5648 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -2,20 +2,20 @@ 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 = () => { @@ -23,31 +23,45 @@ const Clubhouse = () => { 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 }, + }, + }); + } else { + session.end(); + } + }, [tryToConnect, session]); + + useEffect(() => { + if (session.connectionState === 'connected') { + setConnected(true); + } else { + setConnected(false); + if (session.connectionState === 'disconnected') { + setTryToConnect(false); + } + } + }, [session.connectionState]); + return (
- setConnected(true)} - onDisconnected={() => { - setTryToConnect(false); - setConnected(false); - }} - > +
-
+
); }; @@ -104,7 +118,7 @@ const CustomParticipantTile = () => { >
{ 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 = () => { @@ -41,6 +46,26 @@ const CustomizeExample: NextPage = () => { setIsConnected(false); }; + useEffect(() => { + if (connect) { + session.start({ + tracks: { + microphone: { enabled: true }, + }, + }); + } else { + session.end(); + } + }, [connect, session]); + + useEffect(() => { + if (session.connectionState === 'connected') { + setIsConnected(true); + } else { + setIsConnected(false); + } + }, [session.connectionState]); + return (
@@ -52,21 +77,12 @@ const CustomizeExample: NextPage = () => { {connect ? 'Disconnect' : 'Connect'} )} - setIsConnected(true)} - onDisconnected={handleDisconnect} - audio={true} - video={true} - > + {/* Render a custom Stage component once connected */} {isConnected && } - +
); @@ -104,7 +120,7 @@ export function Stage() { {/* In addition, we can still specify a style attribute and further customize the styles. */} {/* Custom components: Here we replace the provided with our own implementation. */} diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 3d88c3472..959dced60 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -1,24 +1,20 @@ 'use client'; -import { LiveKitRoom, useToken, VideoConference, setLogLevel } from '@livekit/components-react'; +import { SessionProvider, useSession, VideoConference, setLogLevel } from '@livekit/components-react'; import type { NextPage } from 'next'; import * as React from 'react'; -import { Room, ExternalE2EEKeyProvider } from 'livekit-client'; +import { Room, ExternalE2EEKeyProvider, TokenSource } from 'livekit-client'; import { generateRandomUserId } from '../lib/helper'; const E2EEExample: NextPage = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; + const params = React.useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = React.useMemo(() => params?.get('user') ?? generateRandomUserId(), []); + const userIdentity = React.useMemo(() => params?.get('user') ?? generateRandomUserId(), [params]); setLogLevel('warn', { liveKitClientLogLevel: 'debug' }); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, - }); - const keyProvider = React.useMemo(() => new ExternalE2EEKeyProvider(), []); keyProvider.setKey('password'); @@ -29,27 +25,44 @@ const E2EEExample: NextPage = () => { e2ee: typeof window !== 'undefined' ? { - keyProvider, - worker: new Worker(new URL('livekit-client/e2ee-worker', import.meta.url)), - } + keyProvider, + worker: new Worker(new URL('livekit-client/e2ee-worker', import.meta.url)), + } : undefined, }), - [], + [keyProvider], ); room.setE2EEEnabled(true); + const tokenSource = React.useMemo(() => { + return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + }, []); + + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + room, + }); + + React.useEffect(() => { + session.start({ + tracks: { + camera: { enabled: true }, + microphone: { enabled: true }, + }, + }); + return () => { + session.end(); + }; + }, [session]); + return (
- + - +
); }; diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 06442a7bf..cb2a3ebf7 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -1,43 +1,47 @@ 'use client'; -import { LiveKitRoom, useToken, VideoConference, setLogLevel } from '@livekit/components-react'; +import { SessionProvider, useSession, VideoConference, setLogLevel } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; +import { TokenSource } from 'livekit-client'; const MinimalExample: NextPage = () => { const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; const roomName = params?.get('room') ?? 'test-room'; setLogLevel('debug', { liveKitClientLogLevel: 'info' }); - const tokenOptions = useMemo(() => { - const userId = params?.get('user') ?? generateRandomUserId(); - return { - userInfo: { - identity: userId, - name: userId, - }, - }; + const userIdentity = useMemo( + () => params?.get('user') ?? generateRandomUserId(), + [params], + ); + + const tokenSource = useMemo(() => { + return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }); + + useEffect(() => { + session.start({ + tracks: { + microphone: { enabled: false }, + }, + }); + return () => { + session.end(); + }; + }, [session]); return (
- { - console.error(e); - alert( - 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', - ); - }} - > + - +
); }; diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index 44f3712ce..12a2cb953 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -4,16 +4,16 @@ import * as React from 'react'; import { setLogLevel } from '@livekit/components-core'; import { GridLayout, - LiveKitRoom, + SessionProvider, + useSession, ParticipantTile, TrackRefContext, useLocalParticipant, - useToken, useTracks, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { ControlBarControls } from '@livekit/components-react'; -import { LocalVideoTrack, Track, TrackProcessor } from 'livekit-client'; +import { LocalVideoTrack, Track, TrackProcessor, TokenSource } from 'livekit-client'; import { BackgroundBlur } from '@livekit/track-processors'; function Stage() { @@ -23,14 +23,18 @@ function Stage() { const [blurEnabled, setBlurEnabled] = React.useState(false); const [processorPending, setProcessorPending] = React.useState(false); const { cameraTrack } = useLocalParticipant(); - const [blur] = React.useState(BackgroundBlur()); + const [blur, setBlur] = React.useState | undefined>(); + + React.useEffect(() => { + setBlur(BackgroundBlur()); + }, []); React.useEffect(() => { const localCamTrack = cameraTrack?.track as LocalVideoTrack | undefined; if (localCamTrack) { setProcessorPending(true); try { - if (blurEnabled && !localCamTrack.getProcessor()) { + if (blurEnabled && !localCamTrack.getProcessor() && blur) { localCamTrack.setProcessor(blur); } else if (!blurEnabled) { localCamTrack.stopProcessor(); @@ -39,7 +43,7 @@ function Stage() { setProcessorPending(false); } } - }, [blurEnabled, cameraTrack]); + }, [blurEnabled, cameraTrack, blur]); return ( <> @@ -65,24 +69,33 @@ const ProcessorsExample: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; const userIdentity = params?.get('user') ?? 'test-identity'; - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, + const tokenSource = React.useMemo(() => { + return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + }, []); + + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, }); + React.useEffect(() => { + session.start({ + tracks: { + camera: { enabled: true }, + microphone: { enabled: false }, + }, + }); + return () => { + session.end(); + }; + }, [session]); + return (
- + - +
); }; diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 7e9e981ab..7b7e3bd49 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -4,17 +4,17 @@ import { ConnectionState, ControlBar, GridLayout, - LiveKitRoom, + SessionProvider, + useSession, ParticipantTile, RoomAudioRenderer, RoomName, TrackRefContext, - useToken, useTracks, } from '@livekit/components-react'; -import { Track } from 'livekit-client'; +import { Track, TokenSource } from 'livekit-client'; import type { NextPage } from 'next'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import styles from '../styles/Simple.module.css'; import { generateRandomUserId } from '../lib/helper'; @@ -25,16 +25,35 @@ const SimpleExample: NextPage = () => { const [connect, setConnect] = useState(false); const [isConnected, setIsConnected] = useState(false); - const userInfo = useMemo(() => { - return { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, - }; + const tokenSource = useMemo(() => { + return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, userInfo); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }); + + useEffect(() => { + if (connect) { + session.start({ + tracks: { + microphone: { enabled: true }, + }, + }); + } else { + session.end(); + } + }, [connect, session]); + + useEffect(() => { + if (session.connectionState === 'connected') { + setIsConnected(true); + } else { + setIsConnected(false); + } + }, [session.connectionState]); const handleDisconnect = () => { setConnect(false); @@ -52,21 +71,13 @@ const SimpleExample: NextPage = () => { {connect ? 'Disconnect' : 'Connect'} )} - setIsConnected(true)} - onDisconnected={handleDisconnect} - audio={true} - video={true} - > + {isConnected && } - +
); diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index 16387bd55..25963aa6c 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -1,16 +1,16 @@ 'use client'; import { - LiveKitRoom, - useToken, useVoiceAssistant, BarVisualizer, RoomAudioRenderer, VoiceAssistantControlBar, + SessionProvider, + useSession, } from '@livekit/components-react'; import type { NextPage } from 'next'; -import { useMemo, useState } from 'react'; -import { MediaDeviceFailure } from 'livekit-client'; +import { useMemo, useState, useEffect } from 'react'; +import { MediaDeviceFailure, TokenSource } from 'livekit-client'; import styles from '../styles/VoiceAssistant.module.scss'; import { generateRandomUserId } from '../lib/helper'; @@ -27,24 +27,32 @@ function SimpleVoiceAssistant() { } const VoiceAssistantExample: NextPage = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; + 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 [shouldConnect, setShouldConnect] = useState(false); - const tokenOptions = useMemo(() => { - const userId = params?.get('user') ?? generateRandomUserId(); - return { - userInfo: { - identity: userId, - name: userId, - }, - }; + const tokenSource = useMemo(() => { + return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions); + const session = useSession(tokenSource, { + roomName, + participantIdentity: params?.get('user') ?? generateRandomUserId(), + }); + + useEffect(() => { + if (shouldConnect) { + session.start(); + } else { + session.end(); + } + }, [shouldConnect, session]); const onDeviceFailure = (e?: MediaDeviceFailure) => { console.error(e); @@ -55,27 +63,21 @@ const VoiceAssistantExample: NextPage = () => { return (
- setShouldConnect(false)} - className={styles.room} - > -
- {shouldConnect ? ( - - ) : ( - - )} + +
+
+ {shouldConnect ? ( + + ) : ( + + )} +
+ +
- - - +
); }; diff --git a/packages/react/src/hooks/useSession.ts b/packages/react/src/hooks/useSession.ts index d4d25cc3b..252c13e47 100644 --- a/packages/react/src/hooks/useSession.ts +++ b/packages/react/src/hooks/useSession.ts @@ -52,6 +52,10 @@ export type SessionConnectOptions = { enabled?: boolean; publishOptions?: TrackPublishOptions; }; + camera?: { + enabled?: boolean; + publishOptions?: TrackPublishOptions; + }; }; /** Options for Room.connect(.., .., opts) */ @@ -95,9 +99,9 @@ type SessionStateConnecting = SessionStateCommon & { type SessionStateConnected = SessionStateCommon & { connectionState: - | ConnectionState.Connected - | ConnectionState.Reconnecting - | ConnectionState.SignalReconnecting; + | ConnectionState.Connected + | ConnectionState.Reconnecting + | ConnectionState.SignalReconnecting; isConnected: true; local: { @@ -321,11 +325,11 @@ export function useSession( connectionState === ConnectionState.SignalReconnecting, }) as { isConnected: State extends - | ConnectionState.Connected - | ConnectionState.Reconnecting - | ConnectionState.SignalReconnecting - ? true - : false; + | ConnectionState.Connected + | ConnectionState.Reconnecting + | ConnectionState.SignalReconnecting + ? true + : false; }, [], ); @@ -550,10 +554,13 @@ export function useSession( // Start microphone (with preconnect buffer) by default tracks.microphone?.enabled ? room.localParticipant.setMicrophoneEnabled( - true, - undefined, - tracks.microphone?.publishOptions ?? {}, - ) + true, + undefined, + tracks.microphone?.publishOptions ?? {}, + ) + : Promise.resolve(), + tracks.camera?.enabled + ? room.localParticipant.setCameraEnabled(true, undefined, tracks.camera?.publishOptions ?? {}) : Promise.resolve(), ]); From b5b3cdeae634dd2bc172836771a8229e895229da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:33:02 +0100 Subject: [PATCH 02/19] Use agent --- examples/nextjs/pages/voice-assistant.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index 25963aa6c..b6ccaa578 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -1,7 +1,7 @@ 'use client'; import { - useVoiceAssistant, + useAgent, BarVisualizer, RoomAudioRenderer, VoiceAssistantControlBar, @@ -15,12 +15,12 @@ import styles from '../styles/VoiceAssistant.module.scss'; import { generateRandomUserId } from '../lib/helper'; function SimpleVoiceAssistant() { - const { state, audioTrack } = useVoiceAssistant(); + const agent = useAgent(); return ( ); From de906a0d895f7698dd553aa186a7d627a9eec673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:14:40 +0100 Subject: [PATCH 03/19] Token generation --- examples/nextjs/pages/api/livekit/token.ts | 55 +++++++++++++++------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/examples/nextjs/pages/api/livekit/token.ts b/examples/nextjs/pages/api/livekit/token.ts index b434a895d..784c6373a 100644 --- a/examples/nextjs/pages/api/livekit/token.ts +++ b/examples/nextjs/pages/api/livekit/token.ts @@ -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); @@ -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, @@ -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 }); } } From 55854f7ba2f745e7d33056291c3c51593de56f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:38:35 +0100 Subject: [PATCH 04/19] Catch --- examples/nextjs/pages/audio-only.tsx | 8 ++++++-- examples/nextjs/pages/clubhouse.tsx | 8 ++++++-- examples/nextjs/pages/customize.tsx | 8 ++++++-- examples/nextjs/pages/e2ee.tsx | 8 ++++++-- examples/nextjs/pages/minimal.tsx | 8 ++++++-- examples/nextjs/pages/processors.tsx | 8 ++++++-- examples/nextjs/pages/simple.tsx | 8 ++++++-- examples/nextjs/pages/voice-assistant.tsx | 10 +++++++--- 8 files changed, 49 insertions(+), 17 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index e91db0aef..85214ed28 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -29,11 +29,15 @@ const AudioExample: NextPage = () => { roomConnectOptions: { autoSubscribe: true, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); return () => { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); }; - }, [session]); + }, [session.start, session.end]); return (
diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index a077e5648..f721e04eb 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -42,11 +42,15 @@ const Clubhouse = () => { tracks: { microphone: { enabled: true }, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); } else { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); } - }, [tryToConnect, session]); + }, [tryToConnect, session.start, session.end]); useEffect(() => { if (session.connectionState === 'connected') { diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 5d7d3c08f..a67bbddb2 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -52,11 +52,15 @@ const CustomizeExample: NextPage = () => { tracks: { microphone: { enabled: true }, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); } else { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); } - }, [connect, session]); + }, [connect, session.start, session.end]); useEffect(() => { if (session.connectionState === 'connected') { diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 959dced60..f419119c1 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -52,11 +52,15 @@ const E2EEExample: NextPage = () => { camera: { enabled: true }, microphone: { enabled: true }, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); return () => { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); }; - }, [session]); + }, [session.start, session.end]); return (
diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index cb2a3ebf7..a0122f997 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -31,11 +31,15 @@ const MinimalExample: NextPage = () => { tracks: { microphone: { enabled: false }, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); return () => { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); }; - }, [session]); + }, [session.start, session.end]); return (
diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index 12a2cb953..85f2aca56 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -85,11 +85,15 @@ const ProcessorsExample: NextPage = () => { camera: { enabled: true }, microphone: { enabled: false }, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); return () => { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); }; - }, [session]); + }, [session.start, session.end]); return (
diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 7b7e3bd49..ed9e156fd 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -41,11 +41,15 @@ const SimpleExample: NextPage = () => { tracks: { microphone: { enabled: true }, }, + }).catch((err) => { + console.error('Failed to start session:', err); }); } else { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); } - }, [connect, session]); + }, [connect, session.start, session.end]); useEffect(() => { if (session.connectionState === 'connected') { diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index b6ccaa578..2da2f9ecd 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -48,11 +48,15 @@ const VoiceAssistantExample: NextPage = () => { useEffect(() => { if (shouldConnect) { - session.start(); + session.start().catch((err) => { + console.error('Failed to start session:', err); + }); } else { - session.end(); + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); } - }, [shouldConnect, session]); + }, [shouldConnect, session.start, session.end]); const onDeviceFailure = (e?: MediaDeviceFailure) => { console.error(e); From 6a3a03706b0fe3e25696a51cacc6626cbc21c2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:47:56 +0100 Subject: [PATCH 05/19] Lint --- examples/nextjs/pages/audio-only.tsx | 1 + examples/nextjs/pages/clubhouse.tsx | 5 ++++- examples/nextjs/pages/customize.tsx | 1 + examples/nextjs/pages/e2ee.tsx | 1 + examples/nextjs/pages/index.tsx | 7 +++++-- examples/nextjs/pages/minimal.tsx | 6 +++++- examples/nextjs/pages/processors.tsx | 1 + examples/nextjs/pages/simple.tsx | 1 + examples/nextjs/pages/voice-assistant.tsx | 1 + 9 files changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 85214ed28..05eb629da 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -37,6 +37,7 @@ const AudioExample: NextPage = () => { console.error('Failed to end session:', err); }); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); return ( diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index f721e04eb..aa5f2a792 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -17,6 +17,7 @@ import styles from '../styles/Clubhouse.module.scss'; import { Track, TokenSource } from 'livekit-client'; import { useMemo, useState, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; +import Image from 'next/image'; const Clubhouse = () => { const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; @@ -50,6 +51,7 @@ const Clubhouse = () => { console.error('Failed to end session:', err); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [tryToConnect, session.start, session.end]); useEffect(() => { @@ -124,12 +126,13 @@ const CustomParticipantTile = () => { className={styles.avatar} // className="z-10 grid aspect-square items-center overflow-hidden rounded-full bg-beige transition-all will-change-transform" > - {`Avatar
diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index a67bbddb2..9aad7bf79 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -60,6 +60,7 @@ const CustomizeExample: NextPage = () => { console.error('Failed to end session:', err); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [connect, session.start, session.end]); useEffect(() => { diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index f419119c1..bc409dee5 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -60,6 +60,7 @@ const E2EEExample: NextPage = () => { console.error('Failed to end session:', err); }); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); return ( diff --git a/examples/nextjs/pages/index.tsx b/examples/nextjs/pages/index.tsx index 3c6cf44bf..564ffe263 100644 --- a/examples/nextjs/pages/index.tsx +++ b/examples/nextjs/pages/index.tsx @@ -3,6 +3,7 @@ import type { NextPage } from 'next'; import styles from '../styles/Home.module.scss'; import Head from 'next/head'; +import Image from 'next/image'; const EXAMPLE_ROUTES = { voiceAssistant: { @@ -40,10 +41,12 @@ const Home: NextPage = () => {
- LiveKit components text logo.

Some simple sample apps to help you get started working with LiveKit Components.

diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index a0122f997..d65ed659e 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -7,7 +7,10 @@ import { useMemo, useEffect } from 'react'; import { TokenSource } from 'livekit-client'; const MinimalExample: 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'; setLogLevel('debug', { liveKitClientLogLevel: 'info' }); @@ -39,6 +42,7 @@ const MinimalExample: NextPage = () => { console.error('Failed to end session:', err); }); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); return ( diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index 85f2aca56..6924baf53 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -93,6 +93,7 @@ const ProcessorsExample: NextPage = () => { console.error('Failed to end session:', err); }); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); return ( diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index ed9e156fd..a0b8d930f 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -49,6 +49,7 @@ const SimpleExample: NextPage = () => { console.error('Failed to end session:', err); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [connect, session.start, session.end]); useEffect(() => { diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index 2da2f9ecd..189c7cd9a 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -56,6 +56,7 @@ const VoiceAssistantExample: NextPage = () => { console.error('Failed to end session:', err); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldConnect, session.start, session.end]); const onDeviceFailure = (e?: MediaDeviceFailure) => { From 07450496cf99743ec1c3a96030ee7197eb83ae1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:04:16 +0100 Subject: [PATCH 06/19] Keep side effects --- examples/nextjs/pages/customize.tsx | 7 +++--- examples/nextjs/pages/minimal.tsx | 19 ++++++++++++++-- examples/nextjs/pages/simple.tsx | 8 +++---- examples/nextjs/pages/voice-assistant.tsx | 27 ++++++++++++++++++----- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 9aad7bf79..d083e3760 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -41,10 +41,6 @@ const CustomizeExample: NextPage = () => { const [connect, setConnect] = useState(false); const [isConnected, setIsConnected] = useState(false); - const handleDisconnect = () => { - setConnect(false); - setIsConnected(false); - }; useEffect(() => { if (connect) { @@ -68,6 +64,9 @@ const CustomizeExample: NextPage = () => { setIsConnected(true); } else { setIsConnected(false); + if (session.connectionState === 'disconnected') { + setConnect(false); + } } }, [session.connectionState]); diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index d65ed659e..7ddc9df3e 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -1,10 +1,10 @@ 'use client'; -import { SessionProvider, useSession, VideoConference, setLogLevel } from '@livekit/components-react'; +import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; import { useMemo, useEffect } from 'react'; -import { TokenSource } from 'livekit-client'; +import { TokenSource, MediaDeviceFailure } from 'livekit-client'; const MinimalExample: NextPage = () => { const params = useMemo( @@ -45,6 +45,21 @@ const MinimalExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); + useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (
diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index a0b8d930f..489e33e85 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -57,14 +57,12 @@ const SimpleExample: NextPage = () => { setIsConnected(true); } else { setIsConnected(false); + if (session.connectionState === 'disconnected') { + setConnect(false); + } } }, [session.connectionState]); - const handleDisconnect = () => { - setConnect(false); - setIsConnected(false); - }; - return (
diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index 189c7cd9a..993b9bc7f 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -7,6 +7,7 @@ import { VoiceAssistantControlBar, SessionProvider, useSession, + SessionEvent, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { useMemo, useState, useEffect } from 'react'; @@ -59,12 +60,26 @@ const VoiceAssistantExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldConnect, session.start, session.end]); - const onDeviceFailure = (e?: MediaDeviceFailure) => { - console.error(e); - alert( - 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', - ); - }; + useEffect(() => { + if (session.connectionState === 'disconnected') { + setShouldConnect(false); + } + }, [session.connectionState]); + + useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); return (
From 86295635107f92a8f843b794339eacd234dd320f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:15:15 +0100 Subject: [PATCH 07/19] Fix hydration errors --- examples/nextjs/pages/e2ee.tsx | 51 +++++++++++++++++++---------- examples/nextjs/pages/minimal.tsx | 53 ++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 35 deletions(-) diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index bc409dee5..35de2ce5f 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -7,14 +7,25 @@ import { Room, ExternalE2EEKeyProvider, TokenSource } from 'livekit-client'; import { generateRandomUserId } from '../lib/helper'; const E2EEExample: NextPage = () => { - const params = React.useMemo( - () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), - [], - ); - const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = React.useMemo(() => params?.get('user') ?? generateRandomUserId(), [params]); + const [roomName, setRoomName] = React.useState('test-room'); + const [userIdentity, setUserIdentity] = React.useState(() => generateRandomUserId()); + const [isReady, setIsReady] = React.useState(false); + setLogLevel('warn', { liveKitClientLogLevel: 'debug' }); + React.useEffect(() => { + const params = new URLSearchParams(location.search); + const room = params.get('room'); + const user = params.get('user'); + if (room) { + setRoomName(room); + } + if (user) { + setUserIdentity(user); + } + setIsReady(true); + }, []); + const keyProvider = React.useMemo(() => new ExternalE2EEKeyProvider(), []); keyProvider.setKey('password'); @@ -39,14 +50,20 @@ const E2EEExample: NextPage = () => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const session = useSession(tokenSource, { - roomName, - participantIdentity: userIdentity, - participantName: userIdentity, - room, - }); + const sessionOptions = React.useMemo( + () => ({ + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + room, + }), + [roomName, userIdentity, room], + ); + + const session = useSession(tokenSource, sessionOptions); React.useEffect(() => { + if (!isReady) return; session.start({ tracks: { camera: { enabled: true }, @@ -61,13 +78,15 @@ const E2EEExample: NextPage = () => { }); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [session.start, session.end]); + }, [isReady, session.start, session.end]); return (
- - - + {isReady && ( + + + + )}
); }; diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 7ddc9df3e..95e4d6783 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -3,33 +3,46 @@ import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useMemo, useEffect } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { TokenSource, MediaDeviceFailure } from 'livekit-client'; const MinimalExample: NextPage = () => { - const params = useMemo( - () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), - [], - ); - const roomName = params?.get('room') ?? 'test-room'; + const [roomName, setRoomName] = useState('test-room'); + const [userIdentity, setUserIdentity] = useState(() => generateRandomUserId()); + const [isReady, setIsReady] = useState(false); + setLogLevel('debug', { liveKitClientLogLevel: 'info' }); - const userIdentity = useMemo( - () => params?.get('user') ?? generateRandomUserId(), - [params], - ); + useEffect(() => { + const params = new URLSearchParams(location.search); + const room = params.get('room'); + const user = params.get('user'); + if (room) { + setRoomName(room); + } + if (user) { + setUserIdentity(user); + } + setIsReady(true); + }, []); const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const session = useSession(tokenSource, { - roomName, - participantIdentity: userIdentity, - participantName: userIdentity, - }); + const sessionOptions = useMemo( + () => ({ + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }), + [roomName, userIdentity], + ); + + const session = useSession(tokenSource, sessionOptions); useEffect(() => { + if (!isReady) return; session.start({ tracks: { microphone: { enabled: false }, @@ -43,7 +56,7 @@ const MinimalExample: NextPage = () => { }); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [session.start, session.end]); + }, [isReady, session.start, session.end]); useEffect(() => { const handleMediaDevicesError = (error: Error) => { @@ -62,9 +75,11 @@ const MinimalExample: NextPage = () => { return (
- - - + {isReady && ( + + + + )}
); }; From d9dd885aaff0df779ea4f01195b5a071892cf228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:48:52 +0100 Subject: [PATCH 08/19] Revert "Fix hydration errors" This reverts commit 86295635107f92a8f843b794339eacd234dd320f. --- examples/nextjs/pages/e2ee.tsx | 51 ++++++++++------------------- examples/nextjs/pages/minimal.tsx | 53 +++++++++++-------------------- 2 files changed, 35 insertions(+), 69 deletions(-) diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 35de2ce5f..bc409dee5 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -7,25 +7,14 @@ import { Room, ExternalE2EEKeyProvider, TokenSource } from 'livekit-client'; import { generateRandomUserId } from '../lib/helper'; const E2EEExample: NextPage = () => { - const [roomName, setRoomName] = React.useState('test-room'); - const [userIdentity, setUserIdentity] = React.useState(() => generateRandomUserId()); - const [isReady, setIsReady] = React.useState(false); - + const params = React.useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); + const roomName = params?.get('room') ?? 'test-room'; + const userIdentity = React.useMemo(() => params?.get('user') ?? generateRandomUserId(), [params]); setLogLevel('warn', { liveKitClientLogLevel: 'debug' }); - React.useEffect(() => { - const params = new URLSearchParams(location.search); - const room = params.get('room'); - const user = params.get('user'); - if (room) { - setRoomName(room); - } - if (user) { - setUserIdentity(user); - } - setIsReady(true); - }, []); - const keyProvider = React.useMemo(() => new ExternalE2EEKeyProvider(), []); keyProvider.setKey('password'); @@ -50,20 +39,14 @@ const E2EEExample: NextPage = () => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const sessionOptions = React.useMemo( - () => ({ - roomName, - participantIdentity: userIdentity, - participantName: userIdentity, - room, - }), - [roomName, userIdentity, room], - ); - - const session = useSession(tokenSource, sessionOptions); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + room, + }); React.useEffect(() => { - if (!isReady) return; session.start({ tracks: { camera: { enabled: true }, @@ -78,15 +61,13 @@ const E2EEExample: NextPage = () => { }); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReady, session.start, session.end]); + }, [session.start, session.end]); return (
- {isReady && ( - - - - )} + + +
); }; diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 95e4d6783..7ddc9df3e 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -3,46 +3,33 @@ import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useMemo, useEffect, useState } from 'react'; +import { useMemo, useEffect } from 'react'; import { TokenSource, MediaDeviceFailure } from 'livekit-client'; const MinimalExample: NextPage = () => { - const [roomName, setRoomName] = useState('test-room'); - const [userIdentity, setUserIdentity] = useState(() => generateRandomUserId()); - const [isReady, setIsReady] = useState(false); - + const params = useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); + const roomName = params?.get('room') ?? 'test-room'; setLogLevel('debug', { liveKitClientLogLevel: 'info' }); - useEffect(() => { - const params = new URLSearchParams(location.search); - const room = params.get('room'); - const user = params.get('user'); - if (room) { - setRoomName(room); - } - if (user) { - setUserIdentity(user); - } - setIsReady(true); - }, []); + const userIdentity = useMemo( + () => params?.get('user') ?? generateRandomUserId(), + [params], + ); const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); - const sessionOptions = useMemo( - () => ({ - roomName, - participantIdentity: userIdentity, - participantName: userIdentity, - }), - [roomName, userIdentity], - ); - - const session = useSession(tokenSource, sessionOptions); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }); useEffect(() => { - if (!isReady) return; session.start({ tracks: { microphone: { enabled: false }, @@ -56,7 +43,7 @@ const MinimalExample: NextPage = () => { }); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReady, session.start, session.end]); + }, [session.start, session.end]); useEffect(() => { const handleMediaDevicesError = (error: Error) => { @@ -75,11 +62,9 @@ const MinimalExample: NextPage = () => { return (
- {isReady && ( - - - - )} + + +
); }; From bfc77d59f5ec5ca27c7a2ce81fdefb5ef4e50b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:10:45 +0100 Subject: [PATCH 09/19] Use session.isConnected --- examples/nextjs/pages/audio-only.tsx | 19 +++++++++++++-- examples/nextjs/pages/clubhouse.tsx | 30 ++++++++++++++++-------- examples/nextjs/pages/customize.tsx | 32 ++++++++++++++++--------- examples/nextjs/pages/e2ee.tsx | 35 ++++++++++++++++++++++------ examples/nextjs/pages/minimal.tsx | 8 ++++--- examples/nextjs/pages/processors.tsx | 18 +++++++++++++- examples/nextjs/pages/simple.tsx | 32 ++++++++++++++++--------- 7 files changed, 129 insertions(+), 45 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 05eb629da..81b8737d5 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -1,10 +1,10 @@ 'use client'; -import { AudioConference, SessionProvider, useSession } from '@livekit/components-react'; +import { AudioConference, SessionProvider, useSession, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; import { useMemo, useState, useEffect } from 'react'; -import { TokenSource } from 'livekit-client'; +import { TokenSource, MediaDeviceFailure } from 'livekit-client'; const AudioExample: NextPage = () => { const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; @@ -40,6 +40,21 @@ const AudioExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); + useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (
diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index aa5f2a792..4c48b26b1 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -12,9 +12,10 @@ import { useIsSpeaking, useTrackRefContext, useTracks, + SessionEvent, } from '@livekit/components-react'; import styles from '../styles/Clubhouse.module.scss'; -import { Track, TokenSource } from 'livekit-client'; +import { Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { useMemo, useState, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; import Image from 'next/image'; @@ -35,7 +36,6 @@ const Clubhouse = () => { }); const [tryToConnect, setTryToConnect] = useState(false); - const [connected, setConnected] = useState(false); useEffect(() => { if (tryToConnect) { @@ -55,16 +55,26 @@ const Clubhouse = () => { }, [tryToConnect, session.start, session.end]); useEffect(() => { - if (session.connectionState === 'connected') { - setConnected(true); - } else { - setConnected(false); - if (session.connectionState === 'disconnected') { - setTryToConnect(false); - } + if (session.connectionState === 'disconnected') { + setTryToConnect(false); } }, [session.connectionState]); + useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (
@@ -79,7 +89,7 @@ const Clubhouse = () => {
-
+

diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index d083e3760..8000d2a3e 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -13,8 +13,9 @@ import { GridLayout, useTracks, TrackRefContext, + SessionEvent, } from '@livekit/components-react'; -import { ConnectionQuality, Room, Track, TokenSource } from 'livekit-client'; +import { ConnectionQuality, Room, Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; import styles from '../styles/Simple.module.css'; import myStyles from '../styles/Customize.module.css'; import type { NextPage } from 'next'; @@ -40,7 +41,6 @@ const CustomizeExample: NextPage = () => { }); const [connect, setConnect] = useState(false); - const [isConnected, setIsConnected] = useState(false); useEffect(() => { if (connect) { @@ -60,23 +60,33 @@ const CustomizeExample: NextPage = () => { }, [connect, session.start, session.end]); useEffect(() => { - if (session.connectionState === 'connected') { - setIsConnected(true); - } else { - setIsConnected(false); - if (session.connectionState === 'disconnected') { - setConnect(false); - } + if (session.connectionState === 'disconnected') { + setConnect(false); } }, [session.connectionState]); + useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (

Welcome to LiveKit

- {!isConnected && ( + {!session.isConnected && ( @@ -84,7 +94,7 @@ const CustomizeExample: NextPage = () => { {/* Render a custom Stage component once connected */} - {isConnected && } + {session.isConnected && }
diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index bc409dee5..02e330df0 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -1,9 +1,9 @@ 'use client'; -import { SessionProvider, useSession, VideoConference, setLogLevel } from '@livekit/components-react'; +import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; import * as React from 'react'; -import { Room, ExternalE2EEKeyProvider, TokenSource } from 'livekit-client'; +import { Room, ExternalE2EEKeyProvider, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { generateRandomUserId } from '../lib/helper'; const E2EEExample: NextPage = () => { @@ -33,8 +33,6 @@ const E2EEExample: NextPage = () => { [keyProvider], ); - room.setE2EEEnabled(true); - const tokenSource = React.useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); @@ -46,6 +44,12 @@ const E2EEExample: NextPage = () => { room, }); + React.useEffect(() => { + if (typeof window !== 'undefined') { + room.setE2EEEnabled(true); + } + }, [room]); + React.useEffect(() => { session.start({ tracks: { @@ -63,11 +67,28 @@ const E2EEExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); + React.useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (
- - - + {session.isConnected && ( + + + + )}
); }; diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 7ddc9df3e..7b3a22ce7 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -62,9 +62,11 @@ const MinimalExample: NextPage = () => { return (
- - - + {session.isConnected && ( + + + + )}
); }; diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index 6924baf53..e8a88c508 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -10,10 +10,11 @@ import { TrackRefContext, useLocalParticipant, useTracks, + SessionEvent, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { ControlBarControls } from '@livekit/components-react'; -import { LocalVideoTrack, Track, TrackProcessor, TokenSource } from 'livekit-client'; +import { LocalVideoTrack, Track, TrackProcessor, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { BackgroundBlur } from '@livekit/track-processors'; function Stage() { @@ -96,6 +97,21 @@ const ProcessorsExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); + React.useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (
diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 489e33e85..702654bbe 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -11,8 +11,9 @@ import { RoomName, TrackRefContext, useTracks, + SessionEvent, } from '@livekit/components-react'; -import { Track, TokenSource } from 'livekit-client'; +import { Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; import type { NextPage } from 'next'; import { useMemo, useState, useEffect } from 'react'; import styles from '../styles/Simple.module.css'; @@ -23,7 +24,6 @@ const SimpleExample: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; const userIdentity = params?.get('user') ?? generateRandomUserId(); const [connect, setConnect] = useState(false); - const [isConnected, setIsConnected] = useState(false); const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); @@ -53,23 +53,33 @@ const SimpleExample: NextPage = () => { }, [connect, session.start, session.end]); useEffect(() => { - if (session.connectionState === 'connected') { - setIsConnected(true); - } else { - setIsConnected(false); - if (session.connectionState === 'disconnected') { - setConnect(false); - } + if (session.connectionState === 'disconnected') { + setConnect(false); } }, [session.connectionState]); + useEffect(() => { + const handleMediaDevicesError = (error: 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', + ); + }; + + session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); + return () => { + session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); + }; + }, [session]); + return (

Welcome to LiveKit

- {!isConnected && ( + {!session.isConnected && ( @@ -78,7 +88,7 @@ const SimpleExample: NextPage = () => { - {isConnected && } + {session.isConnected && }
From 6de86423a2948deae5a41b3450e7b9851f6fe3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:27:58 +0100 Subject: [PATCH 10/19] Change --- .changeset/tricky-donkeys-wonder.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/tricky-donkeys-wonder.md diff --git a/.changeset/tricky-donkeys-wonder.md b/.changeset/tricky-donkeys-wonder.md new file mode 100644 index 000000000..c267ec735 --- /dev/null +++ b/.changeset/tricky-donkeys-wonder.md @@ -0,0 +1,6 @@ +--- +'@livekit/component-example-next': patch +'@livekit/components-react': patch +--- + +Update nextjs examples with useSession/useAgent hooks From f8375f0654e83e3bde18572b2df2ec4cf6acf690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:09:13 +0100 Subject: [PATCH 11/19] Consistency --- examples/nextjs/pages/audio-only.tsx | 7 ++++-- examples/nextjs/pages/clubhouse.tsx | 22 +++++++++--------- examples/nextjs/pages/customize.tsx | 7 ++++-- examples/nextjs/pages/e2ee.tsx | 18 +++++++-------- examples/nextjs/pages/minimal.tsx | 7 ++---- examples/nextjs/pages/processors.tsx | 27 +++++++++++++---------- examples/nextjs/pages/simple.tsx | 8 +++++-- examples/nextjs/pages/voice-assistant.tsx | 3 ++- 8 files changed, 54 insertions(+), 45 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 81b8737d5..c4ab29d7c 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -7,9 +7,12 @@ import { useMemo, useState, useEffect } from 'react'; import { TokenSource, MediaDeviceFailure } from 'livekit-client'; 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 tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index 4c48b26b1..3b3104729 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -18,12 +18,15 @@ import styles from '../styles/Clubhouse.module.scss'; import { Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { useMemo, useState, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; -import Image from 'next/image'; +import type { NextPage } from 'next'; -const Clubhouse = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; +const Clubhouse: NextPage = () => { + const params = useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = params?.get('user') ?? generateRandomUserId(); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); @@ -108,7 +111,7 @@ const Clubhouse = () => { const Stage = () => { const tracksReferences = useTracks([Track.Source.Microphone]); return ( -
+
@@ -128,21 +131,16 @@ const CustomParticipantTile = () => { return (
-
- + {`Avatar
diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 8000d2a3e..325605dc1 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -23,9 +23,12 @@ 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 params = useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = params?.get('user') ?? generateRandomUserId(); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const [room] = useState(new Room()); diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 02e330df0..5fd43e870 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -2,24 +2,24 @@ import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; -import * as React from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { Room, ExternalE2EEKeyProvider, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { generateRandomUserId } from '../lib/helper'; const E2EEExample: NextPage = () => { - const params = React.useMemo( + const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), [], ); const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = React.useMemo(() => params?.get('user') ?? generateRandomUserId(), [params]); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); setLogLevel('warn', { liveKitClientLogLevel: 'debug' }); - const keyProvider = React.useMemo(() => new ExternalE2EEKeyProvider(), []); + const keyProvider = useMemo(() => new ExternalE2EEKeyProvider(), []); keyProvider.setKey('password'); - const room = React.useMemo( + const room = useMemo( () => new Room({ e2ee: @@ -33,7 +33,7 @@ const E2EEExample: NextPage = () => { [keyProvider], ); - const tokenSource = React.useMemo(() => { + const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); @@ -44,13 +44,13 @@ const E2EEExample: NextPage = () => { room, }); - React.useEffect(() => { + useEffect(() => { if (typeof window !== 'undefined') { room.setE2EEEnabled(true); } }, [room]); - React.useEffect(() => { + useEffect(() => { session.start({ tracks: { camera: { enabled: true }, @@ -67,7 +67,7 @@ const E2EEExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); - React.useEffect(() => { + useEffect(() => { const handleMediaDevicesError = (error: Error) => { const failure = MediaDeviceFailure.getFailure(error); console.error(failure); diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 7b3a22ce7..97005e292 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -3,7 +3,7 @@ import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useMemo, useEffect } from 'react'; +import { useMemo, useEffect, useState } from 'react'; import { TokenSource, MediaDeviceFailure } from 'livekit-client'; const MinimalExample: NextPage = () => { @@ -14,10 +14,7 @@ const MinimalExample: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; setLogLevel('debug', { liveKitClientLogLevel: 'info' }); - const userIdentity = useMemo( - () => params?.get('user') ?? generateRandomUserId(), - [params], - ); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index e8a88c508..dbf0ee215 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -1,6 +1,6 @@ 'use client'; -import * as React from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { setLogLevel } from '@livekit/components-core'; import { GridLayout, @@ -13,24 +13,24 @@ import { SessionEvent, } from '@livekit/components-react'; import type { NextPage } from 'next'; -import { ControlBarControls } from '@livekit/components-react'; import { LocalVideoTrack, Track, TrackProcessor, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { BackgroundBlur } from '@livekit/track-processors'; +import { generateRandomUserId } from '../lib/helper'; function Stage() { const cameraTracks = useTracks([Track.Source.Camera]); const screenShareTrackRef = useTracks([Track.Source.ScreenShare])[0]; - const [blurEnabled, setBlurEnabled] = React.useState(false); - const [processorPending, setProcessorPending] = React.useState(false); + const [blurEnabled, setBlurEnabled] = useState(false); + const [processorPending, setProcessorPending] = useState(false); const { cameraTrack } = useLocalParticipant(); - const [blur, setBlur] = React.useState | undefined>(); + const [blur, setBlur] = useState | undefined>(); - React.useEffect(() => { + useEffect(() => { setBlur(BackgroundBlur()); }, []); - React.useEffect(() => { + useEffect(() => { const localCamTrack = cameraTrack?.track as LocalVideoTrack | undefined; if (localCamTrack) { setProcessorPending(true); @@ -66,11 +66,14 @@ function Stage() { } const ProcessorsExample: 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 = params?.get('user') ?? 'test-identity'; + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const tokenSource = React.useMemo(() => { + const tokenSource = useMemo(() => { return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); }, []); @@ -80,7 +83,7 @@ const ProcessorsExample: NextPage = () => { participantName: userIdentity, }); - React.useEffect(() => { + useEffect(() => { session.start({ tracks: { camera: { enabled: true }, @@ -97,7 +100,7 @@ const ProcessorsExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); - React.useEffect(() => { + useEffect(() => { const handleMediaDevicesError = (error: Error) => { const failure = MediaDeviceFailure.getFailure(error); console.error(failure); diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 702654bbe..d132d66f8 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -20,9 +20,12 @@ import styles from '../styles/Simple.module.css'; import { generateRandomUserId } from '../lib/helper'; const SimpleExample: 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 = params?.get('user') ?? generateRandomUserId(); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const [connect, setConnect] = useState(false); const tokenSource = useMemo(() => { @@ -40,6 +43,7 @@ const SimpleExample: NextPage = () => { session.start({ tracks: { microphone: { enabled: true }, + camera: { enabled: true }, }, }).catch((err) => { console.error('Failed to start session:', err); diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index 993b9bc7f..9b69713f3 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -36,6 +36,7 @@ const VoiceAssistantExample: NextPage = () => { () => params?.get('room') ?? 'test-room-' + Math.random().toFixed(5), [params], ); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const [shouldConnect, setShouldConnect] = useState(false); const tokenSource = useMemo(() => { @@ -44,7 +45,7 @@ const VoiceAssistantExample: NextPage = () => { const session = useSession(tokenSource, { roomName, - participantIdentity: params?.get('user') ?? generateRandomUserId(), + participantIdentity: userIdentity, }); useEffect(() => { From dc0e3a678dd86c89ab4f5e0264a68bf08d78fd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:17:06 +0100 Subject: [PATCH 12/19] Prettier --- examples/nextjs/pages/audio-only.tsx | 29 ++++++++++++++--------- examples/nextjs/pages/clubhouse.tsx | 16 +++++++------ examples/nextjs/pages/customize.tsx | 18 ++++++++------- examples/nextjs/pages/e2ee.tsx | 32 ++++++++++++++++---------- examples/nextjs/pages/minimal.tsx | 24 ++++++++++++------- examples/nextjs/pages/processors.tsx | 26 +++++++++++++-------- examples/nextjs/pages/simple.tsx | 18 ++++++++------- packages/react/src/hooks/useSession.ts | 30 +++++++++++++----------- 8 files changed, 117 insertions(+), 76 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index c4ab29d7c..4fade4879 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -1,6 +1,11 @@ 'use client'; -import { AudioConference, SessionProvider, useSession, SessionEvent } from '@livekit/components-react'; +import { + AudioConference, + SessionProvider, + useSession, + SessionEvent, +} from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; import { useMemo, useState, useEffect } from 'react'; @@ -25,16 +30,18 @@ const AudioExample: NextPage = () => { }); useEffect(() => { - session.start({ - tracks: { - microphone: { enabled: true }, - }, - roomConnectOptions: { - autoSubscribe: true, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + 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); diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index 3b3104729..4f61736a2 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -42,13 +42,15 @@ const Clubhouse: NextPage = () => { useEffect(() => { if (tryToConnect) { - session.start({ - tracks: { - microphone: { enabled: true }, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + 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); diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 325605dc1..e36a1fb67 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -47,13 +47,15 @@ const CustomizeExample: NextPage = () => { useEffect(() => { if (connect) { - session.start({ - tracks: { - microphone: { enabled: true }, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + 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); @@ -137,7 +139,7 @@ export function Stage() { {/* In addition, we can still specify a style attribute and further customize the styles. */} {/* Custom components: Here we replace the provided with our own implementation. */} diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 5fd43e870..53fbc7c72 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -1,6 +1,12 @@ 'use client'; -import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; +import { + SessionProvider, + useSession, + VideoConference, + setLogLevel, + SessionEvent, +} from '@livekit/components-react'; import type { NextPage } from 'next'; import { useMemo, useEffect, useState } from 'react'; import { Room, ExternalE2EEKeyProvider, TokenSource, MediaDeviceFailure } from 'livekit-client'; @@ -25,9 +31,9 @@ const E2EEExample: NextPage = () => { e2ee: typeof window !== 'undefined' ? { - keyProvider, - worker: new Worker(new URL('livekit-client/e2ee-worker', import.meta.url)), - } + keyProvider, + worker: new Worker(new URL('livekit-client/e2ee-worker', import.meta.url)), + } : undefined, }), [keyProvider], @@ -51,14 +57,16 @@ const E2EEExample: NextPage = () => { }, [room]); useEffect(() => { - session.start({ - tracks: { - camera: { enabled: true }, - microphone: { enabled: true }, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + session + .start({ + tracks: { + camera: { enabled: true }, + microphone: { enabled: true }, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); return () => { session.end().catch((err) => { console.error('Failed to end session:', err); diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 97005e292..cef87272b 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -1,6 +1,12 @@ 'use client'; -import { SessionProvider, useSession, VideoConference, setLogLevel, SessionEvent } from '@livekit/components-react'; +import { + SessionProvider, + useSession, + VideoConference, + setLogLevel, + SessionEvent, +} from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; import { useMemo, useEffect, useState } from 'react'; @@ -27,13 +33,15 @@ const MinimalExample: NextPage = () => { }); useEffect(() => { - session.start({ - tracks: { - microphone: { enabled: false }, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + session + .start({ + tracks: { + microphone: { enabled: false }, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); return () => { session.end().catch((err) => { console.error('Failed to end session:', err); diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index dbf0ee215..a89dbf3f5 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -13,7 +13,13 @@ import { SessionEvent, } from '@livekit/components-react'; import type { NextPage } from 'next'; -import { LocalVideoTrack, Track, TrackProcessor, TokenSource, MediaDeviceFailure } from 'livekit-client'; +import { + LocalVideoTrack, + Track, + TrackProcessor, + TokenSource, + MediaDeviceFailure, +} from 'livekit-client'; import { BackgroundBlur } from '@livekit/track-processors'; import { generateRandomUserId } from '../lib/helper'; @@ -84,14 +90,16 @@ const ProcessorsExample: NextPage = () => { }); useEffect(() => { - session.start({ - tracks: { - camera: { enabled: true }, - microphone: { enabled: false }, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + session + .start({ + tracks: { + camera: { enabled: true }, + microphone: { enabled: false }, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); return () => { session.end().catch((err) => { console.error('Failed to end session:', err); diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index d132d66f8..108782f52 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -40,14 +40,16 @@ const SimpleExample: NextPage = () => { useEffect(() => { if (connect) { - session.start({ - tracks: { - microphone: { enabled: true }, - camera: { enabled: true }, - }, - }).catch((err) => { - console.error('Failed to start session:', err); - }); + session + .start({ + tracks: { + microphone: { enabled: true }, + camera: { enabled: true }, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); } else { session.end().catch((err) => { console.error('Failed to end session:', err); diff --git a/packages/react/src/hooks/useSession.ts b/packages/react/src/hooks/useSession.ts index 252c13e47..d1409287a 100644 --- a/packages/react/src/hooks/useSession.ts +++ b/packages/react/src/hooks/useSession.ts @@ -99,9 +99,9 @@ type SessionStateConnecting = SessionStateCommon & { type SessionStateConnected = SessionStateCommon & { connectionState: - | ConnectionState.Connected - | ConnectionState.Reconnecting - | ConnectionState.SignalReconnecting; + | ConnectionState.Connected + | ConnectionState.Reconnecting + | ConnectionState.SignalReconnecting; isConnected: true; local: { @@ -325,11 +325,11 @@ export function useSession( connectionState === ConnectionState.SignalReconnecting, }) as { isConnected: State extends - | ConnectionState.Connected - | ConnectionState.Reconnecting - | ConnectionState.SignalReconnecting - ? true - : false; + | ConnectionState.Connected + | ConnectionState.Reconnecting + | ConnectionState.SignalReconnecting + ? true + : false; }, [], ); @@ -554,13 +554,17 @@ export function useSession( // Start microphone (with preconnect buffer) by default tracks.microphone?.enabled ? room.localParticipant.setMicrophoneEnabled( - true, - undefined, - tracks.microphone?.publishOptions ?? {}, - ) + true, + undefined, + tracks.microphone?.publishOptions ?? {}, + ) : Promise.resolve(), tracks.camera?.enabled - ? room.localParticipant.setCameraEnabled(true, undefined, tracks.camera?.publishOptions ?? {}) + ? room.localParticipant.setCameraEnabled( + true, + undefined, + tracks.camera?.publishOptions ?? {}, + ) : Promise.resolve(), ]); From f35d5a178c8d19f4f54c0c6953a0340793723f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82az=CC=87ej=20Pankowski?= <86720177+pblazej@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:26:25 +0100 Subject: [PATCH 13/19] API --- packages/react/etc/components-react.api.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react/etc/components-react.api.md b/packages/react/etc/components-react.api.md index 5dba67415..239fea131 100644 --- a/packages/react/etc/components-react.api.md +++ b/packages/react/etc/components-react.api.md @@ -669,6 +669,10 @@ export type SessionConnectOptions = { enabled?: boolean; publishOptions?: TrackPublishOptions; }; + camera?: { + enabled?: boolean; + publishOptions?: TrackPublishOptions; + }; }; roomConnectOptions?: RoomConnectOptions; }; From 63d3da72586051abd3f2b8afa6f5bf434b513103 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Dec 2025 11:55:14 -0500 Subject: [PATCH 14/19] fix: address stretched header image on index page --- examples/nextjs/pages/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/nextjs/pages/index.tsx b/examples/nextjs/pages/index.tsx index 564ffe263..e2c27d597 100644 --- a/examples/nextjs/pages/index.tsx +++ b/examples/nextjs/pages/index.tsx @@ -42,11 +42,10 @@ const Home: NextPage = () => {
LiveKit components text logo.

Some simple sample apps to help you get started working with LiveKit Components.

From e5573e57aa800bdbc718ecef47ea262fe82083a4 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Dec 2025 12:01:15 -0500 Subject: [PATCH 15/19] fix: migrate all session.internal event emitter usage to useEvents --- examples/nextjs/pages/audio-only.tsx | 22 ++++++++-------------- examples/nextjs/pages/clubhouse.tsx | 22 ++++++++-------------- examples/nextjs/pages/customize.tsx | 22 ++++++++-------------- examples/nextjs/pages/e2ee.tsx | 22 ++++++++-------------- examples/nextjs/pages/minimal.tsx | 22 ++++++++-------------- examples/nextjs/pages/processors.tsx | 22 ++++++++-------------- examples/nextjs/pages/simple.tsx | 22 ++++++++-------------- examples/nextjs/pages/voice-assistant.tsx | 22 ++++++++-------------- 8 files changed, 64 insertions(+), 112 deletions(-) diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 4fade4879..0e54de36f 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -5,6 +5,7 @@ import { SessionProvider, useSession, SessionEvent, + useEvents, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; @@ -50,20 +51,13 @@ const AudioExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index 4f61736a2..3a09bf98e 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -13,6 +13,7 @@ import { useTrackRefContext, useTracks, SessionEvent, + useEvents, } from '@livekit/components-react'; import styles from '../styles/Clubhouse.module.scss'; import { Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; @@ -65,20 +66,13 @@ const Clubhouse: NextPage = () => { } }, [session.connectionState]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + useEvents(session, SessionEvent.MediaDevicesError, (error: 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 (
diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index e36a1fb67..2f68f26a7 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -14,6 +14,7 @@ import { useTracks, TrackRefContext, SessionEvent, + useEvents, } from '@livekit/components-react'; import { ConnectionQuality, Room, Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; import styles from '../styles/Simple.module.css'; @@ -70,20 +71,13 @@ const CustomizeExample: NextPage = () => { } }, [session.connectionState]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 53fbc7c72..67fc56c4a 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -6,6 +6,7 @@ import { VideoConference, setLogLevel, SessionEvent, + useEvents, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { useMemo, useEffect, useState } from 'react'; @@ -75,20 +76,13 @@ const E2EEExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index cef87272b..6f4cb87b6 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -6,6 +6,7 @@ import { VideoConference, setLogLevel, SessionEvent, + useEvents, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; @@ -50,20 +51,13 @@ const MinimalExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index a89dbf3f5..ab5b2d798 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -11,6 +11,7 @@ import { useLocalParticipant, useTracks, SessionEvent, + useEvents, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { @@ -108,20 +109,13 @@ const ProcessorsExample: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.start, session.end]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 108782f52..ec0f9cd20 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -12,6 +12,7 @@ import { TrackRefContext, useTracks, SessionEvent, + useEvents, } from '@livekit/components-react'; import { Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; import type { NextPage } from 'next'; @@ -64,20 +65,13 @@ const SimpleExample: NextPage = () => { } }, [session.connectionState]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx index 9b69713f3..ee998211b 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/voice-assistant.tsx @@ -8,6 +8,7 @@ import { SessionProvider, useSession, SessionEvent, + useEvents, } from '@livekit/components-react'; import type { NextPage } from 'next'; import { useMemo, useState, useEffect } from 'react'; @@ -67,20 +68,13 @@ const VoiceAssistantExample: NextPage = () => { } }, [session.connectionState]); - useEffect(() => { - const handleMediaDevicesError = (error: 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', - ); - }; - - session.internal.emitter.on(SessionEvent.MediaDevicesError, handleMediaDevicesError); - return () => { - session.internal.emitter.off(SessionEvent.MediaDevicesError, handleMediaDevicesError); - }; - }, [session]); + 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 (
From 95bad4f4ed9faebc95b3a39641bcda1314ef01ce Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Dec 2025 12:02:36 -0500 Subject: [PATCH 16/19] refactor: rename VoiceAssistantExample => AgentExample --- examples/nextjs/pages/{voice-assistant.tsx => agent.tsx} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename examples/nextjs/pages/{voice-assistant.tsx => agent.tsx} (96%) diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/agent.tsx similarity index 96% rename from examples/nextjs/pages/voice-assistant.tsx rename to examples/nextjs/pages/agent.tsx index ee998211b..c49949080 100644 --- a/examples/nextjs/pages/voice-assistant.tsx +++ b/examples/nextjs/pages/agent.tsx @@ -28,7 +28,7 @@ function SimpleVoiceAssistant() { ); } -const VoiceAssistantExample: NextPage = () => { +const AgentExample: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), [], @@ -97,4 +97,4 @@ const VoiceAssistantExample: NextPage = () => { ); }; -export default VoiceAssistantExample; +export default AgentExample; From 29d13fd4d33bd17ca1e959378845ffb3d1d98511 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Dec 2025 12:09:42 -0500 Subject: [PATCH 17/19] refactor: move TokenSource outside of example components --- examples/nextjs/pages/agent.tsx | 6 ++---- examples/nextjs/pages/audio-only.tsx | 6 ++---- examples/nextjs/pages/clubhouse.tsx | 6 ++---- examples/nextjs/pages/customize.tsx | 6 ++---- examples/nextjs/pages/e2ee.tsx | 6 ++---- examples/nextjs/pages/minimal.tsx | 6 ++---- examples/nextjs/pages/processors.tsx | 6 ++---- examples/nextjs/pages/simple.tsx | 6 ++---- 8 files changed, 16 insertions(+), 32 deletions(-) diff --git a/examples/nextjs/pages/agent.tsx b/examples/nextjs/pages/agent.tsx index c49949080..002c6e9a4 100644 --- a/examples/nextjs/pages/agent.tsx +++ b/examples/nextjs/pages/agent.tsx @@ -28,6 +28,8 @@ function SimpleVoiceAssistant() { ); } +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), @@ -40,10 +42,6 @@ const AgentExample: NextPage = () => { const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const [shouldConnect, setShouldConnect] = useState(false); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 0e54de36f..11bea4aa8 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -12,6 +12,8 @@ import { generateRandomUserId } from '../lib/helper'; 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 = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -20,10 +22,6 @@ const AudioExample: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index 3a09bf98e..57869bfc9 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -21,6 +21,8 @@ import { useMemo, useState, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; import type { NextPage } from 'next'; +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + const Clubhouse: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -29,10 +31,6 @@ const Clubhouse: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 2f68f26a7..d09e0dc3c 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -23,6 +23,8 @@ import type { NextPage } from 'next'; import { HTMLAttributes, useState, useMemo, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + const CustomizeExample: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -33,10 +35,6 @@ const CustomizeExample: NextPage = () => { 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, diff --git a/examples/nextjs/pages/e2ee.tsx b/examples/nextjs/pages/e2ee.tsx index 67fc56c4a..9880dffab 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -13,6 +13,8 @@ import { useMemo, useEffect, useState } from 'react'; import { Room, ExternalE2EEKeyProvider, TokenSource, MediaDeviceFailure } from 'livekit-client'; import { generateRandomUserId } from '../lib/helper'; +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + const E2EEExample: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -40,10 +42,6 @@ const E2EEExample: NextPage = () => { [keyProvider], ); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 6f4cb87b6..043132bff 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -13,6 +13,8 @@ import { generateRandomUserId } from '../lib/helper'; import { useMemo, useEffect, useState } from 'react'; import { TokenSource, MediaDeviceFailure } from 'livekit-client'; +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + const MinimalExample: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -23,10 +25,6 @@ const MinimalExample: NextPage = () => { const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index ab5b2d798..5219ce636 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -72,6 +72,8 @@ function Stage() { ); } +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + const ProcessorsExample: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -80,10 +82,6 @@ const ProcessorsExample: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index ec0f9cd20..c099519d8 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -20,6 +20,8 @@ import { useMemo, useState, useEffect } from 'react'; import styles from '../styles/Simple.module.css'; import { generateRandomUserId } from '../lib/helper'; +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + const SimpleExample: NextPage = () => { const params = useMemo( () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), @@ -29,10 +31,6 @@ const SimpleExample: NextPage = () => { const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); const [connect, setConnect] = useState(false); - const tokenSource = useMemo(() => { - return TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); - }, []); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, From 238b090a9b58480c6be9a15550ab3a4edebc8c3b Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Dec 2025 12:12:26 -0500 Subject: [PATCH 18/19] refactor: remove noop externally managed room --- examples/nextjs/pages/customize.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index d09e0dc3c..0909f6825 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -33,13 +33,10 @@ const CustomizeExample: NextPage = () => { const roomName = params?.get('room') ?? 'test-room'; const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const [room] = useState(new Room()); - const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, participantName: userIdentity, - room, }); const [connect, setConnect] = useState(false); From 1383d7765411d3dff501c086f8447f1887380a19 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Tue, 9 Dec 2025 12:43:35 -0500 Subject: [PATCH 19/19] refactor: refurbish ai agent example to use more of agent sdk --- examples/nextjs/pages/agent.tsx | 25 ++++++++++++++++--------- examples/nextjs/pages/index.tsx | 4 ++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/examples/nextjs/pages/agent.tsx b/examples/nextjs/pages/agent.tsx index 002c6e9a4..4bc538f18 100644 --- a/examples/nextjs/pages/agent.tsx +++ b/examples/nextjs/pages/agent.tsx @@ -16,13 +16,20 @@ import { MediaDeviceFailure, TokenSource } from 'livekit-client'; import styles from '../styles/VoiceAssistant.module.scss'; import { generateRandomUserId } from '../lib/helper'; -function SimpleVoiceAssistant() { +function SimpleAgent() { const agent = useAgent(); + + useEffect(() => { + if (agent.state === 'failed') { + alert(`Agent error: ${agent.failureReasons.join(', ')}`); + } + }, [agent.state, agent.failureReasons]); + return ( ); @@ -40,15 +47,15 @@ const AgentExample: NextPage = () => { [params], ); const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const [shouldConnect, setShouldConnect] = useState(false); const session = useSession(tokenSource, { roomName, participantIdentity: userIdentity, }); + const [started, setStarted] = useState(false); useEffect(() => { - if (shouldConnect) { + if (started) { session.start().catch((err) => { console.error('Failed to start session:', err); }); @@ -58,11 +65,11 @@ const AgentExample: NextPage = () => { }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldConnect, session.start, session.end]); + }, [started, session.start, session.end]); useEffect(() => { if (session.connectionState === 'disconnected') { - setShouldConnect(false); + setStarted(false); } }, [session.connectionState]); @@ -79,10 +86,10 @@ const AgentExample: NextPage = () => {
- {shouldConnect ? ( - + {started ? ( + ) : ( - )} diff --git a/examples/nextjs/pages/index.tsx b/examples/nextjs/pages/index.tsx index e2c27d597..2d14b3f77 100644 --- a/examples/nextjs/pages/index.tsx +++ b/examples/nextjs/pages/index.tsx @@ -7,8 +7,8 @@ import Image from 'next/image'; const EXAMPLE_ROUTES = { voiceAssistant: { - title: 'AI Voice Assistant example', - href: () => `/voice-assistant`, + title: 'AI Agent example', + href: () => `/agent`, }, minimal: { title: 'VideoConference example with minimal code', href: () => `/minimal` }, simple: { title: 'Simple custom setup example', href: () => `/simple` },