fix : added settings and Participants button

This commit is contained in:
anujsb 2025-02-25 23:08:30 +05:30
parent 9f6c4d7249
commit e6f9cf9a43
6 changed files with 285 additions and 321 deletions

View File

@ -1,123 +1,111 @@
// // 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';
'use client';
import React, { useState, useEffect } from 'react';
import {
TrackToggle,
DisconnectButton
} from '@livekit/components-react';
import {
Room,
RoomEvent,
Track
} from 'livekit-client';
import '../../styles/CustomControlBar.css';
// // const CustomControlBar = ({ roomName, room }) => {
// // const [recording, setRecording] = React.useState(false);
// // const [showSettings, setShowSettings] = React.useState(false);
interface CustomControlBarProps {
room: Room;
roomName: string;
}
// // React.useEffect(() => {
// // if (room) {
// // room.on('recordedStatusChanged', () => {
// // setRecording(room.isRecording);
// // });
// // return () => {
// // room.off('recordedStatusChanged');
// // };
// // }
// // }, [room]);
export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
const [recording, setRecording] = useState(false);
const [participantCount, setParticipantCount] = useState(1);
// // const handleCopyLink = () => {
// // navigator.clipboard.writeText(window.location.href);
// // };
useEffect(() => {
if (room) {
// // return (
// // <div className="bottom-bar">
// // <div className="left-section">
// // <div className="room-name-box">
// // <span className="room-name">{roomName}</span>
// // <button className="copy-link-button" onClick={handleCopyLink}>
// // {/* <MaterialSymbol name="contentCopy" color="#909BAA" /> */}
// // </button>
// // </div>
// // </div>
// // <div className="center-section">
// // <TrackToggle trackKind="audio" className="mic-button" />
// // <TrackToggle trackKind="video" className="camera-button" />
// // {recording && (
// // <div className="record-sign">
// // {/* <MaterialSymbol name="radioButtonChecked" color="#FF6F6F" /> */}
// // </div>
// // )}
// // <TrackToggle trackKind="screen" className="screen-share-button" />
// // <DisconnectButton className="end-call-button" />
// // </div>
// // <div className="right-section">
// // <button className="settings-button" onClick={() => setShowSettings(true)}>
// // {/* <MaterialSymbol name="settings" color="#FFFFFF" /> */}
// // </button>
// // {showSettings && <SettingsMenu onClose={() => setShowSettings(false)} />}
// // </div>
// // </div>
// // );
// // };
const updateRecordingStatus = () => setRecording(room.isRecording);
// // export default CustomControlBar;
const updateParticipantCount = () => {
if (room && room.participants) {
setParticipantCount(room.participants.size + 1);
}
};
if (room.state === 'connected') {
updateParticipantCount();
}
room.on(RoomEvent.Connected, updateParticipantCount);
room.on(RoomEvent.ParticipantConnected, updateParticipantCount);
room.on(RoomEvent.ParticipantDisconnected, updateParticipantCount);
room.on(RoomEvent.RecordingStatusChanged, updateRecordingStatus);
return () => {
room.off(RoomEvent.Connected, updateParticipantCount);
room.off(RoomEvent.ParticipantConnected, updateParticipantCount);
room.off(RoomEvent.ParticipantDisconnected, updateParticipantCount);
room.off(RoomEvent.RecordingStatusChanged, updateRecordingStatus);
};
}
}, [room]);
// import { TrackToggle, DisconnectButton, RoomAudioRenderer, GridLayout } from '@livekit/components-react';
// import { useState, useEffect } from 'react';
const handleCopyLink = () => {
navigator.clipboard.writeText(window.location.href)
.then(() => alert('Link copied to clipboard!'))
.catch((err) => console.error('Failed to copy link:', err));
};
// function CustomControlBar({ room, roomName }) {
// const [recording, setRecording] = useState(false);
return (
<div className="custom-control-bar">
// // 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]);
<div className="room-name-box">
<span className="room-name">{roomName}</span>
<button className="copy-link-button" onClick={handleCopyLink}>
<span className="material-symbols-outlined">content_copy</span>
</button>
</div>
// // 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));
// };
{/* Center: Control Buttons */}
<div className="control-buttons">
<TrackToggle source={Track.Source.Microphone} className="control-button mic-button">
{/* <span className="material-symbols-outlined">mic</span>
<span data-lk-audio-enabled="false" className="material-symbols-outlined">mic_off</span> */}
</TrackToggle>
<TrackToggle source={Track.Source.Camera} className="control-button camera-button">
{/* <span className="material-symbols-outlined">videocam</span>
<span data-lk-video-enabled="false" className="material-symbols-outlined">videocam_off</span> */}
</TrackToggle>
<div className={`control-button record-sign ${recording ? '' : 'disabled'}`}>
<span className="material-symbols-outlined">radio_button_checked</span>
</div>
<TrackToggle source={Track.Source.ScreenShare} className="control-button screen-share-button">
{/* <span className="material-symbols-outlined">screen_share</span>
<span data-lk-screen-share-enabled="true" className="material-symbols-outlined">screen_share</span> */}
</TrackToggle>
<DisconnectButton className="control-button end-call-button">
<span className="material-symbols-outlined">call_end</span>
</DisconnectButton>
</div>
// return (
// <div className="custom-control-bar">
// {/* Left: Room Name Box */}
// <div className="room-name-box">
// <span className="room-name">{roomName}</span>
// <button className="copy-link-button" onClick={handleCopyLink}>
// <span className="material-symbols-outlined">content_copy</span>
// </button>
// </div>
// {/* Center: Control Buttons */}
// <div className="control-buttons">
// <TrackToggle source="audio" className="control-button mic-button" />
// <TrackToggle source="video" className="control-button camera-button" />
// {recording && (
// <div className="record-sign">
// <span className="material-symbols-outlined">radio_button_checked</span>
// </div>
// )}
// <TrackToggle source="screen" className="control-button screen-share-button" />
// <DisconnectButton className="control-button end-call-button">
// <span className="material-symbols-outlined">call_end</span>
// </DisconnectButton>
// </div>
// {/* Right: Settings Button */}
// <div className="settings-section">
// {SHOW_SETTINGS_MENU && (
// <button className="settings-button">
// <span className="material-symbols-outlined">settings</span>
// <SettingsMenu />
// </button>
// )}
// </div>
// </div>
// );
// }
{/* Participants, Settings btn */}
<div className="top-right-controls">
<div className="participant-box">
<span className="material-symbols-outlined">people</span>
<span className="participant-count">{participantCount}</span>
</div>
<div className="settings-box">
<span className="material-symbols-outlined">settings</span>
</div>
</div>
</div>
);
}

27
app/custom/VideoTrack.tsx Normal file
View File

@ -0,0 +1,27 @@
'use client';
import React, { useRef, useEffect } from 'react';
import { TrackReferenceOrPlaceholder } from '@livekit/components-react';
import '../../styles/VideoTrack.css';
interface VideoTrackProps {
ref: TrackReferenceOrPlaceholder;
}
export function VideoTrack({ ref: trackRef }: VideoTrackProps) {
const videoRef = useRef<HTMLVideoElement>(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 <video ref={videoRef} className="video-element" />;
}

View File

@ -1,25 +1,23 @@
'use client';
import React, { useEffect, useState, useMemo } from 'react';
import { decodePassphrase } from '@/lib/client-utils';
import Transcript from '@/lib/Transcript';
import { SettingsMenu } from '@/lib/SettingsMenu';
import { ConnectionDetails } from '@/lib/types';
import {
LocalUserChoices,
PreJoin,
LiveKitRoom,
TrackToggle,
DisconnectButton,
RoomAudioRenderer,
GridLayout,
useTracks,
TrackReferenceOrPlaceholder,
GridLayout,
RoomAudioRenderer,
} from '@livekit/components-react';
import {
ExternalE2EEKeyProvider,
RoomOptions,
RoomEvent,
VideoCodec,
VideoPresets,
Room,
@ -28,11 +26,12 @@ import {
Track,
} from 'livekit-client';
import { useRouter } from 'next/navigation';
import React, { useEffect, useRef, useState } from 'react';
import '../../../styles/CustomControlBar.css';
import { VideoTrack } from '@/app/custom/VideoTrack';
import { CustomControlBar } from '@/app/custom/CustomControlBar';
import '../../../styles/PageClientImpl.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';
export function PageClientImpl(props: {
roomName: string;
@ -40,17 +39,18 @@ export function PageClientImpl(props: {
hq: boolean;
codec: VideoCodec;
}) {
const [preJoinChoices, setPreJoinChoices] = React.useState<LocalUserChoices | undefined>(undefined);
const preJoinDefaults = React.useMemo(() => {
const [preJoinChoices, setPreJoinChoices] = useState<LocalUserChoices | undefined>(undefined);
const [connectionDetails, setConnectionDetails] = useState<ConnectionDetails | undefined>(undefined);
const preJoinDefaults = useMemo(() => {
return {
username: '',
videoEnabled: true,
audioEnabled: true,
};
}, []);
const [connectionDetails, setConnectionDetails] = React.useState<ConnectionDetails | undefined>(undefined);
const handlePreJoinSubmit = React.useCallback(async (values: LocalUserChoices) => {
const handlePreJoinSubmit = async (values: LocalUserChoices) => {
setPreJoinChoices(values);
const url = new URL(CONN_DETAILS_ENDPOINT, window.location.origin);
url.searchParams.append('roomName', props.roomName);
@ -61,14 +61,14 @@ 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), []);
const handlePreJoinError = (e: any) => console.error(e);
return (
<main data-lk-theme="default" style={{ height: '100%' }}>
<main data-lk-theme="default" className="main-container">
{connectionDetails === undefined || preJoinChoices === undefined ? (
<div style={{ display: 'grid', placeItems: 'center', height: '100%' }}>
<div className="pre-join-container">
<PreJoin
defaults={preJoinDefaults}
onSubmit={handlePreJoinSubmit}
@ -91,18 +91,35 @@ function VideoConferenceComponent(props: {
connectionDetails: ConnectionDetails;
options: { hq: boolean; codec: VideoCodec };
}) {
const e2eePassphrase =
typeof window !== 'undefined' && decodePassphrase(location.hash.substring(1));
const worker =
typeof window !== 'undefined' &&
e2eePassphrase &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const e2eeEnabled = !!(e2eePassphrase && worker);
const keyProvider = new ExternalE2EEKeyProvider();
const roomOptions = React.useMemo((): RoomOptions => {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const getE2EEConfig = () => {
if (typeof window === 'undefined') return { enabled: false };
const e2eePassphrase = decodePassphrase(location.hash.substring(1));
const worker = e2eePassphrase &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const e2eeEnabled = !!(e2eePassphrase && worker);
return {
enabled: e2eeEnabled,
passphrase: e2eePassphrase,
worker
};
};
const e2eeConfig = useMemo(() => getE2EEConfig(), []);
const keyProvider = useMemo(() => new ExternalE2EEKeyProvider(), []);
const roomOptions = useMemo((): RoomOptions => {
let videoCodec: VideoCodec | undefined = props.options.codec ? props.options.codec : 'vp9';
if (e2eeEnabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
if (e2eeConfig.enabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
videoCodec = undefined;
}
return {
@ -115,7 +132,7 @@ function VideoConferenceComponent(props: {
videoSimulcastLayers: props.options.hq
? [VideoPresets.h1080, VideoPresets.h720]
: [VideoPresets.h540, VideoPresets.h216],
red: !e2eeEnabled,
red: !e2eeConfig.enabled,
videoCodec,
},
audioCaptureDefaults: {
@ -123,37 +140,37 @@ function VideoConferenceComponent(props: {
},
adaptiveStream: { pixelDensity: 'screen' },
dynacast: true,
e2ee: e2eeEnabled
? { keyProvider, worker }
: undefined,
e2ee: e2eeConfig.enabled ? { keyProvider, worker: e2eeConfig.worker } : undefined,
};
}, [props.userChoices, props.options.hq, props.options.codec]);
}, [props.userChoices, props.options.hq, props.options.codec, e2eeConfig]);
const room = React.useMemo(() => new Room(roomOptions), [roomOptions]);
const room = useMemo(() => new Room(roomOptions), [roomOptions]);
if (e2eeEnabled) {
keyProvider.setKey(decodePassphrase(e2eePassphrase));
room.setE2EEEnabled(true).catch((e) => {
if (e instanceof DeviceUnsupportedError) {
alert(
`You're trying to join an encrypted meeting, but your browser does not support it. Please update it to the latest version and try again.`,
);
console.error(e);
}
});
}
useEffect(() => {
if (e2eeConfig.enabled && e2eeConfig.passphrase) {
keyProvider.setKey(decodePassphrase(e2eeConfig.passphrase));
room.setE2EEEnabled(true).catch((e) => {
if (e instanceof DeviceUnsupportedError) {
alert(
`You're trying to join an encrypted meeting, but your browser does not support it. Please update it to the latest version and try again.`,
);
console.error(e);
}
});
}
}, [room, e2eeConfig, keyProvider]);
const connectOptions = React.useMemo((): RoomConnectOptions => {
const connectOptions = useMemo((): RoomConnectOptions => {
return { autoSubscribe: true };
}, []);
const router = useRouter();
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
const handleOnLeave = () => router.push('/');
const tracks = useTracks(
[{ source: Track.Source.Camera, withPlaceholder: true }],
{ room }
);
const tracks = useTracks([{ source: Track.Source.Camera, withPlaceholder: true }], { room });
// Return null during SSR to prevent hydration errors
if (!isClient) return null;
return (
<LiveKitRoom
@ -166,18 +183,21 @@ function VideoConferenceComponent(props: {
onDisconnected={handleOnLeave}
>
{tracks.length > 0 ? (
<GridLayout tracks={tracks} style={{ height: 'calc(100vh - 60px)' }}>
<GridLayout tracks={tracks} className="video-grid">
{(trackRef: TrackReferenceOrPlaceholder) => (
<div
key={trackRef.publication?.trackSid || `${trackRef.participant.identity}-${trackRef.source}`}
style={{ position: 'relative', width: '100%', height: '100%' }}
key={
trackRef.publication?.trackSid ||
`${trackRef.participant.identity}-${trackRef.source}`
}
className="video-container"
>
<VideoTrack ref={trackRef} />
</div>
)}
</GridLayout>
) : (
<div style={{ height: 'calc(100vh - 60px)', display: 'grid', placeItems: 'center' }}>
<div className="empty-video-container">
<p>No participants with video yet</p>
</div>
)}
@ -186,99 +206,4 @@ function VideoConferenceComponent(props: {
<Transcript latestText={''} />
</LiveKitRoom>
);
}
function VideoTrack({ ref: trackRef }: { ref: TrackReferenceOrPlaceholder }) {
const videoRef = useRef<HTMLVideoElement>(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 (
<video
ref={videoRef}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
);
}
interface CustomControlBarProps {
room: Room;
roomName: string;
}
interface CustomControlBarProps {
room: Room;
roomName: string;
}
function CustomControlBar({ room, roomName }: CustomControlBarProps) {
const [recording, setRecording] = useState(false);
useEffect(() => {
if (room) {
const updateRecordingStatus = () => setRecording(room.isRecording);
room.on(RoomEvent.LocalTrackPublished, updateRecordingStatus);
room.on(RoomEvent.LocalTrackUnpublished, updateRecordingStatus);
return () => {
room.off(RoomEvent.LocalTrackPublished, updateRecordingStatus);
room.off(RoomEvent.LocalTrackUnpublished, updateRecordingStatus);
};
}
}, [room]);
const handleCopyLink = () => {
navigator.clipboard.writeText(window.location.href)
.then(() => alert('Link copied to clipboard!'))
.catch((err) => console.error('Failed to copy link:', err));
};
return (
<div className="custom-control-bar">
{/* Left: Room Name Box */}
<div className="room-name-box">
<span className="room-name">{roomName}</span>
<button className="copy-link-button" onClick={handleCopyLink}>
<span className="material-symbols-outlined">content_copy</span>
</button>
</div>
{/* Center: Control Buttons */}
<div className="control-buttons">
<TrackToggle source={Track.Source.Microphone} className="control-button mic-button" />
<TrackToggle source={Track.Source.Camera} className="control-button camera-button" />
{recording ? (
<div className="control-button record-sign">
<span className="material-symbols-outlined">radio_button_checked</span>
</div>
) : (
<div className="control-button record-sign disabled">
<span className="material-symbols-outlined">radio_button_checked</span>
</div>
)}
<TrackToggle source={Track.Source.ScreenShare} className="control-button screen-share-button" />
<DisconnectButton className="control-button end-call-button">
<span className="material-symbols-outlined">call_end</span>
</DisconnectButton>
</div>
{/* Right: Settings Button */}
<div className="settings-section">
{SHOW_SETTINGS_MENU && (
<button className="settings-button">
<span className="material-symbols-outlined">settings</span>
</button>
)}
</div>
</div>
);
}
}

View File

@ -12,7 +12,8 @@
right: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px); /* Added for Safari compatibility */
}
.room-name-box {
@ -60,69 +61,52 @@
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
/* Mic button */
.mic-button[data-lk-audio-enabled="false"] {
background: #ff5252;
opacity: 0.8;
background: rgba(255, 82, 82, 0.2);
}
.mic-button[data-lk-audio-enabled="true"] .material-symbols-outlined {
content: 'mic';
color: #ffffff;
}
.mic-button[data-lk-audio-enabled="false"] .material-symbols-outlined {
content: 'mic_off';
color: #ffffff;
color: #ff6f6f;
}
/* Camera btn */
.camera-button[data-lk-video-enabled="false"] {
background: #ff5252;
opacity: 0.8;
background: rgba(255, 82, 82, 0.2);
}
.camera-button[data-lk-video-enabled="true"] .material-symbols-outlined {
content: 'videocam';
color: #ffffff;
color: #ffffff;
}
.camera-button[data-lk-video-enabled="false"] .material-symbols-outlined {
content: 'videocam_off';
color: #ffffff;
color: #ff6f6f;
}
/* Screen share btn */
.screen-share-button[data-lk-screen-share-enabled="true"] .material-symbols-outlined {
content: 'screen_share';
color: #49c998;
color: #49c998;
}
.screen-share-button[data-lk-screen-share-enabled="false"] .material-symbols-outlined {
content: 'screen_share';
color: #ffffff;
}
.record-sign {
background: rgba(144, 155, 170, 0.1);
}
.record-sign.disabled {
background: rgba(144, 155, 170, 0.1);
}
.record-sign .material-symbols-outlined,
.record-sign.disabled .material-symbols-outlined {
font-size: 24px;
color: #ffffff;
}
/* Record */
.record-sign .material-symbols-outlined {
color: #ff6f6f;
animation: pulse 1s infinite;
color: #ff6f6f;
animation: pulse 1.5s infinite;
}
.record-sign.disabled .material-symbols-outlined {
color: #ffffff;
animation: none;
}
@keyframes pulse {
@ -131,56 +115,66 @@
100% { opacity: 1; }
}
/* End call */
.end-call-button {
width: 64px;
height: 40px;
background: #ff5252;
background: #ff5252;
border-radius: 8px;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.end-call-button .material-symbols-outlined {
font-size: 24px;
color: #ffffff;
color: #ffffff;
}
.settings-section {
position: relative;
.top-right-controls {
display: flex;
gap: 10px;
align-items: center;
}
.settings-button {
.participant-box {
background-color: rgba(144, 155, 170, 0.1);
border-radius: 8px;
display: flex;
align-items: center;
padding: 5px 10px;
gap: 8px;
}
.participant-box .material-symbols-outlined {
font-size: 20px;
color: white;
}
.participant-count {
font-size: 16px;
font-weight: 500;
color: white;
font-family: 'Roboto', sans-serif;
}
.settings-box {
background-color: rgba(144, 155, 170, 0.1);
border-radius: 8px;
width: 40px;
height: 40px;
background: rgba(144, 155, 170, 0.1);
border-radius: 8px;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.settings-button .material-symbols-outlined {
font-size: 24px;
color: #ffffff;
.settings-box .material-symbols-outlined {
font-size: 20px;
color: white;
}
/* Fix for video layout */
.lk-grid-layout {
height: calc(100vh - 60px) !important;
width: 100% !important;
}
.lk-grid-layout > div {
position: relative !important;
overflow: hidden !important;
}
.lk-video {
object-fit: cover !important;
background-color: #000 !important;
height: calc(100vh - 60px) !important;
}

25
styles/PageClientImpl.css Normal file
View File

@ -0,0 +1,25 @@
.main-container {
height: 100%;
}
.pre-join-container {
display: grid;
place-items: center;
height: 100%;
}
.video-grid {
height: calc(100vh - 60px) !important;
}
.video-container {
position: relative;
width: 100%;
height: 100%;
}
.empty-video-container {
height: calc(100vh - 60px);
display: grid;
place-items: center;
}

5
styles/VideoTrack.css Normal file
View File

@ -0,0 +1,5 @@
.video-element {
width: 100%;
height: 100%;
object-fit: cover;
}