From cc5a3e499fb2d8772d4baba27e50eea742504e24 Mon Sep 17 00:00:00 2001 From: Horatiu Muresan <39557534+horymury@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:42:29 +0300 Subject: [PATCH] feat(aot-prejoin) Add support for showing AOT on prejoin --- conference.js | 13 ++++-- modules/API/API.js | 40 ++++++++++++++++- modules/API/external/external_api.js | 32 ++++++++++++- react/features/always-on-top/AlwaysOnTop.js | 31 ++++++++----- .../features/always-on-top/VideoMuteButton.js | 2 +- .../base/premeeting/components/web/Preview.js | 15 ++++++- react/features/toolbox/actions.any.js | 45 +++++++++++++++++++ .../toolbox/components/VideoMuteButton.js | 41 ++--------------- 8 files changed, 160 insertions(+), 59 deletions(-) diff --git a/conference.js b/conference.js index f9681bd68..80ebe4c93 100644 --- a/conference.js +++ b/conference.js @@ -160,6 +160,7 @@ import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/Au import { createPresenterEffect } from './react/features/stream-effects/presenter'; import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise'; import { endpointMessageReceived } from './react/features/subtitles'; +import { handleToggleVideoMuted } from './react/features/toolbox/actions.any'; import { muteLocal } from './react/features/video-menu/actions.any'; import UIEvents from './service/UI/UIEvents'; @@ -1152,9 +1153,13 @@ export default { * Simulates toolbar button click for video mute. Used by shortcuts and API. * @param {boolean} [showUI] when set to false will not display any error * dialogs in case of media permissions error. + * @param {boolean} ensureTrack - True if we want to ensure that a new track is + * created if missing. */ - toggleVideoMuted(showUI = true) { - this.muteVideo(!this.isLocalVideoMuted(), showUI); + toggleVideoMuted(showUI = true, ensureTrack = false) { + const mute = !this.isLocalVideoMuted(); + + APP.store.dispatch(handleToggleVideoMuted(mute, showUI, ensureTrack)); }, /** @@ -2421,8 +2426,8 @@ export default { APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => { this.muteAudio(muted); }); - APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => { - this.muteVideo(muted); + APP.UI.addListener(UIEvents.VIDEO_MUTED, (muted, showUI = false) => { + this.muteVideo(muted, showUI); }); room.addCommandListener(this.commands.defaults.ETHERPAD, diff --git a/modules/API/API.js b/modules/API/API.js index e0435bab4..720ff4cd3 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -41,9 +41,11 @@ import { isLocalParticipantModerator, hasRaisedHand, grantModerator, - overwriteParticipantsNames + overwriteParticipantsNames, + LOCAL_PARTICIPANT_DEFAULT_ID } from '../../react/features/base/participants'; import { updateSettings } from '../../react/features/base/settings'; +import { getDisplayName } from '../../react/features/base/settings/functions.web'; import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks'; import { autoAssignToBreakoutRooms, @@ -63,6 +65,7 @@ import { openChat } from '../../react/features/chat/actions.web'; import { processExternalDeviceRequest } from '../../react/features/device-selection/functions'; +import { appendSuffix } from '../../react/features/display-name'; import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox'; import { setMediaEncryptionKey, toggleE2EE } from '../../react/features/e2ee/actions'; import { setVolume } from '../../react/features/filmstrip'; @@ -299,7 +302,7 @@ function initCommands() { 'toggle-video': () => { sendAnalytics(createApiEvent('toggle-video')); logger.log('Video toggle: API command received'); - APP.conference.toggleVideoMuted(false /* no UI */); + APP.conference.toggleVideoMuted(false /* no UI */, true /* ensure track */); }, 'toggle-film-strip': () => { sendAnalytics(createApiEvent('film.strip.toggled')); @@ -1451,6 +1454,39 @@ class API { }); } + /** + * Notify external application (if API is enabled) that the prejoin video + * visibility had changed. + * + * @param {boolean} isVisible - Whether the prejoin video is visible. + * @returns {void} + */ + notifyPrejoinVideoVisibilityChanged(isVisible: boolean) { + this._sendEvent({ + name: 'on-prejoin-video-changed', + isVisible + }); + } + + /** + * Notify external application (if API is enabled) that the prejoin + * screen was loaded. + * + * @returns {void} + */ + notifyPrejoinLoaded() { + const state = APP.store.getState(); + const { defaultLocalDisplayName } = state['features/base/config']; + const displayName = getDisplayName(state); + + this._sendEvent({ + name: 'prejoin-screen-loaded', + id: LOCAL_PARTICIPANT_DEFAULT_ID, + displayName, + formattedDisplayName: appendSuffix(displayName, defaultLocalDisplayName) + }); + } + /** * Notify external application of an unexpected camera-related error having * occurred. diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index cbfc1bf2c..a4e092274 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -132,6 +132,7 @@ const events = { 'participant-role-changed': 'participantRoleChanged', 'participants-pane-toggled': 'participantsPaneToggled', 'password-required': 'passwordRequired', + 'prejoin-screen-loaded': 'prejoinScreenLoaded', 'proxy-connection-event': 'proxyConnectionEvent', 'raise-hand-updated': 'raiseHandUpdated', 'recording-link-available': 'recordingLinkAvailable', @@ -356,7 +357,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter { this.invite(invitees); } this._tmpE2EEKey = e2eeKey; - this._isLargeVideoVisible = true; + this._isLargeVideoVisible = false; + this._isPrejoinVideoVisible = false; this._numberOfParticipants = 0; this._participants = {}; this._myUserID = undefined; @@ -464,6 +466,24 @@ export default class JitsiMeetExternalAPI extends EventEmitter { return iframe.contentWindow.document.getElementById('largeVideo'); } + /** + * Getter for the prejoin video element in Jitsi Meet. + * + * @returns {HTMLElement|undefined} - The prejoin video. + */ + _getPrejoinVideo() { + const iframe = this.getIFrame(); + + if (!this._isPrejoinVideoVisible + || !iframe + || !iframe.contentWindow + || !iframe.contentWindow.document) { + return; + } + + return iframe.contentWindow.document.getElementById('prejoinVideo'); + } + /** * Getter for participant specific video element in Jitsi Meet. * @@ -582,6 +602,16 @@ export default class JitsiMeetExternalAPI extends EventEmitter { this._isLargeVideoVisible = data.isVisible; this.emit('largeVideoChanged'); break; + case 'prejoin-screen-loaded': + this._participants[userID] = { + displayName: data.displayName, + formattedDisplayName: data.formattedDisplayName + }; + break; + case 'on-prejoin-video-changed': + this._isPrejoinVideoVisible = data.isVisible; + this.emit('prejoinVideoChanged'); + break; case 'video-conference-left': changeParticipantNumber(this, -1); delete this._participants[this._myUserID]; diff --git a/react/features/always-on-top/AlwaysOnTop.js b/react/features/always-on-top/AlwaysOnTop.js index b34037de4..5e505330f 100644 --- a/react/features/always-on-top/AlwaysOnTop.js +++ b/react/features/always-on-top/AlwaysOnTop.js @@ -61,8 +61,8 @@ export default class AlwaysOnTop extends Component<*, State> { this._avatarChangedListener = this._avatarChangedListener.bind(this); this._displayNameChangedListener = this._displayNameChangedListener.bind(this); - this._largeVideoChangedListener - = this._largeVideoChangedListener.bind(this); + this._videoChangedListener + = this._videoChangedListener.bind(this); this._mouseMove = this._mouseMove.bind(this); this._onMouseOut = this._onMouseOut.bind(this); this._onMouseOver = this._onMouseOver.bind(this); @@ -118,19 +118,19 @@ export default class AlwaysOnTop extends Component<*, State> { TOOLBAR_TIMEOUT); } - _largeVideoChangedListener: () => void; + _videoChangedListener: () => void; /** * Handles large video changed api events. * * @returns {void} */ - _largeVideoChangedListener() { + _videoChangedListener() { const userID = api._getOnStageParticipant(); const avatarURL = api.getAvatarURL(userID); const displayName = api.getDisplayName(userID); const formattedDisplayName = api._getFormattedDisplayName(userID); - const isVideoDisplayed = Boolean(api._getLargeVideo()); + const isVideoDisplayed = Boolean(api._getPrejoinVideo?.() || api._getLargeVideo()); this.setState({ avatarURL, @@ -185,8 +185,7 @@ export default class AlwaysOnTop extends Component<*, State> { customAvatarBackgrounds, displayName, formattedDisplayName, - isVideoDisplayed, - userID + isVideoDisplayed } = this.state; if (isVideoDisplayed) { @@ -197,10 +196,10 @@ export default class AlwaysOnTop extends Component<*, State> {
) + url = { displayName ? null : avatarURL } />)
{ componentDidMount() { api.on('avatarChanged', this._avatarChangedListener); api.on('displayNameChange', this._displayNameChangedListener); - api.on('largeVideoChanged', this._largeVideoChangedListener); + api.on('largeVideoChanged', this._videoChangedListener); + api.on('prejoinVideoChanged', this._videoChangedListener); + api.on('videoConferenceJoined', this._videoChangedListener); - this._largeVideoChangedListener(); + this._videoChangedListener(); window.addEventListener('mousemove', this._mouseMove); @@ -260,7 +261,13 @@ export default class AlwaysOnTop extends Component<*, State> { this._displayNameChangedListener); api.removeListener( 'largeVideoChanged', - this._largeVideoChangedListener); + this._videoChangedListener); + api.removeListener( + 'prejoinVideoChanged', + this._videoChangedListener); + api.removeListener( + 'videoConferenceJoined', + this._videoChangedListener); window.removeEventListener('mousemove', this._mouseMove); } diff --git a/react/features/always-on-top/VideoMuteButton.js b/react/features/always-on-top/VideoMuteButton.js index 290d47fb5..74834fcae 100644 --- a/react/features/always-on-top/VideoMuteButton.js +++ b/react/features/always-on-top/VideoMuteButton.js @@ -124,7 +124,7 @@ export default class VideoMuteButton extends Component { * @returns {void} */ _setVideoMuted(videoMuted: boolean) { // eslint-disable-line no-unused-vars - this.state.videoAvailable && api.executeCommand('toggleVideo'); + this.state.videoAvailable && api.executeCommand('toggleVideo', false, true); } _videoAvailabilityListener: ({ available: boolean }) => void; diff --git a/react/features/base/premeeting/components/web/Preview.js b/react/features/base/premeeting/components/web/Preview.js index eff136d5b..d12b86ef0 100644 --- a/react/features/base/premeeting/components/web/Preview.js +++ b/react/features/base/premeeting/components/web/Preview.js @@ -1,6 +1,6 @@ // @flow -import React from 'react'; +import React, { useEffect } from 'react'; import { getDisplayName } from '../../../../base/settings'; import { Avatar } from '../../../avatar'; @@ -9,6 +9,8 @@ import { getLocalParticipant } from '../../../participants'; import { connect } from '../../../redux'; import { getLocalVideoTrack } from '../../../tracks'; +declare var APP: Object; + export type Props = { /** @@ -47,12 +49,23 @@ function Preview(props: Props) { const { _participantId, flipVideo, name, videoMuted, videoTrack } = props; const className = flipVideo ? 'flipVideoX' : ''; + useEffect(() => { + APP.API.notifyPrejoinVideoVisibilityChanged(Boolean(!videoMuted && videoTrack)); + }, [ videoMuted, videoTrack ]); + + useEffect(() => { + APP.API.notifyPrejoinLoaded(); + + return () => APP.API.notifyPrejoinVideoVisibilityChanged(false); + }, []); + return (
{!videoMuted && videoTrack ? (