From bd6f530be73d3aa438e6041c25ae5a685d75c970 Mon Sep 17 00:00:00 2001 From: Ben Xu Date: Mon, 30 Dec 2024 15:23:35 -0500 Subject: [PATCH] replace hosted livekit meet with local meet link --- software/source/clients/meet/.env.example | 30 + software/source/clients/meet/.eslintrc.json | 3 + .../meet/.github/assets/livekit-mark.png | Bin 0 -> 832 bytes .../meet/.github/assets/livekit-meet.jpg | Bin 0 -> 102195 bytes .../meet/.github/assets/template-graphic.svg | 31 + .../.github/workflows/sync-to-production.yaml | 33 + software/source/clients/meet/.gitignore | 38 + software/source/clients/meet/.prettierignore | 3 + software/source/clients/meet/.prettierrc | 7 + software/source/clients/meet/LICENSE | 202 + software/source/clients/meet/README.md | 16 + .../meet/app/api/connection-details/route.ts | 81 + .../meet/app/api/record/start/route.ts | 70 + .../clients/meet/app/api/record/stop/route.ts | 39 + .../meet/app/components/VideoConference.tsx | 210 + .../app/components/detectMobileBrowser.ts | 20 + .../clients/meet/app/components/logger.ts | 56 + .../clients/meet/app/components/mergeProps.ts | 87 + .../app/components/track-reference-types.ts | 73 + .../meet/app/components/track-reference.ts | 97 + .../clients/meet/app/components/types.ts | 90 + .../components/useWarnAboutMissingStyles.ts | 11 + .../clients/meet/app/components/utils.ts | 62 + .../app/custom/VideoConferenceClientImpl.tsx | 79 + .../source/clients/meet/app/custom/page.tsx | 28 + software/source/clients/meet/app/layout.tsx | 56 + software/source/clients/meet/app/page.tsx | 201 + .../app/rooms/[roomName]/PageClientImpl.tsx | 203 + .../meet/app/rooms/[roomName]/page.tsx | 26 + software/source/clients/meet/next-env.d.ts | 5 + software/source/clients/meet/next.config.js | 16 + software/source/clients/meet/package.json | 37 + software/source/clients/meet/pnpm-lock.yaml | 3669 +++++++++++++++++ .../source/clients/meet/public/favicon.ico | Bin 0 -> 15406 bytes .../public/images/livekit-apple-touch.png | Bin 0 -> 323 bytes .../meet/public/images/livekit-meet-home.svg | 1 + .../public/images/livekit-meet-open-graph.png | Bin 0 -> 22701 bytes .../images/livekit-safari-pinned-tab.svg | 1 + software/source/clients/meet/renovate.json | 17 + .../clients/meet/styles/Debug.module.css | 16 + .../clients/meet/styles/Home.module.css | 40 + .../meet/styles/SettingsMenu.module.css | 23 + .../source/clients/meet/styles/globals.css | 67 + software/source/clients/meet/tsconfig.json | 29 + 44 files changed, 5773 insertions(+) create mode 100644 software/source/clients/meet/.env.example create mode 100644 software/source/clients/meet/.eslintrc.json create mode 100644 software/source/clients/meet/.github/assets/livekit-mark.png create mode 100644 software/source/clients/meet/.github/assets/livekit-meet.jpg create mode 100644 software/source/clients/meet/.github/assets/template-graphic.svg create mode 100644 software/source/clients/meet/.github/workflows/sync-to-production.yaml create mode 100644 software/source/clients/meet/.gitignore create mode 100644 software/source/clients/meet/.prettierignore create mode 100644 software/source/clients/meet/.prettierrc create mode 100644 software/source/clients/meet/LICENSE create mode 100644 software/source/clients/meet/README.md create mode 100644 software/source/clients/meet/app/api/connection-details/route.ts create mode 100644 software/source/clients/meet/app/api/record/start/route.ts create mode 100644 software/source/clients/meet/app/api/record/stop/route.ts create mode 100644 software/source/clients/meet/app/components/VideoConference.tsx create mode 100644 software/source/clients/meet/app/components/detectMobileBrowser.ts create mode 100644 software/source/clients/meet/app/components/logger.ts create mode 100644 software/source/clients/meet/app/components/mergeProps.ts create mode 100644 software/source/clients/meet/app/components/track-reference-types.ts create mode 100644 software/source/clients/meet/app/components/track-reference.ts create mode 100644 software/source/clients/meet/app/components/types.ts create mode 100644 software/source/clients/meet/app/components/useWarnAboutMissingStyles.ts create mode 100644 software/source/clients/meet/app/components/utils.ts create mode 100644 software/source/clients/meet/app/custom/VideoConferenceClientImpl.tsx create mode 100644 software/source/clients/meet/app/custom/page.tsx create mode 100644 software/source/clients/meet/app/layout.tsx create mode 100644 software/source/clients/meet/app/page.tsx create mode 100644 software/source/clients/meet/app/rooms/[roomName]/PageClientImpl.tsx create mode 100644 software/source/clients/meet/app/rooms/[roomName]/page.tsx create mode 100644 software/source/clients/meet/next-env.d.ts create mode 100644 software/source/clients/meet/next.config.js create mode 100644 software/source/clients/meet/package.json create mode 100644 software/source/clients/meet/pnpm-lock.yaml create mode 100644 software/source/clients/meet/public/favicon.ico create mode 100644 software/source/clients/meet/public/images/livekit-apple-touch.png create mode 100644 software/source/clients/meet/public/images/livekit-meet-home.svg create mode 100644 software/source/clients/meet/public/images/livekit-meet-open-graph.png create mode 100644 software/source/clients/meet/public/images/livekit-safari-pinned-tab.svg create mode 100644 software/source/clients/meet/renovate.json create mode 100644 software/source/clients/meet/styles/Debug.module.css create mode 100644 software/source/clients/meet/styles/Home.module.css create mode 100644 software/source/clients/meet/styles/SettingsMenu.module.css create mode 100644 software/source/clients/meet/styles/globals.css create mode 100644 software/source/clients/meet/tsconfig.json diff --git a/software/source/clients/meet/.env.example b/software/source/clients/meet/.env.example new file mode 100644 index 0000000..0f7b5a5 --- /dev/null +++ b/software/source/clients/meet/.env.example @@ -0,0 +1,30 @@ +# 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=http://localhost:10101 + + +# 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/software/source/clients/meet/.eslintrc.json b/software/source/clients/meet/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/software/source/clients/meet/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/software/source/clients/meet/.github/assets/livekit-mark.png b/software/source/clients/meet/.github/assets/livekit-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..e984d810379048e388ea3e2f0f6e3f9f7a0336ca GIT binary patch literal 832 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

CgIfOH31(y& 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software/source/clients/meet/.github/workflows/sync-to-production.yaml b/software/source/clients/meet/.github/workflows/sync-to-production.yaml new file mode 100644 index 0000000..03acb19 --- /dev/null +++ b/software/source/clients/meet/.github/workflows/sync-to-production.yaml @@ -0,0 +1,33 @@ +name: Sync main to sandbox-production + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + 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 diff --git a/software/source/clients/meet/.gitignore b/software/source/clients/meet/.gitignore new file mode 100644 index 0000000..7d093c3 --- /dev/null +++ b/software/source/clients/meet/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/software/source/clients/meet/.prettierignore b/software/source/clients/meet/.prettierignore new file mode 100644 index 0000000..fb6e24e --- /dev/null +++ b/software/source/clients/meet/.prettierignore @@ -0,0 +1,3 @@ +.github/ +.next/ +node_modules/ \ No newline at end of file diff --git a/software/source/clients/meet/.prettierrc b/software/source/clients/meet/.prettierrc new file mode 100644 index 0000000..4148c21 --- /dev/null +++ b/software/source/clients/meet/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "tabWidth": 2, + "printWidth": 100 +} diff --git a/software/source/clients/meet/LICENSE b/software/source/clients/meet/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/software/source/clients/meet/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/software/source/clients/meet/README.md b/software/source/clients/meet/README.md new file mode 100644 index 0000000..6f1b23d --- /dev/null +++ b/software/source/clients/meet/README.md @@ -0,0 +1,16 @@ +# LiveKit Meet Client + +This is a clone of the LiveKit Meet open source video conferencing app built on [LiveKit Components](https://github.com/livekit/components-js), [LiveKit Cloud](https://livekit.io/cloud), and Next.js. Used as a simple web interface to the 01 with screenshare and camera functionality. Can be run using a fully local model, STT, and TTS. + +## Usage + +Run the following command in the software directory to open a Meet instance. + +``` +poetry run 01 --server livekit --meet +``` + + +## Setup + +Ensure that you're in the meet directory. Then run `pnpm install` to install all dependencies. You're now all set to get up and running! diff --git a/software/source/clients/meet/app/api/connection-details/route.ts b/software/source/clients/meet/app/api/connection-details/route.ts new file mode 100644 index 0000000..48d73e1 --- /dev/null +++ b/software/source/clients/meet/app/api/connection-details/route.ts @@ -0,0 +1,81 @@ +import { randomString } from '@/lib/client-utils'; +import { ConnectionDetails } from '@/lib/types'; +import { AccessToken, AccessTokenOptions, VideoGrant } from 'livekit-server-sdk'; +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; + +export async function GET(request: NextRequest) { + try { + // Parse query parameters + const roomName = request.nextUrl.searchParams.get('roomName'); + 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 (livekitServerUrl === undefined) { + throw new Error('Invalid region'); + } + + if (typeof roomName !== 'string') { + return new NextResponse('Missing required query parameter: roomName', { status: 400 }); + } + if (participantName === null) { + return new NextResponse('Missing required query parameter: participantName', { status: 400 }); + } + + // Generate participant token + const participantToken = await createParticipantToken( + { + identity: `${participantName}__${randomString(4)}`, + name: participantName, + metadata, + }, + roomName, + ); + + // Return connection details + const data: ConnectionDetails = { + serverUrl: livekitServerUrl, + roomName: roomName, + participantToken: participantToken, + participantName: participantName, + }; + return NextResponse.json(data); + } catch (error) { + if (error instanceof Error) { + return new NextResponse(error.message, { status: 500 }); + } + } +} + +function createParticipantToken(userInfo: AccessTokenOptions, roomName: string) { + const at = new AccessToken(API_KEY, API_SECRET, userInfo); + at.ttl = '5m'; + const grant: VideoGrant = { + room: roomName, + roomJoin: true, + canPublish: true, + canPublishData: true, + canSubscribe: true, + }; + at.addGrant(grant); + 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; +} diff --git a/software/source/clients/meet/app/api/record/start/route.ts b/software/source/clients/meet/app/api/record/start/route.ts new file mode 100644 index 0000000..dfe88f5 --- /dev/null +++ b/software/source/clients/meet/app/api/record/start/route.ts @@ -0,0 +1,70 @@ +import { EgressClient, EncodedFileOutput, S3Upload } from 'livekit-server-sdk'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + try { + const roomName = req.nextUrl.searchParams.get('roomName'); + + /** + * CAUTION: + * for simplicity this implementation does not authenticate users and therefore allows anyone with knowledge of a roomName + * to start/stop recordings for that room. + * DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS + */ + + if (roomName === null) { + return new NextResponse('Missing roomName parameter', { status: 403 }); + } + + const { + LIVEKIT_API_KEY, + LIVEKIT_API_SECRET, + LIVEKIT_URL, + S3_KEY_ID, + S3_KEY_SECRET, + S3_BUCKET, + S3_ENDPOINT, + S3_REGION, + } = process.env; + + const hostURL = new URL(LIVEKIT_URL!); + hostURL.protocol = 'https:'; + + const egressClient = new EgressClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET); + + const existingEgresses = await egressClient.listEgress({ roomName }); + if (existingEgresses.length > 0 && existingEgresses.some((e) => e.status < 2)) { + return new NextResponse('Meeting is already being recorded', { status: 409 }); + } + + const fileOutput = new EncodedFileOutput({ + filepath: `${new Date(Date.now()).toISOString()}-${roomName}.mp4`, + output: { + case: 's3', + value: new S3Upload({ + endpoint: S3_ENDPOINT, + accessKey: S3_KEY_ID, + secret: S3_KEY_SECRET, + region: S3_REGION, + bucket: S3_BUCKET, + }), + }, + }); + + await egressClient.startRoomCompositeEgress( + roomName, + { + file: fileOutput, + }, + { + layout: 'speaker', + }, + ); + + return new NextResponse(null, { status: 200 }); + } catch (error) { + if (error instanceof Error) { + return new NextResponse(error.message, { status: 500 }); + } + } +} diff --git a/software/source/clients/meet/app/api/record/stop/route.ts b/software/source/clients/meet/app/api/record/stop/route.ts new file mode 100644 index 0000000..e2630ac --- /dev/null +++ b/software/source/clients/meet/app/api/record/stop/route.ts @@ -0,0 +1,39 @@ +import { EgressClient } from 'livekit-server-sdk'; +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + try { + const roomName = req.nextUrl.searchParams.get('roomName'); + + /** + * CAUTION: + * for simplicity this implementation does not authenticate users and therefore allows anyone with knowledge of a roomName + * to start/stop recordings for that room. + * DO NOT USE THIS FOR PRODUCTION PURPOSES AS IS + */ + + if (roomName === null) { + return new NextResponse('Missing roomName parameter', { status: 403 }); + } + + const { LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_URL } = process.env; + + const hostURL = new URL(LIVEKIT_URL!); + hostURL.protocol = 'https:'; + + const egressClient = new EgressClient(hostURL.origin, LIVEKIT_API_KEY, LIVEKIT_API_SECRET); + const activeEgresses = (await egressClient.listEgress({ roomName })).filter( + (info) => info.status < 2, + ); + if (activeEgresses.length === 0) { + return new NextResponse('No active recording found', { status: 404 }); + } + await Promise.all(activeEgresses.map((info) => egressClient.stopEgress(info.egressId))); + + return new NextResponse(null, { status: 200 }); + } catch (error) { + if (error instanceof Error) { + return new NextResponse(error.message, { status: 500 }); + } + } +} diff --git a/software/source/clients/meet/app/components/VideoConference.tsx b/software/source/clients/meet/app/components/VideoConference.tsx new file mode 100644 index 0000000..0e08755 --- /dev/null +++ b/software/source/clients/meet/app/components/VideoConference.tsx @@ -0,0 +1,210 @@ +import type { + MessageDecoder, + MessageEncoder, + TrackReferenceOrPlaceholder, + WidgetState, + } from '@livekit/components-react'; + import { isTrackReference } from '@livekit/components-react'; + import { log } from './logger'; + import { isWeb } from './detectMobileBrowser'; + import { isEqualTrackRef } from './track-reference'; + import { RoomEvent, Track } from 'livekit-client'; + import * as React from 'react'; + import type { MessageFormatter } from '@livekit/components-react'; + import { + CarouselLayout, + ConnectionStateToast, + FocusLayout, + FocusLayoutContainer, + GridLayout, + LayoutContextProvider, + ParticipantTile, + RoomAudioRenderer, + } from '@livekit/components-react'; + import { useCreateLayoutContext } from '@livekit/components-react'; + import { usePinnedTracks, useTracks } from '@livekit/components-react'; + import { ControlBar } from '@livekit/components-react'; + import { useWarnAboutMissingStyles } from './useWarnAboutMissingStyles'; + import { useLocalParticipant } from '@livekit/components-react'; + + /** + * @public + */ + export interface VideoConferenceProps extends React.HTMLAttributes { + chatMessageFormatter?: MessageFormatter; + chatMessageEncoder?: MessageEncoder; + chatMessageDecoder?: MessageDecoder; + /** @alpha */ + SettingsComponent?: React.ComponentType; + } + + /** + * The `VideoConference` ready-made component is your drop-in solution for a classic video conferencing application. + * It provides functionality such as focusing on one participant, grid view with pagination to handle large numbers + * of participants, basic non-persistent chat, screen sharing, and more. + * + * @remarks + * The component is implemented with other LiveKit components like `FocusContextProvider`, + * `GridLayout`, `ControlBar`, `FocusLayoutContainer` and `FocusLayout`. + * You can use these components as a starting point for your own custom video conferencing application. + * + * @example + * ```tsx + * + * + * + * ``` + * @public + */ + export function VideoConference({ + chatMessageFormatter, + chatMessageDecoder, + chatMessageEncoder, + SettingsComponent, + ...props + }: VideoConferenceProps) { + const [widgetState, setWidgetState] = React.useState({ + showChat: false, + unreadMessages: 0, + showSettings: false, + }); + const lastAutoFocusedScreenShareTrack = React.useRef(null); + + const tracks = useTracks( + [ + { source: Track.Source.Camera, withPlaceholder: true }, + { source: Track.Source.ScreenShare, withPlaceholder: false }, + ], + { updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false }, + ); + + const widgetUpdate = (state: WidgetState) => { + log.debug('updating widget state', state); + setWidgetState(state); + }; + + const layoutContext = useCreateLayoutContext(); + + const screenShareTracks = tracks + .filter(isTrackReference) + .filter((track) => track.publication.source === Track.Source.ScreenShare); + + const focusTrack = usePinnedTracks(layoutContext)?.[0]; + const carouselTracks = tracks.filter((track) => !isEqualTrackRef(track, focusTrack)); + + const { localParticipant } = useLocalParticipant(); + + const [isAlwaysListening, setIsAlwaysListening] = React.useState(false); + + const toggleAlwaysListening = () => { + const newValue = !isAlwaysListening; + setIsAlwaysListening(newValue); + handleAlwaysListeningToggle(newValue); + }; + + const handleAlwaysListeningToggle = (newValue: boolean) => { + + if (newValue) { + console.log("SETTING VIDEO CONTEXT ON") + const data = new TextEncoder().encode("{VIDEO_CONTEXT_ON}") + localParticipant.publishData(data, {reliable: true, topic: "video_context"}) + } else { + console.log("SETTING VIDEO CONTEXT OFF") + const data = new TextEncoder().encode("{VIDEO_CONTEXT_OFF}") + localParticipant.publishData(data, {reliable: true, topic: "video_context"}) + } + } + + React.useEffect(() => { + // If screen share tracks are published, and no pin is set explicitly, auto set the screen share. + if ( + screenShareTracks.some((track) => track.publication.isSubscribed) && + lastAutoFocusedScreenShareTrack.current === null + ) { + log.debug('Auto set screen share focus:', { newScreenShareTrack: screenShareTracks[0] }); + layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: screenShareTracks[0] }); + lastAutoFocusedScreenShareTrack.current = screenShareTracks[0]; + } else if ( + lastAutoFocusedScreenShareTrack.current && + !screenShareTracks.some( + (track) => + track.publication.trackSid === + lastAutoFocusedScreenShareTrack.current?.publication?.trackSid, + ) + ) { + log.debug('Auto clearing screen share focus.'); + layoutContext.pin.dispatch?.({ msg: 'clear_pin' }); + lastAutoFocusedScreenShareTrack.current = null; + } + if (focusTrack && !isTrackReference(focusTrack)) { + const updatedFocusTrack = tracks.find( + (tr) => + tr.participant.identity === focusTrack.participant.identity && + tr.source === focusTrack.source, + ); + if (updatedFocusTrack !== focusTrack && isTrackReference(updatedFocusTrack)) { + layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: updatedFocusTrack }); + } + } + }, [ + screenShareTracks + .map((ref) => `${ref.publication.trackSid}_${ref.publication.isSubscribed}`) + .join(), + focusTrack?.publication?.trackSid, + tracks, + ]); + + useWarnAboutMissingStyles(); + + return ( +

+ {isWeb() && ( + +
+ {!focusTrack ? ( +
+ + + +
+ ) : ( +
+ + + + + {focusTrack && } + +
+ )} + + + +
+ {SettingsComponent && ( +
+ +
+ )} +
+ )} + + +
+ ); + } + \ No newline at end of file diff --git a/software/source/clients/meet/app/components/detectMobileBrowser.ts b/software/source/clients/meet/app/components/detectMobileBrowser.ts new file mode 100644 index 0000000..f3ba40c --- /dev/null +++ b/software/source/clients/meet/app/components/detectMobileBrowser.ts @@ -0,0 +1,20 @@ +/** + * @internal + */ +export function isWeb(): boolean { + return typeof document !== 'undefined'; + } + + /** + * Mobile browser detection based on `navigator.userAgent` string. + * Defaults to returning `false` if not in a browser. + * + * @remarks + * This should only be used if feature detection or other methods do not work! + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection + */ + export function isMobileBrowser(): boolean { + return isWeb() ? /Mobi/i.test(window.navigator.userAgent) : false; + } + \ No newline at end of file diff --git a/software/source/clients/meet/app/components/logger.ts b/software/source/clients/meet/app/components/logger.ts new file mode 100644 index 0000000..39c8f9d --- /dev/null +++ b/software/source/clients/meet/app/components/logger.ts @@ -0,0 +1,56 @@ +import { + setLogLevel as setClientSdkLogLevel, + setLogExtension as setClientSdkLogExtension, + LogLevel as LogLevelEnum, + } from 'livekit-client'; + import loglevel from 'loglevel' + + export const log = loglevel.getLogger('lk-components-js'); + log.setDefaultLevel('WARN'); + + type LogLevel = Parameters[0]; + type SetLogLevelOptions = { + liveKitClientLogLevel?: LogLevel; + }; + + /** + * Set the log level for both the `@livekit/components-react` package and the `@livekit-client` package. + * To set the `@livekit-client` log independently, use the `liveKitClientLogLevel` prop on the `options` object. + * @public + */ + export function setLogLevel(level: LogLevel, options: SetLogLevelOptions = {}): void { + log.setLevel(level); + setClientSdkLogLevel(options.liveKitClientLogLevel ?? level); + } + + type LogExtension = (level: LogLevel, msg: string, context?: object) => void; + type SetLogExtensionOptions = { + liveKitClientLogExtension?: LogExtension; + }; + + /** + * Set the log extension for both the `@livekit/components-react` package and the `@livekit-client` package. + * To set the `@livekit-client` log extension, use the `liveKitClientLogExtension` prop on the `options` object. + * @public + */ + export function setLogExtension(extension: LogExtension, options: SetLogExtensionOptions = {}) { + const originalFactory = log.methodFactory; + + log.methodFactory = (methodName, configLevel, loggerName) => { + const rawMethod = originalFactory(methodName, configLevel, loggerName); + + const logLevel = LogLevelEnum[methodName]; + const needLog = logLevel >= configLevel && logLevel < LogLevelEnum.silent; + + return (msg, context?: [msg: string, context: object]) => { + if (context) rawMethod(msg, context); + else rawMethod(msg); + if (needLog) { + extension(logLevel, msg, context); + } + }; + }; + log.setLevel(log.getLevel()); // Be sure to call setLevel method in order to apply plugin + setClientSdkLogExtension(options.liveKitClientLogExtension ?? extension); + } + \ No newline at end of file diff --git a/software/source/clients/meet/app/components/mergeProps.ts b/software/source/clients/meet/app/components/mergeProps.ts new file mode 100644 index 0000000..c1bee7c --- /dev/null +++ b/software/source/clients/meet/app/components/mergeProps.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import clsx from 'clsx'; + +/** + * Calls all functions in the order they were chained with the same arguments. + * @internal + */ +export function chain(...callbacks: any[]): (...args: any[]) => void { + return (...args: any[]) => { + for (const callback of callbacks) { + if (typeof callback === 'function') { + try { + callback(...args); + } catch (e) { + console.error(e); + } + } + } + }; +} + +interface Props { + [key: string]: any; +} + +// taken from: https://stackoverflow.com/questions/51603250/typescript-3-parameter-list-intersection-type/51604379#51604379 +type TupleTypes = { [P in keyof T]: T[P] } extends { [key: number]: infer V } ? V : never; +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never; + +/** + * Merges multiple props objects together. Event handlers are chained, + * classNames are combined, and ids are deduplicated - different ids + * will trigger a side-effect and re-render components hooked up with `useId`. + * For all other props, the last prop object overrides all previous ones. + * @param args - Multiple sets of props to merge together. + * @internal + */ +export function mergeProps(...args: T): UnionToIntersection> { + // Start with a base clone of the first argument. This is a lot faster than starting + // with an empty object and adding properties as we go. + const result: Props = { ...args[0] }; + for (let i = 1; i < args.length; i++) { + const props = args[i]; + for (const key in props) { + const a = result[key]; + const b = props[key]; + + // Chain events + if ( + typeof a === 'function' && + typeof b === 'function' && + // This is a lot faster than a regex. + key[0] === 'o' && + key[1] === 'n' && + key.charCodeAt(2) >= /* 'A' */ 65 && + key.charCodeAt(2) <= /* 'Z' */ 90 + ) { + result[key] = chain(a, b); + + // Merge classnames, sometimes classNames are empty string which eval to false, so we just need to do a type check + } else if ( + (key === 'className' || key === 'UNSAFE_className') && + typeof a === 'string' && + typeof b === 'string' + ) { + result[key] = clsx(a, b); + } else { + result[key] = b !== undefined ? b : a; + } + } + } + + return result as UnionToIntersection>; +} diff --git a/software/source/clients/meet/app/components/track-reference-types.ts b/software/source/clients/meet/app/components/track-reference-types.ts new file mode 100644 index 0000000..94f6db5 --- /dev/null +++ b/software/source/clients/meet/app/components/track-reference-types.ts @@ -0,0 +1,73 @@ +/** + * The TrackReference type is a logical grouping of participant publication and/or subscribed track. + * + */ + +import type { Participant, Track, TrackPublication } from 'livekit-client'; +// ## TrackReference Types + +/** @public */ +export type TrackReferencePlaceholder = { + participant: Participant; + publication?: never; + source: Track.Source; +}; + +/** @public */ +export type TrackReference = { + participant: Participant; + publication: TrackPublication; + source: Track.Source; +}; + +/** @public */ +export type TrackReferenceOrPlaceholder = TrackReference | TrackReferencePlaceholder; + +// ### TrackReference Type Predicates +/** @internal */ +export function isTrackReference(trackReference: unknown): trackReference is TrackReference { + if (typeof trackReference === 'undefined') { + return false; + } + return ( + isTrackReferenceSubscribed(trackReference as TrackReference) || + isTrackReferencePublished(trackReference as TrackReference) + ); +} + +function isTrackReferenceSubscribed(trackReference?: TrackReferenceOrPlaceholder): boolean { + if (!trackReference) { + return false; + } + return ( + trackReference.hasOwnProperty('participant') && + trackReference.hasOwnProperty('source') && + trackReference.hasOwnProperty('track') && + typeof trackReference.publication?.track !== 'undefined' + ); +} + +function isTrackReferencePublished(trackReference?: TrackReferenceOrPlaceholder): boolean { + if (!trackReference) { + return false; + } + return ( + trackReference.hasOwnProperty('participant') && + trackReference.hasOwnProperty('source') && + trackReference.hasOwnProperty('publication') && + typeof trackReference.publication !== 'undefined' + ); +} + +export function isTrackReferencePlaceholder( + trackReference?: TrackReferenceOrPlaceholder, +): trackReference is TrackReferencePlaceholder { + if (!trackReference) { + return false; + } + return ( + trackReference.hasOwnProperty('participant') && + trackReference.hasOwnProperty('source') && + typeof trackReference.publication === 'undefined' + ); +} diff --git a/software/source/clients/meet/app/components/track-reference.ts b/software/source/clients/meet/app/components/track-reference.ts new file mode 100644 index 0000000..ea7d28b --- /dev/null +++ b/software/source/clients/meet/app/components/track-reference.ts @@ -0,0 +1,97 @@ +import type { Track } from 'livekit-client'; +import type { PinState } from './types'; +import type { TrackReferenceOrPlaceholder } from './track-reference-types'; +import { isTrackReference, isTrackReferencePlaceholder } from './track-reference-types'; + +/** + * Returns a id to identify the `TrackReference` or `TrackReferencePlaceholder` based on + * participant, track source and trackSid. + * @remarks + * The id pattern is: `${participantIdentity}_${trackSource}_${trackSid}` for `TrackReference` + * and `${participantIdentity}_${trackSource}_placeholder` for `TrackReferencePlaceholder`. + */ +export function getTrackReferenceId(trackReference: TrackReferenceOrPlaceholder | number) { + if (typeof trackReference === 'string' || typeof trackReference === 'number') { + return `${trackReference}`; + } else if (isTrackReferencePlaceholder(trackReference)) { + return `${trackReference.participant.identity}_${trackReference.source}_placeholder`; + } else if (isTrackReference(trackReference)) { + return `${trackReference.participant.identity}_${trackReference.publication.source}_${trackReference.publication.trackSid}`; + } else { + throw new Error(`Can't generate a id for the given track reference: ${trackReference}`); + } +} + +export type TrackReferenceId = ReturnType; + +/** Returns the Source of the TrackReference. */ +export function getTrackReferenceSource(trackReference: TrackReferenceOrPlaceholder): Track.Source { + if (isTrackReference(trackReference)) { + return trackReference.publication.source; + } else { + return trackReference.source; + } +} + +export function isEqualTrackRef( + a?: TrackReferenceOrPlaceholder, + b?: TrackReferenceOrPlaceholder, +): boolean { + if (a === undefined || b === undefined) { + return false; + } + if (isTrackReference(a) && isTrackReference(b)) { + return a.publication.trackSid === b.publication.trackSid; + } else { + return getTrackReferenceId(a) === getTrackReferenceId(b); + } +} + +/** + * Check if the `TrackReference` is pinned. + */ +export function isTrackReferencePinned( + trackReference: TrackReferenceOrPlaceholder, + pinState: PinState | undefined, +): boolean { + if (typeof pinState === 'undefined') { + return false; + } + if (isTrackReference(trackReference)) { + return pinState.some( + (pinnedTrackReference) => + pinnedTrackReference.participant.identity === trackReference.participant.identity && + isTrackReference(pinnedTrackReference) && + pinnedTrackReference.publication.trackSid === trackReference.publication.trackSid, + ); + } else if (isTrackReferencePlaceholder(trackReference)) { + return pinState.some( + (pinnedTrackReference) => + pinnedTrackReference.participant.identity === trackReference.participant.identity && + isTrackReferencePlaceholder(pinnedTrackReference) && + pinnedTrackReference.source === trackReference.source, + ); + } else { + return false; + } +} + +/** + * Check if the current `currentTrackRef` is the placeholder for next `nextTrackRef`. + * Based on the participant identity and the source. + * @internal + */ +export function isPlaceholderReplacement( + currentTrackRef: TrackReferenceOrPlaceholder, + nextTrackRef: TrackReferenceOrPlaceholder, +) { + // if (typeof nextTrackRef === 'number' || typeof currentTrackRef === 'number') { + // return false; + // } + return ( + isTrackReferencePlaceholder(currentTrackRef) && + isTrackReference(nextTrackRef) && + nextTrackRef.participant.identity === currentTrackRef.participant.identity && + nextTrackRef.source === currentTrackRef.source + ); +} diff --git a/software/source/clients/meet/app/components/types.ts b/software/source/clients/meet/app/components/types.ts new file mode 100644 index 0000000..00d9e09 --- /dev/null +++ b/software/source/clients/meet/app/components/types.ts @@ -0,0 +1,90 @@ +import type { Participant, ParticipantKind, Track, TrackPublication } from 'livekit-client'; +import type { TrackReference, TrackReferenceOrPlaceholder } from './track-reference-types'; + +// ## PinState Type +/** @public */ +export type PinState = TrackReferenceOrPlaceholder[]; +export const PIN_DEFAULT_STATE: PinState = []; + +// ## WidgetState Types +/** @public */ +export type WidgetState = { + showChat: boolean; + unreadMessages: number; + showSettings?: boolean; +}; +export const WIDGET_DEFAULT_STATE: WidgetState = { + showChat: false, + unreadMessages: 0, + showSettings: false, +}; + +// ## Track Source Types +export type TrackSourceWithOptions = { source: Track.Source; withPlaceholder: boolean }; + +export type SourcesArray = Track.Source[] | TrackSourceWithOptions[]; + +// ### Track Source Type Predicates +export function isSourceWitOptions(source: SourcesArray[number]): source is TrackSourceWithOptions { + return typeof source === 'object'; +} + +export function isSourcesWithOptions(sources: SourcesArray): sources is TrackSourceWithOptions[] { + return ( + Array.isArray(sources) && + (sources as TrackSourceWithOptions[]).filter(isSourceWitOptions).length > 0 + ); +} + +// ## Loop Filter Types +export type TrackReferenceFilter = Parameters['0']; +export type ParticipantFilter = Parameters['0']; + +// ## Other Types +/** @internal */ +export interface ParticipantClickEvent { + participant: Participant; + track?: TrackPublication; +} + +export type TrackSource = RequireAtLeastOne< + { source: T; name: string; participant: Participant }, + 'name' | 'source' +>; + +export type ParticipantTrackIdentifier = RequireAtLeastOne< + { sources: Track.Source[]; name: string; kind: Track.Kind }, + 'sources' | 'name' | 'kind' +>; + +/** + * @beta + */ +export type ParticipantIdentifier = RequireAtLeastOne< + { kind: ParticipantKind; identity: string }, + 'identity' | 'kind' +>; + +/** + * The TrackIdentifier type is used to select Tracks either based on + * - Track.Source and/or name of the track, e.g. `{source: Track.Source.Camera}` or `{name: "my-track"}` + * - TrackReference (participant and publication) + * @internal + */ +export type TrackIdentifier = + | TrackSource + | TrackReference; + +// ## Util Types +type RequireAtLeastOne = Pick> & + { + [K in Keys]-?: Required> & Partial>>; + }[Keys]; + +export type RequireOnlyOne = Pick> & + { + [K in Keys]-?: Required> & Partial, undefined>>; + }[Keys]; + +export type AudioSource = Track.Source.Microphone | Track.Source.ScreenShareAudio; +export type VideoSource = Track.Source.Camera | Track.Source.ScreenShare; diff --git a/software/source/clients/meet/app/components/useWarnAboutMissingStyles.ts b/software/source/clients/meet/app/components/useWarnAboutMissingStyles.ts new file mode 100644 index 0000000..623cbec --- /dev/null +++ b/software/source/clients/meet/app/components/useWarnAboutMissingStyles.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { warnAboutMissingStyles } from './utils'; + +/** + * @internal + */ +export function useWarnAboutMissingStyles() { + React.useEffect(() => { + warnAboutMissingStyles(); + }, []); +} diff --git a/software/source/clients/meet/app/components/utils.ts b/software/source/clients/meet/app/components/utils.ts new file mode 100644 index 0000000..e1b2d6f --- /dev/null +++ b/software/source/clients/meet/app/components/utils.ts @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { mergeProps as mergePropsReactAria } from './mergeProps' +import { log } from './logger'; +import clsx from 'clsx'; + +/** @internal */ +export function isProp>( + prop: T | undefined, +): prop is T { + return prop !== undefined; +} + +/** @internal */ +export function mergeProps< + U extends HTMLElement, + T extends Array | undefined>, +>(...props: T) { + return mergePropsReactAria(...props.filter(isProp)); +} + +/** @internal */ +export function cloneSingleChild( + children: React.ReactNode | React.ReactNode[], + props?: Record, + key?: any, +) { + return React.Children.map(children, (child) => { + // Checking isValidElement is the safe way and avoids a typescript + // error too. + if (React.isValidElement(child) && React.Children.only(children)) { + if (child.props.class) { + // make sure we retain classnames of both passed props and child + props ??= {}; + props.class = clsx(child.props.class, props.class); + props.style = { ...child.props.style, ...props.style }; + } + return React.cloneElement(child, { ...props, key }); + } + return child; + }); +} + +/** + * @internal + */ +export function warnAboutMissingStyles(el?: HTMLElement) { + if ( + typeof window !== 'undefined' && + typeof process !== 'undefined' && + // eslint-disable-next-line turbo/no-undeclared-env-vars + (process?.env?.NODE_ENV === 'dev' || + // eslint-disable-next-line turbo/no-undeclared-env-vars + process?.env?.NODE_ENV === 'development') + ) { + const target = el ?? document.querySelector('.lk-room-container'); + if (target && !getComputedStyle(target).getPropertyValue('--lk-has-imported-styles')) { + log.warn( + "It looks like you're not using the `@livekit/components-styles package`. To render the UI with the default styling, please import it in your layout or page.", + ); + } + } +} diff --git a/software/source/clients/meet/app/custom/VideoConferenceClientImpl.tsx b/software/source/clients/meet/app/custom/VideoConferenceClientImpl.tsx new file mode 100644 index 0000000..4dbedc3 --- /dev/null +++ b/software/source/clients/meet/app/custom/VideoConferenceClientImpl.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { formatChatMessageLinks, LiveKitRoom } from '@livekit/components-react'; +import { + ExternalE2EEKeyProvider, + LogLevel, + Room, + RoomConnectOptions, + RoomOptions, + VideoPresets, + type VideoCodec, +} from 'livekit-client'; +import { DebugMode } from '@/lib/Debug'; +import { useMemo } from 'react'; +import { decodePassphrase } from '@/lib/client-utils'; +import { SettingsMenu } from '@/lib/SettingsMenu'; +import { VideoConference } from '../components/VideoConference' + +export function VideoConferenceClientImpl(props: { + liveKitUrl: string; + token: string; + codec: VideoCodec | 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 e2eeEnabled = !!(e2eePassphrase && worker); + const roomOptions = useMemo((): RoomOptions => { + return { + publishDefaults: { + videoSimulcastLayers: [VideoPresets.h540, VideoPresets.h216], + red: !e2eeEnabled, + videoCodec: props.codec, + }, + adaptiveStream: { pixelDensity: 'screen' }, + dynacast: true, + e2ee: e2eeEnabled + ? { + keyProvider, + worker, + } + : undefined, + }; + }, []); + + const room = useMemo(() => new Room(roomOptions), []); + if (e2eeEnabled) { + keyProvider.setKey(e2eePassphrase); + room.setE2EEEnabled(true); + } + const connectOptions = useMemo((): RoomConnectOptions => { + return { + autoSubscribe: true, + }; + }, []); + + return ( + + + + + ); +} diff --git a/software/source/clients/meet/app/custom/page.tsx b/software/source/clients/meet/app/custom/page.tsx new file mode 100644 index 0000000..3b78a75 --- /dev/null +++ b/software/source/clients/meet/app/custom/page.tsx @@ -0,0 +1,28 @@ +import { videoCodecs } from 'livekit-client'; +import { VideoConferenceClientImpl } from './VideoConferenceClientImpl'; +import { isVideoCodec } from '@/lib/types'; + +export default function CustomRoomConnection(props: { + searchParams: { + liveKitUrl?: string; + token?: string; + codec?: string; + }; +}) { + const { liveKitUrl, token, codec } = props.searchParams; + if (typeof liveKitUrl !== 'string') { + return

Missing LiveKit URL

; + } + if (typeof token !== 'string') { + return

Missing LiveKit token

; + } + if (codec !== undefined && !isVideoCodec(codec)) { + return

Invalid codec, if defined it has to be [{videoCodecs.join(', ')}].

; + } + + return ( +
+ +
+ ); +} diff --git a/software/source/clients/meet/app/layout.tsx b/software/source/clients/meet/app/layout.tsx new file mode 100644 index 0000000..bf35374 --- /dev/null +++ b/software/source/clients/meet/app/layout.tsx @@ -0,0 +1,56 @@ +import '../styles/globals.css'; +import '@livekit/components-styles'; +import '@livekit/components-styles/prefabs'; +import type { Metadata, Viewport } from 'next'; + +export const metadata: Metadata = { + title: { + default: 'LiveKit Meet | Conference app build with LiveKit open source', + template: '%s', + }, + description: + 'LiveKit is an open source WebRTC project that gives you everything needed to build scalable and real-time audio and/or video experiences in your applications.', + twitter: { + creator: '@livekitted', + site: '@livekitted', + card: 'summary_large_image', + }, + openGraph: { + url: 'https://meet.livekit.io', + images: [ + { + url: 'https://meet.livekit.io/images/livekit-meet-open-graph.png', + width: 2000, + height: 1000, + type: 'image/png', + }, + ], + siteName: 'LiveKit Meet', + }, + icons: { + icon: { + rel: 'icon', + url: '/favicon.ico', + }, + apple: [ + { + rel: 'apple-touch-icon', + url: '/images/livekit-apple-touch.png', + sizes: '180x180', + }, + { rel: 'mask-icon', url: '/images/livekit-safari-pinned-tab.svg', color: '#070707' }, + ], + }, +}; + +export const viewport: Viewport = { + themeColor: '#070707', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/software/source/clients/meet/app/page.tsx b/software/source/clients/meet/app/page.tsx new file mode 100644 index 0000000..d23d536 --- /dev/null +++ b/software/source/clients/meet/app/page.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import React, { Suspense, useState } from 'react'; +import { encodePassphrase, generateRoomId, randomString } from '@/lib/client-utils'; +import styles from '../styles/Home.module.css'; + +function Tabs(props: React.PropsWithChildren<{}>) { + const searchParams = useSearchParams(); + const tabIndex = searchParams?.get('tab') === 'custom' ? 1 : 0; + + const router = useRouter(); + function onTabSelected(index: number) { + const tab = index === 1 ? 'custom' : 'demo'; + router.push(`/?tab=${tab}`); + } + + let tabs = React.Children.map(props.children, (child, index) => { + return ( + + ); + }); + + return ( +
+
{tabs}
+ {/* @ts-ignore */} + {props.children[tabIndex]} +
+ ); +} + +function DemoMeetingTab(props: { label: string }) { + const router = useRouter(); + const [e2ee, setE2ee] = useState(false); + const [sharedPassphrase, setSharedPassphrase] = useState(randomString(64)); + const startMeeting = () => { + if (e2ee) { + router.push(`/rooms/${generateRoomId()}#${encodePassphrase(sharedPassphrase)}`); + } else { + router.push(`/rooms/${generateRoomId()}`); + } + }; + return ( +
+

Try LiveKit Meet for free with our live demo project.

+ +
+
+ setE2ee(ev.target.checked)} + > + +
+ {e2ee && ( +
+ + setSharedPassphrase(ev.target.value)} + /> +
+ )} +
+
+ ); +} + +function CustomConnectionTab(props: { label: string }) { + const router = useRouter(); + + const [e2ee, setE2ee] = useState(false); + const [sharedPassphrase, setSharedPassphrase] = useState(randomString(64)); + + const onSubmit: React.FormEventHandler = (event) => { + event.preventDefault(); + const formData = new FormData(event.target as HTMLFormElement); + const serverUrl = formData.get('serverUrl'); + const token = formData.get('token'); + if (e2ee) { + router.push( + `/custom/?liveKitUrl=${serverUrl}&token=${token}#${encodePassphrase(sharedPassphrase)}`, + ); + } else { + router.push(`/custom/?liveKitUrl=${serverUrl}&token=${token}`); + } + }; + return ( +
+

+ Connect LiveKit Meet with a custom server using LiveKit Cloud or LiveKit Server. +

+ +