diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 1c42be74c..d8e634cab 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -100,17 +100,6 @@ export default class RemoteVideo extends SmallVideo { */ this._canPlayEventReceived = false; - /** - * The flag is set to true if remote participant's video gets muted - * during his media connection disruption. This is to prevent black video - * being render on the thumbnail, because even though once the video has - * been played the image usually remains on the video element it seems that - * after longer period of the video element being hidden this image can be - * lost. - * @type {boolean} - */ - this.mutedWhileDisconnected = false; - // Bind event handlers so they are only bound once for every instance. // TODO The event handlers should be turned into actions so changes can be // handled through reducers and middleware. @@ -306,36 +295,6 @@ export default class RemoteVideo extends SmallVideo { this._generatePopupContent(); } - /** - * Video muted status changed handler. - */ - onVideoMute() { - super.updateView(); - - // Update 'mutedWhileDisconnected' flag - this._figureOutMutedWhileDisconnected(); - } - - /** - * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into - * account remote participant's network connectivity and video muted status. - * - * @private - */ - _figureOutMutedWhileDisconnected() { - const state = APP.store.getState(); - const participant = getParticipantById(state, this.id); - const connectionState = participant?.connectionStatus; - const isActive = connectionState === JitsiParticipantConnectionStatus.ACTIVE; - const isVideoMuted = isRemoteTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id); - - if (!isActive && isVideoMuted) { - this.mutedWhileDisconnected = true; - } else if (isActive && !isVideoMuted) { - this.mutedWhileDisconnected = false; - } - } - /** * Removes the remote stream element corresponding to the given stream and * parent container. @@ -378,12 +337,12 @@ export default class RemoteVideo extends SmallVideo { */ isVideoPlayable() { const participant = getParticipantById(APP.store.getState(), this.id); - const connectionState = participant?.connectionStatus; + const { connectionStatus, mutedWhileDisconnected } = participant || {}; return super.isVideoPlayable() && this._canPlayEventReceived - && (connectionState === JitsiParticipantConnectionStatus.ACTIVE - || (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED && !this.mutedWhileDisconnected)); + && (connectionStatus === JitsiParticipantConnectionStatus.ACTIVE + || (connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED && !mutedWhileDisconnected)); } /** @@ -391,7 +350,6 @@ export default class RemoteVideo extends SmallVideo { */ updateView() { this.$container.toggleClass('audio-only', APP.conference.isAudioOnly()); - this._figureOutMutedWhileDisconnected(); super.updateView(); } diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 107f23ff4..d46f2e346 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -433,7 +433,7 @@ export default class SmallVideo { */ computeDisplayModeInput() { let isScreenSharing = false; - let connectionStatus; + let connectionStatus, mutedWhileDisconnected; const state = APP.store.getState(); const participant = getParticipantById(state, this.id); @@ -443,6 +443,7 @@ export default class SmallVideo { isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop'; connectionStatus = participant.connectionStatus; + mutedWhileDisconnected = participant.mutedWhileDisconnected; } return { @@ -453,7 +454,7 @@ export default class SmallVideo { isVideoPlayable: this.isVideoPlayable(), hasVideo: Boolean(this.selectVideoElement().length), connectionStatus, - mutedWhileDisconnected: this.mutedWhileDisconnected, + mutedWhileDisconnected, canPlayEventReceived: this._canPlayEventReceived, videoStream: Boolean(this.videoStream), isScreenSharing, diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index ffd0d3c2c..8f88ff061 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -337,7 +337,7 @@ const VideoLayout = { const remoteVideo = remoteVideos[id]; if (remoteVideo) { - remoteVideo.onVideoMute(); + remoteVideo.updateView(); } } @@ -391,12 +391,6 @@ const VideoLayout = { const remoteVideo = remoteVideos[id]; if (remoteVideo) { - // Updating only connection status indicator is not enough, because - // when we the connection is restored while the avatar was displayed - // (due to 'muted while disconnected' condition) we may want to show - // the video stream again and in order to do that the display mode - // must be updated. - // remoteVideo.updateConnectionStatusIndicator(isActive); remoteVideo.updateView(); } }, diff --git a/react/features/base/participants/actions.js b/react/features/base/participants/actions.js index c5b2dd32c..de820b4ab 100644 --- a/react/features/base/participants/actions.js +++ b/react/features/base/participants/actions.js @@ -19,7 +19,8 @@ import { import { getLocalParticipant, getNormalizedDisplayName, - getParticipantDisplayName + getParticipantDisplayName, + figureOutMutedWhileDisconnectedStatus } from './functions'; /** @@ -216,12 +217,15 @@ export function muteRemoteParticipant(id) { * }} */ export function participantConnectionStatusChanged(id, connectionStatus) { - return { - type: PARTICIPANT_UPDATED, - participant: { - connectionStatus, - id - } + return (dispatch, getState) => { + return { + type: PARTICIPANT_UPDATED, + participant: { + connectionStatus, + id, + mutedWhileDisconnected: figureOutMutedWhileDisconnectedStatus(getState(), id, connectionStatus) + } + }; }; } diff --git a/react/features/base/participants/functions.js b/react/features/base/participants/functions.js index 718a21fe7..31c954cd9 100644 --- a/react/features/base/participants/functions.js +++ b/react/features/base/participants/functions.js @@ -5,7 +5,7 @@ import { getGravatarURL } from '@jitsi/js-utils/avatar'; import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet'; import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media'; import { toState } from '../redux'; -import { getTrackByMediaTypeAndParticipant } from '../tracks'; +import { getTrackByMediaTypeAndParticipant, isRemoteTrackMuted } from '../tracks'; import { createDeferred } from '../util'; import { @@ -366,6 +366,45 @@ export function shouldRenderParticipantVideo(stateful: Object | Function, id: st return participantIsInLargeVideoWithScreen; } +/** + * Figures out the value of mutedWhileDisconnected status by taking into + * account remote participant's network connectivity and video muted status. + * The flag is set to true if remote participant's video gets muted + * during his media connection disruption. This is to prevent black video + * being render on the thumbnail, because even though once the video has + * been played the image usually remains on the video element it seems that + * after longer period of the video element being hidden this image can be + * lost. + * + * @param {Object|Function} stateful - Object or function that can be resolved + * to the Redux state. + * @param {string} participantID - The ID of the participant. + * @param {string} [connectionStatus] - A connection status to be used. + * @returns {boolean} - The mutedWhileDisconnected value. + */ +export function figureOutMutedWhileDisconnectedStatus( + stateful: Function | Object, participantID: string, connectionStatus: ?string) { + const state = toState(stateful); + const participant = getParticipantById(state, participantID); + + if (!participant || participant.local) { + return undefined; + } + + const isActive = (connectionStatus || participant.connectionStatus) === JitsiParticipantConnectionStatus.ACTIVE; + const isVideoMuted = isRemoteTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID); + let mutedWhileDisconnected = participant.mutedWhileDisconnected || false; + + if (!isActive && isVideoMuted) { + mutedWhileDisconnected = true; + } else if (isActive && !isVideoMuted) { + mutedWhileDisconnected = false; + } + + return mutedWhileDisconnected; +} + + /** * Resolves the first loadable avatar URL for a participant. * diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index f6762afa9..d03ccd877 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -12,6 +12,7 @@ import { import { JitsiConferenceEvents } from '../lib-jitsi-meet'; import { MiddlewareRegistry, StateListenerRegistry } from '../redux'; import { playSound, registerSound, unregisterSound } from '../sounds'; +import { getTrackByJitsiTrack, TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from '../tracks'; import { DOMINANT_SPEAKER_CHANGED, @@ -41,7 +42,8 @@ import { getLocalParticipant, getParticipantById, getParticipantCount, - getParticipantDisplayName + getParticipantDisplayName, + figureOutMutedWhileDisconnectedStatus } from './functions'; import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds'; @@ -134,6 +136,11 @@ MiddlewareRegistry.register(store => next => action => { case PARTICIPANT_UPDATED: return _participantJoinedOrUpdated(store, next, action); + + case TRACK_ADDED: + case TRACK_REMOVED: + case TRACK_UPDATED: + return _trackChanged(store, next, action); } return next(action); @@ -452,6 +459,55 @@ function _registerSounds({ dispatch }) { dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE)); } +/** + * Notifies the feature base/participants that the action there has been a change in the tracks of the participants. + * + * @param {Store} store - The redux store in which the specified {@code action} is being dispatched. + * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the specified {@code action} in the + * specified {@code store}. + * @param {Action} action - The redux action {@code PARTICIPANT_JOINED} or {@code PARTICIPANT_UPDATED} which is being + * dispatched in the specified {@code store}. + * @private + * @returns {Object} The value returned by {@code next(action)}. + */ +function _trackChanged({ dispatch, getState }, next, action) { + const { jitsiTrack } = action.track; + let track; + + if (action.type === TRACK_REMOVED) { + track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack); + } + + const result = next(action); + + if (action.type !== TRACK_REMOVED) { + track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack); + } + + if (typeof track === 'undefined' || track.local) { + return result; + } + + const { participantId } = track; + const state = getState(); + const participant = getParticipantById(state, participantId); + + if (!participant) { + return result; + } + + const mutedWhileDisconnected = figureOutMutedWhileDisconnectedStatus(state, participantId); + + if (participant.mutedWhileDisconnected !== mutedWhileDisconnected) { + dispatch(participantUpdated({ + id: participantId, + mutedWhileDisconnected + })); + } + + return result; +} + /** * Unregisters sounds related with the participants feature. * diff --git a/react/features/base/participants/reducer.js b/react/features/base/participants/reducer.js index 48e224da9..eb39349fd 100644 --- a/react/features/base/participants/reducer.js +++ b/react/features/base/participants/reducer.js @@ -221,6 +221,7 @@ function _participantJoined({ participant }) { isJigasi, loadableAvatarUrl, local: local || false, + mutedWhileDisconnected: local ? undefined : false, name, pinned: pinned || false, presence,