From c2be831486622fe886121a27a089979e34400d0c Mon Sep 17 00:00:00 2001 From: Mihaela Dumitru Date: Fri, 25 Aug 2023 16:24:43 +0300 Subject: [PATCH] feat(setup) select preferred resolution --- .../common/app-state/setup/action-types.js | 2 + .../src/common/app-state/setup/actions.js | 14 +++++ .../src/common/app-state/setup/reducer.js | 8 ++- .../src/common/app-state/setup/selectors.js | 11 ++++ .../src/common/app-state/setup/setup.test.js | 12 +++++ spot-client/src/common/i18n/en.json | 4 ++ spot-client/src/common/utils/meeting.js | 34 ++++++++++++ .../meeting-frame/AbstractMeetingFrame.js | 1 + .../JitsiMeetingFrame/JitsiMeetingFrame.js | 5 +- .../setup/select-media/select-media.js | 54 ++++++++++++++++++- spot-client/src/spot-tv/ui/views/meeting.js | 5 ++ 11 files changed, 146 insertions(+), 4 deletions(-) diff --git a/spot-client/src/common/app-state/setup/action-types.js b/spot-client/src/common/app-state/setup/action-types.js index e77c1cbf2..aac781332 100644 --- a/spot-client/src/common/app-state/setup/action-types.js +++ b/spot-client/src/common/app-state/setup/action-types.js @@ -14,6 +14,8 @@ export const SET_JWT = 'SET_JWT_TOKEN'; export const SET_PREFERRED_DEVICES = 'SET_PREFERRED_DEVICES'; +export const SET_PREFERRED_RESOLUTION = 'SET_PREFERRED_RESOLUTION'; + export const SET_ROOM_ID = 'SET_ROOM_ID'; export const SET_TENANT = 'SET_TENANT'; diff --git a/spot-client/src/common/app-state/setup/actions.js b/spot-client/src/common/app-state/setup/actions.js index f9fd12f0d..718a12741 100644 --- a/spot-client/src/common/app-state/setup/actions.js +++ b/spot-client/src/common/app-state/setup/actions.js @@ -7,6 +7,7 @@ import { SET_IS_SPOT, SET_JWT, SET_PREFERRED_DEVICES, + SET_PREFERRED_RESOLUTION, SET_ROOM_ID, SET_TENANT } from './action-types'; @@ -111,6 +112,19 @@ export function setPreferredDevices(cameraLabel, micLabel, speakerLabel) { }; } +/** + * Stores the video resolution that should be used when video conferencing. + * + * @param {string} resolution - The resolution value. + * @returns {Object} + */ +export function setPreferredResolution(resolution) { + return { + type: SET_PREFERRED_RESOLUTION, + resolution + }; +} + /** * Sets the room ID provided by the backend. * diff --git a/spot-client/src/common/app-state/setup/reducer.js b/spot-client/src/common/app-state/setup/reducer.js index b7117fcfc..025b0a137 100644 --- a/spot-client/src/common/app-state/setup/reducer.js +++ b/spot-client/src/common/app-state/setup/reducer.js @@ -6,6 +6,7 @@ import { SET_IS_SPOT, SET_JWT, SET_PREFERRED_DEVICES, + SET_PREFERRED_RESOLUTION, SET_TENANT } from './action-types'; @@ -17,6 +18,7 @@ const DEFAULT_STATE = { isSpot: false, preferredCamera: undefined, preferredMic: undefined, + preferredResolution: undefined, preferredSpeaker: undefined, startParams: {} }; @@ -74,7 +76,11 @@ const setup = (state = DEFAULT_STATE, action) => { preferredMic: action.micLabel, preferredSpeaker: action.speakerLabel }; - + case SET_PREFERRED_RESOLUTION: + return { + ...state, + preferredResolution: action.resolution + }; case SET_TENANT: return { ...state, diff --git a/spot-client/src/common/app-state/setup/selectors.js b/spot-client/src/common/app-state/setup/selectors.js index fdb28807d..3e5c6300e 100644 --- a/spot-client/src/common/app-state/setup/selectors.js +++ b/spot-client/src/common/app-state/setup/selectors.js @@ -41,6 +41,17 @@ export function getPreferredCamera(state) { return state.setup.preferredCamera; } +/** + * A selector which returns the resolution that should be + * used to compute video constraints. + * + * @param {Object} state - The Redux state. + * @returns {string} + */ +export function getPreferredResolution(state) { + return state.setup.preferredResolution; +} + /** * A selector which returns the label for the microphone device that should be * attempted to be used when starting a call. diff --git a/spot-client/src/common/app-state/setup/setup.test.js b/spot-client/src/common/app-state/setup/setup.test.js index f24fdbf00..281ed7307 100644 --- a/spot-client/src/common/app-state/setup/setup.test.js +++ b/spot-client/src/common/app-state/setup/setup.test.js @@ -58,4 +58,16 @@ describe('setup state', () => { expect(selectors.getPreferredMic(state)).toEqual(micLabel); expect(selectors.getPreferredSpeaker(state)).toEqual(speakerLabel); }); + + it('saves preferred resolution', () => { + const resolution = '1080'; + + dispatch(actions.setPreferredResolution( + resolution + )); + + const state = getState(); + + expect(selectors.getPreferredResolution(state)).toEqual(resolution); + }); }); diff --git a/spot-client/src/common/i18n/en.json b/spot-client/src/common/i18n/en.json index 790f4fa40..b1478a1b4 100644 --- a/spot-client/src/common/i18n/en.json +++ b/spot-client/src/common/i18n/en.json @@ -150,6 +150,10 @@ "pairAsk": "Would you like to pair a permanent remote control with this room?", "preview": "Preview", "profile": "Room settings", + "resolutionLabel": "Preferred resolution", + "resolutionHighDef": "HD", + "resolutionFullHighDef": "Full HD", + "resolutionUltraHighDef": "4K", "roomByEmail": "Enter an email:", "roomSelect": "Select A Room", "screenShare": "Screen share", diff --git a/spot-client/src/common/utils/meeting.js b/spot-client/src/common/utils/meeting.js index 95065ba98..2cca2c7d8 100644 --- a/spot-client/src/common/utils/meeting.js +++ b/spot-client/src/common/utils/meeting.js @@ -1,5 +1,9 @@ import { parseURIString } from '@jitsi/js-utils/uri'; +const ASPECT_RATIO = 16 / 9; + +const MIN_HEIGHT = 180; + /** * Extrapolates a url for a meeting from given strings and matching domains. * @@ -129,3 +133,33 @@ export function parseMeetingUrl(url) { path: pathParts.join('/') }; } + +/** + * Builds the video constraints based on the preferred aspect ratio and resolution. + * + * @param {string} preferredResolution - The resolution of choice. + * @returns {Object} + */ +export const getVideoConstraints = preferredResolution => { + const resolution = Number(preferredResolution); + + return { + video: { + aspectRatio: ASPECT_RATIO, + height: { + ideal: resolution, + max: resolution, + min: MIN_HEIGHT + }, + width: { + ideal: resolution * ASPECT_RATIO, + max: resolution * ASPECT_RATIO, + min: MIN_HEIGHT * ASPECT_RATIO + }, + frameRate: { + max: 30, + min: 15 + } + } + }; +}; diff --git a/spot-client/src/spot-tv/ui/components/meeting-frame/AbstractMeetingFrame.js b/spot-client/src/spot-tv/ui/components/meeting-frame/AbstractMeetingFrame.js index 3ede1ded6..2c9722b63 100644 --- a/spot-client/src/spot-tv/ui/components/meeting-frame/AbstractMeetingFrame.js +++ b/spot-client/src/spot-tv/ui/components/meeting-frame/AbstractMeetingFrame.js @@ -27,6 +27,7 @@ export default class AbstractMeetingFrame extends React.Component { onMeetingStart: PropTypes.func, preferredCamera: PropTypes.string, preferredMic: PropTypes.string, + preferredResolution: PropTypes.string, preferredSpeaker: PropTypes.string, remoteControlServer: PropTypes.object, screenshareDevice: PropTypes.string, diff --git a/spot-client/src/spot-tv/ui/components/meeting-frame/JitsiMeetingFrame/JitsiMeetingFrame.js b/spot-client/src/spot-tv/ui/components/meeting-frame/JitsiMeetingFrame/JitsiMeetingFrame.js index bed1dc8b9..f094463c8 100644 --- a/spot-client/src/spot-tv/ui/components/meeting-frame/JitsiMeetingFrame/JitsiMeetingFrame.js +++ b/spot-client/src/spot-tv/ui/components/meeting-frame/JitsiMeetingFrame/JitsiMeetingFrame.js @@ -9,7 +9,7 @@ import { } from 'common/app-state'; import { logger } from 'common/logger'; import { COMMANDS, MESSAGES } from 'common/remote-control'; -import { parseMeetingUrl } from 'common/utils'; +import { getVideoConstraints, parseMeetingUrl } from 'common/utils'; import bindAll from 'lodash.bindall'; import React from 'react'; import { connect } from 'react-redux'; @@ -129,6 +129,9 @@ export class JitsiMeetingFrame extends AbstractMeetingFrame { this._jitsiApi = new JitsiMeetExternalAPI(`${host}${path}`, { configOverwrite: { _desktopSharingSourceDevice: this.props.screenshareDevice, + ...Boolean(this.props.preferredResolution) && { + constraints: getVideoConstraints(this.props.preferredResolution) + }, desktopSharingFrameRate: { max: this.props.maxDesktopSharingFramerate, min: this.props.minDesktopSharingFramerate diff --git a/spot-client/src/spot-tv/ui/components/setup/select-media/select-media.js b/spot-client/src/spot-tv/ui/components/setup/select-media/select-media.js index d0ca9784b..efa442362 100644 --- a/spot-client/src/spot-tv/ui/components/setup/select-media/select-media.js +++ b/spot-client/src/spot-tv/ui/components/setup/select-media/select-media.js @@ -2,9 +2,11 @@ import { addNotification, getPreferredCamera, getPreferredMic, + getPreferredResolution, getPreferredSpeaker, getWiredScreenshareInputLabel, setPreferredDevices, + setPreferredResolution, setSpotTVState, setWiredScreenshareInputIdleValue, setWiredScreenshareInputLabel @@ -17,7 +19,6 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; - import { wiredScreenshareService } from './../../../../wired-screenshare-service'; import CameraPreview from './camera-preview'; import MediaSelector from './media-selector'; @@ -43,6 +44,7 @@ class SelectMedia extends React.Component { onSuccess: PropTypes.func, preferredCamera: PropTypes.string, preferredMic: PropTypes.string, + preferredResolution: PropTypes.string, preferredScreenshareDongle: PropTypes.string, preferredSpeaker: PropTypes.string, t: PropTypes.func @@ -62,6 +64,7 @@ class SelectMedia extends React.Component { saving: false, selectedCamera: props.preferredCamera, selectedMic: props.preferredMic, + selectedResolution: props.preferredResolution, selectedScreenshareDongle: props.preferredScreenshareDongle, selectedSpeaker: props.preferredSpeaker }; @@ -69,6 +72,7 @@ class SelectMedia extends React.Component { this._onCameraChange = this._onCameraChange.bind(this); this._onDeviceListChange = this._onDeviceListChange.bind(this); this._onMicChange = this._onMicChange.bind(this); + this._onResolutionChange = this._onResolutionChange.bind(this); this._onScreenshareChange = this._onScreenshareChange.bind(this); this._onSkip = this._onSkip.bind(this); this._onSpeakerChange = this._onSpeakerChange.bind(this); @@ -106,10 +110,12 @@ class SelectMedia extends React.Component { const { cameras, mics, + resolutions, saving, screenshareDongles, selectedCamera, selectedMic, + selectedResolution, selectedScreenshareDongle, selectedSpeaker, speakers @@ -126,6 +132,15 @@ class SelectMedia extends React.Component { onChange = { this._onCameraChange } type = 'camera' /> ); + const resolutionSelect = ( + + ); const micSelect = (
{ cameraSelect } + { resolutionSelect } { micSelect } { speakerSelect } { screenshareSelect } @@ -235,6 +251,21 @@ class SelectMedia extends React.Component { _getDefaultDeviceListState() { return { cameras: [], + resolutions: [ + { + label: this.props.t('setup.noSelection'), + value: '' + }, { + label: this.props.t('setup.resolutionHighDef'), + value: '720' + }, { + label: this.props.t('setup.resolutionFullHighDef'), + value: '1080' + }, { + label: this.props.t('setup.resolutionUltraHighDef'), + value: '2160' + } + ], mics: [], screenshareDongles: [ { @@ -327,6 +358,19 @@ class SelectMedia extends React.Component { this.setState(newDeviceLists); } + /** + * Callback invoked when the selected resolution has changed. + * + * @param {string} resolution - The selected resolution. + * @private + * @returns {void} + */ + _onResolutionChange(resolution) { + this.setState({ + selectedResolution: resolution + }); + } + /** * Callback invoked when the selected mic (audioinput) has changed. * @@ -389,6 +433,7 @@ class SelectMedia extends React.Component { const { selectedCamera, selectedMic, + selectedResolution, selectedScreenshareDongle, selectedSpeaker } = this.state; @@ -423,13 +468,17 @@ class SelectMedia extends React.Component { selectedSpeaker )); + this.props.dispatch(setPreferredResolution( + selectedResolution + )); + this.props.dispatch(setSpotTVState({ wiredScreensharingEnabled: Boolean(selectedScreenshareDongle) })); }) .then(() => this.props.onSuccess()) .catch(error => { - logger.error('Error while saving preferred devices', { error }); + logger.error('Error while saving preferences', { error }); this.props.dispatch(addNotification('error', 'appEvents.devicesNotSaved')); @@ -450,6 +499,7 @@ function mapStateToProps(state) { return { preferredCamera: getPreferredCamera(state), preferredMic: getPreferredMic(state), + preferredResolution: getPreferredResolution(state), preferredScreenshareDongle: getWiredScreenshareInputLabel(state), preferredSpeaker: getPreferredSpeaker(state) }; diff --git a/spot-client/src/spot-tv/ui/views/meeting.js b/spot-client/src/spot-tv/ui/views/meeting.js index 9d136619e..7a1cb3044 100644 --- a/spot-client/src/spot-tv/ui/views/meeting.js +++ b/spot-client/src/spot-tv/ui/views/meeting.js @@ -12,6 +12,7 @@ import { getMeetingJoinTimeout, getPreferredCamera, getPreferredMic, + getPreferredResolution, getPreferredSpeaker, getWiredScreenshareInputLabel, leaveMeetingWithError, @@ -57,6 +58,7 @@ export class Meeting extends React.Component { onError: PropTypes.func, preferredCamera: PropTypes.string, preferredMic: PropTypes.string, + preferredResolution: PropTypes.string, preferredSpeaker: PropTypes.string, processMeetingSummary: PropTypes.func, remoteControlServer: PropTypes.object, @@ -141,6 +143,7 @@ export class Meeting extends React.Component { minDesktopSharingFramerate, preferredCamera, preferredMic, + preferredResolution, preferredSpeaker, remoteControlServer, screenshareDevice, @@ -170,6 +173,7 @@ export class Meeting extends React.Component { onMeetingStart = { this._onMeetingStart } preferredCamera = { preferredCamera } preferredMic = { preferredMic } + preferredResolution = { preferredResolution } preferredSpeaker = { preferredSpeaker } remoteControlServer = { remoteControlServer } screenshareDevice = { screenshareDevice } @@ -350,6 +354,7 @@ function mapStateToProps(state) { showPasswordPrompt: needPassword, preferredCamera: getPreferredCamera(state), preferredMic: getPreferredMic(state), + preferredResolution: getPreferredResolution(state), preferredSpeaker: getPreferredSpeaker(state), screenshareDevice: getWiredScreenshareInputLabel(state), waitingForMeetingStart