diff --git a/conference.js b/conference.js index c51919ba30..f68f73fc71 100644 --- a/conference.js +++ b/conference.js @@ -89,7 +89,7 @@ import { setVideoMuted, setVideoUnmutePermissions } from './react/features/base/media/actions'; -import { MEDIA_TYPE, VIDEO_TYPE } from './react/features/base/media/constants'; +import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY, VIDEO_TYPE } from './react/features/base/media/constants'; import { getStartWithAudioMuted, getStartWithVideoMuted, @@ -205,23 +205,6 @@ function sendData(command, value) { room.sendCommand(command, { value }); } -/** - * Mute or unmute local audio stream if it exists. - * @param {boolean} muted - if audio stream should be muted or unmuted. - */ -function muteLocalAudio(muted) { - APP.store.dispatch(setAudioMuted(muted)); -} - -/** - * Mute or unmute local video stream if it exists. - * @param {boolean} muted if video stream should be muted or unmuted. - * - */ -function muteLocalVideo(muted) { - APP.store.dispatch(setVideoMuted(muted)); -} - /** * A queue for the async replaceLocalTrack action so that multiple audio * replacements cannot happen simultaneously. This solves the issue where @@ -708,11 +691,10 @@ export default { * Simulates toolbar button click for audio mute. Used by shortcuts and API. * * @param {boolean} mute true for mute and false for unmute. - * @param {boolean} [showUI] when set to false will not display any error * dialogs in case of media permissions error. * @returns {Promise} */ - async muteAudio(mute, showUI = true) { + async muteAudio(mute) { const state = APP.store.getState(); if (!mute @@ -731,47 +713,7 @@ export default { return; } - // Not ready to modify track's state yet - if (!this._localTracksInitialized) { - // This will only modify base/media.audio.muted which is then synced - // up with the track at the end of local tracks initialization. - muteLocalAudio(mute); - this.updateAudioIconEnabled(); - - return; - } else if (this.isLocalAudioMuted() === mute) { - // NO-OP - return; - } - - const localAudio = getLocalJitsiAudioTrack(APP.store.getState()); - - if (!localAudio && !mute) { - const maybeShowErrorDialog = error => { - showUI && APP.store.dispatch(notifyMicError(error)); - }; - - APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.PENDING_UNMUTE)); - - await createLocalTracksF({ devices: [ 'audio' ] }) - .then(([ audioTrack ]) => audioTrack) - .catch(error => { - maybeShowErrorDialog(error); - - // Rollback the audio muted status by using null track - return null; - }) - .then(async audioTrack => { - await this._maybeApplyAudioMixerEffect(audioTrack); - - return this.useAudioStream(audioTrack); - }) - .finally(() => { - APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.NONE)); - }); - } else { - muteLocalAudio(mute); - } + await APP.store.dispatch(setAudioMuted(mute, true)); }, /** @@ -801,10 +743,9 @@ export default { /** * Simulates toolbar button click for video mute. Used by shortcuts and API. * @param mute true for mute and false for unmute. - * @param {boolean} [showUI] when set to false will not display any error * dialogs in case of media permissions error. */ - muteVideo(mute, showUI = true) { + muteVideo(mute) { if (this.videoSwitchInProgress) { logger.warn('muteVideo - unable to perform operations while video switch is in progress'); @@ -825,60 +766,7 @@ export default { return; } - // If not ready to modify track's state yet adjust the base/media - if (!this._localTracksInitialized) { - // This will only modify base/media.video.muted which is then synced - // up with the track at the end of local tracks initialization. - muteLocalVideo(mute); - this.setVideoMuteStatus(); - - return; - } else if (this.isLocalVideoMuted() === mute) { - // NO-OP - return; - } - - const localVideo = getLocalJitsiVideoTrack(state); - - if (!localVideo && !mute && !this.isCreatingLocalTrack) { - const maybeShowErrorDialog = error => { - showUI && APP.store.dispatch(notifyCameraError(error)); - }; - - this.isCreatingLocalTrack = true; - - APP.store.dispatch(gumPending([ MEDIA_TYPE.VIDEO ], IGUMPendingState.PENDING_UNMUTE)); - - // Try to create local video if there wasn't any. - // This handles the case when user joined with no video - // (dismissed screen sharing screen or in audio only mode), but - // decided to add it later on by clicking on muted video icon or - // turning off the audio only mode. - // - // FIXME when local track creation is moved to react/redux - // it should take care of the use case described above - createLocalTracksF({ devices: [ 'video' ] }) - .then(([ videoTrack ]) => videoTrack) - .catch(error => { - // FIXME should send some feedback to the API on error ? - maybeShowErrorDialog(error); - - // Rollback the video muted status by using null track - return null; - }) - .then(videoTrack => { - logger.debug(`muteVideo: calling useVideoStream for track: ${videoTrack}`); - - return this.useVideoStream(videoTrack); - }) - .finally(() => { - this.isCreatingLocalTrack = false; - APP.store.dispatch(gumPending([ MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE)); - }); - } else { - // FIXME show error dialog if it fails (should be handled by react) - muteLocalVideo(mute); - } + APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true)); }, /** diff --git a/react/features/base/conference/middleware.any.ts b/react/features/base/conference/middleware.any.ts index 6f04d24a6c..fcc6be85c2 100644 --- a/react/features/base/conference/middleware.any.ts +++ b/react/features/base/conference/middleware.any.ts @@ -22,12 +22,14 @@ import { INotificationProps } from '../../notifications/types'; import { hasDisplayName } from '../../prejoin/utils'; import { stopLocalVideoRecording } from '../../recording/actions.any'; import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager'; +import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect'; import { iAmVisitor } from '../../visitors/functions'; import { overwriteConfig } from '../config/actions'; import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes'; import { connectionDisconnected, disconnect } from '../connection/actions'; import { validateJwt } from '../jwt/functions'; import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet'; +import { MEDIA_TYPE } from '../media/constants'; import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes'; import { PARTICIPANT_ROLE } from '../participants/constants'; import { @@ -656,7 +658,7 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct * @private * @returns {Object} The value returned by {@code next(action)}. */ -function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) { +async function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) { const track = action.track; // TODO All track swapping should happen here instead of conference.js. @@ -664,7 +666,6 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) const { getState } = store; const state = getState(); const conference = getCurrentConference(state); - let promise; if (conference) { const jitsiTrack = action.track.jitsiTrack; @@ -673,14 +674,22 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) // If gUM is slow and tracks are created after the user has already joined the conference, avoid // adding the tracks to the conference if the user is a visitor. if (!iAmVisitor(state)) { - promise = _addLocalTracksToConference(conference, [ jitsiTrack ]); + const { desktopAudioTrack } = state['features/screen-share']; + + // If the user is sharing their screen and has a desktop audio track, we need to replace that with + // the audio mixer effect so that the desktop audio is mixed in with the microphone audio. + if (typeof APP !== 'undefined' && desktopAudioTrack && track.mediaType === MEDIA_TYPE.AUDIO) { + await conference.replaceTrack(desktopAudioTrack, null); + const audioMixerEffect = new AudioMixerEffect(desktopAudioTrack); + + await jitsiTrack.setEffect(audioMixerEffect); + await conference.replaceTrack(null, jitsiTrack); + } else { + await _addLocalTracksToConference(conference, [ jitsiTrack ]); + } } } else { - promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]); - } - - if (promise) { - return promise.then(() => next(action)); + await _removeLocalTracksFromConference(conference, [ jitsiTrack ]); } } } diff --git a/react/features/base/tracks/middleware.any.ts b/react/features/base/tracks/middleware.any.ts index 1690103c80..c8c4f9d2cc 100644 --- a/react/features/base/tracks/middleware.any.ts +++ b/react/features/base/tracks/middleware.any.ts @@ -2,7 +2,6 @@ import { batch } from 'react-redux'; import { IStore } from '../../app/types'; import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes'; -import { isPrejoinPageVisible } from '../../prejoin/functions'; import { getCurrentConference } from '../conference/functions'; import { SET_AUDIO_MUTED, @@ -203,11 +202,8 @@ function _setMuted(store: IStore, { ensureTrack, muted }: { setTrackMuted(jitsiTrack, muted, state, dispatch) .catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted))); } - } else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) { + } else if (!muted && ensureTrack) { typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.PENDING_UNMUTE)); - - // FIXME: This only runs on mobile now because web has its own way of - // creating local tracks. Adjust the check once they are unified. dispatch(createLocalTracksA({ devices: [ mediaType ] })).then(() => { typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.NONE)); }); diff --git a/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx b/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx index 672154d117..9d837cb0ad 100644 --- a/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx +++ b/react/features/recording/components/Recording/native/RecordingConsentDialog.tsx @@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { IReduxState } from '../../../../app/types'; import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDialog'; import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions'; +import { VIDEO_MUTISM_AUTHORITY } from '../../../../base/media/constants'; import Link from '../../../../base/react/components/native/Link'; import styles from '../styles.native'; @@ -31,8 +32,8 @@ export default function RecordingConsentDialog() { const consentAndUnmute = useCallback(() => { dispatch(setAudioUnmutePermissions(false, true)); dispatch(setVideoUnmutePermissions(false, true)); - dispatch(setAudioMuted(false)); - dispatch(setVideoMuted(false)); + dispatch(setAudioMuted(false, true)); + dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true)); return true; }, []); diff --git a/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx b/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx index 8044a9a946..5656e403d3 100644 --- a/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx +++ b/react/features/recording/components/Recording/web/RecordingConsentDialog.tsx @@ -11,6 +11,7 @@ import { setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions'; +import { VIDEO_MUTISM_AUTHORITY } from '../../../../base/media/constants'; import Dialog from '../../../../base/ui/components/web/Dialog'; /** @@ -36,8 +37,8 @@ export default function RecordingConsentDialog() { batch(() => { dispatch(setAudioUnmutePermissions(false, true)); dispatch(setVideoUnmutePermissions(false, true)); - dispatch(setAudioMuted(false)); - dispatch(setVideoMuted(false)); + dispatch(setAudioMuted(false, true)); + dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true)); dispatch(hideDialog()); }); }, []); diff --git a/react/features/stream-effects/audio-mixer/AudioMixerEffect.ts b/react/features/stream-effects/audio-mixer/AudioMixerEffect.ts index 7046239b88..6ca136c8ca 100644 --- a/react/features/stream-effects/audio-mixer/AudioMixerEffect.ts +++ b/react/features/stream-effects/audio-mixer/AudioMixerEffect.ts @@ -68,6 +68,7 @@ export class AudioMixerEffect { * @param {MediaStream} audioStream - Audio stream which will be mixed with _mixAudio. * @returns {MediaStream} - MediaStream containing both audio tracks mixed together. */ + // @ts-ignore startEffect(audioStream: MediaStream) { this._originalStream = audioStream; this._originalTrack = audioStream.getTracks()[0];