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,