From 2067798ad20f7b411944feda72b8c652f2ef1c41 Mon Sep 17 00:00:00 2001 From: "John D. Pope" Date: Sat, 26 Oct 2024 06:02:26 +1100 Subject: [PATCH 1/2] Update customize.tsx dont do a dDOS attack if not logged in --- examples/nextjs/pages/customize.tsx | 114 ++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 31 deletions(-) diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 2f60dd377..7a1ebd399 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -8,7 +8,6 @@ import { isTrackReference, useConnectionQualityIndicator, VideoTrack, - useToken, ControlBar, GridLayout, useTracks, @@ -18,28 +17,68 @@ import { ConnectionQuality, Room, Track } from 'livekit-client'; import styles from '../styles/Simple.module.css'; import myStyles from '../styles/Customize.module.css'; import type { NextPage } from 'next'; -import { HTMLAttributes, useState } from 'react'; +import { HTMLAttributes, useState, useCallback, useEffect } from 'react'; import { generateRandomUserId } from '../lib/helper'; const CustomizeExample: NextPage = () => { - const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; - const roomName = params?.get('room') ?? 'test-room'; - const userIdentity = params?.get('user') ?? generateRandomUserId(); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, { - userInfo: { - identity: userIdentity, - name: userIdentity, - }, + const [room] = useState(() => new Room()); + const [token, setToken] = useState(); + const [connect, setConnect] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(); + + // Get room parameters once on mount + const [roomParams] = useState(() => { + if (typeof window === 'undefined') return { roomName: 'test-room', userIdentity: generateRandomUserId() }; + const params = new URLSearchParams(window.location.search); + return { + roomName: params.get('room') ?? 'test-room', + userIdentity: params.get('user') ?? generateRandomUserId(), + }; }); - const [room] = useState(new Room()); + // Fetch token only when connect button is clicked + const fetchToken = useCallback(async () => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT}?` + + `identity=${encodeURIComponent(roomParams.userIdentity)}&` + + `name=${encodeURIComponent(roomParams.userIdentity)}&` + + `roomName=${encodeURIComponent(roomParams.roomName)}` + ); - const [connect, setConnect] = useState(false); - const [isConnected, setIsConnected] = useState(false); - const handleDisconnect = () => { + if (!response.ok) { + const text = await response.text(); + throw new Error(text || response.statusText); + } + + const data = await response.json(); + if (!data.accessToken) { + throw new Error('No access token received'); + } + + setToken(data.accessToken); + setConnect(true); + setError(undefined); + } catch (e) { + console.error('Error fetching token:', e); + setError(e instanceof Error ? e.message : 'Failed to get access token'); + setConnect(false); + } + }, [roomParams]); + + const handleDisconnect = useCallback(() => { setConnect(false); setIsConnected(false); - }; + setToken(undefined); + setError(undefined); + }, []); + + const handleError = useCallback((err: Error) => { + console.error('LiveKit error:', err); + setError(err.message); + handleDisconnect(); + }, [handleDisconnect]); return (
@@ -47,26 +86,39 @@ const CustomizeExample: NextPage = () => {

Welcome to LiveKit

+ + {error && ( +
+ Error: {error} +
+ )} + {!isConnected && ( - )} - setIsConnected(true)} - onDisconnected={handleDisconnect} - audio={true} - video={true} - > - - {/* Render a custom Stage component once connected */} - {isConnected && } - - + + {token && ( + setIsConnected(true)} + onDisconnected={handleDisconnect} + onError={handleError} + audio={true} + video={true} + > + + {isConnected && } + + + )}
); From 5afa44d13df51ce204a62414560ade857a312af4 Mon Sep 17 00:00:00 2001 From: "John D. Pope" Date: Sat, 26 Oct 2024 06:08:17 +1100 Subject: [PATCH 2/2] Update customize.tsx helper for missing camera / mic --- examples/nextjs/pages/customize.tsx | 51 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/examples/nextjs/pages/customize.tsx b/examples/nextjs/pages/customize.tsx index 7a1ebd399..20f14ee5e 100644 --- a/examples/nextjs/pages/customize.tsx +++ b/examples/nextjs/pages/customize.tsx @@ -26,6 +26,8 @@ const CustomizeExample: NextPage = () => { const [connect, setConnect] = useState(false); const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState(); + const [hasAudio, setHasAudio] = useState(false); + const [hasVideo, setHasVideo] = useState(false); // Get room parameters once on mount const [roomParams] = useState(() => { @@ -37,6 +39,30 @@ const CustomizeExample: NextPage = () => { }; }); + // Check available devices on mount + useEffect(() => { + async function checkDevices() { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + setHasAudio(devices.some(device => device.kind === 'audioinput')); + setHasVideo(devices.some(device => device.kind === 'videoinput')); + } catch (e) { + console.warn('Unable to check media devices:', e); + setHasAudio(false); + setHasVideo(false); + } + } + + // Only check devices if on client side and API is available + if (typeof window !== 'undefined' && navigator.mediaDevices) { + checkDevices(); + + // Listen for device changes + navigator.mediaDevices.addEventListener('devicechange', checkDevices); + return () => navigator.mediaDevices.removeEventListener('devicechange', checkDevices); + } + }, []); + // Fetch token only when connect button is clicked const fetchToken = useCallback(async () => { try { @@ -75,6 +101,11 @@ const CustomizeExample: NextPage = () => { }, []); const handleError = useCallback((err: Error) => { + // Don't treat missing devices as a fatal error + if (err.name === 'NotFoundError' || err.name === 'NotAllowedError') { + console.warn('Media device error:', err); + return; + } console.error('LiveKit error:', err); setError(err.message); handleDisconnect(); @@ -87,8 +118,14 @@ const CustomizeExample: NextPage = () => { Welcome to LiveKit + {!hasAudio && !hasVideo && ( +
+ No camera or microphone detected. You can still join but won't be able to share audio or video. +
+ )} + {error && ( -
+
Error: {error}
)} @@ -111,8 +148,16 @@ const CustomizeExample: NextPage = () => { onConnected={() => setIsConnected(true)} onDisconnected={handleDisconnect} onError={handleError} - audio={true} - video={true} + // Only enable audio/video if devices are available + audio={hasAudio} + video={hasVideo} + options={{ + audioCaptureDefaults: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }, + }} > {isConnected && }