-
+ {searchParams.get('disableVolumeIndicators') !== 'true' &&
}
- {participant.identity}
+ {participant.identity + (switchOffReason ? ' ' + switchOffReason : '')}
{isLocal && ' (You)'}
{screenSharePublication && ' - Screen'}
@@ -173,7 +184,7 @@ export default function MainParticipantInfo({ participant, children }: MainParti
)}
- {(!isVideoEnabled || isVideoSwitchedOff) && (
+ {!isVideoEnabled && (
diff --git a/src/components/ParticipantInfo/ParticipantInfo.tsx b/src/components/ParticipantInfo/ParticipantInfo.tsx
index 6b8227d45..4174cbeb6 100644
--- a/src/components/ParticipantInfo/ParticipantInfo.tsx
+++ b/src/components/ParticipantInfo/ParticipantInfo.tsx
@@ -15,9 +15,12 @@ import usePublications from '../../hooks/usePublications/usePublications';
import useTrack from '../../hooks/useTrack/useTrack';
import useParticipantIsReconnecting from '../../hooks/useParticipantIsReconnecting/useParticipantIsReconnecting';
import { useAppState } from '../../state';
+import useTrackSwitchOffReason from '../../hooks/useTrackSwitchOffReason/useTrackSwitchOffReason';
const borderWidth = 2;
+const searchParams = new URLSearchParams(window.location.search);
+
const useStyles = makeStyles((theme: Theme) =>
createStyles({
container: {
@@ -127,6 +130,14 @@ const useStyles = makeStyles((theme: Theme) =>
cursorPointer: {
cursor: 'pointer',
},
+ blurredVideo: {
+ '& video': {
+ filter: 'blur(3px) grayscale(1) brightness(0.5)',
+ },
+ },
+ dominantSpeaker: {
+ border: `solid ${borderWidth}px #7BEAA5`,
+ },
galleryView: {
border: `${theme.participantBorderWidth}px solid ${theme.galleryViewBackgroundColor}`,
borderRadius: '8px',
@@ -142,9 +153,6 @@ const useStyles = makeStyles((theme: Theme) =>
},
},
},
- dominantSpeaker: {
- border: `solid ${borderWidth}px #7BEAA5`,
- },
})
);
@@ -183,6 +191,8 @@ export default function ParticipantInfo({
const { isGalleryViewActive } = useAppState();
+ const switchOffReason = useTrackSwitchOffReason(videoTrack as LocalVideoTrack | RemoteVideoTrack);
+
const classes = useStyles();
return (
@@ -190,6 +200,7 @@ export default function ParticipantInfo({
className={clsx(classes.container, {
[classes.hideParticipant]: hideParticipant,
[classes.cursorPointer]: Boolean(onClick),
+ [classes.blurredVideo]: isVideoSwitchedOff,
[classes.dominantSpeaker]: isDominantSpeaker,
[classes.galleryView]: isGalleryViewActive,
})}
@@ -205,9 +216,9 @@ export default function ParticipantInfo({
)}
-
+ {searchParams.get('disableVolumeIndicators') !== 'true' && }
- {participant.identity}
+ {participant.identity + (switchOffReason ? ' ' + switchOffReason : '')}
{isLocalParticipant && ' (You)'}
@@ -215,7 +226,7 @@ export default function ParticipantInfo({
- {(!isVideoEnabled || isVideoSwitchedOff) && (
+ {!isVideoEnabled && (
diff --git a/src/hooks/useIsTrackEnabled/useIsTrackEnabled.tsx b/src/hooks/useIsTrackEnabled/useIsTrackEnabled.tsx
index 52f74bfe3..8b4d446dd 100644
--- a/src/hooks/useIsTrackEnabled/useIsTrackEnabled.tsx
+++ b/src/hooks/useIsTrackEnabled/useIsTrackEnabled.tsx
@@ -7,17 +7,35 @@ export default function useIsTrackEnabled(track: TrackType) {
const [isEnabled, setIsEnabled] = useState(track ? track.isEnabled : false);
useEffect(() => {
- setIsEnabled(track ? track.isEnabled : false);
+ //@ts-ignore
if (track) {
- const setEnabled = () => setIsEnabled(true);
- const setDisabled = () => setIsEnabled(false);
- track.on('enabled', setEnabled);
- track.on('disabled', setDisabled);
- return () => {
- track.off('enabled', setEnabled);
- track.off('disabled', setDisabled);
- };
+ if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
+ setIsEnabled(track.isEnabled);
+ const setEnabled = () => setIsEnabled(true);
+ const setDisabled = () => setIsEnabled(false);
+ track.on('enabled', setEnabled);
+ track.on('disabled', setDisabled);
+ return () => {
+ track.off('enabled', setEnabled);
+ track.off('disabled', setDisabled);
+ };
+ } else {
+ setIsEnabled(track?.switchOffReason !== 'disabled-by-publisher');
+ const handleSwitchOff = (_track: TrackType) => {
+ // @ts-ignore
+ if (_track.switchOffReason === 'disabled-by-publisher') {
+ setIsEnabled(false);
+ }
+ };
+ const handleSwitchOn = () => setIsEnabled(true);
+ track.on('switchedOff', handleSwitchOff);
+ track.on('switchedOn', handleSwitchOn);
+ return () => {
+ track.off('switchedOff', handleSwitchOff);
+ track.off('switchedOn', handleSwitchOn);
+ };
+ }
}
}, [track]);
diff --git a/src/hooks/useMediaStreamTrack/useMediaStreamTrack.ts b/src/hooks/useMediaStreamTrack/useMediaStreamTrack.ts
index 03f69e75f..60d6ad9fa 100644
--- a/src/hooks/useMediaStreamTrack/useMediaStreamTrack.ts
+++ b/src/hooks/useMediaStreamTrack/useMediaStreamTrack.ts
@@ -9,16 +9,20 @@ import { AudioTrack, VideoTrack } from 'twilio-video';
* to rerender in response to the mediaStreamTrack changing.
*/
export default function useMediaStreamTrack(track?: AudioTrack | VideoTrack) {
- const [mediaStreamTrack, setMediaStreamTrack] = useState(track?.mediaStreamTrack);
+ const [mediaStreamTrack, setMediaStreamTrack] = useState(track?.mediaStreamTrack || null);
useEffect(() => {
- setMediaStreamTrack(track?.mediaStreamTrack);
+ setMediaStreamTrack(track?.mediaStreamTrack || null);
if (track) {
- const handleStarted = () => setMediaStreamTrack(track.mediaStreamTrack);
- track.on('started', handleStarted);
+ const handleEvent = () => setMediaStreamTrack(track.mediaStreamTrack);
+ track.on('started', handleEvent);
+ track.on('switchedOn', handleEvent);
+ track.on('switchedOff', handleEvent);
return () => {
- track.off('started', handleStarted);
+ track.off('started', handleEvent);
+ track.off('switchedOn', handleEvent);
+ track.off('switchedOff', handleEvent);
};
}
}, [track]);
diff --git a/src/hooks/useSpeakerViewParticipants/useSpeakerViewParticipants.test.tsx b/src/hooks/useSpeakerViewParticipants/useSpeakerViewParticipants.test.tsx
index 76d899000..fbadc7e24 100644
--- a/src/hooks/useSpeakerViewParticipants/useSpeakerViewParticipants.test.tsx
+++ b/src/hooks/useSpeakerViewParticipants/useSpeakerViewParticipants.test.tsx
@@ -45,6 +45,22 @@ describe('the useSpeakerViewParticipants hook', () => {
expect(result.current).toEqual(['participant1', 'participant2']);
});
+ it('should return an array of mockParticipants after a room becomes available', () => {
+ mockedVideoContext.mockImplementation(() => ({
+ room: null,
+ }));
+
+ const { result, rerender } = renderHook(usePresentationParticipants);
+
+ mockedVideoContext.mockImplementation(() => ({
+ room: mockRoom,
+ }));
+
+ rerender();
+
+ expect(result.current).toEqual(['participant1', 'participant2']);
+ });
+
it('should return respond to "participantConnected" events', async () => {
const { result } = renderHook(useSpeakerViewParticipants);
act(() => {
diff --git a/src/hooks/useTrackSwitchOffReason/useTrackSwitchOffReason.ts b/src/hooks/useTrackSwitchOffReason/useTrackSwitchOffReason.ts
new file mode 100644
index 000000000..3aa34c12f
--- /dev/null
+++ b/src/hooks/useTrackSwitchOffReason/useTrackSwitchOffReason.ts
@@ -0,0 +1,28 @@
+import { useState, useEffect } from 'react';
+import { LocalAudioTrack, LocalVideoTrack, RemoteAudioTrack, RemoteVideoTrack } from 'twilio-video';
+
+type TrackType = LocalAudioTrack | LocalVideoTrack | RemoteAudioTrack | RemoteVideoTrack | undefined | null;
+
+export default function useTrackSwitchOffReason(track: TrackType) {
+ //@ts-ignore
+ const [switchOffReason, setSwitchOffReason] = useState(track?.switchOffReason);
+
+ useEffect(() => {
+ if (track) {
+ // @ts-ignore
+ setSwitchOffReason(track.switchOffReason);
+ const handleEvent = (_track: TrackType) => {
+ // @ts-ignore
+ setSwitchOffReason(_track.switchOffReason);
+ };
+ track.on('switchedOff', handleEvent);
+ track.on('switchedOn', handleEvent);
+ return () => {
+ track.off('switchedOff', handleEvent);
+ track.off('switchedOn', handleEvent);
+ };
+ }
+ }, [track]);
+
+ return switchOffReason;
+}
diff --git a/src/state/settings/settingsReducer.ts b/src/state/settings/settingsReducer.ts
index 56df03330..ac44511e9 100644
--- a/src/state/settings/settingsReducer.ts
+++ b/src/state/settings/settingsReducer.ts
@@ -1,12 +1,15 @@
-import { Track, VideoBandwidthProfileOptions } from 'twilio-video';
+import { Track, VideoBandwidthProfile } from 'twilio-video';
+
+const searchParams = new URLSearchParams(window.location.search);
export interface Settings {
- trackSwitchOffMode: VideoBandwidthProfileOptions['trackSwitchOffMode'];
+ trackSwitchOffMode: VideoBandwidthProfile['trackSwitchOffMode'];
dominantSpeakerPriority?: Track.Priority;
- bandwidthProfileMode: VideoBandwidthProfileOptions['mode'];
+ bandwidthProfileMode: VideoBandwidthProfile['mode'];
maxAudioBitrate: string;
contentPreferencesMode?: 'auto' | 'manual';
clientTrackSwitchOffControl?: 'auto' | 'manual';
+ adaptiveSimulcast: string;
}
type SettingsKeys = keyof Settings;
@@ -20,9 +23,10 @@ export const initialSettings: Settings = {
trackSwitchOffMode: undefined,
dominantSpeakerPriority: 'standard',
bandwidthProfileMode: 'collaboration',
- maxAudioBitrate: '16000',
+ maxAudioBitrate: '16',
contentPreferencesMode: 'auto',
clientTrackSwitchOffControl: 'auto',
+ adaptiveSimulcast: searchParams.get('adaptiveSimulcast') ?? 'true',
};
// This inputLabels object is used by ConnectionOptions.tsx. It is used to populate the id, name, and label props
diff --git a/src/utils/useConnectionOptions/useConnectionOptions.ts b/src/utils/useConnectionOptions/useConnectionOptions.ts
index d7c6d8ec1..fce798ccf 100644
--- a/src/utils/useConnectionOptions/useConnectionOptions.ts
+++ b/src/utils/useConnectionOptions/useConnectionOptions.ts
@@ -2,6 +2,8 @@ import { ConnectOptions } from 'twilio-video';
import { isMobile, removeUndefineds } from '..';
import { useAppState } from '../../state';
+const searchParams = new URLSearchParams(window.location.search);
+
export default function useConnectionOptions() {
const { settings } = useAppState();
@@ -19,6 +21,16 @@ export default function useConnectionOptions() {
trackSwitchOffMode: settings.trackSwitchOffMode,
contentPreferencesMode: settings.contentPreferencesMode,
clientTrackSwitchOffControl: settings.clientTrackSwitchOffControl,
+ // @ts-ignore Internal Property
+ maxSwitchedOnTracks: searchParams.get('maxVideoTracks')
+ ? parseInt(searchParams.get('maxVideoTracks')!)
+ : undefined,
+ },
+ audio: {
+ // @ts-ignore Internal Property
+ maxSwitchedOnTracks: searchParams.get('maxAudioTracks')
+ ? parseInt(searchParams.get('maxAudioTracks')!)
+ : undefined,
},
},
dominantSpeaker: true,
@@ -27,7 +39,7 @@ export default function useConnectionOptions() {
// Comment this line if you are playing music.
maxAudioBitrate: Number(settings.maxAudioBitrate),
- preferredVideoCodecs: 'auto',
+ preferredVideoCodecs: settings.adaptiveSimulcast === 'true' ? 'auto' : [{ codec: 'VP8', simulcast: true }],
//@ts-ignore - Internal use only. This property is not exposed in type definitions.
environment: process.env.REACT_APP_TWILIO_ENVIRONMENT,
@@ -35,7 +47,7 @@ export default function useConnectionOptions() {
// For mobile browsers, limit the maximum incoming video bitrate to 2.5 Mbps.
if (isMobile && connectionOptions?.bandwidthProfile?.video) {
- connectionOptions!.bandwidthProfile!.video!.maxSubscriptionBitrate = 2500000;
+ connectionOptions!.bandwidthProfile!.video!.maxSubscriptionBitrate = 2500;
}
if (process.env.REACT_APP_TWILIO_ENVIRONMENT === 'dev') {
diff --git a/twilio-video-room-monitor-1.0.0.tgz b/twilio-video-room-monitor-1.0.0.tgz
new file mode 100644
index 000000000..5b363c1e0
Binary files /dev/null and b/twilio-video-room-monitor-1.0.0.tgz differ