diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index e24008842e..3895e48740 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -11,6 +11,7 @@ import { createScreenSharingIssueEvent } from '../../../react/features/analytics import { sendAnalytics } from '../../../react/features/analytics/functions'; import Avatar from '../../../react/features/base/avatar/components/Avatar'; import theme from '../../../react/features/base/components/themes/participantsPaneTheme.json'; +import { getSsrcRewritingFeatureFlag } from '../../../react/features/base/config/functions.any'; import i18next from '../../../react/features/base/i18n/i18next'; import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet'; import { VIDEO_TYPE } from '../../../react/features/base/media/constants'; @@ -221,10 +222,10 @@ export default class LargeVideoManager { this.updateInProcess = true; - // Include hide()/fadeOut only if we're switching between users - // eslint-disable-next-line eqeqeq + // Include hide()/fadeOut if we're switching between users or between different sources of the same user. const container = this.getCurrentContainer(); - const isUserSwitch = this.newStreamData.id !== container.id; + const isUserSwitch = container.id !== this.newStreamData.id + || container.stream?.getSourceName() !== this.newStreamData.stream?.getSourceName(); const preUpdate = isUserSwitch ? container.hide() : Promise.resolve(); preUpdate.then(() => { diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index e526a3ad00..22e4be5d38 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -501,8 +501,7 @@ export class VideoContainer extends LargeContainer { * @param {string} videoType video type */ setStream(userID, stream, videoType) { - this.userId = userID; - if (this.stream === stream && !stream?.forceStreamToReattach) { + if (this.userId === userID && this.stream === stream && !stream?.forceStreamToReattach) { logger.debug(`SetStream on the large video for user ${userID} ignored: the stream is not changed!`); // Handles the use case for the remote participants when the @@ -516,6 +515,8 @@ export class VideoContainer extends LargeContainer { return; } + this.userId = userID; + if (stream?.forceStreamToReattach) { delete stream.forceStreamToReattach; } @@ -540,9 +541,8 @@ export class VideoContainer extends LargeContainer { logger.error(`Attaching the remote track ${stream} to large video has failed with `, error); }); - // Ensure large video gets play() called on it when a new stream is attached to it. This is necessary in the - // case of Safari as autoplay doesn't kick-in automatically on Safari 15 and newer versions. - browser.isWebKitBased() && this._play(); + // Ensure large video gets play() called on it when a new stream is attached to it. + this._play(); const flipX = stream.isLocal() && this.localFlipX && !this.isScreenSharing(); diff --git a/package-lock.json b/package-lock.json index 9bc7b924de..740cb838b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1815.0.0+c4cee681/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1816.0.0+f16cadce/lib-jitsi-meet.tgz", "lodash": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -12866,8 +12866,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1815.0.0+c4cee681/lib-jitsi-meet.tgz", - "integrity": "sha512-ZpaxL3IHL4sY0+Irpljhj5jONphho2rlU53ykSBaGcTbWhcjpmB8aJuycI1cxatojlnr3kgsni6lsiIpSu+0YA==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1816.0.0+f16cadce/lib-jitsi-meet.tgz", + "integrity": "sha512-ilIfYiJjBEATv8/+l537rcIUlaYep34UVcf3PCzWGOHlTIZeON5zFBPWZT7P9X+HLhOHBtLU5F49xNlGElCwSQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -29374,8 +29374,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1815.0.0+c4cee681/lib-jitsi-meet.tgz", - "integrity": "sha512-ZpaxL3IHL4sY0+Irpljhj5jONphho2rlU53ykSBaGcTbWhcjpmB8aJuycI1cxatojlnr3kgsni6lsiIpSu+0YA==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1816.0.0+f16cadce/lib-jitsi-meet.tgz", + "integrity": "sha512-ilIfYiJjBEATv8/+l537rcIUlaYep34UVcf3PCzWGOHlTIZeON5zFBPWZT7P9X+HLhOHBtLU5F49xNlGElCwSQ==", "requires": { "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", diff --git a/package.json b/package.json index 1236a687c2..a6c8b9d42e 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1815.0.0+c4cee681/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1816.0.0+f16cadce/lib-jitsi-meet.tgz", "lodash": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", diff --git a/react/features/base/participants/functions.ts b/react/features/base/participants/functions.ts index d8546cec35..ddebf4a312 100644 --- a/react/features/base/participants/functions.ts +++ b/react/features/base/participants/functions.ts @@ -12,9 +12,9 @@ import { getCurrentConference } from '../conference/functions'; import { ADD_PEOPLE_ENABLED } from '../flags/constants'; import { getFeatureFlag } from '../flags/functions'; import i18next from '../i18n/i18next'; -import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants'; +import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants'; import { toState } from '../redux/functions'; -import { getScreenShareTrack } from '../tracks/functions.any'; +import { getScreenShareTrack, isLocalTrackMuted } from '../tracks/functions.any'; import { createDeferred } from '../util/helpers'; import { @@ -362,6 +362,42 @@ export function getRemoteParticipantCountWithFake(stateful: IStateful) { return participantsState.remote.size; } +/** + * Returns the muted state of the given media source for a given participant. + * + * @param {(Function|Object)} stateful - The (whole) redux state, or redux's. + * @param {IParticipant} participant - The participant entity. + * @param {MediaType} mediaType - The media type. + * @returns {boolean} - True its muted, false otherwise. + */ +export function getMutedStateByParticipantAndMediaType( + stateful: IStateful, + participant: IParticipant, + mediaType: MediaType): boolean { + const type = mediaType === MEDIA_TYPE.SCREENSHARE ? 'video' : mediaType; + + if (participant.local) { + const state = toState(stateful); + const tracks = state['features/base/tracks']; + + return isLocalTrackMuted(tracks, mediaType); + } + + const sources = participant.sources?.get(type); + + if (!sources) { + return true; + } + + if (mediaType === MEDIA_TYPE.AUDIO) { + return Array.from(sources.values())[0].muted; + } + const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.SCREENSHARE; + const source = Array.from(sources.values()).find(src => src.videoType === videoType); + + return source?.muted ?? true; +} + /** * Returns a count of the known participants in the passed in redux state, * including fake participants. @@ -468,28 +504,28 @@ export function getScreenshareParticipantIds(stateful: IStateful): Array } /** - * Returns a list source name associated with a given remote participant and for the given media type. + * Returns a list of source names associated with a given remote participant and for the given media type. * * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to * retrieve the state. * @param {string} id - The id of the participant whose source names are to be retrieved. * @param {string} mediaType - The type of source, audio or video. - * @returns {Array|undefined} + * @returns {Array} */ -export function getSourceNamesByMediaType( +export function getSourceNamesByMediaTypeAndParticipant( stateful: IStateful, id: string, - mediaType: string): Array | undefined { + mediaType: string): Array { const participant: IParticipant | undefined = getParticipantById(stateful, id); if (!participant) { - return; + return []; } const sources = participant.sources; if (!sources) { - return; + return []; } return Array.from(sources.get(mediaType) ?? new Map()) @@ -497,6 +533,37 @@ export function getSourceNamesByMediaType( .map(s => s[0]); } +/** + * Returns a list of source names associated with a given remote participant and for the given video type (only for + * video sources). + * + * @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to + * retrieve the state. + * @param {string} id - The id of the participant whose source names are to be retrieved. + * @param {string} videoType - The type of video, camera or desktop. + * @returns {Array} + */ +export function getSourceNamesByVideoTypeAndParticipant( + stateful: IStateful, + id: string, + videoType: string): Array { + const participant: IParticipant | undefined = getParticipantById(stateful, id); + + if (!participant) { + return []; + } + + const sources = participant.sources; + + if (!sources) { + return []; + } + + return Array.from(sources.get(MEDIA_TYPE.VIDEO) ?? new Map()) + .filter(source => source[1].videoType === videoType && (videoType === VIDEO_TYPE.CAMERA || !source[1].muted)) + .map(s => s[0]); +} + /** * Returns the presence status of a participant associated with the passed id. * diff --git a/react/features/base/tracks/actionTypes.ts b/react/features/base/tracks/actionTypes.ts index 26b5b66d9c..6a253589c3 100644 --- a/react/features/base/tracks/actionTypes.ts +++ b/react/features/base/tracks/actionTypes.ts @@ -66,16 +66,6 @@ export const TRACK_MUTE_UNMUTE_FAILED = 'TRACK_MUTE_UNMUTE_FAILED'; */ export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE'; -/** - * The type of redux action dispatched when the owner of a track changes due to ssrc remapping. - * - * { - * type: TRACK_OWNER_CHANGED, - * track: Track - * } - */ -export const TRACK_OWNER_CHANGED = 'TRACK_OWNER_CHANGED'; - /** * The type of redux action dispatched when a track has been (locally or * remotely) removed from the conference. diff --git a/react/features/base/tracks/actions.any.ts b/react/features/base/tracks/actions.any.ts index ce9cd6fbb5..2ded652f92 100644 --- a/react/features/base/tracks/actions.any.ts +++ b/react/features/base/tracks/actions.any.ts @@ -26,7 +26,6 @@ import { TRACK_CREATE_ERROR, TRACK_MUTE_UNMUTE_FAILED, TRACK_NO_DATA_FROM_SOURCE, - TRACK_OWNER_CHANGED, TRACK_REMOVED, TRACK_STOPPED, TRACK_UPDATED, @@ -381,9 +380,6 @@ export function trackAdded(track: any) { track.on( JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED, (type: VideoType) => dispatch(trackVideoTypeChanged(track, type))); - track.on( - JitsiTrackEvents.TRACK_OWNER_CHANGED, - (owner: string) => dispatch(trackOwnerChanged(track, owner))); const local = track.isLocal(); const mediaType = track.getVideoType() === VIDEO_TYPE.DESKTOP ? MEDIA_TYPE.SCREENSHARE @@ -625,32 +621,6 @@ export function trackStreamingStatusChanged(track: any, streamingStatus: string) }; } -/** - * Create an action for when the owner of the track changes due to ssrc remapping. - * - * @param {(JitsiRemoteTrack)} track - JitsiTrack instance. - * @param {string} participantId - New owner's participant ID. - * @returns {{ - * type: TRACK_OWNER_CHANGED, - * track: Track - * }} - */ -export function trackOwnerChanged(track: any, participantId: string): { - track: { - jitsiTrack: any; - participantId: string; - }; - type: 'TRACK_OWNER_CHANGED'; -} { - return { - type: TRACK_OWNER_CHANGED, - track: { - jitsiTrack: track, - participantId - } - }; -} - /** * Signals passed tracks to be added. * diff --git a/react/features/base/tracks/middleware.web.ts b/react/features/base/tracks/middleware.web.ts index ae565fd877..5bb3283323 100644 --- a/react/features/base/tracks/middleware.web.ts +++ b/react/features/base/tracks/middleware.web.ts @@ -15,7 +15,6 @@ import { TRACK_ADDED, TRACK_MUTE_UNMUTE_FAILED, TRACK_NO_DATA_FROM_SOURCE, - TRACK_OWNER_CHANGED, TRACK_REMOVED, TRACK_STOPPED, TRACK_UPDATED @@ -82,23 +81,6 @@ MiddlewareRegistry.register(store => next => action => { return result; } - case TRACK_OWNER_CHANGED: { - const oldTrack = getTrackByJitsiTrack(store.getState()['features/base/tracks'], action.track?.jitsiTrack); - const oldOwner = oldTrack?.participantId; - const result = next(action); - const newOwner = action.track?.participantId; - - if (oldOwner) { - logTracksForParticipant(store.getState()['features/base/tracks'], oldOwner, 'Owner changed'); - } - - if (newOwner) { - logTracksForParticipant(store.getState()['features/base/tracks'], newOwner, 'Owner changed'); - } - - return result; - } - case TRACK_MUTE_UNMUTE_FAILED: { const { jitsiTrack } = action.track; const muted = action.wasMuted; diff --git a/react/features/base/tracks/reducer.ts b/react/features/base/tracks/reducer.ts index bb0c8b6a7f..83e44c4a81 100644 --- a/react/features/base/tracks/reducer.ts +++ b/react/features/base/tracks/reducer.ts @@ -10,7 +10,6 @@ import { TRACK_CREATE_CANCELED, TRACK_CREATE_ERROR, TRACK_NO_DATA_FROM_SOURCE, - TRACK_OWNER_CHANGED, TRACK_REMOVED, TRACK_UPDATED, TRACK_WILL_CREATE @@ -43,18 +42,6 @@ function track(state: ITrack, action: AnyAction) { } break; - case TRACK_OWNER_CHANGED: { - const t = action.track; - - if (state.jitsiTrack === t.jitsiTrack) { - return { - ...state, - participantId: t.participantId - }; - } - break; - } - case TRACK_UPDATED: { const t = action.track; @@ -104,7 +91,6 @@ ReducerRegistry.register('features/base/tracks', (state = [], acti switch (action.type) { case PARTICIPANT_ID_CHANGED: case TRACK_NO_DATA_FROM_SOURCE: - case TRACK_OWNER_CHANGED: case TRACK_UPDATED: return state.map((t: ITrack) => track(t, action)); case TRACK_ADDED: { diff --git a/react/features/participants-pane/components/native/MeetingParticipantItem.tsx b/react/features/participants-pane/components/native/MeetingParticipantItem.tsx index 67c817ff23..8cacf7c269 100644 --- a/react/features/participants-pane/components/native/MeetingParticipantItem.tsx +++ b/react/features/participants-pane/components/native/MeetingParticipantItem.tsx @@ -2,9 +2,12 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { IReduxState, IStore } from '../../../app/types'; +import { getSsrcRewritingFeatureFlag } from '../../../base/config/functions.any'; import { translate } from '../../../base/i18n/functions'; +import { MEDIA_TYPE } from '../../../base/media/constants'; import { getLocalParticipant, + getMutedStateByParticipantAndMediaType, getParticipantById, getParticipantDisplayName, hasRaisedHand, @@ -166,8 +169,12 @@ function mapStateToProps(state: IReduxState, ownProps: any) { const { participant } = ownProps; const { ownerId } = state['features/shared-video']; const localParticipantId = getLocalParticipant(state)?.id; - const _isAudioMuted = Boolean(participant && isParticipantAudioMuted(participant, state)); - const _isVideoMuted = isParticipantVideoMuted(participant, state); + const _isAudioMuted = getSsrcRewritingFeatureFlag(state) + ? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.AUDIO)) + : Boolean(participant && isParticipantAudioMuted(participant, state)); + const _isVideoMuted = getSsrcRewritingFeatureFlag(state) + ? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.VIDEO)) + : isParticipantVideoMuted(participant, state); const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state); const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state); const { disableModeratorIndicator } = state['features/base/config']; diff --git a/react/features/participants-pane/components/web/MeetingParticipantItem.tsx b/react/features/participants-pane/components/web/MeetingParticipantItem.tsx index fb2f25978d..68f78f1826 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantItem.tsx +++ b/react/features/participants-pane/components/web/MeetingParticipantItem.tsx @@ -2,10 +2,12 @@ import React, { useCallback, useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { IReduxState } from '../../../app/types'; +import { getSsrcRewritingFeatureFlag } from '../../../base/config/functions.any'; import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet'; import { MEDIA_TYPE } from '../../../base/media/constants'; import { getLocalParticipant, + getMutedStateByParticipantAndMediaType, getParticipantByIdOrUndefined, getParticipantDisplayName, hasRaisedHand, @@ -299,15 +301,15 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { const { participantID, searchString } = ownProps; const { ownerId } = state['features/shared-video']; const localParticipantId = getLocalParticipant(state)?.id; - const participant = getParticipantByIdOrUndefined(state, participantID); - const _displayName = getParticipantDisplayName(state, participant?.id ?? ''); - const _matchesSearch = participantMatchesSearch(participant, searchString); - - const _isAudioMuted = Boolean(participant && isParticipantAudioMuted(participant, state)); - const _isVideoMuted = isParticipantVideoMuted(participant, state); + const _isAudioMuted = getSsrcRewritingFeatureFlag(state) + ? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.AUDIO)) + : Boolean(participant && isParticipantAudioMuted(participant, state)); + const _isVideoMuted = getSsrcRewritingFeatureFlag(state) + ? Boolean(participant && getMutedStateByParticipantAndMediaType(state, participant, MEDIA_TYPE.VIDEO)) + : isParticipantVideoMuted(participant, state); const _audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state); const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state); const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, _isVideoMuted, state); diff --git a/react/features/video-quality/subscriber.ts b/react/features/video-quality/subscriber.ts index 4ccdccaf83..b08c67d62f 100644 --- a/react/features/video-quality/subscriber.ts +++ b/react/features/video-quality/subscriber.ts @@ -3,10 +3,11 @@ import debounce from 'lodash/debounce'; import { IReduxState, IStore } from '../app/types'; import { _handleParticipantError } from '../base/conference/functions'; import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any'; -import { MEDIA_TYPE } from '../base/media/constants'; +import { MEDIA_TYPE, VIDEO_TYPE } from '../base/media/constants'; import { getLocalParticipant, - getSourceNamesByMediaType + getSourceNamesByMediaTypeAndParticipant, + getSourceNamesByVideoTypeAndParticipant } from '../base/participants/functions'; import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks/functions'; @@ -321,10 +322,10 @@ function _getSourceNames(participantList: Array, state: IReduxState): Ar participantList.forEach(participantId => { if (getSsrcRewritingFeatureFlag(state)) { - const sourceNames: string[] | undefined - = getSourceNamesByMediaType(state, participantId, MEDIA_TYPE.VIDEO); + const sourceNames: string[] + = getSourceNamesByMediaTypeAndParticipant(state, participantId, MEDIA_TYPE.VIDEO); - sourceNames?.length && sourceNamesList.push(...sourceNames); + sourceNames.length && sourceNamesList.push(...sourceNames); } else { let sourceName: string; @@ -428,8 +429,9 @@ function _updateReceiverVideoConstraints({ getState }: IStore) { if (remoteScreenShares.includes(largeVideoParticipantId)) { largeVideoSourceName = largeVideoParticipantId; } else { - largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant( - tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId); + largeVideoSourceName = getSsrcRewritingFeatureFlag(state) + ? getSourceNamesByVideoTypeAndParticipant(state, largeVideoParticipantId, VIDEO_TYPE.CAMERA)[0] + : getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId); } }