Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Disable self-view of webcam #17935

Merged
merged 4 commits into from May 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -152,7 +152,7 @@ class Settings extends Component {
const { intl } = this.props;
if (textOnly) {
return status ? intl.formatMessage(intlMessages.on)
: intl.formatMessage(intlMessages.off)
: intl.formatMessage(intlMessages.off);
}
return (
<Styled.ToggleLabel aria-hidden>
Expand Down
Expand Up @@ -117,6 +117,9 @@ const intlMessages = defineMessages({
id: 'app.layout.style.customPush',
description: 'label for custom layout style (push to all)',
},
disableLabel: {
id: 'app.videoDock.webcamDisableLabel',
}
});

class ApplicationMenu extends BaseMenu {
Expand Down Expand Up @@ -447,6 +450,29 @@ class ApplicationMenu extends BaseMenu {
</Styled.Col>
</Styled.Row>

<Styled.Row>
<Styled.Col aria-hidden="true">
<Styled.FormElement>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to use eslint-disable only in exceptional circumstances.

<Styled.Label>
{intl.formatMessage(intlMessages.disableLabel)}
</Styled.Label>
</Styled.FormElement>
</Styled.Col>
<Styled.Col>
<Styled.FormElementRight>
{displaySettingsStatus(settings.disableCam)}
<Toggle
icons={false}
defaultChecked={settings.disableCam}
onChange={() => this.handleToggle('disableCam')}
ariaLabel={`${intl.formatMessage(intlMessages.disableLabel)} - ${displaySettingsStatus(settings.disableCam, false)}`}
showToggleLabel={showToggleLabel}
/>
</Styled.FormElementRight>
</Styled.Col>
</Styled.Row>

<Styled.Row>
<Styled.Col>
<Styled.FormElement>
Expand Down
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useRef, useState } from 'react';
import { injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
Expand All @@ -22,13 +23,14 @@ const VideoListItem = (props) => {
const {
name, voiceUser, isFullscreenContext, layoutContextDispatch, user, onHandleVideoFocus,
cameraId, numOfStreams, focused, onVideoItemMount, onVideoItemUnmount, onVirtualBgDrop,
makeDragOperations, dragging, draggingOver, isRTL
makeDragOperations, dragging, draggingOver, isRTL,
} = props;

const [videoDataLoaded, setVideoDataLoaded] = useState(false);
const [isStreamHealthy, setIsStreamHealthy] = useState(false);
const [isMirrored, setIsMirrored] = useState(VideoService.mirrorOwnWebcam(user?.userId));
const [isVideoSqueezed, setIsVideoSqueezed] = useState(false);
const [isCamDisabled, setIsCamDisabled] = useState(false);

const resizeObserver = new ResizeObserver((entry) => {
if (entry && entry[0]?.contentRect?.width < VIDEO_CONTAINER_WIDTH_BOUND) {
Expand All @@ -40,7 +42,7 @@ const VideoListItem = (props) => {
const videoTag = useRef();
const videoContainer = useRef();

const videoIsReady = isStreamHealthy && videoDataLoaded;
const videoIsReady = isStreamHealthy && videoDataLoaded && !isCamDisabled;
const { animations } = Settings.application;
const talking = voiceUser?.talking;

Expand Down Expand Up @@ -88,21 +90,24 @@ const VideoListItem = (props) => {
});
}
};

// This is here to prevent the videos from freezing when they're
// moved around the dom by react, e.g., when changing the user status
// see https://bugs.chromium.org/p/chromium/issues/detail?id=382879
if (videoDataLoaded) {
if (!isCamDisabled && videoDataLoaded) {
playElement(videoTag.current);
}
}, [videoDataLoaded]);
if (isCamDisabled) {
videoTag.current.pause();
}
}, [isCamDisabled, videoDataLoaded]);

// component will unmount
useEffect(() => () => {
unsubscribeFromStreamStateChange(cameraId, onStreamStateChange);
onVideoItemUnmount(cameraId);
}, []);

useEffect(() => {
setIsCamDisabled(Settings.application.disableCam);
}, [Settings.application.disableCam]);

const renderSqueezedButton = () => (
<UserActions
name={name}
Expand All @@ -115,6 +120,7 @@ const VideoListItem = (props) => {
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
onHandleDisableCam={() => setIsCamDisabled((value) => !value)}
/>
);

Expand All @@ -139,6 +145,7 @@ const VideoListItem = (props) => {
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
onHandleDisableCam={() => setIsCamDisabled((value) => !value)}
/>
<UserStatus
voiceUser={voiceUser}
Expand Down Expand Up @@ -185,6 +192,7 @@ const VideoListItem = (props) => {
focused={focused}
onHandleMirror={() => setIsMirrored((value) => !value)}
isRTL={isRTL}
onHandleDisableCam={() => setIsCamDisabled((value) => !value)}
/>
<UserStatus
voiceUser={voiceUser}
Expand All @@ -206,7 +214,11 @@ const VideoListItem = (props) => {
draggingOver,
}}
>

<Styled.VideoContainer>
{isCamDisabled && (
<Styled.VideoDisabled> Self cam disabled </Styled.VideoDisabled>
)}
<Styled.Video
mirrored={isMirrored}
unhealthyStream={videoDataLoaded && !isStreamHealthy}
Expand All @@ -219,13 +231,14 @@ const VideoListItem = (props) => {
</Styled.VideoContainer>

{/* eslint-disable-next-line no-nested-ternary */}
{videoIsReady
? (isVideoSqueezed)
? renderSqueezedButton()
: renderDefaultButtons()
: (isVideoSqueezed)
? renderWebcamConnectingSqueezed()
: renderWebcamConnecting()}

{(videoIsReady || isCamDisabled) && (
isVideoSqueezed ? renderSqueezedButton() : renderDefaultButtons()
)}
{!videoIsReady && !isCamDisabled && (
isVideoSqueezed ? renderWebcamConnectingSqueezed() : renderWebcamConnecting()
)}

</Styled.Content>
);
};
Expand Down
Expand Up @@ -133,6 +133,23 @@ const Video = styled.video`
`}
`;

const VideoDisabled = styled.div`
color: white;
width: 100%;
height: 20%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
position: absolute;
border-radius: 10px;
z-index: 2;
top: 50%;
padding: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}`;

const TopBar = styled.div`
position: absolute;
display: flex;
Expand Down Expand Up @@ -161,4 +178,5 @@ export default {
Video,
TopBar,
BottomBar,
VideoDisabled,
};
Expand Up @@ -6,6 +6,9 @@ import FullscreenService from '/imports/ui/components/common/fullscreen-button/s
import BBBMenu from '/imports/ui/components/common/menu/component';
import PropTypes from 'prop-types';
import Styled from './styles';
import Auth from '/imports/ui/services/auth';
import Settings from '/imports/ui/services/settings';
import { updateSettings } from '/imports/ui/components/settings/service';

const intlMessages = defineMessages({
focusLabel: {
Expand All @@ -26,6 +29,9 @@ const intlMessages = defineMessages({
unpinLabel: {
id: 'app.videoDock.webcamUnpinLabel',
},
disableLabel: {
id: 'app.videoDock.webcamDisableLabel',
},
pinDesc: {
id: 'app.videoDock.webcamPinDesc',
},
Expand All @@ -46,6 +52,9 @@ const intlMessages = defineMessages({
id: 'app.videoDock.webcamSqueezedButtonLabel',
description: 'User selected webcam squeezed options',
},
disableDesc: {
id: 'app.videoDock.webcamDisableDesc',
},
});

const UserActions = (props) => {
Expand All @@ -66,12 +75,21 @@ const UserActions = (props) => {

const menuItems = [];

const toggleDisableCam = () => {
const applicationValues = { ...Settings.application };
applicationValues.disableCam = !Settings.application.disableCam;
updateSettings({
...Settings,
application: applicationValues,
});
};

if (isVideoSqueezed) {
menuItems.push({
key: `${cameraId}-name`,
label: name,
description: name,
onClick: () => {},
onClick: () => { },
disabled: true,
});

Expand All @@ -84,12 +102,21 @@ const UserActions = (props) => {
},
);
}
if (userId === Auth.userID) {
menuItems.push({
key: `${cameraId}-disable`,
label: intl.formatMessage(intlMessages.disableLabel),
description: intl.formatMessage(intlMessages.disableDesc),
onClick: () => toggleDisableCam(),
dataTest: 'disableWebcamBtn',
});
}

menuItems.push({
key: `${cameraId}-mirror`,
label: intl.formatMessage(intlMessages.mirrorLabel),
description: intl.formatMessage(intlMessages.mirrorDesc),
onClick: () => onHandleMirror(),
onClick: () => onHandleMirror(cameraId),
dataTest: 'mirrorWebcamBtn',
});

Expand Down Expand Up @@ -131,7 +158,7 @@ const UserActions = (props) => {
size="sm"
onClick={() => null}
/>
)}
)}
actions={getAvailableActions()}
/>
</Styled.MenuWrapperSqueezed>
Expand Down Expand Up @@ -185,7 +212,7 @@ export default UserActions;
UserActions.defaultProps = {
focused: false,
isVideoSqueezed: false,
videoContainer: () => {},
videoContainer: () => { },
};

UserActions.propTypes = {
Expand All @@ -204,4 +231,5 @@ UserActions.propTypes = {
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
onHandleMirror: PropTypes.func.isRequired,
onHandleDisableCam: PropTypes.func.isRequired,
};
1 change: 1 addition & 0 deletions bigbluebutton-html5/imports/ui/services/settings/index.js
Expand Up @@ -13,6 +13,7 @@ const SETTINGS = [
'cc',
'dataSaving',
'animations',
'disableCam',
];

const CHANGED_SETTINGS = 'changed_settings';
Expand Down
2 changes: 2 additions & 0 deletions bigbluebutton-html5/public/locales/en.json
Expand Up @@ -1093,6 +1093,8 @@
"app.videoDock.webcamFocusDesc": "Focus the selected webcam",
"app.videoDock.webcamUnfocusLabel": "Unfocus",
"app.videoDock.webcamUnfocusDesc": "Unfocus the selected webcam",
"app.videoDock.webcamDisableLabel": "Disable/Enable self cam",
"app.videoDock.webcamDisableDesc": "Disable/Enable self-cam output",
"app.videoDock.webcamPinLabel": "Pin",
"app.videoDock.webcamPinDesc": "Pin the selected webcam",
"app.videoDock.webcamFullscreenLabel": "Fullscreen webcam",
Expand Down