From 0b1bdafa8e8b57e22ba460d1ace9dd0d5982660f Mon Sep 17 00:00:00 2001 From: Horatiu Muresan <39557534+horymury@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:47:48 +0300 Subject: [PATCH] feat(external-api) add command for setting camera facing mode (#13541) - added command for setting the camera facing mode remotely - enhanced toggleVideo command to optionally accept the facing mode - fix(startSilent) do not create audio track when start silent --- conference.js | 2 +- lang/main.json | 2 + modules/API/API.js | 24 +++++--- modules/API/external/external_api.js | 3 +- react/features/base/tracks/actions.web.ts | 58 ++++++++++++++++++- .../web/AllowToggleCameraDialog.tsx | 44 ++++++++++++++ react/features/base/tracks/constants.ts | 4 ++ .../{middleware.ts => middleware.any.ts} | 0 .../features/conference/middleware.native.ts | 1 + react/features/conference/middleware.web.ts | 45 ++++++++++++++ 10 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 react/features/base/tracks/components/web/AllowToggleCameraDialog.tsx create mode 100644 react/features/base/tracks/constants.ts rename react/features/conference/{middleware.ts => middleware.any.ts} (100%) create mode 100644 react/features/conference/middleware.native.ts create mode 100644 react/features/conference/middleware.web.ts diff --git a/conference.js b/conference.js index a1cd055239a..5c9239f0852 100644 --- a/conference.js +++ b/conference.js @@ -441,7 +441,7 @@ export default { // Always get a handle on the audio input device so that we have statistics (such as "No audio input" or // "Are you trying to speak?" ) even if the user joins the conference muted. - const initialDevices = config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ]; + const initialDevices = config.startSilent || config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ]; const requestedAudio = !config.disableInitialGUM; let requestedVideo = false; diff --git a/lang/main.json b/lang/main.json index deb0af7b16f..32249de7a29 100644 --- a/lang/main.json +++ b/lang/main.json @@ -269,6 +269,8 @@ "addMeetingNote": "Add a note about this meeting", "addOptionalNote": "Add a note (optional):", "allow": "Allow", + "allowToggleCameraDialog": "Do you allow {{initiatorName}} to toggle your camera facing mode?", + "allowToggleCameraTitle": "Allow toggle camera?", "alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.", "alreadySharedVideoTitle": "Only one shared video is allowed at a time", "applicationWindow": "Application window", diff --git a/modules/API/API.js b/modules/API/API.js index cd12dc45297..5ceb00961f9 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -50,8 +50,8 @@ import { } from '../../react/features/base/participants/functions'; import { updateSettings } from '../../react/features/base/settings/actions'; import { getDisplayName } from '../../react/features/base/settings/functions.web'; -import { toggleCamera } from '../../react/features/base/tracks/actions.any'; -import { isToggleCameraEnabled } from '../../react/features/base/tracks/functions'; +import { setCameraFacingMode } from '../../react/features/base/tracks/actions.web'; +import { CAMERA_FACING_MODE_MESSAGE } from '../../react/features/base/tracks/constants'; import { autoAssignToBreakoutRooms, closeBreakoutRoom, @@ -395,12 +395,8 @@ function initCommands() { sendAnalytics(createApiEvent('film.strip.resize')); APP.store.dispatch(resizeFilmStrip(options.width)); }, - 'toggle-camera': () => { - if (!isToggleCameraEnabled(APP.store.getState())) { - return; - } - - APP.store.dispatch(toggleCamera()); + 'toggle-camera': facingMode => { + APP.store.dispatch(setCameraFacingMode(facingMode)); }, 'toggle-camera-mirror': () => { const state = APP.store.getState(); @@ -529,6 +525,18 @@ function initCommands() { logger.error('Failed sending endpoint text message', err); } }, + 'send-camera-facing-mode-message': (to, facingMode) => { + if (!to) { + logger.warn('Participant id not set'); + + return; + } + + APP.conference.sendEndpointMessage(to, { + name: CAMERA_FACING_MODE_MESSAGE, + facingMode + }); + }, 'overwrite-names': participantList => { logger.debug('Overwrite names command received'); diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index ef2c5d69b5a..efff433f5c5 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -54,6 +54,7 @@ const commands = { removeBreakoutRoom: 'remove-breakout-room', resizeFilmStrip: 'resize-film-strip', resizeLargeVideo: 'resize-large-video', + sendCameraFacingMode: 'send-camera-facing-mode-message', sendChatMessage: 'send-chat-message', sendEndpointTextMessage: 'send-endpoint-text-message', sendParticipantToRoom: 'send-participant-to-room', @@ -111,6 +112,7 @@ const events = { 'data-channel-opened': 'dataChannelOpened', 'device-list-changed': 'deviceListChanged', 'display-name-change': 'displayNameChange', + 'dominant-speaker-changed': 'dominantSpeakerChanged', 'email-change': 'emailChange', 'error-occurred': 'errorOccurred', 'endpoint-text-message-received': 'endpointTextMessageReceived', @@ -152,7 +154,6 @@ const events = { 'video-mute-status-changed': 'videoMuteStatusChanged', 'video-quality-changed': 'videoQualityChanged', 'screen-sharing-status-changed': 'screenSharingStatusChanged', - 'dominant-speaker-changed': 'dominantSpeakerChanged', 'subject-change': 'subjectChange', 'suspend-detected': 'suspendDetected', 'tile-view-changed': 'tileViewChanged', diff --git a/react/features/base/tracks/actions.web.ts b/react/features/base/tracks/actions.web.ts index ffe6915d1be..1dc378c40f2 100644 --- a/react/features/base/tracks/actions.web.ts +++ b/react/features/base/tracks/actions.web.ts @@ -13,18 +13,23 @@ import { toggleScreenshotCaptureSummary } from '../../screenshot-capture/actions import { isScreenshotCaptureEnabled } from '../../screenshot-capture/functions'; import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect'; import { getCurrentConference } from '../conference/functions'; +import { openDialog } from '../dialog/actions'; import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet'; import { setScreenshareMuted } from '../media/actions'; import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants'; import { addLocalTrack, - replaceLocalTrack + replaceLocalTrack, + toggleCamera } from './actions.any'; +import AllowToggleCameraDialog from './components/web/AllowToggleCameraDialog'; import { createLocalTracksF, getLocalDesktopTrack, - getLocalJitsiAudioTrack + getLocalJitsiAudioTrack, + getLocalVideoTrack, + isToggleCameraEnabled } from './functions'; import { IShareOptions, IToggleScreenSharingOptions } from './types'; @@ -263,3 +268,52 @@ async function _toggleScreenSharing( APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails); } } + +/** + * Sets the camera facing mode(environment/user). If facing mode not provided, it will do a toggle. + * + * @param {string | undefined} facingMode - The selected facing mode. + * @returns {void} + */ +export function setCameraFacingMode(facingMode: string | undefined) { + return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + const state = getState(); + + if (!isToggleCameraEnabled(state)) { + return; + } + + if (!facingMode) { + dispatch(toggleCamera()); + + return; + } + + const tracks = state['features/base/tracks']; + const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack; + + if (!tracks || !localVideoTrack) { + return; + } + + const currentFacingMode = localVideoTrack.getCameraFacingMode(); + + if (currentFacingMode !== facingMode) { + dispatch(toggleCamera()); + } + }; +} + +/** + * Signals to open the permission dialog for toggling camera remotely. + * + * @param {Function} onAllow - Callback to be executed if permission to toggle camera was granted. + * @param {string} initiatorId - The participant id of the requester. + * @returns {Object} - The open dialog action. + */ +export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: string) { + return openDialog(AllowToggleCameraDialog, { + onAllow, + initiatorId + }); +} diff --git a/react/features/base/tracks/components/web/AllowToggleCameraDialog.tsx b/react/features/base/tracks/components/web/AllowToggleCameraDialog.tsx new file mode 100644 index 00000000000..cb80e7b757e --- /dev/null +++ b/react/features/base/tracks/components/web/AllowToggleCameraDialog.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { WithTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import { IReduxState } from '../../../../app/types'; +import { translate } from '../../../i18n/functions'; +import { getParticipantDisplayName } from '../../../participants/functions'; +import Dialog from '../../../ui/components/web/Dialog'; + + +interface IProps extends WithTranslation { + + /** + * The participant id of the toggle camera requester. + */ + initiatorId: string; + + /** + * Function to be invoked after permission to toggle camera granted. + */ + onAllow: () => void; +} + +/** + * Dialog to allow toggling camera remotely. + * + * @returns {JSX.Element} - The allow toggle camera dialog. + */ +const AllowToggleCameraDialog = ({ onAllow, t, initiatorId }: IProps): JSX.Element => { + const initiatorName = useSelector((state: IReduxState) => getParticipantDisplayName(state, initiatorId)); + + return ( + +
+ { t('dialog.allowToggleCameraDialog', { initiatorName }) } +
+
+ ); +}; + +export default translate(AllowToggleCameraDialog); diff --git a/react/features/base/tracks/constants.ts b/react/features/base/tracks/constants.ts new file mode 100644 index 00000000000..abbe4abc390 --- /dev/null +++ b/react/features/base/tracks/constants.ts @@ -0,0 +1,4 @@ +/** + * The payload name for remotely setting the camera facing mode message. + */ +export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message'; diff --git a/react/features/conference/middleware.ts b/react/features/conference/middleware.any.ts similarity index 100% rename from react/features/conference/middleware.ts rename to react/features/conference/middleware.any.ts diff --git a/react/features/conference/middleware.native.ts b/react/features/conference/middleware.native.ts new file mode 100644 index 00000000000..fefd329e8ae --- /dev/null +++ b/react/features/conference/middleware.native.ts @@ -0,0 +1 @@ +import './middleware.any'; diff --git a/react/features/conference/middleware.web.ts b/react/features/conference/middleware.web.ts new file mode 100644 index 00000000000..d5f630be6ee --- /dev/null +++ b/react/features/conference/middleware.web.ts @@ -0,0 +1,45 @@ + +import { CONFERENCE_JOINED } from '../base/conference/actionTypes'; +import { IJitsiConference } from '../base/conference/reducer'; +import { JitsiConferenceEvents } from '../base/lib-jitsi-meet'; +import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; +import { openAllowToggleCameraDialog, setCameraFacingMode } from '../base/tracks/actions.web'; +import { CAMERA_FACING_MODE_MESSAGE } from '../base/tracks/constants'; + +import './middleware.any'; + +MiddlewareRegistry.register(_store => next => action => { + switch (action.type) { + case CONFERENCE_JOINED: { + _addSetCameraFacingModeListener(action.conference); + break; + } + } + + return next(action); +}); + +/** + * Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that + * will perform various chat related activities. + * + * @param {IJitsiConference} conference - The conference. + * @returns {void} + */ +function _addSetCameraFacingModeListener(conference: IJitsiConference) { + conference.on( + JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, + (...args: any) => { + if (args && args.length >= 2) { + const [ sender, eventData ] = args; + + if (eventData.name === CAMERA_FACING_MODE_MESSAGE) { + APP.store.dispatch(openAllowToggleCameraDialog( + /* onAllow */ () => APP.store.dispatch(setCameraFacingMode(eventData.facingMode)), + /* initiatorId */ sender._id + )); + } + } + } + ); +}