diff --git a/app/README.md b/app/README.md index 9e10c9f6..43efa9f9 100644 --- a/app/README.md +++ b/app/README.md @@ -35,6 +35,7 @@ can be found here: [config.example.js](public/config/config.example.js). | simulcastSharing | Enable or disable simulcast for screen sharing video. | `"boolean"` | ``false`` | | simulcastProfiles | Define different encodings for various resolutions of the video. | `"object"` | ``{ "320": [ { "scaleResolutionDownBy": 1, "maxBitRate": 150000 } ], "640": [ { "scaleResolutionDownBy": 2, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 500000 } ], "1280": [ { "scaleResolutionDownBy": 4, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 2, "maxBitRate": 500000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 1200000 } ], "1920": [ { "scaleResolutionDownBy": 6, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 3, "maxBitRate": 500000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 3500000 } ], "3840": [ { "scaleResolutionDownBy": 12, "maxBitRate": 150000 }, { "scaleResolutionDownBy": 6, "maxBitRate": 500000 }, { "scaleResolutionDownBy": 1, "maxBitRate": 10000000 } ]}`` | | adaptiveScalingFactor | The adaptive spatial layer selection scaling factor in the range [0.5, 1.0]. | | ``0.75`` | +| localRecordingEnabled | If set to true Local Recording feature will be enabled. | `"boolean"` | ``false`` | | audioOutputSupportedBrowsers | White listing browsers that support audio output device selection. | `"array"` | ``[ "chrome", "opera"]`` | | requestTimeout | The Socket.io request timeout. | `"nat"` | ``20000`` | | requestRetries | The Socket.io request maximum retries. | `"nat"` | ``3`` | diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index d2d5e7c6..8cfe7c86 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -143,6 +143,9 @@ var config = { // The adaptive spatial layer selection scaling factor in the range [0.5, 1.0]. adaptiveScalingFactor : 0.75, + // If set to true Local Recording feature will be enabled. + localRecordingEnabled : false, + // White listing browsers that support audio output device selection. audioOutputSupportedBrowsers : [ 'chrome', diff --git a/app/src/BrowserRecorder.js b/app/src/BrowserRecorder.js index befadf0f..07e3965b 100644 --- a/app/src/BrowserRecorder.js +++ b/app/src/BrowserRecorder.js @@ -2,10 +2,9 @@ import Logger from './Logger'; import streamSaver from 'streamsaver'; import { WritableStream } from 'web-streams-polyfill/ponyfill'; import { openDB, deleteDB } from 'idb'; -import * as meActions from './actions/meActions'; import { store } from './store'; import * as requestActions from './actions/requestActions'; -import { RECORDING_PAUSE, RECORDING_RESUME, RECORDING_STOP, RECORDING_START } from './actions/recorderActions'; +import * as recorderActions from './actions/recorderActions'; export default class BrowserRecorder { @@ -21,7 +20,7 @@ export default class BrowserRecorder this.recorderStream = null; this.gdmStream = null; this.roomClient = null; - this.fileName = 'apple.webm'; + this.fileName = 'default.webm'; this.logger = new Logger('Recorder'); // IndexedDB @@ -45,7 +44,7 @@ export default class BrowserRecorder displaySurface : 'browser', width : { ideal: 1920 } }, - audio : true, + audio : false, advanced : [ { width: 1920, height: 1080 }, { width: 1280, height: 720 } @@ -81,7 +80,7 @@ export default class BrowserRecorder async startLocalRecording( { - roomClient, additionalAudioTracks, recordingMimeType + roomClient, additionalAudioTracks, recordingMimeType, roomname }) { this.roomClient = roomClient; @@ -106,6 +105,8 @@ export default class BrowserRecorder try { + store.dispatch(recorderActions.setLocalRecordingState('start')); + // Screensharing this.gdmStream = await navigator.mediaDevices.getDisplayMedia( this.RECORDING_CONSTRAINTS @@ -135,10 +136,17 @@ export default class BrowserRecorder this.recorderStream = this.mixer(null, this.gdmStream); } + const dt = new Date(); + const rdt = `${dt.getFullYear() }-${ (`0${ dt.getMonth()+1}`).slice(-2) }-${ (`0${ dt.getDate()}`).slice(-2) }_${dt.getHours() }_${(`0${ dt.getMinutes()}`).slice(-2) }_${dt.getSeconds()}`; + this.recorder = new MediaRecorder( this.recorderStream, { mimeType: this.recordingMimeType } ); + const ext = this.recorder.mimeType.split(';')[0].split('/')[1]; + + this.fileName = `${roomname}-recording-${rdt}.${ext}`; + if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') { this.logger.warn('IndexedDB API is not available in this browser. Fallback to '); @@ -195,7 +203,7 @@ export default class BrowserRecorder this.recorder.onerror = (error) => { - this.logger.err(`Recorder onerror: ${error}`); + this.logger.error(`Recorder onerror: ${error}`); switch (error.name) { case 'SecurityError': @@ -271,7 +279,18 @@ export default class BrowserRecorder }; this.recorder.start(this.RECORDING_SLICE_SIZE); - meActions.setLocalRecordingState(RECORDING_START); + // abort so it dose not look stuck + + window.onbeforeunload = () => + { + if (this.recorder !== null) + { + this.stopLocalRecording(); + + // evt.returnValue = 'Are you sure you want to leave? Recording in process'; + } + }; + recorderActions.setLocalRecordingState('start'); } } @@ -288,7 +307,7 @@ export default class BrowserRecorder this.logger.error('startLocalRecording() [error:"%o"]', error); if (this.recorder) this.recorder.stop(); - store.dispatch(meActions.setLocalRecordingState(RECORDING_STOP)); + store.dispatch(recorderActions.setLocalRecordingState('stop')); if (typeof this.gdmStream !== 'undefined' && this.gdmStream && typeof this.gdmStream.getTracks === 'function') { this.gdmStream.getTracks().forEach((track) => track.stop()); @@ -303,9 +322,8 @@ export default class BrowserRecorder try { - await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: RECORDING_START }); - store.dispatch(meActions.setLocalRecordingState(RECORDING_START)); + await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: 'start' }); store.dispatch(requestActions.notify( { @@ -329,6 +347,7 @@ export default class BrowserRecorder } } + // eslint-disable-next-line no-unused-vars async stopLocalRecording() { this.logger.debug('stopLocalRecording()'); @@ -344,9 +363,8 @@ export default class BrowserRecorder }) })); - store.dispatch(meActions.setLocalRecordingState(RECORDING_STOP)); - - await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: RECORDING_STOP }); + store.dispatch(recorderActions.setLocalRecordingState('stop')); + await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: 'stop' }); } catch (error) @@ -367,14 +385,14 @@ export default class BrowserRecorder async pauseLocalRecording() { this.recorder.pause(); - store.dispatch(meActions.setLocalRecordingState(RECORDING_PAUSE)); - await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: RECORDING_PAUSE }); + store.dispatch(recorderActions.setLocalRecordingState('pause')); + await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: 'pause' }); } async resumeLocalRecording() { this.recorder.resume(); - store.dispatch(meActions.setLocalRecordingState(RECORDING_RESUME)); - await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: RECORDING_RESUME }); + store.dispatch(recorderActions.setLocalRecordingState('resume')); + await this.roomClient.sendRequest('setLocalRecording', { localRecordingState: 'resume' }); } invokeSaveAsDialog(blob) { @@ -520,12 +538,10 @@ export default class BrowserRecorder // is it already appended to stream? if (this.recorder != null && (this.recorder.state === 'recording' || this.recorder.state === 'paused')) { - - const micProducer = Object.values(producers).find((p) => p.source === 'mic'); + const micProducer = Object.values(producers).find((p) => p.kind === 'audio'); if (micProducer && this.micProducerId !== micProducer.id) { - // delete/dc previous one if (this.micProducerStreamSource) { @@ -542,11 +558,11 @@ export default class BrowserRecorder } } } - checkAudioConsumer(consumers) + checkAudioConsumer(consumers, recordingConsents) { - if (this.recorder != null && (this.recorder.state === 'recording' || this.recorder.state === 'paused')) + if (this.recorder != null && (this.recorder.state === 'recording' || this.recorder.state === 'paused') && recordingConsents!==undefined) { - const audioConsumers = Object.values(consumers).filter((p) => p.kind === 'audio'); + const audioConsumers = Object.values(consumers).filter((p) => p.kind === 'audio' && recordingConsents.includes(p.peerId)); for (let i = 0; i < audioConsumers.length; i++) { @@ -572,4 +588,5 @@ export default class BrowserRecorder } } -} \ No newline at end of file +} +export const recorder = new BrowserRecorder(); \ No newline at end of file diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index d585d2c2..9247ef42 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -20,7 +20,7 @@ import Spotlights from './Spotlights'; import { permissions } from './permissions'; import * as locales from './translations/locales'; import { createIntl } from 'react-intl'; -import { RECORDING_START, RECORDING_PAUSE, RECORDING_RESUME, RECORDING_STOP } from './actions/recorderActions'; +import * as recorderActions from './actions/recorderActions'; import { directReceiverTransform, opusReceiverTransform } from './transforms/receiver'; import { config } from './config'; @@ -3593,7 +3593,16 @@ export default class RoomClient break; } + case 'addConsentForRecording': + { + // eslint-disable-next-line no-unused-vars + const { peerId, consent } = notification.data; + store.dispatch( + peerActions.setPeerLocalRecordingConsent(peerId, consent)); + + break; + } case 'setLocalRecording': { const { peerId, localRecordingState } = notification.data; @@ -3616,7 +3625,7 @@ export default class RoomClient switch (localRecordingState) { - case RECORDING_START: + case 'start': store.dispatch(requestActions.notify( { text : intl.formatMessage({ @@ -3627,7 +3636,7 @@ export default class RoomClient }) })); break; - case RECORDING_RESUME: + case 'resume': store.dispatch(requestActions.notify( { text : intl.formatMessage({ @@ -3638,7 +3647,7 @@ export default class RoomClient }) })); break; - case RECORDING_PAUSE: + case 'pause': { store.dispatch(requestActions.notify( { @@ -3651,7 +3660,7 @@ export default class RoomClient })); break; } - case RECORDING_STOP: + case 'stop': store.dispatch(requestActions.notify( { text : intl.formatMessage({ @@ -4174,6 +4183,23 @@ export default class RoomClient } } + async addConsentForRecording(consent) + { + logger.debug('addConsentForRecording()'); + + try + { + store.dispatch( + recorderActions.setLocalRecordingConsent(consent)); + await this.sendRequest('addConsentForRecording', { consent }); + } + catch (error) + { + + logger.error('addConsentForRecording() [error:"%o"]', error); + } + } + async setAccessCode(code) { logger.debug('setAccessCode()'); diff --git a/app/src/actions/meActions.js b/app/src/actions/meActions.js index 39795745..14b12136 100644 --- a/app/src/actions/meActions.js +++ b/app/src/actions/meActions.js @@ -116,9 +116,3 @@ export const setAutoMuted = (flag) => type : 'SET_AUTO_MUTED', payload : { flag } }); - -export const setLocalRecordingState = (localRecordingState) => - ({ - type : 'SET_LOCAL_RECORDING_STATE', - payload : { localRecordingState } - }); \ No newline at end of file diff --git a/app/src/actions/peerActions.js b/app/src/actions/peerActions.js index 3d39634f..648a24ee 100644 --- a/app/src/actions/peerActions.js +++ b/app/src/actions/peerActions.js @@ -104,3 +104,9 @@ export const setPeerLocalRecordingState = (peerId, localRecordingState) => type : 'SET_PEER_LOCAL_RECORDING_STATE', payload : { peerId, localRecordingState } }); + +export const setPeerLocalRecordingConsent = (peerId, consent) => + ({ + type : 'SET_PEER_LOCAL_RECORDING_CONSENT', + payload : { peerId, consent } + }); diff --git a/app/src/actions/recorderActions.js b/app/src/actions/recorderActions.js index 3cf8b893..9b403650 100644 --- a/app/src/actions/recorderActions.js +++ b/app/src/actions/recorderActions.js @@ -1,10 +1,10 @@ -// Recoding STATE -import BrowserRecorder from '../BrowserRecorder'; - -export const RECORDING_START = 'start'; -export const RECORDING_STOP = 'stop'; -export const RECORDING_PAUSE = 'pause'; -export const RECORDING_RESUME = 'resume'; -export const RECORDING_INIT = null; - -export const recorder = new BrowserRecorder(); \ No newline at end of file +export const setLocalRecordingState = (status) => + ({ + type : 'SET_LOCAL_RECORDING_STATE', + payload : { status } + }); +export const setLocalRecordingConsent = (agreed) => + ({ + type : 'SET_LOCAL_RECORDING_CONSENT', + payload : { agreed } + }); diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 9ab73bcc..350e2268 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import { meProducersSelector, - makePermissionSelector + makePermissionSelector, + recordingConsentsPeersSelector } from '../Selectors'; import { permissions } from '../../permissions'; import { withRoomContext } from '../../RoomContext'; @@ -169,7 +170,9 @@ const Me = (props) => transports, noiseVolume, classes, - theme + theme, + recordingConsents, + localRecordingState } = props; // const width = style.width; @@ -812,6 +815,8 @@ const Me = (props) => {/* /CONTROLS BUTTONS (inside) */}

@@ -1067,7 +1078,9 @@ const makeMapStateToProps = () => hasVideoPermission : canShareVideo(state), hasScreenPermission : canShareScreen(state), noiseVolume : noise, - transports : state.transports + transports : state.transports, + localRecordingState : state.recorderReducer.localRecordingState.status, + recordingConsents : recordingConsentsPeersSelector(state) }; }; @@ -1089,7 +1102,10 @@ export default withRoomContext(connect( prev.peers === next.peers && prev.producers === next.producers && prev.settings === next.settings && - prev.transports === next.transports + prev.transports === next.transports && + prev.recorderReducer.localRecordingState.status === + next.recorderReducer.localRecordingState.status && + recordingConsentsPeersSelector(prev)===recordingConsentsPeersSelector(next) ); } } diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index f114886e..f60f9c14 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; -import { makePeerConsumerSelector } from '../Selectors'; +import { makePeerConsumerSelector, recordingConsentsPeersSelector } from '../Selectors'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; @@ -156,7 +156,9 @@ const Peer = (props) => enableLayersSwitch, isSelected, mode, - theme + theme, + localRecordingState, + recordingConsents } = props; const micEnabled = ( @@ -658,6 +660,8 @@ const Peer = (props) => @@ -1061,14 +1071,16 @@ const makeMapStateToProps = (initialState, { id }) => const mapStateToProps = (state) => { return { - peer : state.peers[id], + peer : state.peers[id], ...getPeerConsumers(state, id), - windowConsumer : state.room.windowConsumer, - fullScreenConsumer : state.room.fullScreenConsumer, - activeSpeaker : id === state.room.activeSpeakerId, - browser : state.me.browser, - isSelected : state.room.selectedPeers.includes(id), - mode : state.room.mode + windowConsumer : state.room.windowConsumer, + fullScreenConsumer : state.room.fullScreenConsumer, + activeSpeaker : id === state.room.activeSpeakerId, + browser : state.me.browser, + isSelected : state.room.selectedPeers.includes(id), + mode : state.room.mode, + localRecordingState : state.recorderReducer.localRecordingState.status, + recordingConsents : recordingConsentsPeersSelector(state) }; }; @@ -1107,7 +1119,10 @@ export default withRoomContext(connect( prev.room.mode === next.room.mode && prev.room.selectedPeers === next.room.selectedPeers && prev.me.browser === next.me.browser && - prev.enableLayersSwitch === next.enableLayersSwitch + prev.enableLayersSwitch === next.enableLayersSwitch && + prev.recorderReducer.localRecordingState.status === + next.recorderReducer.localRecordingState.status && + recordingConsentsPeersSelector(prev)===recordingConsentsPeersSelector(next) ); } } diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index d2fa67c5..e80e6fde 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -6,7 +6,9 @@ import { peersLengthSelector, raisedHandsSelector, makePermissionSelector, - recordingInProgressSelector + recordingInProgressSelector, + recordingInProgressPeersSelector, + recordingConsentsPeersSelector } from '../Selectors'; import { permissions } from '../../permissions'; import * as appPropTypes from '../appPropTypes'; @@ -48,11 +50,8 @@ import PauseCircleOutlineIcon from '@material-ui/icons/PauseCircleOutline'; import PauseCircleFilledIcon from '@material-ui/icons/PauseCircleFilled'; import StopIcon from '@material-ui/icons/Stop'; import randomString from 'random-string'; -import { recorder, RECORDING_START, RECORDING_PAUSE, RECORDING_RESUME } from '../../actions/recorderActions'; -import * as meActions from '../../actions/meActions'; -import { store } from '../../store'; -// import producers from '../../reducers/producers'; -// import logger from 'redux-logger'; +import { recorder } from './../../BrowserRecorder'; + import Logger from '../../Logger'; import { config } from '../../config'; @@ -215,8 +214,8 @@ const TopBar = (props) => const [ mobileMoreAnchorEl, setMobileMoreAnchorEl ] = useState(null); const [ anchorEl, setAnchorEl ] = useState(null); const [ currentMenu, setCurrentMenu ] = useState(null); - const [ recordingConsentNotificationId, - setRecordingConsentNotificationId ] = useState(null); + const [ recordingNotificationsId, + setRecordingNotificationsId ] = useState(null); const handleExited = () => { @@ -255,6 +254,7 @@ const TopBar = (props) => drawerOverlayed, toolAreaOpen, isSafari, + meId, isMobile, loggedIn, loginEnabled, @@ -275,29 +275,48 @@ const TopBar = (props) => unread, canProduceExtraVideo, canLock, + canRecord, canPromote, classes, locale, localesList, localRecordingState, recordingInProgress, + recordingPeers, + recordingMimeType, producers, - consumers + consumers, + recordingConsents } = props; // did it change? recorder.checkMicProducer(producers); - recorder.checkAudioConsumer(consumers); + recorder.checkAudioConsumer(consumers, recordingConsents); useEffect(() => { + // someone else is recording (need consent) or only me(dont need consent notif) + const hasConsent = ( + ( + ( + localRecordingState === undefined || + localRecordingState.consent!=='init' + ) + || ( + recordingPeers.includes(meId) && recordingPeers.length === 1 + ) + ) + ); + if ( recordingInProgress && - !recordingConsentNotificationId) + !recordingNotificationsId && + !hasConsent + ) { const notificationId = randomString({ length: 6 }).toLowerCase(); - setRecordingConsentNotificationId(notificationId); + setRecordingNotificationsId(notificationId); addNotification( { id : notificationId, @@ -309,21 +328,24 @@ const TopBar = (props) => defaultMessage : 'When attending this meeting you agree and give your consent that the meeting will be audio and video recorded and/or live broadcasted through web streaming' } ), - persist : true + peerid : meId, + roomClient : roomClient, + persist : true } ); } if ( !recordingInProgress - && recordingConsentNotificationId) + && recordingNotificationsId) { - closeNotification(recordingConsentNotificationId); - setRecordingConsentNotificationId(null); + closeNotification(recordingNotificationsId); + setRecordingNotificationsId(null); } }, [ - localRecordingState, recordingInProgress, recordingConsentNotificationId, - addNotification, closeNotification, intl + localRecordingState, recordingInProgress, recordingNotificationsId, + addNotification, closeNotification, intl, meId, recordingPeers, roomClient, + room ]); const isMenuOpen = Boolean(anchorEl); @@ -340,8 +362,8 @@ const TopBar = (props) => defaultMessage : 'Lock room' }); - const recordingTooltip = (localRecordingState === RECORDING_START || - localRecordingState === RECORDING_RESUME) ? + const recordingTooltip = (localRecordingState.status === 'start' || + localRecordingState.status === 'resume') ? intl.formatMessage({ id : 'tooltip.stopLocalRecording', defaultMessage : 'Stop local recording' @@ -352,7 +374,7 @@ const TopBar = (props) => defaultMessage : 'Start local recording' }); - const recordingPausedTooltip = localRecordingState === RECORDING_PAUSE ? + const recordingPausedTooltip = localRecordingState.status === 'pause' ? intl.formatMessage({ id : 'tooltip.resumeLocalRecording', defaultMessage : 'Resume local recording' @@ -676,9 +698,9 @@ const TopBar = (props) => { ( - localRecordingState === RECORDING_START || - localRecordingState === RECORDING_RESUME || - localRecordingState === RECORDING_PAUSE + localRecordingState.status === 'start' || + localRecordingState.status === 'resume' || + localRecordingState.status === 'pause' ) && onClick={() => { handleMenuClose(); - if (localRecordingState === RECORDING_PAUSE) + if (localRecordingState.status === 'pause') { recorder.resumeLocalRecording(); } @@ -700,13 +722,13 @@ const TopBar = (props) => - { localRecordingState === RECORDING_PAUSE ? + { localRecordingState.status === 'pause' ? : } - { localRecordingState === RECORDING_PAUSE ? + { localRecordingState.status === 'pause' ?

} - { isSafari && + { config.localRecordingEnabled && isSafari + && canRecord && { handleMenuClose(); - if (localRecordingState === RECORDING_START || - localRecordingState === RECORDING_PAUSE || - localRecordingState === RECORDING_RESUME) + if (localRecordingState.status === 'start' || + localRecordingState.status === 'pause' || + localRecordingState.status === 'resume') { - recorder.stopLocalRecording(); + recorder.stopLocalRecording(meId); } else { try { - const recordingMimeType = - store.getState().settings.recorderPreferredMimeType; const additionalAudioTracks = []; const micProducer = Object.values(producers).find((p) => p.source === 'mic'); if (micProducer) additionalAudioTracks.push(micProducer.track); - await recorder.startLocalRecording({ + const roomname = room.name; + + recorder.startLocalRecording({ roomClient, additionalAudioTracks, - recordingMimeType + recordingMimeType, + roomname }); recorder.checkAudioConsumer(consumers); - meActions.setLocalRecordingState(RECORDING_START); } catch (err) { @@ -770,9 +793,9 @@ const TopBar = (props) => color='primary' > { - (localRecordingState === RECORDING_START || - localRecordingState === RECORDING_PAUSE || - localRecordingState === RECORDING_RESUME) ? + (localRecordingState.status === 'start' || + localRecordingState.status === 'pause' || + localRecordingState.status === 'resume') ? : @@ -780,9 +803,9 @@ const TopBar = (props) => { - (localRecordingState === RECORDING_START || - localRecordingState === RECORDING_PAUSE || - localRecordingState === RECORDING_RESUME) ? + (localRecordingState.status === 'start' || + localRecordingState.status === 'pause' || + localRecordingState.status === 'resume') ?

} { ( - localRecordingState === RECORDING_PAUSE || - localRecordingState === RECORDING_RESUME || - localRecordingState === RECORDING_START + localRecordingState.status === 'pause' || + localRecordingState.status === 'resume' || + localRecordingState.status === 'start' ) && onClick={() => { handleMenuClose(); - if (localRecordingState === RECORDING_PAUSE) + if (localRecordingState.status === 'pause') { recorder.resumeLocalRecording(); } @@ -984,14 +1007,14 @@ const TopBar = (props) => - { localRecordingState === RECORDING_PAUSE ? + { localRecordingState.status === 'pause' ? : } - { localRecordingState === RECORDING_PAUSE ? + { localRecordingState.status === 'pause' ?

@@ -1272,6 +1299,8 @@ const makeMapStateToProps = () => const hasLockPermission = makePermissionSelector(permissions.CHANGE_ROOM_LOCK); + const hasRecordPermission = + makePermissionSelector(permissions.LOCAL_RECORD_ROOM); const hasPromotionPermission = makePermissionSelector(permissions.PROMOTE_PEER); @@ -1279,6 +1308,7 @@ const makeMapStateToProps = () => ({ room : state.room, isSafari : state.me.browser.name !== 'safari', + meId : state.me.id, isMobile : state.me.browser.platform === 'mobile', peersLength : peersLengthSelector(state), lobbyPeers : lobbyPeersKeySelector(state), @@ -1287,16 +1317,19 @@ const makeMapStateToProps = () => toolAreaOpen : state.toolarea.toolAreaOpen, loggedIn : state.me.loggedIn, loginEnabled : state.me.loginEnabled, - localRecordingState : state.me.localRecordingState, + localRecordingState : state.recorderReducer.localRecordingState, recordingInProgress : recordingInProgressSelector(state), + recordingPeers : recordingInProgressPeersSelector(state), + recordingConsents : recordingConsentsPeersSelector(state), unread : state.toolarea.unreadMessages + state.toolarea.unreadFiles + raisedHandsSelector(state), canProduceExtraVideo : hasExtraVideoPermission(state), canLock : hasLockPermission(state), + canRecord : hasRecordPermission(state), canPromote : hasPromotionPermission(state), locale : state.intl.locale, localesList : state.intl.list, - recordingMimeType : state.settings.recordingMimeType, + recordingMimeType : state.settings.recorderPreferredMimeType, producers : state.producers, consumers : state.consumers }); @@ -1375,14 +1408,18 @@ export default withRoomContext(connect( prev.me.loginEnabled === next.me.loginEnabled && prev.me.picture === next.me.picture && prev.me.roles === next.me.roles && - prev.me.localRecordingState === next.me.localRecordingState && + prev.recorderReducer.localRecordingState.status === + next.recorderReducer.localRecordingState.status && prev.toolarea.unreadMessages === next.toolarea.unreadMessages && prev.toolarea.unreadFiles === next.toolarea.unreadFiles && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen && prev.intl.locale === next.intl.locale && prev.intl.localesList === next.intl.localesList && prev.producers === next.producers && - prev.consumers === next.consumers + prev.consumers === next.consumers && + prev.settings.recorderPreferredMimeType === + next.settings.recorderPreferredMimeType && + recordingConsentsPeersSelector(prev)===recordingConsentsPeersSelector(next) ); } } diff --git a/app/src/components/Notifications/Notifications.js b/app/src/components/Notifications/Notifications.js index 9d743a50..226a6532 100644 --- a/app/src/components/Notifications/Notifications.js +++ b/app/src/components/Notifications/Notifications.js @@ -5,9 +5,10 @@ import { withSnackbar } from 'notistack'; import * as notificationActions from '../../actions/notificationActions'; import { config } from '../../config'; import Button from '@material-ui/core/Button'; +import ButtonGroup from '@material-ui/core/ButtonGroup'; import VerifiedUserIcon from '@material-ui/icons/VerifiedUser'; import { FormattedMessage } from 'react-intl'; - +import Lock from '@material-ui/icons/Lock'; class Notifications extends Component { displayed = []; @@ -63,15 +64,39 @@ class Notifications extends Component // customized const okAction = (key) => ( - + + + + + ); @@ -81,6 +106,8 @@ class Notifications extends Component variant : notification.type, autoHideDuration : notification.timeout, persist : notification.persist, + peerid : notification.peerid, + roomClient : notification.roomClient, key : notification.id, action : notification.persist? okAction: null, anchorOrigin : { diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index eeab10e1..70489f9a 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -1,5 +1,4 @@ import { createSelector } from 'reselect'; -import { RECORDING_INIT, RECORDING_START, RECORDING_STOP, RECORDING_PAUSE, RECORDING_RESUME } from '../actions/recorderActions'; const meRolesSelect = (state) => state.me.roles; const userRolesSelect = (state) => state.room.userRoles; @@ -10,6 +9,7 @@ const consumersSelect = (state) => state.consumers; const spotlightsSelector = (state) => state.room.spotlights; const peersSelector = (state) => state.peers; const meSelector = (state) => state.me; +const recorderReducerSelect = (state) => state.recorderReducer; const lobbyPeersSelector = (state) => state.lobbyPeers; const getPeerConsumers = (state, id) => (state.peers[id] ? state.peers[id].consumers : null); @@ -304,30 +304,93 @@ export const makePermissionSelector = (permission) => export const recordingInProgressSelector = createSelector( peersValueSelector, - meSelector, - (peers, me) => + recorderReducerSelect, + (peers, recorderReducer) => { if ( - me.localRecordingState === RECORDING_START || - me.localRecordingState === RECORDING_RESUME || + recorderReducer.localRecordingState.status === 'start' || + recorderReducer.localRecordingState.status === 'resume' || peers.findIndex((e) => - e.localRecordingState === RECORDING_START || - e.localRecordingState === RECORDING_RESUME + e.localRecordingState !== undefined && + ( + e.localRecordingState === 'start' || + e.localRecordingState === 'resume' + ) ) !== -1 ) { return true; } else if ( - me.localRecordingState === RECORDING_INIT || - me.localRecordingState === RECORDING_STOP || - me.localRecordingState === RECORDING_PAUSE || + recorderReducer.localRecordingState.status === 'init' || + recorderReducer.localRecordingState.status === 'stop' || + recorderReducer.localRecordingState.status === 'pause' || peers.findIndex((e) => - e.localRecordingState === RECORDING_START || - e.localRecordingState === RECORDING_RESUME + e.localRecordingState !== undefined && + ( + e.localRecordingState === 'start' || + e.localRecordingState === 'resume' + ) ) === -1 ) return false; } ); + +export const recordingConsentsPeersSelector = createSelector( + peersValueSelector, + (peers) => + { + const recordingconsents = []; + + peers.forEach((e) => + { + if ( + e.localRecordingConsent !== undefined && + e.localRecordingConsent === 'agreed' + ) + { + recordingconsents.push(e.id); + } + }); + + return recordingconsents; + } +); + +export const recordingInProgressPeersSelector = createSelector( + peersValueSelector, + meSelector, + recorderReducerSelect, + (peers, me, recorderReducer) => + { + const recordingpeers = []; + + if ( + recorderReducer !== undefined && + ( + recorderReducer.localRecordingState.status === 'start' || + recorderReducer.localRecordingState.status === 'resume' + ) + ) + { + recordingpeers.push(me.id); + } + peers.forEach((e) => + { + if ( + e.recorderReducer !== undefined && + ( + e.recorderReducer.localRecordingState.status === 'start' || + e.recorderReducer.localRecordingState.status === 'resume' + ) + ) + { + recordingpeers.push(e.id); + } + }); + + return recordingpeers; + } +); diff --git a/app/src/components/VideoContainers/VideoView.js b/app/src/components/VideoContainers/VideoView.js index 4421fa34..b72effec 100644 --- a/app/src/components/VideoContainers/VideoView.js +++ b/app/src/components/VideoContainers/VideoView.js @@ -231,7 +231,10 @@ class VideoView extends React.PureComponent netInfo, width, height, - opusConfig + opusConfig, + localRecordingState, + recordingConsents, + peer } = this.props; const { @@ -427,7 +430,17 @@ class VideoView extends React.PureComponent : - {displayName} + { + ( + ( + localRecordingState==='start' || + localRecordingState==='resume' + )&& + ( + !recordingConsents.includes(peer.id) + ) + ) ? '':displayName + } } @@ -438,7 +451,18 @@ class VideoView extends React.PureComponent