fix : added settings and Participants button
This commit is contained in:
parent
9f6c4d7249
commit
e6f9cf9a43
@ -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
27
app/custom/VideoTrack.tsx
Normal 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" />;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
25
styles/PageClientImpl.css
Normal 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
5
styles/VideoTrack.css
Normal file
@ -0,0 +1,5 @@
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user