Skip to content

Improve PreJoin Component and add custom participant placeholders #1129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/odd-files-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@livekit/components-react': minor
'@livekit/components-styles': minor
---

Improves the PreJoin component and allows for custom placeholders for each participant
4 changes: 4 additions & 0 deletions examples/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const EXAMPLE_ROUTES = {
title: 'Example usage of @livekit/track-processors for background blur',
href: () => `/processors`,
},
prejoin: {
title: 'Example usage of @livekit/prejoin',
href: () => `/prejoin`,
},
} as const;

const Home: NextPage = () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/react/etc/components-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,13 @@ export interface UseRoomInfoOptions {
room?: Room;
}

// @public
export function useSelectedDevice({
kind: 'videoinput' | 'audioinput';
track?: T;
deviceId?: string;
}): { device: any, deviceError: any };

// @public
export function useSortedParticipants(participants: Array<Participant>): Participant[];

Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@livekit/components-react",
"version": "2.8.1",
"version": "2.9.0",
"license": "Apache-2.0",
"author": "LiveKit",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface ParticipantTileProps extends React.HTMLAttributes<HTMLDivElemen
disableSpeakingIndicator?: boolean;

onParticipantClick?: (event: ParticipantClickEvent) => void;
placeholders?: { [index: string]: React.ReactNode };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big fan of passing the whole map to each participant tile.
A pattern where the component can take a single placeholder as a ReactNode seems preferable to me.
In general the codebase tries to avoid passing of components as props and rather works with slottable components (as children). We're already using those for ParticipantTile as custom tile renderings though, so I'm not totally against an exception here.

}

/**
Expand Down Expand Up @@ -100,6 +101,7 @@ export const ParticipantTile: (
children,
onParticipantClick,
disableSpeakingIndicator,
placeholders,
...htmlProps
}: ParticipantTileProps,
ref,
Expand Down Expand Up @@ -156,7 +158,9 @@ export const ParticipantTile: (
)
)}
<div className="lk-participant-placeholder">
<ParticipantPlaceholder />
{placeholders?.[trackReference.participant?.identity] ?? (
<ParticipantPlaceholder />
)}
</div>
<div className="lk-participant-metadata">
<div className="lk-participant-metadata-item">
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export { usePinnedTracks } from './usePinnedTracks';
export { type UseRemoteParticipantOptions, useRemoteParticipant } from './useRemoteParticipant';
export { type UseRemoteParticipantsOptions, useRemoteParticipants } from './useRemoteParticipants';
export { type UseRoomInfoOptions, useRoomInfo } from './useRoomInfo';
export { useSelectedDevice } from './useSelectedDevice';
export { useSortedParticipants } from './useSortedParticipants';
export { useSpeakingParticipants } from './useSpeakingParticipants';
export { type UseStartAudioProps, useStartAudio } from './useStartAudio';
Expand Down
72 changes: 72 additions & 0 deletions packages/react/src/hooks/useSelectedDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { type LocalAudioTrack, type LocalVideoTrack } from 'livekit-client';
import * as React from 'react';
import { useMediaDevices } from './useMediaDevices';

/**
* /**
* The `useSelectedDevice` hook returns the current selected device (audio or video) of the participant.
*
* @example
* ```tsx
* const { selectedDevice } = useSelectedDevice({
* kind: 'videoinput',
* track: track,
* });
*
* <div>
* {selectedDevice?.label}
* </div>
* ```
* @public
*/
export function useSelectedDevice<T extends LocalVideoTrack | LocalAudioTrack>({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

within the livekit-client SDK there's the concept of an active device which is currently used by the user.
That's not as useful for prejoin as of now, but it would mirror the implementation in the client SDK more closely.

kind,
track,
deviceId,
}: {
kind: 'videoinput' | 'audioinput';
track?: T;
deviceId?: string;
}) {
const [deviceError, setDeviceError] = React.useState<Error | null>(null);

const devices = useMediaDevices({ kind });
const [selectedDevice, setSelectedDevice] = React.useState<MediaDeviceInfo | undefined>(
undefined,
);
const [localDeviceId, setLocalDeviceId] = React.useState<string | undefined>(deviceId);

const prevDeviceId = React.useRef(localDeviceId);

const getDeviceId = async () => {
try {
const newDeviceId = await track?.getDeviceId(false);
if (newDeviceId && localDeviceId !== newDeviceId) {
prevDeviceId.current = newDeviceId;
setLocalDeviceId(newDeviceId);
}
} catch (e) {
if (e instanceof Error) {
setDeviceError(e);
}
}
};

React.useEffect(() => {
if (track) getDeviceId();
}, [track]);

React.useEffect(() => {
// in case track doesn't exist, utilize the deviceId passed in
if (!track) setLocalDeviceId(deviceId);
}, [deviceId]);

React.useEffect(() => {
setSelectedDevice(devices?.find((dev) => dev.deviceId === localDeviceId));
}, [localDeviceId, devices]);

return {
selectedDevice,
deviceError,
};
}
Loading