From abb708aa490fd622ff890c5bb07fc0103273a8f8 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 6 Aug 2024 13:40:21 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A9(frontend)=20duplicate=20VideoConfe?= =?UTF-8?q?rence=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basically, duplicate LiveKit code to start iterating on their components, not sure wether it's the optimal strategy, but at least we will be more agile, shipping small features which are lacking. --- .../features/rooms/components/Conference.tsx | 3 +- .../rooms/livekit/prefabs/VideoConference.tsx | 206 ++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 8a00ceb0..524bc123 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next' import { formatChatMessageLinks, LiveKitRoom, - VideoConference, type LocalUserChoices, } from '@livekit/components-react' import { Room, RoomOptions } from 'livekit-client' @@ -19,6 +18,8 @@ import { ApiRoom } from '../api/ApiRoom' import { useCreateRoom } from '../api/createRoom' import { InviteDialog } from './InviteDialog' +import { VideoConference } from '../livekit/prefabs/VideoConference' + export const Conference = ({ roomId, userConfig, diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx new file mode 100644 index 00000000..a113b506 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -0,0 +1,206 @@ +import type { + MessageDecoder, + MessageEncoder, + TrackReferenceOrPlaceholder, + WidgetState, +} from '@livekit/components-core' +import { + isEqualTrackRef, + isTrackReference, + isWeb, + log, +} from '@livekit/components-core' +import { RoomEvent, Track } from 'livekit-client' +import * as React from 'react' + +import { + CarouselLayout, + ConnectionStateToast, + FocusLayout, + FocusLayoutContainer, + GridLayout, + LayoutContextProvider, + ParticipantTile, + RoomAudioRenderer, + MessageFormatter, + usePinnedTracks, + useTracks, + useCreateLayoutContext, + ControlBar, + Chat, +} from '@livekit/components-react' + +/** + * @public + */ +export interface VideoConferenceProps + extends React.HTMLAttributes { + chatMessageFormatter?: MessageFormatter + chatMessageEncoder?: MessageEncoder + chatMessageDecoder?: MessageDecoder + /** @alpha */ + SettingsComponent?: React.ComponentType +} + +/** + * The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application. + * It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers + * of participants, basic non-persistent chat, screen sharing, and more. + * + * @remarks + * The component is implemented with other LiveKit components like `FocusContextProvider`, + * `GridLayout`, `ControlBar`, `FocusLayoutContainer` and `FocusLayout`. + * You can use this components as a starting point for your own custom video conferencing application. + * + * @example + * ```tsx + * + * + * + * ``` + * @public + */ +export function VideoConference({ + chatMessageFormatter, + chatMessageDecoder, + chatMessageEncoder, + SettingsComponent, + ...props +}: VideoConferenceProps) { + const [widgetState, setWidgetState] = React.useState({ + showChat: false, + unreadMessages: 0, + showSettings: false, + }) + const lastAutoFocusedScreenShareTrack = + React.useRef(null) + + const tracks = useTracks( + [ + { source: Track.Source.Camera, withPlaceholder: true }, + { source: Track.Source.ScreenShare, withPlaceholder: false }, + ], + { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false } + ) + + const widgetUpdate = (state: WidgetState) => { + log.debug('updating widget state', state) + setWidgetState(state) + } + + const layoutContext = useCreateLayoutContext() + + const screenShareTracks = tracks + .filter(isTrackReference) + .filter((track) => track.publication.source === Track.Source.ScreenShare) + + const focusTrack = usePinnedTracks(layoutContext)?.[0] + const carouselTracks = tracks.filter( + (track) => !isEqualTrackRef(track, focusTrack) + ) + + /* eslint-disable react-hooks/exhaustive-deps */ + // Code duplicated from LiveKit; this warning will be addressed in the refactoring. + React.useEffect(() => { + // If screen share tracks are published, and no pin is set explicitly, auto set the screen share. + if ( + screenShareTracks.some((track) => track.publication.isSubscribed) && + lastAutoFocusedScreenShareTrack.current === null + ) { + log.debug('Auto set screen share focus:', { + newScreenShareTrack: screenShareTracks[0], + }) + layoutContext.pin.dispatch?.({ + msg: 'set_pin', + trackReference: screenShareTracks[0], + }) + lastAutoFocusedScreenShareTrack.current = screenShareTracks[0] + } else if ( + lastAutoFocusedScreenShareTrack.current && + !screenShareTracks.some( + (track) => + track.publication.trackSid === + lastAutoFocusedScreenShareTrack.current?.publication?.trackSid + ) + ) { + log.debug('Auto clearing screen share focus.') + layoutContext.pin.dispatch?.({ msg: 'clear_pin' }) + lastAutoFocusedScreenShareTrack.current = null + } + if (focusTrack && !isTrackReference(focusTrack)) { + const updatedFocusTrack = tracks.find( + (tr) => + tr.participant.identity === focusTrack.participant.identity && + tr.source === focusTrack.source + ) + if ( + updatedFocusTrack !== focusTrack && + isTrackReference(updatedFocusTrack) + ) { + layoutContext.pin.dispatch?.({ + msg: 'set_pin', + trackReference: updatedFocusTrack, + }) + } + } + }, [ + screenShareTracks + .map( + (ref) => `${ref.publication.trackSid}_${ref.publication.isSubscribed}` + ) + .join(), + focusTrack?.publication?.trackSid, + tracks, + ]) + /* eslint-enable react-hooks/exhaustive-deps */ + + return ( +
+ {isWeb() && ( + +
+ {!focusTrack ? ( +
+ + + +
+ ) : ( +
+ + + + + {focusTrack && } + +
+ )} + +
+ + {SettingsComponent && ( +
+ +
+ )} +
+ )} + + +
+ ) +}