Merge pull request #6 from anujsb/fix-LiveKit-call-redesign---Bottom-bar
Update Bottom Bar and Control Buttons Based on Sphinx Mac Design
This commit is contained in:
commit
b450b759a0
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
|
||||
|
||||
111
app/custom/CustomControlBar.tsx
Normal file
111
app/custom/CustomControlBar.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
'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';
|
||||
|
||||
|
||||
interface CustomControlBarProps {
|
||||
room: Room;
|
||||
roomName: string;
|
||||
}
|
||||
|
||||
export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
|
||||
const [recording, setRecording] = useState(false);
|
||||
const [participantCount, setParticipantCount] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
if (room) {
|
||||
|
||||
const updateRecordingStatus = () => setRecording(room.isRecording);
|
||||
|
||||
|
||||
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]);
|
||||
|
||||
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">
|
||||
|
||||
<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">
|
||||
{/* <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>
|
||||
|
||||
{/* 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,36 +1,37 @@
|
||||
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
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,
|
||||
useTracks,
|
||||
TrackReferenceOrPlaceholder,
|
||||
GridLayout,
|
||||
RoomAudioRenderer,
|
||||
} 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 { 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';
|
||||
console.log('SHOW_SETTINGS_MENU', SHOW_SETTINGS_MENU);
|
||||
|
||||
export function PageClientImpl(props: {
|
||||
roomName: string;
|
||||
@ -38,21 +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);
|
||||
@ -63,13 +61,14 @@ export function PageClientImpl(props: {
|
||||
const connectionDetailsResp = await fetch(url.toString());
|
||||
const connectionDetailsData = await connectionDetailsResp.json();
|
||||
setConnectionDetails(connectionDetailsData);
|
||||
}, []);
|
||||
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}
|
||||
@ -90,24 +89,37 @@ 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 &&
|
||||
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
|
||||
const e2eeEnabled = !!(e2eePassphrase && worker);
|
||||
const keyProvider = new ExternalE2EEKeyProvider();
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
const roomOptions = React.useMemo((): RoomOptions => {
|
||||
|
||||
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 {
|
||||
@ -120,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: {
|
||||
@ -128,89 +140,70 @@ function VideoConferenceComponent(props: {
|
||||
},
|
||||
adaptiveStream: { pixelDensity: 'screen' },
|
||||
dynacast: true,
|
||||
e2ee: e2eeEnabled
|
||||
? {
|
||||
keyProvider,
|
||||
worker,
|
||||
}
|
||||
: undefined,
|
||||
e2ee: e2eeConfig.enabled ? { keyProvider, worker: e2eeConfig.worker } : undefined,
|
||||
};
|
||||
// @ts-ignore
|
||||
setLogLevel('debug', 'lk-e2ee');
|
||||
}, [props.userChoices, props.options.hq, props.options.codec]);
|
||||
}, [props.userChoices, props.options.hq, props.options.codec, e2eeConfig]);
|
||||
|
||||
const room = React.useMemo(() => new Room(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);
|
||||
}
|
||||
});
|
||||
}
|
||||
const connectOptions = React.useMemo((): RoomConnectOptions => {
|
||||
return {
|
||||
autoSubscribe: true,
|
||||
};
|
||||
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 = 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 router = useRouter();
|
||||
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
|
||||
const handleOnLeave = () => router.push('/');
|
||||
|
||||
const tracks = useTracks([{ source: Track.Source.Camera, withPlaceholder: true }], { room });
|
||||
|
||||
// Return null during SSR to prevent hydration errors
|
||||
if (!isClient) return null;
|
||||
|
||||
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} className="video-grid">
|
||||
{(trackRef: TrackReferenceOrPlaceholder) => (
|
||||
<div
|
||||
key={
|
||||
trackRef.publication?.trackSid ||
|
||||
`${trackRef.participant.identity}-${trackRef.source}`
|
||||
}
|
||||
className="video-container"
|
||||
>
|
||||
<VideoTrack ref={trackRef} />
|
||||
</div>
|
||||
)}
|
||||
</GridLayout>
|
||||
) : (
|
||||
<div className="empty-video-container">
|
||||
<p>No participants with video yet</p>
|
||||
</div>
|
||||
)}
|
||||
<RoomAudioRenderer />
|
||||
<CustomControlBar room={room} roomName={props.connectionDetails.roomName} />
|
||||
<Transcript latestText={''} />
|
||||
</LiveKitRoom>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
180
styles/CustomControlBar.css
Normal file
180
styles/CustomControlBar.css
Normal file
@ -0,0 +1,180 @@
|
||||
@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);
|
||||
-webkit-backdrop-filter: blur(5px); /* Added for Safari compatibility */
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Mic button */
|
||||
.mic-button[data-lk-audio-enabled="false"] {
|
||||
background: rgba(255, 82, 82, 0.2);
|
||||
}
|
||||
|
||||
.mic-button[data-lk-audio-enabled="true"] .material-symbols-outlined {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.mic-button[data-lk-audio-enabled="false"] .material-symbols-outlined {
|
||||
color: #ff6f6f;
|
||||
}
|
||||
|
||||
/* Camera btn */
|
||||
.camera-button[data-lk-video-enabled="false"] {
|
||||
background: rgba(255, 82, 82, 0.2);
|
||||
}
|
||||
|
||||
.camera-button[data-lk-video-enabled="true"] .material-symbols-outlined {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.camera-button[data-lk-video-enabled="false"] .material-symbols-outlined {
|
||||
color: #ff6f6f;
|
||||
}
|
||||
|
||||
/* Screen share btn */
|
||||
.screen-share-button[data-lk-screen-share-enabled="true"] .material-symbols-outlined {
|
||||
color: #49c998;
|
||||
}
|
||||
|
||||
.screen-share-button[data-lk-screen-share-enabled="false"] .material-symbols-outlined {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Record */
|
||||
.record-sign .material-symbols-outlined {
|
||||
color: #ff6f6f;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.record-sign.disabled .material-symbols-outlined {
|
||||
color: #ffffff;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* End call */
|
||||
.end-call-button {
|
||||
width: 64px;
|
||||
height: 40px;
|
||||
background: #ff5252;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.end-call-button .material-symbols-outlined {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.top-right-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-box .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Fix for video layout */
|
||||
.lk-grid-layout {
|
||||
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