Compare commits

..

52 Commits

Author SHA1 Message Date
lukasIO
12cee3ed06
Update livekit dependencies (#512) 2026-02-19 17:07:12 +01:00
renovate[bot]
2220072d47
chore(deps): update dependency node to v24 (#491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 16:56:32 +01:00
renovate[bot]
392ca136de
fix(deps): update dependency @livekit/krisp-noise-filter to v0.4.1 (#505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-19 16:54:36 +01:00
renovate[bot]
3a75f3222f
fix(deps): update livekit dependencies (non-major) (#499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 16:24:35 +01:00
vercel[bot]
f80673aba8
Fix React Server Components CVE vulnerabilities (#503)
Updated dependencies to fix Next.js and React CVE vulnerabilities.

The fix-react2shell-next tool automatically updated the following packages to their secure versions:
- next
- react-server-dom-webpack
- react-server-dom-parcel  
- react-server-dom-turbopack

All package.json files have been scanned and vulnerable versions have been patched to the correct fixed versions based on the official React advisory.

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2025-12-26 11:35:52 +01:00
vercel[bot]
690dc1011a
Update Next.js/React Flight RCE vulnerability patches (#501)
## React Flight / Next.js RCE Advisory - Security Update

### Summary
Updated the project to address the React Flight / Next.js RCE advisory (CVE-2024-50383) by upgrading Next.js to the patched version.

### Vulnerability Assessment
 **Project is affected by the advisory:**
- Uses **Next.js 15.2.x** (vulnerable version range)
- Does NOT use React Flight packages (react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack)
- Uses React 18.3.1 (not vulnerable React 19.x versions)

### Changes Made

#### Modified Files:
1. **package.json**
   - Upgraded `next` from `15.2.4` to `15.2.6` (patched version for 15.2.x)
   - No React or React DOM changes required (Next.js manages its own patched React versions)

2. **pnpm-lock.yaml**
   - Updated lockfile to reflect `next@15.2.6` installation
   - All dependencies resolved correctly with patched versions

### Implementation Details
- This project is a Next.js 15 application without React Server Components/Flight
- The RCE vulnerability in Next.js 15.2.x is addressed by upgrading to 15.2.6
- No React Flight packages required updating since they are not used
- React versions (18.3.1) are not affected by this vulnerability

### Build Status
⚠️ **Note on Pre-existing Issue:**
The build fails due to corrupted image files in `public/background-images/` (pre-existing issue):
- `ali-kazal-tbw_KQE3Cbg-unsplash.jpg` (130 bytes - should be larger)
- `samantha-gades-BlIhVfXbi9s-unsplash.jpg` (132 bytes - should be larger)

This image corruption issue exists in the original codebase and is unrelated to the security update. The Next.js upgrade to 15.2.6 itself is successful and the patched version is correctly installed.

### Testing
- Verified dependency installation with `pnpm install`
- Confirmed lockfile contains `next@15.2.6`
- Confirmed no React Flight packages are used
- Pre-existing image corruption prevents full build, but dependency upgrade is verified

### Security Impact
 **Successfully patched against CVE-2024-50383**
- Next.js upgraded to 15.2.6 (patched version for 15.2.x)
- No vulnerable React Flight packages in use
- React versions remain compatible and secure

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2025-12-08 12:50:11 +01:00
renovate[bot]
6de1bc8cc6
fix(deps): update dependency livekit-server-sdk to v2.14.2 (#495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 09:33:18 +00:00
renovate[bot]
563925f757
chore(deps): update actions/checkout action to v6 (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 22:31:46 -08:00
renovate[bot]
0b62ed930e
chore(deps): update devdependencies (non-major) (#480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 20:58:17 -08:00
lukasIO
baa4e787a2
Default to dual peer connection for custom tab (#496) 2025-11-20 15:55:51 +01:00
renovate[bot]
dc82cc23b9
fix(deps): update dependency livekit-client to v2.16.0 (#494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-19 09:56:42 +01:00
renovate[bot]
e9b037bac1
fix(deps): update livekit dependencies (non-major) (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 08:20:55 +01:00
lukasIO
49b83637dc
Enable singlePC mode for meet also on prod (#493)
* Enable singlePC mode for meet also on prod

* fix
2025-11-10 11:04:29 +01:00
renovate[bot]
aa9be8cdc0
fix(deps): update dependency livekit-client to v2.15.13 (#487)
* fix(deps): update dependency livekit-client to v2.15.12

* bump

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: lukasIO <mail@lukasseiler.de>
2025-10-21 19:25:41 +02:00
lukasIO
03aac6591a
Enable single pc connection on staging (#488)
* Enable single pc connection on staging

* fix deps

* 'security'

* vp9

* use util
2025-10-16 10:32:49 +02:00
lukasIO
83424b27d5
Revert "Use single pc (#483)" (#484)
* Revert "Update livekit client and use single pc (#483)"

This reverts commit 55adec00d31c25ef40e10f67ef7dd4880c9e81a6.

* still update livekit client
2025-10-13 17:53:27 +02:00
lukasIO
55adec00d3
Update livekit client and use single pc (#483) 2025-10-13 16:57:59 +02:00
renovate[bot]
5ff6fa32ac
chore(deps): update pnpm to v10.18.2 (#408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 15:38:57 +02:00
renovate[bot]
8e66391a01
fix(deps): update livekit dependencies (non-major) (#481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 17:20:43 +02:00
renovate[bot]
e9dba9861a
fix(deps): update dependency react-hot-toast to v2.6.0 (#473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 13:21:40 +02:00
renovate[bot]
76234cdf93
fix(deps): update livekit dependencies (non-major) (#475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 14:28:39 +02:00
Tobias Fried
0b4af83a3f
chore(ci): tag deployment versions (#478) 2025-09-09 00:48:31 -06:00
renovate[bot]
6fdf7f0b9a
fix(deps): update livekit dependencies (non-major) (#474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 14:04:10 +02:00
renovate[bot]
372cdfe760
chore(deps): update dependency node to v22 (#470)
* chore(deps): update dependency node to v22

* Update test.yaml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: lukasIO <mail@lukasseiler.de>
2025-08-15 12:37:50 +02:00
renovate[bot]
fcec3a2459
chore(deps): update devdependencies (non-major) (#451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 12:36:16 +02:00
renovate[bot]
7d1d62b6c3
fix(deps): update dependency livekit-client to v2.15.5 (#472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 12:35:54 +02:00
renovate[bot]
aa310ade64
fix(deps): update livekit dependencies (non-major) (#463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 17:11:12 +02:00
ThomFoolery
762f1c4a6e
feat: add template media (#465) 2025-07-21 17:50:03 -04:00
ThomFoolery
6ce2570868
feat: add template media (#464) 2025-07-21 16:53:56 -04:00
renovate[bot]
785359f977
fix(deps): update dependency @livekit/components-react to v2.9.13 (#459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 17:52:35 +02:00
lukasIO
26d90de86c
Don't enable krisp by default on low power devices (#457) 2025-07-04 10:14:27 +02:00
David Zhao
f13f8df08e
Dynamic handling of low-power devices (#450) 2025-07-03 19:03:38 +02:00
renovate[bot]
4dd11f412b
fix(deps): update dependency livekit-client to v2.15.2 (#456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 13:10:01 +02:00
renovate[bot]
a8a48d5a7f
fix(deps): update dependency livekit-client to v2.15.1 (#455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 08:06:49 +02:00
renovate[bot]
68046da53c
fix(deps): update dependency @livekit/krisp-noise-filter to v0.3.4 (#453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 18:41:54 +02:00
lukasIO
8826f588a0
Add cross origin headers for shared memory usage (#454) 2025-07-02 18:37:36 +02:00
lukasIO
3f4b5a14b9
Revert "fix(deps): update dependency @livekit/krisp-noise-filter to v0.3.2 (#446)" (#452)
This reverts commit f6cdb176e9eb5119220e5e092f891871ed96ba28.
2025-07-02 17:34:13 +02:00
renovate[bot]
f6cdb176e9
fix(deps): update dependency @livekit/krisp-noise-filter to v0.3.2 (#446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 16:54:10 +02:00
renovate[bot]
3260877886
fix(deps): update livekit dependencies (non-major) (#448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 16:12:42 +02:00
renovate[bot]
e1954a739d
fix(deps): update livekit dependencies (non-major) (#447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 11:39:17 +02:00
renovate[bot]
03fda24b39
fix(deps): update dependency livekit-client to v2.13.8 (#444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 17:56:54 +02:00
renovate[bot]
2e35ce825e
fix(deps): update livekit dependencies (non-major) (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 16:06:06 +02:00
renovate[bot]
62df7245a3
fix(deps): update dependency livekit-client to v2.13.6 (#442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 12:03:42 +02:00
lukasIO
b650fecdd4
Derive region url from project url (#441)
* Derive region url from project url

* add tests

* test workflow

* fix workflow

* ugh

* fix

* fix staging/prod
2025-06-18 17:03:40 +02:00
renovate[bot]
5230af4fb6
chore(deps): update devdependencies (non-major) (#436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:20:23 +02:00
renovate[bot]
8736088a7e
fix(deps): update dependency livekit-client to v2.13.5 (#439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-15 21:36:47 -07:00
renovate[bot]
5619c99aa9
fix(deps): update dependency livekit-client to v2.13.4 (#437)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-14 22:52:07 -07:00
lukasIO
c99a780f58
optimizations for low-powered devices (#438)
* ensure low power devices don't use high quality for krisp

* Update MicrophoneSettings.tsx

* use lower video publish settings

* fix import

* comments

---------

Co-authored-by: David Zhao <dz@livekit.io>
2025-06-14 17:23:36 -07:00
lukasIO
c4ea8a31ec
Unify e2ee setup on demo and custom page (#434)
* Unify e2ee setup on demo and custom page

* fix
2025-05-27 16:40:42 +02:00
lukasIO
bfde08ea91
fix: recreate options when e2ee enabled changes (#432) 2025-05-27 10:08:33 +02:00
renovate[bot]
96b193098d
fix(deps): update livekit dependencies (non-major) (#431)
* fix(deps): update livekit dependencies (non-major)

* update settings

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: lukasIO <mail@lukasseiler.de>
2025-05-23 16:50:31 +02:00
renovate[bot]
b3b8901cf7
fix(deps): update dependency @livekit/components-react to v2.9.8 (#429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 09:17:03 +02:00
27 changed files with 1609 additions and 869 deletions

BIN
.github/assets/template-dark.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

BIN
.github/assets/template-light.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

View File

@ -1,33 +1,16 @@
# .github/workflows/sync-to-production.yaml
name: Sync main to sandbox-production
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: livekit-examples/sandbox-deploy-action@v1
with:
fetch-depth: 0 # Fetch all history so we can force push
- name: Set up Git
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@livekit.io'
- name: Sync to sandbox-production
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git checkout sandbox-production || git checkout -b sandbox-production
git merge --strategy-option theirs main
git push origin sandbox-production
production_branch: 'sandbox-production'
token: ${{ secrets.GITHUB_TOKEN }}

32
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Test
on:
push:
branches: [ main ]
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: ESLint
run: pnpm lint
- name: Prettier
run: pnpm format:check
- name: Run Tests
run: pnpm test

View File

@ -1,4 +1,5 @@
import { randomString } from '@/lib/client-utils';
import { getLiveKitURL } from '@/lib/getLiveKitURL';
import { ConnectionDetails } from '@/lib/types';
import { AccessToken, AccessTokenOptions, VideoGrant } from 'livekit-server-sdk';
import { NextRequest, NextResponse } from 'next/server';
@ -6,6 +7,7 @@ import { NextRequest, NextResponse } from 'next/server';
const API_KEY = process.env.LIVEKIT_API_KEY;
const API_SECRET = process.env.LIVEKIT_API_SECRET;
const LIVEKIT_URL = process.env.LIVEKIT_URL;
const COOKIE_KEY = 'random-participant-postfix';
export async function GET(request: NextRequest) {
@ -15,7 +17,10 @@ export async function GET(request: NextRequest) {
const participantName = request.nextUrl.searchParams.get('participantName');
const metadata = request.nextUrl.searchParams.get('metadata') ?? '';
const region = request.nextUrl.searchParams.get('region');
const livekitServerUrl = region ? getLiveKitURL(region) : LIVEKIT_URL;
if (!LIVEKIT_URL) {
throw new Error('LIVEKIT_URL is not defined');
}
const livekitServerUrl = region ? getLiveKitURL(LIVEKIT_URL, region) : LIVEKIT_URL;
let randomParticipantPostfix = request.cookies.get(COOKIE_KEY)?.value;
if (livekitServerUrl === undefined) {
throw new Error('Invalid region');
@ -75,21 +80,6 @@ function createParticipantToken(userInfo: AccessTokenOptions, roomName: string)
return at.toJwt();
}
/**
* Get the LiveKit server URL for the given region.
*/
function getLiveKitURL(region: string | null): string {
let targetKey = 'LIVEKIT_URL';
if (region) {
targetKey = `LIVEKIT_URL_${region}`.toUpperCase();
}
const url = process.env[targetKey];
if (!url) {
throw new Error(`${targetKey} is not defined`);
}
return url;
}
function getCookieExpirationTime(): string {
var now = new Date();
var time = now.getTime();

View File

@ -11,24 +11,24 @@ import {
type VideoCodec,
} from 'livekit-client';
import { DebugMode } from '@/lib/Debug';
import { useEffect, useMemo } from 'react';
import { decodePassphrase } from '@/lib/client-utils';
import { useEffect, useMemo, useState } from 'react';
import { KeyboardShortcuts } from '@/lib/KeyboardShortcuts';
import { SettingsMenu } from '@/lib/SettingsMenu';
import { useSetupE2EE } from '@/lib/useSetupE2EE';
import { useLowCPUOptimizer } from '@/lib/usePerfomanceOptimiser';
export function VideoConferenceClientImpl(props: {
liveKitUrl: string;
token: string;
codec: VideoCodec | undefined;
singlePeerConnection: boolean | undefined;
}) {
const worker =
typeof window !== 'undefined' &&
new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
const keyProvider = new ExternalE2EEKeyProvider();
const e2eePassphrase =
typeof window !== 'undefined' ? decodePassphrase(window.location.hash.substring(1)) : undefined;
const { worker, e2eePassphrase } = useSetupE2EE();
const e2eeEnabled = !!(e2eePassphrase && worker);
const [e2eeSetupComplete, setE2eeSetupComplete] = useState(false);
const roomOptions = useMemo((): RoomOptions => {
return {
publishDefaults: {
@ -44,14 +44,12 @@ export function VideoConferenceClientImpl(props: {
worker,
}
: undefined,
singlePeerConnection: props.singlePeerConnection,
};
}, []);
}, [e2eeEnabled, props.codec, keyProvider, worker]);
const room = useMemo(() => new Room(roomOptions), [roomOptions]);
const room = useMemo(() => new Room(roomOptions), []);
if (e2eeEnabled) {
keyProvider.setKey(e2eePassphrase);
room.setE2EEEnabled(true);
}
const connectOptions = useMemo((): RoomConnectOptions => {
return {
autoSubscribe: true,
@ -59,13 +57,29 @@ export function VideoConferenceClientImpl(props: {
}, []);
useEffect(() => {
room.connect(props.liveKitUrl, props.token, connectOptions).catch((error) => {
console.error(error);
});
room.localParticipant.enableCameraAndMicrophone().catch((error) => {
console.error(error);
});
}, [room, props.liveKitUrl, props.token, connectOptions]);
if (e2eeEnabled) {
keyProvider.setKey(e2eePassphrase).then(() => {
room.setE2EEEnabled(true).then(() => {
setE2eeSetupComplete(true);
});
});
} else {
setE2eeSetupComplete(true);
}
}, [e2eeEnabled, e2eePassphrase, keyProvider, room, setE2eeSetupComplete]);
useEffect(() => {
if (e2eeSetupComplete) {
room.connect(props.liveKitUrl, props.token, connectOptions).catch((error) => {
console.error(error);
});
room.localParticipant.enableCameraAndMicrophone().catch((error) => {
console.error(error);
});
}
}, [room, props.liveKitUrl, props.token, connectOptions, e2eeSetupComplete]);
useLowCPUOptimizer(room);
return (
<div className="lk-room-container">

View File

@ -7,9 +7,10 @@ export default async function CustomRoomConnection(props: {
liveKitUrl?: string;
token?: string;
codec?: string;
singlePC?: string;
}>;
}) {
const { liveKitUrl, token, codec } = await props.searchParams;
const { liveKitUrl, token, codec, singlePC } = await props.searchParams;
if (typeof liveKitUrl !== 'string') {
return <h2>Missing LiveKit URL</h2>;
}
@ -22,7 +23,12 @@ export default async function CustomRoomConnection(props: {
return (
<main data-lk-theme="default" style={{ height: '100%' }}>
<VideoConferenceClientImpl liveKitUrl={liveKitUrl} token={token} codec={codec} />
<VideoConferenceClientImpl
liveKitUrl={liveKitUrl}
token={token}
codec={codec}
singlePeerConnection={singlePC === 'true'}
/>
</main>
);
}

View File

@ -2,7 +2,7 @@ import '../styles/globals.css';
import '@livekit/components-styles';
import '@livekit/components-styles/prefabs';
import type { Metadata, Viewport } from 'next';
import { Providers } from '@/lib/Providers';
import { Toaster } from 'react-hot-toast';
export const metadata: Metadata = {
title: {
@ -52,7 +52,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
<html lang="en">
<body data-lk-theme="default">
<Providers>{children}</Providers>
<Toaster />
{children}
</body>
</html>
);

View File

@ -23,8 +23,12 @@ import {
DeviceUnsupportedError,
RoomConnectOptions,
RoomEvent,
TrackPublishDefaults,
VideoCaptureOptions,
} from 'livekit-client';
import { useRouter } from 'next/navigation';
import { useSetupE2EE } from '@/lib/useSetupE2EE';
import { useLowCPUOptimizer } from '@/lib/usePerfomanceOptimiser';
const CONN_DETAILS_ENDPOINT =
process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT ?? '/api/connection-details';
@ -93,15 +97,10 @@ function VideoConferenceComponent(props: {
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 { worker, e2eePassphrase } = useSetupE2EE();
const e2eeEnabled = !!(e2eePassphrase && worker);
const [e2eeSetupComplete, setE2eeSetupComplete] = React.useState(false);
const roomOptions = React.useMemo((): RoomOptions => {
@ -109,30 +108,28 @@ function VideoConferenceComponent(props: {
if (e2eeEnabled && (videoCodec === 'av1' || videoCodec === 'vp9')) {
videoCodec = undefined;
}
const videoCaptureDefaults: VideoCaptureOptions = {
deviceId: props.userChoices.videoDeviceId ?? undefined,
resolution: props.options.hq ? VideoPresets.h2160 : VideoPresets.h720,
};
const publishDefaults: TrackPublishDefaults = {
dtx: false,
videoSimulcastLayers: props.options.hq
? [VideoPresets.h1080, VideoPresets.h720]
: [VideoPresets.h540, VideoPresets.h216],
red: !e2eeEnabled,
videoCodec,
};
return {
videoCaptureDefaults: {
deviceId: props.userChoices.videoDeviceId ?? undefined,
resolution: props.options.hq ? VideoPresets.h2160 : VideoPresets.h720,
},
publishDefaults: {
dtx: false,
videoSimulcastLayers: props.options.hq
? [VideoPresets.h1080, VideoPresets.h720]
: [VideoPresets.h540, VideoPresets.h216],
red: !e2eeEnabled,
videoCodec,
},
videoCaptureDefaults: videoCaptureDefaults,
publishDefaults: publishDefaults,
audioCaptureDefaults: {
deviceId: props.userChoices.audioDeviceId ?? undefined,
},
adaptiveStream: { pixelDensity: 'screen' },
adaptiveStream: true,
dynacast: true,
e2ee: e2eeEnabled
? {
keyProvider,
worker,
}
: undefined,
e2ee: keyProvider && worker && e2eeEnabled ? { keyProvider, worker } : undefined,
singlePeerConnection: true,
};
}, [props.userChoices, props.options.hq, props.options.codec]);
@ -170,6 +167,7 @@ function VideoConferenceComponent(props: {
room.on(RoomEvent.Disconnected, handleOnLeave);
room.on(RoomEvent.EncryptionError, handleEncryptionError);
room.on(RoomEvent.MediaDevicesError, handleError);
if (e2eeSetupComplete) {
room
.connect(
@ -198,6 +196,8 @@ function VideoConferenceComponent(props: {
};
}, [e2eeSetupComplete, room, props.connectionDetails, props.userChoices]);
const lowPowerMode = useLowCPUOptimizer(room);
const router = useRouter();
const handleOnLeave = React.useCallback(() => router.push('/'), [router]);
const handleError = React.useCallback((error: Error) => {
@ -211,6 +211,12 @@ function VideoConferenceComponent(props: {
);
}, []);
React.useEffect(() => {
if (lowPowerMode) {
console.warn('Low power mode enabled');
}
}, [lowPowerMode]);
return (
<div className="lk-room-container">
<RoomContext.Provider value={room}>

View File

@ -1,6 +1,7 @@
import React from 'react';
import {
MediaDeviceMenu,
TrackReference,
TrackToggle,
useLocalParticipant,
VideoTrack,
@ -33,8 +34,10 @@ export function CameraSettings() {
null,
);
const camTrackRef = React.useMemo(() => {
return { participant: localParticipant, publication: cameraTrack, source: Track.Source.Camera };
const camTrackRef: TrackReference | undefined = React.useMemo(() => {
return cameraTrack
? { participant: localParticipant, publication: cameraTrack, source: Track.Source.Camera }
: undefined;
}, [localParticipant, cameraTrack]);
const selectBackground = (type: BackgroundType, imagePath?: string) => {
@ -60,15 +63,17 @@ export function CameraSettings() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<VideoTrack
style={{
maxHeight: '280px',
objectFit: 'contain',
objectPosition: 'right',
transform: 'scaleX(-1)',
}}
trackRef={camTrackRef}
/>
{camTrackRef && (
<VideoTrack
style={{
maxHeight: '280px',
objectFit: 'contain',
objectPosition: 'right',
transform: 'scaleX(-1)',
}}
trackRef={camTrackRef}
/>
)}
<section className="lk-button-group">
<TrackToggle source={Track.Source.Camera}>Camera</TrackToggle>

View File

@ -1,109 +1,31 @@
'use client';
import React from 'react';
import { Track } from 'livekit-client';
import { useLocalParticipant, useTrackToggle } from '@livekit/components-react';
import { useSettingsState } from './SettingsContext';
import { KeyCommand } from './types';
import { useTrackToggle } from '@livekit/components-react';
export function KeyboardShortcuts() {
const { state } = useSettingsState();
const { localParticipant, isMicrophoneEnabled } = useLocalParticipant();
const { toggle: toggleMic, pending: pendingMicChange } = useTrackToggle({ source: Track.Source.Microphone });
const { toggle: toggleCamera, pending: pendingCameraChange } = useTrackToggle({ source: Track.Source.Camera });
const pttHeldRef = React.useRef(false);
const { toggle: toggleMic } = useTrackToggle({ source: Track.Source.Microphone });
const { toggle: toggleCamera } = useTrackToggle({ source: Track.Source.Camera });
React.useEffect(() => {
const handlers = Object.entries(state.keybindings)
.flatMap(([command, binding]) => {
switch (command) {
case KeyCommand.PTT:
if (!state.enablePTT || !Array.isArray(binding)) return [];
function handleShortcut(event: KeyboardEvent) {
// Toggle microphone: Cmd/Ctrl-Shift-A
if (toggleMic && event.key === 'A' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
toggleMic();
}
const [enable, disable] = binding;
const t = getEventTarget(enable.target);
if (!t) return null;
// Toggle camera: Cmd/Ctrl-Shift-V
if (event.key === 'V' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
toggleCamera();
}
}
const on = async (event: KeyboardEvent) => {
if (enable.guard(event)) {
event.preventDefault();
if (!isMicrophoneEnabled) {
pttHeldRef.current = true;
localParticipant?.setMicrophoneEnabled(true);
}
}
};
const off = async (event: KeyboardEvent) => {
if (disable.guard(event)) {
event.preventDefault();
if (pttHeldRef.current && isMicrophoneEnabled) {
pttHeldRef.current = false;
localParticipant?.setMicrophoneEnabled(false);
}
}
};
t.addEventListener(enable.eventName, on as any);
t.addEventListener(disable.eventName, off as any);
return [
{ eventName: enable.eventName, target: t, handler: on },
{ eventName: disable.eventName, target: t, handler: off },
];
case KeyCommand.ToggleMic:
if (!Array.isArray(binding)) {
const t = getEventTarget(binding.target);
if (!t) return null;
const handler = async (event: KeyboardEvent) => {
if (binding.guard(event) && !pendingMicChange) {
event.preventDefault();
toggleMic?.().catch(console.error);
}
};
t.addEventListener(binding.eventName, handler as any);
return { eventName: binding.eventName, target: t, handler };
}
case KeyCommand.ToggleCamera:
if (!Array.isArray(binding)) {
const t = getEventTarget(binding.target);
if (!t) return null;
const handler = async (event: KeyboardEvent) => {
if (binding.guard(event) && !pendingCameraChange) {
event.preventDefault();
toggleCamera?.().catch(console.error);
}
};
t.addEventListener(binding.eventName, handler as any);
return { eventName: binding.eventName, target: t, handler };
}
default:
return [];
}
})
.filter(Boolean) as Array<{
target: EventTarget;
eventName: string;
handler: (event: KeyboardEvent) => void;
}>;
return () => {
handlers.forEach(({ target, eventName, handler }) => {
target.removeEventListener(eventName, handler as any);
});
};
}, [state, toggleCamera, pendingCameraChange, toggleMic, pendingMicChange, localParticipant, isMicrophoneEnabled]);
window.addEventListener('keydown', handleShortcut);
return () => window.removeEventListener('keydown', handleShortcut);
}, [toggleMic, toggleCamera]);
return null;
}
function getEventTarget(
target: Window | Document | HTMLElement | string = window,
): EventTarget | null {
const targetElement = typeof target === 'string' ? document.querySelector(target) : target;
if (!targetElement) {
console.warn(`Target element not found for ${target}`);
return null;
}
return targetElement;
}

View File

@ -3,14 +3,27 @@ import { useKrispNoiseFilter } from '@livekit/components-react/krisp';
import { TrackToggle } from '@livekit/components-react';
import { MediaDeviceMenu } from '@livekit/components-react';
import { Track } from 'livekit-client';
import { isLowPowerDevice } from './client-utils';
export function MicrophoneSettings() {
const { isNoiseFilterEnabled, setNoiseFilterEnabled, isNoiseFilterPending } =
useKrispNoiseFilter();
const { isNoiseFilterEnabled, setNoiseFilterEnabled, isNoiseFilterPending } = useKrispNoiseFilter(
{
filterOptions: {
bufferOverflowMs: 100,
bufferDropMs: 200,
quality: isLowPowerDevice() ? 'low' : 'medium',
onBufferDrop: () => {
console.warn(
'krisp buffer dropped, noise filter versions >= 0.3.2 will automatically disable the filter',
);
},
},
},
);
React.useEffect(() => {
// enable Krisp by default
setNoiseFilterEnabled(true);
// enable Krisp by default on non-low power devices
setNoiseFilterEnabled(!isLowPowerDevice());
}, []);
return (
<div

View File

@ -1,13 +0,0 @@
'use client';
import { Toaster } from 'react-hot-toast';
import { SettingsStateProvider } from './SettingsContext';
export function Providers({ children }: React.PropsWithChildren) {
return (
<SettingsStateProvider>
<Toaster />
{children}
</SettingsStateProvider>
);
}

View File

@ -1,94 +0,0 @@
'use client';
import React, { createContext, SetStateAction, useCallback, useContext, useMemo } from 'react';
import type {
SettingsState,
SettingsStateContextType,
SerializedSettingsState,
KeyBindings,
} from './types';
import { defaultKeyBindings, commonKeyBindings } from './keybindings';
import { usePersistToLocalStorage } from './persistence';
const AUXILIARY_USER_CHOICES_KEY = `lk-auxiliary-user-choices`;
const initialState: SettingsState = {
keybindings: defaultKeyBindings,
enablePTT: false,
};
function serializeSettingsState(state: SettingsState): SerializedSettingsState {
return {
...state,
keybindings: Object.entries(state.keybindings).reduce<Record<string, string>>(
(acc, [key, value]) => {
const commonName = Object.entries(commonKeyBindings).find(([_, v]) => v === value)?.[0];
if (commonName) {
acc[key] = commonName;
}
return acc;
},
{},
),
};
}
function deserializeSettingsState(state: SerializedSettingsState): SettingsState {
return {
...state,
keybindings: {
...defaultKeyBindings,
...Object.entries(state.keybindings).reduce<KeyBindings>((acc, [key, commonName]) => {
const commonBinding = commonKeyBindings[commonName as keyof typeof commonKeyBindings];
if (commonBinding) {
acc[key as keyof typeof defaultKeyBindings] = commonBinding;
}
return acc;
}, {}),
},
};
}
const SettingsStateContext = createContext<SettingsStateContextType>({
state: initialState,
set: () => { },
});
const SettingsStateProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, set] = usePersistToLocalStorage<SerializedSettingsState>(
AUXILIARY_USER_CHOICES_KEY,
serializeSettingsState(initialState),
);
const deserializedState = useMemo(() => deserializeSettingsState(state), [state]);
const setSettingsState = useCallback(
(dispatch: SetStateAction<SettingsState>) => {
if (typeof dispatch === 'function') {
set((prev) => {
const next = serializeSettingsState(dispatch(deserializeSettingsState(prev)));
return next;
});
} else {
set(serializeSettingsState(dispatch));
}
},
[set],
);
return (
<SettingsStateContext.Provider value={{ state: deserializedState, set: setSettingsState }}>
{children}
</SettingsStateContext.Provider>
);
};
const useSettingsState = () => {
const ctx = useContext(SettingsStateContext);
if (ctx === null) {
throw new Error('useSettingsState must be used within SettingsStateProvider');
}
return ctx!;
};
export { useSettingsState, SettingsStateProvider, SettingsStateContext };

View File

@ -1,18 +1,16 @@
'use client';
import * as React from 'react';
import { Track } from 'livekit-client';
import {
useMaybeLayoutContext,
MediaDeviceMenu,
TrackToggle,
useRoomContext,
useIsRecording,
} from '@livekit/components-react';
import styles from '../styles/SettingsMenu.module.css';
import { CameraSettings } from './CameraSettings';
import { MicrophoneSettings } from './MicrophoneSettings';
import { useSettingsState } from './SettingsContext';
import { KeyBinding, KeyCommand } from './types';
import { keybindingOptions } from './keybindings';
/**
* @alpha
*/
@ -22,7 +20,6 @@ export interface SettingsMenuProps extends React.HTMLAttributes<HTMLDivElement>
* @alpha
*/
export function SettingsMenu(props: SettingsMenuProps) {
const { state, set: setSettingsState } = useSettingsState();
const layoutContext = useMaybeLayoutContext();
const room = useRoomContext();
const recordingEndpoint = process.env.NEXT_PUBLIC_LK_RECORD_ENDPOINT;
@ -31,11 +28,7 @@ export function SettingsMenu(props: SettingsMenuProps) {
return {
media: { camera: true, microphone: true, label: 'Media Devices', speaker: true },
recording: recordingEndpoint ? { label: 'Recording' } : undefined,
keyboard: {
label: 'Keybindings',
keybindings: keybindingOptions,
},
} as const;
};
}, []);
const tabs = React.useMemo(
@ -80,16 +73,6 @@ export function SettingsMenu(props: SettingsMenuProps) {
}
};
const setKeyBinding = (key: KeyCommand, binds: KeyBinding | [KeyBinding, KeyBinding]) => {
setSettingsState((prev) => ({
...prev,
keybindings: {
...prev.keybindings,
[key]: binds,
},
}));
};
return (
<div className="settings-menu" style={{ width: '100%', position: 'relative' }} {...props}>
<div className={styles.tabs}>
@ -102,7 +85,10 @@ export function SettingsMenu(props: SettingsMenuProps) {
onClick={() => setActiveTab(tab)}
aria-pressed={tab === activeTab}
>
{settings[tab].label}
{
// @ts-ignore
settings[tab].label
}
</button>
),
)}
@ -154,36 +140,6 @@ export function SettingsMenu(props: SettingsMenuProps) {
</section>
</>
)}
{activeTab === 'keyboard' && (
<>
<h3>PTT</h3>
<section>
<button
className="lk-button"
onClick={() => {
setSettingsState((prev) => ({ ...prev, enablePTT: !prev.enablePTT }));
}}
>
{`${state.enablePTT ? 'Disable' : 'Enable'} PTT`}
</button>
</section>
<h4>PTT trigger</h4>
<section>
{settings.keyboard.keybindings[KeyCommand.PTT]?.map(({ label, binds }) => (
<div key={label}>
<input
type="radio"
name="ptt"
id={label}
defaultChecked={state.keybindings[KeyCommand.PTT] === binds}
onChange={() => setKeyBinding(KeyCommand.PTT, binds)}
/>
<label htmlFor={label}>{label}</label>
</div>
))}
</section>
</>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%' }}>
<button

View File

@ -19,3 +19,11 @@ export function randomString(length: number): string {
}
return result;
}
export function isLowPowerDevice() {
return navigator.hardwareConcurrency < 6;
}
export function isMeetStaging() {
return new URL(location.origin).host === 'meet.staging.livekit.io';
}

35
lib/getLiveKitURL.test.ts Normal file
View File

@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest';
import { getLiveKitURL } from './getLiveKitURL';
describe('getLiveKitURL', () => {
it('returns the original URL if no region is provided', () => {
const url = 'https://myproject.livekit.cloud';
expect(getLiveKitURL(url, null)).toBe(url + '/');
});
it('inserts the region into livekit.cloud URLs', () => {
const url = 'https://myproject.livekit.cloud';
const region = 'eu';
expect(getLiveKitURL(url, region)).toBe('https://myproject.eu.production.livekit.cloud/');
});
it('inserts the region into livekit.cloud URLs and preserves the staging environment', () => {
const url = 'https://myproject.staging.livekit.cloud';
const region = 'eu';
expect(getLiveKitURL(url, region)).toBe('https://myproject.eu.staging.livekit.cloud/');
});
it('returns the original URL for non-livekit.cloud hosts, even with region', () => {
const url = 'https://example.com';
const region = 'us';
expect(getLiveKitURL(url, region)).toBe(url + '/');
});
it('handles URLs with paths and query params', () => {
const url = 'https://myproject.livekit.cloud/room?foo=bar';
const region = 'ap';
expect(getLiveKitURL(url, region)).toBe(
'https://myproject.ap.production.livekit.cloud/room?foo=bar',
);
});
});

12
lib/getLiveKitURL.ts Normal file
View File

@ -0,0 +1,12 @@
export function getLiveKitURL(projectUrl: string, region: string | null): string {
const url = new URL(projectUrl);
if (region && url.hostname.includes('livekit.cloud')) {
let [projectId, ...hostParts] = url.hostname.split('.');
if (hostParts[0] !== 'staging') {
hostParts = ['production', ...hostParts];
}
const regionURL = [projectId, region, ...hostParts].join('.');
url.hostname = regionURL;
}
return url.toString();
}

View File

@ -1,93 +0,0 @@
import { type KeyBinding, KeyBindings, KeyCommand } from './types';
export function isInteractiveElement(event: Event) {
return (
event.target instanceof HTMLButtonElement ||
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLLabelElement ||
event.target instanceof HTMLSelectElement ||
event.target instanceof HTMLOptionElement ||
event.target instanceof HTMLTextAreaElement ||
(event.target instanceof HTMLElement && event.target.isContentEditable)
);
}
export function isMouseButton(value: number, event: Event) {
return event instanceof MouseEvent && event.button === value;
}
export const commonKeyBindings: Record<
string,
KeyBinding | [enable: KeyBinding, disable: KeyBinding]
> = {
spacebar: [
{
eventName: 'keydown',
guard: (event) => {
return event.code === 'Space' && !isInteractiveElement(event);
},
},
{
eventName: 'keyup',
guard: (event) => event.code === 'Space',
},
],
leftMouse: [
{
eventName: 'mousedown',
guard: (event) => {
return isMouseButton(0, event) && !isInteractiveElement(event);
},
},
{
eventName: 'mouseup',
guard: (event) => isMouseButton(0, event),
},
],
middleMouse: [
{
eventName: 'mousedown',
guard: (event) => isMouseButton(1, event),
},
{
eventName: 'mouseup',
guard: (event) => isMouseButton(1, event),
},
],
metaShiftA: {
eventName: 'keydown',
guard: (event) => event.key === 'A' && (event.ctrlKey || event.metaKey),
},
metaShiftV: {
eventName: 'keydown',
guard: (event) => event.key === 'V' && (event.ctrlKey || event.metaKey),
},
} as const;
export const defaultKeyBindings: KeyBindings = {
[KeyCommand.PTT]: commonKeyBindings.spacebar,
[KeyCommand.ToggleMic]: commonKeyBindings.metaShiftA,
[KeyCommand.ToggleCamera]: commonKeyBindings.metaShiftV,
};
export const keybindingOptions: Partial<
Record<
KeyCommand,
{ label: string; binds: KeyBinding | [enable: KeyBinding, disable: KeyBinding] }[]
>
> = {
[KeyCommand.PTT]: [
{
label: 'Spacebar',
binds: commonKeyBindings.spacebar,
},
{
label: 'Left Mouse Button',
binds: commonKeyBindings.leftMouse,
},
{
label: 'Middle Mouse Button',
binds: commonKeyBindings.middleMouse,
},
],
};

View File

@ -1,74 +0,0 @@
'use client';
import { Dispatch, SetStateAction, useState } from 'react';
function saveToLocalStorage<T extends object>(key: string, value: T): void {
if (typeof localStorage === 'undefined') {
console.error('Local storage is not available.');
return;
}
try {
if (value) {
const nonEmptySettings = Object.fromEntries(
Object.entries(value).filter(([, value]) => value !== ''),
);
localStorage.setItem(key, JSON.stringify(nonEmptySettings));
}
} catch (error) {
console.error(`Error setting item to local storage: ${error}`);
}
}
function loadFromLocalStorage<T extends object>(key: string): T | undefined {
if (typeof localStorage === 'undefined') {
console.error('Local storage is not available.');
return undefined;
}
try {
const item = localStorage.getItem(key);
if (!item) {
console.warn(`Item with key ${key} does not exist in local storage.`);
return undefined;
}
return JSON.parse(item);
} catch (error) {
console.error(`Error getting item from local storage: ${error}`);
return undefined;
}
}
export function createLocalStorageInterface<T extends object>(
key: string,
): { load: () => T | undefined; save: (value: T) => void } {
return {
load: () => loadFromLocalStorage<T>(key),
save: (value: T) => saveToLocalStorage<T>(key, value),
};
}
export function usePersistToLocalStorage<T extends object>(
key: string,
initialValue: T,
): [T, Dispatch<SetStateAction<T>>] {
const [value, setValue] = useState<T>(() => {
const storedValue = loadFromLocalStorage<T>(key);
return storedValue !== undefined ? storedValue : initialValue;
});
const saveValue = (dispatch: SetStateAction<T>) => {
if (typeof dispatch === 'function') {
setValue((prev) => {
const next = dispatch(prev);
saveToLocalStorage(key, next);
return next;
});
} else {
setValue(dispatch);
saveToLocalStorage(key, dispatch);
}
};
return [value, saveValue];
}

View File

@ -1,6 +1,5 @@
import { LocalAudioTrack, LocalVideoTrack, videoCodecs } from 'livekit-client';
import { VideoCodec } from 'livekit-client';
import { Dispatch } from 'react';
export interface SessionProps {
roomName: string;
@ -27,33 +26,3 @@ export type ConnectionDetails = {
participantName: string;
participantToken: string;
};
export type KeyBinding = {
eventName: keyof GlobalEventHandlersEventMap;
guard: (event: KeyboardEvent) => boolean;
target?: Window | Document | HTMLElement | string;
};
export type KeyBindings = Partial<
Record<KeyCommand, KeyBinding | [enable: KeyBinding, disable: KeyBinding]>
>;
export enum KeyCommand {
PTT = 'ptt',
ToggleMic = 'toggle-mic',
ToggleCamera = 'toggle-camera',
}
export type SettingsState = {
keybindings: KeyBindings;
enablePTT: boolean;
};
export type SettingsStateContextType = {
state: SettingsState;
set: Dispatch<React.SetStateAction<SettingsState>>;
};
export type SerializedSettingsState = Omit<SettingsState, 'keybindings'> & {
keybindings: Record<string, string>;
};

View File

@ -0,0 +1,71 @@
import {
Room,
ParticipantEvent,
RoomEvent,
RemoteTrack,
RemoteTrackPublication,
VideoQuality,
LocalVideoTrack,
isVideoTrack,
} from 'livekit-client';
import * as React from 'react';
export type LowCPUOptimizerOptions = {
reducePublisherVideoQuality: boolean;
reduceSubscriberVideoQuality: boolean;
disableVideoProcessing: boolean;
};
const defaultOptions: LowCPUOptimizerOptions = {
reducePublisherVideoQuality: true,
reduceSubscriberVideoQuality: true,
disableVideoProcessing: false,
} as const;
/**
* This hook ensures that on devices with low CPU, the performance is optimised when needed.
* This is done by primarily reducing the video quality to low when the CPU is constrained.
*/
export function useLowCPUOptimizer(room: Room, options: Partial<LowCPUOptimizerOptions> = {}) {
const [lowPowerMode, setLowPowerMode] = React.useState(false);
const opts = React.useMemo(() => ({ ...defaultOptions, ...options }), [options]);
React.useEffect(() => {
const handleCpuConstrained = async (track: LocalVideoTrack) => {
setLowPowerMode(true);
console.warn('Local track CPU constrained', track);
if (opts.reducePublisherVideoQuality) {
track.prioritizePerformance();
}
if (opts.disableVideoProcessing && isVideoTrack(track)) {
track.stopProcessor();
}
if (opts.reduceSubscriberVideoQuality) {
room.remoteParticipants.forEach((participant) => {
participant.videoTrackPublications.forEach((publication) => {
publication.setVideoQuality(VideoQuality.LOW);
});
});
}
};
room.localParticipant.on(ParticipantEvent.LocalTrackCpuConstrained, handleCpuConstrained);
return () => {
room.localParticipant.off(ParticipantEvent.LocalTrackCpuConstrained, handleCpuConstrained);
};
}, [room, opts.reducePublisherVideoQuality, opts.reduceSubscriberVideoQuality]);
React.useEffect(() => {
const lowerQuality = (_: RemoteTrack, publication: RemoteTrackPublication) => {
publication.setVideoQuality(VideoQuality.LOW);
};
if (lowPowerMode && opts.reduceSubscriberVideoQuality) {
room.on(RoomEvent.TrackSubscribed, lowerQuality);
}
return () => {
room.off(RoomEvent.TrackSubscribed, lowerQuality);
};
}, [lowPowerMode, room, opts.reduceSubscriberVideoQuality]);
return lowPowerMode;
}

15
lib/useSetupE2EE.ts Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import { ExternalE2EEKeyProvider } from 'livekit-client';
import { decodePassphrase } from './client-utils';
export function useSetupE2EE() {
const e2eePassphrase =
typeof window !== 'undefined' ? decodePassphrase(location.hash.substring(1)) : undefined;
const worker: Worker | undefined =
typeof window !== 'undefined' && e2eePassphrase
? new Worker(new URL('livekit-client/e2ee-worker', import.meta.url))
: undefined;
return { worker, e2eePassphrase };
}

View File

@ -15,6 +15,23 @@ const nextConfig = {
return config;
},
headers: async () => {
return [
{
source: '/(.*)',
headers: [
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
{
key: 'Cross-Origin-Embedder-Policy',
value: 'credentialless',
},
],
},
];
},
};
module.exports = nextConfig;

View File

@ -8,34 +8,37 @@
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"test": "vitest run",
"format:check": "prettier --check \"**/*.{ts,tsx,md,json}\"",
"format:write": "prettier --write \"**/*.{ts,tsx,md,json}\""
},
"dependencies": {
"@datadog/browser-logs": "^5.23.3",
"@livekit/components-react": "2.9.5",
"@livekit/components-styles": "1.1.6",
"@livekit/krisp-noise-filter": "0.3.0",
"@livekit/track-processors": "^0.5.4",
"livekit-client": "2.13.1",
"livekit-server-sdk": "2.13.0",
"next": "15.2.4",
"@livekit/components-react": "2.9.19",
"@livekit/components-styles": "1.2.0",
"@livekit/krisp-noise-filter": "0.4.1",
"@livekit/track-processors": "^0.7.0",
"livekit-client": "2.17.2",
"livekit-server-sdk": "2.15.0",
"next": "15.2.8",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-hot-toast": "^2.5.2",
"tinykeys": "^3.0.0"
},
"devDependencies": {
"@types/node": "22.15.19",
"@types/react": "18.3.21",
"@types/node": "24.10.13",
"@types/react": "18.3.27",
"@types/react-dom": "18.3.7",
"eslint": "9.27.0",
"eslint-config-next": "15.3.2",
"eslint": "9.39.1",
"eslint-config-next": "15.5.6",
"prettier": "3.7.3",
"source-map-loader": "^5.0.0",
"typescript": "5.8.3"
"typescript": "5.9.3",
"vitest": "^3.2.4"
},
"engines": {
"node": ">=18"
},
"packageManager": "pnpm@9.15.9"
"packageManager": "pnpm@10.18.2"
}

1539
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -15,10 +15,3 @@
.tabs > .tab[aria-pressed='true'] {
border-color: var(--lk-accent-bg);
}
.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}