From 9f6c4d724987f2f5a1d1ce5ab1af4bfc6afe626d Mon Sep 17 00:00:00 2001 From: anujsb <141896390+anujsb@users.noreply.github.com> Date: Sun, 23 Feb 2025 17:32:31 +0530 Subject: [PATCH 01/19] Fix : LiveKit call redesign - Bottom bar --- .env.example | 30 ---- app/custom/CustomControlBar.tsx | 123 +++++++++++++ app/rooms/[roomName]/PageClientImpl.tsx | 228 +++++++++++++++--------- package.json | 1 + pnpm-lock.yaml | 105 +++++++---- styles/CustomControlBar.css | 186 +++++++++++++++++++ 6 files changed, 524 insertions(+), 149 deletions(-) delete mode 100644 .env.example create mode 100644 app/custom/CustomControlBar.tsx create mode 100644 styles/CustomControlBar.css diff --git a/.env.example b/.env.example deleted file mode 100644 index f961fa04..00000000 --- a/.env.example +++ /dev/null @@ -1,30 +0,0 @@ -# 1. Copy this file and rename it to .env.local -# 2. Update the enviroment variables below. - -# REQUIRED SETTINGS -# ################# -# If you are using LiveKit Cloud, the API key and secret can be generated from the Cloud Dashboard. -LIVEKIT_API_KEY= -LIVEKIT_API_SECRET= -# URL pointing to the LiveKit server. (example: `wss://my-livekit-project.livekit.cloud`) -LIVEKIT_URL= - - -# OPTIONAL SETTINGS -# ################# -# Recording -# S3_KEY_ID= -# S3_KEY_SECRET= -# S3_ENDPOINT= -# S3_BUCKET= -# S3_REGION= - -# PUBLIC -# Uncomment settings menu when using a LiveKit Cloud, it'll enable Krisp noise filters. -# NEXT_PUBLIC_SHOW_SETTINGS_MENU=true -# NEXT_PUBLIC_LK_RECORD_ENDPOINT=/api/record - -# Optional, to pipe logs to datadog -# NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=client-token -# NEXT_PUBLIC_DATADOG_SITE=datadog-site - diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx new file mode 100644 index 00000000..4269dba7 --- /dev/null +++ b/app/custom/CustomControlBar.tsx @@ -0,0 +1,123 @@ +// // import React from 'react'; +// // import { TrackToggle, DisconnectButton } from '@livekit/components-react'; +// // import SettingsMenu from '@/lib/SettingsMenu'; +// // import { MaterialSymbol } from 'material-symbols'; +// // import '../../styles/CustomControlBar.css'; + + +// // const CustomControlBar = ({ roomName, room }) => { +// // const [recording, setRecording] = React.useState(false); +// // const [showSettings, setShowSettings] = React.useState(false); + +// // React.useEffect(() => { +// // if (room) { +// // room.on('recordedStatusChanged', () => { +// // setRecording(room.isRecording); +// // }); +// // return () => { +// // room.off('recordedStatusChanged'); +// // }; +// // } +// // }, [room]); + +// // const handleCopyLink = () => { +// // navigator.clipboard.writeText(window.location.href); +// // }; + +// // return ( +// //
+// //
+// //
+// // {roomName} +// // +// //
+// //
+// //
+// // +// // +// // {recording && ( +// //
+// // {/* */} +// //
+// // )} +// // +// // +// //
+// //
+// // +// // {showSettings && setShowSettings(false)} />} +// //
+// //
+// // ); +// // }; + +// // export default CustomControlBar; + + +// import { TrackToggle, DisconnectButton, RoomAudioRenderer, GridLayout } from '@livekit/components-react'; +// import { useState, useEffect } from 'react'; + +// function CustomControlBar({ room, roomName }) { +// const [recording, setRecording] = useState(false); + +// // Update recording status +// useEffect(() => { +// if (room) { +// const updateRecordingStatus = () => setRecording(room.isRecording); +// room.on(RoomEvent.RecordingStarted, updateRecordingStatus); +// room.on(RoomEvent.RecordingStopped, updateRecordingStatus); +// return () => { +// room.off(RoomEvent.RecordingStarted, updateRecordingStatus); +// room.off(RoomEvent.RecordingStopped, updateRecordingStatus); +// }; +// } +// }, [room]); + +// // Copy room link to clipboard +// const handleCopyLink = () => { +// navigator.clipboard.writeText(window.location.href) +// .then(() => alert('Link copied to clipboard!')) +// .catch((err) => console.error('Failed to copy link:', err)); +// }; + +// return ( +//
+// {/* Left: Room Name Box */} +//
+// {roomName} +// +//
+ +// {/* Center: Control Buttons */} +//
+// +// +// {recording && ( +//
+// radio_button_checked +//
+// )} +// +// +// call_end +// +//
+ +// {/* Right: Settings Button */} +//
+// {SHOW_SETTINGS_MENU && ( +// +// )} +//
+//
+// ); +// } \ No newline at end of file diff --git a/app/rooms/[roomName]/PageClientImpl.tsx b/app/rooms/[roomName]/PageClientImpl.tsx index daf17f96..063d7a00 100644 --- a/app/rooms/[roomName]/PageClientImpl.tsx +++ b/app/rooms/[roomName]/PageClientImpl.tsx @@ -1,36 +1,38 @@ + 'use client'; import { decodePassphrase } from '@/lib/client-utils'; import Transcript from '@/lib/Transcript'; -import { RecordingIndicator } from '@/lib/RecordingIndicator'; import { SettingsMenu } from '@/lib/SettingsMenu'; import { ConnectionDetails } from '@/lib/types'; import { - formatChatMessageLinks, - LiveKitRoom, LocalUserChoices, PreJoin, - VideoConference, + LiveKitRoom, + TrackToggle, + DisconnectButton, + RoomAudioRenderer, + GridLayout, + useTracks, + TrackReferenceOrPlaceholder, } from '@livekit/components-react'; import { ExternalE2EEKeyProvider, RoomOptions, RoomEvent, - TranscriptionSegment, VideoCodec, VideoPresets, Room, DeviceUnsupportedError, RoomConnectOptions, + Track, } from 'livekit-client'; -import { setLazyProp } from 'next/dist/server/api-utils'; import { useRouter } from 'next/navigation'; -import React from 'react'; - +import React, { useEffect, useRef, useState } from 'react'; +import '../../../styles/CustomControlBar.css'; const CONN_DETAILS_ENDPOINT = process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT ?? '/api/connection-details'; const SHOW_SETTINGS_MENU = process.env.NEXT_PUBLIC_SHOW_SETTINGS_MENU == 'true'; -console.log('SHOW_SETTINGS_MENU', SHOW_SETTINGS_MENU); export function PageClientImpl(props: { roomName: string; @@ -38,9 +40,7 @@ export function PageClientImpl(props: { hq: boolean; codec: VideoCodec; }) { - const [preJoinChoices, setPreJoinChoices] = React.useState( - undefined, - ); + const [preJoinChoices, setPreJoinChoices] = React.useState(undefined); const preJoinDefaults = React.useMemo(() => { return { username: '', @@ -48,9 +48,7 @@ export function PageClientImpl(props: { audioEnabled: true, }; }, []); - const [connectionDetails, setConnectionDetails] = React.useState( - undefined, - ); + const [connectionDetails, setConnectionDetails] = React.useState(undefined); const handlePreJoinSubmit = React.useCallback(async (values: LocalUserChoices) => { setPreJoinChoices(values); @@ -63,7 +61,8 @@ export function PageClientImpl(props: { const connectionDetailsResp = await fetch(url.toString()); const connectionDetailsData = await connectionDetailsResp.json(); setConnectionDetails(connectionDetailsData); - }, []); + }, [props.roomName, props.region]); + const handlePreJoinError = React.useCallback((e: any) => console.error(e), []); return ( @@ -90,14 +89,10 @@ export function PageClientImpl(props: { function VideoConferenceComponent(props: { userChoices: LocalUserChoices; connectionDetails: ConnectionDetails; - options: { - hq: boolean; - codec: VideoCodec; - }; + options: { hq: boolean; codec: VideoCodec }; }) { const e2eePassphrase = typeof window !== 'undefined' && decodePassphrase(location.hash.substring(1)); - const worker = typeof window !== 'undefined' && e2eePassphrase && @@ -129,17 +124,12 @@ function VideoConferenceComponent(props: { adaptiveStream: { pixelDensity: 'screen' }, dynacast: true, e2ee: e2eeEnabled - ? { - keyProvider, - worker, - } + ? { keyProvider, worker } : undefined, }; - // @ts-ignore - setLogLevel('debug', 'lk-e2ee'); }, [props.userChoices, props.options.hq, props.options.codec]); - const room = React.useMemo(() => new Room(roomOptions), []); + const room = React.useMemo(() => new Room(roomOptions), [roomOptions]); if (e2eeEnabled) { keyProvider.setKey(decodePassphrase(e2eePassphrase)); @@ -152,65 +142,143 @@ function VideoConferenceComponent(props: { } }); } + const connectOptions = React.useMemo((): RoomConnectOptions => { - return { - autoSubscribe: true, - }; + return { autoSubscribe: true }; }, []); - const [transcriptions, setTranscriptions] = React.useState<{ - [id: string]: TranscriptionSegment; - }>({}); - const [latestText, setLatestText] = React.useState(''); - React.useEffect(() => { - if (!room) { - return; + const router = useRouter(); + const handleOnLeave = React.useCallback(() => router.push('/'), [router]); + + const tracks = useTracks( + [{ source: Track.Source.Camera, withPlaceholder: true }], + { room } + ); + + return ( + + {tracks.length > 0 ? ( + + {(trackRef: TrackReferenceOrPlaceholder) => ( +
+ +
+ )} +
+ ) : ( +
+

No participants with video yet

+
+ )} + + + +
+ ); +} + +function VideoTrack({ ref: trackRef }: { ref: TrackReferenceOrPlaceholder }) { + const videoRef = useRef(null); + + useEffect(() => { + const videoEl = videoRef.current; + const track = trackRef.publication?.track; + + if (videoEl && track) { + track.attach(videoEl); + return () => { + track.detach(videoEl); + }; + } + }, [trackRef.publication?.track]); + + return ( +