From c25b0fc713dd789685a832918a5b294045b54641 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 24 Oct 2022 17:03:12 +0200 Subject: [PATCH] Initial public release --- .env.example | 9 + .eslintrc.json | 3 + .gitignore | 38 + .prettierignore | 3 + .prettierrc | 7 + README.md | 22 + components/ActiveRoom.tsx | 140 ++ components/ChatEntry.tsx | 19 + components/ChatOverlay.tsx | 113 + components/Controls.tsx | 120 + components/DebugOverlay.tsx | 237 ++ components/PreJoin.tsx | 216 ++ components/TextField.tsx | 66 + lib/clients.ts | 27 + lib/types.ts | 16 + next-env.d.ts | 5 + next.config.js | 6 + package.json | 35 + pages/_app.tsx | 16 + pages/api/token.ts | 40 + pages/index.tsx | 48 + pages/rooms/[name].tsx | 109 + public/favicon.ico | Bin 0 -> 1150 bytes public/fonts/avenir-pro-black.woff | Bin 0 -> 26919 bytes public/fonts/avenir-pro-black.woff2 | Bin 0 -> 26240 bytes public/fonts/avenir-pro-heavy.woff | Bin 0 -> 27043 bytes public/fonts/avenir-pro-heavy.woff2 | Bin 0 -> 26268 bytes public/fonts/avenir-pro-medium.woff | Bin 0 -> 26611 bytes public/fonts/avenir-pro-medium.woff2 | Bin 0 -> 25804 bytes public/fonts/avenir-pro-roman.woff | Bin 0 -> 26551 bytes public/fonts/avenir-pro-roman.woff2 | Bin 0 -> 25724 bytes public/livekit.svg | 16 + styles/Home.module.css | 12 + styles/Room.module.css | 34 + styles/globals.css | 11 + tsconfig.json | 20 + yarn.lock | 3223 ++++++++++++++++++++++++++ 37 files changed, 4611 insertions(+) create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 components/ActiveRoom.tsx create mode 100644 components/ChatEntry.tsx create mode 100644 components/ChatOverlay.tsx create mode 100644 components/Controls.tsx create mode 100644 components/DebugOverlay.tsx create mode 100644 components/PreJoin.tsx create mode 100644 components/TextField.tsx create mode 100644 lib/clients.ts create mode 100644 lib/types.ts create mode 100644 next-env.d.ts create mode 100644 next.config.js create mode 100644 package.json create mode 100644 pages/_app.tsx create mode 100644 pages/api/token.ts create mode 100644 pages/index.tsx create mode 100644 pages/rooms/[name].tsx create mode 100644 public/favicon.ico create mode 100644 public/fonts/avenir-pro-black.woff create mode 100644 public/fonts/avenir-pro-black.woff2 create mode 100644 public/fonts/avenir-pro-heavy.woff create mode 100644 public/fonts/avenir-pro-heavy.woff2 create mode 100644 public/fonts/avenir-pro-medium.woff create mode 100644 public/fonts/avenir-pro-medium.woff2 create mode 100644 public/fonts/avenir-pro-roman.woff create mode 100644 public/fonts/avenir-pro-roman.woff2 create mode 100644 public/livekit.svg create mode 100644 styles/Home.module.css create mode 100644 styles/Room.module.css create mode 100644 styles/globals.css create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4bae6ab --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# 1. Copy this file and rename it to .env.local +# 2. Update the enviroment variables below. + +# URL pointing to the LiveKit server. +LIVEKIT_URL=wss://your-host + +# API key and secret. If you use LiveKit Cloud this can be generated via the cloud dashboard. +LIVEKIT_API_KEY=<____key_goes_here____> +LIVEKIT_API_SECRET=<____secret_goes_here____> \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d093c3 --- /dev/null +++ b/.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/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fb6e24e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +.github/ +.next/ +node_modules/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4148c21 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "tabWidth": 2, + "printWidth": 100 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f3b09b --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# LiveKit Meet + +This project is home for a simple video conferencing app built with LiveKit. + +## Tech Stack + +- This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +- App is built with [livekit-react](https://github.com/livekit/livekit-react/) library + +## Demo + +Give it a try at https://meet.livekit.io + +## Dev Setup + +Steps to get a local dev setup up and running: + +1. Run `yarn install` to install all dependencies. +2. Copy `.env.example` in the project root and rename it to `.env.local`. +3. Update the missing environment variables in the newly created `.env.local` file. +4. Run `yarn dev` to start the development server and visit [http://localhost:3000](http://localhost:3000) to see the result. +5. Start development 🎉 diff --git a/components/ActiveRoom.tsx b/components/ActiveRoom.tsx new file mode 100644 index 0000000..18166c3 --- /dev/null +++ b/components/ActiveRoom.tsx @@ -0,0 +1,140 @@ +import { Box, useToast } from '@chakra-ui/react'; +import { DisplayContext, DisplayOptions, LiveKitRoom } from '@livekit/react-components'; +import { Room, RoomEvent, VideoPresets } from 'livekit-client'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; +import 'react-aspect-ratio/aspect-ratio.css'; +import tinykeys from 'tinykeys'; +import { SessionProps, TokenResult } from '../lib/types'; +import Controls from './Controls'; +import DebugOverlay from './DebugOverlay'; + +const ActiveRoom = ({ + roomName, + identity, + region, + audioTrack, + videoTrack, + turnServer, + forceRelay, +}: SessionProps) => { + const [tokenResult, setTokenResult] = useState(); + const [room, setRoom] = useState(); + const [displayOptions, setDisplayOptions] = useState({ + stageLayout: 'grid', + }); + const router = useRouter(); + const toast = useToast(); + + useEffect(() => { + // cleanup + return () => { + audioTrack?.stop(); + videoTrack?.stop(); + }; + }, []); + + const onLeave = () => { + router.push('/'); + }; + + const onConnected = useCallback( + (room: Room) => { + setRoom(room); + /* @ts-ignore */ + window.currentRoom = room; + if (audioTrack) { + room.localParticipant.publishTrack(audioTrack); + } + if (videoTrack) { + room.localParticipant.publishTrack(videoTrack); + } + room.on(RoomEvent.Disconnected, (reason) => { + toast({ + title: 'Disconnected', + description: `You've been disconnected from the room`, + duration: 4000, + onCloseComplete: () => { + onLeave(); + }, + }); + }); + }, + [audioTrack, videoTrack], + ); + + useEffect(() => { + const params: { [key: string]: string } = { + roomName, + identity, + }; + if (region) { + params.region = region; + } + fetch('/api/token?' + new URLSearchParams(params)) + .then((res) => res.json()) + .then((data: TokenResult) => { + setTokenResult(data); + }); + }, []); + + useEffect(() => { + if (window) { + let unsubscribe = tinykeys(window, { + 'Shift+S': () => { + displayOptions.showStats = displayOptions.showStats ? false : true; + setDisplayOptions(displayOptions); + }, + }); + return () => { + unsubscribe(); + }; + } + }, [displayOptions]); + + if (!tokenResult) { + return ; + } + + let rtcConfig: RTCConfiguration | undefined; + if (turnServer) { + rtcConfig = { + iceServers: [turnServer], + iceTransportPolicy: 'relay', + }; + } else if (forceRelay) { + rtcConfig = { + iceTransportPolicy: 'relay', + }; + } + + return ( + + + + {room && } + + + ); +}; + +export default ActiveRoom; diff --git a/components/ChatEntry.tsx b/components/ChatEntry.tsx new file mode 100644 index 0000000..50be0be --- /dev/null +++ b/components/ChatEntry.tsx @@ -0,0 +1,19 @@ +import { HStack, Text } from '@chakra-ui/react'; +import { Participant } from 'livekit-client'; + +export interface ChatData { + sentAt: Date; + message: string; + from?: Participant; +} + +const ChatEntry = ({ message, from }: ChatData) => { + return ( + + {from ? {`${from.name || from.identity}`}: : null} + {message} + + ); +}; + +export default ChatEntry; diff --git a/components/ChatOverlay.tsx b/components/ChatOverlay.tsx new file mode 100644 index 0000000..f252a39 --- /dev/null +++ b/components/ChatOverlay.tsx @@ -0,0 +1,113 @@ +import { + Button, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + Grid, + GridItem, + HStack, + Textarea, + useDisclosure, +} from '@chakra-ui/react'; +import { DataPacket_Kind, Participant, Room, RoomEvent } from 'livekit-client'; +import { useEffect, useState } from 'react'; +import ChatEntry, { ChatData } from './ChatEntry'; + +interface ChatProps { + room: Room; + isOpen: boolean; + onClose: () => void; + onUnreadChanged?: (num: number) => void; +} + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +const ChatOverlay = ({ + room, + isOpen: extIsOpen, + onClose: extOnClose, + onUnreadChanged, +}: ChatProps) => { + const { isOpen, onClose } = useDisclosure({ isOpen: extIsOpen, onClose: extOnClose }); + const [input, setInput] = useState(); + const [messages, setMessages] = useState([]); + const [numUnread, setNumUnread] = useState(0); + + useEffect(() => { + const onDataReceived = (payload: Uint8Array, participant?: Participant) => { + const data = decoder.decode(payload); + setMessages((messages) => [ + ...messages, + { + sentAt: new Date(), + message: data, + from: participant, + }, + ]); + setNumUnread((numUnread) => numUnread + 1); + }; + room.on(RoomEvent.DataReceived, onDataReceived); + return () => { + room.off(RoomEvent.DataReceived, onDataReceived); + }; + }, [room]); + + useEffect(() => { + if (isOpen) { + setNumUnread(0); + } + }, [isOpen]); + + useEffect(() => { + if (onUnreadChanged) { + onUnreadChanged(numUnread); + } + }, [numUnread, onUnreadChanged]); + + const sendMessage = () => { + if (!input) { + return; + } + room.localParticipant.publishData(encoder.encode(input), DataPacket_Kind.RELIABLE); + setMessages((messages) => [ + ...messages, + { + sentAt: new Date(), + message: input, + from: room.localParticipant, + }, + ]); + setInput(''); + }; + + return ( + + + + + Chat + + + + {messages.map((message, idx) => ( + + ))} + + + +