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
) : (
-