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 (
{isSelected && }
-
- {(!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(); }