Remove volume button in call interface in Firefox
Add click event for `CallingVolumeButton` and `CallingScreenShareButton`
in call interface to open their respective menu.
Remove localVideoRef and remoteVideoRef from CallProvider
Change-Id: I87601153cb3e24b38a5f764b11935e32402caa31
diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx
index a89726c..4e0d5ac 100644
--- a/client/src/components/Button.tsx
+++ b/client/src/components/Button.tsx
@@ -108,6 +108,7 @@
export type ExpandableButtonProps = IconButtonProps & {
isVertical?: boolean;
+ expandMenuOnClick?: boolean;
Icon?: ComponentType<SvgIconProps>;
expandMenuOptions?: (ExpandMenuOption | ExpandMenuRadioOption)[];
IconButtonComp?: ComponentType<IconButtonProps>;
@@ -116,6 +117,7 @@
export const ExpandableButton = ({
isVertical,
Icon,
+ expandMenuOnClick,
expandMenuOptions = undefined,
IconButtonComp = IconButton,
...props
@@ -194,7 +196,18 @@
/>
</IconButton>
)}
- <IconButtonComp {...props}>{Icon && <Icon />}</IconButtonComp>
+ <IconButtonComp
+ onClick={
+ expandMenuOnClick
+ ? (event) => {
+ setAnchorEl(event.currentTarget);
+ }
+ : undefined
+ }
+ {...props}
+ >
+ {Icon && <Icon />}
+ </IconButtonComp>
</Box>
</>
);
diff --git a/client/src/components/CallButtons.tsx b/client/src/components/CallButtons.tsx
index 2ecc4fc..d1beab8 100644
--- a/client/src/components/CallButtons.tsx
+++ b/client/src/components/CallButtons.tsx
@@ -152,6 +152,7 @@
return (
<CallButton
aria-label="screen share"
+ expandMenuOnClick
Icon={ScreenShareArrowIcon}
expandMenuOptions={[
{
@@ -206,17 +207,13 @@
export const CallingVolumeButton = (props: ExpandableButtonProps) => {
const options = useMediaDeviceExpandMenuOptions('audiooutput');
- const { remoteVideoRef } = useContext(CallContext);
-
- // Audio out options are only available on chrome and other browsers that support `setSinkId`
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility
- const hasSetSinkId = remoteVideoRef.current?.setSinkId != null;
return (
<CallButton
aria-label="volume options"
+ expandMenuOnClick
Icon={VolumeIcon}
- expandMenuOptions={hasSetSinkId ? options : undefined}
+ expandMenuOptions={options}
{...props}
/>
);
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index fdb6935..13e0d24 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -16,7 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
import { CallAction, CallBegin, WebSocketMessageType } from 'jami-web-common';
-import { createContext, MutableRefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import LoadingPage from '../components/Loading';
@@ -46,24 +46,10 @@
};
type CurrentMediaDeviceIds = Record<MediaDeviceKind, MediaDeviceIdState>;
-/**
- * HTMLVideoElement with the `sinkId` and `setSinkId` optional properties.
- *
- * These properties are defined only on supported browsers
- * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility
- */
-interface VideoElementWithSinkId extends HTMLVideoElement {
- sinkId?: string;
- setSinkId?: (deviceId: string) => void;
-}
-
export interface ICallContext {
mediaDevices: MediaDevicesInfo;
currentMediaDeviceIds: CurrentMediaDeviceIds;
- localVideoRef: MutableRefObject<VideoElementWithSinkId | null>;
- remoteVideoRef: MutableRefObject<VideoElementWithSinkId | null>;
-
isAudioOn: boolean;
setIsAudioOn: SetState<boolean>;
isVideoOn: boolean;
@@ -101,9 +87,6 @@
},
},
- localVideoRef: { current: null },
- remoteVideoRef: { current: null },
-
isAudioOn: false,
setIsAudioOn: () => {},
isVideoOn: false,
@@ -144,9 +127,6 @@
const { conversationId, conversation } = useContext(ConversationContext);
const navigate = useNavigate();
- const localVideoRef = useRef<HTMLVideoElement | null>(null);
- const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
-
const [mediaDevices, setMediaDevices] = useState(defaultCallContext.mediaDevices);
const [audioInputDeviceId, setAudioInputDeviceId] = useState<string>();
const [audioOutputDeviceId, setAudioOutputDeviceId] = useState<string>();
@@ -404,8 +384,6 @@
value={{
mediaDevices,
currentMediaDeviceIds,
- localVideoRef,
- remoteVideoRef,
isAudioOn,
setIsAudioOn,
isVideoOn,
diff --git a/client/src/pages/CallInterface.tsx b/client/src/pages/CallInterface.tsx
index b58b326..54c9b10 100644
--- a/client/src/pages/CallInterface.tsx
+++ b/client/src/pages/CallInterface.tsx
@@ -49,6 +49,7 @@
import { CallContext, CallStatus } from '../contexts/CallProvider';
import { ConversationContext } from '../contexts/ConversationProvider';
import { WebRtcContext } from '../contexts/WebRtcProvider';
+import { VideoElementWithSinkId } from '../utils/utils';
import { CallPending } from './CallPending';
export default () => {
@@ -90,9 +91,9 @@
currentMediaDeviceIds: {
audiooutput: { id: audioOutDeviceId },
},
- localVideoRef,
- remoteVideoRef,
} = useContext(CallContext);
+ const localVideoRef = useRef<VideoElementWithSinkId | null>(null);
+ const remoteVideoRef = useRef<VideoElementWithSinkId | null>(null);
const gridItemRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
@@ -122,6 +123,8 @@
}
}, [audioOutDeviceId, remoteVideoRef]);
+ const hasSetSinkId = remoteVideoRef.current?.setSinkId != null;
+
return (
<Box display="flex" flexGrow={1}>
<video
@@ -157,7 +160,7 @@
</div>
</Grid>
<Grid item xs sx={{ display: 'flex', justifyContent: 'flex-end' }} ref={gridItemRef}>
- <CallInterfaceSecondaryButtons gridItemRef={gridItemRef} />
+ <CallInterfaceSecondaryButtons showVolumeButton={hasSetSinkId} gridItemRef={gridItemRef} />
</Grid>
</Grid>
</Box>
@@ -233,24 +236,34 @@
CallingFullScreenButton,
];
-const CallInterfaceSecondaryButtons = (props: Props & { gridItemRef: RefObject<HTMLElement> }) => {
+const CallInterfaceSecondaryButtons = ({
+ gridItemRef,
+ showVolumeButton,
+}: Props & { showVolumeButton: boolean; gridItemRef: RefObject<HTMLElement> }) => {
const stackRef = useRef<HTMLElement>(null);
const [initialMeasurementDone, setInitialMeasurementDone] = useState(false);
const [hiddenStackCount, setHiddenStackCount] = useState(0);
const [hiddenMenuVisible, setHiddenMenuVisible] = useState(false);
+ // Audio out options are only available on Chrome and other browsers that support `setSinkId`.
+ // This removes the `CallingVolumeButton` if `setSinkId` is not defined.
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility
+ const secondaryButtons = useMemo(() => {
+ return showVolumeButton ? SECONDARY_BUTTONS : SECONDARY_BUTTONS.slice(1);
+ }, [showVolumeButton]);
+
const calculateStackCount = useCallback(() => {
- if (stackRef?.current && props.gridItemRef?.current) {
+ if (stackRef?.current && gridItemRef?.current) {
const buttonWidth = stackRef.current.children[0].clientWidth;
- const availableSpace = props.gridItemRef.current.clientWidth;
+ const availableSpace = gridItemRef.current.clientWidth;
let availableButtons = Math.floor((availableSpace - 1) / buttonWidth);
- if (availableButtons < SECONDARY_BUTTONS.length) {
+ if (availableButtons < secondaryButtons.length) {
availableButtons -= 1; // Leave room for CallingMoreVerticalButton
}
- setHiddenStackCount(SECONDARY_BUTTONS.length - availableButtons);
+ setHiddenStackCount(secondaryButtons.length - availableButtons);
}
- }, [props.gridItemRef]);
+ }, [gridItemRef, secondaryButtons]);
useLayoutEffect(() => {
// Run once, at the beginning, for initial measurements
@@ -271,19 +284,20 @@
const { displayedButtons, hiddenButtons } = useMemo(() => {
const displayedButtons: ComponentType<ExpandableButtonProps>[] = [];
const hiddenButtons: ComponentType<ExpandableButtonProps>[] = [];
- SECONDARY_BUTTONS.forEach((button, i) => {
- if (i < SECONDARY_BUTTONS.length - hiddenStackCount) {
+ for (let i = 0; i < secondaryButtons.length; i++) {
+ const button = secondaryButtons[i];
+ if (i < secondaryButtons.length - hiddenStackCount) {
displayedButtons.push(button);
} else {
hiddenButtons.push(button);
}
- });
+ }
return {
displayedButtons,
hiddenButtons,
};
- }, [hiddenStackCount]);
+ }, [hiddenStackCount, secondaryButtons]);
return (
<Card sx={{ backgroundColor: '#00000088', overflow: 'visible', height: '100%' }}>
diff --git a/client/src/pages/CallPending.tsx b/client/src/pages/CallPending.tsx
index 02f06e4..4bdbef4 100644
--- a/client/src/pages/CallPending.tsx
+++ b/client/src/pages/CallPending.tsx
@@ -17,7 +17,7 @@
*/
import { Box, CircularProgress, Grid, IconButtonProps, Stack, Typography } from '@mui/material';
-import { ComponentType, ReactNode, useContext, useEffect, useMemo } from 'react';
+import { ComponentType, ReactNode, useContext, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
@@ -31,11 +31,13 @@
import { CallContext, CallStatus } from '../contexts/CallProvider';
import { ConversationContext } from '../contexts/ConversationProvider';
import { WebRtcContext } from '../contexts/WebRtcProvider';
+import { VideoElementWithSinkId } from '../utils/utils';
export const CallPending = () => {
const { localStream } = useContext(WebRtcContext);
const { conversation } = useContext(ConversationContext);
- const { callRole, localVideoRef } = useContext(CallContext);
+ const { callRole } = useContext(CallContext);
+ const localVideoRef = useRef<VideoElementWithSinkId | null>(null);
useEffect(() => {
if (localStream && localVideoRef.current) {
diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts
index ffe37a4..770c7a8 100644
--- a/client/src/utils/utils.ts
+++ b/client/src/utils/utils.ts
@@ -22,3 +22,14 @@
};
export type SetState<T> = Dispatch<SetStateAction<T>>;
+
+/**
+ * HTMLVideoElement with the `sinkId` and `setSinkId` optional properties.
+ *
+ * These properties are defined only on supported browsers
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility
+ */
+export interface VideoElementWithSinkId extends HTMLVideoElement {
+ sinkId?: string;
+ setSinkId?: (deviceId: string) => void;
+}