diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar
index 6e47da1b9c487fbe8a0b47643c75111c9ff8cd69..4b256148829d6ecd5bbc21ec46b847955212b6ea 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-linux-x86_64-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar
index d7afa49c12551564a5316882bea744b2d03de502..c1352f988803561f26f0cf143f2caeed66587c0b 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-macosx-x86_64-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar
index aba354529523e4e9be32b8514782d3c525aab061..c7f70e4361d203c85f5f34c2ae97b72e42557a0f 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar
index 08f3de1035fcdc49fa2f20e7d6ff3ee6303577ef..fc66280b36b0b5ef8eca3fa47f34fda0462bb48f 100644
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar and b/bbb-screenshare/app/jws/lib/ffmpeg-win-x86_64-svc2.jar differ
diff --git a/bbb-screenshare/app/jws/lib/ffmpeg.jar b/bbb-screenshare/app/jws/lib/ffmpeg.jar
index 74a76e65086bc8b784ba8204ae3778a357a8a8d0..d7cdcaebc3485cae63174913a1d55a8b348e72bd 100755
Binary files a/bbb-screenshare/app/jws/lib/ffmpeg.jar and b/bbb-screenshare/app/jws/lib/ffmpeg.jar differ
diff --git a/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar b/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar
index ddfa8a89108c115b3b30fad54a4c98dd85851288..90fd2843fd81898e0bf1c2b82afa268d1847ca72 100755
Binary files a/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar and b/bbb-screenshare/app/jws/lib/javacv-screenshare-0.0.1.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar
index 5dbdc8e794c60a6dbcd3bd4dc7dde3d050e3e402..152c2809cbd352e39ddad781b70eeeeef9f63026 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-linux-x86_64-svc2-unsigned.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar
index 9423d37bda46152ded9faee813a139fb210f0444..5e3f3f20f16355efa4202c272a91674963db7441 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-macosx-x86_64-svc2-unsigned.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar
index 6a3463a45ba98e5a99600071773f5a7a3b74dcc9..72ac594607242c505ffe1e28365fb1b8c6cc051f 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86-svc2-unsigned.jar differ
diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar
index 2c90fec4215c695370e23e93b7488e8b5090dca9..27399a878e24ab148670ae3ec2c11ff02476bee9 100644
Binary files a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-win-x86_64-svc2-unsigned.jar differ
diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
index 4c89be6da6d21a1ec2a0dab11ad0df8436ef0e06..86fef8d16f6d2688bb6a8d3118226bc42e83cfeb 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml
@@ -398,7 +398,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				}
 
 				if (!StringUtils.isEmpty(brandingOptions.toolbarColor)) {
-					setStyle("backgroundColor", brandingOptions.toolbarColor);
+					mainBox.setStyle("backgroundColor", uint("0x" + brandingOptions.toolbarColor.substr(1)));
 				}
 			}
 			
diff --git a/bigbluebutton-config/cron.daily/bigbluebutton b/bigbluebutton-config/cron.daily/bigbluebutton
index e3be361bb6fd9241705ed00cebd440b8f7ef59bf..9349a42b5041b8b79a3225864b59802f1fa89c6d 100755
--- a/bigbluebutton-config/cron.daily/bigbluebutton
+++ b/bigbluebutton-config/cron.daily/bigbluebutton
@@ -115,10 +115,3 @@ remove_raw_of_published_recordings(){
 #
 find /tmp -name "*.afm" -mtime +$history -delete
 find /tmp -name "*.pfb" -mtime +$history -delete
-
-#
-# If there are no users currently logged in, restart libreoffice to clear its memory usage
-#
-if [[ $(netstat -ant | egrep ":1935\ " | egrep -v ":::|0.0.0.0"  | wc | awk '{print $1}') == 0 ]]; then
-  systemctl restart libreoffice.service
-fi
diff --git a/bigbluebutton-config/web/index_html5_vs_flash.html b/bigbluebutton-config/web/index_html5_vs_flash.html
new file mode 100644
index 0000000000000000000000000000000000000000..0a7e2fb139de4d9a6888ecdfa806e6e7b0dd0328
--- /dev/null
+++ b/bigbluebutton-config/web/index_html5_vs_flash.html
@@ -0,0 +1,298 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+    <title>BigBlueButton - Open Source Web Conferencing</title>
+    <meta name="description" content="BigBlueButton enables universities and colleges to deliver a high-quality learning experience to remote students.">
+    <meta name="keywords" content="BigBlueButton, Open Source Web Conferencing, Distance Education, Courses Online, Web Conferencing, Open Source, Desktop Sharing, Video Conferencing, Video Collaboration, Presentation Sharing, Audio Sharing, Voice Collaboration, Public Chat, Webcam Sharing, Annotation, Whiteboard, Integrated Voice Over IP, Collaboration Software, Online Collaboration, Collaborative Learning, Virtual Classroom">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<link rel="icon" href="images/favicon.png">
+
+    <link rel="stylesheet" href="css/bijou.min.css">
+    <link rel="stylesheet" href="css/style.css">
+    <link rel="stylesheet" href="css/font-awesome.min.css">
+    <link rel="stylesheet" href="css/bbb-bootstrap.css">
+
+    <script src="js/jquery.min.js"></script>
+    <script src="js/bootstrap.min.js"></script>
+    <script src="js/bigbluebutton.js"></script>
+	</head>
+  <body>
+    <div class='main'>
+
+    <!-- Github Fork Ribbon -->
+    <a href="https://github.com/bigbluebutton/bigbluebutton">
+        <img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub">
+    </a>
+
+    <!-- Messages -->
+    <div id='messages' class='hidden'>
+    </div>
+
+    <!-- Header -->
+    <div class='navbar'>
+      <div class='container'>
+        <div class="logo">
+          <img src="images/bbb-logo.png" alt="BigBlueButton Demo"/>
+        </div>
+      </div>
+    </div>
+
+    <!-- Body -->
+    <div class='container'>
+
+
+
+
+
+      <!-- Welcome Message & Login Into Demo -->
+      <div class='row'>
+        <div class='span six html5clientOnly'>
+          <div class='join-meeting '>
+            <h4>Try BigBlueButton via HTML5</h4>
+            <p>Join a demo session on this server.</p>
+
+            <form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demoHTML5.jsp">
+              <input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus>
+              <input type="submit" value="Join" class="submit_btn button success large"><br>
+              <input type="hidden" name="action" value="create">
+            </form>
+
+          <a class="watch" href="#video" class="pull-right">New to BigBlueButton?  Watch these videos.</a>
+
+          </div>
+        </div>
+        <div class='span six'>
+          <div class='join-meeting'>
+            <h4>Try BigBlueButton via Flash</h4>
+            <p>Join a demo session on this server.</p>
+
+            <form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demo1.jsp">
+              <input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus>
+              <input type="submit" value="Join" class="submit_btn button success large"><br>
+              <input type="hidden" name="action" value="create">
+            </form>
+
+            <a class="watch" href="#video" class="pull-right">New to BigBlueButton?  Watch these videos.</a>
+          </div>
+        </div>
+      </div>
+
+      <hr class="featurette-divider">
+      <!-- Welcome Message & Login Into Demo -->
+      <div class='row'>
+       <div >
+          <h2>BigBlueButton HTML5 client test server</h2>
+          <p> <a href="http://bigbluebutton.org/" target="_blank">BigBlueButton</a> is an open source web conferencing system for on-line learning. This is a public test server for the BigBlueButton <a href="http://docs.bigbluebutton.org/html/html5-overview.html">HTML5 client</a> currently under development.</p>
+	<p> Our goal for the upcoming release of the HTML5 client is to implement all the <a href="https://youtu.be/oh0bEk3YSwI">viewer capabilities</a> of the Flash client.  Students join online classes as a viewer.  The HTML5 client will give remote students the ability to join from their Android mobile devices.  Users using the Flash and HTML5 clients can join the same meeting (hence the two choices above).  We built the HTML5 client using web real-time communication (WebRTC), <a href="https://facebook.github.io/react/">React</a>, and <a href="https://www.mongodb.com/">MongoDB</a>.</p>
+	<p>  The HTML5 works well with desktop and Android devices (phone and tablets) as they all support WebRTC.  Apple does not (yet) support WebRTC in Safari for iOS devices, but don't worry -- we are working in parallel on app for iOS devices.   What can this developer build of the HTML5 client do right now?  Pretty much everything the Flash client can do for viewers except (a) view a desktop sharing stream from the presenter and (b) send/receive webcam streams.  We're working on (a) and (b).  For now, we are really happy to share with you our progress and get <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a> on what has been implemeted so far.  Enjoy!</p>
+
+          <h4>For Developers</h4>
+          <p> The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. </p>
+          <p>See <a href="/demo/demo1.jsp" target="_blank">API examples </a> for how to integrate BigBlueButton with your project.</p>
+        </div>
+        <div class="span one"></div>
+
+      </div>
+
+
+
+      <hr class="featurette-divider">
+
+      <!-- BigBlueButton Features -->
+      <div class="bbb-features">
+	      <div class='row'>
+	        <div class='featurette-heading'>
+	          <h2>Features</h2>
+	        </div>
+
+	        <div class='span four first'>
+	          <i class="fa fa-play-circle-o"></i>
+
+	          <div class="bbb-features-content">
+	            <h3>Record and Playback</h3>
+	            <p>BigBlueButton can record your sessions for later playback by students.</p>
+	          </div>
+	        </div>
+
+	        <div class='span four'>
+	          <i class="fa fa-pencil-square-o"></i>
+
+	          <div class="bbb-features-content">
+	            <h3>Whiteboard</h3>
+	            <p>The whiteboard controls let you annotate key parts of your presentation.</p>
+	          </div>
+	        </div>
+
+	        <div class='span four last'>
+	          <i class="fa fa-desktop"></i>
+
+	          <div class="bbb-features-content">
+	            <h3><a href="https://youtu.be/xTFuEvmEqB0">Desktop Sharing</a></h3>
+	            <p>You can broadcast your desktop for all users to see (requires lastest version of Java for presenter only).</p>
+	          </div>
+	        </div>
+	      </div>
+
+
+	      <div class='row'>
+	        <div class='span four first'>
+	          <i class="fa fa-microphone"></i>
+
+	          <div class="bbb-features-content">
+	            <h3><a href="https://youtu.be/4Y__UsUrRx0">WebRTC Audio</a></h3>
+	            <p>Users of Chrome and FireFox browsers will benefit from high-quality, low-latency WebRTC audio. (Users of other browsers will seamlessly use Flash-based audio.)</p>
+	          </div>
+	        </div>
+
+	        <div class='span four'>
+	          <i class="fa fa-bar-chart-o"></i>
+
+	          <div class="bbb-features-content">
+	            <h3><a href="https://youtu.be/J9mbw00P9W0">Presentation</a></h3>
+	            <p>You can upload any PDF presentation or MS office document. BigBlueButton keeps everyone in sync with your current slide, zoom, pan, annotations, and mouse pointer.</p>
+	          </div>
+	        </div>
+
+	        <div class='span four last'>
+	          <i class="fa fa-video-camera"></i>
+
+	          <div class="bbb-features-content">
+	            <h3>Web Cam</h3>
+	            <p>Multiple users can share their webcam at the same time. There is no built-in limit on the number of simultaneously active webcams.</p>
+	          </div>
+	        </div>
+	      </div>
+
+              <div class='row'>
+                <div class='span four first'>
+                  <i class="fa fa-smile-o"></i>
+
+                  <div class="bbb-features-content">
+                    <h3>Emoji</h3>
+                    <p>Students can raise hand and use emoji icons for feedback.</p>
+                  </div>
+                </div>
+
+                <div class='span four'>
+                  <i class="fa fa-check-square-o"></i>
+
+                  <div class="bbb-features-content">
+                    <h3>Polling</h3>
+                    <p>You can poll students anytime to increase engagement.</p>
+                  </div>
+                </div>
+
+                <div class='span four last'>
+                  <i class="fa fa-comments-o"></i>
+
+                  <div class="bbb-features-content">
+                    <h3>Chat</h3>
+                    <p>You can interact with students through public and private chat.</p>
+                  </div>
+                </div>
+              </div>
+
+              <div class='row'>
+                <div class='span four first'>
+                  <i class="fa fa-cc"></i>
+
+                  <div class="bbb-features-content">
+                    <h3><a href="https://youtu.be/vDpurrMgal0">Live Captioning</a></h3>
+                    <p>You can enter live captions for students.  These captions will later appear as subtitles in recordings.</p>
+                  </div>
+                </div>
+
+                <div class='span four'>
+                  <i class="fa fa-users"></i>
+
+                  <div class="bbb-features-content">
+                    <h3><a href="https://youtu.be/q5N-lcocJss">Breakout Rooms</a></h3>
+                    <p>You can group and place students into breakout rooms (full BigBlueButton sessions) for give number of minutes for increased collaboration.</p>
+                  </div>
+                </div>
+
+                <div class='span four last'>
+                  <i class="fa fa-blind"></i>
+
+                  <div class="bbb-features-content">
+                    <h3>Screen Reader</h3>
+                    <p>Students with visual disabilities can use JAWS screen reader to interact with BigBlueButton.</p>
+                  </div>
+                </div>
+              </div>
+
+      </div>
+      <hr class="featurette-divider">
+
+      <!-- BigBlueButton Videos -->
+      <div id="video" class="bbb-videos">
+	      <div class='row'>
+	        <div class='featurette-heading'>
+	          <h2>Getting Started Quickly</h2>
+	        </div>
+
+	        <div class='span four first video-item'>
+	        	<a href="https://www.youtube.com/watch?v=4Y__UsUrRx0&feature=youtu.be" target="_blank">
+		        	<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
+		          <img src="images/bbb-setup-audio.jpg" alt="Setting Up Audio"/>
+	        	</a>
+	          <h3><a href="https://www.youtube.com/watch?v=4Y__UsUrRx0&feature=youtu.be" title="Setup Audio" target="_blank">Setting Up Audio</a></h3>
+	        </div>
+
+	        <div class='span four video-item'>
+	        	<a href="https://www.youtube.com/watch?v=oh0bEk3YSwI" target="_blank">
+		        	<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
+		          <img src="images/bbb-viewer-overview.jpg" alt="BigBlueButton Viewer Overview Video"/>
+	        	</a>
+	          <h3><a href="https://www.youtube.com/watch?v=oh0bEk3YSwI;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3>
+	        </div>
+
+	        <div class='span four last video-item'>
+	        	<a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" target="_blank">
+		        	<div class="video-btn"><i class="fa fa-play-circle-o"></i></div>
+		          <img  src="images/bbb-presenter-overview.jpg" alt="Moderator/Presenter Overview Video"/>
+	        	</a>
+	          <h3><a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3>
+	        </div>
+	      </div>
+      </div>
+
+      </div>
+    </div>
+
+    <!--  Footer -->
+    <footer>
+      <div class='container'>
+
+	      <div class="row">
+	      	<div class="span six first">
+		        <p>BigBlueButton and the BigBlueButton logo are trademarks of <a href="http://bigbluebutton.org/">BigBlueButton Inc.</a></p>
+          </div>
+	      	<div class="span six last">
+						<ul>
+							<li>
+								Follow Us:
+							</li>
+							<li><a class="twitter" href="http://www.twitter.com/bigbluebutton" title="BigBlueButton Twitter Page" target="_blank"><i class="fa fa-twitter"></i></a></li>
+							<li><a class="facebook" href="http://www.facebook.com/bigbluebutton" title="BigBlueButton Facebook Page" target="_blank"><i class="fa fa-facebook"></i></a></li>
+							<li><a class="youtube" href="http://www.youtube.com/bigbluebuttonshare" title="BigBlueButton YouTube Page" target="_blank"><i class="fa fa-youtube"></i> </a></li>
+							<li><a class="google" href="http://google.com/+bigbluebutton" title="BigBlueButton Google Plus" target="_blank"><i class="fa fa-google-plus"></i></a></li>
+						</ul>
+	      	</div>
+	      </div> 
+
+
+	      <div class="row">
+	      	<div class="span twelve center">
+		        <p>Copyright &copy; 2017 BigBlueButton Inc.<br>
+		        <small>Version <a href="http://docs.bigbluebutton.org/">2.0-beta</a></small>
+		        </p>
+	      	</div>
+	      </div>
+      </div>
+    </footer>
+  </body>
+</html>
+
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
index 58037d1e5340be2ccf2787d649456a226113b702..69e256274f1c44a6468168fb41d85cba8b4c352f 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
@@ -1,14 +1,35 @@
 export default class BaseAudioBridge {
-  constructor() {
+  constructor(userData) {
+    this.userData = userData;
+
+    this.baseErrorCodes = {
+      INVALID_TARGET: 'INVALID_TARGET',
+      CONNECTION_ERROR: 'CONNECTION_ERROR',
+      REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
+      GENERIC_ERROR: 'GENERIC_ERROR',
+      MEDIA_ERROR: 'MEDIA_ERROR',
+    };
+
+    this.baseCallStates = {
+      started: 'started',
+      ended: 'ended',
+      failed: 'failed',
+    };
   }
 
   exitAudio() {
+    console.error('The Bridge must implement exitAudio');
   }
 
-  joinListenOnly() {
+  joinAudio() {
+    console.error('The Bridge must implement joinAudio');
   }
 
-  joinMicrophone() {
+  changeInputDevice() {
+    console.error('The Bridge must implement changeInputDevice');
   }
 
+  changeOutputDevice() {
+    console.error('The Bridge must implement changeOutputDevice');
+  }
 }
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 81b9825f44a6aed5eb0630cf706a18859af35e41..271b8e5a3634373776114af6a4b076535e3f9f75 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -1,85 +1,373 @@
-import { makeCall } from '/imports/ui/services/api';
-
+import VoiceUsers from '/imports/api/voice-users';
+import { Tracker } from 'meteor/tracker';
 import BaseAudioBridge from './base';
 
-const APP_CONFIG = Meteor.settings.public.app;
-const MEDIA_CONFIG = Meteor.settings.public.media;
+const MEDIA = Meteor.settings.public.media;
+const STUN_TURN_FETCH_URL = MEDIA.stunTurnServersFetchAddress;
+const MEDIA_TAG = MEDIA.mediaTag;
+const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
+const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
+const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
+
+const fetchStunTurnServers = (sessionToken) => {
+  const handleStunTurnResponse = ({ stunServers, turnServers }) => {
+    if (!stunServers && !turnServers) {
+      return { error: 404, stun: [], turn: [] };
+    }
+    return {
+      stun: stunServers.map(server => server.url),
+      turn: turnServers.map(server => server.url),
+    };
+  };
+
+  const url = `${STUN_TURN_FETCH_URL}?sessionToken=${sessionToken}`;
+  return fetch(url)
+    .then(res => res.json())
+    .then(handleStunTurnResponse)
+    .then((response) => {
+      if (response.error) {
+        return Promise.reject('Could not fetch the stuns/turns servers!');
+      }
+      return response;
+    });
+};
 
-let triedHangup = false;
 
 export default class SIPBridge extends BaseAudioBridge {
   constructor(userData) {
-    super();
-    this.userData = userData;
+    super(userData);
+
+    const {
+      userId,
+      username,
+      sessionToken,
+    } = userData;
+
+    this.user = {
+      userId,
+      sessionToken,
+      name: username,
+    };
+
+    this.media = {
+      inputDevice: {},
+    };
+
+    this.protocol = window.document.location.protocol;
+    this.hostname = window.document.location.hostname;
+
+    const causes = window.SIP.C.causes;
+
+    this.errorCodes = {
+      [causes.REQUEST_TIMEOUT]: this.baseErrorCodes.REQUEST_TIMEOUT,
+      [causes.INVALID_TARGET]: this.baseErrorCodes.INVALID_TARGET,
+      [causes.CONNECTION_ERROR]: this.baseErrorCodes.CONNECTION_ERROR,
+    };
+  }
+
+  joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
+    return new Promise((resolve, reject) => {
+      const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
+
+      const callback = (message) => {
+        managerCallback(message).then(resolve);
+      };
+
+      this.callback = callback;
+
+      return this.doCall({ callExtension, isListenOnly, inputStream })
+                 .catch((reason) => {
+                   callback({
+                     status: this.baseCallStates.failed,
+                     error: this.baseErrorCodes.GENERIC_ERROR,
+                     bridgeError: reason,
+                   });
+                   reject(reason);
+                 });
+    });
   }
 
-  joinListenOnly(stunServers, turnServers, callbackFromManager) {
-    makeCall('listenOnlyToggle', true);
-    this._joinVoiceCallSIP({ isListenOnly: true }, stunServers, turnServers, callbackFromManager);
+  doCall(options) {
+    const {
+      isListenOnly,
+    } = options;
+
+    const {
+      userId,
+      name,
+      sessionToken,
+    } = this.user;
+
+    const callerIdName = [
+      userId,
+      'bbbID',
+      isListenOnly ? `LISTENONLY-${name}` : name,
+    ].join('-');
+
+    this.user.callerIdName = callerIdName;
+    this.callOptions = options;
+
+    return fetchStunTurnServers(sessionToken)
+                        .then(this.createUserAgent.bind(this))
+                        .then(this.inviteUserAgent.bind(this))
+                        .then(this.setupEventHandlers.bind(this));
   }
 
-  joinMicrophone(stunServers, turnServers, callbackFromManager) {
-    this._joinVoiceCallSIP({ isListenOnly: false }, stunServers, turnServers, callbackFromManager);
+  transferCall(onTransferSuccess) {
+    return new Promise((resolve, reject) => {
+      let trackerControl = null;
+
+      const timeout = setTimeout(() => {
+        clearTimeout(timeout);
+        trackerControl.stop();
+        this.callback({
+          status: this.baseCallStates.failed,
+          error: this.baseErrorCodes.REQUEST_TIMEOUT,
+          bridgeError: 'Timeout on call transfer' });
+        reject(this.baseErrorCodes.REQUEST_TIMEOUT);
+      }, CALL_TRANSFER_TIMEOUT);
+
+      // This is is the call transfer code ask @chadpilkey
+      this.currentSession.dtmf(1);
+
+      Tracker.autorun((c) => {
+        trackerControl = c;
+        const selector = { meetingId: this.userData.meetingId, intId: this.userData.userId };
+        const query = VoiceUsers.find(selector);
+
+        query.observeChanges({
+          changed: (id, fields) => {
+            if (fields.joined) {
+              clearTimeout(timeout);
+              onTransferSuccess();
+              c.stop();
+              resolve();
+            }
+          },
+        });
+      });
+    });
   }
 
-  // Periodically check the status of the WebRTC call, when a call has been established attempt to
-  // hangup, retry if a call is in progress, send the leave voice conference message to BBB
-  exitAudio(isListenOnly, afterExitCall = () => { }) {
-    // To be called when the hangup is confirmed
-    const hangupCallback = function () {
-      console.log(`Exited Voice Conference, listenOnly=${isListenOnly}`);
+  exitAudio() {
+    return new Promise((resolve, reject) => {
+      let hangupRetries = 0;
+      let hangup = false;
+      const tryHangup = () => {
+        this.currentSession.bye();
+        hangupRetries += 1;
 
-      // notify BBB-apps we are leaving the call if we are in listen only mode
-      if (isListenOnly) {
-        makeCall('listenOnlyToggle', false);
-      }
+        setTimeout(() => {
+          if (hangupRetries > CALL_HANGUP_MAX_RETRIES) {
+            this.callback({
+              status: this.baseCallStates.failed,
+              error: this.baseErrorCodes.REQUEST_TIMEOUT,
+              bridgeError: 'Timeout on call hangup',
+            });
+            return reject(this.baseErrorCodes.REQUEST_TIMEOUT);
+          }
+
+          if (!hangup) return tryHangup();
+          return resolve();
+        }, CALL_HANGUP_TIMEOUT);
+      };
+
+      this.currentSession.on('bye', () => {
+        hangup = true;
+        resolve();
+      });
+
+      return tryHangup();
+    });
+  }
+
+  createUserAgent({ stun, turn }) {
+    return new Promise((resolve, reject) => {
+      const {
+        hostname,
+        protocol,
+      } = this;
+
+      const {
+        callerIdName,
+      } = this.user;
+
+      let userAgent = new window.SIP.UA({
+        uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
+        wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
+        // log: {
+        //   builtinEnabled: false,
+        // },
+        displayName: callerIdName,
+        register: false,
+        traceSip: true,
+        autostart: false,
+        userAgentString: 'BigBlueButton',
+        stunServers: stun,
+        turnServers: turn,
+      });
+
+      userAgent.removeAllListeners('connected');
+      userAgent.removeAllListeners('disconnected');
+
+      const handleUserAgentConnection = () => {
+        resolve(userAgent);
+      };
+
+      const handleUserAgentDisconnection = () => {
+        userAgent.stop();
+        userAgent = null;
+        this.callback({
+          status: this.baseCallStates.failed,
+          error: this.baseErrorCodes.CONNECTION_ERROR,
+          bridgeError: 'User Agent Disconnected' });
+        reject(this.baseErrorCodes.CONNECTION_ERROR);
+      };
+
+      userAgent.on('connected', handleUserAgentConnection);
+      userAgent.on('disconnected', handleUserAgentDisconnection);
+
+      userAgent.start();
+    });
+  }
+
+  inviteUserAgent(userAgent) {
+    const {
+      hostname,
+    } = this;
+
+    const {
+      inputStream,
+      callExtension,
+    } = this.callOptions;
+
+    const options = {
+      media: {
+        stream: inputStream,
+        constraints: {
+          audio: true,
+          video: false,
+        },
+        render: {
+          remote: document.querySelector(MEDIA_TAG),
+        },
+      },
+      RTCConstraints: {
+        mandatory: {
+          OfferToReceiveAudio: true,
+          OfferToReceiveVideo: false,
+        },
+      },
     };
 
-    // Checks periodically until a call is established so we can successfully
-    // end the call clean state
-    triedHangup = false;
+    return userAgent.invite(`sip:${callExtension}@${hostname}`, options);
+  }
 
-    // function to initiate call
-    const checkToHangupCall = ((context, afterExitCall = () => { }) => {
-      // if an attempt to hang up the call is made when the current session is not yet finished,
-      // the request has no effect keep track in the session if we haven't tried a hangup
-      if (window.getCallStatus() != null && !triedHangup) {
-        console.log('Attempting to hangup on WebRTC call');
-        window.webrtc_hangup(hangupCallback);
+  setupEventHandlers(currentSession) {
+    return new Promise((resolve) => {
+      this.connectionCompleted = false;
 
-        // we have hung up, prevent retries
-        triedHangup = true;
+      const handleConnectionCompleted = () => {
+        if (this.connectionCompleted) return;
+        this.callback({ status: this.baseCallStates.started });
+        this.connectionCompleted = true;
+        resolve();
+      };
 
-        if (afterExitCall) {
-          afterExitCall(this, APP_CONFIG.listenOnly);
+      const handleSessionTerminated = (message, cause) => {
+        this.connectionCompleted = false;
+        if (!message && !cause) {
+          return this.callback({
+            status: this.baseCallStates.ended,
+          });
         }
-      } else {
-        console.log('RETRYING hangup on WebRTC call in ' +
-          `${MEDIA_CONFIG.WebRTCHangupRetryInterval} ms`);
 
-        // try again periodically
-        setTimeout(checkToHangupCall, MEDIA_CONFIG.WebRTCHangupRetryInterval);
-      }
-    })(this, afterExitCall);
+        const mappedCause = cause in this.errorCodes ?
+                            this.errorCodes[cause] :
+                            this.baseErrorCodes.GENERIC_ERROR;
+
+        return this.callback({
+          status: this.baseCallStates.failed,
+          error: mappedCause,
+          bridgeError: cause,
+        });
+      };
+
+      currentSession.on('terminated', handleSessionTerminated);
+      currentSession.mediaHandler.on('iceConnectionCompleted', handleConnectionCompleted);
+      currentSession.mediaHandler.on('iceConnectionConnected', handleConnectionCompleted);
+
+      this.currentSession = currentSession;
+    });
+  }
+
+  getMediaStream(constraints) {
+    return navigator.mediaDevices.getUserMedia(constraints).catch((err) => {
+      console.error(err);
+      throw new Error(this.baseErrorCodes.MEDIA_ERROR);
+    });
+  }
 
-    return false;
+  async setDefaultInputDevice() {
+    const mediaStream = await this.getMediaStream({ audio: true });
+    const deviceLabel = mediaStream.getAudioTracks()[0].label;
+    const mediaDevices = await navigator.mediaDevices.enumerateDevices();
+    const device = mediaDevices.find(d => d.label === deviceLabel);
+    return this.changeInputDevice(device.deviceId);
   }
 
-  // join the conference. If listen only send the request to the server
-  _joinVoiceCallSIP(options, stunServers, turnServers, callbackFromManager) {
-    const extension = this.userData.voiceBridge;
-    console.log(options);
+  async changeInputDevice(value) {
+    const {
+      media,
+    } = this;
 
-    // create voice call params
-    const joinCallback = function (message) {
-      console.log('Beginning WebRTC Conference Call');
-    };
+    if (media.inputDevice.audioContext) {
+      media.inputDevice.audioContext.close().then(() => {
+        media.inputDevice.audioContext = null;
+        media.inputDevice.scriptProcessor = null;
+        media.inputDevice.source = null;
+        return this.changeInputDevice(value);
+      });
+    }
 
-    const stunsAndTurns = {
-      stun: stunServers,
-      turn: turnServers,
+    media.inputDevice.id = value;
+    if ('AudioContext' in window) {
+      media.inputDevice.audioContext = new window.AudioContext();
+    } else {
+      media.inputDevice.audioContext = new window.webkitAudioContext();
+    }
+    media.inputDevice.scriptProcessor = media.inputDevice.audioContext
+                                              .createScriptProcessor(2048, 1, 1);
+    media.inputDevice.source = null;
+
+    const constraints = {
+      audio: {
+        deviceId: value,
+      },
     };
 
-    callIntoConference(extension, callbackFromManager, options.isListenOnly, stunsAndTurns);
+    const mediaStream = await this.getMediaStream(constraints);
+    media.inputDevice.stream = mediaStream;
+    media.inputDevice.source = media.inputDevice.audioContext.createMediaStreamSource(mediaStream);
+    media.inputDevice.source.connect(media.inputDevice.scriptProcessor);
+    media.inputDevice.scriptProcessor.connect(media.inputDevice.audioContext.destination);
+
+    return this.media.inputDevice;
+  }
+
+  async changeOutputDevice(value) {
+    const audioContext = document.querySelector(MEDIA_TAG);
+
+    if (audioContext.setSinkId) {
+      try {
+        await audioContext.setSinkId(value);
+        this.media.outputDeviceId = value;
+      } catch (err) {
+        console.error(err);
+        throw new Error(this.baseErrorCodes.MEDIA_ERROR);
+      }
+    }
+
+    return this.media.outputDeviceId;
   }
 }
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js b/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js
index df5a59fd3742222f2b95e5396c48b1382d640c39..0293d77a358254629b696462277a9a857dc17c9b 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/verto.js
@@ -17,17 +17,10 @@ export default class VertoBridge extends BaseAudioBridge {
     window.vertoExitAudio();
   }
 
-  joinListenOnly() {
-    window.vertoJoinListenOnly(
-      'remote-media',
-      this.voiceBridge,
-      this.vertoUsername,
-      null,
-    );
-  }
+  joinAudio({ isListenOnly }) {
+    const vertoJoin = isListenOnly ? 'vertoJoinListenOnly' : 'vertoJoinMicrophone';
 
-  joinMicrophone() {
-    window.vertoJoinMicrophone(
+    window[vertoJoin](
       'remote-media',
       this.voiceBridge,
       this.vertoUsername,
diff --git a/bigbluebutton-html5/imports/api/audio/client/manager/index.js b/bigbluebutton-html5/imports/api/audio/client/manager/index.js
deleted file mode 100644
index 621f720136696728c28e9ec1fa5308dc1bf3d67d..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/api/audio/client/manager/index.js
+++ /dev/null
@@ -1,237 +0,0 @@
-import Auth from '/imports/ui/services/auth';
-import BaseAudioBridge from '../bridge/base';
-import VertoBridge from '../bridge/verto';
-import SIPBridge from '../bridge/sip';
-
-class CallStates {
-  static get init() {
-    return 'initialized state';
-  }
-  static get echo() {
-    return 'do echo test state';
-  }
-  static get callIntoEcho() {
-    return 'calling into echo test state';
-  }
-  static get inEchoTest() {
-    return 'in echo test state';
-  }
-  static get joinVoiceConference() {
-    return 'join voice conference state';
-  }
-  static get callIntoConference() {
-    return 'calling into conference state';
-  }
-  static get inConference() {
-    return 'in conference state';
-  }
-  static get transferToConference() {
-    return 'joining from echo into conference state';
-  }
-  static get echoTestFailed() {
-    return 'echo test failed state';
-  }
-  static get callToListenOnly() {
-    return 'call to listen only state';
-  }
-  static get connectToListenOnly() {
-    return 'connecting to listen only state';
-  }
-  static get inListenOnly() {
-    return 'in listen only state';
-  }
-  static get reconnecting() {
-    return 'reconecting';
-  }
-}
-
-const ErrorCodes = {
-  CODE_1001: '1001',
-  CODE_1002: '1002',
-  CODE_1003: '1003',
-  CODE_1004: '1004',
-  CODE_1005: '1005',
-  CODE_1006: '1006',
-  CODE_1007: '1007',
-  CODE_1008: '1008',
-  CODE_1009: '1009',
-  CODE_1010: '1010',
-  CODE_1011: '1011',
-};
-
-const AudioErrorCodes = Object.freeze(ErrorCodes);
-
-// manages audio calls and audio bridges
-class AudioManager {
-  init(userData) {
-    // this check ensures changing locales will not rerun init
-    if (this.currentState !== undefined) {
-      return;
-    }
-    const MEDIA_CONFIG = Meteor.settings.public.media;
-    const audioBridge = MEDIA_CONFIG.useSIPAudio
-      ? new SIPBridge(userData)
-      : new VertoBridge(userData);
-
-    if (!(audioBridge instanceof BaseAudioBridge)) {
-      throw 'Audio Bridge not compatible';
-    }
-
-    this.bridge = audioBridge;
-    this.isListenOnly = false;
-    this.microphoneLockEnforced = userData.microphoneLockEnforced;
-    this.callStates = CallStates;
-    this.currentState = this.callStates.init;
-
-    callbackToAudioBridge = function (message) {
-      switch (message.status) {
-        case 'failed': {
-          this.currentState = this.callStates.init;
-          const audioFailed = new CustomEvent('bbb.webrtc.failed', {
-            detail: {
-              status: 'Failed',
-              errorCode: message.errorcode,
-            },
-          });
-          window.dispatchEvent(audioFailed);
-          break;
-        }
-        case 'mediafail': {
-          const mediaFailed = new CustomEvent('bbb.webrtc.mediaFailed', {
-            detail: {
-              status: 'MediaFailed',
-            },
-          });
-          window.dispatchEvent(mediaFailed);
-          break;
-        }
-        case 'mediasuccess':
-        case 'started': {
-          const connected = new CustomEvent('bbb.webrtc.connected', {
-            detail: {
-              status: 'started',
-            },
-          });
-          window.dispatchEvent(connected);
-          break;
-        }
-      }
-    };
-  }
-
-  getCurrentState() {
-    return this.currentState;
-  }
-
-  exitAudio() {
-    this.bridge.exitAudio(this.isListenOnly);
-    this.currentState = this.callStates.init;
-  }
-
-  joinAudio(listenOnly) {
-    AudioManager.fetchServers().then(({ error, stunServers, turnServers }) => {
-      if (error || error !== undefined) {
-        // We need to alert the user about this problem by some gui message.
-        console.error("Couldn't fetch the stuns/turns servers!");
-        AudioManager.stunTurnServerFail();
-        return;
-      }
-
-      if (listenOnly || this.microphoneLockEnforced) {
-        this.isListenOnly = true;
-        this.bridge.joinListenOnly(stunServers, turnServers, callbackToAudioBridge.bind(this));
-        // TODO: remove line below after echo test implemented, use webRTCCallStarted instead
-        this.currentState = this.callStates.inListenOnly;
-      } else {
-        this.bridge.joinMicrophone(stunServers, turnServers, callbackToAudioBridge.bind(this));
-        // TODO: remove line below after echo test implemented, use webRTCCallStarted instead
-        this.currentState = this.callStates.inConference;
-      }
-    });
-  }
-
-  transferToConference() {
-    // TODO: transfer from initialized state
-    // TODO: transfer from echo test to conference
-    // this.bridge.transferToConference();
-  }
-
-  webRTCCallStarted(inEchoTest) {
-    if (this.isListenOnly) {
-      this.currentState = this.callStates.inListenOnly;
-    }
-    this.currentState = this.callStates.inConference;
-  }
-
-  webRTCCallFailed(inEchoTest, errorcode, cause) {
-    if (this.currentState !== this.CallStates.reconecting) {
-      this.currentState = this.CallStates.reconecting;
-    }
-  }
-
-  getMicId() {
-    // Placeholder, will get the microphone ID for switching input device
-    // this.bridge.getMicId();
-  }
-
-  setMicId() {
-    // Placeholder, will set the microphone ID for switching input device
-    // this.bridge.setMicId();
-  }
-
-  getSpeakerId() {
-    // Placeholder, will get the speaker ID for switching output device
-    // this.bridge.getSpeakerId();
-  }
-
-  setSpeakerId() {
-    // Placeholder, will set the speaker ID for switching output device
-    // this.bridge.setSpeakerId();
-  }
-
-  getActiveMic() {
-    // Placeholder, will detect active input hardware
-    // this.bridge.getActiveMic();
-  }
-
-  stunTurnServerFail() {
-    const audioFailed = new CustomEvent('bbb.webrtc.failed', {
-      detail: {
-        status: 'Failed',
-        errorCode: AudioErrorCodes.CODE_1009,
-      },
-    });
-    window.dispatchEvent(audioFailed);
-  }
-
-  // We use on the SIP an String Array, while in the server, it comes as
-  // an Array of objects, we need to map from Array<Object> to Array<String>
-  static mapToArray({ response, stunServers, turnServers }) {
-    const promise = new Promise((resolve) => {
-      if (response) {
-        resolve({ error: 404, stunServers: [], turnServers: [] });
-      }
-      resolve({
-        stunServers: stunServers.map(server => server.url),
-        turnServers: turnServers.map(server => ({
-          urls: server.url,
-          username: server.username,
-          password: server.password,
-        })),
-      });
-    });
-    return promise;
-  }
-
-  static fetchServers() {
-    const url = `/bigbluebutton/api/stuns?sessionToken=${Auth.sessionToken}`;
-
-    return fetch(url)
-      .then(response => response.json())
-      .then(json => AudioManager.mapToArray(json));
-  }
-}
-
-const AudioManagerSingleton = new AudioManager();
-export default AudioManagerSingleton;
-export { AudioErrorCodes };
diff --git a/bigbluebutton-html5/imports/api/bbb/index.js b/bigbluebutton-html5/imports/api/bbb/index.js
deleted file mode 100644
index c8b4406628a1c012919bd107129b332106621c5b..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/api/bbb/index.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import AudioManager from '/imports/api/audio/client/manager';
-import Auth from '/imports/ui/services/auth';
-import Users from '/imports/api/users';
-import Meetings from '/imports/api/meetings';
-
-class BBB {
-
-  getUserId() {
-    const userID = Auth.userID;
-    return userID;
-  }
-
-  getUsername() {
-    return Users.findOne({ userId: this.getUserId() }).name;
-  }
-
-  getExtension() {
-    const extension = Meetings.findOne().voiceProp.voiceConf;
-    return extension;
-  }
-
-  getMyUserInfo(callback) {
-    const result = {
-      myUserID: this.getUserId(),
-      myUsername: this.getUsername(),
-      myInternalUserID: this.getUserId(),
-      myAvatarURL: null,
-      myRole: 'getMyRole',
-      amIPresenter: 'false',
-      voiceBridge: this.getExtension(),
-      dialNumber: null,
-    };
-    return callback(result);
-  }
-
-  webRTCCallFailed(inEchoTest, errorcode, cause) {
-    AudioManager.webRTCCallFailed(inEchoTest, errorcode, cause);
-  }
-
-  webRTCCallStarted(inEchoTest) {
-    AudioManager.webRTCCallStarted(inEchoTest);
-  }
-
-  getSessionToken(callback) {
-    callback(Auth.sessionToken);
-  }
-}
-
-export const initBBB = () => {
-  if (window.BBB == undefined) {
-    window.BBB = new BBB();
-  }
-};
diff --git a/bigbluebutton-html5/imports/startup/client/auth.js b/bigbluebutton-html5/imports/startup/client/auth.js
index b70a1549a82338a2ef24bd3dc8982013605c932b..bc4c15923e7f84347e637bc2876760413e1d0c3d 100644
--- a/bigbluebutton-html5/imports/startup/client/auth.js
+++ b/bigbluebutton-html5/imports/startup/client/auth.js
@@ -1,5 +1,5 @@
 import Auth from '/imports/ui/services/auth';
-import { logClient } from '/imports/ui/services/api';
+import { log } from '/imports/ui/services/api';
 
 // disconnected and trying to open a new connection
 const STATUS_CONNECTING = 'connecting';
@@ -88,11 +88,7 @@ export function authenticatedRouteHandler(nextState, replace, callback) {
   Auth.authenticate()
     .then(callback)
     .catch((reason) => {
-      logClient('error', {
-        error: reason,
-        method: 'authenticatedRouteHandler',
-        credentialsSnapshot,
-      });
+      log('error', reason);
 
       // make sure users who did not connect are not added to the meeting
       // do **not** use the custom call - it relies on expired data
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 3583457ace752cf3bdf53f9dfcce3ad8ea6b27ca..425f64adcd25fb502cdb292a1ad78ee35f7b429b 100644
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -6,7 +6,6 @@ import AppContainer from '/imports/ui/components/app/container';
 import ErrorScreen from '/imports/ui/components/error-screen/component';
 import LoadingScreen from '/imports/ui/components/loading-screen/component';
 import Settings from '/imports/ui/services/settings';
-import { initBBB } from '/imports/api/bbb';
 import IntlStartup from './intl';
 
 const BROWSER_LANGUAGE = window.navigator.userLanguage || window.navigator.language;
@@ -35,8 +34,6 @@ class Base extends Component {
 
     this.updateLoadingState = this.updateLoadingState.bind(this);
     this.updateErrorState = this.updateErrorState.bind(this);
-
-    initBBB();
   }
 
   updateLoadingState(loading = false) {
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 1f9ec1b10d90daa4f4f44027eef4a3c41142306a..ad405b15fd069caf4e9ba80d590cf90fb0bdf962 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -2,25 +2,18 @@ import React from 'react';
 import styles from './styles.scss';
 import EmojiContainer from './emoji-menu/container';
 import ActionsDropdown from './actions-dropdown/component';
-import JoinAudioOptionsContainer from '../audio/audio-menu/container';
-import MuteAudioContainer from './mute-button/container';
+import AudioControlsContainer from '../audio/audio-controls/container';
 
 const ActionsBar = ({
   isUserPresenter,
-  handleOpenJoinAudio,
-  handleExitAudio,
 }) => (
   <div className={styles.actionsbar}>
     <div className={styles.left}>
       <ActionsDropdown {...{ isUserPresenter }} />
     </div>
     <div className={styles.center}>
-      <MuteAudioContainer />
-      <JoinAudioOptionsContainer
-        handleJoinAudio={handleOpenJoinAudio}
-        handleCloseAudio={handleExitAudio}
-      />
-      {/* <JoinVideo />*/}
+      <AudioControlsContainer />
+      {/* <JoinVideo /> */}
       <EmojiContainer />
     </div>
   </div>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
index efd132d6fe463d2f0875d770f5ef650130a7518b..c14930d45945edd8d73097b0b0ca7debfbfef5fb 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/component.jsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
+import { EMOJI_NORMALIZE } from '/imports/utils/statuses';
 
 import Button from '/imports/ui/components/button/component';
 import Dropdown from '/imports/ui/components/dropdown/component';
@@ -9,8 +10,7 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component';
 import DropdownList from '/imports/ui/components/dropdown/list/component';
 import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
 import DropdownListSeparator from '/imports/ui/components/dropdown/list/separator/component';
-
-import { EMOJI_NORMALIZE } from '/imports/utils/statuses';
+import styles from './styles';
 
 const intlMessages = defineMessages({
   statusTriggerLabel: {
@@ -109,6 +109,7 @@ const intlMessages = defineMessages({
 
 const propTypes = {
   // Emoji status of the current user
+  intl: intlShape.isRequired,
   userEmojiStatus: PropTypes.string.isRequired,
   actions: PropTypes.object.isRequired,
 };
@@ -121,6 +122,7 @@ const EmojiMenu = ({
   <Dropdown autoFocus>
     <DropdownTrigger tabIndex={0}>
       <Button
+        className={styles.button}
         role="button"
         label={intl.formatMessage(intlMessages.statusTriggerLabel)}
         aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..136d7cad950af9b4c99c08fd6eb35d83fa0f0172
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-menu/styles.scss
@@ -0,0 +1,9 @@
+.button {
+  &:focus {
+    outline: none !important;
+  }
+
+  span:first-child {
+    box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/component.jsx
deleted file mode 100644
index 6e04a34a78c315e34cde96550c86d478df368258..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/component.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { defineMessages, injectIntl } from 'react-intl';
-import React from 'react';
-import Button from '/imports/ui/components/button/component';
-import styles from '../styles.scss';
-
-const intlMessages = defineMessages({
-  muteLabel: {
-    id: 'app.actionsBar.muteLabel',
-    description: 'Mute audio button label',
-  },
-  unmuteLabel: {
-    id: 'app.actionsBar.unmuteLabel',
-    description: 'Unmute audio button label',
-  },
-});
-
-const MuteAudio = ({ intl, toggleSelfVoice, voiceUserData }) => {
-  const { isInAudio, isMuted, isTalking, listenOnly } = voiceUserData;
-
-  if (!isInAudio || listenOnly) return null;
-  const muteLabel = intl.formatMessage(intlMessages.muteLabel);
-  const unmuteLabel = intl.formatMessage(intlMessages.unmuteLabel);
-
-  const label = !isMuted ? muteLabel : unmuteLabel;
-  const icon = !isMuted ? 'unmute' : 'mute';
-  const tabIndex = !isInAudio ? -1 : 0;
-  let className = null;
-
-  if (isInAudio && isTalking) {
-    className = styles.circleGlow;
-  }
-
-  return (
-    <Button
-      onClick={toggleSelfVoice}
-      label={label}
-      color={'primary'}
-      icon={icon}
-      size={'lg'}
-      circle
-      className={className}
-      tabIndex={tabIndex}
-    />
-  );
-};
-
-export default injectIntl(MuteAudio);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/container.jsx
deleted file mode 100644
index 4e5437b42472ff139db12e854c8b0af9313ce2ca..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/mute-button/container.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import MuteAudio from './component';
-import MuteAudioService from '../service';
-
-const MuteAudioContainer = props => (<MuteAudio {...props} />);
-
-export default createContainer(() => ({
-  toggleSelfVoice: MuteAudioService.toggleSelfVoice,
-  voiceUserData: MuteAudioService.getVoiceUserData(),
-}), MuteAudioContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
index b74ee7dfa20d98d04c4c65c2cabcd73f2cd340e3..dbea4fdf5f5e29ee97d4a60e1b58ce12d6a4e679 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
@@ -24,7 +24,3 @@
 .center {
   align-items: center;
 }
-
-.circleGlow > :first-child{
-    box-shadow: 0 0 .15rem #FFF !important;
-}
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index f3c0b2440aaf21e1820f893783ff4e0ecb4c4b82..0949efcc97f0d24dfda41c6a5ae5b06ac152fa9e 100755
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -7,7 +7,6 @@ import cx from 'classnames';
 import ToastContainer from '../toast/container';
 import ModalContainer from '../modal/container';
 import NotificationsBarContainer from '../notifications-bar/container';
-import AudioNotificationContainer from '../audio/audio-notification/container';
 import AudioContainer from '../audio/container';
 import ChatNotificationContainer from '../chat/notification/container';
 import styles from './styles';
@@ -176,7 +175,6 @@ class App extends Component {
 
     return (
       <main className={styles.main}>
-        <AudioNotificationContainer />
         <NotificationsBarContainer />
         <section className={styles.wrapper}>
           {this.renderUserList()}
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 0d0d3430c7b224b84f0afc5e49ac09c1a9de894d..88abd7c9b6e1e4d076700b4a8e27247437c05537 100644
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -75,6 +75,7 @@ const AppContainer = (props) => {
 export default withRouter(injectIntl(withModalMounter(createContainer((
   { router, intl, baseControls }) => {
   const currentUser = Users.findOne({ userId: Auth.userID });
+  const isMeetingBreakout = meetingIsBreakout();
 
   if (!currentUser.approved) {
     baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
@@ -101,9 +102,8 @@ export default withRouter(injectIntl(withModalMounter(createContainer((
   // forcelly logged out when the meeting is ended
   Meetings.find({ meetingId: Auth.meetingID }).observeChanges({
     removed() {
-      if (!meetingIsBreakout) {
-        sendToError(410, intl.formatMessage(intlMessages.endMeetingMessage));
-      }
+      if (isMeetingBreakout) return;
+      sendToError(410, intl.formatMessage(intlMessages.endMeetingMessage));
     },
   });
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3d6a95c3277c2ea25cd03f02a202b0d82a3d7499
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/component.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Button from '/imports/ui/components/button/component';
+import styles from './styles';
+
+const propTypes = {
+  handleToggleMuteMicrophone: PropTypes.func.isRequired,
+  handleJoinAudio: PropTypes.func.isRequired,
+  handleLeaveAudio: PropTypes.func.isRequired,
+  disable: PropTypes.bool.isRequired,
+  unmute: PropTypes.bool.isRequired,
+  mute: PropTypes.bool.isRequired,
+  join: PropTypes.bool.isRequired,
+};
+
+const AudioControls = ({
+  handleToggleMuteMicrophone,
+  handleJoinAudio,
+  handleLeaveAudio,
+  mute,
+  unmute,
+  disable,
+  join,
+}) => (
+  <span className={styles.container}>
+    {mute ?
+      <Button
+        className={styles.button}
+        onClick={handleToggleMuteMicrophone}
+        disabled={disable}
+        label={unmute ? 'Unmute' : 'Mute'}
+        color={'primary'}
+        icon={unmute ? 'mute' : 'unmute'}
+        size={'lg'}
+        circle
+      /> : null}
+    <Button
+      className={styles.button}
+      onClick={join ? handleLeaveAudio : handleJoinAudio}
+      disabled={disable}
+      label={join ? 'Leave Audio' : 'Join Audio'}
+      color={join ? 'danger' : 'primary'}
+      icon={join ? 'audio_off' : 'audio_on'}
+      size={'lg'}
+      circle
+    />
+  </span>);
+
+AudioControls.propTypes = propTypes;
+
+export default AudioControls;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..63504178b3a490018099b45a90c7280ddec7b2af
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import AudioControls from './component';
+import AudioModalContainer from '../audio-modal/container';
+import Service from '../service';
+
+const AudioControlsContainer = props => <AudioControls {...props} />;
+
+export default withModalMounter(createContainer(({ mountModal }) =>
+   ({
+     mute: Service.isConnected() && !Service.isListenOnly() && !Service.isEchoTest(),
+     unmute: Service.isConnected() && !Service.isListenOnly() && Service.isMuted(),
+     join: Service.isConnected() && !Service.isEchoTest(),
+     disable: Service.isConnecting() || Service.isHangingUp(),
+     handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
+     handleJoinAudio: () => mountModal(<AudioModalContainer />),
+     handleLeaveAudio: () => Service.exitAudio(),
+   }), AudioControlsContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b29874dc65d7e6eb7d5baafffd8e17ed996b035c
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/styles.scss
@@ -0,0 +1,22 @@
+.container {
+  display: flex;
+  flex-flow: row;
+
+  > * {
+    margin: 0 1rem;
+
+    span:first-child {
+      box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
+    }
+  }
+
+  > :last-child {
+    margin-right: 0;
+  }
+}
+
+.button {
+  &:focus {
+    outline: none !important;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-menu/component.jsx
deleted file mode 100644
index 68a107185166324d65bee740af82faf6d43d2a55..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/component.jsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import Button from '/imports/ui/components/button/component';
-import { withRouter } from 'react-router';
-import { defineMessages, injectIntl } from 'react-intl';
-import AudioManager from '/imports/api/audio/client/manager';
-
-const intlMessages = defineMessages({
-  joinAudio: {
-    id: 'app.audio.joinAudio',
-    description: 'Join audio button label',
-  },
-  leaveAudio: {
-    id: 'app.audio.leaveAudio',
-    description: 'Leave audio button label',
-  },
-});
-
-class JoinAudioOptions extends React.Component {
-  render() {
-    const {
-      intl,
-      isInAudio,
-      isInListenOnly,
-      handleJoinAudio,
-      handleCloseAudio,
-    } = this.props;
-
-    if (isInAudio || isInListenOnly) {
-      if (AudioManager.currentState == AudioManager.callStates.inConference ||
-      AudioManager.currentState == AudioManager.callStates.inListenOnly) {
-        return (
-          <Button
-            onClick={handleCloseAudio}
-            label={intl.formatMessage(intlMessages.leaveAudio)}
-            color={'danger'}
-            icon={'audio_off'}
-            size={'lg'}
-            circle
-          />
-        );
-      }
-    }
-
-    return (
-      <Button
-        onClick={handleJoinAudio}
-        label={intl.formatMessage(intlMessages.joinAudio)}
-        color={'primary'}
-        icon={'audio_on'}
-        size={'lg'}
-        circle
-      />
-    );
-  }
-}
-
-export default withRouter(injectIntl(JoinAudioOptions));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-menu/container.jsx
deleted file mode 100644
index 26c410c557f703924e92723562d48b0d10ff9f09..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-menu/container.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import VoiceUsers from '/imports/api/voice-users';
-import Auth from '/imports/ui/services/auth/index';
-import JoinAudioOptions from './component';
-
-const JoinAudioOptionsContainer = props => (<JoinAudioOptions {...props} />);
-
-export default createContainer((params) => {
-  const userId = Auth.userID;
-  const voiceUser = VoiceUsers.findOne({ intId: userId });
-
-  const { joined, listenOnly } = voiceUser;
-
-  return {
-    isInAudio: joined,
-    isInListenOnly: listenOnly,
-    handleJoinAudio: params.handleJoinAudio,
-    handleCloseAudio: params.handleCloseAudio,
-  };
-}, JoinAudioOptionsContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
index 2685e511b49698d37e7a0a14c9eb0abaacf16c64..c063971081b4552ebb0d6cc77a70dbe7db82889f 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
@@ -1,54 +1,287 @@
-import React from 'react';
-import ModalBase from '../../modal/base/component';
-import styles from './styles.scss';
-import JoinAudio from '../join-audio/component';
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import ModalBase from '/imports/ui/components/modal/base/component';
+import Button from '/imports/ui/components/button/component';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
+import styles from './styles';
 import AudioSettings from '../audio-settings/component';
+import EchoTest from '../echo-test/component';
 
-export default class AudioModal extends React.Component {
+const propTypes = {
+  intl: intlShape.isRequired,
+  closeModal: PropTypes.func.isRequired,
+  joinMicrophone: PropTypes.func.isRequired,
+  joinListenOnly: PropTypes.func.isRequired,
+  joinEchoTest: PropTypes.func.isRequired,
+  exitAudio: PropTypes.func.isRequired,
+  leaveEchoTest: PropTypes.func.isRequired,
+  changeInputDevice: PropTypes.func.isRequired,
+  changeOutputDevice: PropTypes.func.isRequired,
+  isEchoTest: PropTypes.bool.isRequired,
+  isConnecting: PropTypes.bool.isRequired,
+  isConnected: PropTypes.bool.isRequired,
+  inputDeviceId: PropTypes.string,
+  outputDeviceId: PropTypes.string,
+};
+
+const defaultProps = {
+  inputDeviceId: null,
+  outputDeviceId: null,
+};
+
+const intlMessages = defineMessages({
+  microphoneLabel: {
+    id: 'app.audioModal.microphoneLabel',
+    description: 'Join mic audio button label',
+  },
+  listenOnlyLabel: {
+    id: 'app.audioModal.listenOnlyLabel',
+    description: 'Join listen only audio button label',
+  },
+  closeLabel: {
+    id: 'app.audioModal.closeLabel',
+    description: 'close audio modal button label',
+  },
+  audioChoiceLabel: {
+    id: 'app.audioModal.audioChoiceLabel',
+    description: 'Join audio modal title',
+  },
+  echoTestTitle: {
+    id: 'app.audioModal.echoTestTitle',
+    description: 'Title for the echo test',
+  },
+  settingsTitle: {
+    id: 'app.audioModal.settingsTitle',
+    description: 'Title for the audio modal',
+  },
+  connecting: {
+    id: 'app.audioModal.connecting',
+    description: 'Message for audio connecting',
+  },
+  connectingEchoTest: {
+    id: 'app.audioModal.connectingEchoTest',
+    description: 'Message for echo test connecting',
+  },
+});
+
+class AudioModal extends Component {
   constructor(props) {
     super(props);
 
-    this.JOIN_AUDIO = 0;
-    this.AUDIO_SETTINGS = 1;
+    this.state = {
+      content: null,
+    };
+
+    const {
+      intl,
+      closeModal,
+      joinListenOnly,
+      joinEchoTest,
+      exitAudio,
+      leaveEchoTest,
+      changeInputDevice,
+      changeOutputDevice,
+    } = props;
+
+    this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this);
+    this.handleGoToAudioSettings = this.handleGoToAudioSettings.bind(this);
+    this.handleGoToEchoTest = this.handleGoToEchoTest.bind(this);
+    this.handleJoinMicrophone = this.handleJoinMicrophone.bind(this);
+    this.closeModal = closeModal;
+    this.handleJoinListenOnly = joinListenOnly;
+    this.joinEchoTest = joinEchoTest;
+    this.exitAudio = exitAudio;
+    this.leaveEchoTest = leaveEchoTest;
+    this.changeInputDevice = changeInputDevice;
+    this.changeOutputDevice = changeOutputDevice;
+
+    this.contents = {
+      echoTest: {
+        title: intl.formatMessage(intlMessages.echoTestTitle),
+        component: () => this.renderEchoTest(),
+      },
+      settings: {
+        title: intl.formatMessage(intlMessages.settingsTitle),
+        component: () => this.renderAudioSettings(),
+      },
+    };
+  }
+
+  componentWillUnmount() {
+    const {
+      isEchoTest,
+    } = this.props;
 
-    this.submenus = [];
+    if (isEchoTest) {
+      this.exitAudio();
+    }
   }
 
-  componentWillMount() {
-    /* activeSubmenu represents the submenu in the submenus array to be displayed to the user,
-     * initialized to 0
-     */
-    this.setState({ activeSubmenu: 0 });
-    this.submenus.push({ componentName: JoinAudio });
-    this.submenus.push({ componentName: AudioSettings });
+  handleGoToAudioOptions() {
+    this.setState({
+      content: null,
+    });
   }
 
-  handleSubmenuChange(i) {
-    this.setState({ activeSubmenu: i });
+  handleGoToAudioSettings() {
+    this.leaveEchoTest().then(() => {
+      this.setState({
+        content: 'settings',
+      });
+    });
   }
 
-  renderSubmenu(key) {
-    const curr = this.state.activeSubmenu ? 0 : this.state.activeSubmenu;
+  handleGoToEchoTest() {
+    this.joinEchoTest().then(() => {
+      this.setState({
+        content: 'echoTest',
+      });
+    });
+  }
 
-    const props = {
-      changeMenu: this.handleSubmenuChange.bind(this),
-      JOIN_AUDIO: this.JOIN_AUDIO,
-      AUDIO_SETTINGS: this.AUDIO_SETTINGS,
-      LISTEN_ONLY: this.LISTEN_ONLY,
-      handleJoinListenOnly: this.props.handleJoinListenOnly,
-    };
+  handleJoinMicrophone() {
+    const {
+      joinMicrophone,
+    } = this.props;
 
-    const Submenu = this.submenus[key].componentName;
-    return <Submenu {...props} />;
+    joinMicrophone().catch(this.handleGoToAudioOptions);
+  }
+
+  renderAudioOptions() {
+    const {
+      intl,
+    } = this.props;
+
+    return (
+      <span>
+        <Button
+          className={styles.audioBtn}
+          label={intl.formatMessage(intlMessages.microphoneLabel)}
+          icon={'unmute'}
+          circle
+          size={'jumbo'}
+          onClick={this.handleGoToEchoTest}
+        />
+        <Button
+          className={styles.audioBtn}
+          label={intl.formatMessage(intlMessages.listenOnlyLabel)}
+          icon={'listen'}
+          circle
+          size={'jumbo'}
+          onClick={this.handleJoinListenOnly}
+        />
+      </span>
+    );
+  }
+
+  renderContent() {
+    const {
+      isConnecting,
+      isEchoTest,
+      intl,
+    } = this.props;
+
+    const {
+      content,
+    } = this.state;
+
+    if (isConnecting) {
+      return (
+        <span className={styles.connecting}>
+          { !isEchoTest ?
+            intl.formatMessage(intlMessages.connecting) :
+            intl.formatMessage(intlMessages.connectingEchoTest)
+          }
+        </span>
+      );
+    }
+    return content ? this.contents[content].component() : this.renderAudioOptions();
+  }
+
+  renderEchoTest() {
+    const {
+      isConnecting,
+    } = this.props;
+
+    return (
+      <EchoTest
+        isConnecting={isConnecting}
+        joinEchoTest={this.joinEchoTest}
+        leaveEchoTest={this.leaveEchoTest}
+        handleNo={this.handleGoToAudioSettings}
+        handleYes={this.handleJoinMicrophone}
+      />
+    );
+  }
+
+  renderAudioSettings() {
+    const {
+      isConnecting,
+      isConnected,
+      isEchoTest,
+      inputDeviceId,
+      outputDeviceId,
+    } = this.props;
+
+    return (
+      <AudioSettings
+        handleBack={this.handleGoToAudioOptions}
+        handleRetry={this.handleGoToEchoTest}
+        joinEchoTest={this.joinEchoTest}
+        exitAudio={this.exitAudio}
+        changeInputDevice={this.changeInputDevice}
+        changeOutputDevice={this.changeOutputDevice}
+        isConnecting={isConnecting}
+        isConnected={isConnected}
+        isEchoTest={isEchoTest}
+        inputDeviceId={inputDeviceId}
+        outputDeviceId={outputDeviceId}
+      />
+    );
   }
 
   render() {
+    const {
+      intl,
+      isConnecting,
+    } = this.props;
+
+    const {
+      content,
+    } = this.state;
+
     return (
-      <ModalBase overlayClassName={styles.overlay} className={styles.modal}>
-        <div>
-          {this.renderSubmenu(this.state.activeSubmenu)}
+      <ModalBase
+        overlayClassName={styles.overlay}
+        className={styles.modal}
+        onRequestClose={this.closeModal}
+      >
+        { isConnecting ? null :
+        <header className={styles.header}>
+          <h3 className={styles.title}>
+            { content ?
+              this.contents[content].title :
+              intl.formatMessage(intlMessages.audioChoiceLabel)}
+          </h3>
+          <Button
+            className={styles.closeBtn}
+            label={intl.formatMessage(intlMessages.closeLabel)}
+            icon={'close'}
+            size={'md'}
+            hideLabel
+            onClick={this.closeModal}
+          />
+        </header>
+        }
+        <div className={styles.content}>
+          { this.renderContent() }
         </div>
       </ModalBase>
     );
   }
 }
+
+AudioModal.propTypes = propTypes;
+AudioModal.defaultProps = defaultProps;
+
+export default injectIntl(AudioModal);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..27d11d6302d0e06e17f3c5af452fcdee54f83380
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { createContainer } from 'meteor/react-meteor-data';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import AudioModal from './component';
+import Service from '../service';
+
+const AudioModalContainer = props => <AudioModal {...props} />;
+
+export default withModalMounter(createContainer(({ mountModal }) =>
+   ({
+     closeModal: () => {
+       if (!Service.isConnecting()) mountModal(null);
+     },
+     joinMicrophone: () =>
+       new Promise((resolve, reject) => {
+         Service.transferCall().then(() => {
+           mountModal(null);
+           resolve();
+         }).catch(() => {
+           Service.exitAudio();
+           reject();
+         });
+       }),
+     joinListenOnly: () => Service.joinListenOnly().then(() => mountModal(null)),
+     leaveEchoTest: () => {
+       if (!Service.isEchoTest()) {
+         return Promise.resolve();
+       }
+       return Service.exitAudio();
+     },
+     changeInputDevice: inputDeviceId => Service.changeInputDevice(inputDeviceId),
+     changeOutputDevice: outputDeviceId => Service.changeOutputDevice(outputDeviceId),
+     joinEchoTest: () => Service.joinEchoTest(),
+     exitAudio: () => Service.exitAudio(),
+     isConnecting: Service.isConnecting(),
+     isConnected: Service.isConnected(),
+     isEchoTest: Service.isEchoTest(),
+     inputDeviceId: Service.inputDeviceId(),
+     outputDeviceId: Service.outputDeviceId(),
+   }), AudioModalContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
index 8b5cd2048c1ac4a5f409db2e1d8ee573ae1c07b1..0d3e497c58bbc4476cad217521cab43248737039 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
@@ -1,189 +1,141 @@
 @import "/imports/ui/stylesheets/variables/_all";
 @import "/imports/ui/components/modal/simple/styles";
 
-.overlay {
-  @extend .overlay;
-}
-
-.modal {
-  @extend .modal;
+.header {
+  margin: 0;
+  padding: 0;
+  border: none;
+  line-height: 2rem;
 }
 
-.center {
+.content {
   display: flex;
   justify-content: center;
-  align-items: center;
-  padding: 2rem 0;
+  padding: 0;
+  margin-top: auto;
+  margin-bottom: auto;
+  padding: 0.5rem 0;
+
+  .audioBtn:first-child {
+    margin-right: 3rem;
+
+    @include mq($small-only) {
+      margin-right: 1rem;
+    }
+  }
 }
 
-.closeBtnWrapper {
-   display: flex;
-   justify-content: flex-end;
+.overlay {
+  @extend .overlay;
+}
+
+.modal {
+  @extend .modal;
+  padding: 1.5rem;
+  min-height: 20rem;
 }
 
 .closeBtn {
+  right: 0;
+  top: 0;
+  position: absolute;
   background-color: $color-white;
   border: none;
+  padding: .75rem;
+
   i {
     color: $color-gray-light;
   }
 
   &:focus,
   &:hover{
-    background-color: #0a5eac;
+    background-color: $color-white;
     i{
-      color: $color-white;
+      color: $color-primary;
     }
   }
 }
 
-Button.audioBtn {
+.audioBtn {
+  &:focus {
+    outline: none !important;
+  }
+
   i{
     color: #3c5764;
   }
 }
 
 // Modifies the audio button icon colour
-Button.audioBtn span:first-child {
+.audioBtn span:first-child {
   color: #1b3c4b;
   background-color: #f1f8ff;
   box-shadow: none;
   border: 5px solid  #f1f8ff;
+  font-size: 3.5rem;
+
+  @include mq($small-only) {
+    font-size: 2.5rem;
+  }
 }
 
 // When hovering over a button of class audioBtn, change the border colour of first span-child
-Button.audioBtn:hover span:first-child, 
-Button.audioBtn:focus span:first-child {
+.audioBtn:hover span:first-child,
+.audioBtn:focus span:first-child {
   border: 5px solid $color-primary;
   background-color: #f1f8ff;
 }
 
 // Modifies the button label text
-Button.audioBtn span:last-child {
+.audioBtn span:last-child {
   color: black;
-  font-size: 0.8rem;
+  font-size: 1rem;
   font-weight: 600;
 }
 
-Button.audioBtn:first-of-type {
-  margin-right: 5%;
-}
-
-Button.audioBtn:last-of-type {
-  margin-left: 5%;
-}
-
-.backBtn {
-  border: none;
-  i {
-    color: $color-link;
-  }
-
-  &,
-  &:focus,
-  &:hover {
-    i {
-      color: $color-white;
-    }
-  }
-}
-
-.topRow {
-  align-items: center;
-  display: flex;
-}
-
-.audioNote {
-  color: $color-text;
-  display: inline-block;
-  font-size: 0.9rem;
-}
-
 .title {
   text-align: center;
-  margin: auto;
-  color: black;
   font-weight: 400;
   font-size: 1.3rem;
-  display: block;
-}
+  white-space: normal;
 
-.form {
-  display: flex;
-  flex-flow: column;
-  padding: 2em;
+  @include mq($small-only) {
+    font-size: 1rem;
+    padding: 0 1rem;
+  }
 }
 
-.row {
-  display: flex;
-  flex-flow: row;
-  flex-grow: 1;
-  justify-content: space-between;
-  margin-bottom: 0.7rem;
+.connecting {
+  font-size: 2rem;
 }
 
-.col {
-  display: flex;
-  flex-grow: 1;
-  flex-basis: 0;
-  margin-right: 1rem;
+.connecting:after {
+  overflow: hidden;
+  display: inline-block;
+  vertical-align: bottom;
+  -webkit-animation: ellipsis steps(4,end) 900ms infinite;
+  animation: ellipsis steps(4,end) 900ms infinite;
+  content: "\2026"; /* ascii code for the ellipsis character */
+  width: 0;
+  margin-right: 1.25em;
+}
 
-  &:last-child {
+@keyframes ellipsis {
+  to {
+    width: 1.25em;
     margin-right: 0;
-    padding-right: 0.1rem;
-    padding-left: 4rem;
   }
 }
 
-.labelSmall {
-  color: black;
-  font-size: 0.7rem;
-  font-weight: 600;
-  margin-bottom: 0.3rem;
-}
-
-.formElement {
-  position: relative;
-  display: flex;
-  flex-flow: column;
-  flex-grow: 1;
+@-webkit-keyframes ellipsis {
+  to {
+    width: 1.25em;
+    margin-right: 0;
+  }
 }
 
-.select {
-  @extend %customSelectFocus;
-  background-color: $color-white;
-  border: 0;
-  border-bottom: 0.1rem solid $color-text;
+.audioNote {
   color: $color-text;
-  width: 100%;
-  // appearance: none;
-  height: 1.75rem;
-}
-
-.audioMeter {
-  width: 100%;
-}
-
-.pullContentRight {
-  display: flex;
-  justify-content: flex-end;
-  flex-flow: row;
-}
-
-.verticalLine {
-  color: #f3f6f9;
-  border-left: 1px solid;
-  height: 5rem;
-}
-
-.enterAudio {
-  display: flex;
-  justify-content: flex-end;
-  margin-right: 2rem;
-}
-
-.chooseAudio {
-  position:absolute;
-  left:50%;
-  transform: translate(-50%, 0);
-}
+  display: inline-block;
+  font-size: 0.9rem;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-notification/component.jsx
deleted file mode 100644
index d0c3d1823f4e599bb3b2bb8cbe713407fc14578f..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/component.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, injectIntl } from 'react-intl';
-import styles from './styles.scss';
-import cx from 'classnames';
-import Button from '/imports/ui/components/button/component';
-
-const COLORS = [
-  'default', 'primary', 'danger', 'success',
-];
-
-const propTypes = {
-  color: PropTypes.oneOf(COLORS),
-  message: PropTypes.string,
-};
-
-const defaultProps = {
-  color: 'default',
-};
-
-const intlMessages = defineMessages({
-  closeLabel: {
-    id: 'app.audioNotification.closeLabel',
-    description: 'Audio notification dismiss label',
-  },
-});
-
-class AudioNotification extends Component {
-  constructor(props) {
-    super(props);
-
-    this.handleClose = this.handleClose.bind(this);
-  }
-
-  handleClose() {
-    this.props.handleClose();
-  }
-
-  render() {
-    const {
-      color,
-      message,
-      intl,
-    } = this.props;
-
-    if (!color || !message) {
-      return null;
-    }
-    return (
-      <div
-        role="alert"
-        className={cx(styles.audioNotifications, styles[this.props.color])}
-      >
-        {message}
-        <Button
-          className={styles.closeBtn}
-          label={intl.formatMessage(intlMessages.closeLabel)}
-          icon={'close'}
-          size={'sm'}
-          circle
-          hideLabel
-          onClick={this.handleClose}
-        />
-      </div>
-    );
-  }
-}
-
-AudioNotification.propTypes = propTypes;
-AudioNotification.defaultProps = defaultProps;
-
-export default injectIntl(AudioNotification);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-notification/container.jsx
deleted file mode 100644
index ec6a7de4ccdda937b22104eefd03535796531211..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/container.jsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import { createContainer } from 'meteor/react-meteor-data';
-import React, { Component } from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-import AudioNotification from './component';
-import AudioManager, { AudioErrorCodes } from '/imports/api/audio/client/manager';
-
-const intlMessages = defineMessages({
-  [AudioErrorCodes.CODE_1001]: {
-    id: 'app.audioNotification.audioFailedError1001',
-    description: 'Audio connection failed with error 1001',
-  },
-  [AudioErrorCodes.CODE_1002]: {
-    id: 'app.audioNotification.audioFailedError1002',
-    description: 'Audio connection failed with error 1002',
-  },
-  [AudioErrorCodes.CODE_1003]: {
-    id: 'app.audioNotification.audioFailedError1003',
-    description: 'Audio connection failed with error 1003',
-  },
-  [AudioErrorCodes.CODE_1004]: {
-    id: 'app.audioNotification.audioFailedError1004',
-    description: 'Audio connection failed with error 1004',
-  },
-  [AudioErrorCodes.CODE_1005]: {
-    id: 'app.audioNotification.audioFailedError1005',
-    description: 'Audio connection failed with error 1005',
-  },
-  [AudioErrorCodes.CODE_1006]: {
-    id: 'app.audioNotification.audioFailedError1006',
-    description: 'Audio connection failed with error 1006',
-  },
-  [AudioErrorCodes.CODE_1007]: {
-    id: 'app.audioNotification.audioFailedError1007',
-    description: 'Audio connection failed with error 1007',
-  },
-  [AudioErrorCodes.CODE_1008]: {
-    id: 'app.audioNotification.audioFailedError1008',
-    description: 'Audio connection failed with error 1008',
-  },
-  [AudioErrorCodes.CODE_1009]: {
-    id: 'app.audioNotification.audioFailedError1009',
-    description: 'Audio connection failed with error 1009',
-  },
-  [AudioErrorCodes.CODE_1010]: {
-    id: 'app.audioNotification.audioFailedError1010',
-    description: 'Audio connection failed with error 1010',
-  },
-  [AudioErrorCodes.CODE_1011]: {
-    id: 'app.audioNotification.audioFailedError1011',
-    description: 'Audio connection failed with error 1011',
-  },
-  audioFailed: {
-    id: 'app.audioNotification.audioFailedMessage',
-    description: 'The audio could not connect',
-  },
-  mediaFailed: {
-    id: 'app.audioNotification.mediaFailedMessage',
-    description: 'Could not access getUserMicMedia',
-  },
-});
-
-class AudioNotificationContainer extends Component {
-  constructor(props) {
-    super(props);
-
-    this.color = null;
-    this.message = null;
-
-    this.state = {
-      status: null,
-    };
-
-    this.handleAudioFailure = this.handleAudioFailure.bind(this);
-    this.handleMediaFailure = this.handleMediaFailure.bind(this);
-    this.handleClose = this.handleClose.bind(this);
-  }
-
-  componentDidMount() {
-    window.addEventListener('bbb.webrtc.failed', this.handleAudioFailure);
-    window.addEventListener('bbb.webrtc.mediaFailed', this.handleMediaFailure);
-    window.addEventListener('bbb.webrtc.connected', this.handleClose);
-  }
-
-  componentWillUnmount() {
-    window.removeEventListener('bbb.webrtc.failed', this.handleAudioFailure);
-    window.removeEventListener('bbb.webrtc.mediaFailed', this.handleMediaFailure);
-    window.removeEventListener('bbb.webrtc.connected', this.handleClose);
-  }
-
-  handleClose() {
-    this.color = null;
-    this.message = null;
-    this.setState({ status: null });
-  }
-
-  handleAudioFailure(e) {
-    this.message = this.props.messages[e.detail.errorCode];
-    if (this.message == null || this.message == undefined) {
-      this.message = this.props.audioFailure;
-    }
-    this.setState({ status: 'failed' });
-  }
-
-  handleMediaFailure() {
-    this.message = this.props.mediaFailure;
-    this.setState({ status: 'failed' });
-  }
-
-  render() {
-    const handleClose = this.handleClose;
-    this.color = 'danger';
-
-    return (
-      <AudioNotification
-        color={this.color}
-        message={this.message}
-        handleClose={handleClose}
-      />
-    );
-  }
-}
-
-export default injectIntl(createContainer(({ intl }) => {
-  const messages = {};
-  messages[AudioErrorCodes.CODE_1001] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1001]);
-  messages[AudioErrorCodes.CODE_1002] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1002]);
-  messages[AudioErrorCodes.CODE_1003] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1003]);
-  messages[AudioErrorCodes.CODE_1004] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1004]);
-  messages[AudioErrorCodes.CODE_1005] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1005]);
-  messages[AudioErrorCodes.CODE_1006] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1006]);
-  messages[AudioErrorCodes.CODE_1007] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1007]);
-  messages[AudioErrorCodes.CODE_1008] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1008]);
-  messages[AudioErrorCodes.CODE_1009] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1009]);
-  messages[AudioErrorCodes.CODE_1010] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1010]);
-  messages[AudioErrorCodes.CODE_1011] = intl.formatMessage(intlMessages[AudioErrorCodes.CODE_1011]);
-  messages.audioFailure = intl.formatMessage(intlMessages.audioFailed);
-  messages.mediaFailure = intl.formatMessage(intlMessages.mediaFailed);
-
-  return { messages };
-}, AudioNotificationContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-notification/styles.scss
deleted file mode 100644
index 1033621bbdba24af89257aea1d57bf8a0342931c..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-notification/styles.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-@import "/imports/ui/stylesheets/variables/_all";
-
-$nb-default-color: $color-gray;
-$nb-default-bg: $color-white;
-$nb-default-border: $color-white;
-
-$nb-primary-color: $color-white;
-$nb-primary-bg: $color-primary;
-$nb-primary-border: $color-primary;
-
-$nb-success-color: $color-white;
-$nb-success-bg: $color-success;
-$nb-success-border: $color-success;
-
-$nb-danger-color: $color-white;
-$nb-danger-bg: $color-danger;
-$nb-danger-border: $color-danger;
-
-.audioNotifications {
-  padding: $line-height-computed / 2;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-  font-weight: 600;
-}
-
-.closeBtn {
-  position: absolute;
-  right: 1.65em;
-  top: .5em;
-}
-
-// Modifies the close button style
-Button.closeBtn span:first-child {
-  color: $color-gray-light;
-  background: none;
-  border: none;
-  box-shadow: none;
-}
-
-
-@mixin nb-variant($color, $background, $border) {
-  color: $color;
-  background-color: $background;
-  border-color: $border;
-}
-
-.default {
-  @include nb-variant($nb-default-color, $nb-default-bg, $nb-default-border);
-}
-
-.primary {
-  @include nb-variant($nb-primary-color, $nb-primary-bg, $nb-primary-border);
-}
-
-.success {
-  @include nb-variant($nb-success-color, $nb-success-bg, $nb-success-border);
-}
-
-.danger {
-  @include nb-variant($nb-danger-color, $nb-danger-bg, $nb-danger-border);
-}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
index 3c76e00120a3311d1b6a387e9dce536b9b774fd0..6e6534192d45e5c9ba88eb4976acb4243f38b2ca 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/component.jsx
@@ -1,73 +1,103 @@
 import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import Button from '/imports/ui/components/button/component';
 import { withModalMounter } from '/imports/ui/components/modal/service';
-import styles from '../audio-modal/styles.scss';
-
 import DeviceSelector from '/imports/ui/components/audio/device-selector/component';
-import AudioStreamVolume from '/imports/ui/components/audio/audio-stream-volume/component';
-import EnterAudioContainer from '/imports/ui/components/audio/enter-audio/container';
 import AudioTestContainer from '/imports/ui/components/audio/audio-test/container';
 import cx from 'classnames';
+import styles from './styles';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  exitAudio: PropTypes.func.isRequired,
+  changeInputDevice: PropTypes.func.isRequired,
+  changeOutputDevice: PropTypes.func.isRequired,
+  handleBack: PropTypes.func.isRequired,
+  handleRetry: PropTypes.func.isRequired,
+  isConnecting: PropTypes.bool.isRequired,
+  inputDeviceId: PropTypes.string.isRequired,
+  outputDeviceId: PropTypes.string.isRequired,
+};
+
+const intlMessages = defineMessages({
+  backLabel: {
+    id: 'app.audio.backLabel',
+    description: 'audio settings back button label',
+  },
+  descriptionLabel: {
+    id: 'app.audio.audioSettings.descriptionLabel',
+    description: 'audio settings description label',
+  },
+  micSourceLabel: {
+    id: 'app.audio.audioSettings.microphoneSourceLabel',
+    description: 'Label for mic source',
+  },
+  speakerSourceLabel: {
+    id: 'app.audio.audioSettings.speakerSourceLabel',
+    description: 'Label for speaker source',
+  },
+  streamVolumeLabel: {
+    id: 'app.audio.audioSettings.microphoneStreamLabel',
+    description: 'Label for stream volume',
+  },
+  retryLabel: {
+    id: 'app.audio.audioSettings.retryLabel',
+    description: 'Retry button label',
+  },
+});
 
 class AudioSettings extends React.Component {
   constructor(props) {
     super(props);
 
-    this.chooseAudio = this.chooseAudio.bind(this);
+    const {
+      inputDeviceId,
+      outputDeviceId,
+    } = props;
+
     this.handleInputChange = this.handleInputChange.bind(this);
     this.handleOutputChange = this.handleOutputChange.bind(this);
-    this.handleClose = this.handleClose.bind(this);
 
     this.state = {
-      inputDeviceId: undefined,
+      inputDeviceId,
+      outputDeviceId,
     };
   }
 
-  chooseAudio() {
-    this.props.changeMenu(this.props.JOIN_AUDIO);
-  }
-
   handleInputChange(deviceId) {
-    console.log(`INPUT DEVICE CHANGED: ${deviceId}`);
+    const {
+      changeInputDevice,
+    } = this.props;
+
+    changeInputDevice(deviceId);
     this.setState({
       inputDeviceId: deviceId,
     });
   }
 
   handleOutputChange(deviceId) {
-    console.log(`OUTPUT DEVICE CHANGED: ${deviceId}`);
-  }
+    const {
+      changeOutputDevice,
+    } = this.props;
 
-  handleClose() {
-    this.setState({ isOpen: false });
-    this.props.mountModal(null);
+    changeOutputDevice(deviceId);
+    this.setState({
+      outputDeviceId: deviceId,
+    });
   }
 
   render() {
     const {
+      isConnecting,
       intl,
+      handleBack,
+      handleRetry,
     } = this.props;
 
     return (
       <div>
-        <div className={styles.topRow}>
-          <Button
-            className={styles.backBtn}
-            label={intl.formatMessage(intlMessages.backLabel)}
-            icon={'left_arrow'}
-            size={'md'}
-            color={'primary'}
-            ghost
-            onClick={this.chooseAudio}
-          />
-          <div className={cx(styles.title, styles.chooseAudio)}>
-            {intl.formatMessage(intlMessages.titleLabel)}
-          </div>
-        </div>
-
         <div className={styles.form}>
-
           <div className={styles.row}>
             <div className={styles.audioNote}>
               {intl.formatMessage(intlMessages.descriptionLabel)}
@@ -77,9 +107,13 @@ class AudioSettings extends React.Component {
           <div className={styles.row}>
             <div className={styles.col}>
               <div className={styles.formElement}>
-                <label className={cx(styles.label, styles.labelSmall)}>
+                <label
+                  htmlFor="inputDeviceSelector"
+                  className={cx(styles.label, styles.labelSmall)}
+                >
                   {intl.formatMessage(intlMessages.micSourceLabel)}
                   <DeviceSelector
+                    id="inputDeviceSelector"
                     value={this.state.inputDeviceId}
                     className={styles.select}
                     kind="audioinput"
@@ -90,9 +124,13 @@ class AudioSettings extends React.Component {
             </div>
             <div className={styles.col}>
               <div className={styles.formElement}>
-                <label className={cx(styles.label, styles.labelSmall)}>
+                <label
+                  htmlFor="outputDeviceSelector"
+                  className={cx(styles.label, styles.labelSmall)}
+                >
                   {intl.formatMessage(intlMessages.speakerSourceLabel)}
                   <DeviceSelector
+                    id="outputDeviceSelector"
                     value={this.state.outputDeviceId}
                     className={styles.select}
                     kind="audiooutput"
@@ -104,57 +142,42 @@ class AudioSettings extends React.Component {
           </div>
 
           <div className={styles.row}>
-            <div className={styles.col}>
-              <div className={styles.formElement}>
-                <label className={cx(styles.label, styles.labelSmall)}>
-                  {intl.formatMessage(intlMessages.streamVolumeLabel)}
-                  <AudioStreamVolume
-                    deviceId={this.state.inputDeviceId}
-                    className={styles.audioMeter}
-                  />
-                </label>
-              </div>
-            </div>
-            <div className={styles.col}>
-              <label className={styles.label}> </label>
-              <AudioTestContainer />
+            <div className={cx(styles.col, styles.spacedLeft)}>
+              <label
+                htmlFor="audioTest"
+                className={styles.labelSmall}
+              >
+                Test your speaker volume
+                <AudioTestContainer id="audioTest" />
+              </label>
             </div>
           </div>
         </div>
 
+
         <div className={styles.enterAudio}>
-          <EnterAudioContainer isFullAudio />
+          <Button
+            className={styles.backBtn}
+            label={intl.formatMessage(intlMessages.backLabel)}
+            size={'md'}
+            color={'primary'}
+            onClick={handleBack}
+            disabled={isConnecting}
+            ghost
+          />
+          <Button
+            size={'md'}
+            color={'primary'}
+            label={intl.formatMessage(intlMessages.retryLabel)}
+            onClick={handleRetry}
+          />
         </div>
       </div>
     );
   }
+
 }
 
-const intlMessages = defineMessages({
-  backLabel: {
-    id: 'app.audio.backLabel',
-    description: 'audio settings back button label',
-  },
-  titleLabel: {
-    id: 'app.audio.audioSettings.titleLabel',
-    description: 'audio setting title label',
-  },
-  descriptionLabel: {
-    id: 'app.audio.audioSettings.descriptionLabel',
-    description: 'audio settings description label',
-  },
-  micSourceLabel: {
-    id: 'app.audio.audioSettings.microphoneSourceLabel',
-    description: 'Label for mic source',
-  },
-  speakerSourceLabel: {
-    id: 'app.audio.audioSettings.speakerSourceLabel',
-    description: 'Label for speaker source',
-  },
-  streamVolumeLabel: {
-    id: 'app.audio.audioSettings.microphoneStreamLabel',
-    description: 'Label for stream volume',
-  },
-});
+AudioSettings.propTypes = propTypes;
 
 export default withModalMounter(injectIntl(AudioSettings));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-settings/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..0ee59b2e4a8bc4df9f2fde592fecd7548bd061c4
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-settings/styles.scss
@@ -0,0 +1,126 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.form {
+  display: flex;
+  flex-flow: column;
+  margin-top: 1.5rem;
+}
+
+.audioNote {
+  @include mq($small-only) {
+    font-size: 0.8rem;
+  }
+}
+
+.row {
+  display: flex;
+  flex-flow: row;
+  justify-content: space-between;
+  margin-bottom: 0.7rem;
+}
+
+.col {
+  display: flex;
+  flex-grow: 1;
+  flex-basis: 0;
+  margin-right: 1rem;
+
+  &:last-child {
+    margin-right: 0;
+    padding-right: 0.1rem;
+    padding-left: 4rem;
+  }
+
+  &.spacedLeft {
+    // @extend .spaced;
+
+    label {
+      flex-grow: 1;
+      flex-basis: 0;
+      margin-right: 0;
+      padding-right: 0.1rem;
+      padding-left: 4rem;
+    }
+
+    &:before {
+      content: "";
+      display: block;
+      flex-grow: 1;
+      flex-basis: 0;
+      margin-right: 1rem;
+    }
+
+    &:last-child {
+      margin-right: 0;
+      padding-right: 0;
+      padding-left: 0;
+    }
+  }
+}
+
+.labelSmall {
+  color: black;
+  font-size: 0.85rem;
+  font-weight: 600;
+
+  & > :first-child {
+    margin-top: 0.5rem;
+  }
+}
+
+.formElement {
+  position: relative;
+  display: flex;
+  flex-flow: column;
+  flex-grow: 1;
+}
+
+.select {
+  -webkit-appearance: none;
+  -webkit-border-radius: 0px;
+  background: $color-white url("data:image/svg+xml;charset=utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'><path fill='#667189' d='M2 0L0 2h4zm0 5L0 3h4z'/></svg>") no-repeat right .35rem center/.4rem .5rem;
+  background-repeat: no-repeat;
+  border: 0.07rem solid $color-gray-light;
+  border-radius: .125rem;
+  color: $color-text;
+  width: 100%;
+  // appearance: none;
+  padding: .4rem;
+}
+
+.audioMeter {
+  width: 100%;
+}
+
+.pullContentRight {
+  display: flex;
+  justify-content: flex-end;
+  flex-flow: row;
+}
+
+.verticalLine {
+  color: #f3f6f9;
+  border-left: 1px solid;
+  height: 5rem;
+}
+
+.backBtn {
+  margin-right: 0.5rem;
+  border: none;
+
+  @include mq($small-only) {
+    margin-right: auto;
+  }
+}
+
+.enterAudio {
+  margin-top: 1.5rem;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.chooseAudio {
+  position:absolute;
+  left:50%;
+  transform: translate(-50%, 0);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx
index ce1b691aa462887bcc8c0ab8267f8835d318a828..24dac7849875a785f3cdb2173941d6c58005b3a3 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-stream-volume/component.jsx
@@ -99,7 +99,7 @@ class AudioStreamVolume extends Component {
 
     this.setState(prevState => ({
       instant,
-      slow: 0.75 * prevState.slow + 0.25 * instant,
+      slow: (0.75 * prevState.slow) + (0.25 * instant),
     }));
   }
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx
index 33ace856628851727ace7c1b0e192c0e80c05e32..cb57d7fca081160092b97dbdcf90301e0eb41c36 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-test/component.jsx
@@ -1,15 +1,36 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import Button from '/imports/ui/components/button/component';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
 import styles from './styles.scss';
-import { defineMessages, injectIntl } from 'react-intl';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  handlePlayAudioSample: PropTypes.func.isRequired,
+  outputDeviceId: PropTypes.string,
+};
+
+const defaultProps = {
+  outputDeviceId: null,
+};
+
+const intlMessages = defineMessages({
+  playSoundLabel: {
+    id: 'app.audio.playSoundLabel',
+    description: 'Play sound button label',
+  },
+});
 
 class AudioTest extends React.Component {
   constructor(props) {
     super(props);
+
+    this.handlePlayAudioSample = props.handlePlayAudioSample.bind(this);
   }
 
   render() {
     const {
+      outputDeviceId,
       intl,
     } = this.props;
 
@@ -20,17 +41,13 @@ class AudioTest extends React.Component {
         icon={'unmute'}
         size={'sm'}
         color={'primary'}
-        onClick={this.props.handlePlayAudioSample}
+        onClick={() => this.handlePlayAudioSample(outputDeviceId)}
       />
     );
   }
 }
 
-const intlMessages = defineMessages({
-  playSoundLabel: {
-    id: 'app.audio.playSoundLabel',
-    description: 'Play sound button label',
-  },
-});
+AudioTest.propTypes = propTypes;
+AudioTest.defaultProps = defaultProps;
 
 export default injectIntl(AudioTest);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx
index 37420893afe78df3005acb3826e9ae1fb85dbd16..8e74d4fb66e964df7834c81b5cc31effa0395885 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-test/container.jsx
@@ -1,24 +1,15 @@
-import React, { Component } from 'react';
+import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
+import Service from '/imports/ui/components/audio/service';
 import AudioTest from './component';
 
-class AudioTestContainer extends Component {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    return (
-      <AudioTest {...this.props}>
-        {this.props.children}
-      </AudioTest>
-    );
-  }
-}
+const AudioTestContainer = props => <AudioTest {...props} />;
 
 export default createContainer(() => ({
-  handlePlayAudioSample: () => {
-    const snd = new Audio('resources/sounds/audioSample.mp3');
-    snd.play();
+  outputDeviceId: Service.outputDeviceId(),
+  handlePlayAudioSample: (deviceId) => {
+    const sound = new Audio('resources/sounds/audioSample.mp3');
+    if (deviceId && sound.setSinkId) sound.setSinkId(deviceId);
+    sound.play();
   },
 }), AudioTestContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss
index b9ff81b24d83075b58b77c47453223500846bb26..2df5f1a13de80fd66cc40fd2df34eb7c5d3257f9 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-test/styles.scss
@@ -1,12 +1,21 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
 .testAudioBtn {
-  border: none;
-  padding-left: 0;
   background-color: transparent;
   color: $color-primary;
   font-weight: normal;
+  border: none;
+
   i {
     color: $color-primary;
+    transition: all .2s ease-in-out;
+  }
+
+  &:hover, &:focus, &:active {
+    background-color: transparent !important;
+    color: darken($color-primary, 17%) !important;
+    i {
+      color: darken($color-primary, 17%);
+    }
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/component.jsx
index 978cbe9af0ead95e4fd69bf3aa5174e27ea7ace5..717836bd01404a41ead99856dab6ef6038a10fa0 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/component.jsx
@@ -1,12 +1,28 @@
 import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+  init: PropTypes.func.isRequired,
+};
 
 export default class Audio extends Component {
   constructor(props) {
     super(props);
-    props.init.call(this);
+
+    this.init = props.init.bind(this);
+  }
+
+  componentDidMount() {
+    this.init();
   }
 
   render() {
-    return (<audio id="remote-media" autoPlay="autoplay" />);
+    return (
+      <audio id="remote-media" autoPlay="autoplay">
+        <track kind="captions" /> {/* These captions are brought to you by eslint */}
+      </audio>
+    );
   }
 }
+
+Audio.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index 845ac98ee0d8b717b5a2665def2bce4787f1162b..fbf0ea116c9cfd9d9cdbb38c30af8d33182f6167 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -1,10 +1,11 @@
 import React from 'react';
 import { createContainer } from 'meteor/react-meteor-data';
 import { withModalMounter } from '/imports/ui/components/modal/service';
+import { injectIntl, defineMessages } from 'react-intl';
 import PropTypes from 'prop-types';
 import Service from './service';
 import Audio from './component';
-import AudioModal from './audio-modal/component';
+import AudioModalContainer from './audio-modal/container';
 
 const propTypes = {
   children: PropTypes.element,
@@ -14,28 +15,75 @@ const defaultProps = {
   children: null,
 };
 
-const AudioContainer = props =>
-  (<Audio {...props}>
-    {props.children}
-  </Audio>
-  );
+const intlMessages = defineMessages({
+  joinedAudio: {
+    id: 'app.audioManager.joinedAudio',
+    description: 'Joined audio toast message',
+  },
+  joinedEcho: {
+    id: 'app.audioManager.joinedEcho',
+    description: 'Joined echo test toast message',
+  },
+  leftAudio: {
+    id: 'app.audioManager.leftAudio',
+    description: 'Left audio toast message',
+  },
+  genericError: {
+    id: 'app.audioManager.genericError',
+    description: 'Generic error messsage',
+  },
+  connectionError: {
+    id: 'app.audioManager.connectionError',
+    description: 'Connection error messsage',
+  },
+  requestTimeout: {
+    id: 'app.audioManager.requestTimeout',
+    description: 'Request timeout error messsage',
+  },
+  invalidTarget: {
+    id: 'app.audioManager.invalidTarget',
+    description: 'Invalid target error messsage',
+  },
+  mediaError: {
+    id: 'app.audioManager.mediaError',
+    description: 'Media error messsage',
+  },
+});
+
+
+const AudioContainer = props => <Audio {...props} />;
 
 let didMountAutoJoin = false;
 
-export default withModalMounter(createContainer(({ mountModal }) => {
+export default withModalMounter(injectIntl(createContainer(({ mountModal, intl }) => {
   const APP_CONFIG = Meteor.settings.public.app;
 
   const { autoJoinAudio } = APP_CONFIG;
 
+  const messages = {
+    info: {
+      JOINED_AUDIO: intl.formatMessage(intlMessages.joinedAudio),
+      JOINED_ECHO: intl.formatMessage(intlMessages.joinedEcho),
+      LEFT_AUDIO: intl.formatMessage(intlMessages.leftAudio),
+    },
+    error: {
+      GENERIC_ERROR: intl.formatMessage(intlMessages.genericError),
+      CONNECTION_ERROR: intl.formatMessage(intlMessages.connectionError),
+      REQUEST_TIMEOUT: intl.formatMessage(intlMessages.requestTimeout),
+      INVALID_TARGET: intl.formatMessage(intlMessages.invalidTarget),
+    },
+  };
+
   return {
     init: () => {
-      Service.init();
+      Service.init(messages);
+      Service.changeOutputDevice(document.querySelector('#remote-media').sinkId);
       if (!autoJoinAudio || didMountAutoJoin) return;
-      mountModal(<AudioModal handleJoinListenOnly={Service.joinListenOnly} />);
+      mountModal(<AudioModalContainer />);
       didMountAutoJoin = true;
     },
   };
-}, AudioContainer));
+}, AudioContainer)));
 
 AudioContainer.propTypes = propTypes;
 AudioContainer.defaultProps = defaultProps;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx
index ec40ddb175df0186d1c21c16b9ee61739dc0594f..7c68859eb02a25eb199a7b38f2de24b301c70b74 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/device-selector/component.jsx
@@ -1,24 +1,28 @@
 import React, { Component } from 'react';
+import _ from 'lodash';
 import PropTypes from 'prop-types';
+import cx from 'classnames';
 import styles from '../audio-modal/styles';
 
 const propTypes = {
   kind: PropTypes.oneOf(['audioinput', 'audiooutput', 'videoinput']),
   onChange: PropTypes.func.isRequired,
   value: PropTypes.string,
+  handleDeviceChange: PropTypes.func,
+  className: PropTypes.string,
 };
 
 const defaultProps = {
   kind: 'audioinput',
   value: undefined,
+  className: null,
+  handleDeviceChange: null,
 };
 
 class DeviceSelector extends Component {
   constructor(props) {
     super(props);
 
-    this.handleEnumerateDevicesSuccess = this.handleEnumerateDevicesSuccess.bind(this);
-    this.handleEnumerateDevicesError = this.handleEnumerateDevicesError.bind(this);
     this.handleSelectChange = this.handleSelectChange.bind(this);
 
     this.state = {
@@ -29,26 +33,22 @@ class DeviceSelector extends Component {
   }
 
   componentDidMount() {
-    navigator.mediaDevices
-      .enumerateDevices()
-      .then(this.handleEnumerateDevicesSuccess)
-      .catch(this.handleEnumerateDevicesError);
-  }
+    const handleEnumerateDevicesSuccess = (deviceInfos) => {
+      const devices = deviceInfos.filter(d => d.kind === this.props.kind);
 
-  handleEnumerateDevicesSuccess(deviceInfos) {
-    const devices = deviceInfos.filter(d => d.kind === this.props.kind);
-
-    this.setState({
-      devices,
-      options: devices.map((d, i) => ({
-        label: d.label || `${this.props.kind} - ${i}`,
-        value: d.deviceId,
-      })),
-    });
-  }
+      this.setState({
+        devices,
+        options: devices.map((d, i) => ({
+          label: d.label || `${this.props.kind} - ${i}`,
+          value: d.deviceId,
+          key: _.uniqueId('device-option-'),
+        })),
+      });
+    };
 
-  handleEnumerateDevicesError(error) {
-    log('error', error);
+    navigator.mediaDevices
+      .enumerateDevices()
+      .then(handleEnumerateDevicesSuccess);
   }
 
   handleSelectChange(event) {
@@ -61,7 +61,7 @@ class DeviceSelector extends Component {
   }
 
   render() {
-    const { kind, handleDeviceChange, ...props } = this.props;
+    const { kind, handleDeviceChange, className, ...props } = this.props;
     const { options, value } = this.state;
 
     return (
@@ -70,13 +70,13 @@ class DeviceSelector extends Component {
         value={value}
         onChange={this.handleSelectChange}
         disabled={!options.length}
-        className={styles.select}
+        className={cx(styles.select, className)}
       >
         {
           options.length ?
-            options.map((option, i) => (
+            options.map(option => (
               <option
-                key={i}
+                key={option.key}
                 value={option.value}
               >
                 {option.label}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..ffc72587bb2d4735ef46a0545c2fbcc38dc282f6
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/component.jsx
@@ -0,0 +1,64 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Button from '/imports/ui/components/button/component';
+import { defineMessages, intlShape, injectIntl } from 'react-intl';
+import styles from './styles';
+
+const intlMessages = defineMessages({
+  yes: {
+    id: 'app.audioModal.yes',
+    description: 'Hear yourself yes',
+  },
+  no: {
+    id: 'app.audioModal.no',
+    description: 'Hear yourself no',
+  },
+});
+
+const propTypes = {
+  handleYes: PropTypes.func.isRequired,
+  handleNo: PropTypes.func.isRequired,
+  intl: intlShape.isRequired,
+};
+
+class EchoTest extends Component {
+  constructor(props) {
+    super(props);
+
+    this.handleYes = props.handleYes.bind(this);
+    this.handleNo = props.handleNo.bind(this);
+  }
+
+  render() {
+    const {
+      intl,
+    } = this.props;
+
+    return (
+      <span>
+        <Button
+          className={styles.button}
+          label={intl.formatMessage(intlMessages.yes)}
+          icon={'thumbs_up'}
+          circle
+          color={'success'}
+          size={'jumbo'}
+          onClick={this.handleYes}
+        />
+        <Button
+          className={styles.button}
+          label={intl.formatMessage(intlMessages.no)}
+          icon={'thumbs_down'}
+          circle
+          color={'danger'}
+          size={'jumbo'}
+          onClick={this.handleNo}
+        />
+      </span>
+    );
+  }
+}
+
+export default injectIntl(EchoTest);
+
+EchoTest.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b196de52294feecebd641dbb0a58e310eee4ed47
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/audio/echo-test/styles.scss
@@ -0,0 +1,21 @@
+@import "/imports/ui/stylesheets/variables/_all";
+
+.button {
+  &:focus {
+    outline: none !important;
+  }
+
+  &:first-child {
+    margin-right: 3rem;
+
+    @include mq($small-only) {
+      margin-right: 1rem;
+    }
+  }
+
+  span:last-child {
+    color: black;
+    font-size: 1rem;
+    font-weight: 600;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/enter-audio/component.jsx
deleted file mode 100644
index fceea096a8adb822b117ea1345be759125d8479d..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/component.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { defineMessages, injectIntl } from 'react-intl';
-import Button from '/imports/ui/components/button/component';
-
-class EnterAudio extends React.Component {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    const {
-      intl,
-    } = this.props;
-
-    return (
-      <Button
-        label={intl.formatMessage(intlMessages.enterSessionLabel)}
-        size={'md'}
-        color={'primary'}
-        onClick={this.props.handleJoin}
-      />
-    );
-  }
-}
-
-const intlMessages = defineMessages({
-  enterSessionLabel: {
-    id: 'app.audio.enterSessionLabel',
-    description: 'enter session button label',
-  },
-});
-
-export default injectIntl(EnterAudio);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/enter-audio/container.jsx
deleted file mode 100644
index 30a84bea882d683c03c822b1ceca7cfdcfb5a564..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/enter-audio/container.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, { Component } from 'react';
-import { createContainer } from 'meteor/react-meteor-data';
-import AudioService from '../service';
-import { withModalMounter } from '/imports/ui/components/modal/service';
-import EnterAudio from './component';
-
-class EnterAudioContainer extends Component {
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    const {
-      isFullAudio,
-      mountModal,
-    } = this.props;
-
-    const handleJoin = () => {
-      mountModal(null);
-      return isFullAudio ? AudioService.joinMicrophone() : AudioService.joinListenOnly();
-    };
-
-    return (
-      <EnterAudio handleJoin={handleJoin} />
-    );
-  }
-}
-
-export default withModalMounter(EnterAudioContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/audio/join-audio/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/join-audio/component.jsx
deleted file mode 100644
index 10fc993dbd49c4805dc67fe95fd2408e74ac2ed7..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/audio/join-audio/component.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import styles from '../audio-modal/styles.scss';
-import Button from '/imports/ui/components/button/component';
-import { withModalMounter } from '/imports/ui/components/modal/service';
-import { defineMessages, injectIntl } from 'react-intl';
-
-const intlMessages = defineMessages({
-  microphoneLabel: {
-    id: 'app.audioModal.microphoneLabel',
-    description: 'Join mic audio button label',
-  },
-  listenOnlyLabel: {
-    id: 'app.audioModal.listenOnlyLabel',
-    description: 'Join listen only audio button label',
-  },
-  closeLabel: {
-    id: 'app.audioModal.closeLabel',
-    description: 'close audio modal button label',
-  },
-  audioChoiceLabel: {
-    id: 'app.audioModal.audioChoiceLabel',
-    description: 'Join audio modal title',
-  },
-});
-
-class JoinAudio extends React.Component {
-  constructor(props) {
-    super(props);
-
-    this.handleClose = this.handleClose.bind(this);
-    this.openAudio = this.openAudio.bind(this);
-    this.openListen = this.openListen.bind(this);
-  }
-
-  handleClose() {
-    /* TODO: Refactor this to the outer component (audio-modal/container) */
-    this.props.mountModal(null);
-  }
-
-  openAudio() {
-    this.props.changeMenu(this.props.AUDIO_SETTINGS);
-  }
-
-  openListen() {
-    this.handleClose();
-    this.props.handleJoinListenOnly();
-  }
-
-  render() {
-    const { intl } = this.props;
-    return (
-      <div>
-        <div className={styles.closeBtnWrapper}>
-          <Button
-            className={styles.closeBtn}
-            label={intl.formatMessage(intlMessages.closeLabel)}
-            icon={'close'}
-            size={'lg'}
-            hideLabel
-            onClick={this.handleClose}
-          />
-        </div>
-
-        <div className={styles.title}>
-          {intl.formatMessage(intlMessages.audioChoiceLabel)}
-        </div>
-        <div className={styles.center}>
-          <Button
-            className={styles.audioBtn}
-            label={intl.formatMessage(intlMessages.microphoneLabel)}
-            icon={'unmute'}
-            circle
-            size={'jumbo'}
-            onClick={this.openAudio}
-          />
-
-          <span className={styles.verticalLine} />
-          <Button
-            className={styles.audioBtn}
-            label={intl.formatMessage(intlMessages.listenOnlyLabel)}
-            icon={'listen'}
-            circle
-            size={'jumbo'}
-            onClick={this.openListen}
-          />
-        </div>
-      </div>
-    );
-  }
-}
-
-export default withModalMounter(injectIntl(JoinAudio));
diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js
index 8859320184b66d72e89d4facddf420b508ae3460..694d302f3f3a695861ea6045983257137de9ca56 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/service.js
+++ b/bigbluebutton-html5/imports/ui/components/audio/service.js
@@ -1,10 +1,12 @@
 import Users from '/imports/api/users';
 import Auth from '/imports/ui/services/auth';
-import AudioManager from '/imports/api/audio/client/manager';
+import AudioManager from '/imports/ui/services/audio-manager';
 import Meetings from '/imports/api/meetings';
 
-const init = () => {
+const init = (messages) => {
+  const meetingId = Auth.meetingID;
   const userId = Auth.userID;
+  const sessionToken = Auth.sessionToken;
   const User = Users.findOne({ userId });
   const username = User.name;
   const Meeting = Meetings.findOne({ meetingId: User.meetingId });
@@ -14,22 +16,34 @@ const init = () => {
   const microphoneLockEnforced = false;
 
   const userData = {
+    meetingId,
     userId,
+    sessionToken,
     username,
     voiceBridge,
     microphoneLockEnforced,
   };
 
-  AudioManager.init(userData);
+  AudioManager.init(userData, messages);
 };
 
-const exitAudio = () => AudioManager.exitAudio();
-const joinListenOnly = () => AudioManager.joinAudio(true);
-const joinMicrophone = () => AudioManager.joinAudio(false);
-
 export default {
   init,
-  exitAudio,
-  joinListenOnly,
-  joinMicrophone,
+  exitAudio: () => AudioManager.exitAudio(),
+  transferCall: () => AudioManager.transferCall(),
+  joinListenOnly: () => AudioManager.joinAudio({ isListenOnly: true }),
+  joinMicrophone: () => AudioManager.joinAudio(),
+  joinEchoTest: () => AudioManager.joinAudio({ isEchoTest: true }),
+  toggleMuteMicrophone: () => AudioManager.toggleMuteMicrophone(),
+  changeInputDevice: inputDeviceId => AudioManager.changeInputDevice(inputDeviceId),
+  changeOutputDevice: outputDeviceId => AudioManager.changeOutputDevice(outputDeviceId),
+  isConnected: () => AudioManager.isConnected,
+  isHangingUp: () => AudioManager.isHangingUp,
+  isMuted: () => AudioManager.isMuted,
+  isConnecting: () => AudioManager.isConnecting,
+  isListenOnly: () => AudioManager.isListenOnly,
+  inputDeviceId: () => AudioManager.inputDeviceId,
+  outputDeviceId: () => AudioManager.outputDeviceId,
+  isEchoTest: () => AudioManager.isEchoTest,
+  error: () => AudioManager.error,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss
index c10260a0a9d692ca892889de8fa09bf2827f85e4..7124c89d82e30f1a0d537b2695891098a3760ad5 100644
--- a/bigbluebutton-html5/imports/ui/components/button/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss
@@ -93,6 +93,21 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
   vertical-align: middle;
   background: none;
   padding: 0 !important;
+
+  &:active {
+    &:focus {
+      outline: thin dotted;
+      outline: 5px auto -webkit-focus-ring-color;
+      outline-offset: -2px;
+    }
+  }
+
+  &[aria-disabled="true"] {
+    cursor: not-allowed;
+    opacity: .65;
+    box-shadow: none;
+    pointer-events: none;
+  }
 }
 
 .label {
diff --git a/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss b/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss
index baa112c8891d90a11f886cbaf90b07e1a5f01186..b235b549c30f6275b39d295c3202f351cf21a812 100644
--- a/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/modal/simple/styles.scss
@@ -5,6 +5,7 @@
   display: flex;
   flex-direction: column;
   padding: ($line-height-computed / 2) $line-height-computed;
+  box-shadow : 0px 0px 15px rgba(0, 0, 0, 0.5);
 }
 
 .content {
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2480edb5b44bae70e1cdfaf9b2172610af188c62
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -0,0 +1,226 @@
+import { Tracker } from 'meteor/tracker';
+import { makeCall } from '/imports/ui/services/api';
+import VertoBridge from '/imports/api/audio/client/bridge/verto';
+import SIPBridge from '/imports/api/audio/client/bridge/sip';
+import { notify } from '/imports/ui/services/notification';
+
+const MEDIA = Meteor.settings.public.media;
+const USE_SIP = MEDIA.useSIPAudio;
+const ECHO_TEST_NUMBER = MEDIA.echoTestNumber;
+
+const CALL_STATES = {
+  STARTED: 'started',
+  ENDED: 'ended',
+  FAILED: 'failed',
+};
+
+class AudioManager {
+  constructor() {
+    this._inputDevice = {
+      tracker: new Tracker.Dependency(),
+    };
+
+    this.defineProperties({
+      isMuted: false,
+      isConnected: false,
+      isConnecting: false,
+      isHangingUp: false,
+      isListenOnly: false,
+      isEchoTest: false,
+      error: null,
+      outputDeviceId: null,
+    });
+  }
+
+  init(userData, messages) {
+    this.bridge = USE_SIP ? new SIPBridge(userData) : new VertoBridge(userData);
+    this.userData = userData;
+    this.messages = messages;
+  }
+
+  defineProperties(obj) {
+    Object.keys(obj).forEach((key) => {
+      const privateKey = `_${key}`;
+      this[privateKey] = {
+        value: obj[key],
+        tracker: new Tracker.Dependency(),
+      };
+
+      Object.defineProperty(this, key, {
+        set: (value) => {
+          this[privateKey].value = value;
+          this[privateKey].tracker.changed();
+        },
+        get: () => {
+          this[privateKey].tracker.depend();
+          return this[privateKey].value;
+        },
+      });
+    });
+  }
+
+  joinAudio(options = {}) {
+    const {
+      isListenOnly,
+      isEchoTest,
+    } = options;
+
+    if (!this.devicesInitialized) {
+      this.setDefaultInputDevice();
+      this.changeOutputDevice('default');
+      this.devicesInitialized = true;
+    }
+
+    this.isConnecting = true;
+    this.isMuted = false;
+    this.error = null;
+    this.isListenOnly = isListenOnly || false;
+    this.isEchoTest = isEchoTest || false;
+
+    const callOptions = {
+      isListenOnly: this.isListenOnly,
+      extension: isEchoTest ? ECHO_TEST_NUMBER : null,
+      inputStream: this.isListenOnly ? this.createListenOnlyStream() : this.inputStream,
+    };
+
+    return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
+  }
+
+  exitAudio() {
+    this.isHangingUp = true;
+    return this.bridge.exitAudio();
+  }
+
+  transferCall() {
+    this.onTransferStart();
+    return this.bridge.transferCall(this.onAudioJoin.bind(this));
+  }
+
+  toggleMuteMicrophone() {
+    makeCall('toggleSelfVoice').then(() => {
+      this.onToggleMicrophoneMute();
+    });
+  }
+
+  onAudioJoin() {
+    this.isConnecting = false;
+    this.isConnected = true;
+
+    if (!this.isEchoTest) {
+      this.notify(this.messages.info.JOINED_AUDIO);
+    }
+  }
+
+  onTransferStart() {
+    this.isEchoTest = false;
+    this.isConnecting = true;
+  }
+
+  onAudioExit() {
+    this.isConnected = false;
+    this.isConnecting = false;
+    this.isHangingUp = false;
+
+
+    if (!this.error && !this.isEchoTest) {
+      this.notify(this.messages.info.LEFT_AUDIO);
+    }
+    this.isEchoTest = false;
+  }
+
+  onToggleMicrophoneMute() {
+    this.isMuted = !this.isMuted;
+  }
+
+  callStateCallback(response) {
+    return new Promise((resolve) => {
+      const {
+        STARTED,
+        ENDED,
+        FAILED,
+      } = CALL_STATES;
+
+      const {
+        status,
+        error,
+        bridgeError,
+      } = response;
+
+      if (status === STARTED) {
+        this.onAudioJoin();
+        resolve(STARTED);
+      } else if (status === ENDED) {
+        this.onAudioExit();
+      } else if (status === FAILED) {
+        this.error = error;
+        this.notify(this.messages.error[error]);
+        console.error('Audio Error:', error, bridgeError);
+        this.onAudioExit();
+      }
+    });
+  }
+
+  createListenOnlyStream() {
+    if (this.listenOnlyAudioContext) {
+      this.listenOnlyAudioContext.close();
+    }
+
+    this.listenOnlyAudioContext = window.AudioContext ?
+                                  new window.AudioContext() :
+                                  new window.webkitAudioContext();
+
+    return this.listenOnlyAudioContext.createMediaStreamDestination().stream;
+  }
+
+  setDefaultInputDevice() {
+    this.changeInputDevice();
+  }
+
+  async changeInputDevice(deviceId) {
+    try {
+      if (!deviceId) {
+        this.inputDevice = await await this.bridge.setDefaultInputDevice();
+        return;
+      }
+      this.inputDevice = await this.bridge.changeInputDevice(deviceId);
+    } catch(err) {
+      this.error = err;
+      this.notify('There was a problem getting the media devices');
+    }
+  }
+
+  async changeOutputDevice(deviceId) {
+    this.outputDeviceId = await this.bridge.changeOutputDevice(deviceId);
+  }
+
+  set inputDevice(value) {
+    Object.assign(this._inputDevice, value);
+    this._inputDevice.tracker.changed();
+  }
+
+  get inputStream() {
+    return this._inputDevice.stream;
+  }
+
+  get inputDeviceId() {
+    this._inputDevice.tracker.depend();
+    return this._inputDevice.id;
+  }
+
+  set userData(value) {
+    this._userData = value;
+  }
+
+  get userData() {
+    return this._userData;
+  }
+
+  notify(message) {
+    notify(message,
+           this.error ? 'error' : 'info',
+           this.isListenOnly ? 'audio_on' : 'unmute');
+  }
+}
+
+const audioManager = new AudioManager();
+export default audioManager;
diff --git a/bigbluebutton-html5/imports/ui/services/user/mapUser.js b/bigbluebutton-html5/imports/ui/services/user/mapUser.js
index 9685a7c60addc8fc8a7daaae8658caef83779376..2acaf92df655c48eb4d0540b6764bb47525500cd 100644
--- a/bigbluebutton-html5/imports/ui/services/user/mapUser.js
+++ b/bigbluebutton-html5/imports/ui/services/user/mapUser.js
@@ -22,7 +22,7 @@ const mapUser = (user) => {
     isPresenter: user.presenter,
     isModerator: user.role === ROLE_MODERATOR,
     isCurrent: user.userId === userId,
-    isVoiceUser: voiceUser.joined,
+    isVoiceUser: voiceUser ? voiceUser.joined : false,
     isMuted: voiceUser ? voiceUser.muted : false,
     isTalking: voiceUser ? voiceUser.talking : false,
     isListenOnly: voiceUser ? voiceUser.listenOnly : false,
diff --git a/bigbluebutton-html5/private/config/public/media.yaml b/bigbluebutton-html5/private/config/public/media.yaml
index 61f690757456087cc840c0fa3a19ea2d223f7468..23329f1e456b1db739173b5a88a89193635af40d 100644
--- a/bigbluebutton-html5/private/config/public/media.yaml
+++ b/bigbluebutton-html5/private/config/public/media.yaml
@@ -8,3 +8,9 @@ media:
   vertoPort: "8082"
   # specifies whether to use SIP.js for audio over mod_verto
   useSIPAudio: true
+  stunTurnServersFetchAddress: '/bigbluebutton/api/stuns'
+  mediaTag: '#remote-media'
+  callTransferTimeout: 5000
+  callHangupTimeout: 2000
+  callHangupMaximumRetries: 10
+  echoTestNumber: '9196'
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 67c053ab7d9129f42afe01572b8b9c201ef84cef..ceba85eaa22145f9153873464e7fb40e97c2c60a 100644
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -202,6 +202,20 @@
     "app.audioModal.audioChoiceLabel": "How would you like to join the audio?",
     "app.audioModal.audioChoiceDesc": "Select how to join the audio in this meeting",
     "app.audioModal.closeLabel": "Close",
+    "app.audioModal.yes": "Yes",
+    "app.audioModal.no": "No",
+    "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?",
+    "app.audioModal.settingsTitle": "Change your audio settings",
+    "app.audioModal.connecting": "Connecting",
+    "app.audioModal.connectingEchoTest": "Connecting to echo test",
+    "app.audioManager.joinedAudio": "You have joined the audio conference",
+    "app.audioManager.joinedEcho": "You have joined the echo test",
+    "app.audioManager.leftAudio": "You have left the audio conference",
+    "app.audioManager.genericError": "Error: An error has occurred, please try again",
+    "app.audioManager.connectionError": "Error: Connection error",
+    "app.audioManager.requestTimeout": "Error: There was a timeout in the request",
+    "app.audioManager.invalidTarget": "Error: Tried to request something to an invalid target",
+    "app.audioManager.mediaError": "Error: There was an error getting your media devices",
     "app.audio.joinAudio": "Join Audio",
     "app.audio.leaveAudio": "Leave Audio",
     "app.audio.enterSessionLabel": "Enter Session",
@@ -212,6 +226,7 @@
     "app.audio.audioSettings.microphoneSourceLabel": "Microphone source",
     "app.audio.audioSettings.speakerSourceLabel": "Speaker source",
     "app.audio.audioSettings.microphoneStreamLabel": "Your audio stream volume",
+    "app.audio.audioSettings.retryLabel": "Retry",
     "app.audio.listenOnly.backLabel": "Back",
     "app.audio.listenOnly.closeLabel": "Close",
     "app.error.kicked": "You have been kicked out of the meeting",
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 2cb809e165f82385eb00dab17cb51f8a6d5d1f97..9b988bf241b00a5f6c54264d502d68f1adc7e7f6 100755
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -156,7 +156,7 @@ webcamsOnlyForModerator=false
 #----------------------------------------------------
 # This URL is where the BBB client is accessible. When a user sucessfully
 # enters a name and password, she is redirected here to load the client.
-bigbluebutton.web.serverURL=https://ritz-ss.blindside-dev.com
+bigbluebutton.web.serverURL=http://192.168.246.131
 
 
 #----------------------------------------------------