diff --git a/src/components/MainParticipantInfo/MainParticipantInfo.test.tsx b/src/components/MainParticipantInfo/MainParticipantInfo.test.tsx
index d77866a70..122fb6624 100644
--- a/src/components/MainParticipantInfo/MainParticipantInfo.test.tsx
+++ b/src/components/MainParticipantInfo/MainParticipantInfo.test.tsx
@@ -60,12 +60,12 @@ describe('the MainParticipantInfo component', () => {
expect(wrapper.find(AvatarIcon).exists()).toBe(false);
});
- it('should render the AvatarIcon component when video is switched off', () => {
+ it('should display the video has been switched off message when the video track is switchedOff', () => {
mockUseIsTrackSwitchedOff.mockImplementationOnce(() => true);
const wrapper = shallow(
mock children
);
- expect(wrapper.find(AvatarIcon).exists()).toBe(true);
+ expect(wrapper.text()).toContain('Video has been switched off to conserve bandwidth.');
});
it('should not render the reconnecting UI when the user is connected', () => {
diff --git a/src/components/MainParticipantInfo/MainParticipantInfo.tsx b/src/components/MainParticipantInfo/MainParticipantInfo.tsx
index cf70c46a9..26070fe79 100644
--- a/src/components/MainParticipantInfo/MainParticipantInfo.tsx
+++ b/src/components/MainParticipantInfo/MainParticipantInfo.tsx
@@ -22,6 +22,10 @@ const useStyles = makeStyles((theme: Theme) => ({
position: 'relative',
display: 'flex',
alignItems: 'center',
+ '& video': {
+ filter: 'none',
+ transition: 'filter 0.25s cubic-bezier(0.22, 0.61, 0.36, 1)',
+ },
},
identity: {
background: 'rgba(0, 0, 0, 0.5)',
@@ -52,6 +56,21 @@ const useStyles = makeStyles((theme: Theme) => ({
background: 'rgba(40, 42, 43, 0.75)',
zIndex: 1,
},
+ trackSwitchOffContainer: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 1,
+ textAlign: 'center',
+ opacity: 0,
+ visibility: 'hidden',
+ transition: 'all 0.25s cubic-bezier(0.22, 0.61, 0.36, 1)',
+ },
fullWidth: {
gridArea: '1 / 1 / 2 / 3',
[theme.breakpoints.down('sm')]: {
@@ -89,6 +108,21 @@ const useStyles = makeStyles((theme: Theme) => ({
top: 0,
},
},
+ switchedOffMessage: {
+ textShadow: '0 0 3px rgba(0, 0, 0, 0.7)',
+ color: 'white',
+ },
+ isSwitchedOff: {
+ opacity: 1,
+ visibility: 'visible',
+ transition: 'all 0.5s linear 2s',
+ },
+ blur: {
+ '& video': {
+ filter: 'blur(10px)',
+ transition: 'filter 1s cubic-bezier(0.22, 0.61, 0.36, 1)',
+ },
+ },
circle: {
height: '12px',
width: '12px',
@@ -145,6 +179,7 @@ export default function MainParticipantInfo({ participant, children }: MainParti
data-cy-participant={participant.identity}
className={clsx(classes.container, {
[classes.fullWidth]: !isRemoteParticipantScreenSharing,
+ [classes.blur]: isVideoSwitchedOff,
})}
>
@@ -173,11 +208,16 @@ export default function MainParticipantInfo({ participant, children }: MainParti
)}
- {(!isVideoEnabled || isVideoSwitchedOff) && (
+ {!isVideoEnabled && (
)}
+
+
+ Video has been switched off to conserve bandwidth.
+
+
{isParticipantReconnecting && (
diff --git a/src/components/ParticipantInfo/ParticipantInfo.test.tsx b/src/components/ParticipantInfo/ParticipantInfo.test.tsx
index e3ad063cf..6ff492b2f 100644
--- a/src/components/ParticipantInfo/ParticipantInfo.test.tsx
+++ b/src/components/ParticipantInfo/ParticipantInfo.test.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import AvatarIcon from '../../icons/AvatarIcon';
import ParticipantInfo from './ParticipantInfo';
import PinIcon from './PinIcon/PinIcon';
@@ -43,7 +42,7 @@ describe('the ParticipantInfo component', () => {
expect(wrapper.find(AvatarIcon).exists()).toBe(false);
});
- it('should render the AvatarIcon component when the video track is switchedOff', () => {
+ it('should display the video has been switched off message when the video track is switchedOff', () => {
mockUseIsTrackSwitchedOff.mockImplementation(() => true);
mockUsePublications.mockImplementation(() => [{ trackName: '', kind: 'video' }]);
const wrapper = shallow(
@@ -51,7 +50,7 @@ describe('the ParticipantInfo component', () => {
mock children
);
- expect(wrapper.find(AvatarIcon).exists()).toBe(true);
+ expect(wrapper.text()).toContain('Video has been switched off to conserve bandwidth.');
});
it('should not render the reconnecting UI when the user is connected', () => {
diff --git a/src/components/ParticipantInfo/ParticipantInfo.tsx b/src/components/ParticipantInfo/ParticipantInfo.tsx
index 6b8227d45..c2bbcdf84 100644
--- a/src/components/ParticipantInfo/ParticipantInfo.tsx
+++ b/src/components/ParticipantInfo/ParticipantInfo.tsx
@@ -2,22 +2,18 @@ import React from 'react';
import clsx from 'clsx';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import { LocalAudioTrack, LocalVideoTrack, Participant, RemoteAudioTrack, RemoteVideoTrack } from 'twilio-video';
-
import AudioLevelIndicator from '../AudioLevelIndicator/AudioLevelIndicator';
import AvatarIcon from '../../icons/AvatarIcon';
import NetworkQualityLevel from '../NetworkQualityLevel/NetworkQualityLevel';
import PinIcon from './PinIcon/PinIcon';
import ScreenShareIcon from '../../icons/ScreenShareIcon';
import Typography from '@material-ui/core/Typography';
-
import useIsTrackSwitchedOff from '../../hooks/useIsTrackSwitchedOff/useIsTrackSwitchedOff';
import usePublications from '../../hooks/usePublications/usePublications';
import useTrack from '../../hooks/useTrack/useTrack';
import useParticipantIsReconnecting from '../../hooks/useParticipantIsReconnecting/useParticipantIsReconnecting';
import { useAppState } from '../../state';
-
const borderWidth = 2;
-
const useStyles = makeStyles((theme: Theme) =>
createStyles({
container: {
@@ -30,6 +26,7 @@ const useStyles = makeStyles((theme: Theme) =>
marginBottom: '0.5em',
'& video': {
objectFit: 'contain !important',
+ transition: 'filter 0.25s cubic-bezier(0.22, 0.61, 0.36, 1)',
},
borderRadius: '4px',
border: `${theme.participantBorderWidth}px solid rgb(245, 248, 255)`,
@@ -91,6 +88,21 @@ const useStyles = makeStyles((theme: Theme) =>
background: 'rgba(40, 42, 43, 0.75)',
zIndex: 1,
},
+ trackSwitchOffContainer: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 1,
+ textAlign: 'center',
+ opacity: 0,
+ visibility: 'hidden',
+ transition: 'all 0.25s cubic-bezier(0.22, 0.61, 0.36, 1)',
+ },
screenShareIconContainer: {
background: 'rgba(0, 0, 0, 0.5)',
padding: '0.18em 0.3em',
@@ -121,6 +133,20 @@ const useStyles = makeStyles((theme: Theme) =>
fontSize: '0.75rem',
},
},
+ switchedOffMessage: {
+ textShadow: '0 0 3px rgba(0, 0, 0, 0.7)',
+ },
+ isSwitchedOff: {
+ opacity: 1,
+ visibility: 'visible',
+ transition: 'all 0.5s linear 2s',
+ },
+ blur: {
+ '& video': {
+ filter: 'blur(5px)',
+ transition: 'filter 1s cubic-bezier(0.22, 0.61, 0.36, 1)',
+ },
+ },
hideParticipant: {
display: 'none',
},
@@ -147,7 +173,6 @@ const useStyles = makeStyles((theme: Theme) =>
},
})
);
-
interface ParticipantInfoProps {
participant: Participant;
children: React.ReactNode;
@@ -157,7 +182,6 @@ interface ParticipantInfoProps {
hideParticipant?: boolean;
isDominantSpeaker?: boolean;
}
-
export default function ParticipantInfo({
participant,
onClick,
@@ -168,23 +192,16 @@ export default function ParticipantInfo({
isDominantSpeaker,
}: ParticipantInfoProps) {
const publications = usePublications(participant);
-
const audioPublication = publications.find(p => p.kind === 'audio');
const videoPublication = publications.find(p => !p.trackName.includes('screen') && p.kind === 'video');
-
const isVideoEnabled = Boolean(videoPublication);
const isScreenShareEnabled = publications.find(p => p.trackName.includes('screen'));
-
const videoTrack = useTrack(videoPublication);
const isVideoSwitchedOff = useIsTrackSwitchedOff(videoTrack as LocalVideoTrack | RemoteVideoTrack);
-
const audioTrack = useTrack(audioPublication) as LocalAudioTrack | RemoteAudioTrack | undefined;
const isParticipantReconnecting = useParticipantIsReconnecting(participant);
-
const { isGalleryViewActive } = useAppState();
-
const classes = useStyles();
-
return (
-
- {(!isVideoEnabled || isVideoSwitchedOff) && (
+
+
+
+ Video has been switched off to conserve bandwidth.
+
+
+ {!isVideoEnabled && (
diff --git a/src/stories/App.stories.jsx b/src/stories/App.stories.jsx
index daa0311c5..016e20deb 100644
--- a/src/stories/App.stories.jsx
+++ b/src/stories/App.stories.jsx
@@ -20,11 +20,11 @@ export default {
unpublishAllAudio: {
control: { type: 'boolean' },
},
- unpublishAllVideo: {
- control: { type: 'boolean' },
+ unpublishVideo: {
+ control: { type: 'text' },
},
- switchOffAllVideo: {
- control: { type: 'boolean' },
+ switchOffVideo: {
+ control: { type: 'text' },
},
},
};
@@ -38,6 +38,6 @@ Prod.args = {
presentationParticipant: null,
disableAllAudio: false,
unpublishAllAudio: false,
- unpublishAllVideo: false,
- switchOffAllVideo: false,
+ unpublishVideo: null,
+ switchOffVideo: null,
};
diff --git a/src/stories/mocks/twilio-video.js b/src/stories/mocks/twilio-video.js
index cb1680a40..84d15bfe5 100644
--- a/src/stories/mocks/twilio-video.js
+++ b/src/stories/mocks/twilio-video.js
@@ -105,6 +105,7 @@ class LocalParticipant extends EventEmitter {
]);
this.identity = 'Local Participant';
+ this.sid = this.identity;
}
}
@@ -122,6 +123,7 @@ const mockRoom = new MockRoom();
class MockParticipant extends EventEmitter {
constructor(name) {
super();
+ this.sid = name;
this.identity = name;
this.tracks = new Map([
['video', new MockPublication('video')],
@@ -223,20 +225,24 @@ export function decorator(story, { args }) {
videoTrack?.enable();
}
- if (args.unpublishAllAudio) {
- mockParticipant.unpublishTrack('audio');
- } else {
- mockParticipant.publishTrack('audio');
- }
-
- if (args.unpublishAllVideo) {
- mockParticipant.unpublishTrack('video');
+ if (args.unpublishVideo) {
+ const pList = args.unpublishVideo.split(',');
+ if (pList.includes(i.toString())) {
+ mockParticipant.unpublishTrack('video');
+ } else {
+ mockParticipant.publishTrack('video');
+ }
} else {
mockParticipant.publishTrack('video');
}
- if (args.switchOffAllVideo) {
- videoTrack?.switchOff();
+ if (args.switchOffVideo) {
+ const pList = args.switchOffVideo.split(',');
+ if (pList.includes(i.toString())) {
+ videoTrack?.switchOff();
+ } else {
+ videoTrack?.switchOn();
+ }
} else {
videoTrack?.switchOn();
}