Skip to content

Commit 26ecfa7

Browse files
VIDEO-11239 Add Krisp Noise Cancellation (#750)
* add krisp * krisp implementation * ui * update UI * add marginRight to MuiSwitch for theme * move krisp state to useappstate * remove css from settings button on desktop * remove some padding from intro container * refactor device selection screen for noise cancellation * add tool tip * krisp logo and disabling track ui * refactor device selection screen * add krisp to device selection dialog * use krisp toggle useeffect * remove browser supporession * move toggle back to original spot * polish css * remove krisp sdk * move isKrispInstalled to useLocalTracks hook * remove unnecessary useEffect in useKrispToggle * fix switch button logic * fix flicker in UI for unsupported browsers * VIDEO-11239 Changes to make noise cancellation work. * VIDEO-11239 Updating twilio-video and CHANGELOG.md. * VIDEO-11239 UI changes. * VIDEO-11239 Prep for 0.8.0. * VIDEO-11239 Get current unit tests to pass. * VIDEO-11239 Updating twilio-video to 2.24.2. * VIDEO-11239 Make Krisp dependency optional. * VIDEO-11239 Fixing unit tests. * VIDEO-11239 s/suppression/cancellation/g in CHANGELOG.md. * VIDEO-11239 Update README section for noise cancellation. Co-authored-by: mmalavalli <[email protected]>
1 parent 0246608 commit 26ecfa7

File tree

22 files changed

+399
-64
lines changed

22 files changed

+399
-64
lines changed

.circleci/config.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ jobs:
1414
- v1-deps-{{ .Branch }}
1515
- v1-deps
1616

17-
- run: npm ci
17+
- run:
18+
name: 'Install Dependencies'
19+
command: |
20+
npm ci
21+
npm run noisecancellation:krisp
1822
1923
- save_cache:
2024
key: v1-deps-{{ .Branch }}-{{ checksum "package-lock.json" }}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ yarn-debug.log*
2626
yarn-error.log*
2727

2828
.env
29+
.env.*
2930
.vscode
3031

3132
test-reports
3233
junit.xml
3334
serviceAccountKey.json
3435

36+
public/noisecancellation/
3537
public/virtualbackground/

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
## 0.9.0 (November 22, 2022)
2+
3+
### New Features
4+
5+
- Krisp audio noise cancellation has been added. [#750](https://github.com/twilio/twilio-video-app-react/pull/750)
6+
17
## 0.8.0 (November 14, 2022)
28

39
### New Feature
410

511
- 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.
612

13+
### Dependency Changes
14+
15+
- `twilio-video` has been upgraded from 2.23.0 to 2.25.0.
16+
717
## 0.7.1 (August 5, 2022)
818

919
### Dependency Upgrades

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Run `npm install` inside the main project folder to install all dependencies fro
3939

4040
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`.
4141

42+
### Add Noise Cancellation
43+
44+
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).
45+
4246
## Install Twilio CLI and RTC Plugin
4347

4448
### Install the Twilio CLI

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
},
8787
"scripts": {
8888
"postinstall": "rimraf public/virtualbackground && copyfiles -f node_modules/@twilio/video-processors/dist/build/* public/virtualbackground",
89+
"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",
8990
"start": "concurrently npm:server npm:dev",
9091
"dev": "react-scripts start",
9192
"build": "node ./scripts/build.js",

server/__tests__/createExpressHandler.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable import/first */
2+
process.env.REACT_APP_TWILIO_ENVIRONMENT = 'prod';
23
process.env.TWILIO_ACCOUNT_SID = 'mockAccountSid';
34
process.env.TWILIO_API_KEY_SID = 'mockApiKeySid';
45
process.env.TWILIO_API_KEY_SECRET = 'mockApiKeySecret';

src/components/DeviceSelectionDialog/AudioInputList/AudioInputList.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ export default function AudioInputList() {
1212
const { localTracks } = useVideoContext();
1313

1414
const localAudioTrack = localTracks.find(track => track.kind === 'audio') as LocalAudioTrack;
15+
const srcMediaStreamTrack = localAudioTrack?.noiseCancellation?.sourceTrack;
1516
const mediaStreamTrack = useMediaStreamTrack(localAudioTrack);
16-
const localAudioInputDeviceId = mediaStreamTrack?.getSettings().deviceId;
17-
17+
const localAudioInputDeviceId =
18+
srcMediaStreamTrack?.getSettings().deviceId || mediaStreamTrack?.getSettings().deviceId;
1819
function replaceTrack(newDeviceId: string) {
1920
window.localStorage.setItem(SELECTED_AUDIO_INPUT_KEY, newDeviceId);
2021
localAudioTrack?.restart({ deviceId: { exact: newDeviceId } });

src/components/DeviceSelectionDialog/DeviceSelectionDialog.tsx

+67
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ import {
1212
Theme,
1313
DialogTitle,
1414
Hidden,
15+
FormControlLabel,
16+
Switch,
17+
Tooltip,
1518
} from '@material-ui/core';
1619
import { makeStyles } from '@material-ui/core/styles';
1720
import VideoInputList from './VideoInputList/VideoInputList';
1821
import MaxGalleryViewParticipants from './MaxGalleryViewParticipants/MaxGalleryViewParticipants';
22+
import { useKrispToggle } from '../../hooks/useKrispToggle/useKrispToggle';
23+
import SmallCheckIcon from '../../icons/SmallCheckIcon';
24+
import InfoIconOutlined from '../../icons/InfoIconOutlined';
25+
import KrispLogo from '../../icons/KrispLogo';
26+
import { useAppState } from '../../state';
27+
import useVideoContext from '../../hooks/useVideoContext/useVideoContext';
1928

2029
const useStyles = makeStyles((theme: Theme) => ({
2130
container: {
@@ -46,9 +55,28 @@ const useStyles = makeStyles((theme: Theme) => ({
4655
margin: '1em 0 2em 0',
4756
},
4857
},
58+
noiseCancellationContainer: {
59+
display: 'flex',
60+
justifyContent: 'space-between',
61+
},
62+
krispContainer: {
63+
display: 'flex',
64+
alignItems: 'center',
65+
'& svg': {
66+
'&:not(:last-child)': {
67+
margin: '0 0.3em',
68+
},
69+
},
70+
},
71+
krispInfoText: {
72+
margin: '0 0 1.5em 0.5em',
73+
},
4974
}));
5075

5176
export default function DeviceSelectionDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
77+
const { isAcquiringLocalTracks } = useVideoContext();
78+
const { isKrispEnabled, isKrispInstalled } = useAppState();
79+
const { toggleKrisp } = useKrispToggle();
5280
const classes = useStyles();
5381

5482
return (
@@ -67,6 +95,45 @@ export default function DeviceSelectionDialog({ open, onClose }: { open: boolean
6795
<Typography variant="h6" className={classes.headline}>
6896
Audio
6997
</Typography>
98+
99+
{isKrispInstalled && (
100+
<div className={classes.noiseCancellationContainer}>
101+
<div className={classes.krispContainer}>
102+
<Typography variant="subtitle2">Noise Cancellation powered by </Typography>
103+
<KrispLogo />
104+
<Tooltip
105+
title="Suppress background noise from your microphone"
106+
interactive
107+
leaveDelay={250}
108+
leaveTouchDelay={15000}
109+
enterTouchDelay={0}
110+
>
111+
<div>
112+
<InfoIconOutlined />
113+
</div>
114+
</Tooltip>
115+
</div>
116+
<FormControlLabel
117+
control={
118+
<Switch
119+
checked={!!isKrispEnabled}
120+
checkedIcon={<SmallCheckIcon />}
121+
disableRipple={true}
122+
onClick={toggleKrisp}
123+
/>
124+
}
125+
label={isKrispEnabled ? 'Enabled' : 'Disabled'}
126+
style={{ marginRight: 0 }}
127+
disabled={isAcquiringLocalTracks}
128+
/>
129+
</div>
130+
)}
131+
{isKrispInstalled && (
132+
<Typography variant="body1" color="textSecondary" className={classes.krispInfoText}>
133+
Suppress background noise from your microphone.
134+
</Typography>
135+
)}
136+
70137
<AudioInputList />
71138
</div>
72139
<div className={classes.listSection}>

src/components/IntroContainer/IntroContainer.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const useStyles = makeStyles((theme: Theme) => ({
7373
content: {
7474
background: 'white',
7575
width: '100%',
76-
padding: '4em',
76+
padding: '3em 4em',
7777
flex: 1,
7878
[theme.breakpoints.down('sm')]: {
7979
padding: '2em',

src/components/MenuBar/Menu/Menu.test.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import useLocalVideoToggle from '../../../hooks/useLocalVideoToggle/useLocalVide
2020
jest.mock('../../../hooks/useFlipCameraToggle/useFlipCameraToggle');
2121
jest.mock('@material-ui/core/useMediaQuery');
2222
jest.mock('../../../state');
23-
jest.mock('../../../hooks/useVideoContext/useVideoContext', () => () => ({ room: { sid: 'mockRoomSid' } }));
23+
jest.mock('../../../hooks/useVideoContext/useVideoContext', () => () => ({
24+
localTracks: [],
25+
room: { sid: 'mockRoomSid' },
26+
}));
2427
jest.mock('../../../hooks/useIsRecording/useIsRecording');
2528
jest.mock('../../../hooks/useChatContext/useChatContext');
2629
jest.mock('../../../hooks/useLocalVideoToggle/useLocalVideoToggle');

src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.test.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mockUseVideoContext.mockImplementation(() => ({
2525
connect: mockConnect,
2626
isAcquiringLocalTracks: false,
2727
isConnecting: false,
28+
localTracks: [],
2829
}));
2930

3031
describe('the DeviceSelectionScreen component', () => {
@@ -38,6 +39,7 @@ describe('the DeviceSelectionScreen component', () => {
3839
connect: mockConnect,
3940
isAcquiringLocalTracks: false,
4041
isConnecting: true,
42+
localTracks: [],
4143
}));
4244

4345
const wrapper = shallow(<DeviceSelectionScreen name="test name" roomName="test room name" setStep={() => {}} />);
@@ -60,6 +62,7 @@ describe('the DeviceSelectionScreen component', () => {
6062
connect: mockConnect,
6163
isAcquiringLocalTracks: true,
6264
isConnecting: false,
65+
localTracks: [],
6366
}));
6467

6568
const wrapper = shallow(<DeviceSelectionScreen name="test name" roomName="test room name" setStep={() => {}} />);
@@ -82,6 +85,7 @@ describe('the DeviceSelectionScreen component', () => {
8285
connect: mockConnect,
8386
isAcquiringLocalTracks: false,
8487
isConnecting: false,
88+
localTracks: [],
8589
}));
8690
mockUseAppState.mockImplementationOnce(() => ({ getToken: mockGetToken, isFetching: true }));
8791
const wrapper = shallow(<DeviceSelectionScreen name="test name" roomName="test room name" setStep={() => {}} />);

src/components/PreJoinScreens/DeviceSelectionScreen/DeviceSelectionScreen.tsx

+93-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
2-
import { makeStyles, Typography, Grid, Button, Theme, Hidden } from '@material-ui/core';
2+
import { makeStyles, Typography, Grid, Button, Theme, Hidden, Switch, Tooltip } from '@material-ui/core';
33
import CircularProgress from '@material-ui/core/CircularProgress';
4+
import Divider from '@material-ui/core/Divider';
45
import LocalVideoPreview from './LocalVideoPreview/LocalVideoPreview';
56
import SettingsMenu from './SettingsMenu/SettingsMenu';
67
import { Steps } from '../PreJoinScreens';
@@ -9,6 +10,10 @@ import ToggleVideoButton from '../../Buttons/ToggleVideoButton/ToggleVideoButton
910
import { useAppState } from '../../../state';
1011
import useChatContext from '../../../hooks/useChatContext/useChatContext';
1112
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
13+
import FormControlLabel from '@material-ui/core/FormControlLabel';
14+
import { useKrispToggle } from '../../../hooks/useKrispToggle/useKrispToggle';
15+
import SmallCheckIcon from '../../../icons/SmallCheckIcon';
16+
import InfoIconOutlined from '../../../icons/InfoIconOutlined';
1217

1318
const useStyles = makeStyles((theme: Theme) => ({
1419
gutterBottom: {
@@ -24,6 +29,7 @@ const useStyles = makeStyles((theme: Theme) => ({
2429
},
2530
localPreviewContainer: {
2631
paddingRight: '2em',
32+
marginBottom: '2em',
2733
[theme.breakpoints.down('sm')]: {
2834
padding: '0 2.5em',
2935
},
@@ -50,6 +56,17 @@ const useStyles = makeStyles((theme: Theme) => ({
5056
padding: '0.8em 0',
5157
margin: 0,
5258
},
59+
toolTipContainer: {
60+
display: 'flex',
61+
alignItems: 'center',
62+
'& div': {
63+
display: 'flex',
64+
alignItems: 'center',
65+
},
66+
'& svg': {
67+
marginLeft: '0.3em',
68+
},
69+
},
5370
}));
5471

5572
interface DeviceSelectionScreenProps {
@@ -60,9 +77,10 @@ interface DeviceSelectionScreenProps {
6077

6178
export default function DeviceSelectionScreen({ name, roomName, setStep }: DeviceSelectionScreenProps) {
6279
const classes = useStyles();
63-
const { getToken, isFetching } = useAppState();
80+
const { getToken, isFetching, isKrispEnabled, isKrispInstalled } = useAppState();
6481
const { connect: chatConnect } = useChatContext();
6582
const { connect: videoConnect, isAcquiringLocalTracks, isConnecting } = useVideoContext();
83+
const { toggleKrisp } = useKrispToggle();
6684
const disableButtons = isFetching || isAcquiringLocalTracks || isConnecting;
6785

6886
const handleJoin = () => {
@@ -102,32 +120,89 @@ export default function DeviceSelectionScreen({ name, roomName, setStep }: Devic
102120
<Hidden mdUp>
103121
<ToggleAudioButton className={classes.mobileButton} disabled={disableButtons} />
104122
<ToggleVideoButton className={classes.mobileButton} disabled={disableButtons} />
123+
<SettingsMenu mobileButtonClass={classes.mobileButton} />
105124
</Hidden>
106-
<SettingsMenu mobileButtonClass={classes.mobileButton} />
107125
</div>
108126
</Grid>
109127
<Grid item md={5} sm={12} xs={12}>
110-
<Grid container direction="column" justifyContent="space-between" style={{ height: '100%' }}>
128+
<Grid container direction="column" justifyContent="space-between" style={{ alignItems: 'normal' }}>
111129
<div>
112130
<Hidden smDown>
113131
<ToggleAudioButton className={classes.deviceButton} disabled={disableButtons} />
114132
<ToggleVideoButton className={classes.deviceButton} disabled={disableButtons} />
115133
</Hidden>
116134
</div>
117-
<div className={classes.joinButtons}>
118-
<Button variant="outlined" color="primary" onClick={() => setStep(Steps.roomNameStep)}>
119-
Cancel
120-
</Button>
121-
<Button
122-
variant="contained"
123-
color="primary"
124-
data-cy-join-now
125-
onClick={handleJoin}
126-
disabled={disableButtons}
127-
>
128-
Join Now
129-
</Button>
130-
</div>
135+
</Grid>
136+
</Grid>
137+
138+
<Grid item md={12} sm={12} xs={12}>
139+
{isKrispInstalled && (
140+
<Grid
141+
container
142+
direction="row"
143+
justifyContent="space-between"
144+
alignItems="center"
145+
style={{ marginBottom: '1em' }}
146+
>
147+
<div className={classes.toolTipContainer}>
148+
<Typography variant="subtitle2">Noise Cancellation</Typography>
149+
<Tooltip
150+
title="Suppress background noise from your microphone"
151+
interactive
152+
leaveDelay={250}
153+
leaveTouchDelay={15000}
154+
enterTouchDelay={0}
155+
>
156+
<div>
157+
<InfoIconOutlined />
158+
</div>
159+
</Tooltip>
160+
</div>
161+
162+
<FormControlLabel
163+
control={
164+
<Switch
165+
checked={!!isKrispEnabled}
166+
checkedIcon={<SmallCheckIcon />}
167+
disableRipple={true}
168+
onClick={toggleKrisp}
169+
/>
170+
}
171+
label={isKrispEnabled ? 'Enabled' : 'Disabled'}
172+
style={{ marginRight: 0 }}
173+
// Prevents <Switch /> from being temporarily enabled (and then quickly disabled) in unsupported browsers after
174+
// isAcquiringLocalTracks becomes false:
175+
disabled={isKrispEnabled && isAcquiringLocalTracks}
176+
/>
177+
</Grid>
178+
)}
179+
<Divider />
180+
</Grid>
181+
182+
<Grid item md={12} sm={12} xs={12}>
183+
<Grid container direction="row" alignItems="center" style={{ marginTop: '1em' }}>
184+
<Hidden smDown>
185+
<Grid item md={7} sm={12} xs={12}>
186+
<SettingsMenu mobileButtonClass={classes.mobileButton} />
187+
</Grid>
188+
</Hidden>
189+
190+
<Grid item md={5} sm={12} xs={12}>
191+
<div className={classes.joinButtons}>
192+
<Button variant="outlined" color="primary" onClick={() => setStep(Steps.roomNameStep)}>
193+
Cancel
194+
</Button>
195+
<Button
196+
variant="contained"
197+
color="primary"
198+
data-cy-join-now
199+
onClick={handleJoin}
200+
disabled={disableButtons}
201+
>
202+
Join Now
203+
</Button>
204+
</div>
205+
</Grid>
131206
</Grid>
132207
</Grid>
133208
</Grid>

0 commit comments

Comments
 (0)