diff --git a/.env.example b/.env.example
deleted file mode 100644
index f961fa0..0000000
--- a/.env.example
+++ /dev/null
@@ -1,30 +0,0 @@
-# 1. Copy this file and rename it to .env.local
-# 2. Update the enviroment variables below.
-
-# REQUIRED SETTINGS
-# #################
-# If you are using LiveKit Cloud, the API key and secret can be generated from the Cloud Dashboard.
-LIVEKIT_API_KEY=
-LIVEKIT_API_SECRET=
-# URL pointing to the LiveKit server. (example: `wss://my-livekit-project.livekit.cloud`)
-LIVEKIT_URL=
-
-
-# OPTIONAL SETTINGS
-# #################
-# Recording
-# S3_KEY_ID=
-# S3_KEY_SECRET=
-# S3_ENDPOINT=
-# S3_BUCKET=
-# S3_REGION=
-
-# PUBLIC
-# Uncomment settings menu when using a LiveKit Cloud, it'll enable Krisp noise filters.
-# NEXT_PUBLIC_SHOW_SETTINGS_MENU=true
-# NEXT_PUBLIC_LK_RECORD_ENDPOINT=/api/record
-
-# Optional, to pipe logs to datadog
-# NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=client-token
-# NEXT_PUBLIC_DATADOG_SITE=datadog-site
-
diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx
new file mode 100644
index 0000000..9b3d4fe
--- /dev/null
+++ b/app/custom/CustomControlBar.tsx
@@ -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 (
+
+
+
+ {roomName}
+
+
+
+ {/* Center: Control Buttons */}
+
+
+ {/* mic
+ mic_off */}
+
+
+
+ {/* videocam
+ videocam_off */}
+
+
+
+ radio_button_checked
+
+
+
+ {/* screen_share
+ screen_share */}
+
+
+
+ call_end
+
+
+
+ {/* Participants, Settings btn */}
+
+
+ people
+ {participantCount}
+
+
+
+ settings
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/custom/VideoTrack.tsx b/app/custom/VideoTrack.tsx
new file mode 100644
index 0000000..9fc9fb6
--- /dev/null
+++ b/app/custom/VideoTrack.tsx
@@ -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(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 ;
+}
\ No newline at end of file
diff --git a/app/rooms/[roomName]/PageClientImpl.tsx b/app/rooms/[roomName]/PageClientImpl.tsx
index daf17f9..8881900 100644
--- a/app/rooms/[roomName]/PageClientImpl.tsx
+++ b/app/rooms/[roomName]/PageClientImpl.tsx
@@ -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(
- undefined,
- );
- const preJoinDefaults = React.useMemo(() => {
+ const [preJoinChoices, setPreJoinChoices] = useState(undefined);
+ const [connectionDetails, setConnectionDetails] = useState(undefined);
+
+ const preJoinDefaults = useMemo(() => {
return {
username: '',
videoEnabled: true,
audioEnabled: true,
};
}, []);
- const [connectionDetails, setConnectionDetails] = React.useState(
- 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 (
-
+
{connectionDetails === undefined || preJoinChoices === undefined ? (
-
+
{
+ 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 (
- <>
-
-
- {/* */}
-
-
-
- >
+
+ {tracks.length > 0 ? (
+
+ {(trackRef: TrackReferenceOrPlaceholder) => (
+
+
+
+ )}
+
+ ) : (
+
+
No participants with video yet
+
+ )}
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 13413d7..aaf098e 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7b2137e..f0f8262 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
diff --git a/styles/CustomControlBar.css b/styles/CustomControlBar.css
new file mode 100644
index 0000000..f7c9e59
--- /dev/null
+++ b/styles/CustomControlBar.css
@@ -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;
+}
\ No newline at end of file
diff --git a/styles/PageClientImpl.css b/styles/PageClientImpl.css
new file mode 100644
index 0000000..67a47ed
--- /dev/null
+++ b/styles/PageClientImpl.css
@@ -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;
+ }
\ No newline at end of file
diff --git a/styles/VideoTrack.css b/styles/VideoTrack.css
new file mode 100644
index 0000000..be08892
--- /dev/null
+++ b/styles/VideoTrack.css
@@ -0,0 +1,5 @@
+.video-element {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
\ No newline at end of file