feat: add <PreJoinView /> for voice channel

pull/105/merge
moonrailgun 2 years ago
parent e828c4157b
commit 79a5b76ba4

@ -7,6 +7,9 @@ const PLUGIN_NAME = 'Markdown Panel';
console.log(`Plugin ${PLUGIN_NAME}(${PLUGIN_ID}) is loaded`);
/**
* @note PLUGIN_ID PLUGIN_NAME,
*/
regGroupPanel({
name: `${PLUGIN_NAME}/customwebpanel`,
label: Translate.name,

@ -35,6 +35,7 @@ export { Link } from 'react-router-dom';
export { MessageAckContainer } from '@/components/ChatBox/ChatMessageList/MessageAckContainer';
export { BaseChatInputButton } from '@/components/ChatBox/ChatInputBox/BaseChatInputButton';
export { useChatInputActionContext } from '@/components/ChatBox/ChatInputBox/context';
export { GroupPanelContainer } from '@/components/Panel/group/shared/GroupPanelContainer';
export { GroupExtraDataPanel } from '@/components/Panel/group/GroupExtraDataPanel';
export { Image } from '@/components/Image';
export { IconBtn } from '@/components/IconBtn';

@ -153,7 +153,9 @@ declare module '@capital/common' {
deps?: React.DependencyList
) => [{ loading: boolean; value?: any }, T];
export const useEvent: any;
export const useEvent: <T extends (this: any, ...args: any[]) => any>(
fn: T
) => T;
export const uploadFile: any;
@ -204,7 +206,12 @@ declare module '@capital/common' {
export const getTextColorHex: any;
export const useCurrentUserInfo: any;
export const useCurrentUserInfo: () => {
email?: string;
nickname?: string;
discriminator: string;
avatar?: string;
};
export const createPluginRequest: (pluginName: string) => {
get: (actionName: string, config?: any) => Promise<any>;

@ -1939,6 +1939,44 @@ importers:
specifier: ^5.0.0
version: 5.0.0
server/plugins/com.msgbyte.livekit:
dependencies:
tailchat-server-sdk:
specifier: '*'
version: link:../../packages/sdk
devDependencies:
'@types/react':
specifier: 18.0.20
version: 18.0.20
mini-star:
specifier: '*'
version: 1.3.1
server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit:
dependencies:
'@livekit/components-core':
specifier: ^0.6.11
version: 0.6.11(livekit-client@1.12.1)
'@livekit/components-react':
specifier: ^1.0.8
version: 1.0.8(livekit-client@1.12.1)(react-dom@18.2.0)(react@18.2.0)
'@livekit/components-styles':
specifier: ^1.0.4
version: 1.0.4
livekit-client:
specifier: ^1.12.1
version: 1.12.1
devDependencies:
'@types/styled-components':
specifier: ^5.1.26
version: 5.1.26
react:
specifier: 18.2.0
version: 18.2.0
styled-components:
specifier: ^5.3.6
version: 5.3.10(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
server/plugins/com.msgbyte.meeting:
devDependencies:
less:
@ -6711,6 +6749,16 @@ packages:
text-decoding: 1.0.0
dev: false
/@floating-ui/core@1.3.1:
resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==}
dev: false
/@floating-ui/dom@1.4.5:
resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==}
dependencies:
'@floating-ui/core': 1.3.1
dev: false
/@gar/promisify@1.1.3:
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
dev: true
@ -7126,6 +7174,41 @@ packages:
engines: {node: '>=10.0.0'}
dev: false
/@livekit/components-core@0.6.11(livekit-client@1.12.1):
resolution: {integrity: sha512-FufCqJ/G0rznKR4hCY1EM+9GtwGK+easNgOPw4WPvhFEOO4L00KuEYWpzbWBzVFBHYqBuypkZS4enOsSHwPAJQ==}
engines: {node: '>=14'}
peerDependencies:
livekit-client: ^1.12.0
dependencies:
'@floating-ui/dom': 1.4.5
email-regex: 5.0.0
global-tld-list: 0.0.1139
livekit-client: 1.12.1
loglevel: 1.8.1
rxjs: 7.8.0
dev: false
/@livekit/components-react@1.0.8(livekit-client@1.12.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-JOGcoLwOXI2vQW0U7Zwh+DXIoC/Nm9/yOg7U6tAv6+Sps/BlAjyY09XVnI6GBLeJpiaqzlY2ualmfMKiWDgDhw==}
engines: {node: '>=14'}
peerDependencies:
livekit-client: ^1.12.0
react: '>=18'
react-dom: '>=18'
dependencies:
'@livekit/components-core': 0.6.11(livekit-client@1.12.1)
'@react-hook/latest': 1.0.3(react@18.2.0)
clsx: 1.2.1
livekit-client: 1.12.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@livekit/components-styles@1.0.4:
resolution: {integrity: sha512-bLCj+asyZLKtviKK+wU2WJ/pe0K/QSkzolXAOKkVedojs2Yc4e6i1tjOr0rTRSJxl4opYrZK9Ye89HLq6J04fA==}
engines: {node: '>=14'}
dev: false
/@loadable/component@5.15.3(react@18.2.0):
resolution: {integrity: sha512-VOgYgCABn6+/7aGIpg7m0Ruj34tGetaJzt4bQ345FwEovDQZ+dua+NWLmuJKv8rWZyxOUSfoJkmGnzyDXH2BAQ==}
engines: {node: '>=8'}
@ -7961,6 +8044,49 @@ packages:
- supports-color
dev: false
/@protobufjs/aspromise@1.1.2:
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
dev: false
/@protobufjs/base64@1.1.2:
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
dev: false
/@protobufjs/codegen@2.0.4:
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
dev: false
/@protobufjs/eventemitter@1.1.0:
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
dev: false
/@protobufjs/fetch@1.1.0:
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/inquire': 1.1.0
dev: false
/@protobufjs/float@1.0.2:
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
dev: false
/@protobufjs/inquire@1.1.0:
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
dev: false
/@protobufjs/path@1.1.2:
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
dev: false
/@protobufjs/pool@1.1.0:
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
dev: false
/@protobufjs/utf8@1.1.0:
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
dev: false
/@rc-component/portal@1.1.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-tbXM9SB1r5FOuZjRCljERFByFiEUcMmCWMXLog/NmgCzlAzreXyf23Vei3ZpSMxSMavzPnhCovfZjZdmxS3d1w==}
engines: {node: '>=8.x'}
@ -8011,6 +8137,14 @@ packages:
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
dev: false
/@react-hook/latest@1.0.3(react@18.2.0):
resolution: {integrity: sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==}
peerDependencies:
react: '>=16.8'
dependencies:
react: 18.2.0
dev: false
/@reduxjs/toolkit@1.8.5(react-redux@8.0.2)(react@18.2.0):
resolution: {integrity: sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==}
peerDependencies:
@ -17369,6 +17503,23 @@ packages:
/electron-to-chromium@1.4.310:
resolution: {integrity: sha512-/xlATgfwkm5uDDwLw5nt/MNEf7c1oazLURMZLy39vOioGYyYzLWIDT8fZMJak6qTiAJ7udFTy7JG7ziyjNutiA==}
/elliptic@6.5.4:
resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==}
dependencies:
bn.js: 4.12.0
brorand: 1.1.0
hash.js: 1.1.7
hmac-drbg: 1.0.1
inherits: 2.0.4
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
dev: false
/email-regex@5.0.0:
resolution: {integrity: sha512-he76Cm8JFxb6OGQHabLBPdsiStgPmJeAEhctmw0uhonUh1pCBsHpI6/rB62s2GNzjBb0YlhIcF/1l9Lp5AfH0Q==}
engines: {node: '>=12'}
dev: false
/emittery@0.8.1:
resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==}
engines: {node: '>=10'}
@ -18269,6 +18420,10 @@ packages:
/eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
/eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
dev: false
/events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@ -19591,6 +19746,10 @@ packages:
which: 1.3.1
dev: false
/global-tld-list@0.0.1139:
resolution: {integrity: sha512-TCWjAwHPzFV6zbQ5jnJvJTctesHGJr9BppxivRuIxTiIFUzaxy1F0674cxjoJecW5s8V32Q5i35dBFqvAy7eGQ==}
dev: false
/global@4.4.0:
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
dependencies:
@ -22758,6 +22917,17 @@ packages:
wrap-ansi: 7.0.0
dev: true
/livekit-client@1.12.1:
resolution: {integrity: sha512-/mob04a/Mb0D+4sIzB7/pqakpJMCORSK+Qu5oTIcuSpgL+eBYGzHPE2sutGCGoe3Ns9sITAqUTyiui5+GN3i2w==}
dependencies:
eventemitter3: 5.0.1
loglevel: 1.8.0
protobufjs: 7.2.4
sdp-transform: 2.14.1
ts-debounce: 4.0.0
webrtc-adapter: 8.2.3
dev: false
/load-json-file@1.1.0:
resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==}
engines: {node: '>=0.10.0'}
@ -23009,6 +23179,15 @@ packages:
engines: {node: '>= 0.6.0'}
dev: false
/loglevel@1.8.1:
resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==}
engines: {node: '>= 0.6.0'}
dev: false
/long@5.2.3:
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
dev: false
/longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
dev: false
@ -27094,6 +27273,25 @@ packages:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
dev: true
/protobufjs@7.2.4:
resolution: {integrity: sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==}
engines: {node: '>=12.0.0'}
requiresBuild: true
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
'@protobufjs/codegen': 2.0.4
'@protobufjs/eventemitter': 1.1.0
'@protobufjs/fetch': 1.1.0
'@protobufjs/float': 1.0.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 18.11.9
long: 5.2.3
dev: false
/protocols@2.0.1:
resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==}
dev: true
@ -29820,6 +30018,10 @@ packages:
hasBin: true
dev: false
/sdp@3.2.0:
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
dev: false
/section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
@ -31943,6 +32145,10 @@ packages:
resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==}
dev: false
/ts-debounce@4.0.0:
resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
dev: false
/ts-dedent@2.2.0:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'}
@ -33636,6 +33842,13 @@ packages:
std-env: 3.3.2
webpack: 5.75.0(esbuild@0.12.29)(webpack-cli@4.10.0)
/webrtc-adapter@8.2.3:
resolution: {integrity: sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==}
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
dependencies:
sdp: 3.2.0
dev: false
/websocket-driver@0.7.4:
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
engines: {node: '>=0.8.0'}

@ -7,7 +7,12 @@
"scripts": {
"sync:declaration": "tailchat declaration github"
},
"dependencies": {},
"dependencies": {
"@livekit/components-core": "^0.6.11",
"@livekit/components-react": "^1.0.8",
"@livekit/components-styles": "^1.0.4",
"livekit-client": "^1.12.1"
},
"devDependencies": {
"@types/styled-components": "^5.1.26",
"react": "18.2.0",

@ -0,0 +1,9 @@
import styled from 'styled-components';
import '@livekit/components-styles';
export const LivekitContainer = styled.div.attrs({
'data-lk-theme': 'default',
})`
height: 100%;
background-color: var(--lk-bg);
`;

@ -0,0 +1,316 @@
import {
useEvent,
getJWTUserInfo,
useAsync,
useCurrentUserInfo,
} from '@capital/common';
import { Avatar, Button } from '@capital/component';
import { MediaDeviceMenu, TrackToggle } from '@livekit/components-react';
import type {
CreateLocalTracksOptions,
LocalAudioTrack,
LocalTrack,
LocalVideoTrack,
} from 'livekit-client';
import {
createLocalTracks,
facingModeFromLocalTrack,
Track,
} from 'livekit-client';
import * as React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { log } from '@livekit/components-core';
import { Translate } from '../translate';
/**
* Fork <PreJoin /> from "@livekit/components-react"
*/
/** @public */
export type LocalUserChoices = {
username: string;
videoEnabled: boolean;
audioEnabled: boolean;
videoDeviceId: string;
audioDeviceId: string;
};
const DEFAULT_USER_CHOICES = {
username: '',
videoEnabled: true,
audioEnabled: true,
videoDeviceId: 'default',
audioDeviceId: 'default',
};
/** @public */
export type PreJoinProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'onSubmit'
> & {
/** This function is called with the `LocalUserChoices` if validation is passed. */
onSubmit?: (values: LocalUserChoices) => void;
/**
* Provide your custom validation function. Only if validation is successful the user choices are past to the onSubmit callback.
*/
onValidate?: (values: LocalUserChoices) => boolean;
onError?: (error: Error) => void;
/** Prefill the input form with initial values. */
defaults?: Partial<LocalUserChoices>;
/** Display a debug window for your convenience. */
debug?: boolean;
joinLabel?: string;
micLabel?: string;
camLabel?: string;
userLabel?: string;
};
/**
* The PreJoin prefab component is normally presented to the user before he enters a room.
* This component allows the user to check and select the preferred media device (camera und microphone).
* On submit the user decisions are returned, which can then be passed on to the LiveKitRoom so that the user enters the room with the correct media devices.
*
* @remarks
* This component is independent from the LiveKitRoom component and don't has to be nested inside it.
* Because it only access the local media tracks this component is self contained and works without connection to the LiveKit server.
*
* @example
* ```tsx
* <PreJoin />
* ```
* @public
*/
export const PreJoinView: React.FC<PreJoinProps> = React.memo(
({
defaults = {},
onValidate,
onSubmit,
onError,
debug,
joinLabel = Translate.joinLabel,
micLabel = Translate.micLabel,
camLabel = Translate.camLabel,
...htmlProps
}) => {
const { nickname, avatar } = useCurrentUserInfo();
const [userChoices, setUserChoices] = useState(DEFAULT_USER_CHOICES);
const [videoEnabled, setVideoEnabled] = useState<boolean>(
defaults.videoEnabled ?? DEFAULT_USER_CHOICES.videoEnabled
);
const initialVideoDeviceId =
defaults.videoDeviceId ?? DEFAULT_USER_CHOICES.videoDeviceId;
const [videoDeviceId, setVideoDeviceId] =
useState<string>(initialVideoDeviceId);
const initialAudioDeviceId =
defaults.audioDeviceId ?? DEFAULT_USER_CHOICES.audioDeviceId;
const [audioEnabled, setAudioEnabled] = useState<boolean>(
defaults.audioEnabled ?? DEFAULT_USER_CHOICES.audioEnabled
);
const [audioDeviceId, setAudioDeviceId] =
useState<string>(initialAudioDeviceId);
const tracks = usePreviewTracks(
{
audio: audioEnabled ? { deviceId: initialAudioDeviceId } : false,
video: videoEnabled ? { deviceId: initialVideoDeviceId } : false,
},
onError
);
const videoEl = useRef(null);
const videoTrack = useMemo(
() =>
tracks?.filter(
(track) => track.kind === Track.Kind.Video
)[0] as LocalVideoTrack,
[tracks]
);
const facingMode = useMemo(() => {
if (videoTrack) {
const { facingMode } = facingModeFromLocalTrack(videoTrack);
return facingMode;
} else {
return 'undefined';
}
}, [videoTrack]);
const audioTrack = useMemo(
() =>
tracks?.filter(
(track) => track.kind === Track.Kind.Audio
)[0] as LocalAudioTrack,
[tracks]
);
useEffect(() => {
if (videoEl.current && videoTrack) {
videoTrack.unmute();
videoTrack.attach(videoEl.current);
}
return () => {
videoTrack?.detach();
};
}, [videoTrack]);
const [isValid, setIsValid] = useState<boolean>();
const handleValidation = useEvent((values: LocalUserChoices) => {
if (typeof onValidate === 'function') {
return onValidate(values);
} else {
return values.username !== '';
}
});
useEffect(() => {
const newUserChoices = {
username: nickname,
videoEnabled: videoEnabled,
videoDeviceId: videoDeviceId,
audioEnabled: audioEnabled,
audioDeviceId: audioDeviceId,
};
setUserChoices(newUserChoices);
setIsValid(handleValidation(newUserChoices));
}, [
videoEnabled,
handleValidation,
audioEnabled,
audioDeviceId,
videoDeviceId,
]);
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
if (handleValidation(userChoices)) {
if (typeof onSubmit === 'function') {
onSubmit(userChoices);
}
} else {
log.warn('Validation failed with: ', userChoices);
}
}
return (
<div className="lk-prejoin" {...htmlProps}>
<div className="lk-video-container" style={{ borderRadius: 10 }}>
{videoTrack && (
<video
ref={videoEl}
width="1280"
height="720"
data-lk-facing-mode={facingMode}
/>
)}
{(!videoTrack || !videoEnabled) && (
<div className="lk-camera-off-note">
<Avatar size={128} src={avatar} name={nickname} />
</div>
)}
</div>
<div className="lk-button-group-container">
<div className="lk-button-group audio">
<TrackToggle
initialState={audioEnabled}
source={Track.Source.Microphone}
onChange={(enabled) => setAudioEnabled(enabled)}
>
{micLabel}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
initialSelection={audioDeviceId}
kind="audioinput"
disabled={!audioTrack}
tracks={{ audioinput: audioTrack }}
onActiveDeviceChange={(_, id) => setAudioDeviceId(id)}
/>
</div>
</div>
<div className="lk-button-group video">
<TrackToggle
initialState={videoEnabled}
source={Track.Source.Camera}
onChange={(enabled) => setVideoEnabled(enabled)}
>
{camLabel}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
initialSelection={videoDeviceId}
kind="videoinput"
disabled={!videoTrack}
tracks={{ videoinput: videoTrack }}
onActiveDeviceChange={(_, id) => setVideoDeviceId(id)}
/>
</div>
</div>
</div>
<Button
size="large"
type="primary"
onClick={handleSubmit}
disabled={!isValid}
>
{joinLabel}
</Button>
{debug && (
<>
<strong>User Choices:</strong>
<ul
className="lk-list"
style={{ overflow: 'hidden', maxWidth: '15rem' }}
>
<li>Video Enabled: {`${userChoices.videoEnabled}`}</li>
<li>Audio Enabled: {`${userChoices.audioEnabled}`}</li>
<li>Video Device: {`${userChoices.videoDeviceId}`}</li>
<li>Audio Device: {`${userChoices.audioDeviceId}`}</li>
</ul>
</>
)}
</div>
);
}
);
PreJoinView.displayName = 'PreJoinView';
/** @alpha */
function usePreviewTracks(
options: CreateLocalTracksOptions,
onError?: (err: Error) => void
) {
const [tracks, setTracks] = useState<LocalTrack[]>();
useEffect(() => {
let trackPromise: Promise<LocalTrack[]> | undefined = undefined;
let needsCleanup = false;
if (options.audio || options.video) {
trackPromise = createLocalTracks(options);
trackPromise
.then((tracks) => {
if (needsCleanup) {
tracks.forEach((tr) => tr.stop());
} else {
setTracks(tracks);
}
})
.catch(onError);
}
return () => {
needsCleanup = true;
trackPromise?.then((tracks) =>
tracks.forEach((track) => {
track.stop();
})
);
};
}, [JSON.stringify(options)]);
return tracks;
}

@ -0,0 +1,52 @@
import {
showErrorToasts,
useEvent,
useGroupPanelContext,
} from '@capital/common';
import { GroupPanelContainer } from '@capital/component';
import React, { useState } from 'react';
import {
LiveKitRoom,
LocalUserChoices,
useToken,
VideoConference,
formatChatMessageLinks,
} from '@livekit/components-react';
import { LogLevel, RoomOptions, VideoPresets } from 'livekit-client';
import { PreJoinView } from '../components/PreJoinView';
import { LivekitContainer } from '../components/LivekitContainer';
export const LivekitPanel: React.FC = React.memo(() => {
const { groupId, panelId } = useGroupPanelContext();
const [preJoinChoices, setPreJoinChoices] = useState<
LocalUserChoices | undefined
>(undefined);
const handleError = useEvent((err: Error) => {
showErrorToasts('error while setting up prejoin');
console.log('error while setting up prejoin', err);
});
return (
<GroupPanelContainer groupId={groupId} panelId={panelId}>
<LivekitContainer>
<div style={{ display: 'grid', placeItems: 'center', height: '100%' }}>
<PreJoinView
onError={handleError}
defaults={{
videoEnabled: false,
audioEnabled: false,
}}
onSubmit={(values) => {
console.log('Joining with: ', values);
setPreJoinChoices(values);
}}
/>
</div>
</LivekitContainer>
</GroupPanelContainer>
);
});
LivekitPanel.displayName = 'LivekitPanel';
export default LivekitPanel;

@ -1 +1,16 @@
import { regGroupPanel } from '@capital/common';
import { Loadable } from '@capital/component';
import { Translate } from './translate';
const PLUGIN_ID = 'com.msgbyte.livekit';
console.log('Plugin livekit is loaded');
regGroupPanel({
name: `${PLUGIN_ID}/livekitPanel`,
label: Translate.voiceChannel,
provider: PLUGIN_ID,
render: Loadable(() => import('./group/LivekitPanel'), {
componentName: `${PLUGIN_ID}:LivekitPanel`,
}),
});

@ -0,0 +1,20 @@
import { localTrans } from '@capital/common';
export const Translate = {
voiceChannel: localTrans({
'zh-CN': '语音频道',
'en-US': 'Voice Channel',
}),
joinLabel: localTrans({
'zh-CN': '加入房间',
'en-US': 'Join Room',
}),
micLabel: localTrans({
'zh-CN': '麦克风',
'en-US': 'Microphone',
}),
camLabel: localTrans({
'zh-CN': '摄像头',
'en-US': 'Camera',
}),
};

@ -573,4 +573,6 @@ declare module '@capital/component' {
export const JumpToConverseButton: any;
export const NoData: any;
export const GroupPanelContainer: any;
}

Loading…
Cancel
Save