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>&nbsp;</td>
+                        <td style="text-align: right; ">Full Name:</td>
+                        <td style="width: 5px; ">&nbsp;</td>
+                        <td style="text-align: left "><input type="text" autofocus required name="username" /></td>
+                </tr>
+
+                <tr>
+                        <td>&nbsp;</td>
+                        <td style="text-align: right; ">Meeting Name:</td>
+                        <td style="width: 5px; ">&nbsp;</td>
+                        <td style="text-align: left "><input type="text" required name="meetingname" value="Demo Meeting" /></td>
+                <tr>
+
+                <tr>
+                        <td>&nbsp;</td>
+                        <td style="text-align: right; ">Moderator Role:</td>
+                        <td style="width: 5px; ">&nbsp;</td>
+                        <td style="text-align: left "><input type=checkbox name=isModerator value="true" checked></td>
+                <tr>
+
+                <tr>
+                        <td>&nbsp;</td>
+                        <td>&nbsp;</td>
+                        <td>&nbsp;</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: