diff --git a/.env.example b/.env.example index 1d92e0e..497ad72 100644 --- a/.env.example +++ b/.env.example @@ -9,4 +9,6 @@ LIVEKIT_API_SECRET=secret LIVEKIT_URL=wss://my-livekit-project.livekit.cloud ## PUBLIC -NEXT_PUBLIC_LK_TOKEN_ENDPOINT=/api/token \ No newline at end of file +NEXT_PUBLIC_LK_TOKEN_ENDPOINT=/api/token + +NEXT_PUBLIC_SHOW_SETTINGS_MENU=true diff --git a/lib/Debug.tsx b/lib/Debug.tsx index 47dcce3..81191e0 100644 --- a/lib/Debug.tsx +++ b/lib/Debug.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { useRoomContext } from '@livekit/components-react'; -import { setLogLevel, LogLevel, RemoteTrackPublication } from 'livekit-client'; +import { setLogLevel, getLogger, LogLevel, RemoteTrackPublication } from 'livekit-client'; import { tinykeys } from 'tinykeys'; import styles from '../styles/Debug.module.css'; @@ -9,6 +9,9 @@ export const useDebugMode = ({ logLevel }: { logLevel?: LogLevel }) => { React.useEffect(() => { setLogLevel(logLevel ?? 'debug'); + // @ts-ignore + setLogLevel('debug', 'lk-e2ee'); + // @ts-expect-error window.__lk_room = room; diff --git a/lib/SettingsMenu.tsx b/lib/SettingsMenu.tsx new file mode 100644 index 0000000..48f0f76 --- /dev/null +++ b/lib/SettingsMenu.tsx @@ -0,0 +1,141 @@ +'use client'; +import * as React from 'react'; +import { LocalAudioTrack, Track } from 'livekit-client'; +import { + useMaybeLayoutContext, + useLocalParticipant, + MediaDeviceMenu, + TrackToggle, +} from '@livekit/components-react'; +import styles from '../styles/SettingsMenu.module.css'; + +/** + * @alpha + */ +export interface SettingsMenuProps extends React.HTMLAttributes {} + +/** + * @alpha + */ +export function SettingsMenu(props: SettingsMenuProps) { + const layoutContext = useMaybeLayoutContext(); + + const settings = React.useMemo(() => { + return { + media: { camera: true, microphone: true, label: 'Media Devices', speaker: false }, + effects: { label: 'Effects' }, + }; + }, []); + + const tabs = React.useMemo( + () => Object.keys(settings) as Array, + [settings], + ); + const { microphoneTrack } = useLocalParticipant(); + + const [activeTab, setActiveTab] = React.useState(tabs[0]); + const [isNoiseFilterEnabled, setIsNoiseFilterEnabled] = React.useState(true); + + React.useEffect(() => { + const micPublication = microphoneTrack; + if (micPublication && micPublication.track instanceof LocalAudioTrack) { + const currentProcessor = micPublication.track.getProcessor(); + if (currentProcessor && !isNoiseFilterEnabled) { + micPublication.track.stopProcessor(); + } else if (!currentProcessor && isNoiseFilterEnabled) { + import('@livekit/noise-filter') + .then(({ NoiseFilter, isNoiseFilterSupported }) => { + if (!isNoiseFilterSupported()) { + console.error('Enhanced noise filter is not supported for this browser'); + setIsNoiseFilterEnabled(false); + return; + } + micPublication?.track + // @ts-ignore + ?.setProcessor(NoiseFilter()) + .then(() => console.log('successfully set noise filter')); + }) + .catch((e) => console.error('Failed to load noise filter', e)); + } + } + }, [isNoiseFilterEnabled, microphoneTrack]); + + return ( +
+
+ {tabs.map( + (tab) => + settings[tab] && ( + + ), + )} +
+
+ {activeTab === 'media' && ( + <> + {settings.media && settings.media.camera && ( + <> +

Camera

+
+ Camera +
+ +
+
+ + )} + {settings.media && settings.media.microphone && ( + <> +

Microphone

+
+ Camera +
+ +
+
+ + )} + {settings.media && settings.media.speaker && ( + <> +

Speaker & Headphones

+
+ +
+ + )} + + )} + {activeTab === 'effects' && ( + <> +

Audio

+
+ + setIsNoiseFilterEnabled(ev.target.checked)} + checked={isNoiseFilterEnabled} + > +
+ + )} +
+ +
+ ); +} diff --git a/next.config.js b/next.config.js index 29f4c9c..4a43abd 100644 --- a/next.config.js +++ b/next.config.js @@ -5,14 +5,11 @@ const nextConfig = { productionBrowserSourceMaps: true, webpack: (config, { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }) => { // Important: return the modified config - config.module.rules = [ - ...config.module.rules, - { - test: /\.mjs$/, - enforce: 'pre', - use: ['source-map-loader'], - }, - ]; + config.module.rules.push({ + test: /\.mjs$/, + enforce: 'pre', + use: ['source-map-loader'], + }); return config; }, }; diff --git a/package.json b/package.json index ca177c8..ee441d5 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@livekit/components-react": "2.0.2", "@livekit/components-styles": "1.0.10", + "@livekit/noise-filter": "^0.1.3", "livekit-client": "2.0.4", "livekit-server-sdk": "2.0.4", "next": "14.1.0", @@ -30,5 +31,8 @@ }, "engines": { "node": ">=18" + }, + "pnpm": { + "overrides": {} } } diff --git a/pages/rooms/[name].tsx b/pages/rooms/[name].tsx index aa76967..337ea3d 100644 --- a/pages/rooms/[name].tsx +++ b/pages/rooms/[name].tsx @@ -15,6 +15,7 @@ import { RoomOptions, VideoCodec, VideoPresets, + setLogLevel, } from 'livekit-client'; import type { NextPage } from 'next'; @@ -24,6 +25,7 @@ import { useRouter } from 'next/router'; import * as React from 'react'; import { DebugMode } from '../../lib/Debug'; import { decodePassphrase, useServerUrl } from '../../lib/client-utils'; +import { SettingsMenu } from '../../lib/SettingsMenu'; const PreJoinNoSSR = dynamic( async () => { @@ -144,6 +146,8 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => { } : undefined, }; + // @ts-ignore + setLogLevel('debug', 'lk-e2ee'); }, [userChoices, hq, codec]); const room = React.useMemo(() => new Room(roomOptions), []); @@ -177,8 +181,13 @@ const ActiveRoom = ({ roomName, userChoices, onLeave }: ActiveRoomProps) => { audio={userChoices.audioEnabled} onDisconnected={onLeave} > - - + + )} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 75400ec..83474a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@livekit/components-styles': specifier: 1.0.10 version: 1.0.10 + '@livekit/noise-filter': + specifier: ^0.1.3 + version: 0.1.3(livekit-client@2.0.4) livekit-client: specifier: 2.0.4 version: 2.0.4 @@ -236,6 +239,14 @@ packages: engines: {node: '>=18'} dev: false + /@livekit/noise-filter@0.1.3(livekit-client@2.0.4): + resolution: {integrity: sha512-y5FVS8wOmDJ40ml3nqVJ2I40pFUwIx37B3XiLhPQrgXaY2saZpZebfN4D22XsbaEqPs5kCMQ82IqdLxRkQpbMA==} + peerDependencies: + livekit-client: ^2.0.2 + dependencies: + livekit-client: 2.0.4 + dev: false + /@next/env@14.1.0: resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} dev: false @@ -1971,7 +1982,7 @@ packages: '@bufbuild/protobuf': 1.4.2 events: 3.3.0 loglevel: 1.9.1 - sdp-transform: 2.14.1 + sdp-transform: 2.14.2 ts-debounce: 4.0.0 tslib: 2.6.2 typed-emitter: 2.1.0 @@ -2444,7 +2455,6 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - requiresBuild: true dependencies: tslib: 2.6.2 dev: false @@ -2490,8 +2500,8 @@ packages: ajv-keywords: 3.5.2(ajv@6.12.6) dev: true - /sdp-transform@2.14.1: - resolution: {integrity: sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==} + /sdp-transform@2.14.2: + resolution: {integrity: sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA==} hasBin: true dev: false diff --git a/styles/SettingsMenu.module.css b/styles/SettingsMenu.module.css new file mode 100644 index 0000000..2ea4f26 --- /dev/null +++ b/styles/SettingsMenu.module.css @@ -0,0 +1,23 @@ +.settingsCloseButton { + position: absolute; + right: var(--lk-grid-gap); + bottom: var(--lk-grid-gap); +} + +.tabs { + position: relative; + display: flex; + align-content: space-between; +} + +.tabs > .tab { + padding: 0.5rem; + border-radius: 0; + padding-bottom: 0.5rem; + border-bottom: 3px solid; + border-color: var(--bg5); +} + +.tabs > .tab[aria-pressed='true'] { + border-color: var(--lk-accent-bg); +}