Fix : LiveKit call redesign - Bottom bar
This commit is contained in:
parent
148d7520b9
commit
9f6c4d7249
30
.env.example
30
.env.example
@ -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
|
||||
|
||||
123
app/custom/CustomControlBar.tsx
Normal file
123
app/custom/CustomControlBar.tsx
Normal file
@ -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 (
|
||||
// // <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>
|
||||
// // );
|
||||
// // };
|
||||
|
||||
// // 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 (
|
||||
// <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>
|
||||
// );
|
||||
// }
|
||||
@ -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<LocalUserChoices | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [preJoinChoices, setPreJoinChoices] = React.useState<LocalUserChoices | undefined>(undefined);
|
||||
const preJoinDefaults = React.useMemo(() => {
|
||||
return {
|
||||
username: '',
|
||||
@ -48,9 +48,7 @@ export function PageClientImpl(props: {
|
||||
audioEnabled: true,
|
||||
};
|
||||
}, []);
|
||||
const [connectionDetails, setConnectionDetails] = React.useState<ConnectionDetails | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [connectionDetails, setConnectionDetails] = React.useState<ConnectionDetails | undefined>(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,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [transcriptions, setTranscriptions] = React.useState<{
|
||||
[id: string]: TranscriptionSegment;
|
||||
}>({});
|
||||
const [latestText, setLatestText] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const updateTranscriptions = (
|
||||
segments: TranscriptionSegment[],
|
||||
participant: any,
|
||||
publication: any,
|
||||
) => {
|
||||
if (segments.length > 0) {
|
||||
setLatestText(segments[0].text);
|
||||
}
|
||||
// setTranscriptions((prev) => {
|
||||
// const newTranscriptions = { ...prev };
|
||||
// for (const segment of segments) {
|
||||
// newTranscriptions[segment.id] = segment;
|
||||
// }
|
||||
// console.log('===>', newTranscriptions);
|
||||
// return newTranscriptions;
|
||||
// });
|
||||
};
|
||||
room.on(RoomEvent.TranscriptionReceived, updateTranscriptions);
|
||||
return () => {
|
||||
room.off(RoomEvent.TranscriptionReceived, updateTranscriptions);
|
||||
};
|
||||
}, [room]);
|
||||
const connectOptions = React.useMemo((): RoomConnectOptions => {
|
||||
return { autoSubscribe: true };
|
||||
}, []);
|
||||
|
||||
const router = useRouter();
|
||||
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
|
||||
|
||||
const tracks = useTracks(
|
||||
[{ source: Track.Source.Camera, withPlaceholder: true }],
|
||||
{ room }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LiveKitRoom
|
||||
room={room}
|
||||
token={props.connectionDetails.participantToken}
|
||||
serverUrl={props.connectionDetails.serverUrl}
|
||||
connectOptions={connectOptions}
|
||||
video={props.userChoices.videoEnabled}
|
||||
audio={props.userChoices.audioEnabled}
|
||||
onDisconnected={handleOnLeave}
|
||||
>
|
||||
<VideoConference
|
||||
chatMessageFormatter={formatChatMessageLinks}
|
||||
SettingsComponent={SHOW_SETTINGS_MENU ? SettingsMenu : undefined}
|
||||
/>
|
||||
{/* <DebugMode /> */}
|
||||
<RecordingIndicator />
|
||||
<Transcript latestText={latestText} />
|
||||
</LiveKitRoom>
|
||||
</>
|
||||
<LiveKitRoom
|
||||
room={room}
|
||||
token={props.connectionDetails.participantToken}
|
||||
serverUrl={props.connectionDetails.serverUrl}
|
||||
connectOptions={connectOptions}
|
||||
video={props.userChoices.videoEnabled}
|
||||
audio={props.userChoices.audioEnabled}
|
||||
onDisconnected={handleOnLeave}
|
||||
>
|
||||
{tracks.length > 0 ? (
|
||||
<GridLayout tracks={tracks} style={{ height: 'calc(100vh - 60px)' }}>
|
||||
{(trackRef: TrackReferenceOrPlaceholder) => (
|
||||
<div
|
||||
key={trackRef.publication?.trackSid || `${trackRef.participant.identity}-${trackRef.source}`}
|
||||
style={{ position: 'relative', width: '100%', height: '100%' }}
|
||||
>
|
||||
<VideoTrack ref={trackRef} />
|
||||
</div>
|
||||
)}
|
||||
</GridLayout>
|
||||
) : (
|
||||
<div style={{ height: 'calc(100vh - 60px)', display: 'grid', placeItems: 'center' }}>
|
||||
<p>No participants with video yet</p>
|
||||
</div>
|
||||
)}
|
||||
<RoomAudioRenderer />
|
||||
<CustomControlBar room={room} roomName={props.connectionDetails.roomName} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"@livekit/krisp-noise-filter": "^0.2.8",
|
||||
"livekit-client": "2.8.1",
|
||||
"livekit-server-sdk": "2.9.7",
|
||||
"material-symbols": "^0.28.2",
|
||||
"next": "14.2.12",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
105
pnpm-lock.yaml
generated
105
pnpm-lock.yaml
generated
@ -13,19 +13,22 @@ importers:
|
||||
version: 5.26.0
|
||||
'@livekit/components-react':
|
||||
specifier: 2.6.0
|
||||
version: 2.6.0(@livekit/protocol@1.20.1)(livekit-client@2.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.7.0)
|
||||
version: 2.6.0(@livekit/protocol@1.34.0)(livekit-client@2.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1)
|
||||
'@livekit/components-styles':
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2
|
||||
'@livekit/krisp-noise-filter':
|
||||
specifier: ^0.2.8
|
||||
version: 0.2.8(livekit-client@2.5.2)
|
||||
version: 0.2.8(livekit-client@2.8.1)
|
||||
livekit-client:
|
||||
specifier: 2.5.2
|
||||
version: 2.5.2
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
livekit-server-sdk:
|
||||
specifier: 2.6.2
|
||||
version: 2.6.2
|
||||
specifier: 2.9.7
|
||||
version: 2.9.7
|
||||
material-symbols:
|
||||
specifier: ^0.28.2
|
||||
version: 0.28.2
|
||||
next:
|
||||
specifier: 14.2.12
|
||||
version: 14.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -43,8 +46,8 @@ importers:
|
||||
specifier: 20.16.3
|
||||
version: 20.16.3
|
||||
'@types/react':
|
||||
specifier: 18.3.5
|
||||
version: 18.3.5
|
||||
specifier: 18.3.8
|
||||
version: 18.3.8
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.0
|
||||
version: 18.3.0
|
||||
@ -172,8 +175,14 @@ packages:
|
||||
peerDependencies:
|
||||
livekit-client: ^2.0.8
|
||||
|
||||
'@livekit/protocol@1.20.1':
|
||||
resolution: {integrity: sha512-TgyuwOx+XJn9inEYT9OKfFNs9YIPS4BdLa4pF5FDf9MhWRnahKwPe7jxr/+sVdWxYbZmy9hRrH58jSAFu0ONHw==}
|
||||
'@livekit/mutex@1.1.1':
|
||||
resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==}
|
||||
|
||||
'@livekit/protocol@1.30.0':
|
||||
resolution: {integrity: sha512-SDI9ShVKj8N3oOSinr8inaxD3FXgmgoJlqN35uU/Yx1sdoDeQbzAuBFox7bYjM+VhnZ1V22ivIDjAsKr00H+XQ==}
|
||||
|
||||
'@livekit/protocol@1.34.0':
|
||||
resolution: {integrity: sha512-bU7pCLAMRVTVZb1KSxA46q55bhOc4iATrY/gccy2/oX1D57tiZEI+8wGRWHeDwBb0UwnABu6JXzC4tTFkdsaOg==}
|
||||
|
||||
'@next/env@14.2.12':
|
||||
resolution: {integrity: sha512-3fP29GIetdwVIfIRyLKM7KrvJaqepv+6pVodEbx0P5CaMLYBtx+7eEg8JYO5L9sveJO87z9eCReceZLi0hxO1Q==}
|
||||
@ -282,8 +291,8 @@ packages:
|
||||
'@types/react-dom@18.3.0':
|
||||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
|
||||
|
||||
'@types/react@18.3.5':
|
||||
resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==}
|
||||
'@types/react@18.3.8':
|
||||
resolution: {integrity: sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==}
|
||||
|
||||
'@typescript-eslint/parser@7.2.0':
|
||||
resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==}
|
||||
@ -1101,12 +1110,12 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
livekit-client@2.5.2:
|
||||
resolution: {integrity: sha512-rzWFH02UznHxpnbj+WEEoHxL1ZSo9BdFK+7ltSZWniTt2llnNckdqeXNsjkBH6k+C9agHTF4XikmxKcpWa4YrQ==}
|
||||
livekit-client@2.8.1:
|
||||
resolution: {integrity: sha512-HPv9iHNrnBANI9ucK7CKZspx0sBZK3hjR2EbwaV08+J3RM9+tNGL2ob2n76nxJLEZG7LzdWlLZdbr4fQBP6Hkg==}
|
||||
|
||||
livekit-server-sdk@2.6.2:
|
||||
resolution: {integrity: sha512-3fFzHu7sAynUaUFTCKtRP9lgQCU0Qe/x7XA99GpT1ro7fTy1ZVzaWq34WcXEyUGBBMFxG19LlSIAQBcGZVStWQ==}
|
||||
engines: {node: '>=19'}
|
||||
livekit-server-sdk@2.9.7:
|
||||
resolution: {integrity: sha512-uIkFOaqBCJnVgYOidZdanPWQH5G0LMxe0+Qp5zbx7MZCkJ7lGiju//yonfEvFofriJBKACjMq/KQHBex96QpeA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
loader-runner@4.3.0:
|
||||
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
|
||||
@ -1137,6 +1146,9 @@ packages:
|
||||
resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
material-symbols@0.28.2:
|
||||
resolution: {integrity: sha512-JLK+Bgtfg5Dn9V2WYk6lSwmxciNNF2zmqc/V8MLmH0K9LttUhPCaauJzrS1Vw3mJPs/Tyfi/tszynNRX6nWQOA==}
|
||||
|
||||
merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
@ -1577,6 +1589,9 @@ packages:
|
||||
tslib@2.7.0:
|
||||
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -1777,33 +1792,39 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@livekit/components-core@0.11.5(@livekit/protocol@1.20.1)(livekit-client@2.5.2)(tslib@2.7.0)':
|
||||
'@livekit/components-core@0.11.5(@livekit/protocol@1.34.0)(livekit-client@2.8.1)(tslib@2.8.1)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.11
|
||||
'@livekit/protocol': 1.20.1
|
||||
livekit-client: 2.5.2
|
||||
'@livekit/protocol': 1.34.0
|
||||
livekit-client: 2.8.1
|
||||
loglevel: 1.9.1
|
||||
rxjs: 7.8.1
|
||||
tslib: 2.7.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@livekit/components-react@2.6.0(@livekit/protocol@1.20.1)(livekit-client@2.5.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.7.0)':
|
||||
'@livekit/components-react@2.6.0(@livekit/protocol@1.34.0)(livekit-client@2.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tslib@2.8.1)':
|
||||
dependencies:
|
||||
'@livekit/components-core': 0.11.5(@livekit/protocol@1.20.1)(livekit-client@2.5.2)(tslib@2.7.0)
|
||||
'@livekit/protocol': 1.20.1
|
||||
'@livekit/components-core': 0.11.5(@livekit/protocol@1.34.0)(livekit-client@2.8.1)(tslib@2.8.1)
|
||||
'@livekit/protocol': 1.34.0
|
||||
clsx: 2.1.1
|
||||
livekit-client: 2.5.2
|
||||
livekit-client: 2.8.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
tslib: 2.7.0
|
||||
tslib: 2.8.1
|
||||
usehooks-ts: 3.1.0(react@18.3.1)
|
||||
|
||||
'@livekit/components-styles@1.1.2': {}
|
||||
|
||||
'@livekit/krisp-noise-filter@0.2.8(livekit-client@2.5.2)':
|
||||
'@livekit/krisp-noise-filter@0.2.8(livekit-client@2.8.1)':
|
||||
dependencies:
|
||||
livekit-client: 2.5.2
|
||||
livekit-client: 2.8.1
|
||||
|
||||
'@livekit/protocol@1.20.1':
|
||||
'@livekit/mutex@1.1.1': {}
|
||||
|
||||
'@livekit/protocol@1.30.0':
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 1.10.0
|
||||
|
||||
'@livekit/protocol@1.34.0':
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 1.10.0
|
||||
|
||||
@ -1880,9 +1901,9 @@ snapshots:
|
||||
|
||||
'@types/react-dom@18.3.0':
|
||||
dependencies:
|
||||
'@types/react': 18.3.5
|
||||
'@types/react': 18.3.8
|
||||
|
||||
'@types/react@18.3.5':
|
||||
'@types/react@18.3.8':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.12
|
||||
csstype: 3.1.3
|
||||
@ -2403,7 +2424,7 @@ snapshots:
|
||||
eslint: 9.9.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@9.9.1))(eslint@9.9.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.9.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@9.9.1))(eslint@9.9.1))(eslint@9.9.1)
|
||||
eslint-plugin-jsx-a11y: 6.9.0(eslint@9.9.1)
|
||||
eslint-plugin-react: 7.35.0(eslint@9.9.1)
|
||||
eslint-plugin-react-hooks: 4.6.2(eslint@9.9.1)
|
||||
@ -2434,7 +2455,7 @@ snapshots:
|
||||
is-bun-module: 1.1.0
|
||||
is-glob: 4.0.3
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.9.1)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@9.9.1))(eslint@9.9.1))(eslint@9.9.1)
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/parser'
|
||||
- eslint-import-resolver-node
|
||||
@ -2452,7 +2473,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3)(eslint@9.9.1):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@9.9.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@9.9.1))(eslint@9.9.1))(eslint@9.9.1):
|
||||
dependencies:
|
||||
array-includes: 3.1.8
|
||||
array.prototype.findlastindex: 1.2.5
|
||||
@ -2924,20 +2945,22 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
type-check: 0.4.0
|
||||
|
||||
livekit-client@2.5.2:
|
||||
livekit-client@2.8.1:
|
||||
dependencies:
|
||||
'@livekit/protocol': 1.20.1
|
||||
'@livekit/mutex': 1.1.1
|
||||
'@livekit/protocol': 1.30.0
|
||||
events: 3.3.0
|
||||
loglevel: 1.9.1
|
||||
sdp-transform: 2.14.2
|
||||
ts-debounce: 4.0.0
|
||||
tslib: 2.7.0
|
||||
tslib: 2.8.1
|
||||
typed-emitter: 2.1.0
|
||||
webrtc-adapter: 9.0.1
|
||||
|
||||
livekit-server-sdk@2.6.2:
|
||||
livekit-server-sdk@2.9.7:
|
||||
dependencies:
|
||||
'@livekit/protocol': 1.20.1
|
||||
'@bufbuild/protobuf': 1.10.0
|
||||
'@livekit/protocol': 1.34.0
|
||||
camelcase-keys: 9.1.3
|
||||
jose: 5.8.0
|
||||
|
||||
@ -2961,6 +2984,8 @@ snapshots:
|
||||
|
||||
map-obj@5.0.0: {}
|
||||
|
||||
material-symbols@0.28.2: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
@ -3408,6 +3433,8 @@ snapshots:
|
||||
|
||||
tslib@2.7.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
186
styles/CustomControlBar.css
Normal file
186
styles/CustomControlBar.css
Normal file
@ -0,0 +1,186 @@
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||
|
||||
.custom-control-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.room-name-box {
|
||||
background: rgba(144, 155, 170, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #909baa;
|
||||
}
|
||||
|
||||
.copy-link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.copy-link-button .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
color: #909baa;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
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;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.mic-button[data-lk-audio-enabled="false"] {
|
||||
background: #ff5252;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.camera-button[data-lk-video-enabled="false"] {
|
||||
background: #ff5252;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.camera-button[data-lk-video-enabled="true"] .material-symbols-outlined {
|
||||
content: 'videocam';
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.camera-button[data-lk-video-enabled="false"] .material-symbols-outlined {
|
||||
content: 'videocam_off';
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.screen-share-button[data-lk-screen-share-enabled="true"] .material-symbols-outlined {
|
||||
content: 'screen_share';
|
||||
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;
|
||||
}
|
||||
|
||||
.record-sign .material-symbols-outlined {
|
||||
color: #ff6f6f;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
.record-sign.disabled .material-symbols-outlined {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.end-call-button {
|
||||
width: 64px;
|
||||
height: 40px;
|
||||
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;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
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;
|
||||
}
|
||||
|
||||
.settings-button .material-symbols-outlined {
|
||||
font-size: 24px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user