diff --git a/conference.js b/conference.js index eb512b3fa..d9c57c93f 100644 --- a/conference.js +++ b/conference.js @@ -2660,17 +2660,6 @@ export default { APP.UI.updateLargeVideo(displayedUserId, true); } }); - - // Used in non-multi-stream. - APP.UI.addListener( - UIEvents.TOGGLE_SCREENSHARING, ({ enabled, audioOnly, ignoreDidHaveVideo, desktopStream }) => { - this.toggleScreenSharing(enabled, - { - audioOnly, - desktopStream - }, ignoreDidHaveVideo); - } - ); }, /** diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 87bfae518..96dbe8b24 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -36,7 +36,8 @@ import { isTrackStreamingStatusInactive, isTrackStreamingStatusInterrupted } from '../../../react/features/connection-indicator/functions'; -import { FILMSTRIP_BREAKPOINT, getVerticalViewMaxWidth, isFilmstripResizable } from '../../../react/features/filmstrip'; +import { FILMSTRIP_BREAKPOINT } from '../../../react/features/filmstrip/constants'; +import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../../react/features/filmstrip/functions'; import { updateKnownLargeVideoResolution } from '../../../react/features/large-video/actions'; diff --git a/react/features/base/conference/middleware.native.js b/react/features/base/conference/middleware.native.js index fbe74c7d7..fefd329e8 100644 --- a/react/features/base/conference/middleware.native.js +++ b/react/features/base/conference/middleware.native.js @@ -1,78 +1 @@ -// @flow - -import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions'; -import { setAudioOnly } from '../audio-only'; -import JitsiMeetJS from '../lib-jitsi-meet'; -import { MiddlewareRegistry } from '../redux'; -import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes'; -import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from '../tracks/actions'; -import { getLocalVideoTrack, isLocalVideoTrackDesktop } from '../tracks/functions'; - import './middleware.any'; - -MiddlewareRegistry.register(store => next => action => { - switch (action.type) { - case TOGGLE_SCREENSHARING: { - _toggleScreenSharing(action.enabled, store); - break; - } - } - - return next(action); -}); - -/** - * Toggles screen sharing. - * - * @private - * @param {boolean} enabled - The state to toggle screen sharing to. - * @param {Store} store - The redux. - * @returns {void} - */ -function _toggleScreenSharing(enabled, store) { - const { dispatch, getState } = store; - const state = getState(); - - if (enabled) { - const isSharing = isLocalVideoTrackDesktop(state); - - if (!isSharing) { - _startScreenSharing(dispatch, state); - } - } else { - dispatch(destroyLocalDesktopTrackIfExists()); - setPictureInPictureEnabled(true); - } -} - -/** - * Creates desktop track and replaces the local one. - * - * @private - * @param {Dispatch} dispatch - The redux {@code dispatch} function. - * @param {Object} state - The redux state. - * @returns {void} - */ -function _startScreenSharing(dispatch, state) { - setPictureInPictureEnabled(false); - - JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] }) - .then(tracks => { - const track = tracks[0]; - const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']); - const currentJitsiTrack = currentLocalTrack && currentLocalTrack.jitsiTrack; - - dispatch(replaceLocalTrack(currentJitsiTrack, track)); - - const { enabled: audioOnly } = state['features/base/audio-only']; - - if (audioOnly) { - dispatch(setAudioOnly(false)); - } - }) - .catch(error => { - console.log('ERROR creating ScreeSharing stream ', error); - - setPictureInPictureEnabled(true); - }); -} diff --git a/react/features/base/conference/middleware.web.js b/react/features/base/conference/middleware.web.js index f15190d04..5318805cb 100644 --- a/react/features/base/conference/middleware.web.js +++ b/react/features/base/conference/middleware.web.js @@ -1,48 +1,14 @@ -// @flow -import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors'; -import UIEvents from '../../../../service/UI/UIEvents'; -import { showModeratedNotification } from '../../av-moderation/actions'; -import { shouldShowModeratedNotification } from '../../av-moderation/functions'; -import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions'; -import { - NOTIFICATION_TIMEOUT_TYPE, - isModerationNotificationDisplayed, - showNotification -} from '../../notifications'; import { setPrejoinPageVisibility, setSkipPrejoinOnReload } from '../../prejoin'; -import { - isAudioOnlySharing, - isScreenVideoShared, - setScreenAudioShareState, - setScreenshareAudioTrack -} from '../../screen-share'; -import { isScreenshotCaptureEnabled, toggleScreenshotCaptureSummary } from '../../screenshot-capture'; -import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect'; -import { setAudioOnly } from '../audio-only'; -import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any'; -import { JitsiConferenceErrors, JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet'; -import { MEDIA_TYPE, VIDEO_TYPE, setScreenshareMuted } from '../media'; +import { JitsiConferenceErrors } from '../lib-jitsi-meet'; import { MiddlewareRegistry } from '../redux'; -import { - TOGGLE_SCREENSHARING, - addLocalTrack, - createLocalTracksF, - getLocalDesktopTrack, - getLocalJitsiAudioTrack, - replaceLocalTrack, - toggleScreensharing -} from '../tracks'; import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_JOIN_IN_PROGRESS } from './actionTypes'; -import { getCurrentConference } from './functions'; import './middleware.any'; -declare var APP: Object; - MiddlewareRegistry.register(store => next => action => { const { dispatch, getState } = store; const { enableForcedReload } = getState()['features/base/config']; @@ -69,220 +35,7 @@ MiddlewareRegistry.register(store => next => action => { break; } - case TOGGLE_SCREENSHARING: - if (typeof APP === 'object') { - // check for A/V Moderation when trying to start screen sharing - if ((action.enabled || action.enabled === undefined) - && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) { - if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, store.getState())) { - store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER)); - } - - return; - } - - const { enabled, audioOnly, ignoreDidHaveVideo, shareOptions } = action; - - if (getMultipleVideoSendingSupportFeatureFlag(store.getState())) { - _toggleScreenSharing(action, store); - } else { - APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, - { - enabled, - audioOnly, - ignoreDidHaveVideo, - desktopStream: shareOptions?.desktopStream - }); - } - } - break; } return next(action); }); - -/** - * Displays a UI notification for screensharing failure based on the error passed. - * - * @private - * @param {Object} error - The error. - * @param {Object} store - The redux store. - * @returns {void} - */ -function _handleScreensharingError(error, { dispatch }) { - if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) { - return; - } - let descriptionKey, titleKey; - - if (error.name === JitsiTrackErrors.PERMISSION_DENIED) { - descriptionKey = 'dialog.screenSharingPermissionDeniedError'; - titleKey = 'dialog.screenSharingFailedTitle'; - } else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) { - descriptionKey = 'dialog.cameraConstraintFailedError'; - titleKey = 'deviceError.cameraError'; - } else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) { - descriptionKey = 'dialog.screenSharingFailed'; - titleKey = 'dialog.screenSharingFailedTitle'; - } else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) { - descriptionKey = 'notify.screenShareNoAudio'; - titleKey = 'notify.screenShareNoAudioTitle'; - } - - dispatch(showNotification({ - titleKey, - descriptionKey - }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); -} - -/** - * Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop - * audio track is added to the conference. - * - * @private - * @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference. - * @param {*} state - The redux state. - * @returns {void} - */ -async function _maybeApplyAudioMixerEffect(desktopAudioTrack, state) { - const localAudio = getLocalJitsiAudioTrack(state); - const conference = getCurrentConference(state); - - if (localAudio) { - // If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API. - const mixerEffect = new AudioMixerEffect(desktopAudioTrack); - - await localAudio.setEffect(mixerEffect); - } else { - // If no local stream is present ( i.e. no input audio devices) we use the screen share audio - // stream as we would use a regular stream. - await conference.replaceTrack(null, desktopAudioTrack); - } -} - -/** - * Toggles screen sharing. - * - * @private - * @param {boolean} enabled - The state to toggle screen sharing to. - * @param {Store} store - The redux store. - * @returns {void} - */ -async function _toggleScreenSharing({ enabled, audioOnly = false, shareOptions = {} }, store) { - const { dispatch, getState } = store; - const state = getState(); - const audioOnlySharing = isAudioOnlySharing(state); - const screenSharing = isScreenVideoShared(state); - const conference = getCurrentConference(state); - const localAudio = getLocalJitsiAudioTrack(state); - const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']); - - // Toggle screenshare or audio-only share if the new state is not passed. Happens in the following two cases. - // 1. ShareAudioDialog passes undefined when the user hits continue in the share audio demo modal. - // 2. Toggle screenshare called from the external API. - const enable = audioOnly - ? enabled ?? !audioOnlySharing - : enabled ?? !screenSharing; - const screensharingDetails = {}; - - if (enable) { - let tracks; - - // Spot proxy stream. - if (shareOptions.desktopStream) { - tracks = [ shareOptions.desktopStream ]; - } else { - const { _desktopSharingSourceDevice } = state['features/base/config']; - - if (!shareOptions.desktopSharingSources && _desktopSharingSourceDevice) { - shareOptions.desktopSharingSourceDevice = _desktopSharingSourceDevice; - } - const options = { - devices: [ VIDEO_TYPE.DESKTOP ], - ...shareOptions - }; - - try { - tracks = await createLocalTracksF(options); - } catch (error) { - _handleScreensharingError(error, store); - - return; - } - } - - const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO); - const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO); - - if (audioOnly) { - // Dispose the desktop track for audio-only screensharing. - desktopVideoTrack.dispose(); - - if (!desktopAudioTrack) { - _handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store); - - return; - } - } else if (desktopVideoTrack) { - if (localScreenshare) { - await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference)); - } else { - await dispatch(addLocalTrack(desktopVideoTrack)); - } - if (isScreenshotCaptureEnabled(state, false, true)) { - dispatch(toggleScreenshotCaptureSummary(true)); - } - screensharingDetails.sourceType = desktopVideoTrack.sourceType; - } - - // Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference - // otherwise without unmuting the microphone. - if (desktopAudioTrack) { - // Noise suppression doesn't work with desktop audio because we can't chain track effects yet, disable it - // first. We need to to wait for the effect to clear first or it might interfere with the audio mixer. - await dispatch(setNoiseSuppressionEnabled(false)); - _maybeApplyAudioMixerEffect(desktopAudioTrack, state); - dispatch(setScreenshareAudioTrack(desktopAudioTrack)); - - // Handle the case where screen share was stopped from the browsers 'screen share in progress' window. - if (audioOnly) { - desktopAudioTrack?.on( - JitsiTrackEvents.LOCAL_TRACK_STOPPED, - () => dispatch(toggleScreensharing(undefined, true))); - } - } - - // Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to - // audio-only screensharing. - const { enabled: bestPerformanceMode } = state['features/base/audio-only']; - - if (bestPerformanceMode && !audioOnly) { - dispatch(setAudioOnly(false)); - } - } else { - const { desktopAudioTrack } = state['features/screen-share']; - - dispatch(toggleScreenshotCaptureSummary(false)); - - // Mute the desktop track instead of removing it from the conference since we don't want the client to signal - // a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the - // same sender will be re-used without the need for signaling a new ssrc through source-add. - dispatch(setScreenshareMuted(true)); - if (desktopAudioTrack) { - if (localAudio) { - localAudio.setEffect(undefined); - } else { - await conference.replaceTrack(desktopAudioTrack, null); - } - desktopAudioTrack.dispose(); - dispatch(setScreenshareAudioTrack(null)); - } - } - - if (audioOnly) { - dispatch(setScreenAudioShareState(enable)); - } else { - // Notify the external API. - APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails); - } -} diff --git a/react/features/base/tracks/actionTypes.ts b/react/features/base/tracks/actionTypes.ts index 965ef98b3..151880c0d 100644 --- a/react/features/base/tracks/actionTypes.ts +++ b/react/features/base/tracks/actionTypes.ts @@ -9,16 +9,6 @@ */ export const SET_NO_SRC_DATA_NOTIFICATION_UID = 'SET_NO_SRC_DATA_NOTIFICATION_UID'; -/** - * The type of redux action dispatched to disable screensharing or to start the - * flow for enabling screenshare. - * - * { - * type: TOGGLE_SCREENSHARING - * } - */ -export const TOGGLE_SCREENSHARING = 'TOGGLE_SCREENSHARING'; - /** * The type of redux action dispatched when a track has been (locally or * remotely) added to the conference. diff --git a/react/features/base/tracks/actions.ts b/react/features/base/tracks/actions.any.ts similarity index 95% rename from react/features/base/tracks/actions.ts rename to react/features/base/tracks/actions.any.ts index 1226ae588..c815f6e87 100644 --- a/react/features/base/tracks/actions.ts +++ b/react/features/base/tracks/actions.any.ts @@ -24,7 +24,6 @@ import { updateSettings } from '../settings/actions'; import { SET_NO_SRC_DATA_NOTIFICATION_UID, - TOGGLE_SCREENSHARING, TRACK_ADDED, TRACK_CREATE_CANCELED, TRACK_CREATE_ERROR, @@ -298,32 +297,6 @@ export function showNoDataFromSourceVideoError(jitsiTrack: any) { }; } -/** - * Signals that the local participant is ending screensharing or beginning the screensharing flow. - * - * @param {boolean} enabled - The state to toggle screen sharing to. - * @param {boolean} audioOnly - Only share system audio. - * @param {boolean} ignoreDidHaveVideo - Whether or not to ignore if video was on when sharing started. - * @param {Object} shareOptions - The options to be passed for capturing screenshare. - * @returns {{ - * type: TOGGLE_SCREENSHARING, - * on: boolean, - * audioOnly: boolean, - * ignoreDidHaveVideo: boolean, - * shareOptions: Object - * }} - */ -export function toggleScreensharing(enabled: boolean | undefined, audioOnly = false, - ignoreDidHaveVideo = false, shareOptions = {}) { - return { - type: TOGGLE_SCREENSHARING, - enabled, - audioOnly, - ignoreDidHaveVideo, - shareOptions - }; -} - /** * Replaces one track with another for one renegotiation instead of invoking * two renegotiations with a separate removeTrack and addTrack. Disposes the @@ -396,7 +369,7 @@ function replaceStoredTracks(oldTrack: any, newTrack: any) { * conference. * * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance. - * @returns {{ type: TRACK_ADDED, track: Track }} + * @returns {Function} */ export function trackAdded(track: any) { return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => { @@ -490,7 +463,13 @@ export function trackAdded(track: any) { * track: Track * }} */ -export function trackMutedChanged(track: any) { +export function trackMutedChanged(track: any): { + track: { + jitsiTrack: any; + muted: boolean; + }; + type: 'TRACK_UPDATED'; +} { return { type: TRACK_UPDATED, track: { @@ -511,7 +490,11 @@ export function trackMutedChanged(track: any) { * track: Track * }} */ -export function trackMuteUnmuteFailed(track: any, wasMuting: boolean) { +export function trackMuteUnmuteFailed(track: any, wasMuting: boolean): { + track: any; + type: 'TRACK_MUTE_UNMUTE_FAILED'; + wasMuting: boolean; +} { return { type: TRACK_MUTE_UNMUTE_FAILED, track, @@ -549,7 +532,12 @@ export function trackNoDataFromSourceNotificationInfoChanged(track: any, noDataF * track: Track * }} */ -export function trackRemoved(track: any) { +export function trackRemoved(track: any): { + track: { + jitsiTrack: any; + }; + type: 'TRACK_REMOVED'; +} { track.removeAllListeners(JitsiTrackEvents.TRACK_MUTE_CHANGED); track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED); track.removeAllListeners(JitsiTrackEvents.NO_DATA_FROM_SOURCE); @@ -571,7 +559,13 @@ export function trackRemoved(track: any) { * track: Track * }} */ -export function trackVideoStarted(track: any) { +export function trackVideoStarted(track: any): { + track: { + jitsiTrack: any; + videoStarted: true; + }; + type: 'TRACK_UPDATED'; +} { return { type: TRACK_UPDATED, track: { @@ -611,7 +605,13 @@ export function trackVideoTypeChanged(track: any, videoType: VideoType) { * track: Track * }} */ -export function trackStreamingStatusChanged(track: any, streamingStatus: string) { +export function trackStreamingStatusChanged(track: any, streamingStatus: string): { + track: { + jitsiTrack: any; + streamingStatus: string; + }; + type: 'TRACK_UPDATED'; +} { return { type: TRACK_UPDATED, track: { @@ -644,7 +644,7 @@ function _addTracks(tracks: any[]) { * about here is to be sure that the {@code getUserMedia} callbacks have * completed (i.e. Returned from the native side). */ -function _cancelGUMProcesses(getState: IStore['getState']) { +function _cancelGUMProcesses(getState: IStore['getState']): Promise { const logError = (error: Error) => logger.error('gumProcess.cancel failed', JSON.stringify(error)); @@ -678,9 +678,9 @@ export function _disposeAndRemoveTracks(tracks: any[]) { * @returns {Promise} - A Promise resolved once {@link JitsiTrack.dispose()} is * done for every track from the list. */ -function _disposeTracks(tracks: any) { +function _disposeTracks(tracks: any[]): Promise { return Promise.all( - tracks.map((t: any) => + tracks.map(t => t.dispose() .catch((err: Error) => { // Track might be already disposed so ignore such an error. @@ -701,7 +701,7 @@ function _disposeTracks(tracks: any) { * @private * @returns {Function} */ -function _onCreateLocalTracksRejected(error: Error, device: string) { +function _onCreateLocalTracksRejected(error?: Error, device?: string) { return (dispatch: IStore['dispatch']) => { // If permissions are not allowed, alert the user. dispatch({ @@ -728,7 +728,7 @@ function _onCreateLocalTracksRejected(error: Error, device: string) { * @private * @returns {boolean} */ -function _shouldMirror(track: any) { +function _shouldMirror(track: any): boolean { return ( track?.isLocal() && track?.isVideoTrack() @@ -755,7 +755,10 @@ function _shouldMirror(track: any) { * trackType: MEDIA_TYPE * }} */ -function _trackCreateCanceled(mediaType: MediaType) { +function _trackCreateCanceled(mediaType: MediaType): { + trackType: MediaType; + type: 'TRACK_CREATE_CANCELED'; +} { return { type: TRACK_CREATE_CANCELED, trackType: mediaType @@ -806,7 +809,11 @@ export function setNoSrcDataNotificationUid(uid?: string) { * name: string * }} */ -export function updateLastTrackVideoMediaEvent(track: any, name: string) { +export function updateLastTrackVideoMediaEvent(track: any, name: string): { + name: string; + track: any; + type: 'TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT'; +} { return { type: TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT, track, diff --git a/react/features/base/tracks/actions.native.ts b/react/features/base/tracks/actions.native.ts new file mode 100644 index 000000000..896cfe692 --- /dev/null +++ b/react/features/base/tracks/actions.native.ts @@ -0,0 +1,80 @@ +/* eslint-disable lines-around-comment */ +import { IState, IStore } from '../../app/types'; +// @ts-ignore +import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions'; +// @ts-ignore +import { setAudioOnly } from '../audio-only'; +import JitsiMeetJS from '../lib-jitsi-meet'; + +import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from './actions.any'; +// @ts-ignore +import { getLocalVideoTrack, isLocalVideoTrackDesktop } from './functions'; +/* eslint-enable lines-around-comment */ + +export * from './actions.any'; + +/** + * Signals that the local participant is ending screensharing or beginning the screensharing flow. + * + * @param {boolean} enabled - The state to toggle screen sharing to. + * @returns {Function} + */ +export function toggleScreensharing(enabled: boolean): Function { + return (store: IStore) => _toggleScreenSharing(enabled, store); +} + +/** + * Toggles screen sharing. + * + * @private + * @param {boolean} enabled - The state to toggle screen sharing to. + * @param {Store} store - The redux. + * @returns {void} + */ +function _toggleScreenSharing(enabled: boolean, store: IStore): void { + const { dispatch, getState } = store; + const state = getState(); + + if (enabled) { + const isSharing = isLocalVideoTrackDesktop(state); + + if (!isSharing) { + _startScreenSharing(dispatch, state); + } + } else { + dispatch(destroyLocalDesktopTrackIfExists()); + setPictureInPictureEnabled(true); + } +} + +/** + * Creates desktop track and replaces the local one. + * + * @private + * @param {Dispatch} dispatch - The redux {@code dispatch} function. + * @param {Object} state - The redux state. + * @returns {void} + */ +function _startScreenSharing(dispatch: Function, state: IState) { + setPictureInPictureEnabled(false); + + JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] }) + .then((tracks: any[]) => { + const track = tracks[0]; + const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']); + const currentJitsiTrack = currentLocalTrack?.jitsiTrack; + + dispatch(replaceLocalTrack(currentJitsiTrack, track)); + + const { enabled: audioOnly } = state['features/base/audio-only']; + + if (audioOnly) { + dispatch(setAudioOnly(false)); + } + }) + .catch((error: any) => { + console.log('ERROR creating ScreeSharing stream ', error); + + setPictureInPictureEnabled(true); + }); +} diff --git a/react/features/base/tracks/actions.web.ts b/react/features/base/tracks/actions.web.ts new file mode 100644 index 000000000..a2d792251 --- /dev/null +++ b/react/features/base/tracks/actions.web.ts @@ -0,0 +1,284 @@ +/* eslint-disable lines-around-comment */ +// @ts-ignore +import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors'; +import { IState, IStore } from '../../app/types'; +import { showModeratedNotification } from '../../av-moderation/actions'; +import { shouldShowModeratedNotification } from '../../av-moderation/functions'; +import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions'; +import { showNotification } from '../../notifications/actions'; +import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants'; +import { isModerationNotificationDisplayed } from '../../notifications/functions'; +// @ts-ignore +import { stopReceiver } from '../../remote-control/actions'; +// @ts-ignore +import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share/actions'; +import { isAudioOnlySharing, isScreenVideoShared } from '../../screen-share/functions'; +// @ts-ignore +import { isScreenshotCaptureEnabled, toggleScreenshotCaptureSummary } from '../../screenshot-capture'; +// @ts-ignore +import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect'; +import { setAudioOnly } from '../audio-only/actions'; +import { getCurrentConference } from '../conference/functions'; +import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any'; +import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet'; +import { setScreenshareMuted } from '../media/actions'; +import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants'; +/* eslint-enable lines-around-comment */ + +import { + addLocalTrack, + replaceLocalTrack +} from './actions.any'; +import { + createLocalTracksF, + getLocalDesktopTrack, + getLocalJitsiAudioTrack +} from './functions'; +import { ShareOptions, ToggleScreenSharingOptions } from './types'; + +export * from './actions.any'; + +declare const APP: any; + +/** + * Signals that the local participant is ending screensharing or beginning the screensharing flow. + * + * @param {boolean} enabled - The state to toggle screen sharing to. + * @param {boolean} audioOnly - Only share system audio. + * @param {boolean} ignoreDidHaveVideo - Whether or not to ignore if video was on when sharing started. + * @param {Object} shareOptions - The options to be passed for capturing screenshare. + * @returns {Function} + */ +export function toggleScreensharing( + enabled?: boolean, + audioOnly = false, + ignoreDidHaveVideo = false, + shareOptions: ShareOptions = {}) { + return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + // check for A/V Moderation when trying to start screen sharing + if ((enabled || enabled === undefined) + && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, getState())) { + if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, getState())) { + dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER)); + } + + return Promise.reject(); + } + + if (getMultipleVideoSendingSupportFeatureFlag(getState())) { + return _toggleScreenSharing({ + enabled, + audioOnly, + shareOptions + }, { + dispatch, + getState + }); + } + + return APP.conference.toggleScreenSharing(enabled, { + audioOnly, + desktopStream: shareOptions?.desktopStream + }, ignoreDidHaveVideo); + }; +} + +/** + * Displays a UI notification for screensharing failure based on the error passed. + * + * @private + * @param {Object} error - The error. + * @param {Object} store - The redux store. + * @returns {void} + */ +function _handleScreensharingError( + error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, + { dispatch }: IStore): void { + if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) { + return; + } + let descriptionKey, titleKey; + + if (error.name === JitsiTrackErrors.PERMISSION_DENIED) { + descriptionKey = 'dialog.screenSharingPermissionDeniedError'; + titleKey = 'dialog.screenSharingFailedTitle'; + } else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) { + descriptionKey = 'dialog.cameraConstraintFailedError'; + titleKey = 'deviceError.cameraError'; + } else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) { + descriptionKey = 'dialog.screenSharingFailed'; + titleKey = 'dialog.screenSharingFailedTitle'; + } else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) { + descriptionKey = 'notify.screenShareNoAudio'; + titleKey = 'notify.screenShareNoAudioTitle'; + } + + dispatch(showNotification({ + titleKey, + descriptionKey + }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM)); +} + + +/** + * Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop + * audio track is added to the conference. + * + * @private + * @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference. + * @param {*} state - The redux state. + * @returns {void} + */ +async function _maybeApplyAudioMixerEffect(desktopAudioTrack: any, state: IState): Promise { + const localAudio = getLocalJitsiAudioTrack(state); + const conference = getCurrentConference(state); + + if (localAudio) { + // If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API. + const mixerEffect = new AudioMixerEffect(desktopAudioTrack); + + await localAudio.setEffect(mixerEffect); + } else { + // If no local stream is present ( i.e. no input audio devices) we use the screen share audio + // stream as we would use a regular stream. + await conference.replaceTrack(null, desktopAudioTrack); + } +} + + +/** + * Toggles screen sharing. + * + * @private + * @param {boolean} enabled - The state to toggle screen sharing to. + * @param {Store} store - The redux store. + * @returns {void} + */ +async function _toggleScreenSharing( + { + enabled, + audioOnly = false, + shareOptions = {} + }: ToggleScreenSharingOptions, + store: IStore +): Promise { + const { dispatch, getState } = store; + const state = getState(); + const audioOnlySharing = isAudioOnlySharing(state); + const screenSharing = isScreenVideoShared(state); + const conference = getCurrentConference(state); + const localAudio = getLocalJitsiAudioTrack(state); + const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']); + + // Toggle screenshare or audio-only share if the new state is not passed. Happens in the following two cases. + // 1. ShareAudioDialog passes undefined when the user hits continue in the share audio demo modal. + // 2. Toggle screenshare called from the external API. + const enable = audioOnly + ? enabled ?? !audioOnlySharing + : enabled ?? !screenSharing; + const screensharingDetails: { sourceType?: string; } = {}; + + if (enable) { + let tracks; + + // Spot proxy stream. + if (shareOptions.desktopStream) { + tracks = [ shareOptions.desktopStream ]; + } else { + const { _desktopSharingSourceDevice } = state['features/base/config']; + + if (!shareOptions.desktopSharingSources && _desktopSharingSourceDevice) { + shareOptions.desktopSharingSourceDevice = _desktopSharingSourceDevice; + } + + const options = { + devices: [ VIDEO_TYPE.DESKTOP ], + ...shareOptions + }; + + try { + tracks = await createLocalTracksF(options) as any[]; + } catch (error) { + _handleScreensharingError(error as any, store); + + throw error; + } + } + + const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO); + const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO); + + if (audioOnly) { + // Dispose the desktop track for audio-only screensharing. + desktopVideoTrack.dispose(); + + if (!desktopAudioTrack) { + _handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store); + + throw new Error(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK); + } + } else if (desktopVideoTrack) { + if (localScreenshare) { + await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference)); + } else { + await dispatch(addLocalTrack(desktopVideoTrack)); + } + if (isScreenshotCaptureEnabled(state, false, true)) { + dispatch(toggleScreenshotCaptureSummary(true)); + } + screensharingDetails.sourceType = desktopVideoTrack.sourceType; + } + + // Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference + // otherwise without unmuting the microphone. + if (desktopAudioTrack) { + // Noise suppression doesn't work with desktop audio because we can't chain track effects yet, disable it + // first. We need to to wait for the effect to clear first or it might interfere with the audio mixer. + await dispatch(setNoiseSuppressionEnabled(false)); + _maybeApplyAudioMixerEffect(desktopAudioTrack, state); + dispatch(setScreenshareAudioTrack(desktopAudioTrack)); + + // Handle the case where screen share was stopped from the browsers 'screen share in progress' window. + if (audioOnly) { + desktopAudioTrack?.on( + JitsiTrackEvents.LOCAL_TRACK_STOPPED, + () => dispatch(toggleScreensharing(undefined, true))); + } + } + + // Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to + // audio-only screensharing. + const { enabled: bestPerformanceMode } = state['features/base/audio-only']; + + if (bestPerformanceMode && !audioOnly) { + dispatch(setAudioOnly(false)); + } + } else { + const { desktopAudioTrack } = state['features/screen-share']; + + dispatch(stopReceiver()); + + dispatch(toggleScreenshotCaptureSummary(false)); + + // Mute the desktop track instead of removing it from the conference since we don't want the client to signal + // a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the + // same sender will be re-used without the need for signaling a new ssrc through source-add. + dispatch(setScreenshareMuted(true)); + if (desktopAudioTrack) { + if (localAudio) { + localAudio.setEffect(undefined); + } else { + await conference.replaceTrack(desktopAudioTrack, null); + } + desktopAudioTrack.dispose(); + dispatch(setScreenshareAudioTrack(null)); + } + } + + if (audioOnly) { + dispatch(setScreenAudioShareState(enable)); + } else { + // Notify the external API. + APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails); + } +} diff --git a/react/features/base/tracks/logger.ts b/react/features/base/tracks/logger.ts index 54bc5de70..63d7574de 100644 --- a/react/features/base/tracks/logger.ts +++ b/react/features/base/tracks/logger.ts @@ -1,5 +1,3 @@ -// @flow - import { getLogger } from '../logging/functions'; export default getLogger('features/base/tracks'); diff --git a/react/features/base/tracks/middleware.ts b/react/features/base/tracks/middleware.ts index 07d288482..540e1adab 100644 --- a/react/features/base/tracks/middleware.ts +++ b/react/features/base/tracks/middleware.ts @@ -42,6 +42,8 @@ import { trackMuteUnmuteFailed, trackNoDataFromSourceNotificationInfoChanged, trackRemoved + + // @ts-ignore } from './actions'; import { getLocalTrack, diff --git a/react/features/base/tracks/types.ts b/react/features/base/tracks/types.ts index 121008ec1..ea83c24d4 100644 --- a/react/features/base/tracks/types.ts +++ b/react/features/base/tracks/types.ts @@ -17,3 +17,15 @@ export interface TrackOptions { micDeviceId?: string | null; timeout?: number; } + +export interface ToggleScreenSharingOptions { + audioOnly: boolean; + enabled?: boolean; + shareOptions: ShareOptions; +} + +export interface ShareOptions { + desktopSharingSourceDevice?: string; + desktopSharingSources?: string[]; + desktopStream?: any; +} diff --git a/react/features/conference/components/web/Conference.js b/react/features/conference/components/web/Conference.js index 07c5b591d..b1070f592 100644 --- a/react/features/conference/components/web/Conference.js +++ b/react/features/conference/components/web/Conference.js @@ -21,7 +21,7 @@ import { Prejoin, isPrejoinPageVisible } from '../../../prejoin'; import { toggleToolboxVisible } from '../../../toolbox/actions.any'; import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web'; import { JitsiPortal, Toolbox } from '../../../toolbox/components/web'; -import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; +import { LAYOUT_CLASSNAMES, getCurrentLayout } from '../../../video-layout'; import { maybeShowSuboptimalExperienceNotification } from '../../functions'; import { AbstractConference, @@ -48,20 +48,6 @@ const FULL_SCREEN_EVENTS = [ 'fullscreenchange' ]; -/** - * The CSS class to apply to the root element of the conference so CSS can - * modify the app layout. - * - * @private - * @type {Object} - */ -export const LAYOUT_CLASSNAMES = { - [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip', - [LAYOUTS.TILE_VIEW]: 'tile-view', - [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip', - [LAYOUTS.STAGE_FILMSTRIP_VIEW]: 'stage-filmstrip' -}; - /** * The type of the React {@code Component} props of {@link Conference}. */ diff --git a/react/features/filmstrip/components/web/ScreenshareFilmstrip.js b/react/features/filmstrip/components/web/ScreenshareFilmstrip.js index 9c29820b4..2fa545098 100644 --- a/react/features/filmstrip/components/web/ScreenshareFilmstrip.js +++ b/react/features/filmstrip/components/web/ScreenshareFilmstrip.js @@ -2,8 +2,7 @@ import React from 'react'; import { connect } from '../../../base/redux'; -import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference'; -import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; +import { LAYOUTS, LAYOUT_CLASSNAMES, getCurrentLayout } from '../../../video-layout'; import { FILMSTRIP_TYPE } from '../../constants'; diff --git a/react/features/filmstrip/components/web/StageFilmstrip.js b/react/features/filmstrip/components/web/StageFilmstrip.js index 5d6daae96..ef588d026 100644 --- a/react/features/filmstrip/components/web/StageFilmstrip.js +++ b/react/features/filmstrip/components/web/StageFilmstrip.js @@ -4,8 +4,7 @@ import React from 'react'; import { getToolbarButtons } from '../../../base/config'; import { isMobileBrowser } from '../../../base/environment/utils'; import { connect } from '../../../base/redux'; -import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference'; -import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; +import { LAYOUTS, LAYOUT_CLASSNAMES, getCurrentLayout } from '../../../video-layout'; import { ASPECT_RATIO_BREAKPOINT, FILMSTRIP_TYPE, diff --git a/react/features/filmstrip/components/web/Thumbnail.tsx b/react/features/filmstrip/components/web/Thumbnail.tsx index 07ecb0e48..4a2b876f1 100644 --- a/react/features/filmstrip/components/web/Thumbnail.tsx +++ b/react/features/filmstrip/components/web/Thumbnail.tsx @@ -32,6 +32,7 @@ import { import { Participant } from '../../../base/participants/types'; import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants'; import { isTestModeEnabled } from '../../../base/testing/functions'; +// @ts-ignore import { trackStreamingStatusChanged, updateLastTrackVideoMediaEvent } from '../../../base/tracks/actions'; import { getLocalAudioTrack, diff --git a/react/features/remote-control/actions.js b/react/features/remote-control/actions.js index e335dc47b..f1361b8b9 100644 --- a/react/features/remote-control/actions.js +++ b/react/features/remote-control/actions.js @@ -5,9 +5,15 @@ import $ from 'jquery'; import { getMultipleVideoSendingSupportFeatureFlag } from '../base/config/functions.any'; import { openDialog } from '../base/dialog'; import { JitsiConferenceEvents } from '../base/lib-jitsi-meet'; -import { getParticipantDisplayName, getPinnedParticipant, pinParticipant } from '../base/participants'; +import { + getParticipantDisplayName, + getPinnedParticipant, + getVirtualScreenshareParticipantByOwnerId, + pinParticipant +} from '../base/participants'; import { getLocalDesktopTrack, getLocalVideoTrack, toggleScreensharing } from '../base/tracks'; import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications'; +import { isScreenVideoShared } from '../screen-share'; import { CAPTURE_EVENTS, @@ -198,9 +204,12 @@ export function processPermissionRequestReply(participantId: string, event: Obje // the remote control permissions has been granted // pin the controlled participant const pinnedParticipant = getPinnedParticipant(state); + const virtualScreenshareParticipantId = getVirtualScreenshareParticipantByOwnerId(state, participantId); const pinnedId = pinnedParticipant?.id; - if (pinnedId !== participantId) { + if (virtualScreenshareParticipantId && pinnedId !== virtualScreenshareParticipantId) { + dispatch(pinParticipant(virtualScreenshareParticipantId)); + } else if (!virtualScreenshareParticipantId && pinnedId !== participantId) { dispatch(pinParticipant(participantId)); } } @@ -508,6 +517,10 @@ export function sendStartRequest() { const { sourceId } = track?.jitsiTrack || {}; const { transport } = state['features/remote-control'].receiver; + if (typeof sourceId === 'undefined') { + return Promise.reject(new Error('Cannot identify screen for the remote control session')); + } + return transport.sendRequest({ name: REMOTE_CONTROL_MESSAGE_NAME, type: REQUESTS.start, @@ -536,7 +549,7 @@ export function grant(participantId: string) { const tracks = state['features/base/tracks']; const isMultiStreamSupportEnabled = getMultipleVideoSendingSupportFeatureFlag(state); const track = isMultiStreamSupportEnabled ? getLocalDesktopTrack(tracks) : getLocalVideoTrack(tracks); - const isScreenSharing = track?.videoType === 'desktop'; + const isScreenSharing = isScreenVideoShared(state); const { sourceType } = track?.jitsiTrack || {}; if (isScreenSharing && sourceType === 'screen') { diff --git a/react/features/screen-share/components/ShareAudioDialog.tsx b/react/features/screen-share/components/ShareAudioDialog.tsx index ff4ded908..891339220 100644 --- a/react/features/screen-share/components/ShareAudioDialog.tsx +++ b/react/features/screen-share/components/ShareAudioDialog.tsx @@ -6,6 +6,8 @@ import { translate } from '../../base/i18n/functions'; import { connect } from '../../base/redux/functions'; import { updateSettings } from '../../base/settings/actions'; import { shouldHideShareAudioHelper } from '../../base/settings/functions.any'; +// eslint-disable-next-line lines-around-comment +// @ts-ignore import { toggleScreensharing } from '../../base/tracks/actions'; import Checkbox from '../../base/ui/components/web/Checkbox'; import Dialog from '../../base/ui/components/web/Dialog'; diff --git a/react/features/screen-share/components/ShareScreenWarningDialog.tsx b/react/features/screen-share/components/ShareScreenWarningDialog.tsx index 5e5969b72..2372d41ae 100644 --- a/react/features/screen-share/components/ShareScreenWarningDialog.tsx +++ b/react/features/screen-share/components/ShareScreenWarningDialog.tsx @@ -4,6 +4,8 @@ import { WithTranslation } from 'react-i18next'; import { IStore } from '../../app/types'; import { translate } from '../../base/i18n/functions'; import { connect } from '../../base/redux/functions'; +// eslint-disable-next-line lines-around-comment +// @ts-ignore import { toggleScreensharing } from '../../base/tracks/actions'; import Dialog from '../../base/ui/components/web/Dialog'; diff --git a/react/features/screen-share/reducer.ts b/react/features/screen-share/reducer.ts index ebf4005db..3f4f39d8b 100644 --- a/react/features/screen-share/reducer.ts +++ b/react/features/screen-share/reducer.ts @@ -9,7 +9,7 @@ import { export interface IScreenShareState { captureFrameRate?: number; - desktopAudioTrack?: Object; + desktopAudioTrack?: any; isSharingAudio?: boolean; } diff --git a/react/features/video-layout/constants.ts b/react/features/video-layout/constants.ts index 102fc5fa2..eb8598bed 100644 --- a/react/features/video-layout/constants.ts +++ b/react/features/video-layout/constants.ts @@ -9,3 +9,16 @@ export const LAYOUTS = { VERTICAL_FILMSTRIP_VIEW: 'vertical-filmstrip-view', STAGE_FILMSTRIP_VIEW: 'stage-filmstrip-view' }; + + +/** + * The CSS class to apply so CSS can modify the app layout. + * + * @private + */ +export const LAYOUT_CLASSNAMES = { + [LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip', + [LAYOUTS.TILE_VIEW]: 'tile-view', + [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip', + [LAYOUTS.STAGE_FILMSTRIP_VIEW]: 'stage-filmstrip' +}; diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index e1e07e6d5..1ca4701e7 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -40,7 +40,6 @@ export default { */ TOGGLE_FILMSTRIP: 'UI.toggle_filmstrip', - TOGGLE_SCREENSHARING: 'UI.toggle_screensharing', HANGUP: 'UI.hangup', LOGOUT: 'UI.logout', VIDEO_DEVICE_CHANGED: 'UI.video_device_changed',