diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 0967ef4..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/app/custom/CustomControlBar.tsx b/app/custom/CustomControlBar.tsx
index 9b3d4fe..7b0c9ab 100644
--- a/app/custom/CustomControlBar.tsx
+++ b/app/custom/CustomControlBar.tsx
@@ -1,17 +1,14 @@
'use client';
import React, { useState, useEffect } from 'react';
-import {
- TrackToggle,
- DisconnectButton
-} from '@livekit/components-react';
-import {
- Room,
- RoomEvent,
- Track
-} from 'livekit-client';
+import { DisconnectButton, useLayoutContext, useRoomContext } from '@livekit/components-react';
+import { Room, RoomEvent, Track } from 'livekit-client';
+import { mergeClasses } from '@/lib/client-utils';
+import { ToggleSource } from '@livekit/components-core';
import '../../styles/CustomControlBar.css';
-
+import { CameraOffSVG, CameraOnSVG } from '../svg/camera';
+import { MicOffSVG, MicOnSVG } from '../svg/mic';
+import { ScreenShareOnSVG } from '../svg/screen-share';
interface CustomControlBarProps {
room: Room;
@@ -20,30 +17,21 @@ interface CustomControlBarProps {
export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
const [recording, setRecording] = useState(false);
- const [participantCount, setParticipantCount] = useState(1);
+ const [participantCount, setParticipantCount] = useState(1);
+ const { dispatch } = useLayoutContext().widget;
useEffect(() => {
if (room) {
-
const updateRecordingStatus = () => setRecording(room.isRecording);
-
-
const updateParticipantCount = () => {
- if (room && room.participants) {
- setParticipantCount(room.participants.size + 1);
- }
+ setParticipantCount(room.numParticipants);
};
-
- 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);
@@ -54,14 +42,14 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
}, [room]);
const handleCopyLink = () => {
- navigator.clipboard.writeText(window.location.href)
+ 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
@@ -101,11 +78,103 @@ export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
people
{participantCount}
-
-
+
+
{
+ if (dispatch) dispatch({ msg: 'toggle_settings' });
+ }}
+ >
settings
);
-}
\ No newline at end of file
+}
+
+interface ControlButtonProps {
+ enabled?: boolean;
+ icon: React.ReactNode;
+ className?: string;
+ onClick?: () => void;
+}
+
+function ControlButton({ enabled = true, icon, className, onClick }: ControlButtonProps) {
+ return (
+
+ );
+}
+
+function TrackToggle({ source }: { source: ToggleSource }) {
+ const { enabled, toggle } = useTrackToggle({ source });
+ const isScreenShare = source === Track.Source.ScreenShare;
+
+ return (
+
}
+ />
+ );
+}
+
+interface TrackIconProps {
+ trackSource: ToggleSource;
+ enabled: boolean;
+}
+
+function TrackIcon({ trackSource, enabled }: TrackIconProps) {
+ switch (trackSource) {
+ case Track.Source.Camera:
+ return enabled ?
:
;
+ case Track.Source.Microphone:
+ return enabled ?
:
;
+ case Track.Source.ScreenShare:
+ return enabled ? (
+
+ ) : (
+
+ stop_screen_share
+
+ );
+ }
+}
+
+// Custom hook for track toggle
+function useTrackToggle({ source }: { source: ToggleSource }) {
+ const { localParticipant } = useRoomContext();
+
+ const toggle = () => {
+ switch (source) {
+ case Track.Source.Camera:
+ return localParticipant.setCameraEnabled(!enabled);
+ case Track.Source.Microphone:
+ return localParticipant.setMicrophoneEnabled(!enabled);
+ case Track.Source.ScreenShare:
+ return localParticipant.setScreenShareEnabled(!enabled);
+ }
+ };
+
+ const enabled = (() => {
+ switch (source) {
+ case Track.Source.Camera:
+ return localParticipant.isCameraEnabled;
+ case Track.Source.Microphone:
+ return localParticipant.isMicrophoneEnabled;
+ case Track.Source.ScreenShare:
+ return localParticipant.isScreenShareEnabled;
+ }
+ })();
+
+ return { enabled, toggle };
+}
diff --git a/app/custom/VideoConferenceClientImpl.tsx b/app/custom/VideoConferenceClientImpl.tsx
index 391ddb6..2ec69b7 100644
--- a/app/custom/VideoConferenceClientImpl.tsx
+++ b/app/custom/VideoConferenceClientImpl.tsx
@@ -15,7 +15,6 @@ import {
import { DebugMode } from '@/lib/Debug';
import { useMemo, useEffect, useState } from 'react';
import { decodePassphrase } from '@/lib/client-utils';
-import { SettingsMenu } from '@/lib/SettingsMenu';
export function VideoConferenceClientImpl(props: {
liveKitUrl: string;
@@ -65,11 +64,7 @@ export function VideoConferenceClientImpl(props: {
return;
}
console.log('ROOM!!!');
- const updateTranscriptions = (
- segments: TranscriptionSegment[],
- participant: any,
- publication: any,
- ) => {
+ const updateTranscriptions = (segments: TranscriptionSegment[]) => {
console.log('received transcriptions', segments);
setTranscriptions((prev) => {
const newTranscriptions = { ...prev };
@@ -95,12 +90,7 @@ export function VideoConferenceClientImpl(props: {
audio={true}
video={true}
>
-
+
);
diff --git a/app/custom/VideoTrack.tsx b/app/custom/VideoTrack.tsx
index 9fc9fb6..338d83e 100644
--- a/app/custom/VideoTrack.tsx
+++ b/app/custom/VideoTrack.tsx
@@ -24,4 +24,4 @@ export function VideoTrack({ ref: trackRef }: VideoTrackProps) {
}, [trackRef.publication?.track]);
return
;
-}
\ No newline at end of file
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index cd7a298..0c93c4f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,7 +1,7 @@
-import '../styles/globals.css';
import '@livekit/components-styles';
import '@livekit/components-styles/prefabs';
import '../styles/participant-tile.css';
+import '../styles/globals.css';
import type { Metadata, Viewport } from 'next';
export const metadata: Metadata = {
diff --git a/app/rooms/[roomName]/PageClientImpl.tsx b/app/rooms/[roomName]/PageClientImpl.tsx
index ce418f5..8113821 100644
--- a/app/rooms/[roomName]/PageClientImpl.tsx
+++ b/app/rooms/[roomName]/PageClientImpl.tsx
@@ -1,19 +1,14 @@
-
-
'use client';
import React, { useEffect, useState, useMemo } from 'react';
import { decodePassphrase } from '@/lib/client-utils';
-import Transcript from '@/lib/Transcript';
import { ConnectionDetails } from '@/lib/types';
import {
LocalUserChoices,
PreJoin,
LiveKitRoom,
- useTracks,
- TrackReferenceOrPlaceholder,
- GridLayout,
RoomAudioRenderer,
+ VideoConference,
} from '@livekit/components-react';
import {
ExternalE2EEKeyProvider,
@@ -23,14 +18,12 @@ import {
Room,
DeviceUnsupportedError,
RoomConnectOptions,
- Track,
} from 'livekit-client';
import { useRouter } from 'next/navigation';
-import { VideoTrack } from '@/app/custom/VideoTrack';
-import { CustomControlBar } from '@/app/custom/CustomControlBar';
import '../../../styles/PageClientImpl.css';
-
import { CustomVideoLayout } from '@/lib/CustomVideoLayout';
+import { RecordingIndicator } from '@/lib/RecordingIndicator';
+
const CONN_DETAILS_ENDPOINT =
process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT ?? '/api/connection-details';
@@ -41,8 +34,10 @@ export function PageClientImpl(props: {
codec: VideoCodec;
}) {
const [preJoinChoices, setPreJoinChoices] = useState
(undefined);
- const [connectionDetails, setConnectionDetails] = useState(undefined);
-
+ const [connectionDetails, setConnectionDetails] = useState(
+ undefined,
+ );
+
const preJoinDefaults = useMemo(() => {
return {
username: '',
@@ -92,26 +87,28 @@ function VideoConferenceComponent(props: {
connectionDetails: ConnectionDetails;
options: { hq: boolean; codec: VideoCodec };
}) {
-
const [isClient, setIsClient] = useState(false);
-
+
useEffect(() => {
setIsClient(true);
}, []);
-
const getE2EEConfig = () => {
- if (typeof window === 'undefined') return { enabled: false };
+ if (typeof window === 'undefined')
+ return {
+ enabled: false,
+ passphrase: undefined,
+ worker: undefined,
+ };
const e2eePassphrase = decodePassphrase(location.hash.substring(1));
- const worker = e2eePassphrase &&
- new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
+ const worker = new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const e2eeEnabled = !!(e2eePassphrase && worker);
-
+
return {
enabled: e2eeEnabled,
passphrase: e2eePassphrase,
- worker
+ worker,
};
};
@@ -141,7 +138,9 @@ function VideoConferenceComponent(props: {
},
adaptiveStream: { pixelDensity: 'screen' },
dynacast: true,
- e2ee: e2eeConfig.enabled ? { keyProvider, worker: e2eeConfig.worker } : undefined,
+ ...(e2eeConfig.enabled && e2eeConfig.worker
+ ? { e2ee: { keyProvider, worker: e2eeConfig.worker } }
+ : null),
};
}, [props.userChoices, props.options.hq, props.options.codec, e2eeConfig]);
@@ -168,9 +167,6 @@ function VideoConferenceComponent(props: {
const router = useRouter();
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 (
@@ -183,29 +179,9 @@ function VideoConferenceComponent(props: {
audio={props.userChoices.audioEnabled}
onDisconnected={handleOnLeave}
>
- {tracks.length > 0 ? (
-
- {(trackRef: TrackReferenceOrPlaceholder) => (
-
-
-
- )}
-
- ) : (
-
-
No participants with video yet
-
- )}
+
-
-
);
-}
\ No newline at end of file
+}
diff --git a/app/svg/camera.tsx b/app/svg/camera.tsx
new file mode 100644
index 0000000..cc4f4e9
--- /dev/null
+++ b/app/svg/camera.tsx
@@ -0,0 +1,37 @@
+const CameraOnSVG = () => {
+ return (
+
+ );
+};
+
+const CameraOffSVG = () => {
+ return (
+
+ );
+};
+
+export { CameraOnSVG, CameraOffSVG };
diff --git a/app/svg/mic.tsx b/app/svg/mic.tsx
new file mode 100644
index 0000000..d544fe2
--- /dev/null
+++ b/app/svg/mic.tsx
@@ -0,0 +1,36 @@
+const MicOnSVG = () => {
+ return (
+
+ );
+};
+
+const MicOffSVG = () => {
+ return (
+
+ );
+};
+
+export { MicOnSVG, MicOffSVG };
diff --git a/app/svg/screen-share.tsx b/app/svg/screen-share.tsx
new file mode 100644
index 0000000..c7b126f
--- /dev/null
+++ b/app/svg/screen-share.tsx
@@ -0,0 +1,32 @@
+const ScreenShareOnSVG = () => {
+ return (
+
+ );
+};
+
+export { ScreenShareOnSVG };
diff --git a/assetlinks.json b/assetlinks.json
index 6d34992..17c71e8 100644
--- a/assetlinks.json
+++ b/assetlinks.json
@@ -1,2 +1,32 @@
-[{"relation": ["delegate_permission/common.handle_all_urls"],"target": {"namespace": "android_app","package_name": "chat.sphinx.v2","sha256_cert_fingerprints":["26:10:D3:CB:C0:BF:28:0D:8C:CB:07:74:85:46:5D:44:58:03:B6:09:87:1D:11:CE:95:41:FB:AD:47:39:EE:4B"]}},{"relation": ["delegate_permission/common.handle_all_urls"],"target": {"namespace": "android_app","package_name": "chat.sphinx.v2","sha256_cert_fingerprints":["64:CF:11:B5:30:12:2F:C3:00:58:28:65:4F:24:41:97:98:EA:C1:74:51:39:CE:92:1E:86:A9:B5:64:FE:E1:DC"]}},{"relation": ["delegate_permission/common.handle_all_urls"],"target": {"namespace": "android_app","package_name": "chat.sphinx.v2.debug","sha256_cert_fingerprints":
- ["B8:FE:7D:3A:D5:E4:46:CF:54:A6:45:15:17:D7:1D:06:0A:78:D1:27:68:FC:D8:7D:94:58:DD:17:DE:A4:8B:9F"]}}]
+[
+ {
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "chat.sphinx.v2",
+ "sha256_cert_fingerprints": [
+ "26:10:D3:CB:C0:BF:28:0D:8C:CB:07:74:85:46:5D:44:58:03:B6:09:87:1D:11:CE:95:41:FB:AD:47:39:EE:4B"
+ ]
+ }
+ },
+ {
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "chat.sphinx.v2",
+ "sha256_cert_fingerprints": [
+ "64:CF:11:B5:30:12:2F:C3:00:58:28:65:4F:24:41:97:98:EA:C1:74:51:39:CE:92:1E:86:A9:B5:64:FE:E1:DC"
+ ]
+ }
+ },
+ {
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "chat.sphinx.v2.debug",
+ "sha256_cert_fingerprints": [
+ "B8:FE:7D:3A:D5:E4:46:CF:54:A6:45:15:17:D7:1D:06:0A:78:D1:27:68:FC:D8:7D:94:58:DD:17:DE:A4:8B:9F"
+ ]
+ }
+ }
+]
diff --git a/dev.yaml b/dev.yaml
index 19080e3..3cf4b3f 100644
--- a/dev.yaml
+++ b/dev.yaml
@@ -1,15 +1,10 @@
services:
-
livekit:
image: sphinx-livekit
- command: ["node", "server.js"]
+ command: ['node', 'server.js']
restart: on-failure
container_name: livekit.sphinx
environment:
- HOSTNAME=0.0.0.0
ports:
- 3000:3000
-
-
-
-
diff --git a/lib/CustomVideoLayout.tsx b/lib/CustomVideoLayout.tsx
index 7492c48..f6a2d65 100644
--- a/lib/CustomVideoLayout.tsx
+++ b/lib/CustomVideoLayout.tsx
@@ -1,110 +1,102 @@
-import React from 'react';
-import {
- GridLayout,
- ControlBar,
- useTracks,
- RoomAudioRenderer,
- LayoutContextProvider,
- Chat,
-} from '@livekit/components-react';
-import { Track } from 'livekit-client';
-import { ParticipantTile } from './ParticipantTile';
-
-export const CustomVideoLayout: React.FC = () => {
- const [showChat, setShowChat] = React.useState(false);
-
- const tracks = useTracks(
- [
- { source: Track.Source.Camera, withPlaceholder: true },
- { source: Track.Source.ScreenShare, withPlaceholder: false },
- ],
- { onlySubscribed: false },
- );
-
- return (
- {},
- },
- widget: {
- state: {
- showChat,
- unreadMessages: 0,
- },
- dispatch: (action: any) => {
- if ('msg' in action && action.msg === 'toggle_chat') {
- setShowChat((prev) => !prev);
- }
- },
- },
- }}
- >
-
-
-
- {showChat && (
-
-
-
- )}
-
-
-
-
- );
-};
-
-export default CustomVideoLayout;
+import React from 'react';
+import { GridLayout, useTracks, LayoutContextProvider, Chat } from '@livekit/components-react';
+import { Track, Room } from 'livekit-client';
+import { ParticipantTile } from './ParticipantTile';
+import { CustomControlBar } from '@/app/custom/CustomControlBar';
+import { SettingsMenu } from './SettingsMenu';
+
+interface CustomVideoLayoutProps {
+ room: Room;
+ roomName: string;
+}
+
+export const CustomVideoLayout: React.FC = ({ room, roomName }) => {
+ const [showChat, setShowChat] = React.useState(false);
+ const [showSettings, setShowSettings] = React.useState(false);
+
+ const tracks = useTracks(
+ [
+ { source: Track.Source.Camera, withPlaceholder: true },
+ { source: Track.Source.ScreenShare, withPlaceholder: false },
+ ],
+ { onlySubscribed: false },
+ );
+
+ return (
+ {},
+ },
+ widget: {
+ state: {
+ showChat,
+ showSettings,
+ unreadMessages: 0,
+ },
+ dispatch: (action: any) => {
+ if ('msg' in action && action.msg === 'toggle_chat') {
+ setShowChat((prev) => !prev);
+ }
+ if ('msg' in action && action.msg === 'toggle_settings') {
+ setShowSettings((prev) => !prev);
+ }
+ },
+ },
+ }}
+ >
+
+
+
+ {showChat && (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default CustomVideoLayout;
diff --git a/lib/ParticipantTile.tsx b/lib/ParticipantTile.tsx
index f31a146..d9e8137 100644
--- a/lib/ParticipantTile.tsx
+++ b/lib/ParticipantTile.tsx
@@ -1,144 +1,132 @@
-import React, { useEffect, useState } from 'react';
-import { AudioTrack, useTracks, VideoTrack, useTrackRefContext } from '@livekit/components-react';
-import { Track, Participant } from 'livekit-client';
-
-function getAvatarColor(identity: string): string {
- const colors = [
- '#4CAF50',
- '#8BC34A',
- '#CDDC39',
- '#FFC107',
- '#FF9800',
- '#FF5722',
- '#F44336',
- '#E91E63',
- '#9C27B0',
- '#673AB7',
- '#3F51B5',
- '#2196F3',
- '#03A9F4',
- '#00BCD4',
- '#009688',
- ];
-
- let hash = 0;
- for (let i = 0; i < identity.length; i++) {
- hash = identity.charCodeAt(i) + ((hash << 5) - hash);
- }
-
- const index = Math.abs(hash) % colors.length;
- return colors[index];
-}
-
-function getInitials(name: string): string {
- if (!name) return '?';
-
- const parts = name.split(' ');
- if (parts.length === 1) {
- return parts[0].charAt(0).toUpperCase();
- }
-
- return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
-}
-
-export interface ParticipantTileProps {
- participant?: Participant;
-}
-
-export const ParticipantTile: React.FC = ({
- participant: propParticipant,
-}) => {
- const trackRef = useTrackRefContext();
- const participant = propParticipant || trackRef?.participant;
-
- if (!participant) return null;
-
- const [profilePictureUrl, setProfilePictureUrl] = useState(null);
-
- const isValidTrackRef =
- trackRef && 'publication' in trackRef && trackRef.publication !== undefined;
-
- const cameraTrack =
- isValidTrackRef && trackRef.source === Track.Source.Camera
- ? trackRef
- : useTracks([Track.Source.Camera], { onlySubscribed: false }).filter(
- (track) => track.participant.identity === participant.identity,
- )[0];
-
- const microphoneTrack = useTracks([Track.Source.Microphone], { onlySubscribed: false }).filter(
- (track) => track.participant.identity === participant.identity,
- )[0];
-
- const isSpeaking = participant.isSpeaking;
-
- useEffect(() => {
- if (participant.metadata) {
- try {
- const metadata = JSON.parse(participant.metadata);
- if (metadata.profilePictureUrl) {
- setProfilePictureUrl(metadata.profilePictureUrl);
- }
- } catch (e) {
- console.error('Failed to parse participant metadata', e);
- }
- }
- }, [participant.metadata]);
-
- const hasCamera = !!cameraTrack;
- const isCameraEnabled = hasCamera && !cameraTrack.publication?.isMuted;
-
- const hasMicrophone = !!microphoneTrack;
- const isMicrophoneEnabled = hasMicrophone && !microphoneTrack.publication?.isMuted;
-
- const avatarColor = getAvatarColor(participant.identity);
- const initials = getInitials(participant.name || participant.identity);
-
- return (
-
- {isCameraEnabled ? (
-
-
-
- ) : (
-
- {profilePictureUrl ? (
-

- ) : (
-
{initials}
- )}
-
- )}
-
-
- {isMicrophoneEnabled ? (
- isSpeaking ? (
-
- graphic_eq
-
- ) : (
- mic
- )
- ) : (
- mic_off
- )}
- {participant.name || participant.identity}
-
-
- {hasMicrophone && microphoneTrack &&
}
-
- );
-};
-
-export default ParticipantTile;
+import React, { useEffect, useState } from 'react';
+import {
+ AudioTrack,
+ useTracks,
+ VideoTrack,
+ useTrackRefContext,
+ useEnsureTrackRef,
+ TrackRefContextIfNeeded,
+} from '@livekit/components-react';
+import { Track, Participant } from 'livekit-client';
+import { isTrackReference } from '@livekit/components-core';
+
+function getAvatarColor(identity: string): string {
+ const colors = [
+ '#4CAF50',
+ '#8BC34A',
+ '#CDDC39',
+ '#FFC107',
+ '#FF9800',
+ '#FF5722',
+ '#F44336',
+ '#E91E63',
+ '#9C27B0',
+ '#673AB7',
+ '#3F51B5',
+ '#2196F3',
+ '#03A9F4',
+ '#00BCD4',
+ '#009688',
+ ];
+
+ let hash = 0;
+ for (let i = 0; i < identity.length; i++) {
+ hash = identity.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ const index = Math.abs(hash) % colors.length;
+ return colors[index];
+}
+
+function getInitials(name: string): string {
+ if (!name) return '?';
+
+ const parts = name.split(' ');
+ if (parts.length === 1) {
+ return parts[0].charAt(0).toUpperCase();
+ }
+
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
+}
+
+export interface ParticipantTileProps {
+ participant?: Participant;
+}
+
+export const ParticipantTile: React.FC = ({
+ participant: propParticipant,
+}) => {
+ const trackRef = useTrackRefContext();
+ const trackReference = useEnsureTrackRef(trackRef);
+ const participant = propParticipant || trackRef?.participant;
+
+ if (!participant) return null;
+
+ const [profilePictureUrl, setProfilePictureUrl] = useState(null);
+
+ const microphoneTrack = useTracks([Track.Source.Microphone], { onlySubscribed: false }).filter(
+ (track) => track.participant.identity === participant.identity,
+ )[0];
+
+ const isSpeaking = participant.isSpeaking;
+
+ useEffect(() => {
+ if (participant.metadata) {
+ try {
+ const metadata = JSON.parse(participant.metadata);
+ if (metadata.profilePictureUrl) {
+ setProfilePictureUrl(metadata.profilePictureUrl);
+ }
+ } catch (e) {
+ console.error('Failed to parse participant metadata', e);
+ }
+ }
+ }, [participant.metadata]);
+
+ const isCameraEnabled =
+ (trackReference.source === Track.Source.Camera && !trackReference.publication?.isMuted) ||
+ trackReference.source === Track.Source.ScreenShare;
+
+ const hasMicrophone = !!microphoneTrack;
+ const isMicrophoneEnabled = hasMicrophone && !microphoneTrack.publication?.isMuted;
+
+ const avatarColor = getAvatarColor(participant.identity);
+ const initials = getInitials(participant.name || participant.identity);
+
+ return (
+
+ {isTrackReference(trackReference) && isCameraEnabled ? (
+
+
+
+ ) : (
+
+ {profilePictureUrl ? (
+

+ ) : (
+
{initials}
+ )}
+
+ )}
+
+
+ {isMicrophoneEnabled ? (
+ <>
+ {isSpeaking ? (
+ graphic_eq
+ ) : (
+ mic
+ )}
+ >
+ ) : (
+ mic_off
+ )}
+ {participant.name || participant.identity}
+
+
+ {hasMicrophone && microphoneTrack &&
}
+
+ );
+};
+
+export default ParticipantTile;
diff --git a/lib/SettingsMenu.tsx b/lib/SettingsMenu.tsx
index 898b3e5..976e3df 100644
--- a/lib/SettingsMenu.tsx
+++ b/lib/SettingsMenu.tsx
@@ -9,13 +9,14 @@ import {
useRoomContext,
useIsRecording,
} from '@livekit/components-react';
-import styles from '../styles/SettingsMenu.module.css';
import type { KrispNoiseFilterProcessor } from '@livekit/krisp-noise-filter';
/**
* @alpha
*/
-export interface SettingsMenuProps extends React.HTMLAttributes {}
+export interface SettingsMenuProps extends React.HTMLAttributes {
+ showSettings: boolean;
+}
/**
* @alpha
@@ -111,14 +112,16 @@ export function SettingsMenu(props: SettingsMenuProps) {
}
};
+ if (!props.showSettings) return null;
+
return (
-
-
+
+
{tabs.map(
(tab) =>
settings[tab] && (
-
+
{activeTab === 'media' && (
<>
{settings.media && settings.media.camera && (
- <>
+
Camera
Camera
@@ -143,10 +146,10 @@ export function SettingsMenu(props: SettingsMenuProps) {
- >
+
)}
{settings.media && settings.media.microphone && (
- <>
+
Microphone
Microphone
@@ -154,10 +157,10 @@ export function SettingsMenu(props: SettingsMenuProps) {
- >
+
)}
{settings.media && settings.media.speaker && (
- <>
+
Speaker & Headphones
Audio Output
@@ -165,7 +168,7 @@ export function SettingsMenu(props: SettingsMenuProps) {
- >
+
)}
>
)}
@@ -201,7 +204,7 @@ export function SettingsMenu(props: SettingsMenuProps) {
)}