diff --git a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js index a489f44738619a1c4512bce476cc47b7f52a1428..e0fbf321e6238fae0561b83a2556e5d4e06c2109 100644 --- a/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js +++ b/bigbluebutton-html5/imports/api/meetings/server/eventHandlers.js @@ -6,6 +6,7 @@ import handleMeetingDestruction from './handlers/meetingDestruction'; import handleMeetingLocksChange from './handlers/meetingLockChange'; import handleUserLockChange from './handlers/userLockChange'; import handleRecordingStatusChange from './handlers/recordingStatusChange'; +import handleRecordingTimerChange from './handlers/recordingTimerChange'; import handleChangeWebcamOnlyModerator from './handlers/webcamOnlyModerator'; RedisPubSub.on('MeetingCreatedEvtMsg', handleMeetingCreation); @@ -15,4 +16,5 @@ RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction); RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange); RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange); RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange); +RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange); RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator); diff --git a/bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js b/bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js new file mode 100755 index 0000000000000000000000000000000000000000..28cb2c4ffb734bff043174215177d6ec76e9d08c --- /dev/null +++ b/bigbluebutton-html5/imports/api/meetings/server/handlers/recordingTimerChange.js @@ -0,0 +1,23 @@ +import { check } from 'meteor/check'; +import Meetings from '/imports/api/meetings'; +import Logger from '/imports/startup/server/logger'; + +export default function handleRecordingStatusChange({ body }, meetingId) { + const { time } = body; + + const selector = { + meetingId, + }; + + const modifier = { + $set: { 'recordProp.time': time }, + }; + + const cb = (err) => { + if (err) { + Logger.error(`Changing recording time: ${err}`); + } + }; + + return Meetings.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 1e25af4a5ef377814745a1dddab08d696ff41b59..fa53c39b39e7c306c8321197c2be7a5b8975753b 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -9,12 +9,9 @@ import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import PresentationUploaderContainer from '/imports/ui/components/presentation/presentation-uploader/container'; import { withModalMounter } from '/imports/ui/components/modal/service'; -import getFromUserSettings from '/imports/ui/services/users-settings'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; -import RecordingContainer from '/imports/ui/components/recording/container'; import BreakoutRoom from '../create-breakout-room/component'; import { styles } from '../styles'; -import ActionBarService from '../service'; const propTypes = { isUserPresenter: PropTypes.bool.isRequired, @@ -91,14 +88,6 @@ class ActionsDropdown extends Component { this.createBreakoutRoomId = _.uniqueId('action-item-'); } - componentDidMount() { - if (Meteor.settings.public.allowOutsideCommands.toggleRecording || - getFromUserSettings('outsideToggleRecording', false)) { - ActionBarService.connectRecordingObserver(); - window.addEventListener('message', ActionBarService.processOutsideToggleRecording); - } - } - componentWillUpdate(nextProps) { const { isUserPresenter: isPresenter } = nextProps; const { isUserPresenter: wasPresenter, mountModal } = this.props; @@ -112,12 +101,8 @@ class ActionsDropdown extends Component { intl, isUserPresenter, isUserModerator, - allowStartStopRecording, - isRecording, - record, togglePollMenu, meetingIsBreakout, - mountModal, hasBreakoutRoom, } = this.props; @@ -141,17 +126,6 @@ class ActionsDropdown extends Component { onClick={this.handlePresentationClick} /> : null), - (record && isUserModerator && allowStartStopRecording ? - <DropdownListItem - icon="record" - label={intl.formatMessage(isRecording ? - intlMessages.stopRecording : intlMessages.startRecording)} - description={intl.formatMessage(isRecording ? - intlMessages.stopRecording : intlMessages.startRecording)} - key={this.recordId} - onClick={() => mountModal(<RecordingContainer />)} - /> - : null), (isUserModerator && !meetingIsBreakout && !hasBreakoutRoom ? <DropdownListItem icon="rooms" diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index da38b1bec2b22ef28ab9dd6a42e8d92e344fe22c..9b45847331dd76537aae3bad58046ce8b7bc9355 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -2,8 +2,6 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { Session } from 'meteor/session'; import getFromUserSettings from '/imports/ui/services/users-settings'; -import Meetings from '/imports/api/meetings'; -import Auth from '/imports/ui/services/auth'; import ActionsBar from './component'; import Service from './service'; import VideoService from '../video-provider/service'; @@ -29,18 +27,6 @@ export default withTracker(() => { return showPoll ? show() : hide(); }; - Meetings.find({ meetingId: Auth.meetingID }).observeChanges({ - changed: (id, fields) => { - if (fields.recordProp && fields.recordProp.recording) { - this.window.parent.postMessage({ response: 'recordingStarted' }, '*'); - } - - if (fields.recordProp && !fields.recordProp.recording) { - this.window.parent.postMessage({ response: 'recordingStopped' }, '*'); - } - }, - }); - return { isUserPresenter: Service.isUserPresenter(), isUserModerator: Service.isUserModerator(), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js index 1e673ea0315b226aed131f145dc3f2a5d92d4447..44d8a4357bd1219bfca9b58615f442e2cbad5d54 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -4,32 +4,7 @@ import { makeCall } from '/imports/ui/services/api'; import Meetings from '/imports/api/meetings'; import Breakouts from '/imports/api/breakouts'; -const processOutsideToggleRecording = (e) => { - switch (e.data) { - case 'c_record': { - makeCall('toggleRecording'); - break; - } - case 'c_recording_status': { - const recordingState = Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording; - const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped'; - this.window.parent.postMessage({ response: recordingMessage }, '*'); - break; - } - default: { - // console.log(e.data); - } - } -}; - -const connectRecordingObserver = () => { - // notify on load complete - this.window.parent.postMessage({ response: 'readyToConnect' }, '*'); -}; - - export default { - connectRecordingObserver: () => connectRecordingObserver(), isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter, isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator, recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp, @@ -37,6 +12,5 @@ export default { meetingName: () => Meetings.findOne({ meetingId: Auth.meetingID }).meetingProp.name, hasBreakoutRoom: () => Breakouts.find({ parentMeetingId: Auth.meetingID }).fetch().length > 0, toggleRecording: () => makeCall('toggleRecording'), - processOutsideToggleRecording: arg => processOutsideToggleRecording(arg), createBreakoutRoom: (numberOfRooms, durationInMinutes, freeJoin = true, record = false) => makeCall('createBreakoutRoom', numberOfRooms, durationInMinutes, freeJoin, record), }; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx index d4a16804c486417d0bb6d9d961ada84ec4015066..4853e0f831a59f143e3f7a1c3d52e88898518759 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/component.jsx @@ -11,11 +11,13 @@ import DropdownList from '/imports/ui/components/dropdown/list/component'; import DropdownListItem from '/imports/ui/components/dropdown/list/item/component'; import { withModalMounter } from '/imports/ui/components/modal/service'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; +import getFromUserSettings from '/imports/ui/services/users-settings'; import { defineMessages, injectIntl } from 'react-intl'; import { styles } from './styles.scss'; import Button from '../button/component'; import RecordingIndicator from './recording-indicator/component'; import SettingsDropdownContainer from './settings-dropdown/container'; +import ActionBarService from './service'; const intlMessages = defineMessages({ toggleUserListLabel: { @@ -42,22 +44,30 @@ const intlMessages = defineMessages({ id: 'app.navBar.recording.off', description: 'label for indicator when the session is not being recorded', }, + startTitle: { + id: 'app.recording.startTitle', + description: 'start recording title', + }, + stopTitle: { + id: 'app.recording.stopTitle', + description: 'stop recording title', + }, }); const propTypes = { presentationTitle: PropTypes.string, hasUnreadMessages: PropTypes.bool, - beingRecorded: PropTypes.object, + beingRecorded: PropTypes.objectOf(PropTypes.any), shortcuts: PropTypes.string, }; const defaultProps = { presentationTitle: 'Default Room Title', hasUnreadMessages: false, - beingRecorded: false, + beingRecorded: null, shortcuts: '', }; - +const interval = null; const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mountModal(<BreakoutJoinConfirmation breakout={breakout} @@ -74,18 +84,36 @@ class NavBar extends PureComponent { this.state = { isActionsOpen: false, didSendBreakoutInvite: false, + time: (props.beingRecorded.time ? props.beingRecorded.time : 0), }; + this.incrementTime = this.incrementTime.bind(this); this.handleToggleUserList = this.handleToggleUserList.bind(this); } + componentDidMount() { + if (Meteor.settings.public.allowOutsideCommands.toggleRecording || + getFromUserSettings('outsideToggleRecording', false)) { + ActionBarService.connectRecordingObserver(); + window.addEventListener('message', ActionBarService.processOutsideToggleRecording); + } + } + componentDidUpdate(oldProps) { const { breakouts, isBreakoutRoom, mountModal, + beingRecorded, } = this.props; + if (!beingRecorded.recording) { + clearInterval(this.interval); + this.interval = null; + } else if (this.interval === null) { + this.interval = setInterval(this.incrementTime, 1000); + } + const hadBreakouts = oldProps.breakouts.length; const hasBreakouts = breakouts.length; @@ -108,6 +136,10 @@ class NavBar extends PureComponent { } } + componentWillUnmount() { + clearInterval(interval); + } + handleToggleUserList() { this.props.toggleUserList(); } @@ -122,6 +154,16 @@ class NavBar extends PureComponent { }); } + incrementTime() { + const { beingRecorded } = this.props; + + if (beingRecorded.time > this.state.time) { + this.setState({ time: beingRecorded.time + 1 }); + } else { + this.setState({ time: this.state.time + 1 }); + } + } + renderPresentationTitle() { const { breakouts, @@ -178,10 +220,15 @@ class NavBar extends PureComponent { isExpanded, intl, shortcuts: TOGGLE_USERLIST_AK, + mountModal, } = this.props; const recordingMessage = beingRecorded.recording ? 'recordingIndicatorOn' : 'recordingIndicatorOff'; + if (!this.interval) { + this.interval = setInterval(this.incrementTime, 1000); + } + const toggleBtnClasses = {}; toggleBtnClasses[styles.btn] = true; toggleBtnClasses[styles.btnWithNotificationDot] = hasUnreadMessages; @@ -214,6 +261,10 @@ class NavBar extends PureComponent { <RecordingIndicator {...beingRecorded} title={intl.formatMessage(intlMessages[recordingMessage])} + buttonTitle={(!beingRecorded.recording ? intl.formatMessage(intlMessages.startTitle) : + intl.formatMessage(intlMessages.stopTitle))} + mountModal={mountModal} + time={this.state.time} /> </div> <div className={styles.right}> diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx index 25a03f51595c1c18d41ec314c0cc407c2edb22ed..bd22dfa7f3e5d862b93ea3c63b291cd1b481f380 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/container.jsx @@ -25,7 +25,6 @@ export default withTracker(() => { let meetingTitle; let meetingRecorded; - const meetingId = Auth.meetingID; const meetingObject = Meetings.findOne({ meetingId, @@ -51,6 +50,18 @@ export default withTracker(() => { .some(receiverID => ChatService.hasUnreadMessages(receiverID)); }; + Meetings.find({ meetingId: Auth.meetingID }).observeChanges({ + changed: (id, fields) => { + if (fields.recordProp && fields.recordProp.recording) { + this.window.parent.postMessage({ response: 'recordingStarted' }, '*'); + } + + if (fields.recordProp && !fields.recordProp.recording) { + this.window.parent.postMessage({ response: 'recordingStopped' }, '*'); + } + }, + }); + const breakouts = Service.getBreakouts(); const currentUserId = Auth.userID; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx index d4f2b2dcd4b47306673b643c100ef042b63806a4..b005473115f68bb63f4bff80315a06b6266af6cb 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/component.jsx @@ -1,8 +1,13 @@ import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import Button from '/imports/ui/components/button/component'; +import RecordingContainer from '/imports/ui/components/recording/container'; +import humanizeSeconds from '/imports/utils/humanizeSeconds'; import { styles } from './styles'; +import cx from 'classnames'; const RecordingIndicator = ({ - record, title, recording, + record, title, recording, buttonTitle, mountModal, time, }) => { if (!record) return null; @@ -11,7 +16,20 @@ const RecordingIndicator = ({ aria-label={title} className={styles.recordState} > - <div className={recording ? styles.recordIndicator : styles.notRecording} /> + <div className={styles.border}> + <Button + label={buttonTitle} + hideLabel + ghost + className={cx(styles.btn, recording ? styles.recordIndicator : styles.notRecording)} + onClick={() => { + mountModal(<RecordingContainer />); + }} + /> + </div> + <div className={styles.presentationTitle}> + {recording ? humanizeSeconds(time) : <div>{buttonTitle}</div>} + </div> </div> ); }; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss index 9b90b3c10bc4daa4c03f1f96e174c5b7d8a5ed25..097a52de7ca43cab2510f0e40345b9c808940557 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss @@ -3,35 +3,73 @@ %baseIndicator { position: relative; display: inline-block; - width: var(--font-size-base); - height: var(--font-size-base); + width: var(--font-size-large); + height: var(--font-size-large); border-radius: 50%; border: 1px solid var(--color-white); &:after { content: ''; - width: calc(var(--font-size-base) / 2); - height: calc(var(--font-size-base) / 2); + width: calc(var(--font-size-large) / 2); + height: calc(var(--font-size-large) / 2); position: absolute; top: 50%; left: 50%; - margin-top: calc((var(--font-size-base) / -4)); - margin-left: calc((var(--font-size-base) / -4)); + margin-top: calc((var(--font-size-large) / -4)); + margin-left: calc((var(--font-size-large) / -4)); border-radius: 50%; background-color: var(--color-danger); } } +.border { + border: 1px solid var(--color-white); + border-radius: 50%; +} + .recordIndicator { @extend %baseIndicator; } +.btn { + border-radius: 50%; + display: block; + padding: 0; + + &:hover, + &:focus { + background-color: transparent !important; + color: var(--color-white) !important; + opacity: .75; + } +} + +.presentationTitle { + display: flex; + flex-direction: column; + justify-content: center; + font-weight: 200; + color: var(--color-white); + font-size: var(--font-size-base); + margin: 0; + padding: 0; + margin-left: var(--sm-padding-x); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 30vw; + + > [class^="icon-bbb-"] { + font-size: 75%; + } +} + .notRecording { @extend %baseIndicator; - border: 1px solid var(--color-gray); + border: 1px solid var(--color-white); &:after { - background-color: var(--color-gray); + background-color: var(--color-white); } } diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/service.js b/bigbluebutton-html5/imports/ui/components/nav-bar/service.js index 56522370cb68cb6871dd9a31a5c2e2472e055e30..857da9f7403f0b66d945b81dcbdc210e1721156e 100755 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/service.js @@ -1,8 +1,37 @@ +import Auth from '/imports/ui/services/auth'; import Breakouts from '/imports/api/breakouts'; +import { makeCall } from '/imports/ui/services/api'; +import Meetings from '/imports/api/meetings'; const getBreakouts = () => Breakouts.find({}, { sort: { sequence: 1 } }).fetch(); +const processOutsideToggleRecording = (e) => { + switch (e.data) { + case 'c_record': { + makeCall('toggleRecording'); + break; + } + case 'c_recording_status': { + const recordingState = Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording; + const recordingMessage = recordingState ? 'recordingStarted' : 'recordingStopped'; + this.window.parent.postMessage({ response: recordingMessage }, '*'); + break; + } + default: { + // console.log(e.data); + } + } +}; + + +const connectRecordingObserver = () => { + // notify on load complete + this.window.parent.postMessage({ response: 'readyToConnect' }, '*'); +}; + export default { + connectRecordingObserver: () => connectRecordingObserver(), + processOutsideToggleRecording: arg => processOutsideToggleRecording(arg), getBreakouts, }; diff --git a/bigbluebutton-html5/imports/ui/components/recording/component.jsx b/bigbluebutton-html5/imports/ui/components/recording/component.jsx index 49ec90132ebab2cc195b0098d0f53fe85210e23a..34f408f5e8b0659a7db90648f4c03f8dd6e1d2eb 100755 --- a/bigbluebutton-html5/imports/ui/components/recording/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/recording/component.jsx @@ -1,5 +1,6 @@ -import React, { Component } from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, intlShape } from 'react-intl'; import Button from '/imports/ui/components/button/component'; import Modal from '/imports/ui/components/modal/simple/component'; import { styles } from './styles'; @@ -31,7 +32,18 @@ const intlMessages = defineMessages({ }, }); -class RecordingComponent extends Component { +const propTypes = { + intl: intlShape.isRequired, + closeModal: PropTypes.func.isRequired, + toggleRecording: PropTypes.func.isRequired, + recordingStatus: PropTypes.bool, +}; + +const defaultProps = { + recordingStatus: false, +}; + +class RecordingComponent extends React.PureComponent { constructor(props) { super(props); const { @@ -44,7 +56,7 @@ class RecordingComponent extends Component { } render() { - const { intl, meeting } = this.props; + const { intl, recordingStatus } = this.props; return ( <Modal @@ -55,12 +67,12 @@ class RecordingComponent extends Component { > <div className={styles.container}> <div className={styles.header}> - <div className={styles.title}>{intl.formatMessage(!meeting.recordProp.recording ? + <div className={styles.title}>{intl.formatMessage(!recordingStatus ? intlMessages.startTitle : intlMessages.stopTitle)} </div> </div> <div className={styles.description}> - {`${intl.formatMessage(!meeting.recordProp.recording ? intlMessages.startDescription : + {`${intl.formatMessage(!recordingStatus ? intlMessages.startDescription : intlMessages.stopDescription)}`} </div> <div className={styles.footer}> @@ -82,4 +94,7 @@ class RecordingComponent extends Component { } } +RecordingComponent.propTypes = propTypes; +RecordingComponent.defaultProps = defaultProps; + export default injectIntl(RecordingComponent); diff --git a/bigbluebutton-html5/imports/ui/components/recording/container.jsx b/bigbluebutton-html5/imports/ui/components/recording/container.jsx index 21f445ca6ff94d137483ca853b6e6c5fef51f0da..72815e90b48c75dd0b6de05b8b33a670ddd54460 100755 --- a/bigbluebutton-html5/imports/ui/components/recording/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/recording/container.jsx @@ -18,5 +18,6 @@ export default withModalMounter(withTracker(({ mountModal }) => ({ mountModal(null); }, - meeting: (Meetings.findOne({ meetingId: Auth.meetingID })), + recordingStatus: (Meetings.findOne({ meetingId: Auth.meetingID }).recordProp.recording), + }))(RecordingContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/toast/container.jsx b/bigbluebutton-html5/imports/ui/components/toast/container.jsx old mode 100644 new mode 100755 index 9be9f2073cf4d9abf5ba975f727a668764f9be82..1e742bb847ce102bbd446481ecaae91fbc523b61 --- a/bigbluebutton-html5/imports/ui/components/toast/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/toast/container.jsx @@ -55,13 +55,14 @@ export default injectIntl(injectNotify(withTracker(({ notify, intl }) => { const meetingId = Auth.meetingID; - Meetings.find({ meetingId }).observeChanges({ - changed: (id, fields) => { - if (fields.recordProp && fields.recordProp.recording) { + Meetings.find({ meetingId }).observe({ + changed: (newDocument, oldDocument) => { + if (newDocument.recordProp && newDocument.recordProp.recording && + newDocument.recordProp.recording!== oldDocument.recordProp.recording) { notify(intl.formatMessage(intlMessages.notificationRecordingStart), 'success', 'record'); } - if (fields.recordProp && !fields.recordProp.recording) { + if (newDocument.recordProp && !newDocument.recordProp.recording) { notify(intl.formatMessage(intlMessages.notificationRecordingStop), 'error', 'record'); } }, diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 057222f99fcfca8468fa2fb72a78fa87d9dcb90d..7ad08ab4d789a5ea7b9204d1920bedd1a82d1e32 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -99,7 +99,7 @@ public: enableListenOnly: false autoShareWebcam: false allowOutsideCommands: - toggleRecording: false + toggleRecording: true toggleSelfVoice: false poll: max_custom: 5