diff --git a/.circleci/config.yml b/.circleci/config.yml index bbb286f6b..b9844359e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,11 @@ jobs: - v1-deps-{{ .Branch }} - v1-deps - - run: npm ci + - run: + name: 'Install Dependencies' + command: | + npm ci + npm run noisecancellation:krisp - save_cache: key: v1-deps-{{ .Branch }}-{{ checksum "package-lock.json" }} diff --git a/.gitignore b/.gitignore index b9e734d42..85162c36d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,12 @@ yarn-debug.log* yarn-error.log* .env +.env.* .vscode test-reports junit.xml serviceAccountKey.json +public/noisecancellation/ public/virtualbackground/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a56842be3..9a282a4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,19 @@ +## 0.9.0 (November 22, 2022) + +### New Features + +- Krisp audio noise cancellation has been added. [#750](https://github.com/twilio/twilio-video-app-react/pull/750) + ## 0.8.0 (November 14, 2022) ### New Feature - This release adds the ability to maintain audio continuity when the default audio input device changes. If the user chooses a specific audio device from the audio settings, then this feature does not apply. +### Dependency Changes + +- `twilio-video` has been upgraded from 2.23.0 to 2.25.0. + ## 0.7.1 (August 5, 2022) ### Dependency Upgrades diff --git a/README.md b/README.md index 1a42c9e54..a7de70b8d 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ Run `npm install` inside the main project folder to install all dependencies fro If you want to use `yarn` to install dependencies, first run the [yarn import](https://classic.yarnpkg.com/en/docs/cli/import/) command. This will ensure that yarn installs the package versions that are specified in `package-lock.json`. +### Add Noise Cancellation + +Twilio Video has partnered with [Krisp Technologies Inc.](https://krisp.ai/) to add [noise cancellation](https://www.twilio.com/docs/video/noise-cancellation) to the local audio track. This feature is licensed under the [Krisp Plugin for Twilio](https://twilio.github.io/krisp-audio-plugin/LICENSE.html). In order to add this feature to your application, please run `npm install noisecancellation:krisp` immediately after the [previous step](#install-dependencies). + ## Install Twilio CLI and RTC Plugin ### Install the Twilio CLI diff --git a/package.json b/package.json index e93fde8a2..b711aa039 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ }, "scripts": { "postinstall": "rimraf public/virtualbackground && copyfiles -f node_modules/@twilio/video-processors/dist/build/* public/virtualbackground", + "noisecancellation:krisp": "npm install @twilio/krisp-audio-plugin && rimraf public/noisecancellation && copyfiles -f \"node_modules/@twilio/krisp-audio-plugin/dist/*\" public/noisecancellation && copyfiles -f \"node_modules/@twilio/krisp-audio-plugin/dist/weights/*\" public/noisecancellation/weights", "start": "concurrently npm:server npm:dev", "dev": "react-scripts start", "build": "node ./scripts/build.js", diff --git a/server/__tests__/createExpressHandler.test.ts b/server/__tests__/createExpressHandler.test.ts index 4b61b4646..fe292cbe8 100644 --- a/server/__tests__/createExpressHandler.test.ts +++ b/server/__tests__/createExpressHandler.test.ts @@ -1,4 +1,5 @@ /* eslint-disable import/first */ +process.env.REACT_APP_TWILIO_ENVIRONMENT = 'prod'; process.env.TWILIO_ACCOUNT_SID = 'mockAccountSid'; process.env.TWILIO_API_KEY_SID = 'mockApiKeySid'; process.env.TWILIO_API_KEY_SECRET = 'mockApiKeySecret'; diff --git a/src/components/DeviceSelectionDialog/AudioInputList/AudioInputList.tsx b/src/components/DeviceSelectionDialog/AudioInputList/AudioInputList.tsx index ee95bca21..3653ec79c 100644 --- a/src/components/DeviceSelectionDialog/AudioInputList/AudioInputList.tsx +++ b/src/components/DeviceSelectionDialog/AudioInputList/AudioInputList.tsx @@ -12,9 +12,10 @@ export default function AudioInputList() { const { localTracks } = useVideoContext(); const localAudioTrack = localTracks.find(track => track.kind === 'audio') as LocalAudioTrack; + const srcMediaStreamTrack = localAudioTrack?.noiseCancellation?.sourceTrack; const mediaStreamTrack = useMediaStreamTrack(localAudioTrack); - const localAudioInputDeviceId = mediaStreamTrack?.getSettings().deviceId; - + const localAudioInputDeviceId = + srcMediaStreamTrack?.getSettings().deviceId || mediaStreamTrack?.getSettings().deviceId; function replaceTrack(newDeviceId: string) { window.localStorage.setItem(SELECTED_AUDIO_INPUT_KEY, newDeviceId); localAudioTrack?.restart({ deviceId: { exact: newDeviceId } }); diff --git a/src/components/DeviceSelectionDialog/DeviceSelectionDialog.tsx b/src/components/DeviceSelectionDialog/DeviceSelectionDialog.tsx index 985a375a9..00d48fedf 100644 --- a/src/components/DeviceSelectionDialog/DeviceSelectionDialog.tsx +++ b/src/components/DeviceSelectionDialog/DeviceSelectionDialog.tsx @@ -12,10 +12,19 @@ import { Theme, DialogTitle, Hidden, + FormControlLabel, + Switch, + Tooltip, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import VideoInputList from './VideoInputList/VideoInputList'; import MaxGalleryViewParticipants from './MaxGalleryViewParticipants/MaxGalleryViewParticipants'; +import { useKrispToggle } from '../../hooks/useKrispToggle/useKrispToggle'; +import SmallCheckIcon from '../../icons/SmallCheckIcon'; +import InfoIconOutlined from '../../icons/InfoIconOutlined'; +import KrispLogo from '../../icons/KrispLogo'; +import { useAppState } from '../../state'; +import useVideoContext from '../../hooks/useVideoContext/useVideoContext'; const useStyles = makeStyles((theme: Theme) => ({ container: { @@ -46,9 +55,28 @@ const useStyles = makeStyles((theme: Theme) => ({ margin: '1em 0 2em 0', }, }, + noiseCancellationContainer: { + display: 'flex', + justifyContent: 'space-between', + }, + krispContainer: { + display: 'flex', + alignItems: 'center', + '& svg': { + '&:not(:last-child)': { + margin: '0 0.3em', + }, + }, + }, + krispInfoText: { + margin: '0 0 1.5em 0.5em', + }, })); export default function DeviceSelectionDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const { isAcquiringLocalTracks } = useVideoContext(); + const { isKrispEnabled, isKrispInstalled } = useAppState(); + const { toggleKrisp } = useKrispToggle(); const classes = useStyles(); return ( @@ -67,6 +95,45 @@ export default function DeviceSelectionDialog({ open, onClose }: { open: boolean Audio + + {isKrispInstalled && ( +
+
+ Noise Cancellation powered by + + +
+ +
+
+
+ } + disableRipple={true} + onClick={toggleKrisp} + /> + } + label={isKrispEnabled ? 'Enabled' : 'Disabled'} + style={{ marginRight: 0 }} + disabled={isAcquiringLocalTracks} + /> +
+ )} + {isKrispInstalled && ( + + Suppress background noise from your microphone. + + )} +
diff --git a/src/components/IntroContainer/IntroContainer.tsx b/src/components/IntroContainer/IntroContainer.tsx index 0a2085246..eef7aedac 100644 --- a/src/components/IntroContainer/IntroContainer.tsx +++ b/src/components/IntroContainer/IntroContainer.tsx @@ -73,7 +73,7 @@ const useStyles = makeStyles((theme: Theme) => ({ content: { background: 'white', width: '100%', - padding: '4em', + padding: '3em 4em', flex: 1, [theme.breakpoints.down('sm')]: { padding: '2em', diff --git a/src/components/MenuBar/Menu/Menu.test.tsx b/src/components/MenuBar/Menu/Menu.test.tsx index 0c0f7d69a..a916cb4f8 100644 --- a/src/components/MenuBar/Menu/Menu.test.tsx +++ b/src/components/MenuBar/Menu/Menu.test.tsx @@ -20,7 +20,10 @@ import useLocalVideoToggle from '../../../hooks/useLocalVideoToggle/useLocalVide jest.mock('../../../hooks/useFlipCameraToggle/useFlipCameraToggle'); jest.mock('@material-ui/core/useMediaQuery'); jest.mock('../../../state'); -jest.mock('../../../hooks/useVideoContext/useVideoContext', () => () => ({ room: { sid: 'mockRoomSid' } })); +jest.mock('../../../hooks/useVideoContext/useVideoContext', () => () => ({ + localTracks: [], + room: { sid: 'mockRoomSid' }, +})); jest.mock('../../../hooks/useIsRecording/useIsRecording'); jest.mock('../../../hooks/useChatContext/useChatContext'); jest.mock('../../../hooks/useLocalVideoToggle/useLocalVideoToggle'); diff --git a/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.test.tsx b/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.test.tsx index 31c14b770..8dea1874f 100644 --- a/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.test.tsx +++ b/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.test.tsx @@ -25,6 +25,7 @@ mockUseVideoContext.mockImplementation(() => ({ connect: mockConnect, isAcquiringLocalTracks: false, isConnecting: false, + localTracks: [], })); describe('the DeviceSelectionScreen component', () => { @@ -38,6 +39,7 @@ describe('the DeviceSelectionScreen component', () => { connect: mockConnect, isAcquiringLocalTracks: false, isConnecting: true, + localTracks: [], })); const wrapper = shallow( {}} />); @@ -60,6 +62,7 @@ describe('the DeviceSelectionScreen component', () => { connect: mockConnect, isAcquiringLocalTracks: true, isConnecting: false, + localTracks: [], })); const wrapper = shallow( {}} />); @@ -82,6 +85,7 @@ describe('the DeviceSelectionScreen component', () => { connect: mockConnect, isAcquiringLocalTracks: false, isConnecting: false, + localTracks: [], })); mockUseAppState.mockImplementationOnce(() => ({ getToken: mockGetToken, isFetching: true })); const wrapper = shallow( {}} />); diff --git a/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.tsx b/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.tsx index f39ff591c..ef3f059ff 100644 --- a/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.tsx +++ b/src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { makeStyles, Typography, Grid, Button, Theme, Hidden } from '@material-ui/core'; +import { makeStyles, Typography, Grid, Button, Theme, Hidden, Switch, Tooltip } from '@material-ui/core'; import CircularProgress from '@material-ui/core/CircularProgress'; +import Divider from '@material-ui/core/Divider'; import LocalVideoPreview from './LocalVideoPreview/LocalVideoPreview'; import SettingsMenu from './SettingsMenu/SettingsMenu'; import { Steps } from '../PreJoinScreens'; @@ -9,6 +10,10 @@ import ToggleVideoButton from '../../Buttons/ToggleVideoButton/ToggleVideoButton import { useAppState } from '../../../state'; import useChatContext from '../../../hooks/useChatContext/useChatContext'; import useVideoContext from '../../../hooks/useVideoContext/useVideoContext'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { useKrispToggle } from '../../../hooks/useKrispToggle/useKrispToggle'; +import SmallCheckIcon from '../../../icons/SmallCheckIcon'; +import InfoIconOutlined from '../../../icons/InfoIconOutlined'; const useStyles = makeStyles((theme: Theme) => ({ gutterBottom: { @@ -24,6 +29,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, localPreviewContainer: { paddingRight: '2em', + marginBottom: '2em', [theme.breakpoints.down('sm')]: { padding: '0 2.5em', }, @@ -50,6 +56,17 @@ const useStyles = makeStyles((theme: Theme) => ({ padding: '0.8em 0', margin: 0, }, + toolTipContainer: { + display: 'flex', + alignItems: 'center', + '& div': { + display: 'flex', + alignItems: 'center', + }, + '& svg': { + marginLeft: '0.3em', + }, + }, })); interface DeviceSelectionScreenProps { @@ -60,9 +77,10 @@ interface DeviceSelectionScreenProps { export default function DeviceSelectionScreen({ name, roomName, setStep }: DeviceSelectionScreenProps) { const classes = useStyles(); - const { getToken, isFetching } = useAppState(); + const { getToken, isFetching, isKrispEnabled, isKrispInstalled } = useAppState(); const { connect: chatConnect } = useChatContext(); const { connect: videoConnect, isAcquiringLocalTracks, isConnecting } = useVideoContext(); + const { toggleKrisp } = useKrispToggle(); const disableButtons = isFetching || isAcquiringLocalTracks || isConnecting; const handleJoin = () => { @@ -102,32 +120,89 @@ export default function DeviceSelectionScreen({ name, roomName, setStep }: Devic + -
- +
-
- - -
+
+
+ + + {isKrispInstalled && ( + +
+ Noise Cancellation + +
+ +
+
+
+ + } + disableRipple={true} + onClick={toggleKrisp} + /> + } + label={isKrispEnabled ? 'Enabled' : 'Disabled'} + style={{ marginRight: 0 }} + // Prevents from being temporarily enabled (and then quickly disabled) in unsupported browsers after + // isAcquiringLocalTracks becomes false: + disabled={isKrispEnabled && isAcquiringLocalTracks} + /> +
+ )} + +
+ + + + + + + + + + +
+ + +
+
diff --git a/src/components/PreJoinScreens/DeviceSelectionScreen/SettingsMenu/SettingsMenu.tsx b/src/components/PreJoinScreens/DeviceSelectionScreen/SettingsMenu/SettingsMenu.tsx index 07ab6f2e0..a7c25a9a6 100644 --- a/src/components/PreJoinScreens/DeviceSelectionScreen/SettingsMenu/SettingsMenu.tsx +++ b/src/components/PreJoinScreens/DeviceSelectionScreen/SettingsMenu/SettingsMenu.tsx @@ -4,7 +4,7 @@ import MenuContainer from '@material-ui/core/Menu'; import MenuItem from '@material-ui/core/MenuItem'; import MoreIcon from '@material-ui/icons/MoreVert'; import Typography from '@material-ui/core/Typography'; -import { makeStyles, Theme, useMediaQuery } from '@material-ui/core'; +import { Theme, useMediaQuery } from '@material-ui/core'; import AboutDialog from '../../../AboutDialog/AboutDialog'; import ConnectionOptionsDialog from '../../../ConnectionOptionsDialog/ConnectionOptionsDialog'; @@ -12,14 +12,7 @@ import DeviceSelectionDialog from '../../../DeviceSelectionDialog/DeviceSelectio import SettingsIcon from '../../../../icons/SettingsIcon'; import { useAppState } from '../../../../state'; -const useStyles = makeStyles({ - settingsButton: { - margin: '1.8em 0 0', - }, -}); - export default function SettingsMenu({ mobileButtonClass }: { mobileButtonClass?: string }) { - const classes = useStyles(); const { roomType } = useAppState(); const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); const [menuOpen, setMenuOpen] = useState(false); @@ -41,12 +34,7 @@ export default function SettingsMenu({ mobileButtonClass }: { mobileButtonClass? More ) : ( - )} diff --git a/src/components/VideoProvider/index.test.tsx b/src/components/VideoProvider/index.test.tsx index b9f9e92b7..8061922ad 100644 --- a/src/components/VideoProvider/index.test.tsx +++ b/src/components/VideoProvider/index.test.tsx @@ -61,7 +61,6 @@ describe('the VideoProvider component', () => { onError: expect.any(Function), connect: expect.any(Function), getLocalVideoTrack: expect.any(Function), - getLocalAudioTrack: expect.any(Function), removeLocalVideoTrack: expect.any(Function), isAcquiringLocalTracks: true, toggleScreenShare: expect.any(Function), diff --git a/src/components/VideoProvider/index.tsx b/src/components/VideoProvider/index.tsx index 4f3921a4e..e2fdcadab 100644 --- a/src/components/VideoProvider/index.tsx +++ b/src/components/VideoProvider/index.tsx @@ -26,7 +26,6 @@ export interface IVideoContext { connect: (token: string) => Promise; onError: ErrorCallback; getLocalVideoTrack: (newOptions?: CreateLocalTrackOptions) => Promise; - getLocalAudioTrack: (deviceId?: string) => Promise; isAcquiringLocalTracks: boolean; removeLocalVideoTrack: () => void; isSharingScreen: boolean; @@ -58,7 +57,6 @@ export function VideoProvider({ options, children, onError = () => {} }: VideoPr const { localTracks, getLocalVideoTrack, - getLocalAudioTrack, isAcquiringLocalTracks, removeLocalAudioTrack, removeLocalVideoTrack, @@ -94,7 +92,6 @@ export function VideoProvider({ options, children, onError = () => {} }: VideoPr isConnecting, onError: onErrorCallback, getLocalVideoTrack, - getLocalAudioTrack, connect, isAcquiringLocalTracks, removeLocalVideoTrack, diff --git a/src/components/VideoProvider/useLocalTracks/useLocalTracks.test.tsx b/src/components/VideoProvider/useLocalTracks/useLocalTracks.test.tsx index 4b2bb2b1c..6471206ec 100644 --- a/src/components/VideoProvider/useLocalTracks/useLocalTracks.test.tsx +++ b/src/components/VideoProvider/useLocalTracks/useLocalTracks.test.tsx @@ -1,12 +1,18 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { getDeviceInfo, isPermissionDenied } from '../../../utils'; import { SELECTED_AUDIO_INPUT_KEY, SELECTED_VIDEO_INPUT_KEY, DEFAULT_VIDEO_CONSTRAINTS } from '../../../constants'; +import { useAppState } from '../../../state'; import useLocalTracks from './useLocalTracks'; import Video from 'twilio-video'; +jest.mock('../../../state'); jest.mock('../../../utils'); + const mockGetDeviceInfo = getDeviceInfo as jest.Mock; const mockIsPermissionDenied = isPermissionDenied as jest.Mock>; +const mockUseAppState = useAppState as jest.Mock; + +mockUseAppState.mockImplementation(() => ({ setIsKrispEnabled: false })); describe('the useLocalTracks hook', () => { beforeEach(() => { @@ -33,7 +39,12 @@ describe('the useLocalTracks hook', () => { }); expect(Video.createLocalTracks).toHaveBeenCalledWith({ - audio: true, + audio: { + noiseCancellationOptions: { + sdkAssetsPath: '/noisecancellation', + vendor: 'krisp', + }, + }, video: { frameRate: 24, width: 1280, @@ -52,7 +63,12 @@ describe('the useLocalTracks hook', () => { }); expect(Video.createLocalTracks).toHaveBeenCalledWith({ - audio: true, + audio: { + noiseCancellationOptions: { + sdkAssetsPath: '/noisecancellation', + vendor: 'krisp', + }, + }, video: false, }); }); @@ -107,6 +123,10 @@ describe('the useLocalTracks hook', () => { deviceId: { exact: 'mockAudioDeviceId', }, + noiseCancellationOptions: { + sdkAssetsPath: '/noisecancellation', + vendor: 'krisp', + }, }, video: { frameRate: 24, @@ -130,7 +150,12 @@ describe('the useLocalTracks hook', () => { }); expect(Video.createLocalTracks).toHaveBeenCalledWith({ - audio: true, + audio: { + noiseCancellationOptions: { + sdkAssetsPath: '/noisecancellation', + vendor: 'krisp', + }, + }, video: { frameRate: 24, width: 1280, @@ -157,7 +182,12 @@ describe('the useLocalTracks hook', () => { }); expect(Video.createLocalTracks).toHaveBeenCalledWith({ - audio: true, + audio: { + noiseCancellationOptions: { + sdkAssetsPath: '/noisecancellation', + vendor: 'krisp', + }, + }, video: false, }); }); diff --git a/src/components/VideoProvider/useLocalTracks/useLocalTracks.ts b/src/components/VideoProvider/useLocalTracks/useLocalTracks.ts index 42bc14fd0..c575e4499 100644 --- a/src/components/VideoProvider/useLocalTracks/useLocalTracks.ts +++ b/src/components/VideoProvider/useLocalTracks/useLocalTracks.ts @@ -1,26 +1,25 @@ import { DEFAULT_VIDEO_CONSTRAINTS, SELECTED_AUDIO_INPUT_KEY, SELECTED_VIDEO_INPUT_KEY } from '../../../constants'; import { getDeviceInfo, isPermissionDenied } from '../../../utils'; import { useCallback, useState } from 'react'; -import Video, { LocalVideoTrack, LocalAudioTrack, CreateLocalTrackOptions } from 'twilio-video'; +import Video, { + LocalVideoTrack, + LocalAudioTrack, + CreateLocalTrackOptions, + NoiseCancellationOptions, +} from 'twilio-video'; +import { useAppState } from '../../../state'; + +const noiseCancellationOptions: NoiseCancellationOptions = { + sdkAssetsPath: '/noisecancellation', + vendor: 'krisp', +}; export default function useLocalTracks() { + const { setIsKrispEnabled, setIsKrispInstalled } = useAppState(); const [audioTrack, setAudioTrack] = useState(); const [videoTrack, setVideoTrack] = useState(); const [isAcquiringLocalTracks, setIsAcquiringLocalTracks] = useState(false); - const getLocalAudioTrack = useCallback((deviceId?: string) => { - const options: CreateLocalTrackOptions = {}; - - if (deviceId) { - options.deviceId = { exact: deviceId }; - } - - return Video.createLocalAudioTrack(options).then(newTrack => { - setAudioTrack(newTrack); - return newTrack; - }); - }, []); - const getLocalVideoTrack = useCallback(async () => { const selectedVideoDeviceId = window.localStorage.getItem(SELECTED_VIDEO_INPUT_KEY); @@ -88,9 +87,10 @@ export default function useLocalTracks() { name: `camera-${Date.now()}`, ...(hasSelectedVideoDevice && { deviceId: { exact: selectedVideoDeviceId! } }), }, - audio: - shouldAcquireAudio && - (hasSelectedAudioDevice ? { deviceId: { exact: selectedAudioDeviceId! } } : hasAudioInputDevices), + audio: shouldAcquireAudio && { + noiseCancellationOptions, + ...(hasSelectedAudioDevice && { deviceId: { exact: selectedAudioDeviceId! } }), + }, }; return Video.createLocalTracks(localTrackConstraints) @@ -108,6 +108,10 @@ export default function useLocalTracks() { } if (newAudioTrack) { setAudioTrack(newAudioTrack); + if (newAudioTrack.noiseCancellation) { + setIsKrispEnabled(true); + setIsKrispInstalled(true); + } } // These custom errors will be picked up by the MediaErrorSnackbar component. @@ -126,7 +130,7 @@ export default function useLocalTracks() { } }) .finally(() => setIsAcquiringLocalTracks(false)); - }, [audioTrack, videoTrack, isAcquiringLocalTracks]); + }, [audioTrack, videoTrack, isAcquiringLocalTracks, setIsKrispEnabled, setIsKrispInstalled]); const localTracks = [audioTrack, videoTrack].filter(track => track !== undefined) as ( | LocalAudioTrack @@ -136,7 +140,6 @@ export default function useLocalTracks() { return { localTracks, getLocalVideoTrack, - getLocalAudioTrack, isAcquiringLocalTracks, removeLocalAudioTrack, removeLocalVideoTrack, diff --git a/src/hooks/useKrispToggle/useKrispToggle.tsx b/src/hooks/useKrispToggle/useKrispToggle.tsx new file mode 100644 index 000000000..c8f0ddf3c --- /dev/null +++ b/src/hooks/useKrispToggle/useKrispToggle.tsx @@ -0,0 +1,22 @@ +import { LocalAudioTrack } from 'twilio-video'; +import { useCallback } from 'react'; +import { useAppState } from '../../state'; +import useVideoContext from '../useVideoContext/useVideoContext'; + +export function useKrispToggle() { + const { localTracks } = useVideoContext(); + const audioTrack = localTracks.find(track => track.kind === 'audio') as LocalAudioTrack; + const noiseCancellation = audioTrack && audioTrack.noiseCancellation; + const vendor = noiseCancellation && noiseCancellation.vendor; + const { setIsKrispEnabled } = useAppState(); + + const toggleKrisp = useCallback(() => { + if (noiseCancellation) { + noiseCancellation[noiseCancellation.isEnabled ? 'disable' : 'enable']().then(() => { + setIsKrispEnabled(noiseCancellation.isEnabled); + }); + } + }, [noiseCancellation, setIsKrispEnabled]); + + return { vendor, toggleKrisp }; +} diff --git a/src/icons/KrispLogo.tsx b/src/icons/KrispLogo.tsx new file mode 100644 index 000000000..b48e9de8a --- /dev/null +++ b/src/icons/KrispLogo.tsx @@ -0,0 +1,30 @@ +export default function KrispLogo() { + return ( + + + + + + + + + ); +} diff --git a/src/icons/SmallCheckIcon.tsx b/src/icons/SmallCheckIcon.tsx new file mode 100644 index 000000000..19903f319 --- /dev/null +++ b/src/icons/SmallCheckIcon.tsx @@ -0,0 +1,40 @@ +export default function SmallCheckIcon() { + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/state/index.tsx b/src/state/index.tsx index 1413b368f..c5bbbacc9 100644 --- a/src/state/index.tsx +++ b/src/state/index.tsx @@ -27,6 +27,10 @@ export interface StateContextType { setIsGalleryViewActive: React.Dispatch>; maxGalleryViewParticipants: number; setMaxGalleryViewParticipants: React.Dispatch>; + isKrispEnabled: boolean; + setIsKrispEnabled: React.Dispatch>; + isKrispInstalled: boolean; + setIsKrispInstalled: React.Dispatch>; } export const StateContext = createContext(null!); @@ -52,6 +56,9 @@ export default function AppStateProvider(props: React.PropsWithChildren<{}>) { 6 ); + const [isKrispEnabled, setIsKrispEnabled] = useState(false); + const [isKrispInstalled, setIsKrispInstalled] = useState(false); + let contextValue = { error, setError, @@ -65,6 +72,10 @@ export default function AppStateProvider(props: React.PropsWithChildren<{}>) { setIsGalleryViewActive, maxGalleryViewParticipants, setMaxGalleryViewParticipants, + isKrispEnabled, + setIsKrispEnabled, + isKrispInstalled, + setIsKrispInstalled, } as StateContextType; if (process.env.REACT_APP_SET_AUTH === 'firebase') { diff --git a/src/theme.ts b/src/theme.ts index 20aa1a463..caedb133c 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -108,6 +108,49 @@ export default createTheme({ borderColor: 'rgb(136, 140, 142)', }, }, + MuiSwitch: { + root: { + width: 42, + height: 18, + padding: 0, + display: 'flex', + marginRight: '0.5em', + }, + switchBase: { + padding: 2, + color: '#FFFFFF', + '&$checked': { + transform: 'translateX(18px)', + top: '50%', + marginTop: -24 / 2, + '&$disabled': { + '& + $track': { + opacity: '0.5', + }, + }, + }, + }, + colorSecondary: { + '&$checked': { + // Controls checked color for the thumb + color: 'FFFFF', + }, + }, + thumb: { + width: 14, + height: 14, + boxShadow: 'none', + }, + track: { + borderRadius: 16 / 2, + opacity: 1, + backgroundColor: '#E1E3EA', + '$checked$checked + &': { + opacity: 1, + backgroundColor: '#14B053', + }, + }, + }, }, typography: { fontFamily: 'Inter, sans-serif',