diff --git a/packages/react/etc/components-react.api.md b/packages/react/etc/components-react.api.md index acdf30abf..974d2995d 100644 --- a/packages/react/etc/components-react.api.md +++ b/packages/react/etc/components-react.api.md @@ -1063,6 +1063,30 @@ export interface UserInfo { name?: string; } +// @public +export function useRoomConnection(options: UseRoomConnectionOptions): UseRoomConnectionResult; + +// @public (undocumented) +export type UseRoomConnectionDetails = { + serverUrl: string; + participantToken: string; +}; + +// @public (undocumented) +export type UseRoomConnectionOptions = { + getConnectionDetails: () => Promise; + onConnectionError?: (err: Error) => void; + room?: Room; + connected?: boolean; + trackPublishOptions?: TrackPublishOptions; +}; + +// @public (undocumented) +export type UseRoomConnectionResult = { + room: Room; + status: 'idle' | 'connecting' | 'disconnecting'; +}; + // @public export function useRoomContext(): Room; diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index cdd9ed342..b57e08e6e 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -56,3 +56,4 @@ export * from './useParticipantAttributes'; export * from './useIsRecording'; export * from './useTextStream'; export * from './useTranscriptions'; +export * from './useRoomConnection'; diff --git a/packages/react/src/hooks/useRoomConnection.ts b/packages/react/src/hooks/useRoomConnection.ts new file mode 100644 index 000000000..bfbf47c62 --- /dev/null +++ b/packages/react/src/hooks/useRoomConnection.ts @@ -0,0 +1,97 @@ +import { Mutex, type TrackPublishOptions, Room } from 'livekit-client'; +import { useEffect, useMemo, useState } from 'react'; +import { useMaybeRoomContext } from '../context'; + +/** @public */ +export type UseRoomConnectionDetails = { + serverUrl: string; + participantToken: string; +}; + +/** @public */ +export type UseRoomConnectionOptions = { + getConnectionDetails: () => Promise; + onConnectionError?: (err: Error) => void; + + room?: Room; + /** Should the room attempt to connect? If false, the room will disconnect. */ + connected?: boolean; + trackPublishOptions?: TrackPublishOptions; +}; + +/** @public */ +export type UseRoomConnectionResult = { + room: Room; + + /** What operation is the useRoomConnection hook currently in the midst of performing? */ + status: 'idle' | 'connecting' | 'disconnecting'; +}; + +/** + * The `useRoomConnection` hook provides a fully managed way to connect / disconnect from a LiveKit + * room. To control whether the connection is active or not, use `options.connected`. + * @remarks + * Can be called inside a `RoomContext` or by passing a `Room` instance, otherwise creates a Room + * itself. + * + * @example + * ```tsx + * const { room } = useRoomConnection({ + * getConnectionDetails: async () => { + * // compute the below value out of band: + * return { serverUrl: "...", participantToken: "..." }; + * }, + * }); + * ``` + * @public + */ +export function useRoomConnection(options: UseRoomConnectionOptions): UseRoomConnectionResult { + const roomFromContext = useMaybeRoomContext(); + const room = useMemo(() => { + return roomFromContext ?? options.room ?? new Room(); + }, [options.room, roomFromContext]); + + const connected = options.connected ?? true; + + const [status, setStatus] = useState<'idle' | 'connecting' | 'disconnecting'>('idle'); + + // NOTE: it would on the surface seem that managing a room's connection with a useEffect would be + // straightforward, but `room.disconnect()` is async and useEffect doesn't support async cleanup + // functions, which means `room.connect()` can run in the midst of `room.disconnect()`, causing + // race conditions. + const connectDisconnectLock = useMemo(() => new Mutex(), []); + useEffect(() => { + connectDisconnectLock.lock().then(async (unlock) => { + if (connected) { + setStatus('connecting'); + const connectionDetails = await options.getConnectionDetails(); + try { + await Promise.all([ + room.localParticipant.setMicrophoneEnabled( + true, + undefined, + options.trackPublishOptions, + ), + room.connect(connectionDetails.serverUrl, connectionDetails.participantToken), + ]); + } catch (error) { + options.onConnectionError?.(error as Error); + } finally { + setStatus('idle'); + } + } else { + setStatus('disconnecting'); + try { + await room.disconnect(); + } catch (error) { + options.onConnectionError?.(error as Error); + } finally { + setStatus('idle'); + } + } + unlock(); + }); + }, [connected]); + + return { room, status }; +}