fix(remote-control): when multistream is enabled

This commit is contained in:
Hristo Terezov 2022-10-06 12:18:22 -05:00
parent e218c0d3af
commit 53e4f584f9
21 changed files with 466 additions and 413 deletions

View File

@ -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);
}
);
},
/**

View File

@ -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';

View File

@ -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);
});
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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<any> {
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<any> {
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,

View File

@ -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);
});
}

View File

@ -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<void> {
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<void> {
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);
}
}

View File

@ -1,5 +1,3 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/tracks');

View File

@ -42,6 +42,8 @@ import {
trackMuteUnmuteFailed,
trackNoDataFromSourceNotificationInfoChanged,
trackRemoved
// @ts-ignore
} from './actions';
import {
getLocalTrack,

View File

@ -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;
}

View File

@ -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}.
*/

View File

@ -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';

View File

@ -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,

View File

@ -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,

View File

@ -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') {

View File

@ -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';

View File

@ -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';

View File

@ -9,7 +9,7 @@ import {
export interface IScreenShareState {
captureFrameRate?: number;
desktopAudioTrack?: Object;
desktopAudioTrack?: any;
isSharingAudio?: boolean;
}

View File

@ -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'
};

View File

@ -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',