diff --git a/bbb-api-demo/src/main/webapp/demo_iframe.jsp b/bbb-api-demo/src/main/webapp/demo_iframe.jsp new file mode 100644 index 0000000000000000000000000000000000000000..3cd02f6fda6db623efc735311ae0a3e3a8e19f5f --- /dev/null +++ b/bbb-api-demo/src/main/webapp/demo_iframe.jsp @@ -0,0 +1,242 @@ +<!-- + +BigBlueButton - http://www.bigbluebutton.org + +Copyright (c) 2008-2018 by respective authors (see below). All rights reserved. + +BigBlueButton is free software; you can redistribute it and/or modify it under the +terms of the GNU Lesser General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with BigBlueButton; if not, If not, see <http://www.gnu.org/licenses/>. + +Authors: James Jung + Anton Georgiev + +--> +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8"%> +<% + request.setCharacterEncoding("UTF-8"); + response.setCharacterEncoding("UTF-8"); +%> +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Join Meeting via HTML5 Client (API)</title> + <style> + #controls { + width:50%; + height:200px; + float:left; + } + #client { + width:100%; + height:700px; + float:left; + } + #client-content { + width:100%; + height:100%; + } + </style> +</head> + +<body> + +<p>You must have the BigBlueButton HTML5 client installed to use this API demo.</p> + +<%@ include file="bbb_api.jsp"%> + +<% +if (request.getParameterMap().isEmpty()) { + // + // Assume we want to create a meeting + // + %> +<%@ include file="demo_header.jsp"%> + +<h2>Join Meeting via HTML5 Client (API)</h2> + +<FORM NAME="form1" METHOD="GET"> +<table cellpadding="5" cellspacing="5" style="width: 400px; "> + <tbody> + <tr> + <td> </td> + <td style="text-align: right; ">Full Name:</td> + <td style="width: 5px; "> </td> + <td style="text-align: left "><input type="text" autofocus required name="username" /></td> + </tr> + + <tr> + <td> </td> + <td style="text-align: right; ">Meeting Name:</td> + <td style="width: 5px; "> </td> + <td style="text-align: left "><input type="text" required name="meetingname" value="Demo Meeting" /></td> + <tr> + + <tr> + <td> </td> + <td style="text-align: right; ">Moderator Role:</td> + <td style="width: 5px; "> </td> + <td style="text-align: left "><input type=checkbox name=isModerator value="true" checked></td> + <tr> + + <tr> + <td> </td> + <td> </td> + <td> </td> + <td><input type="submit" value="Join" /></td> + <tr> + </tbody> +</table> +<INPUT TYPE=hidden NAME=action VALUE="create"> +</FORM> + + +<% +} else if (request.getParameter("action").equals("create")) { + + String username = request.getParameter("username"); + + // set defaults and overwrite them if custom values exist + String meetingname = "Demo Meeting"; + if (request.getParameter("meetingname") != null) { + meetingname = request.getParameter("meetingname"); + } + + Boolean isModerator = new Boolean(false); + Boolean isHTML5 = new Boolean(true); + Boolean isRecorded = new Boolean(true); + if (request.getParameter("isModerator") != null) { + isModerator = Boolean.parseBoolean(request.getParameter("isModerator")); + } + + String joinURL = getJoinURLExtended(username, meetingname, isRecorded.toString(), null, null, null, isHTML5.toString(), isModerator.toString()); + + if (joinURL.startsWith("http://") || joinURL.startsWith("https://")) { +%> + +<script language="javascript" type="text/javascript"> + +const recButton = document.createElement('button'); +recButton.id = 'recButton'; +const muteButton = document.createElement('button'); +muteButton.id = 'muteButton'; + +function getInitialState() { + document.getElementById('client-content').contentWindow.postMessage('c_recording_status', '*'); + document.getElementById('client-content').contentWindow.postMessage('c_mute_status', '*'); +} + +function handleMessage(e) { + switch (e) { + case 'readyToConnect': { + // get initial state + getInitialState(); break; } + case 'recordingStarted': { + recButton.innerHTML = 'Stop Recording'; + break; + } + case 'recordingStopped': { + recButton.innerHTML = 'Start Recording'; + break; + } + case 'selfMuted': { + muteButton.innerHTML = 'Unmute me'; + break; + } + case 'selfUnmuted': { + muteButton.innerHTML = 'Mute me'; + break; + } + case 'notInAudio': { + muteButton.innerHTML = 'Not in audio'; + document.getElementById('muteButton').disabled = true; + break; + } + case 'joinedAudio': { + muteButton.innerHTML = ''; + document.getElementById('muteButton').disabled = false; getInitialState(); + break; + } + default: console.log('neither', { e }); + } +} + +// EventListener(Getting message from iframe) +window.addEventListener('message', function(e) { + handleMessage(e.data.response); +}); + +// Clean up the body node before loading controls and the client +document.body.innerHTML = ''; + +// Node for the Client +const client = document.createElement('div'); +client.setAttribute('id', 'client'); + +const clientContent = document.createElement('iframe'); +clientContent.setAttribute('id', 'client-content'); +clientContent.setAttribute('src','<%=joinURL%>'); + +// // in case your iframe is on a different domain MYDOMAIN.com +// clientContent.setAttribute('src','https://MYDOMAIN.com/demo/demoHTML5.jsp'); + +// to enable microphone or camera use allow your iframe domain explicitly +// clientContent.setAttribute('allow','microphone https://MYDOMAIN.com; camera https://MYDOMAIN.com'); + +client.appendChild(clientContent); + +// Node for the Controls +const controls = document.createElement('div'); +controls.setAttribute('id', 'controls'); +controls.setAttribute('align', 'middle'); +controls.setAttribute('float', 'left'); + +// ****************** Controls *****************************/ +function recToggle(){ + document.getElementById("client-content").contentWindow.postMessage('c_record', '*'); +} + +function muteToggle(){ + document.getElementById("client-content").contentWindow.postMessage('c_mute', '*'); +} + +// Node for the control which controls recording functionality of the html5Client +recButton.setAttribute('onClick', 'recToggle();'); +controls.appendChild(recButton); + +muteButton.setAttribute('onClick', 'muteToggle();'); +controls.appendChild(muteButton); + +// Append the nodes of contents to the body node +document.body.appendChild(controls); +document.body.appendChild(client); + +</script> +<% + } else { +%> + +Error: getJoinURL() failed +<p/> +<%=joinURL %> + +<% + } +} +%> + +<%@ include file="demo_footer.jsp"%> + +</body> +</html> + diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js index 97c79f89db793c1c1b78a35fd43f485f740916c6..4310a9fe0b0ab6c10a15f3b056b6879ed20f5e4d 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js @@ -38,6 +38,9 @@ export default function addUserSettings(credentials, meetingId, userId, settings // LAYOUT 'autoSwapLayout', 'hidePresentation', + // OUTSIDE COMMANDS + 'outsideToggleSelfVoice', + 'outsideToggleRecording', ]; if (!handledHTML5Parameters.includes(key)) { return acc; 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 176c59251e84856595577eda03fae70906d7c6fc..0da0a77e1c76900d15be4fc0e06e8ebbbaa8ab95 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,9 +9,11 @@ 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 BreakoutRoom from '../create-breakout-room/component'; import { styles } from '../styles'; +import ActionBarService from '../service'; const propTypes = { isUserPresenter: PropTypes.bool.isRequired, @@ -83,12 +85,19 @@ class ActionsDropdown extends Component { componentWillMount() { this.presentationItemId = _.uniqueId('action-item-'); - this.videoItemId = _.uniqueId('action-item-'); this.recordId = _.uniqueId('action-item-'); this.pollId = _.uniqueId('action-item-'); 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; diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index c929503b172b9f3e32762cb6e1d070d9fdfceb70..da38b1bec2b22ef28ab9dd6a42e8d92e344fe22c 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -2,6 +2,8 @@ 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'; @@ -9,7 +11,7 @@ import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/ const ActionsBarContainer = props => <ActionsBar {...props} />; -export default withTracker(({ }) => { +export default withTracker(() => { const togglePollMenu = () => { const showPoll = Session.equals('isPollOpen', false) || !Session.get('isPollOpen'); @@ -27,6 +29,18 @@ 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 44d8a4357bd1219bfca9b58615f442e2cbad5d54..1e673ea0315b226aed131f145dc3f2a5d92d4447 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js @@ -4,7 +4,32 @@ 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, @@ -12,5 +37,6 @@ 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/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx index 0b4cc607394acb006ee40f8b8d511e2c96982ea0..ceb0249952521f078d13c67b46c665f86e4f520c 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; import { defineMessages, intlShape, injectIntl } from 'react-intl'; import Button from '/imports/ui/components/button/component'; +import getFromUserSettings from '/imports/ui/services/users-settings'; import withShortcutHelper from '/imports/ui/components/shortcut-help/service'; import { styles } from './styles'; @@ -26,6 +27,7 @@ const intlMessages = defineMessages({ }); const propTypes = { + processToggleMuteFromOutside: PropTypes.func.isRequired, handleToggleMuteMicrophone: PropTypes.func.isRequired, handleJoinAudio: PropTypes.func.isRequired, handleLeaveAudio: PropTypes.func.isRequired, @@ -42,47 +44,65 @@ const defaultProps = { unmute: false, }; -const AudioControls = ({ - handleToggleMuteMicrophone, - handleJoinAudio, - handleLeaveAudio, - mute, - unmute, - disable, - glow, - join, - intl, - shortcuts, -}) => ( - <span className={styles.container}> - {mute ? - <Button - className={glow ? cx(styles.button, styles.glow) : styles.button} - onClick={handleToggleMuteMicrophone} - disabled={disable} - hideLabel - label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)} - aria-label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)} - color="primary" - icon={unmute ? 'mute' : 'unmute'} - size="lg" - circle - accessKey={shortcuts.toggleMute} - /> : null} - <Button - className={styles.button} - onClick={join ? handleLeaveAudio : handleJoinAudio} - disabled={disable} - hideLabel - aria-label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)} - label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)} - color={join ? 'danger' : 'primary'} - icon={join ? 'audio_off' : 'audio_on'} - size="lg" - circle - accessKey={join ? shortcuts.leaveAudio : shortcuts.joinAudio} - /> - </span>); +class AudioControls extends Component { + componentDidMount() { + const { processToggleMuteFromOutside } = this.props; + if (Meteor.settings.public.allowOutsideCommands.toggleSelfVoice || + getFromUserSettings('outsideToggleSelfVoice', false)) { + window.addEventListener('message', processToggleMuteFromOutside); + } + } + + render() { + const { + handleToggleMuteMicrophone, + handleJoinAudio, + handleLeaveAudio, + mute, + unmute, + disable, + glow, + join, + intl, + shortcuts, + } = this.props; + + return ( + <span className={styles.container}> + {mute ? + <Button + className={glow ? cx(styles.button, styles.glow) : styles.button} + onClick={handleToggleMuteMicrophone} + disabled={disable} + hideLabel + label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : + intl.formatMessage(intlMessages.muteAudio)} + aria-label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : + intl.formatMessage(intlMessages.muteAudio)} + color="primary" + icon={unmute ? 'mute' : 'unmute'} + size="lg" + circle + accessKey={shortcuts.toggleMute} + /> : null} + <Button + className={styles.button} + onClick={join ? handleLeaveAudio : handleJoinAudio} + disabled={disable} + hideLabel + aria-label={join ? intl.formatMessage(intlMessages.leaveAudio) : + intl.formatMessage(intlMessages.joinAudio)} + label={join ? intl.formatMessage(intlMessages.leaveAudio) : + intl.formatMessage(intlMessages.joinAudio)} + color={join ? 'danger' : 'primary'} + icon={join ? 'audio_off' : 'audio_on'} + size="lg" + circle + accessKey={join ? shortcuts.leaveAudio : shortcuts.joinAudio} + /> + </span>); + } +} AudioControls.propTypes = propTypes; AudioControls.defaultProps = defaultProps; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx index 51578831f3ca51845c74adb06735c635abaa86ca..b2334ede83a8d269b2a0f1313bdae8309f8543db 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx @@ -1,14 +1,38 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { withModalMounter } from '/imports/ui/components/modal/service'; +import AudioManager from '/imports/ui/services/audio-manager'; +import { makeCall } from '/imports/ui/services/api'; import AudioControls from './component'; import AudioModalContainer from '../audio-modal/container'; import Service from '../service'; const AudioControlsContainer = props => <AudioControls {...props} />; +const processToggleMuteFromOutside = (e) => { + switch (e.data) { + case 'c_mute': { + makeCall('toggleSelfVoice'); + break; + } + case 'c_mute_status': { + if (!AudioManager.isUsingAudio()) { + this.window.parent.postMessage({ response: 'notInAudio' }, '*'); + return; + } + const muteState = AudioManager.isMuted ? 'selfMuted' : 'selfUnmuted'; + this.window.parent.postMessage({ response: muteState }, '*'); + break; + } + default: { + // console.log(e.data); + } + } +}; + export default withModalMounter(withTracker(({ mountModal }) => ({ + processToggleMuteFromOutside: arg => processToggleMuteFromOutside(arg), mute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest(), unmute: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(), join: Service.isConnected() && !Service.isEchoTest(), diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx index bd868773200d9f6cff8a63d8bff52a56c8ef6c5c..53d9ac4d0e906b781d1afb8e0e955e1206bffe35 100644 --- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx @@ -98,7 +98,7 @@ class Dropdown extends Component { }); } - handleWindowClick() { + handleWindowClick(event) { const triggerElement = findDOMNode(this.trigger); const contentElement = findDOMNode(this.content); const closeDropdown = this.props.isOpen && this.state.isOpen && triggerElement.contains(event.target); diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 5bf04b42ef9e002aacd7bcd42c163ae6e015c300..616496c5ca0ee6e0d1fb7f22a04cc8c204a28029 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -136,7 +136,7 @@ class AudioManager { this.isEchoTest = false; const { name } = browser(); // The kurento bridge isn't a full audio bridge yet, so we have to differ it - const bridge = USE_KURENTO ? this.listenOnlyBridge : this.bridge; + const bridge = this.useKurento ? this.listenOnlyBridge : this.bridge; const callOptions = { isListenOnly: true, @@ -146,7 +146,7 @@ class AudioManager { // Webkit ICE restrictions demand a capture device permission to release // host candidates - if (name == 'safari') { + if (name === 'safari') { await this.askDevicesPermissions(); } @@ -162,12 +162,11 @@ class AudioManager { } logger.error('Listen only error:', err, 'on try', retries); - const error = { + throw { type: 'MEDIA_ERROR', message: this.messages.error.MEDIA_ERROR, - } - throw error; - } + }; + }; return this.onAudioJoining() .then(() => Promise.race([ @@ -181,14 +180,14 @@ class AudioManager { // Exit previous SFU session and clean audio tag state window.kurentoExitAudio(); this.useKurento = false; - let audio = document.querySelector(MEDIA_TAG); + const audio = document.querySelector(MEDIA_TAG); audio.muted = false; } try { await this.joinListenOnly(++retries); - } catch (err) { - return handleListenOnlyError(err); + } catch (error) { + return handleListenOnlyError(error); } } else { handleListenOnlyError(err); @@ -207,7 +206,7 @@ class AudioManager { exitAudio() { if (!this.isConnected) return Promise.resolve(); - const bridge = (this.useKurento && this.isListenOnly) ? this.listenOnlyBridge : this.bridge; + const bridge = (this.useKurento && this.isListenOnly) ? this.listenOnlyBridge : this.bridge; this.isHangingUp = true; this.isEchoTest = false; @@ -235,6 +234,12 @@ class AudioManager { changed: (id, fields) => { if (fields.muted !== undefined && fields.muted !== this.isMuted) { this.isMuted = fields.muted; + const muteState = this.isMuted ? 'selfMuted' : 'selfUnmuted'; + window.parent.postMessage({ response: muteState }, '*'); + } + + if (fields.joined) { + window.parent.postMessage({ response: 'joinedAudio' }, '*'); } if (fields.talking !== undefined && fields.talking !== this.isTalking) { @@ -313,17 +318,14 @@ class AudioManager { new window.AudioContext() : new window.webkitAudioContext(); - // Create a placeholder buffer to upstart audio context - const pBuffer = this.listenOnlyAudioContext.createBuffer(2, this.listenOnlyAudioContext.sampleRate * 3, this.listenOnlyAudioContext.sampleRate); - - var dest = this.listenOnlyAudioContext.createMediaStreamDestination(); + const dest = this.listenOnlyAudioContext.createMediaStreamDestination(); - let audio = document.querySelector(MEDIA_TAG); + const audio = document.querySelector(MEDIA_TAG); // Play bogus silent audio to try to circumvent autoplay policy on Safari - audio.src = 'resources/sounds/silence.mp3' + audio.src = 'resources/sounds/silence.mp3'; - audio.play().catch(e => { + audio.play().catch((e) => { logger.warn('Error on playing test audio:', e); }); diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 006e607b622227623234808f5f7d9e17dda6fbea..195bbb60d45c8f21352c057c65d5ed8b89a43866 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -99,6 +99,9 @@ public: enableVideoStats: false enableListenOnly: false autoShareWebcam: false + allowOutsideCommands: + toggleRecording: false + toggleSelfVoice: false poll: max_custom: 5 chat: