diff --git a/css/modals/settings/_settings.scss b/css/modals/settings/_settings.scss index 78019c37c..5e0fad204 100644 --- a/css/modals/settings/_settings.scss +++ b/css/modals/settings/_settings.scss @@ -36,6 +36,12 @@ .calendar-tab, .more-tab, + .box { + display: flex; + justify-content: space-between; + width: 100%; + } + .profile-edit { display: flex; width: 100%; @@ -45,18 +51,24 @@ flex: 1; } .settings-sub-pane { - flex-grow: 1; + flex: 1; } + .settings-sub-pane .right { + flex: 1; + } + .settings-sub-pane .left { + flex: 1; + } + + .settings-sub-pane-element { + text-align: left; + flex: 1; + } .profile-edit-field { margin-right: 20px; } - .language-settings { - max-width: 50%; - width: 35%; - } - .calendar-tab { align-items: center; flex-direction: column; diff --git a/lang/main.json b/lang/main.json index 2f5b132ae..bd7d70f28 100644 --- a/lang/main.json +++ b/lang/main.json @@ -719,8 +719,12 @@ "signedIn": "Currently accessing calendar events for {{email}}. Click the Disconnect button below to stop accessing calendar events.", "title": "Calendar" }, + "desktopShareFramerate": "Desktop sharing frame rate", + "desktopShareWarning": "You need to restart the screen share for the new settings to take effect.", + "desktopShareHighFpsWarning": "A higher frame rate for desktop sharing might affect your bandwidth. You need to restart the screen share for the new settings to take effect.", "devices": "Devices", "followMe": "Everyone follows me", + "framesPerSecond": "frames-per-second", "language": "Language", "loggedIn": "Logged in as {{name}}", "microphones": "Microphones", diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index 582d351ce..e178a0eb4 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -13,6 +13,7 @@ import '../old-client-notification/middleware'; import '../power-monitor/middleware'; import '../prejoin/middleware'; import '../remote-control/middleware'; +import '../screen-share/middleware'; import '../shared-video/middleware'; import '../talk-while-muted/middleware'; import '../virtual-background/middleware'; diff --git a/react/features/app/reducers.web.js b/react/features/app/reducers.web.js index 5a9c9d568..c93428000 100644 --- a/react/features/app/reducers.web.js +++ b/react/features/app/reducers.web.js @@ -11,6 +11,7 @@ import '../participants-pane/reducer'; import '../power-monitor/reducer'; import '../prejoin/reducer'; import '../remote-control/reducer'; +import '../screen-share/reducer'; import '../screenshot-capture/reducer'; import '../shared-video/reducer'; import '../talk-while-muted/reducer'; diff --git a/react/features/screen-share/actionTypes.js b/react/features/screen-share/actionTypes.js index a5d310a9c..89c3a31ab 100644 --- a/react/features/screen-share/actionTypes.js +++ b/react/features/screen-share/actionTypes.js @@ -10,3 +10,11 @@ */ export const SET_SCREEN_AUDIO_SHARE_STATE = 'SET_SCREEN_AUDIO_SHARE_STATE'; +/** + * Type of action which sets the capture frame rate for screenshare. + * { + * type: SET_SCREENSHARE_CAPTURE_FRAME_RATE, + * captureFrameRate: number + * } + */ +export const SET_SCREENSHARE_CAPTURE_FRAME_RATE = 'SET_SCREENSHARE_CAPTURE_FRAME_RATE'; diff --git a/react/features/screen-share/actions.js b/react/features/screen-share/actions.js index 83ed8ad4f..8f5b9b3b5 100644 --- a/react/features/screen-share/actions.js +++ b/react/features/screen-share/actions.js @@ -1,6 +1,6 @@ // @flow -import { SET_SCREEN_AUDIO_SHARE_STATE } from './actionTypes'; +import { SET_SCREEN_AUDIO_SHARE_STATE, SET_SCREENSHARE_CAPTURE_FRAME_RATE } from './actionTypes'; /** * Updates the current known status of the shared video. @@ -17,3 +17,19 @@ export function setScreenAudioShareState(isSharingAudio: boolean) { isSharingAudio }; } + +/** + * Updates the capture frame rate for screenshare in redux. + * + * @param {number} captureFrameRate - The frame rate to be used for screenshare. + * @returns {{ + * type: SET_SCREENSHARE_CAPTURE_FRAME_RATE, + * captureFrameRate: number + * }} + */ +export function setScreenshareFramerate(captureFrameRate: number) { + return { + type: SET_SCREENSHARE_CAPTURE_FRAME_RATE, + captureFrameRate + }; +} diff --git a/react/features/screen-share/logger.js b/react/features/screen-share/logger.js new file mode 100644 index 000000000..fff715752 --- /dev/null +++ b/react/features/screen-share/logger.js @@ -0,0 +1,5 @@ +// @flow + +import { getLogger } from '../base/logging/functions'; + +export default getLogger('features/screen-share'); diff --git a/react/features/screen-share/middleware.js b/react/features/screen-share/middleware.js new file mode 100644 index 000000000..e05e249d8 --- /dev/null +++ b/react/features/screen-share/middleware.js @@ -0,0 +1,57 @@ +// @flow + +import { CONFERENCE_JOINED } from '../base/conference'; +import { MiddlewareRegistry } from '../base/redux'; + +import { SET_SCREENSHARE_CAPTURE_FRAME_RATE } from './actionTypes'; +import logger from './logger'; + +/** + * Implements the middleware of the feature screen-share. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + const result = next(action); + + switch (action.type) { + case CONFERENCE_JOINED: { + _setScreenshareCaptureFps(store); + break; + } + case SET_SCREENSHARE_CAPTURE_FRAME_RATE: { + const { captureFrameRate } = action; + + _setScreenshareCaptureFps(store, captureFrameRate); + break; + } + } + + return result; +}); + +/** + * Sets the capture frame rate for screenshare. + * + * @param {Store} store - The redux store. + * @param {number} frameRate - Frame rate to be configured. + * @private + * @returns {void} + */ +function _setScreenshareCaptureFps(store, frameRate) { + const state = store.getState(); + const { conference } = state['features/base/conference']; + const { captureFrameRate } = state['features/screen-share']; + const screenShareFps = frameRate ?? captureFrameRate; + + if (!conference) { + return; + } + + if (screenShareFps) { + logger.debug(`Setting screenshare capture frame rate as ${screenShareFps}`); + conference.setDesktopSharingFrameRate(screenShareFps); + } + +} diff --git a/react/features/screen-share/reducer.js b/react/features/screen-share/reducer.js index df8816408..89a8665f7 100644 --- a/react/features/screen-share/reducer.js +++ b/react/features/screen-share/reducer.js @@ -1,13 +1,13 @@ import { ReducerRegistry } from '../base/redux'; -import { SET_SCREEN_AUDIO_SHARE_STATE } from './actionTypes'; +import { SET_SCREEN_AUDIO_SHARE_STATE, SET_SCREENSHARE_CAPTURE_FRAME_RATE } from './actionTypes'; /** * Reduces the Redux actions of the feature features/screen-share. */ ReducerRegistry.register('features/screen-share', (state = {}, action) => { - const { isSharingAudio } = action; + const { captureFrameRate, isSharingAudio } = action; switch (action.type) { case SET_SCREEN_AUDIO_SHARE_STATE: @@ -16,6 +16,12 @@ ReducerRegistry.register('features/screen-share', (state = {}, action) => { isSharingAudio }; + case SET_SCREENSHARE_CAPTURE_FRAME_RATE: + return { + ...state, + captureFrameRate + }; + default: return state; } diff --git a/react/features/settings/actions.js b/react/features/settings/actions.js index cd537f36f..1c4a77d86 100644 --- a/react/features/settings/actions.js +++ b/react/features/settings/actions.js @@ -5,6 +5,7 @@ import { openDialog } from '../base/dialog'; import { i18next } from '../base/i18n'; import { updateSettings } from '../base/settings'; import { setPrejoinPageVisibility } from '../prejoin/actions'; +import { setScreenshareFramerate } from '../screen-share/actions'; import { SET_AUDIO_SETTINGS_VISIBILITY, @@ -99,6 +100,12 @@ export function submitMoreTab(newState: Object): Function { if (newState.currentLanguage !== currentState.currentLanguage) { i18next.changeLanguage(newState.currentLanguage); } + + if (newState.currentFramerate !== currentState.currentFramerate) { + const frameRate = parseInt(newState.currentFramerate, 10); + + dispatch(setScreenshareFramerate(frameRate)); + } }; } diff --git a/react/features/settings/components/web/MoreTab.js b/react/features/settings/components/web/MoreTab.js index e0a5a8e72..efd4914d8 100644 --- a/react/features/settings/components/web/MoreTab.js +++ b/react/features/settings/components/web/MoreTab.js @@ -11,6 +11,7 @@ import { AbstractDialogTab } from '../../../base/dialog'; import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; import { translate } from '../../../base/i18n'; import TouchmoveHack from '../../../chat/components/web/TouchmoveHack'; +import { SS_DEFAULT_FRAME_RATE } from '../../constants'; /** * The type of the React {@code Component} props of {@link MoreTab}. @@ -18,12 +19,22 @@ import TouchmoveHack from '../../../chat/components/web/TouchmoveHack'; export type Props = { ...$Exact, + /** + * The currently selected desktop share frame rate in the frame rate select dropdown. + */ + currentFramerate: string, + /** * The currently selected language to display in the language select * dropdown. */ currentLanguage: string, + /** + * All available desktop capture frame rates. + */ + desktopShareFramerates: Array, + /** * Whether or not follow me is currently active (enabled by some other participant). */ @@ -59,7 +70,6 @@ export type Props = { */ showPrejoinPage: boolean, - /** * Whether or not the user has selected the Start Audio Muted feature to be * enabled. @@ -83,6 +93,11 @@ export type Props = { */ type State = { + /** + * Whether or not the desktop share frame rate select dropdown is open. + */ + isFramerateSelectOpen: boolean, + /** * Whether or not the language select dropdown is open. */ @@ -105,12 +120,14 @@ class MoreTab extends AbstractDialogTab { super(props); this.state = { + isFramerateSelectOpen: false, isLanguageSelectOpen: false }; // Bind event handler so it is only bound once for every instance. - this._onLanguageDropdownOpenChange - = this._onLanguageDropdownOpenChange.bind(this); + this._onFramerateDropdownOpenChange = this._onFramerateDropdownOpenChange.bind(this); + this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this); + this._onLanguageDropdownOpenChange = this._onLanguageDropdownOpenChange.bind(this); this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this); this._onStartAudioMutedChanged = this._onStartAudioMutedChanged.bind(this); this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this); @@ -126,25 +143,40 @@ class MoreTab extends AbstractDialogTab { * @returns {ReactElement} */ render() { - const { showModeratorSettings, showLanguageSettings, showPrejoinSettings } = this.props; const content = []; - if (showPrejoinSettings) { - content.push(this._renderPrejoinScreenSettings()); - } + content.push(this._renderSettingsLeft()); + content.push(this._renderSettingsRight()); - content.push(this._renderKeyboardShortcutCheckbox()); + return
{ content }
; + } + _onFramerateDropdownOpenChange: (Object) => void; - if (showModeratorSettings) { - content.push(this._renderModeratorSettings()); - } + /** + * Callback invoked to toggle display of the desktop share framerate select dropdown. + * + * @param {Object} event - The event for opening or closing the dropdown. + * @private + * @returns {void} + */ + _onFramerateDropdownOpenChange({ isOpen }) { + this.setState({ isFramerateSelectOpen: isOpen }); + } - if (showLanguageSettings) { - content.push(this._renderLangaugeSelect()); - } + _onFramerateItemSelect: (Object) => void; - return
{ content }
; + /** + * Callback invoked to select a frame rate from the select dropdown. + * + * @param {Object} e - The key event to handle. + * @private + * @returns {void} + */ + _onFramerateItemSelect(e) { + const frameRate = e.currentTarget.getAttribute('data-framerate'); + + super._onChange({ currentFramerate: frameRate }); } _onLanguageDropdownOpenChange: (Object) => void; @@ -246,13 +278,89 @@ class MoreTab extends AbstractDialogTab { super._onChange({ keyboardShortcutEnable: checked }); } + /** + * Returns the React Element for the desktop share frame rate dropdown. + * + * @returns {ReactElement} + */ + _renderFramerateSelect() { + const { currentFramerate, desktopShareFramerates, t } = this.props; + const frameRateItems = desktopShareFramerates.map(frameRate => ( + + { `${frameRate} ${t('settings.framesPerSecond')}` } + )); + + return ( +
+

+ { t('settings.desktopShareFramerate') } +

+
+ + + + { frameRateItems } + + + +
+
+ { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE + ? t('settings.desktopShareHighFpsWarning') + : t('settings.desktopShareWarning') } +
+
+ ); + } + + /** + * Returns the React Element for keyboardShortcut settings. + * + * @private + * @returns {ReactElement} + */ + _renderKeyboardShortcutCheckbox() { + const { t } = this.props; + + return ( +
+

+ { t('keyboardShortcuts.keyboardShortcuts') } +

+ +
+ ); + } + /** * Returns the menu item for changing displayed language. * * @private * @returns {ReactElement} */ - _renderLangaugeSelect() { + _renderLanguageSelect() { const { currentLanguage, languages, @@ -270,7 +378,7 @@ class MoreTab extends AbstractDialogTab { return (

{ t('settings.language') } @@ -315,7 +423,7 @@ class MoreTab extends AbstractDialogTab { return (

{ t('settings.moderator') } @@ -351,7 +459,7 @@ class MoreTab extends AbstractDialogTab { return (

{ t('prejoin.premeeting') } @@ -366,26 +474,37 @@ class MoreTab extends AbstractDialogTab { } /** - * Returns the React Element for keyboardShortcut settings. + * Returns the React element that needs to be displayed on the right half of the more tabs. * * @private * @returns {ReactElement} */ - _renderKeyboardShortcutCheckbox() { - const { t } = this.props; + _renderSettingsRight() { + const { showLanguageSettings } = this.props; return (
-

- { t('keyboardShortcuts.keyboardShortcuts') } -

- + className = 'settings-sub-pane right'> + { showLanguageSettings && this._renderLanguageSelect() } + { this._renderFramerateSelect() } +
+ ); + } + + /** + * Returns the React element that needs to be displayed on the left half of the more tabs. + * + * @returns {ReactElement} + */ + _renderSettingsLeft() { + const { showPrejoinSettings, showModeratorSettings } = this.props; + + return ( +
+ { showPrejoinSettings && this._renderPrejoinScreenSettings() } + { this._renderKeyboardShortcutCheckbox() } + { showModeratorSettings && this._renderModeratorSettings() }
); } diff --git a/react/features/settings/components/web/SettingsDialog.js b/react/features/settings/components/web/SettingsDialog.js index 064335b66..ce1f54a53 100644 --- a/react/features/settings/components/web/SettingsDialog.js +++ b/react/features/settings/components/web/SettingsDialog.js @@ -194,6 +194,7 @@ function _mapStateToProps(state) { return { ...newProps, + currentFramerate: tabState.currentFramerate, currentLanguage: tabState.currentLanguage, followMeEnabled: tabState.followMeEnabled, showPrejoinPage: tabState.showPrejoinPage, diff --git a/react/features/settings/constants.js b/react/features/settings/constants.js index 692f820b9..7d1c3e1ba 100644 --- a/react/features/settings/constants.js +++ b/react/features/settings/constants.js @@ -9,3 +9,13 @@ export const SETTINGS_TABS = { * View ID for the Settings modal. */ export const SETTINGS_VIEW_ID = 'SETTINGS_VIEW_ID'; + +/** + * Default frame rate to be used for capturing screenshare. + */ +export const SS_DEFAULT_FRAME_RATE = 5; + +/** + * Supported framerates to be used for capturing screenshare. + */ +export const SS_SUPPORTED_FRAMERATES = [ 5, 15, 30 ]; diff --git a/react/features/settings/functions.js b/react/features/settings/functions.js index 5f55e740b..f30be6d22 100644 --- a/react/features/settings/functions.js +++ b/react/features/settings/functions.js @@ -11,6 +11,8 @@ import { toState } from '../base/redux'; import { parseStandardURIString } from '../base/util'; import { isFollowMeActive } from '../follow-me'; +import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants'; + declare var interfaceConfig: Object; /** @@ -95,6 +97,7 @@ export function shouldShowOnlyDeviceSelection() { */ export function getMoreTabProps(stateful: Object | Function) { const state = toState(stateful); + const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE; const language = i18next.language || DEFAULT_LANGUAGE; const { conference, @@ -112,7 +115,9 @@ export function getMoreTabProps(stateful: Object | Function) { && isLocalParticipantModerator(state)); return { + currentFramerate: framerate, currentLanguage: language, + desktopShareFramerates: SS_SUPPORTED_FRAMERATES, followMeActive: Boolean(conference && followMeActive), followMeEnabled: Boolean(conference && followMeEnabled), languages: LANGUAGES,