From aef0287605f8839bbd5235b1537073e5f60b22ed Mon Sep 17 00:00:00 2001
From: Mihai-Andrei Uscat <52234168+muscat1@users.noreply.github.com>
Date: Wed, 17 Mar 2021 10:44:18 +0200
Subject: [PATCH] feat(ToggleCamera): Implement for web.
---
.eslintignore | 1 +
react/features/base/config/constants.js | 3 +-
.../base/icons/svg/camera-refresh.svg | 3 +
react/features/base/icons/svg/index.js | 1 +
.../base/lib-jitsi-meet/functions.any.js | 7 +-
react/features/base/tracks/actions.js | 40 +++++++++-
.../components/web/ToggleCameraButton.js | 77 +++++++++++++++++++
.../toolbox/components/web/Toolbox.js | 7 +-
8 files changed, 134 insertions(+), 5 deletions(-)
create mode 100644 react/features/base/icons/svg/camera-refresh.svg
create mode 100644 react/features/toolbox/components/web/ToggleCameraButton.js
diff --git a/.eslintignore b/.eslintignore
index 1e6b4b72b5..d19de9fbb0 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -7,6 +7,7 @@ flow-typed/*
libs/*
resources/*
react/features/stream-effects/virtual-background/vendor/*
+load-test/*
# ESLint will by default ignore its own configuration file. However, there does
# not seem to be a reason why we will want to risk being inconsistent with our
diff --git a/react/features/base/config/constants.js b/react/features/base/config/constants.js
index 8f63ecb7ac..db004c17b7 100644
--- a/react/features/base/config/constants.js
+++ b/react/features/base/config/constants.js
@@ -18,5 +18,6 @@ export const TOOLBAR_BUTTONS = [
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
- 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
+ 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
+ 'security', 'toggle-camera'
];
diff --git a/react/features/base/icons/svg/camera-refresh.svg b/react/features/base/icons/svg/camera-refresh.svg
new file mode 100644
index 0000000000..3ba72747f3
--- /dev/null
+++ b/react/features/base/icons/svg/camera-refresh.svg
@@ -0,0 +1,3 @@
+
diff --git a/react/features/base/icons/svg/index.js b/react/features/base/icons/svg/index.js
index f5f84e4cdf..713997e542 100644
--- a/react/features/base/icons/svg/index.js
+++ b/react/features/base/icons/svg/index.js
@@ -20,6 +20,7 @@ export { default as IconCamera } from './camera.svg';
export { default as IconCameraDisabled } from './camera-disabled.svg';
export { default as IconCameraEmpty } from './camera-empty.svg';
export { default as IconCameraEmptyDisabled } from './camera-empty-disabled.svg';
+export { default as IconCameraRefresh } from './camera-refresh.svg';
export { default as IconCancelSelection } from './cancel.svg';
export { default as IconChat } from './chat.svg';
export { default as IconChatSend } from './send.svg';
diff --git a/react/features/base/lib-jitsi-meet/functions.any.js b/react/features/base/lib-jitsi-meet/functions.any.js
index f86a7fd2a2..a337558a00 100644
--- a/react/features/base/lib-jitsi-meet/functions.any.js
+++ b/react/features/base/lib-jitsi-meet/functions.any.js
@@ -14,9 +14,11 @@ const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
* are "video" or "audio".
* @param {string} deviceId - The id of the target media source.
* @param {number} [timeout] - A timeout for the JitsiMeetJS.createLocalTracks function call.
+ * @param {Object} additionalOptions - Extra options to be passed to lib-jitsi-meet's {@code createLocalTracks}.
+ *
* @returns {Promise}
*/
-export function createLocalTrack(type: string, deviceId: string, timeout: ?number) {
+export function createLocalTrack(type: string, deviceId: string, timeout: ?number, additionalOptions: ?Object) {
return (
JitsiMeetJS.createLocalTracks({
cameraDeviceId: deviceId,
@@ -26,7 +28,8 @@ export function createLocalTrack(type: string, deviceId: string, timeout: ?numbe
firefox_fake_device:
window.config && window.config.firefox_fake_device,
micDeviceId: deviceId,
- timeout
+ timeout,
+ ...additionalOptions
})
.then(([ jitsiLocalTrack ]) => jitsiLocalTrack));
}
diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js
index 5be7d504b7..274630eabd 100644
--- a/react/features/base/tracks/actions.js
+++ b/react/features/base/tracks/actions.js
@@ -1,9 +1,11 @@
+/* global APP */
+
import {
createTrackMutedEvent,
sendAnalytics
} from '../../analytics';
import { showErrorNotification, showNotification } from '../../notifications';
-import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
+import { JitsiTrackErrors, JitsiTrackEvents, createLocalTrack } from '../lib-jitsi-meet';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
@@ -13,6 +15,7 @@ import {
VIDEO_TYPE
} from '../media';
import { getLocalParticipant } from '../participants';
+import { updateSettings } from '../settings';
import {
SET_NO_SRC_DATA_NOTIFICATION_UID,
@@ -717,3 +720,38 @@ export function updateLastTrackVideoMediaEvent(track, name) {
name
};
}
+
+/**
+ * Toggles the facingMode constraint on the video stream.
+ *
+ * @returns {Function}
+ */
+export function toggleCamera() {
+ return async (dispatch, getState) => {
+ const state = getState();
+ const tracks = state['features/base/tracks'];
+ const localVideoTrack = getLocalVideoTrack(tracks).jitsiTrack;
+ const currentFacingMode = localVideoTrack.getCameraFacingMode();
+
+ /**
+ * FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
+ * but it seems to not trigger the re-rendering of the local video on Chrome;
+ * could be due to a plan B vs unified plan issue. Therefore, we use the legacy
+ * method defined in conference.js that manually takes care of updating the local
+ * video as well.
+ */
+ await APP.conference.useVideoStream(null);
+
+ const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
+ ? CAMERA_FACING_MODE.ENVIRONMENT
+ : CAMERA_FACING_MODE.USER;
+
+ // Update the flipX value so the environment facing camera is not flipped, before the new track is created.
+ dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER }));
+
+ const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
+
+ // FIXME: See above.
+ await APP.conference.useVideoStream(newVideoTrack);
+ };
+}
diff --git a/react/features/toolbox/components/web/ToggleCameraButton.js b/react/features/toolbox/components/web/ToggleCameraButton.js
new file mode 100644
index 0000000000..43c2b69bd7
--- /dev/null
+++ b/react/features/toolbox/components/web/ToggleCameraButton.js
@@ -0,0 +1,77 @@
+// @flow
+
+import { isMobileBrowser } from '../../../base/environment/utils';
+import { translate } from '../../../base/i18n';
+import { IconCameraRefresh } from '../../../base/icons';
+import { connect } from '../../../base/redux';
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
+import { isLocalCameraTrackMuted, toggleCamera } from '../../../base/tracks';
+
+/**
+ * The type of the React {@code Component} props of {@link ToggleCameraButton}.
+ */
+type Props = AbstractButtonProps & {
+
+ /**
+ * Whether the current conference is in audio only mode or not.
+ */
+ _audioOnly: boolean,
+
+ /**
+ * Whether video is currently muted or not.
+ */
+ _videoMuted: boolean,
+
+ /**
+ * The Redux dispatch function.
+ */
+ dispatch: Function
+};
+
+/**
+ * An implementation of a button for toggling the camera facing mode.
+ */
+class ToggleCameraButton extends AbstractButton {
+ accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
+ icon = IconCameraRefresh;
+ label = 'toolbar.toggleCamera';
+
+ /**
+ * Handles clicking/pressing the button.
+ *
+ * @returns {void}
+ */
+ _handleClick() {
+ this.props.dispatch(toggleCamera());
+ }
+
+ /**
+ * Whether this button is disabled or not.
+ *
+ * @returns {boolean}
+ */
+ _isDisabled() {
+ return this.props._audioOnly || this.props._videoMuted;
+ }
+}
+
+/**
+ * Maps (parts of) the redux state to the associated props for the
+ * {@code ToggleCameraButton} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @returns {Props}
+ */
+function mapStateToProps(state): Object {
+ const { enabled: audioOnly } = state['features/base/audio-only'];
+ const tracks = state['features/base/tracks'];
+ const { videoInput } = state['features/base/devices'].availableDevices;
+
+ return {
+ _audioOnly: Boolean(audioOnly),
+ _videoMuted: isLocalCameraTrackMuted(tracks),
+ visible: isMobileBrowser() && videoInput.length > 1
+ };
+}
+
+export default translate(connect(mapStateToProps)(ToggleCameraButton));
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js
index 7aba3353ca..a5ae8eb415 100644
--- a/react/features/toolbox/components/web/Toolbox.js
+++ b/react/features/toolbox/components/web/Toolbox.js
@@ -83,6 +83,7 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
import AudioSettingsButton from './AudioSettingsButton';
import OverflowMenuButton from './OverflowMenuButton';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
+import ToggleCameraButton from './ToggleCameraButton';
import ToolbarButton from './ToolbarButton';
import VideoSettingsButton from './VideoSettingsButton';
@@ -929,7 +930,7 @@ class Toolbox extends Component {
}
/**
- * Returns true if the profile button is visible and false otherwise.
+ * Returns true if the embed meeting button is visible and false otherwise.
*
* @returns {boolean}
*/
@@ -967,6 +968,10 @@ class Toolbox extends Component {
const group1 = [
...additionalButtons,
+ this._shouldShowButton('toggle-camera')
+ && ,
this._shouldShowButton('videoquality')
&&