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 diff --git a/examples/nextjs/pages/agent.tsx b/examples/nextjs/pages/agent.tsx new file mode 100644 index 000000000..4bc538f18 --- /dev/null +++ b/examples/nextjs/pages/agent.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { + useAgent, + BarVisualizer, + RoomAudioRenderer, + VoiceAssistantControlBar, + SessionProvider, + useSession, + SessionEvent, + useEvents, +} from '@livekit/components-react'; +import type { NextPage } from 'next'; +import { useMemo, useState, useEffect } from 'react'; +import { MediaDeviceFailure, TokenSource } from 'livekit-client'; +import styles from '../styles/VoiceAssistant.module.scss'; +import { generateRandomUserId } from '../lib/helper'; + +function SimpleAgent() { + const agent = useAgent(); + + useEffect(() => { + if (agent.state === 'failed') { + alert(`Agent error: ${agent.failureReasons.join(', ')}`); + } + }, [agent.state, agent.failureReasons]); + + return ( + + ); +} + +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + +const AgentExample: NextPage = () => { + const params = useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); + const roomName = useMemo( + () => params?.get('room') ?? 'test-room-' + Math.random().toFixed(5), + [params], + ); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); + + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + }); + + const [started, setStarted] = useState(false); + useEffect(() => { + if (started) { + session.start().catch((err) => { + console.error('Failed to start session:', err); + }); + } else { + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [started, session.start, session.end]); + + useEffect(() => { + if (session.connectionState === 'disconnected') { + setStarted(false); + } + }, [session.connectionState]); + + useEvents(session, SessionEvent.MediaDevicesError, (error) => { + const failure = MediaDeviceFailure.getFailure(error); + console.error(failure); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }, []); + + return ( +
+ +
+
+ {started ? ( + + ) : ( + + )} +
+ + +
+
+
+ ); +}; + +export default AgentExample; 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 }); } } diff --git a/examples/nextjs/pages/audio-only.tsx b/examples/nextjs/pages/audio-only.tsx index 803eadfde..11bea4aa8 100644 --- a/examples/nextjs/pages/audio-only.tsx +++ b/examples/nextjs/pages/audio-only.tsx @@ -1,32 +1,67 @@ 'use client'; -import { AudioConference, LiveKitRoom, useToken } from '@livekit/components-react'; +import { + AudioConference, + SessionProvider, + useSession, + SessionEvent, + useEvents, +} from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useState } from 'react'; +import { useMemo, useState, useEffect } from 'react'; +import { TokenSource, MediaDeviceFailure } from 'livekit-client'; + +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); const AudioExample: NextPage = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; + const params = useMemo( + () => (typeof window !== 'undefined' ? new URLSearchParams(location.search) : null), + [], + ); const roomName = params?.get('room') ?? 'test-room'; - const [userIdentity] = useState(params?.get('user') ?? generateRandomUserId()); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, }); + useEffect(() => { + session + .start({ + tracks: { + microphone: { enabled: true }, + }, + roomConnectOptions: { + autoSubscribe: true, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); + return () => { + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session.start, session.end]); + + useEvents(session, SessionEvent.MediaDevicesError, (error) => { + const failure = MediaDeviceFailure.getFailure(error); + console.error(failure); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }, []); + return (
- + - +
); }; diff --git a/examples/nextjs/pages/clubhouse.tsx b/examples/nextjs/pages/clubhouse.tsx index a35d8a187..57869bfc9 100644 --- a/examples/nextjs/pages/clubhouse.tsx +++ b/examples/nextjs/pages/clubhouse.tsx @@ -2,52 +2,79 @@ import { ControlBar, - LiveKitRoom, + SessionProvider, + useSession, RoomAudioRenderer, RoomName, TrackLoop, TrackMutedIndicator, useIsMuted, useIsSpeaking, - useToken, useTrackRefContext, useTracks, + SessionEvent, + useEvents, } from '@livekit/components-react'; import styles from '../styles/Clubhouse.module.scss'; -import { Track } from 'livekit-client'; -import { useMemo, useState } from 'react'; +import { Track, TokenSource, MediaDeviceFailure } from 'livekit-client'; +import { useMemo, useState, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; +import type { NextPage } from 'next'; -const Clubhouse = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; +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), + [], + ); const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = params?.get('user') ?? generateRandomUserId(); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, }); const [tryToConnect, setTryToConnect] = useState(false); - const [connected, setConnected] = useState(false); + + useEffect(() => { + if (tryToConnect) { + session + .start({ + tracks: { + microphone: { enabled: true }, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); + } else { + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tryToConnect, session.start, session.end]); + + useEffect(() => { + if (session.connectionState === 'disconnected') { + setTryToConnect(false); + } + }, [session.connectionState]); + + 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 (
- setConnected(true)} - onDisconnected={() => { - setTryToConnect(false); - setConnected(false); - }} - > +
-
+

@@ -70,7 +97,7 @@ const Clubhouse = () => { />
- +
); }; @@ -78,7 +105,7 @@ const Clubhouse = () => { const Stage = () => { const tracksReferences = useTracks([Track.Source.Microphone]); return ( -
+
@@ -98,14 +125,10 @@ const CustomParticipantTile = () => { return (
-
+
{ - 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 token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, - }); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); - const [room] = useState(new Room()); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }); const [connect, setConnect] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const handleDisconnect = () => { - setConnect(false); - setIsConnected(false); - }; + + useEffect(() => { + if (connect) { + session + .start({ + tracks: { + microphone: { enabled: true }, + }, + }) + .catch((err) => { + console.error('Failed to start session:', err); + }); + } else { + session.end().catch((err) => { + console.error('Failed to end session:', err); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connect, session.start, session.end]); + + useEffect(() => { + if (session.connectionState === 'disconnected') { + setConnect(false); + } + }, [session.connectionState]); + + useEvents(session, SessionEvent.MediaDevicesError, (error) => { + const failure = MediaDeviceFailure.getFailure(error); + console.error(failure); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }, []); return (
@@ -47,26 +80,17 @@ const CustomizeExample: NextPage = () => {

Welcome to LiveKit

- {!isConnected && ( + {!session.isConnected && ( )} - setIsConnected(true)} - onDisconnected={handleDisconnect} - audio={true} - video={true} - > + {/* 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 3d88c3472..9880dffab 100644 --- a/examples/nextjs/pages/e2ee.tsx +++ b/examples/nextjs/pages/e2ee.tsx @@ -1,29 +1,34 @@ 'use client'; -import { LiveKitRoom, useToken, VideoConference, setLogLevel } from '@livekit/components-react'; +import { + SessionProvider, + useSession, + VideoConference, + setLogLevel, + SessionEvent, + useEvents, +} from '@livekit/components-react'; import type { NextPage } from 'next'; -import * as React from 'react'; -import { Room, ExternalE2EEKeyProvider } from 'livekit-client'; +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 = 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 = React.useMemo(() => params?.get('user') ?? generateRandomUserId(), []); + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); 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(), []); + const keyProvider = useMemo(() => new ExternalE2EEKeyProvider(), []); keyProvider.setKey('password'); - const room = React.useMemo( + const room = useMemo( () => new Room({ e2ee: @@ -34,22 +39,56 @@ const E2EEExample: NextPage = () => { } : undefined, }), - [], + [keyProvider], ); - room.setE2EEEnabled(true); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + room, + }); + + useEffect(() => { + if (typeof window !== 'undefined') { + room.setE2EEEnabled(true); + } + }, [room]); + + useEffect(() => { + 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); + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session.start, session.end]); + + useEvents(session, SessionEvent.MediaDevicesError, (error) => { + const failure = MediaDeviceFailure.getFailure(error); + console.error(failure); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }, []); return (
- - - + {session.isConnected && ( + + + + )}
); }; diff --git a/examples/nextjs/pages/index.tsx b/examples/nextjs/pages/index.tsx index 3c6cf44bf..2d14b3f77 100644 --- a/examples/nextjs/pages/index.tsx +++ b/examples/nextjs/pages/index.tsx @@ -3,11 +3,12 @@ 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: { - 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` }, @@ -40,10 +41,11 @@ 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 06442a7bf..043132bff 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -1,43 +1,69 @@ 'use client'; -import { LiveKitRoom, useToken, VideoConference, setLogLevel } from '@livekit/components-react'; +import { + SessionProvider, + useSession, + VideoConference, + setLogLevel, + SessionEvent, + useEvents, +} from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useMemo } from 'react'; +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 = 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' }); - const tokenOptions = useMemo(() => { - const userId = params?.get('user') ?? generateRandomUserId(); - return { - userInfo: { - identity: userId, - name: userId, - }, + const [userIdentity] = useState(() => params?.get('user') ?? generateRandomUserId()); + + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }); + + useEffect(() => { + 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); + }); }; - }, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session.start, session.end]); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions); + 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 (
- { - 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', - ); - }} - > - - + {session.isConnected && ( + + + + )}
); }; diff --git a/examples/nextjs/pages/processors.tsx b/examples/nextjs/pages/processors.tsx index 44f3712ce..5219ce636 100644 --- a/examples/nextjs/pages/processors.tsx +++ b/examples/nextjs/pages/processors.tsx @@ -1,36 +1,48 @@ 'use client'; -import * as React from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { setLogLevel } from '@livekit/components-core'; import { GridLayout, - LiveKitRoom, + SessionProvider, + useSession, ParticipantTile, TrackRefContext, useLocalParticipant, - useToken, useTracks, + SessionEvent, + useEvents, } 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, + 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] = React.useState(BackgroundBlur()); + const [blur, setBlur] = useState | undefined>(); + + useEffect(() => { + setBlur(BackgroundBlur()); + }, []); - React.useEffect(() => { + 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 +51,7 @@ function Stage() { setProcessorPending(false); } } - }, [blurEnabled, cameraTrack]); + }, [blurEnabled, cameraTrack, blur]); return ( <> @@ -60,29 +72,54 @@ function Stage() { ); } +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + 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 token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, }); + useEffect(() => { + session + .start({ + tracks: { + 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); + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [session.start, session.end]); + + useEvents(session, SessionEvent.MediaDevicesError, (error) => { + const failure = MediaDeviceFailure.getFailure(error); + console.error(failure); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }, []); + return (
- + - +
); }; diff --git a/examples/nextjs/pages/simple.tsx b/examples/nextjs/pages/simple.tsx index 7e9e981ab..c099519d8 100644 --- a/examples/nextjs/pages/simple.tsx +++ b/examples/nextjs/pages/simple.tsx @@ -4,42 +4,72 @@ import { ConnectionState, ControlBar, GridLayout, - LiveKitRoom, + SessionProvider, + useSession, ParticipantTile, RoomAudioRenderer, RoomName, TrackRefContext, - useToken, useTracks, + SessionEvent, + useEvents, } from '@livekit/components-react'; -import { Track } from 'livekit-client'; +import { Track, TokenSource, MediaDeviceFailure } 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'; +const tokenSource = TokenSource.endpoint(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT!); + 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 [isConnected, setIsConnected] = useState(false); - const userInfo = useMemo(() => { - return { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, - }; - }, []); + const session = useSession(tokenSource, { + roomName, + participantIdentity: userIdentity, + participantName: userIdentity, + }); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, userInfo); + useEffect(() => { + if (connect) { + 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); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connect, session.start, session.end]); - const handleDisconnect = () => { - setConnect(false); - setIsConnected(false); - }; + useEffect(() => { + if (session.connectionState === 'disconnected') { + setConnect(false); + } + }, [session.connectionState]); + + useEvents(session, SessionEvent.MediaDevicesError, (error) => { + const failure = MediaDeviceFailure.getFailure(error); + console.error(failure); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }, []); return (
@@ -47,26 +77,18 @@ const SimpleExample: NextPage = () => {

Welcome to LiveKit

- {!isConnected && ( + {!session.isConnected && ( )} - setIsConnected(true)} - onDisconnected={handleDisconnect} - audio={true} - video={true} - > + - {isConnected && } + {session.isConnected && } - +
); diff --git a/examples/nextjs/pages/voice-assistant.tsx b/examples/nextjs/pages/voice-assistant.tsx deleted file mode 100644 index 16387bd55..000000000 --- a/examples/nextjs/pages/voice-assistant.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client'; - -import { - LiveKitRoom, - useToken, - useVoiceAssistant, - BarVisualizer, - RoomAudioRenderer, - VoiceAssistantControlBar, -} from '@livekit/components-react'; -import type { NextPage } from 'next'; -import { useMemo, useState } from 'react'; -import { MediaDeviceFailure } from 'livekit-client'; -import styles from '../styles/VoiceAssistant.module.scss'; -import { generateRandomUserId } from '../lib/helper'; - -function SimpleVoiceAssistant() { - const { state, audioTrack } = useVoiceAssistant(); - return ( - - ); -} - -const VoiceAssistantExample: NextPage = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; - const roomName = useMemo( - () => params?.get('room') ?? 'test-room-' + Math.random().toFixed(5), - [], - ); - const [shouldConnect, setShouldConnect] = useState(false); - - const tokenOptions = useMemo(() => { - const userId = params?.get('user') ?? generateRandomUserId(); - return { - userInfo: { - identity: userId, - name: userId, - }, - }; - }, []); - - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions); - - 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', - ); - }; - - return ( -
- setShouldConnect(false)} - className={styles.room} - > -
- {shouldConnect ? ( - - ) : ( - - )} -
- - -
-
- ); -}; - -export default VoiceAssistantExample; 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; }; diff --git a/packages/react/src/hooks/useSession.ts b/packages/react/src/hooks/useSession.ts index d4d25cc3b..d1409287a 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) */ @@ -555,6 +559,13 @@ export function useSession( tracks.microphone?.publishOptions ?? {}, ) : Promise.resolve(), + tracks.camera?.enabled + ? room.localParticipant.setCameraEnabled( + true, + undefined, + tracks.camera?.publishOptions ?? {}, + ) + : Promise.resolve(), ]); await waitUntilConnected(signal);