diff --git a/akka-bbb-apps/.dockerignore b/akka-bbb-apps/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..c9c56767351f2752f2fad6e75af920655d3efa8d
--- /dev/null
+++ b/akka-bbb-apps/.dockerignore
@@ -0,0 +1,2 @@
+Dockerfile
+
diff --git a/akka-bbb-apps/Dockerfile b/akka-bbb-apps/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..e8adc296c1cbe6f283c5c44dea86fe2a01a90019
--- /dev/null
+++ b/akka-bbb-apps/Dockerfile
@@ -0,0 +1,24 @@
+FROM bbb-common-message AS builder
+
+ARG COMMON_VERSION=0.0.1-SNAPSHOT
+
+COPY . /source
+
+RUN cd /source \
+ && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
+ && sbt compile
+
+RUN apt-get update \
+ && apt-get -y install fakeroot
+
+RUN cd /source \
+ && sbt debian:packageBin
+
+# FROM ubuntu:16.04
+FROM openjdk:8-jre-slim-stretch
+
+COPY --from=builder /source/target/*.deb /root/
+
+RUN dpkg -i /root/*.deb
+
+CMD ["/usr/share/bbb-apps-akka/bin/bbb-apps-akka"]
diff --git a/akka-bbb-apps/src/main/resources/application.conf b/akka-bbb-apps/src/main/resources/application.conf
index dfcf1bb5826282edcba0838cd189562417d1377d..e6904f7e9d99b9aae44fb0428072f15ebef56a5c 100755
--- a/akka-bbb-apps/src/main/resources/application.conf
+++ b/akka-bbb-apps/src/main/resources/application.conf
@@ -86,3 +86,8 @@ voiceConf {
 recording {
   chapterBreakLengthInMinutes = 180
 }
+
+whiteboard {
+  multiUserDefault = false
+}
+
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala
index 131336f55193023b5c7f75f2b9ae07102e8ef116..d2986d262a88d84363f7870e563f11d981312a6f 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala
@@ -63,4 +63,6 @@ trait SystemConfiguration {
 
   lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false)
   lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2)
+  lazy val multiUserWhiteboardDefault = Try(config.getBoolean("whiteboard.multiUserDefault")).getOrElse(false)
 }
+
diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala
index a3c2b8e1f231c98069d110b84b770fc21088a0d7..f73d74a4e5537e794c42c7ce3635d841c1adf4d8 100755
--- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala
+++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala
@@ -7,8 +7,9 @@ import scala.collection.immutable.HashMap
 import scala.collection.JavaConverters._
 import org.bigbluebutton.common2.msgs.AnnotationVO
 import org.bigbluebutton.core.apps.whiteboard.Whiteboard
+import org.bigbluebutton.SystemConfiguration
 
-class WhiteboardModel {
+class WhiteboardModel extends SystemConfiguration {
   private var _whiteboards = new HashMap[String, Whiteboard]()
 
   private def saveWhiteboard(wb: Whiteboard) {
@@ -24,7 +25,7 @@ class WhiteboardModel {
   }
 
   private def createWhiteboard(wbId: String): Whiteboard = {
-    new Whiteboard(wbId, false, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]())
+    new Whiteboard(wbId, multiUserWhiteboardDefault, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]())
   }
 
   private def getAnnotationsByUserId(wb: Whiteboard, id: String): List[AnnotationVO] = {
diff --git a/akka-bbb-fsesl/Dockerfile b/akka-bbb-fsesl/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..ff06041d93c9a190dfd4b85ae11368ae5f76ad3b
--- /dev/null
+++ b/akka-bbb-fsesl/Dockerfile
@@ -0,0 +1,26 @@
+FROM bbb-fsesl-client AS builder
+
+ARG COMMON_VERSION=0.0.1-SNAPSHOT
+
+COPY . /source
+
+RUN cd /source \
+ && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
+ && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-fsesl-client[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
+ && sbt compile
+
+RUN apt-get update \
+ && apt-get -y install fakeroot
+
+RUN cd /source \
+ && sbt debian:packageBin
+
+FROM openjdk:8-jre-slim-stretch
+
+COPY --from=builder /source/target/*.deb /root/
+
+RUN dpkg -i /root/*.deb
+
+COPY wait-for-it.sh /usr/local/bin/
+
+CMD ["/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"]
diff --git a/akka-bbb-fsesl/wait-for-it.sh b/akka-bbb-fsesl/wait-for-it.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bbe404324bce73f9ca9999dd2d6936cf90e5c184
--- /dev/null
+++ b/akka-bbb-fsesl/wait-for-it.sh
@@ -0,0 +1,177 @@
+#!/usr/bin/env bash
+#   Use this script to test if a given TCP host/port are available
+
+cmdname=$(basename $0)
+
+echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+    cat << USAGE >&2
+Usage:
+    $cmdname host:port [-s] [-t timeout] [-- command args]
+    -h HOST | --host=HOST       Host or IP under test
+    -p PORT | --port=PORT       TCP port under test
+                                Alternatively, you specify the host and port as host:port
+    -s | --strict               Only execute subcommand if the test succeeds
+    -q | --quiet                Don't output any status messages
+    -t TIMEOUT | --timeout=TIMEOUT
+                                Timeout in seconds, zero for no timeout
+    -- COMMAND ARGS             Execute command with args after the test finishes
+USAGE
+    exit 1
+}
+
+wait_for()
+{
+    if [[ $TIMEOUT -gt 0 ]]; then
+        echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT"
+    else
+        echoerr "$cmdname: waiting for $HOST:$PORT without a timeout"
+    fi
+    start_ts=$(date +%s)
+    while :
+    do
+        if [[ $ISBUSY -eq 1 ]]; then
+            nc -z $HOST $PORT
+            result=$?
+        else
+            (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1
+            result=$?
+        fi
+        if [[ $result -eq 0 ]]; then
+            end_ts=$(date +%s)
+            echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds"
+            break
+        fi
+        sleep 1
+    done
+    return $result
+}
+
+wait_for_wrapper()
+{
+    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+    if [[ $QUIET -eq 1 ]]; then
+        timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    else
+        timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT &
+    fi
+    PID=$!
+    trap "kill -INT -$PID" INT
+    wait $PID
+    RESULT=$?
+    if [[ $RESULT -ne 0 ]]; then
+        echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT"
+    fi
+    return $RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+    case "$1" in
+        *:* )
+        hostport=(${1//:/ })
+        HOST=${hostport[0]}
+        PORT=${hostport[1]}
+        shift 1
+        ;;
+        --child)
+        CHILD=1
+        shift 1
+        ;;
+        -q | --quiet)
+        QUIET=1
+        shift 1
+        ;;
+        -s | --strict)
+        STRICT=1
+        shift 1
+        ;;
+        -h)
+        HOST="$2"
+        if [[ $HOST == "" ]]; then break; fi
+        shift 2
+        ;;
+        --host=*)
+        HOST="${1#*=}"
+        shift 1
+        ;;
+        -p)
+        PORT="$2"
+        if [[ $PORT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --port=*)
+        PORT="${1#*=}"
+        shift 1
+        ;;
+        -t)
+        TIMEOUT="$2"
+        if [[ $TIMEOUT == "" ]]; then break; fi
+        shift 2
+        ;;
+        --timeout=*)
+        TIMEOUT="${1#*=}"
+        shift 1
+        ;;
+        --)
+        shift
+        CLI=("$@")
+        break
+        ;;
+        --help)
+        usage
+        ;;
+        *)
+        echoerr "Unknown argument: $1"
+        usage
+        ;;
+    esac
+done
+
+if [[ "$HOST" == "" || "$PORT" == "" ]]; then
+    echoerr "Error: you need to provide a host and port to test."
+    usage
+fi
+
+TIMEOUT=${TIMEOUT:-15}
+STRICT=${STRICT:-0}
+CHILD=${CHILD:-0}
+QUIET=${QUIET:-0}
+
+# check to see if timeout is from busybox?
+# check to see if timeout is from busybox?
+TIMEOUT_PATH=$(realpath $(which timeout))
+if [[ $TIMEOUT_PATH =~ "busybox" ]]; then
+        ISBUSY=1
+        BUSYTIMEFLAG="-t"
+else
+        ISBUSY=0
+        BUSYTIMEFLAG=""
+fi
+
+if [[ $CHILD -gt 0 ]]; then
+    wait_for
+    RESULT=$?
+    exit $RESULT
+else
+    if [[ $TIMEOUT -gt 0 ]]; then
+        wait_for_wrapper
+        RESULT=$?
+    else
+        wait_for
+        RESULT=$?
+    fi
+fi
+
+if [[ $CLI != "" ]]; then
+    if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then
+        echoerr "$cmdname: strict mode, refusing to execute subprocess"
+        exit $RESULT
+    fi
+    exec "${CLI[@]}"
+else
+    exit $RESULT
+fi
diff --git a/bbb-common-message/Dockerfile b/bbb-common-message/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..223159915350214fdaf6c9f9493bf52c6f7e9580
--- /dev/null
+++ b/bbb-common-message/Dockerfile
@@ -0,0 +1,13 @@
+FROM sbt:0.13.8
+
+ARG COMMON_VERSION
+
+COPY . /bbb-common-message
+
+RUN cd /bbb-common-message \
+ && sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
+ && echo 'publishTo := Some(Resolver.file("file",  new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
+ && sbt compile \
+ && sbt publish \
+ && sbt publishLocal
+
diff --git a/bbb-common-web/Dockerfile b/bbb-common-web/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..874219a3ba36e6b2ef2ed34321cdaa7734155c4f
--- /dev/null
+++ b/bbb-common-web/Dockerfile
@@ -0,0 +1,13 @@
+FROM bbb-common-message
+
+ARG COMMON_VERSION
+
+COPY . /bbb-common-web
+
+RUN cd /bbb-common-web \
+ && sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
+ && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
+ && echo 'publishTo := Some(Resolver.file("file",  new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
+ && sbt compile \
+ && sbt publish \
+ && sbt publishLocal
diff --git a/bbb-fsesl-client/Dockerfile b/bbb-fsesl-client/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..c9fd93a8417e592f09e68ec1e258e0b16d139279
--- /dev/null
+++ b/bbb-fsesl-client/Dockerfile
@@ -0,0 +1,13 @@
+FROM bbb-common-message
+
+ARG COMMON_VERSION
+
+COPY . /bbb-fsesl-client
+
+RUN cd /bbb-fsesl-client \
+ && sed -i "s|\(version := \)\".*|\1\"$COMMON_VERSION\"|g" build.sbt \
+ && find -name build.sbt -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^\"]*\"[ ]*%[ ]*\)\"[^\"]*\"\(.*\)|\1\"$COMMON_VERSION\"\2|g" {} \; \
+ && echo 'publishTo := Some(Resolver.file("file",  new File(Path.userHome.absolutePath+"/.m2/repository")))' | tee -a build.sbt \
+ && sbt compile \
+ && sbt publish \
+ && sbt publishLocal
diff --git a/bbb-lti/.dockerignore b/bbb-lti/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a
--- /dev/null
+++ b/bbb-lti/.dockerignore
@@ -0,0 +1 @@
+Dockerfile
diff --git a/bbb-lti/Dockerfile b/bbb-lti/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..4503f2d172e525085026332f16b97e38b46f4a2c
--- /dev/null
+++ b/bbb-lti/Dockerfile
@@ -0,0 +1,52 @@
+FROM java:8-jdk AS builder
+
+RUN mkdir -p /root/tools \
+ && cd /root/tools \
+ && wget http://services.gradle.org/distributions/gradle-2.12-bin.zip \
+ && unzip gradle-2.12-bin.zip \
+ && ln -s gradle-2.12 gradle
+
+RUN mkdir -p /root/tools \
+ && cd /root/tools \
+ && wget https://github.com/grails/grails-core/releases/download/v2.5.2/grails-2.5.2.zip \
+ && unzip grails-2.5.2.zip \
+ && ln -s grails-2.5.2 grails
+
+ENV PATH="/root/tools/gradle/bin:/root/tools/grails/bin:${PATH}"
+
+COPY . /source
+
+# build with:
+# docker build -t mconftec/bbb-lti --build-arg title=Mconf --build-arg description='Single Sign On into Mconf' --build-arg vendor_code=mconf --build-arg vendor_name=Mconf --build-arg vendor_description='Mconf web conferencing' --build-arg vendor_url=https://mconf.com .
+
+ARG title=BigBlueButton
+ARG description='Single Sign On into BigBlueButton'
+ARG vendor_code=bigbluebutton
+ARG vendor_name=BigBlueButton
+ARG vendor_description='Open source web conferencing system for distance learning.'
+ARG vendor_url=http://www.bigbluebutton.org/
+
+RUN cd /source \
+ && sed -i "s|\(<blti:title>\)[^<]*|\1$title|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \
+ && sed -i "s|\(<blti:description>\)[^<]*|\1$description|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \
+ && sed -i "s|\(<lticp:code>\)[^<]*|\1$vendor_code|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \
+ && sed -i "s|\(<lticp:name>\)[^<]*|\1$vendor_name|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \
+ && sed -i "s|\(<lticp:description>\)[^<]*|\1$vendor_description|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \
+ && sed -i "s|\(<lticp:url>\)[^<]*|\1$vendor_url|g" grails-app/controllers/org/bigbluebutton/ToolController.groovy \
+ && grails war
+
+FROM tomcat:7-jre8
+
+WORKDIR $CATALINA_HOME
+
+# clean default webapps
+RUN rm -r webapps/*
+
+COPY --from=builder /source/target/lti-*.war webapps/lti.war
+
+RUN unzip -q webapps/lti.war -d webapps/lti \
+ && rm webapps/lti.war
+
+COPY docker-entrypoint.sh /usr/local/bin/
+
+CMD ["docker-entrypoint.sh"]
diff --git a/bbb-lti/docker-entrypoint.sh b/bbb-lti/docker-entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..608af0aa7ad42fa92ec1d1b73f85021484974b78
--- /dev/null
+++ b/bbb-lti/docker-entrypoint.sh
@@ -0,0 +1,7 @@
+#!/bin/bash -xe
+
+export JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -DbigbluebuttonSalt=$BIGBLUEBUTTON_SHARED_SECRET -DbigbluebuttonURL=$BIGBLUEBUTTON_URL -DltiEndPoint=$LTI_ENDPOINT -DltiConsumers=$LTI_CONSUMERS -DltiAllRecordedByDefault=$RECORDED_BY_DEFAULT"
+sed -i "s|^securerandom\.source=.*|securerandom.source=file:/dev/./urandom|g" $JAVA_HOME/lib/security/java.security
+
+catalina.sh run
+
diff --git a/bbb-webhooks/Dockerfile b/bbb-webhooks/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..afa9470fdc7330c434f6433c62ce1179b7dd3042
--- /dev/null
+++ b/bbb-webhooks/Dockerfile
@@ -0,0 +1,16 @@
+FROM node:8
+
+ADD . app
+
+WORKDIR app
+
+RUN cp config_local.js.example config_local.js
+
+ENV NODE_ENV production
+
+RUN npm install \
+ && npm cache clear --force
+
+EXPOSE 3005
+
+CMD ["node", "app.js"]
diff --git a/bbb-webhooks/application.js b/bbb-webhooks/application.js
index 2817826712746cc2f49e2819e2c2482b84d56e79..b6e9985b1254bf729a2d226204d680600ead0903 100644
--- a/bbb-webhooks/application.js
+++ b/bbb-webhooks/application.js
@@ -13,8 +13,12 @@ const async = require("async");
 module.exports = class Application {
 
   constructor() {
-    config.redis.pubSubClient = redis.createClient();
-    config.redis.client = redis.createClient()
+    const options = {
+      host : process.env.REDIS_HOST || config.redis.host,
+      port : process.env.REDIS_PORT || config.redis.port
+    };
+    config.redis.pubSubClient = redis.createClient(options);
+    config.redis.client = redis.createClient(options);
     this.webHooks = new WebHooks();
     this.webServer = new WebServer();
   }
diff --git a/bbb-webhooks/callback_emitter.js b/bbb-webhooks/callback_emitter.js
index 11416774cb1461cb5a37009d868dc497f13c7ce2..6ec7d553632c42d0c4a71da85bfb371cd80c59c6 100644
--- a/bbb-webhooks/callback_emitter.js
+++ b/bbb-webhooks/callback_emitter.js
@@ -62,11 +62,19 @@ module.exports = class CallbackEmitter extends EventEmitter {
 
   _emitMessage(callback) {
     let data,requestOptions;
+    const serverDomain = process.env.SERVER_DOMAIN || config.bbb.serverDomain;
+    const sharedSecret = process.env.SHARED_SECRET || config.bbb.sharedSecret;
+    const bearerAuth = process.env.BEARER_AUTH || config.bbb.auth2_0;
+
+    // data to be sent
+    // note: keep keys in alphabetical order
+    data = {
+      event: "[" + this.message + "]",
+      timestamp: this.timestamp,
+      domain: serverDomain
+    };
 
-    if (config.bbb.auth2_0) {
-      // Send data as a JSON
-      data = "[" + this.message + "]";
-
+    if (bearerAuth) {
       const callbackURL = this.callbackURL;
 
       requestOptions = {
@@ -76,20 +84,13 @@ module.exports = class CallbackEmitter extends EventEmitter {
         method: "POST",
         form: data,
         auth: {
-          bearer: config.bbb.sharedSecret
+          bearer: sharedSecret
         }
       };
     }
     else {
-      // data to be sent
-      // note: keep keys in alphabetical order
-      data = {
-        event: "[" + this.message + "]",
-        timestamp: this.timestamp
-      };
-
       // calculate the checksum
-      const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${config.bbb.sharedSecret}`);
+      const checksum = Utils.checksum(`${this.callbackURL}${JSON.stringify(data)}${sharedSecret}`);
 
       // get the final callback URL, including the checksum
       const urlObj = url.parse(this.callbackURL, true);
diff --git a/bbb-webhooks/config.js b/bbb-webhooks/config.js
index 80845f1fc9183e360f06b21d470aa922442451df..cfc7ccda07e0a8f5f33c3f695da7c6e9d274d9b9 100644
--- a/bbb-webhooks/config.js
+++ b/bbb-webhooks/config.js
@@ -51,6 +51,8 @@ config.mappings.timeout = 1000*60*60*24; // 24 hours, in ms
 
 // Redis
 config.redis = {};
+config.redis.host = '127.0.0.1';
+config.redis.port = 6379;
 config.redis.keys = {};
 config.redis.keys.hook = id => `bigbluebutton:webhooks:hook:${id}`;
 config.redis.keys.hooks = "bigbluebutton:webhooks:hooks";
diff --git a/bbb-webhooks/config/monit-bbb-webhooks b/bbb-webhooks/config/monit-bbb-webhooks
deleted file mode 100755
index 18e1f1e445dfa9f30f84907d7d8312e48f0bcb91..0000000000000000000000000000000000000000
--- a/bbb-webhooks/config/monit-bbb-webhooks
+++ /dev/null
@@ -1,12 +0,0 @@
-#!monit
-set logfile /var/log/monit.log
-
-check process bbb-webhooks with pidfile "/var/run/bbb-webhooks.pid"
-    start program = "/sbin/start bbb-webhooks"
-    stop program  = "/sbin/stop bbb-webhooks"
-
-    if failed port 3005 protocol HTTP
-        request /bigbluebutton/api/hooks/ping
-        with timeout 30 seconds
-        then restart
-    # if 5 restarts within 5 cycles then timeout
diff --git a/bbb-webhooks/config/upstart-bbb-webhooks.conf b/bbb-webhooks/config/upstart-bbb-webhooks.conf
deleted file mode 100644
index af2e5069340f1650edcc42dede5ece593d76d1e1..0000000000000000000000000000000000000000
--- a/bbb-webhooks/config/upstart-bbb-webhooks.conf
+++ /dev/null
@@ -1,34 +0,0 @@
-# bbb-webhooks
-
-description "bbb-webhooks"
-author "BigBlueButton"
-
-start on (local-filesystems and net-device-up IFACE=eth3)
-stop on shutdown
-
-# respawn # we're using monit for it
-
-env USER=firstuser
-env APP=app.js
-env CMD_OPTS=""
-env SRC_DIR="/usr/local/bigbluebutton/bbb-webhooks"
-env LOGFILE="/var/log/bbb-webhooks.log"
-env NODE=/usr/local/bin/node
-env PIDFILE="/var/run/bbb-webhooks.pid"
-env NODE_ENV="production"
-
-script
-  cd $SRC_DIR
-  echo $$ > $PIDFILE
-  exec sudo -u $USER NODE_ENV=$NODE_ENV $NODE $APP $CMD_OPTS 1>> $LOGFILE 2>> $LOGFILE
-end script
-
-pre-start script
-    # Date format same as (new Date()).toISOString() for consistency
-    echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Starting" >> $LOGFILE
-end script
-
-pre-stop script
-    rm $PIDFILE
-    echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> $LOGFILE
-end script
diff --git a/bbb-webhooks/config_local.js.example b/bbb-webhooks/config_local.js.example
index 0a64ac0d5fc504b12c270cead9d4cd99d0e4f7ea..0bebd13924dafc0e7f86e9cfb5d84ebe8a390551 100644
--- a/bbb-webhooks/config_local.js.example
+++ b/bbb-webhooks/config_local.js.example
@@ -4,6 +4,7 @@ const config = {};
 
 // Shared secret of your BigBlueButton server.
 config.bbb = {};
+config.bbb.serverDomain = "myserver.com";
 config.bbb.sharedSecret = "mysharedsecret";
 // Whether to use Auth2.0 or not, Auth2.0 sends the sharedSecret whithin an Authorization header as a bearer
 config.bbb.auth2_0 = false;
diff --git a/bbb-webhooks/extra/events.js b/bbb-webhooks/extra/events.js
index 9abaf828d5622b2b69aed49f16e58bd5df8f5add..c527879ac95f1d8062087c1ecedfbb9581ea38a9 100644
--- a/bbb-webhooks/extra/events.js
+++ b/bbb-webhooks/extra/events.js
@@ -6,7 +6,7 @@ const redis = require("redis");
 const config = require('../config.js');
 var target_meeting = null;
 var events_printed = [];
-var subscriber = redis.createClient();
+var subscriber = redis.createClient(process.env.REDIS_PORT || config.redis.port, process.env.REDIS_HOST || config.redis.host);
 
 subscriber.on("psubscribe", function(channel, count) {
   console.log("subscribed to " + channel);
diff --git a/bbb-webhooks/messageMapping.js b/bbb-webhooks/messageMapping.js
index f79d9410b932661866acf0bdb5ebfdf933a68a4b..7ea2062c8a022894279ad9f4f15c20f82a4aac7e 100644
--- a/bbb-webhooks/messageMapping.js
+++ b/bbb-webhooks/messageMapping.js
@@ -7,8 +7,8 @@ module.exports = class MessageMapping {
   constructor() {
     this.mappedObject = {};
     this.mappedMessage = {};
-    this.meetingEvents = ["MeetingCreatedEvtMsg","MeetingDestroyedEvtMsg"];
-    this.userEvents = ["UserJoinedMeetingEvtMsg","UserLeftMeetingEvtMsg","UserJoinedVoiceConfToClientEvtMsg","UserLeftVoiceConfToClientEvtMsg","PresenterAssignedEvtMsg", "PresenterUnassignedEvtMsg"];
+    this.meetingEvents = ["MeetingCreatedEvtMsg","MeetingDestroyedEvtMsg", "ScreenshareRtmpBroadcastStartedEvtMsg", "ScreenshareRtmpBroadcastStoppedEvtMsg", "SetCurrentPresentationEvtMsg", "RecordingStatusChangedEvtMsg"];
+    this.userEvents = ["UserJoinedMeetingEvtMsg","UserLeftMeetingEvtMsg","UserJoinedVoiceConfToClientEvtMsg","UserLeftVoiceConfToClientEvtMsg","PresenterAssignedEvtMsg", "PresenterUnassignedEvtMsg", "UserBroadcastCamStartedEvtMsg", "UserBroadcastCamStoppedEvtMsg", "UserEmojiChangedEvtMsg"];
     this.chatEvents = ["SendPublicMessageEvtMsg","SendPrivateMessageEvtMsg"];
     this.rapEvents = ["archive_started","archive_ended","sanity_started","sanity_ended","post_archive_started","post_archive_ended","process_started","process_ended","post_process_started","post_process_ended","publish_started","publish_ended","post_publish_started","post_publish_ended"];
   }
@@ -41,13 +41,14 @@ module.exports = class MessageMapping {
   // Map internal to external message for meeting information
   meetingTemplate(messageObj) {
     const props = messageObj.core.body.props;
+    const meetingId = messageObj.core.body.meetingId || messageObj.core.header.meetingId;
     this.mappedObject.data = {
       "type": "event",
       "id": this.mapInternalMessage(messageObj),
       "attributes":{
         "meeting":{
-          "internal-meeting-id": messageObj.core.body.meetingId,
-          "external-meeting-id": IDMapping.getExternalMeetingID(messageObj.core.body.meetingId)
+          "internal-meeting-id": meetingId,
+          "external-meeting-id": IDMapping.getExternalMeetingID(meetingId)
         }
       },
       "event":{
@@ -82,7 +83,7 @@ module.exports = class MessageMapping {
   userTemplate(messageObj) {
     const msgBody = messageObj.core.body;
     const msgHeader = messageObj.core.header;
-    const extId = UserMapping.getExternalUserID(msgHeader.userId) ? UserMapping.getExternalUserID(msgHeader.userId) : msgBody.extId;
+    const extId = UserMapping.getExternalUserID(msgHeader.userId) || msgBody.extId || "";
     this.mappedObject.data = {
       "type": "event",
       "id": this.mapInternalMessage(messageObj),
@@ -93,7 +94,7 @@ module.exports = class MessageMapping {
         },
         "user":{
           "internal-user-id": msgHeader.userId,
-          "external-user-id": extId ? extId : "",
+          "external-user-id": extId,
           "sharing-mic": msgBody.muted,
           "name": msgBody.name,
           "role": msgBody.role,
@@ -146,31 +147,36 @@ module.exports = class MessageMapping {
   }
 
   rapTemplate(messageObj) {
-    data = messageObj.payload
+    const data = messageObj.payload;
     this.mappedObject.data = {
       "type": "event",
       "id": this.mapInternalMessage(messageObj.header.name),
       "attributes": {
         "meeting": {
           "internal-meeting-id": data.meeting_id,
-          "external-meeting-id": IDMapping.getExternalMeetingID(data.meeting_id)
+          "external-meeting-id": data.external_meeting_id
         },
-        "recording": {
-          "name": data.metadata.meetingName,
-          "isBreakout": data.metadata.isBreakout,
-          "startTime": data.startTime,
-          "endTime": data.endTime,
-          "size": data.playback.size,
-          "rawSize": data.rawSize,
-          "metadata": data.metadata,
-          "playback": data.playback,
-          "download": data.download
-        }
+        "success": data.success,
+        "step-time": data.step_time
       },
       "event": {
         "ts": messageObj.header.current_time
       }
     };
+
+    if (this.mappedObject.data["id"] == "rap-publish-ended") {
+      this.mappedObject.data["attributes"]["recording"] = {
+        "name": data.metadata.meetingName,
+        "isBreakout": data.metadata.isBreakout,
+        "startTime": data.startTime,
+        "endTime": data.endTime,
+        "size": data.playback.size,
+        "rawSize": data.rawSize,
+        "metadata": data.metadata,
+        "playback": data.playback,
+        "download": data.download
+      }
+    }
     this.mappedMessage = JSON.stringify(this.mappedObject);
     Logger.info("[MessageMapping] Mapped message:", this.mappedMessage);
   }
@@ -186,6 +192,10 @@ module.exports = class MessageMapping {
     const mappedMsg = (() => { switch (message) {
       case "MeetingCreatedEvtMsg": return "meeting-created";
       case "MeetingDestroyedEvtMsg": return "meeting-ended";
+      case "RecordingStatusChangedEvtMsg": return "meeting-recording-changed";
+      case "ScreenshareRtmpBroadcastStartedEvtMsg": return "meeting-screenshare-started";
+      case "ScreenshareRtmpBroadcastStoppedEvtMsg": return "meeting-screenshare-stopped";
+      case "SetCurrentPresentationEvtMsg": return "meeting-presentation-changed";
       case "UserJoinedMeetingEvtMsg": return "user-joined";
       case "UserLeftMeetingEvtMsg": return "user-left";
       case "UserJoinedVoiceConfToClientEvtMsg": return "user-audio-voice-enabled";
@@ -193,7 +203,8 @@ module.exports = class MessageMapping {
       case "UserBroadcastCamStartedEvtMsg": return "user-cam-broadcast-start";
       case "UserBroadcastCamStoppedEvtMsg": return "user-cam-broadcast-end";
       case "PresenterAssignedEvtMsg": return "user-presenter-assigned";
-      case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned"
+      case "PresenterUnassignedEvtMsg": return "user-presenter-unassigned";
+      case "UserEmojiChangedEvtMsg": return "user-emoji-changed";
       case "SendPublicMessageEvtMsg": return "chat-public-message-sent";
       case "SendPrivateMessageEvtMsg": return "chat-private-message-sent";
       case "archive_started": return "rap-archive-started";
diff --git a/bbb-webhooks/package-lock.json b/bbb-webhooks/package-lock.json
index 756667bad9b6e41d2e885a38a2ee14ed62a66509..d219d53434a1cd81410f9b651b99fd5f20f757e5 100644
--- a/bbb-webhooks/package-lock.json
+++ b/bbb-webhooks/package-lock.json
@@ -4,42 +4,14 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
-    "accepts": {
-      "version": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz",
-      "integrity": "sha1-1xyW99QdD+2iw4zRToonwEFY30o=",
-      "requires": {
-        "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
-        "negotiator": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz"
-      },
-      "dependencies": {
-        "mime-db": {
-          "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
-          "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc="
-        },
-        "mime-types": {
-          "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
-          "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=",
-          "requires": {
-            "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz"
-          }
-        }
-      }
-    },
-    "asn1": {
-      "version": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz",
-      "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc="
-    },
-    "assert-plus": {
-      "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
-      "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
-    },
     "assertion-error": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz",
       "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw="
     },
     "async": {
-      "version": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
       "integrity": "sha1-rDYTsdqb7RtHUQu0ZRuJMeRxRsc="
     },
     "asynckit": {
@@ -48,44 +20,152 @@
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
       "dev": true
     },
-    "aws-sign2": {
-      "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz",
-      "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM="
-    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
       "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
       "dev": true
     },
-    "bl": {
-      "version": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz",
-      "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=",
-      "requires": {
-        "readable-stream": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
-      }
-    },
     "body-parser": {
-      "version": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz",
-      "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=",
-      "requires": {
-        "bytes": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz",
-        "content-type": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz",
-        "debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
-        "depd": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
-        "http-errors": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
-        "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
-        "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
-        "qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
-        "raw-body": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz",
-        "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz"
-      }
-    },
-    "boom": {
-      "version": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz",
-      "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=",
+      "version": "1.18.3",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
+      "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
       "requires": {
-        "hoek": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz"
+        "bytes": "3.0.0",
+        "content-type": "1.0.4",
+        "debug": "2.6.9",
+        "depd": "1.1.2",
+        "http-errors": "1.6.3",
+        "iconv-lite": "0.4.23",
+        "on-finished": "2.3.0",
+        "qs": "6.5.2",
+        "raw-body": "2.3.3",
+        "type-is": "1.6.16"
+      },
+      "dependencies": {
+        "bytes": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+          "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+        },
+        "content-type": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+          "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+        },
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "depd": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+          "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+        },
+        "ee-first": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+          "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+        },
+        "http-errors": {
+          "version": "1.6.3",
+          "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+          "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+          "requires": {
+            "depd": "1.1.2",
+            "inherits": "2.0.3",
+            "setprototypeof": "1.1.0",
+            "statuses": "1.5.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "requires": {
+            "safer-buffer": "2.1.2"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+          "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+        },
+        "media-typer": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+          "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+        },
+        "mime-db": {
+          "version": "1.35.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz",
+          "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg=="
+        },
+        "mime-types": {
+          "version": "2.1.19",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz",
+          "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==",
+          "requires": {
+            "mime-db": "1.35.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        },
+        "on-finished": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+          "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+          "requires": {
+            "ee-first": "1.1.1"
+          }
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        },
+        "raw-body": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+          "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+          "requires": {
+            "bytes": "3.0.0",
+            "http-errors": "1.6.3",
+            "iconv-lite": "0.4.23",
+            "unpipe": "1.0.0"
+          }
+        },
+        "setprototypeof": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+          "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+        },
+        "statuses": {
+          "version": "1.5.0",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+          "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+        },
+        "type-is": {
+          "version": "1.6.16",
+          "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+          "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+          "requires": {
+            "media-typer": "0.3.0",
+            "mime-types": "2.1.19"
+          }
+        },
+        "unpipe": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+          "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+        }
       }
     },
     "brace-expansion": {
@@ -104,45 +184,27 @@
       "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
       "dev": true
     },
-    "bytes": {
-      "version": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz",
-      "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk="
-    },
-    "caseless": {
-      "version": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz",
-      "integrity": "sha1-gWfBq4OX+1u5X5bSjlqBxQ8kesQ="
-    },
-    "chai": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz",
-      "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=",
-      "requires": {
-        "assertion-error": "1.0.2",
-        "deep-eql": "0.1.3",
-        "type-detect": "1.0.0"
-      }
-    },
-    "charenc": {
-      "version": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
-      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
-    },
-    "coffee-script": {
-      "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz",
-      "integrity": "sha1-nJ8dK0pSoADe0Vtll5FwNkgmPB0=",
+    "build": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/build/-/build-0.1.4.tgz",
+      "integrity": "sha1-cH/gJv/O3crL/c3zVur9pk8VEEY=",
       "requires": {
-        "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+        "cssmin": "0.3.2",
+        "jsmin": "1.0.1",
+        "jxLoader": "0.1.1",
+        "moo-server": "1.3.0",
+        "promised-io": "0.3.5",
+        "timespan": "2.3.0",
+        "uglify-js": "1.3.5",
+        "walker": "1.0.7",
+        "winston": "0.8.3",
+        "wrench": "1.3.9"
       }
     },
-    "colors": {
-      "version": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
-      "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w="
-    },
-    "combined-stream": {
-      "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
-      "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=",
-      "requires": {
-        "delayed-stream": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz"
-      }
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII="
     },
     "commander": {
       "version": "2.9.0",
@@ -165,22 +227,6 @@
       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
       "dev": true
     },
-    "content-disposition": {
-      "version": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz",
-      "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4="
-    },
-    "content-type": {
-      "version": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz",
-      "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0="
-    },
-    "cookie": {
-      "version": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz",
-      "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE="
-    },
-    "cookie-signature": {
-      "version": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz",
-      "integrity": "sha1-oSLj8VA+yg9TVXlbBxG7I2jUUPk="
-    },
     "cookiejar": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz",
@@ -189,82 +235,36 @@
     },
     "core-util-is": {
       "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
-    },
-    "crc": {
-      "version": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz",
-      "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII="
-    },
-    "crypt": {
-      "version": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
-      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
-    },
-    "cryptiles": {
-      "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz",
-      "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=",
-      "requires": {
-        "boom": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz"
-      }
-    },
-    "ctype": {
-      "version": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz",
-      "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8="
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
     },
-    "cycle": {
-      "version": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
-      "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI="
+    "cssmin": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/cssmin/-/cssmin-0.3.2.tgz",
+      "integrity": "sha1-3c5MVHtRCuDVlKjx+/iq+OLFwA0="
     },
     "debug": {
       "version": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
       "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=",
+      "dev": true,
       "requires": {
         "ms": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz"
       }
     },
-    "deep-eql": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
-      "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=",
-      "requires": {
-        "type-detect": "0.1.1"
-      },
-      "dependencies": {
-        "type-detect": {
-          "version": "0.1.1",
-          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz",
-          "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI="
-        }
-      }
-    },
     "deep-equal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
       "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
     },
-    "delayed-stream": {
-      "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz",
-      "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8="
-    },
-    "depd": {
-      "version": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
-      "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
-    },
-    "destroy": {
-      "version": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz",
-      "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk="
-    },
     "diff": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
       "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k="
     },
-    "ee-first": {
-      "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
-      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
-    },
-    "escape-html": {
-      "version": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
-      "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A="
+    "double-ended-queue": {
+      "version": "2.1.0-0",
+      "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
+      "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
     },
     "escape-string-regexp": {
       "version": "1.0.5",
@@ -272,91 +272,260 @@
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
       "dev": true
     },
-    "etag": {
-      "version": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz",
-      "integrity": "sha1-VMUN4E7kJpVWKSWsVmWIKRvn6eo=",
-      "requires": {
-        "crc": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz"
-      }
-    },
     "express": {
-      "version": "https://registry.npmjs.org/express/-/express-4.10.2.tgz",
+      "version": "4.10.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.10.2.tgz",
       "integrity": "sha1-3wbd6U2WiTKCnUQKIATF7+ZElbA=",
       "requires": {
-        "accepts": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz",
-        "content-disposition": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz",
-        "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz",
-        "cookie-signature": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz",
-        "debug": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
-        "depd": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
-        "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
-        "etag": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz",
-        "finalhandler": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.2.tgz",
-        "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz",
-        "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-        "merge-descriptors": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz",
-        "methods": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz",
-        "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz",
-        "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
-        "path-to-regexp": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz",
-        "proxy-addr": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz",
-        "qs": "https://registry.npmjs.org/qs/-/qs-2.3.2.tgz",
-        "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz",
-        "send": "https://registry.npmjs.org/send/-/send-0.10.1.tgz",
-        "serve-static": "https://registry.npmjs.org/serve-static/-/serve-static-1.7.2.tgz",
-        "type-is": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz",
-        "utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
-        "vary": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
+        "accepts": "1.1.4",
+        "content-disposition": "0.5.0",
+        "cookie": "0.1.2",
+        "cookie-signature": "1.0.5",
+        "debug": "2.1.3",
+        "depd": "1.0.1",
+        "escape-html": "1.0.1",
+        "etag": "1.5.1",
+        "finalhandler": "0.3.2",
+        "fresh": "0.2.4",
+        "media-typer": "0.3.0",
+        "merge-descriptors": "0.0.2",
+        "methods": "1.1.0",
+        "on-finished": "2.1.1",
+        "parseurl": "1.3.2",
+        "path-to-regexp": "0.1.3",
+        "proxy-addr": "1.0.10",
+        "qs": "2.3.2",
+        "range-parser": "1.0.3",
+        "send": "0.10.1",
+        "serve-static": "1.7.2",
+        "type-is": "1.5.7",
+        "utils-merge": "1.0.0",
+        "vary": "1.0.1"
       },
       "dependencies": {
+        "accepts": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.1.4.tgz",
+          "integrity": "sha1-1xyW99QdD+2iw4zRToonwEFY30o=",
+          "requires": {
+            "mime-types": "2.0.14",
+            "negotiator": "0.4.9"
+          }
+        },
+        "content-disposition": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz",
+          "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4="
+        },
+        "cookie": {
+          "version": "0.1.2",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz",
+          "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE="
+        },
+        "cookie-signature": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.5.tgz",
+          "integrity": "sha1-oSLj8VA+yg9TVXlbBxG7I2jUUPk="
+        },
+        "crc": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz",
+          "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII="
+        },
         "debug": {
-          "version": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
           "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=",
           "requires": {
-            "ms": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
+            "ms": "0.7.0"
           }
         },
         "depd": {
-          "version": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
           "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo="
         },
+        "destroy": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz",
+          "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk="
+        },
         "ee-first": {
-          "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz",
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz",
           "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q="
         },
+        "escape-html": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
+          "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A="
+        },
+        "etag": {
+          "version": "1.5.1",
+          "resolved": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz",
+          "integrity": "sha1-VMUN4E7kJpVWKSWsVmWIKRvn6eo=",
+          "requires": {
+            "crc": "3.2.1"
+          }
+        },
+        "finalhandler": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.2.tgz",
+          "integrity": "sha1-ezibD9Nkem+QvVZOImJL+KSnf7U=",
+          "requires": {
+            "debug": "2.1.3",
+            "escape-html": "1.0.1",
+            "on-finished": "2.1.1"
+          }
+        },
+        "forwarded": {
+          "version": "0.1.2",
+          "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+          "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+        },
+        "fresh": {
+          "version": "0.2.4",
+          "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz",
+          "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw="
+        },
+        "ipaddr.js": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz",
+          "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c="
+        },
+        "media-typer": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+          "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+        },
+        "merge-descriptors": {
+          "version": "0.0.2",
+          "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz",
+          "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc="
+        },
+        "methods": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz",
+          "integrity": "sha1-XcpO4S31L/OwVhRZhqjwHLyGQ28="
+        },
+        "mime": {
+          "version": "1.2.11",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
+          "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA="
+        },
         "mime-db": {
-          "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
+          "version": "1.12.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz",
           "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc="
         },
         "mime-types": {
-          "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
+          "version": "2.0.14",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz",
           "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=",
           "requires": {
-            "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz"
+            "mime-db": "1.12.0"
           }
         },
         "ms": {
-          "version": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
+          "version": "0.7.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
           "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M="
         },
+        "negotiator": {
+          "version": "0.4.9",
+          "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz",
+          "integrity": "sha1-kuRrbbU8fkIe1koryU8IvnYw3z8="
+        },
         "on-finished": {
-          "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz",
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz",
           "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=",
           "requires": {
-            "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz"
+            "ee-first": "1.1.0"
+          }
+        },
+        "parseurl": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+          "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+        },
+        "path-to-regexp": {
+          "version": "0.1.3",
+          "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz",
+          "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss="
+        },
+        "proxy-addr": {
+          "version": "1.0.10",
+          "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz",
+          "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=",
+          "requires": {
+            "forwarded": "0.1.2",
+            "ipaddr.js": "1.0.5"
           }
         },
         "qs": {
-          "version": "https://registry.npmjs.org/qs/-/qs-2.3.2.tgz",
+          "version": "2.3.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.2.tgz",
           "integrity": "sha1-1F7CSeS5sCmvAIgpoQHV/36XJ5A="
         },
+        "range-parser": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz",
+          "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU="
+        },
+        "send": {
+          "version": "0.10.1",
+          "resolved": "https://registry.npmjs.org/send/-/send-0.10.1.tgz",
+          "integrity": "sha1-d0XFDscvEVEVmA6PsXmuwBkA4Io=",
+          "requires": {
+            "debug": "2.1.3",
+            "depd": "1.0.1",
+            "destroy": "1.0.3",
+            "escape-html": "1.0.1",
+            "etag": "1.5.1",
+            "fresh": "0.2.4",
+            "mime": "1.2.11",
+            "ms": "0.6.2",
+            "on-finished": "2.1.1",
+            "range-parser": "1.0.3"
+          },
+          "dependencies": {
+            "ms": {
+              "version": "0.6.2",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
+              "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw="
+            }
+          }
+        },
+        "serve-static": {
+          "version": "1.7.2",
+          "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.7.2.tgz",
+          "integrity": "sha1-MWTOBtTmw0Wb3MnWAY+0+zXoSzk=",
+          "requires": {
+            "escape-html": "1.0.1",
+            "parseurl": "1.3.2",
+            "send": "0.10.1",
+            "utils-merge": "1.0.0"
+          }
+        },
         "type-is": {
-          "version": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz",
+          "version": "1.5.7",
+          "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz",
           "integrity": "sha1-uTaKWTzG730GReeLL0xky+zQXpA=",
           "requires": {
-            "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-            "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz"
+            "media-typer": "0.3.0",
+            "mime-types": "2.0.14"
           }
+        },
+        "utils-merge": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
+          "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg="
+        },
+        "vary": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz",
+          "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA="
         }
       }
     },
@@ -366,56 +535,6 @@
       "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
       "dev": true
     },
-    "eyes": {
-      "version": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
-      "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A="
-    },
-    "finalhandler": {
-      "version": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.2.tgz",
-      "integrity": "sha1-ezibD9Nkem+QvVZOImJL+KSnf7U=",
-      "requires": {
-        "debug": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
-        "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
-        "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz"
-      },
-      "dependencies": {
-        "debug": {
-          "version": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
-          "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=",
-          "requires": {
-            "ms": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
-          }
-        },
-        "ee-first": {
-          "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz",
-          "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q="
-        },
-        "ms": {
-          "version": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
-          "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M="
-        },
-        "on-finished": {
-          "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz",
-          "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=",
-          "requires": {
-            "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz"
-          }
-        }
-      }
-    },
-    "forever-agent": {
-      "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz",
-      "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA="
-    },
-    "form-data": {
-      "version": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz",
-      "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=",
-      "requires": {
-        "async": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
-        "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
-        "mime": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz"
-      }
-    },
     "formatio": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz",
@@ -430,20 +549,17 @@
       "integrity": "sha1-lriIb3w8NQi5Mta9cMTTqI818ak=",
       "dev": true
     },
-    "forwarded": {
-      "version": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz",
-      "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M="
-    },
-    "fresh": {
-      "version": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz",
-      "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw="
-    },
     "fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
       "dev": true
     },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE="
+    },
     "glob": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
@@ -476,43 +592,6 @@
       "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=",
       "dev": true
     },
-    "hawk": {
-      "version": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz",
-      "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=",
-      "requires": {
-        "boom": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz",
-        "cryptiles": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz",
-        "hoek": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz",
-        "sntp": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz"
-      }
-    },
-    "hoek": {
-      "version": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz",
-      "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU="
-    },
-    "http-errors": {
-      "version": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
-      "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
-      "requires": {
-        "depd": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
-        "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
-        "setprototypeof": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
-        "statuses": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz"
-      }
-    },
-    "http-signature": {
-      "version": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz",
-      "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=",
-      "requires": {
-        "asn1": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz",
-        "assert-plus": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
-        "ctype": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz"
-      }
-    },
-    "iconv-lite": {
-      "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
-      "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es="
-    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -525,23 +604,22 @@
     },
     "inherits": {
       "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
-      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
-    },
-    "ipaddr.js": {
-      "version": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz",
-      "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c="
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
     },
     "isarray": {
       "version": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
       "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
     },
-    "isstream": {
-      "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+    "js-yaml": {
+      "version": "0.3.7",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-0.3.7.tgz",
+      "integrity": "sha1-1znY7oZGHlSzVNan19HyrZoWf2I="
     },
-    "json-stringify-safe": {
-      "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    "jsmin": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/jsmin/-/jsmin-1.0.1.tgz",
+      "integrity": "sha1-570NzWSWw79IYyNb9GGj2YqjuYw="
     },
     "json3": {
       "version": "3.3.2",
@@ -554,8 +632,20 @@
       "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.22.tgz",
       "integrity": "sha1-MzCvdWyralQnAMZLLk5KoGLVL/8="
     },
+    "jxLoader": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/jxLoader/-/jxLoader-0.1.1.tgz",
+      "integrity": "sha1-ATTqUUTlM7WU/B/yX/GU4jXFPs0=",
+      "requires": {
+        "js-yaml": "0.3.7",
+        "moo-server": "1.3.0",
+        "promised-io": "0.3.5",
+        "walker": "1.0.7"
+      }
+    },
     "lodash": {
-      "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz",
       "integrity": "sha1-W3cjA03aTSYuWkb7LFjXzCL3FCA="
     },
     "lodash._baseassign": {
@@ -603,6 +693,11 @@
         "lodash._isiterateecall": "3.0.9"
       }
     },
+    "lodash.get": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
+    },
     "lodash.isarguments": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -631,29 +726,23 @@
       "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.2.tgz",
       "integrity": "sha1-JpS5U8nqTQE+W4v7qJHJkQJbJik="
     },
-    "media-typer": {
-      "version": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
-    },
-    "merge-descriptors": {
-      "version": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz",
-      "integrity": "sha1-w2pSp4FDdRPFcnXzndnTF1FKyMc="
-    },
-    "methods": {
-      "version": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz",
-      "integrity": "sha1-XcpO4S31L/OwVhRZhqjwHLyGQ28="
-    },
-    "mime": {
-      "version": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
-      "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA="
+    "makeerror": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
+      "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=",
+      "requires": {
+        "tmpl": "1.0.4"
+      }
     },
     "mime-db": {
       "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz",
-      "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg="
+      "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=",
+      "dev": true
     },
     "mime-types": {
       "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz",
       "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=",
+      "dev": true,
       "requires": {
         "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz"
       }
@@ -672,10 +761,6 @@
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
       "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
     },
-    "mkdirp": {
-      "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
-      "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc="
-    },
     "mocha": {
       "version": "3.5.0",
       "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz",
@@ -715,19 +800,21 @@
         }
       }
     },
+    "moo-server": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz",
+      "integrity": "sha1-XceVaVZaENbv7VQ5SR5p0jkuWPE="
+    },
     "ms": {
       "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
     },
     "native-promise-only": {
       "version": "0.8.1",
       "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz",
       "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE="
     },
-    "negotiator": {
-      "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.9.tgz",
-      "integrity": "sha1-kuRrbbU8fkIe1koryU8IvnYw3z8="
-    },
     "nise": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/nise/-/nise-1.0.1.tgz",
@@ -755,25 +842,59 @@
       }
     },
     "nock": {
-      "version": "9.0.14",
-      "resolved": "https://registry.npmjs.org/nock/-/nock-9.0.14.tgz",
-      "integrity": "sha1-IhFVAlMXPOKYvNifyoJeg4E8pys=",
+      "version": "9.4.3",
+      "resolved": "https://registry.npmjs.org/nock/-/nock-9.4.3.tgz",
+      "integrity": "sha512-inJFXR3REBvHbZy6nVVwaoKbVTR8Y4Ag051Y/pd2pNPy7HDYtQkenfilBwxToNsW9p1RTeBUml4SPK/mWrFihA==",
       "requires": {
-        "chai": "3.5.0",
-        "debug": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
+        "chai": "4.1.2",
+        "debug": "3.1.0",
         "deep-equal": "1.0.1",
-        "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-        "lodash": "4.17.4",
+        "json-stringify-safe": "5.0.1",
+        "lodash": "4.17.10",
         "mkdirp": "0.5.1",
-        "propagate": "0.4.0",
-        "qs": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
-        "semver": "5.4.1"
+        "propagate": "1.0.0",
+        "qs": "6.5.2",
+        "semver": "5.5.0"
       },
       "dependencies": {
+        "chai": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
+          "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
+          "requires": {
+            "assertion-error": "1.0.2",
+            "check-error": "1.0.2",
+            "deep-eql": "3.0.1",
+            "get-func-name": "2.0.0",
+            "pathval": "1.1.0",
+            "type-detect": "4.0.8"
+          }
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "deep-eql": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+          "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+          "requires": {
+            "type-detect": "4.0.8"
+          }
+        },
+        "json-stringify-safe": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+          "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+        },
         "lodash": {
-          "version": "4.17.4",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
-          "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
+          "version": "4.17.10",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
+          "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg=="
         },
         "mkdirp": {
           "version": "0.5.1",
@@ -782,24 +903,34 @@
           "requires": {
             "minimist": "0.0.8"
           }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        },
+        "propagate": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
+          "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk="
+        },
+        "qs": {
+          "version": "6.5.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+          "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+        },
+        "semver": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+          "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
+        },
+        "type-detect": {
+          "version": "4.0.8",
+          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+          "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
         }
       }
     },
-    "node-uuid": {
-      "version": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
-      "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc="
-    },
-    "oauth-sign": {
-      "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz",
-      "integrity": "sha1-8ilW8x6nFRqCHl8vsywRPK2Ln2k="
-    },
-    "on-finished": {
-      "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
-      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
-      "requires": {
-        "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
-      }
-    },
     "once": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -809,23 +940,16 @@
         "wrappy": "1.0.2"
       }
     },
-    "parseurl": {
-      "version": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
-      "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY="
-    },
     "path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
       "dev": true
     },
-    "path-to-regexp": {
-      "version": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz",
-      "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss="
-    },
-    "pkginfo": {
-      "version": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz",
-      "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE="
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA="
     },
     "process-nextick-args": {
       "version": "1.0.7",
@@ -833,83 +957,259 @@
       "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
       "dev": true
     },
-    "propagate": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz",
-      "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE="
-    },
-    "proxy-addr": {
-      "version": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz",
-      "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=",
-      "requires": {
-        "forwarded": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz",
-        "ipaddr.js": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz"
-      }
+    "promised-io": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/promised-io/-/promised-io-0.3.5.tgz",
+      "integrity": "sha1-StIXuzZYvKrplGsXqGaOzYUeE1Y="
     },
-    "punycode": {
-      "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+    "psl": {
+      "version": "1.1.28",
+      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.28.tgz",
+      "integrity": "sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw=="
     },
     "qs": {
       "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
-      "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM="
-    },
-    "range-parser": {
-      "version": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz",
-      "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU="
+      "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=",
+      "dev": true
     },
-    "raw-body": {
-      "version": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz",
-      "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=",
+    "redis": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
+      "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
       "requires": {
-        "bytes": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz",
-        "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
-        "unpipe": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
+        "double-ended-queue": "2.1.0-0",
+        "redis-commands": "1.3.5",
+        "redis-parser": "2.6.0"
       }
     },
-    "readable-stream": {
-      "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
-      "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
-      "requires": {
-        "core-util-is": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-        "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
-        "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-        "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
-      }
+    "redis-commands": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz",
+      "integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA=="
     },
-    "redis": {
-      "version": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz",
-      "integrity": "sha1-ZN92rQ/IrOuuvSoGReikj6xJGF4="
+    "redis-parser": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
+      "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
     },
     "request": {
-      "version": "https://registry.npmjs.org/request/-/request-2.47.0.tgz",
+      "version": "2.47.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.47.0.tgz",
       "integrity": "sha1-Cen9Gk/tZZOoBe+CArIPDF7LSF8=",
       "requires": {
-        "aws-sign2": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz",
-        "bl": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz",
-        "caseless": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz",
-        "combined-stream": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
-        "forever-agent": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz",
-        "form-data": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz",
-        "hawk": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz",
-        "http-signature": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz",
-        "json-stringify-safe": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-        "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz",
-        "node-uuid": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
-        "oauth-sign": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz",
-        "qs": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
-        "stringstream": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
-        "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
-        "tunnel-agent": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz"
+        "aws-sign2": "0.5.0",
+        "bl": "0.9.5",
+        "caseless": "0.6.0",
+        "combined-stream": "0.0.7",
+        "forever-agent": "0.5.2",
+        "form-data": "0.1.4",
+        "hawk": "1.1.1",
+        "http-signature": "0.10.1",
+        "json-stringify-safe": "5.0.1",
+        "mime-types": "1.0.2",
+        "node-uuid": "1.4.8",
+        "oauth-sign": "0.4.0",
+        "qs": "2.3.3",
+        "stringstream": "0.0.6",
+        "tough-cookie": "2.4.3",
+        "tunnel-agent": "0.4.3"
       },
       "dependencies": {
+        "asn1": {
+          "version": "0.1.11",
+          "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz",
+          "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc="
+        },
+        "assert-plus": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
+          "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
+        },
+        "aws-sign2": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz",
+          "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM="
+        },
+        "bl": {
+          "version": "0.9.5",
+          "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz",
+          "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=",
+          "requires": {
+            "readable-stream": "1.0.34"
+          }
+        },
+        "boom": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz",
+          "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=",
+          "requires": {
+            "hoek": "0.9.1"
+          }
+        },
+        "caseless": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.6.0.tgz",
+          "integrity": "sha1-gWfBq4OX+1u5X5bSjlqBxQ8kesQ="
+        },
+        "combined-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
+          "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=",
+          "requires": {
+            "delayed-stream": "0.0.5"
+          }
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+          "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+        },
+        "cryptiles": {
+          "version": "0.2.2",
+          "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz",
+          "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=",
+          "requires": {
+            "boom": "0.4.2"
+          }
+        },
+        "ctype": {
+          "version": "0.5.3",
+          "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz",
+          "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8="
+        },
+        "delayed-stream": {
+          "version": "0.0.5",
+          "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz",
+          "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8="
+        },
+        "forever-agent": {
+          "version": "0.5.2",
+          "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz",
+          "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA="
+        },
+        "form-data": {
+          "version": "0.1.4",
+          "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz",
+          "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=",
+          "requires": {
+            "async": "0.9.0",
+            "combined-stream": "0.0.7",
+            "mime": "1.2.11"
+          }
+        },
+        "hawk": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz",
+          "integrity": "sha1-h81JH5tG5OKurKM1QWdmiF0tHtk=",
+          "requires": {
+            "boom": "0.4.2",
+            "cryptiles": "0.2.2",
+            "hoek": "0.9.1",
+            "sntp": "0.2.4"
+          }
+        },
+        "hoek": {
+          "version": "0.9.1",
+          "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz",
+          "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU="
+        },
+        "http-signature": {
+          "version": "0.10.1",
+          "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz",
+          "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=",
+          "requires": {
+            "asn1": "0.1.11",
+            "assert-plus": "0.1.5",
+            "ctype": "0.5.3"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+          "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+        },
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "json-stringify-safe": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+          "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+        },
+        "mime": {
+          "version": "1.2.11",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
+          "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA="
+        },
         "mime-types": {
-          "version": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz",
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz",
           "integrity": "sha1-mVrhOSq4r/y/yyZB3QVOlDwNXc4="
         },
+        "node-uuid": {
+          "version": "1.4.8",
+          "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
+          "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc="
+        },
+        "oauth-sign": {
+          "version": "0.4.0",
+          "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.4.0.tgz",
+          "integrity": "sha1-8ilW8x6nFRqCHl8vsywRPK2Ln2k="
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+          "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+        },
         "qs": {
-          "version": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
           "integrity": "sha1-6eha2+ddoLvkyOBHaghikPhjtAQ="
+        },
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "0.0.1",
+            "string_decoder": "0.10.31"
+          }
+        },
+        "sntp": {
+          "version": "0.2.4",
+          "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz",
+          "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=",
+          "requires": {
+            "hoek": "0.9.1"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        },
+        "stringstream": {
+          "version": "0.0.6",
+          "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
+          "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA=="
+        },
+        "tough-cookie": {
+          "version": "2.4.3",
+          "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+          "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+          "requires": {
+            "psl": "1.1.28",
+            "punycode": "1.4.1"
+          }
+        },
+        "tunnel-agent": {
+          "version": "0.4.3",
+          "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
+          "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us="
         }
       }
     },
@@ -919,142 +1219,75 @@
       "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
       "dev": true
     },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
     "samsam": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz",
       "integrity": "sha1-7dOQk6MYQ3DLhZJDsr3yVefY6mc="
     },
-    "semver": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
-      "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
-    },
-    "send": {
-      "version": "https://registry.npmjs.org/send/-/send-0.10.1.tgz",
-      "integrity": "sha1-d0XFDscvEVEVmA6PsXmuwBkA4Io=",
+    "sha1": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.0.tgz",
+      "integrity": "sha1-j8IEe+OezrHcVOv+NaUEmxigBAs=",
       "requires": {
-        "debug": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
-        "depd": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
-        "destroy": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz",
-        "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
-        "etag": "https://registry.npmjs.org/etag/-/etag-1.5.1.tgz",
-        "fresh": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz",
-        "mime": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
-        "ms": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
-        "on-finished": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz",
-        "range-parser": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
+        "charenc": "0.0.2",
+        "crypt": "0.0.2"
       },
       "dependencies": {
-        "debug": {
-          "version": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
-          "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=",
-          "requires": {
-            "ms": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
-          },
-          "dependencies": {
-            "ms": {
-              "version": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
-              "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M="
-            }
-          }
-        },
-        "depd": {
-          "version": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz",
-          "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo="
+        "charenc": {
+          "version": "0.0.2",
+          "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+          "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
         },
-        "ee-first": {
-          "version": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz",
-          "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q="
-        },
-        "ms": {
-          "version": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
-          "integrity": "sha1-2JwhJMb9wTU9Zai3e/GqxLGTcIw="
-        },
-        "on-finished": {
-          "version": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz",
-          "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=",
-          "requires": {
-            "ee-first": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz"
-          }
+        "crypt": {
+          "version": "0.0.2",
+          "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+          "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
         }
       }
     },
-    "serve-static": {
-      "version": "https://registry.npmjs.org/serve-static/-/serve-static-1.7.2.tgz",
-      "integrity": "sha1-MWTOBtTmw0Wb3MnWAY+0+zXoSzk=",
-      "requires": {
-        "escape-html": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
-        "parseurl": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
-        "send": "https://registry.npmjs.org/send/-/send-0.10.1.tgz",
-        "utils-merge": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
-      }
-    },
-    "setprototypeof": {
-      "version": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
-      "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
-    },
-    "sha1": {
-      "version": "https://registry.npmjs.org/sha1/-/sha1-1.1.0.tgz",
-      "integrity": "sha1-j8IEe+OezrHcVOv+NaUEmxigBAs=",
-      "requires": {
-        "charenc": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
-        "crypt": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz"
-      }
-    },
     "sinon": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/sinon/-/sinon-3.2.1.tgz",
-      "integrity": "sha512-KY3OLOWpek/I4NGAMHetuutVgS2aRgMR5g5/1LSYvPJ3qo2BopIvk3esFztPxF40RWf/NNNJzdFPriSkXUVK3A==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-3.3.0.tgz",
+      "integrity": "sha512-/flfGfIxIRXSvZBHJzIf3iAyGYkmMQq6SQjA0cx9SOuVuq+4ZPPO4LJtH1Ce0Lznax1KSG1U6Dad85wIcSW19w==",
       "requires": {
+        "build": "0.1.4",
         "diff": "3.2.0",
         "formatio": "1.2.0",
+        "lodash.get": "4.4.2",
         "lolex": "2.1.2",
         "native-promise-only": "0.8.1",
         "nise": "1.0.1",
         "path-to-regexp": "1.7.0",
         "samsam": "1.2.1",
         "text-encoding": "0.6.4",
-        "type-detect": "4.0.3"
+        "type-detect": "4.0.8"
       },
       "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
         "path-to-regexp": {
           "version": "1.7.0",
           "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
           "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
           "requires": {
-            "isarray": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+            "isarray": "0.0.1"
           }
         },
         "type-detect": {
-          "version": "4.0.3",
-          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz",
-          "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo="
+          "version": "4.0.8",
+          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+          "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
         }
       }
     },
-    "sntp": {
-      "version": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz",
-      "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=",
-      "requires": {
-        "hoek": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz"
-      }
-    },
-    "stack-trace": {
-      "version": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
-      "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
-    },
-    "statuses": {
-      "version": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
-      "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
-    },
-    "string_decoder": {
-      "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
-      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
-    },
-    "stringstream": {
-      "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
-      "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
-    },
     "superagent": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.6.0.tgz",
@@ -1175,33 +1408,20 @@
       "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
       "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk="
     },
-    "tough-cookie": {
-      "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
-      "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=",
-      "requires": {
-        "punycode": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz"
-      }
-    },
-    "tunnel-agent": {
-      "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
-      "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us="
-    },
-    "type-detect": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz",
-      "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI="
+    "timespan": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz",
+      "integrity": "sha1-SQLOBAvRPYRcj1myfp1ZutbzmSk="
     },
-    "type-is": {
-      "version": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
-      "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
-      "requires": {
-        "media-typer": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-        "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz"
-      }
+    "tmpl": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
+      "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE="
     },
-    "unpipe": {
-      "version": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
-      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    "uglify-js": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz",
+      "integrity": "sha1-S1v/+Rhu/7qoiOTJ6UvZ/EyUkp0="
     },
     "util-deprecate": {
       "version": "1.0.2",
@@ -1209,30 +1429,62 @@
       "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
       "dev": true
     },
-    "utils-merge": {
-      "version": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
-      "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg="
-    },
-    "vary": {
-      "version": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz",
-      "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA="
+    "walker": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
+      "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=",
+      "requires": {
+        "makeerror": "1.0.11"
+      }
     },
     "winston": {
-      "version": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz",
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/winston/-/winston-0.8.3.tgz",
       "integrity": "sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=",
       "requires": {
-        "async": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
-        "colors": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
-        "cycle": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
-        "eyes": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
-        "isstream": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-        "pkginfo": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz",
-        "stack-trace": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz"
+        "async": "0.2.10",
+        "colors": "0.6.2",
+        "cycle": "1.0.3",
+        "eyes": "0.1.8",
+        "isstream": "0.1.2",
+        "pkginfo": "0.3.1",
+        "stack-trace": "0.0.10"
       },
       "dependencies": {
         "async": {
-          "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
+          "version": "0.2.10",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
           "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
+        },
+        "colors": {
+          "version": "0.6.2",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
+          "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w="
+        },
+        "cycle": {
+          "version": "1.0.3",
+          "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
+          "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI="
+        },
+        "eyes": {
+          "version": "0.1.8",
+          "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
+          "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A="
+        },
+        "isstream": {
+          "version": "0.1.2",
+          "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+          "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+        },
+        "pkginfo": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz",
+          "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE="
+        },
+        "stack-trace": {
+          "version": "0.0.10",
+          "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+          "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
         }
       }
     },
@@ -1242,6 +1494,11 @@
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
     },
+    "wrench": {
+      "version": "1.3.9",
+      "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.3.9.tgz",
+      "integrity": "sha1-bxPsNRRTF+spLKX2UxORskQRFBE="
+    },
     "xmldom": {
       "version": "0.1.27",
       "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
diff --git a/bbb-webhooks/package.json b/bbb-webhooks/package.json
index cbe8a8dc77d2dd5596509a2500fb8d6f188fcd12..b6b938e669c95cd5dd0e6c0463c5897f58996698 100755
--- a/bbb-webhooks/package.json
+++ b/bbb-webhooks/package.json
@@ -10,11 +10,10 @@
   "dependencies": {
     "async": "0.9.0",
     "body-parser": "^1.9.2",
-    "coffee-script": "1.8.0",
     "express": "4.10.2",
     "lodash": "2.4.1",
     "nock": "^9.0.14",
-    "redis": "0.12.1",
+    "redis": "^2.8.0",
     "request": "2.47.0",
     "sha1": "1.1.0",
     "sinon": "^3.2.1",
diff --git a/bbb-webhooks/test/test.js b/bbb-webhooks/test/test.js
index 51b2d0ab2037d24646ddd1efcb6ee854400c66e8..7239b9f16f3327383a2776591c735e1621bd7211 100644
--- a/bbb-webhooks/test/test.js
+++ b/bbb-webhooks/test/test.js
@@ -9,6 +9,8 @@ const Helpers = require('./helpers.js')
 const sinon = require('sinon');
 const winston = require('winston');
 
+const sharedSecret = process.env.SHARED_SECRET || sharedSecret;
+
 // Block winston from logging
 Logger.remove(winston.transports.Console);
 describe('bbb-webhooks tests', () => {
@@ -38,7 +40,7 @@ describe('bbb-webhooks tests', () => {
 
   describe('GET /hooks/list permanent', () => {
     it('should list permanent hook', (done) => {
-      let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, config.bbb.sharedSecret);
+      let getUrl = utils.checksumAPI(Helpers.url + Helpers.listUrl, sharedSecret);
       getUrl = Helpers.listUrl + '?checksum=' + getUrl
 
       request(Helpers.url)
@@ -62,7 +64,7 @@ describe('bbb-webhooks tests', () => {
       Hook.removeSubscription(hooks[hooks.length-1].id, () => { done(); });
     });
     it('should create a hook', (done) => {
-      let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl, config.bbb.sharedSecret);
+      let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl, sharedSecret);
       getUrl = Helpers.createUrl + '&checksum=' + getUrl
 
       request(Helpers.url)
@@ -87,7 +89,7 @@ describe('bbb-webhooks tests', () => {
     it('should destroy a hook', (done) => {
       const hooks = Hook.allGlobalSync();
       const hook = hooks[hooks.length-1].id;
-      let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), config.bbb.sharedSecret);
+      let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyUrl(hook), sharedSecret);
       getUrl = Helpers.destroyUrl(hook) + '&checksum=' + getUrl
 
       request(Helpers.url)
@@ -103,7 +105,7 @@ describe('bbb-webhooks tests', () => {
 
   describe('GET /hooks/destroy permanent hook', () => {
     it('should not destroy the permanent hook', (done) => {
-      let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, config.bbb.sharedSecret);
+      let getUrl = utils.checksumAPI(Helpers.url + Helpers.destroyPermanent, sharedSecret);
       getUrl = Helpers.destroyPermanent + '&checksum=' + getUrl
       request(Helpers.url)
       .get(getUrl)
@@ -126,7 +128,7 @@ describe('bbb-webhooks tests', () => {
       Hook.removeSubscription(hooks[hooks.length-1].id, () => { done(); });
     });
     it('should create a hook with getRaw=true', (done) => {
-      let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, config.bbb.sharedSecret);
+      let getUrl = utils.checksumAPI(Helpers.url + Helpers.createUrl + Helpers.createRaw, sharedSecret);
       getUrl = Helpers.createUrl + '&checksum=' + getUrl + Helpers.createRaw
 
       request(Helpers.url)
diff --git a/bbb-webhooks/web_hooks.js b/bbb-webhooks/web_hooks.js
index a91718f1e889e6724f1ec35b095f7011f5c4919c..c00a2cf0b14c5eb2af40d16eeeb399fdd089c266 100644
--- a/bbb-webhooks/web_hooks.js
+++ b/bbb-webhooks/web_hooks.js
@@ -69,7 +69,7 @@ module.exports = class WebHooks {
           }
         }
       } catch (e) {
-        Logger.error("[WebHooks] error processing the message:", JSON.stringify(raw), ":", e);
+        Logger.error("[WebHooks] error processing the message:", JSON.stringify(raw), ":", e.message);
       }
     });
 
diff --git a/bbb-webhooks/web_server.js b/bbb-webhooks/web_server.js
index e0f77f0b405a6d7c55e3052ae6fd4d11136137dc..be5a0057836e696141adbd44e51349569678808f 100644
--- a/bbb-webhooks/web_server.js
+++ b/bbb-webhooks/web_server.js
@@ -144,8 +144,9 @@ module.exports = class WebServer {
   _validateChecksum(req, res, next) {
     const urlObj = url.parse(req.url, true);
     const checksum = urlObj.query["checksum"];
+    const sharedSecret = process.env.SHARED_SECRET || config.bbb.sharedSecret;
 
-    if (checksum === Utils.checksumAPI(req.url, config.bbb.sharedSecret)) {
+    if (checksum === Utils.checksumAPI(req.url, sharedSecret)) {
       next();
     } else {
       Logger.info("[WebServer] checksum check failed, sending a checksumError response");
diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties
index 5d45826868b817e03dd08a2a0178fc86443a691e..eae95489ce4fe06c5ad66c47a9300f82b6481d7f 100644
--- a/bigbluebutton-client/locale/en_US/bbbResources.properties
+++ b/bigbluebutton-client/locale/en_US/bbbResources.properties
@@ -500,6 +500,7 @@ bbb.screenshareView.actualSize = Display actual size
 bbb.screenshareView.minimizeBtn.accessibilityName = Minimize the Screen Sharing View Window
 bbb.screenshareView.maximizeRestoreBtn.accessibilityName = Maximize the Screen Sharing View Window
 bbb.screenshareView.closeBtn.accessibilityName = Close the Screen Sharing View Window
+bbb.screenshareView.warning.ffMacWebRTC = We recommend that you switch to Chrome on Mac for a better screenshare viewing quality.
 bbb.toolbar.phone.toolTip.start = Enable Audio (microphone or listen only)
 bbb.toolbar.phone.toolTip.stop = Disable Audio
 bbb.toolbar.phone.toolTip.mute = Stop listening the conference
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as
index 060ad4e8e6437c243e269ae084f20a94b96417e5..5b21d425ac0155135b5eab3969aa970e8111e720 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/utils/WebRTCScreenshareUtility.as
@@ -20,13 +20,14 @@
 package org.bigbluebutton.modules.screenshare.utils
 {
   import flash.external.ExternalInterface;
+  import flash.system.Capabilities;
   
   import org.as3commons.lang.StringUtils;
   import org.as3commons.logging.api.ILogger;
   import org.as3commons.logging.api.getClassLogger;
   import org.bigbluebutton.core.Options;
   import org.bigbluebutton.modules.screenshare.model.ScreenshareOptions;
-  import org.bigbluebutton.util.browser.BrowserCheck;
+  import org.bigbluebutton.util.browser.BrowserCheck;
 
   public class WebRTCScreenshareUtility {
     private static const LOGGER:ILogger = getClassLogger(WebRTCScreenshareUtility);
@@ -66,7 +67,11 @@ package org.bigbluebutton.modules.screenshare.utils
 
       // if its firefox go ahead and let webrtc handle it
       if (BrowserCheck.isFirefox()) {
-        webRTCWorksAndConfigured("Firefox, lets try");
+        if (Capabilities.os.indexOf("Mac") >= 0) {
+          cannotUseWebRTC("Firefox on Mac performs poorly fallback to Java");
+        } else {
+          webRTCWorksAndConfigured("Firefox, lets try");
+        }
         return;
 
       // if its chrome we need to check for the extension
diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml
index 52df561d2c6b4330974cd36e0e53161dd4200f01..8577bea46f66c9c5ad6a75ded5fc9d6e562349be 100755
--- a/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml
+++ b/bigbluebutton-client/src/org/bigbluebutton/modules/screenshare/view/components/WebRTCDesktopViewWindow.mxml
@@ -69,6 +69,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			import org.bigbluebutton.modules.screenshare.model.ScreenshareOptions;
 			import org.bigbluebutton.modules.screenshare.services.red5.WebRTCConnectionEvent;
 			import org.bigbluebutton.util.ConnUtil;
+			import org.bigbluebutton.util.browser.BrowserCheck;
 			import org.bigbluebutton.util.i18n.ResourceUtil;
 
 			public static const LOG:String = "Deskshare::DesktopViewWindow - ";
@@ -113,6 +114,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 				videoHolder.addEventListener(MouseEvent.MOUSE_OUT, videoHolder_mouseOutHanlder);
 				addEventListener(MDIWindowEvent.RESIZE_END, onResizeEndEvent);
 				
+				if (BrowserCheck.isFirefox() && Capabilities.os.indexOf("Mac") >= 0) {
+					ffMacWarning.visible = ffMacWarning.includeInLayout = true;
+				}
+				
 				resourcesChanged();
 			}
 
@@ -475,5 +480,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			   mouseOver="btnActualSize.alpha = 1"
 			   label="{    btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.screenshareView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.screenshareView.actualSize')    }"
 			   toolTip="{    btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.screenshareView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.screenshareView.actualSize')    }"/>
-
+	<mx:Box id="ffMacWarning"
+			visible="false"
+			includeInLayout="false"
+			width="260"
+			styleName="lockSettingsHintBoxStyle"
+			horizontalCenter="0"
+			top="{VIDEO_HEIGHT_PADDING*2+btnActualSize.height}">
+		<mx:Text width="100%"
+				 textAlign="center"
+				 styleName="lockSettingHintTextStyle"
+				 text="{ResourceUtil.getInstance().getString('bbb.screenshareView.warning.ffMacWebRTC')}" />
+	</mx:Box>
 </CustomMdiWindow>
diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release
index beffee177ee32c109e5ff426c38b0f2c2a0524d7..3e3fed860f984d91395ad78a951be478a636713a 100644
--- a/bigbluebutton-config/bigbluebutton-release
+++ b/bigbluebutton-config/bigbluebutton-release
@@ -1 +1,2 @@
 BIGBLUEBUTTON_RELEASE=2.2.0-dev
+
diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf
index 7ecdf03e24698590f5cd9c74dc07967032dcab83..e3a624968a5ac1802abffbc503368e2f8dd884ec 100755
--- a/bigbluebutton-config/bin/bbb-conf
+++ b/bigbluebutton-config/bin/bbb-conf
@@ -79,6 +79,8 @@ FREESWITCH_EXTERNAL=/opt/freeswitch/conf/sip_profiles/external.xml
 FREESWITCH_PID=/opt/freeswitch/run/freeswitch.pid
 FREESWITCH_EVENT_SOCKET=/opt/freeswitch/conf/autoload_configs/event_socket.conf.xml
 
+HTML5_CONFIG=/usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json
+
 if [ -f /etc/redhat-release ]; then
   DISTRIB_ID=centos
   SERVLET_LOGS=/usr/share/tomcat/logs
@@ -944,15 +946,21 @@ check_configuration() {
 			echo "#    $FREESWITCH_EXTERNAL"
 			echo 
 		fi
-                if grep ws-binding $FREESWITCH_EXTERNAL > /dev/null; then
-                        echo "# Warning: You have configured SSL, but FreeSWITCH is still listening"
-                        echo "# on port 5066.  You should edit "
-                        echo "#"
-                        echo "#    $FREESWITCH_EXTERNAL"
-                        echo "#"
-                        echo "# and remove the line that contains ws-binding."
-                        echo
-                fi
+
+	if [ -f $HTML5_CONFIG ]; then 
+          if grep \"enableListenOnly\".*true $HTML5_CONFIG > /dev/null; then
+            if ! grep -q ws-binding $FREESWITCH_EXTERNAL ;  then
+                echo "# Warning: You have enabled listen-only audio via Kurento"
+                echo "# but FreeSWITCH is not listening on port 5066.  You should edit "
+                echo "#"
+                echo "#    $FREESWITCH_EXTERNAL"
+                echo "#"
+                echo "# and add a line that enables ws-binding on port 5066."
+                echo
+            fi
+          fi
+        fi
+
 	fi
 
 	if [ $DISTRIB_ID != "centos" ]; then
@@ -1787,23 +1795,24 @@ if [ -n "$HOST" ]; then
 	echo "Assigning $HOST for servername in /etc/nginx/sites-available/bigbluebutton"
 	sudo sed -i "s/server_name  .*/server_name  $HOST;/g" /etc/nginx/sites-available/bigbluebutton
 
-        #
-        # Update configuration for BigBlueButton client (and preserve hostname for chromeExtensionLink if exists)
-        #
+	#
+	# Update configuration for BigBlueButton client (and preserve hostname for chromeExtensionLink if exists)
+	#
 
-        echo "Assigning $HOST for http[s]:// in /var/www/bigbluebutton/client/conf/config.xml"
-        sudo sed -i "s/http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_HTTP:\/\/$HOST\2/g" \
-	        /var/www/bigbluebutton/client/conf/config.xml
+	# Extract the chrome store URL before updating config.xml. We will be able to restore it.
 	chromeExtensionLinkURL=$(cat /var/www/bigbluebutton/client/conf/config.xml | sed -n '/chromeExtensionLink/{s/.*https*:\/\///;s/\/.*//;p}')
+	
+	echo "Assigning $HOST for http[s]:// in /var/www/bigbluebutton/client/conf/config.xml"
+	sudo sed -i "s/http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_HTTP:\/\/$HOST\2/g" \
+		/var/www/bigbluebutton/client/conf/config.xml
 
-        if ! echo "$chromeExtensionLinkURL" | grep -q '""'; then
-          sudo sed -i "s/chromeExtensionLink=\"https:\/\/[^\/]*/chromeExtensionLink=\"https:\/\/$chromeExtensionLinkURL/g" \
-                /var/www/bigbluebutton/client/conf/config.xml
-        fi
-
+	if ! echo "$chromeExtensionLinkURL" | grep -q '""'; then
+		sudo sed -i "s/chromeExtensionLink=\"https:\/\/[^\/]*/chromeExtensionLink=\"https:\/\/$chromeExtensionLinkURL/g" \
+			/var/www/bigbluebutton/client/conf/config.xml
+	fi
 
-        echo "Assigning $HOST for publishURI in /var/www/bigbluebutton/client/conf/config.xml"
-        sudo sed -i "s/publishURI=\"[^\"]*\"/publishURI=\"$HOST\"/" /var/www/bigbluebutton/client/conf/config.xml
+	echo "Assigning $HOST for publishURI in /var/www/bigbluebutton/client/conf/config.xml"
+	sudo sed -i "s/publishURI=\"[^\"]*\"/publishURI=\"$HOST\"/" /var/www/bigbluebutton/client/conf/config.xml
 
 	#
 	# Update configuration for BigBlueButton web app
@@ -1893,10 +1902,9 @@ if [ -n "$HOST" ]; then
         #
         # Update HTML5 client
         #
-	if [ -f /usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json ]; then 
+	if [ -f $HTML5_CONFIG ]; then
         	WS=$(cat $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | sed -n '/^bigbluebutton.web.serverURL/{s/.*=//;p}' | sed 's/https/wss/g' | sed s'/http/ws/g')
-        	sed -i "s|\"wsUrl.*|\"wsUrl\": \"$WS/bbb-webrtc-sfu\",|g" \
-                	/usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json
+        	sed -i "s|\"wsUrl.*|\"wsUrl\": \"$WS/bbb-webrtc-sfu\",|g" $HTML5_CONFIG
 
 		if [ -f /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml ]; then
 		  change_yml_value /usr/local/bigbluebutton/bbb-webrtc-sfu/config/default.yml kurentoUrl "ws://$HOST:8888/kurento"
diff --git a/bigbluebutton-config/cron.daily/bigbluebutton b/bigbluebutton-config/cron.daily/bigbluebutton
index efbe26faf67294b4616571e6873172f0d23a33d5..9e9c57ae9f3cd0a65c6ba77f85b0fbfbb1c9acbd 100755
--- a/bigbluebutton-config/cron.daily/bigbluebutton
+++ b/bigbluebutton-config/cron.daily/bigbluebutton
@@ -50,7 +50,7 @@ done
 #
 # Delete streams in kurento older than N days
 #
-for app in recording screenshare; do
+for app in recordings screenshare; do
 	app_dir=/var/kurento/$app
 	if [[ -d $app_dir ]]; then
 		find $app_dir -name "*.mkv" -mtime +$history -delete
diff --git a/bigbluebutton-html5/.dockerignore b/bigbluebutton-html5/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..bf9b07fdc25d27e22a5052d1d5801bf882fb2f88
--- /dev/null
+++ b/bigbluebutton-html5/.dockerignore
@@ -0,0 +1,2 @@
+Dockerfile
+Dockerfile.dev
diff --git a/bigbluebutton-html5/Dockerfile b/bigbluebutton-html5/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..8e4b24cb172a218115b648bd67c6002ab5ed796f
--- /dev/null
+++ b/bigbluebutton-html5/Dockerfile
@@ -0,0 +1,38 @@
+FROM node:8
+
+RUN set -x \
+ && curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \
+ && useradd -m -G users -s /bin/bash meteor
+
+RUN apt-get update && apt-get -y install jq
+
+COPY . /source
+
+RUN cd /source \
+ && mv docker-entrypoint.sh /usr/local/bin/ \
+ && chown -R meteor:meteor . \
+ && mkdir /app \
+ && chown -R meteor:meteor /app
+
+USER meteor
+
+RUN cd /source \
+ && meteor npm install \
+ && meteor build --directory /app
+
+ENV NODE_ENV production
+
+RUN cd /app/bundle/programs/server \
+ && npm install \
+ && npm cache clear --force
+
+WORKDIR /app/bundle
+
+ENV MONGO_URL=mongodb://mongo:27017/html5client \
+    PORT=3000 \
+    ROOT_URL=http://localhost:3000 \
+    METEOR_SETTINGS_MODIFIER=.
+
+EXPOSE 3000
+
+CMD ["docker-entrypoint.sh"]
diff --git a/bigbluebutton-html5/Dockerfile.dev b/bigbluebutton-html5/Dockerfile.dev
new file mode 100644
index 0000000000000000000000000000000000000000..692c200be719aca865919535210674ff871c76ee
--- /dev/null
+++ b/bigbluebutton-html5/Dockerfile.dev
@@ -0,0 +1,24 @@
+FROM node:8
+
+COPY . /source
+
+RUN set -x \
+ && curl -sL https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh \
+ && useradd -m -G users -s /bin/bash meteor \
+ && chown -R meteor:meteor /source
+
+USER meteor
+
+RUN cd /source \
+ && meteor npm install
+
+WORKDIR /source
+
+ENV MONGO_URL=mongodb://mongo:27017/html5client \
+    PORT=3000 \
+    ROOT_URL=http://localhost:3000
+
+EXPOSE 3000
+
+CMD ["npm", "start"]
+
diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js
index c55b99e2d164bf3c3c0fe24562825709c9a52674..4ca52d78a9b90bdf298d19a5b835d763f922e72e 100644
--- a/bigbluebutton-html5/client/compatibility/kurento-extension.js
+++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js
@@ -13,7 +13,6 @@ Kurento = function (
   onSuccess,
   options = {},
 ) {
-
   this.ws = null;
   this.video = null;
   this.screen = null;
@@ -30,10 +29,10 @@ Kurento = function (
 
   Object.assign(this, options);
 
-  this.SEND_ROLE = "send";
-  this.RECV_ROLE = "recv";
-  this.SFU_APP = "screenshare";
-  this.ON_ICE_CANDIDATE_MSG = "iceCandidate";
+  this.SEND_ROLE = 'send';
+  this.RECV_ROLE = 'recv';
+  this.SFU_APP = 'screenshare';
+  this.ON_ICE_CANDIDATE_MSG = 'iceCandidate';
   this.PING_INTERVAL = 15000;
 
   window.Logger = this.logger || console;
@@ -45,11 +44,11 @@ Kurento = function (
   }
 
   if (this.chromeScreenshareSources == null) {
-    this.chromeScreenshareSources = ["screen", "window"];
+    this.chromeScreenshareSources = ['screen', 'window'];
   }
 
   if (this.firefoxScreenshareSource == null) {
-    this.firefoxScreenshareSource = "window";
+    this.firefoxScreenshareSource = 'window';
   }
 
   // Limiting max resolution to WQXGA
@@ -78,7 +77,7 @@ Kurento = function (
   if (onSuccess != null) {
     this.onSuccess = Kurento.normalizeCallback(onSuccess);
   } else {
-    var _this = this;
+    const _this = this;
     this.onSuccess = function () {
       _this.logSuccess('Default success handler');
     };
@@ -93,7 +92,6 @@ this.KurentoManager = function () {
 
 KurentoManager.prototype.exitScreenShare = function () {
   if (typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) {
-
     if (this.kurentoScreenshare.logger !== null) {
       this.kurentoScreenshare.logger.info('  [exitScreenShare] Exiting screensharing');
     }
@@ -119,6 +117,10 @@ KurentoManager.prototype.exitScreenShare = function () {
 KurentoManager.prototype.exitVideo = function () {
   if (typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) {
 
+    if(this.kurentoVideo.webRtcPeer) {
+      this.kurentoVideo.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
+    }
+
     if (this.kurentoVideo.logger !== null) {
       this.kurentoVideo.logger.info('  [exitScreenShare] Exiting screensharing viewing');
     }
@@ -139,7 +141,6 @@ KurentoManager.prototype.exitVideo = function () {
 
 KurentoManager.prototype.exitAudio = function () {
   if (typeof this.kurentoAudio !== 'undefined' && this.kurentoAudio) {
-
     if (this.kurentoAudio.logger !== null) {
       this.kurentoAudio.logger.info('  [exitAudio] Exiting listen only audio');
     }
@@ -177,15 +178,15 @@ KurentoManager.prototype.joinWatchVideo = function (tag) {
 
 KurentoManager.prototype.getFirefoxScreenshareSource = function () {
   return this.kurentoScreenshare.firefoxScreenshareSource;
-}
+};
 
 KurentoManager.prototype.getChromeScreenshareSources = function () {
   return this.kurentoScreenshare.chromeScreenshareSources;
-}
+};
 
 KurentoManager.prototype.getChromeExtensionKey = function () {
   return this.kurentoScreenshare.chromeExtension;
-}
+};
 
 
 Kurento.prototype.setScreensharing = function (tag) {
@@ -201,13 +202,13 @@ Kurento.prototype.create = function (tag) {
 Kurento.prototype.downscaleResolution = function (oldWidth, oldHeight) {
   const factorWidth = this.vid_max_width / oldWidth;
   const factorHeight = this.vid_max_height / oldHeight;
-  let width, height;
+  let width,
+    height;
 
   if (factorWidth < factorHeight) {
     width = Math.trunc(oldWidth * factorWidth);
     height = Math.trunc(oldHeight * factorWidth);
-  }
-  else {
+  } else {
     width = Math.trunc(oldWidth * factorHeight);
     height = Math.trunc(oldHeight * factorHeight);
   }
@@ -253,7 +254,7 @@ Kurento.prototype.onWSMessage = function (message) {
       this.onSuccess(parsedMessage.success);
       break;
     case 'webRTCAudioError':
-      this.onFail(parsedMessage.error);
+      this.onFail(parsedMessage);
       break;
     case 'pong':
       break;
@@ -269,13 +270,12 @@ Kurento.prototype.setRenderTag = function (tag) {
 Kurento.prototype.startResponse = function (message) {
   if (message.response !== 'accepted') {
     const errorMsg = message.message ? message.message : 'Unknow error';
-    this.logger.warn("Call not accepted for the following reason:", {error: errorMsg});
+    this.logger.warn('Call not accepted for the following reason:', { error: errorMsg });
     switch (message.type) {
       case 'screenshare':
         if (message.role === this.SEND_ROLE) {
           kurentoManager.exitScreenShare();
-        }
-        else if (message.role === this.RECV_ROLE) {
+        } else if (message.role === this.RECV_ROLE) {
           kurentoManager.exitVideo();
         }
         break;
@@ -284,7 +284,7 @@ Kurento.prototype.startResponse = function (message) {
         break;
     }
   } else {
-    this.logger.debug(`Procedure for ${message.type} was accepted with SDP =>`, {sdpAnswer: message.sdpAnswer});
+    this.logger.debug(`Procedure for ${message.type} was accepted with SDP =>`, { sdpAnswer: message.sdpAnswer });
     this.webRtcPeer.processAnswer(message.sdpAnswer);
   }
 };
@@ -310,7 +310,7 @@ Kurento.prototype.onOfferPresenter = function (error, offerSdp) {
     vw: this.width,
   };
 
-  this.logger.info("onOfferPresenter sending to screenshare server => ", { sdpOffer: message });
+  this.logger.info('onOfferPresenter sending to screenshare server => ', { sdpOffer: message });
   this.sendMessage(message);
 };
 
@@ -335,7 +335,7 @@ Kurento.prototype.startScreensharing = function () {
     sendSource: 'desktop',
   };
 
-  this.logger.info(" Peer options =>", options);
+  this.logger.info(' Peer options =>', options);
 
   let resolution;
   this.logger.debug(`Screenshare screen dimensions are ${this.width} x ${this.height}`);
@@ -343,23 +343,25 @@ Kurento.prototype.startScreensharing = function () {
     resolution = this.downscaleResolution(this.width, this.height);
     this.width = resolution.width;
     this.height = resolution.height;
-    this.logger.debug("Screenshare track dimensions have been resized to", this.width, "x", this.height);
+    this.logger.debug('Screenshare track dimensions have been resized to', this.width, 'x', this.height);
   }
 
   this.addIceServers(this.iceServers, options);
 
   this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, (error) => {
     if (error) {
-      this.logger.error("WebRtcPeerSendonly constructor error:", {error});
+      this.logger.error('WebRtcPeerSendonly constructor error:', { error });
       this.onFail(error);
       return kurentoManager.exitScreenShare();
     }
 
     this.webRtcPeer.generateOffer(this.onOfferPresenter.bind(this));
-    this.logger.info("Generated peer offer w/ options:", {options: options});
+    this.logger.info('Generated peer offer w/ options:', { options });
 
     const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0];
+    const _this = this;
     localStream.getVideoTracks()[0].onended = function () {
+      _this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
       return kurentoManager.exitScreenShare();
     };
 
@@ -367,15 +369,24 @@ Kurento.prototype.startScreensharing = function () {
       return kurentoManager.exitScreenShare();
     };
   });
+  this.webRtcPeer.peerConnection.oniceconnectionstatechange = () => {
+    if (this.webRtcPeer) {
+      const connectionState = this.webRtcPeer.peerConnection.iceConnectionState;
+      if (connectionState === 'failed' || connectionState === 'closed') {
+        this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
+        this.onFail('ICE connection failed');
+      }
+    }
+  };
 };
 
 Kurento.prototype.onIceCandidate = function (candidate, role) {
   const self = this;
-  this.logger.info("Local candidate:", {candidate});
+  this.logger.info('Local candidate:', { candidate });
 
   const message = {
     id: this.ON_ICE_CANDIDATE_MSG,
-    role: role,
+    role,
     type: this.SFU_APP,
     voiceBridge: self.voiceBridge,
     candidate,
@@ -398,13 +409,13 @@ Kurento.prototype.viewer = function () {
   if (!this.webRtcPeer) {
     const options = {
       mediaConstraints: {
-        audio: false
+        audio: false,
       },
       remoteVideo: document.getElementById(this.renderTag),
       onicecandidate: (candidate) => {
         this.onIceCandidate(candidate, this.RECV_ROLE);
-      }
-    }
+      },
+    };
 
     this.addIceServers(this.iceServers, options);
 
@@ -415,6 +426,15 @@ Kurento.prototype.viewer = function () {
 
       this.generateOffer(self.onOfferViewer.bind(self));
     });
+    self.webRtcPeer.peerConnection.oniceconnectionstatechange = () => {
+      if (this.webRtcPeer) {
+        const connectionState = this.webRtcPeer.peerConnection.iceConnectionState;
+        if (connectionState === 'failed' || connectionState === 'closed') {
+          this.webRtcPeer.peerConnection.oniceconnectionstatechange = null;
+          this.onFail('ICE connection failed');
+        }
+      }
+    };
   }
 };
 
@@ -434,15 +454,15 @@ Kurento.prototype.onOfferViewer = function (error, offerSdp) {
     sdpOffer: offerSdp,
   };
 
-  this.logger.info("onOfferViewer sending to screenshare server: ", {sdpOffer: message.sdpOffer});
+  this.logger.info('onOfferViewer sending to screenshare server: ', { sdpOffer: message.sdpOffer });
   this.sendMessage(message);
 };
 
 KurentoManager.prototype.joinAudio = function (tag) {
   this.exitAudio();
-  var obj = Object.create(Kurento.prototype);
+  const obj = Object.create(Kurento.prototype);
   Kurento.apply(obj, arguments);
-  this.kurentoAudio= obj;
+  this.kurentoAudio = obj;
   this.kurentoAudio.setAudio(tag);
 };
 
@@ -453,20 +473,23 @@ Kurento.prototype.setAudio = function (tag) {
 
 Kurento.prototype.listenOnly = function () {
   var self = this;
+  const remoteVideo = document.getElementById(this.renderTag);
+  remoteVideo.muted = true;
   if (!this.webRtcPeer) {
     var options = {
-      remoteVideo: document.getElementById(this.renderTag),
+      audioStream: this.inputStream,
+      remoteVideo,
       onicecandidate : this.onListenOnlyIceCandidate.bind(this),
       mediaConstraints: {
-        audio:true,
-        video:false
-      }
-    }
+        audio: true,
+        video: false,
+      },
+    };
 
     this.addIceServers(this.iceServers, options);
 
-    self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) {
-      if(error) {
+    self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function (error) {
+      if (error) {
         return self.onFail(PEER_ERROR);
       }
 
@@ -476,39 +499,39 @@ Kurento.prototype.listenOnly = function () {
 };
 
 Kurento.prototype.onListenOnlyIceCandidate = function (candidate) {
-  let self = this;
-  this.logger.debug("[onListenOnlyIceCandidate]", {candidate});
+  const self = this;
+  this.logger.debug('[onListenOnlyIceCandidate]', { candidate });
 
-  var message = {
-    id : this.ON_ICE_CANDIDATE_MSG,
+  const message = {
+    id: this.ON_ICE_CANDIDATE_MSG,
     type: 'audio',
     role: 'viewer',
     voiceBridge: self.voiceBridge,
-    candidate : candidate,
-  }
+    candidate,
+  };
   this.sendMessage(message);
 };
 
 Kurento.prototype.onOfferListenOnly = function (error, offerSdp) {
-  let self = this;
-  if(error)  {
-    this.logger.error("[onOfferListenOnly]", error);
+  const self = this;
+  if (error) {
+    this.logger.error('[onOfferListenOnly]', error);
     return this.onFail(SDP_ERROR);
   }
 
-  let message = {
-    id : 'start',
+  const message = {
+    id: 'start',
     type: 'audio',
     role: 'viewer',
     voiceBridge: self.voiceBridge,
     caleeName: self.caleeName,
-    sdpOffer : offerSdp,
+    sdpOffer: offerSdp,
     userId: self.userId,
     userName: self.userName,
-    internalMeetingId: self.internalMeetingId
+    internalMeetingId: self.internalMeetingId,
   };
 
-  this.logger.debug("[onOfferListenOnly]", {message});
+  this.logger.debug('[onOfferListenOnly]', { message });
   this.sendMessage(message);
 };
 
@@ -519,7 +542,7 @@ Kurento.prototype.pauseTrack = function (message) {
   if (track) {
     track.enabled = false;
   }
-}
+};
 
 Kurento.prototype.resumeTrack = function (message) {
   const localStream = this.webRtcPeer.peerConnection.getLocalStreams()[0];
@@ -528,10 +551,10 @@ Kurento.prototype.resumeTrack = function (message) {
   if (track) {
     track.enabled = true;
   }
-}
+};
 
 Kurento.prototype.addIceServers = function (iceServers, options) {
-  this.logger.debug("Adding iceServers", iceServers);
+  this.logger.debug('Adding iceServers', iceServers);
   if (iceServers && iceServers.length > 0) {
     options.configuration = {};
     options.configuration.iceServers = iceServers;
@@ -559,14 +582,14 @@ Kurento.prototype.dispose = function () {
 
 Kurento.prototype.ping = function () {
   const message = {
-    id: 'ping'
+    id: 'ping',
   };
   this.sendMessage(message);
-}
+};
 
 Kurento.prototype.sendMessage = function (message) {
   const jsonMessage = JSON.stringify(message);
-  this.logger.info("Sending message:", {message});
+  this.logger.info('Sending message:', { message });
   this.ws.send(jsonMessage);
 };
 
@@ -635,12 +658,12 @@ window.getScreenConstraints = function (sendSource, callback) {
         { googCpuOveruseDetection: true },
         { googCpuOveruseEncodeUsage: true },
         { googCpuUnderuseThreshold: 55 },
-        { googCpuOveruseThreshold: 100},
+        { googCpuOveruseThreshold: 100 },
         { googPayloadPadding: true },
         { googScreencastMinBitrate: 600 },
         { googHighStartBitrate: true },
         { googHighBitrate: true },
-        { googVeryHighBitrate: true }
+        { googVeryHighBitrate: true },
       ];
 
       console.log('getScreenConstraints for Chrome returns => ', screenConstraints);
@@ -659,7 +682,6 @@ window.getScreenConstraints = function (sendSource, callback) {
   if (isSafari) {
     // At this time (version 11.1), Safari doesn't support screenshare.
     document.dispatchEvent(new Event('safariScreenshareNotSupported'));
-    return;
   }
 };
 
@@ -736,4 +758,4 @@ window.checkChromeExtInstalled = function (callback, chromeExtensionId) {
       callback(true);
     }
   );
-}*/
+} */
diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js
index a5494ed6588973ba5bca3c1e10f2f43311951c7d..e3b8a003d39e81798390c59f75bbf22d5a46d8a0 100644
--- a/bigbluebutton-html5/client/compatibility/kurento-utils.js
+++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js
@@ -287,13 +287,30 @@ function WebRtcPeer(mode, options, callback) {
         return pc.remoteDescription;
     };
     function setRemoteVideo() {
-        if (remoteVideo) {
-            var stream = pc.getRemoteStreams()[0];
-            remoteVideo.pause();
-            remoteVideo.srcObject = stream;
-            remoteVideo.load();
-            logger.info('Remote URL:', remoteVideo.srcObject);
+      if (remoteVideo) {
+        // TODO review the retry - prlanzarin 08/18
+        let played = false;
+        const MAX_RETRIES = 5;
+        let attempt = 0;
+        const playVideo = () => {
+          if (!played && attempt < MAX_RETRIES) {
+            remoteVideo.play().catch(e => {
+                attempt++;
+                playVideo(remoteVideo);
+            }).then(() => { remoteVideo.muted = false; played = true; attempt = 0;});
+          }
         }
+        var stream = pc.getRemoteStreams()[0];
+
+        remoteVideo.oncanplaythrough = function() {
+          playVideo();
+        };
+
+        remoteVideo.pause();
+        remoteVideo.srcObject = stream;
+        remoteVideo.load();
+        logger.info('Remote URL:', remoteVideo.srcObject);
+      }
     }
     this.showLocalVideo = function () {
         localVideo.srcObject = videoStream;
@@ -1045,7 +1062,7 @@ module.exports = function(stream, options) {
   harker.setInterval = function(i) {
     interval = i;
   };
-  
+
   harker.stop = function() {
     running = false;
     harker.emit('volume_change', -100, threshold);
@@ -1063,12 +1080,12 @@ module.exports = function(stream, options) {
   // and emit events if changed
   var looper = function() {
     setTimeout(function() {
-    
+
       //check if stop has been called
       if(!running) {
         return;
       }
-      
+
       var currentVolume = getMaxVolume(analyser, fftBins);
 
       harker.emit('volume_change', currentVolume, threshold);
@@ -4359,4 +4376,4 @@ WildEmitter.mixin = function (constructor) {
 WildEmitter.mixin(WildEmitter);
 
 },{}]},{},[2])(2)
-});
\ No newline at end of file
+});
diff --git a/bigbluebutton-html5/client/stylesheets/bbb-icons.css b/bigbluebutton-html5/client/stylesheets/bbb-icons.css
index 886cea65cac0119ec5640bd003d64c3e6d9b4648..8e934fa833f77188fa6ac14e206d2534e3d8692d 100755
--- a/bigbluebutton-html5/client/stylesheets/bbb-icons.css
+++ b/bigbluebutton-html5/client/stylesheets/bbb-icons.css
@@ -89,6 +89,9 @@
 .icon-bbb-desktop:before {
   content: "\e928";
 }
+.icon-bbb-desktop_off:before {
+  content: "\e953";
+}
 .icon-bbb-logout:before {
   content: "\e900";
 }
diff --git a/bigbluebutton-html5/docker-entrypoint.sh b/bigbluebutton-html5/docker-entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..951269c88166e2c753ad2996e39c5711d0685d2f
--- /dev/null
+++ b/bigbluebutton-html5/docker-entrypoint.sh
@@ -0,0 +1,5 @@
+#!/bin/bash -e
+
+export METEOR_SETTINGS=` jq "${METEOR_SETTINGS_MODIFIER}" ./programs/server/assets/app/config/settings-production.json `
+
+node main.js
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
index 52b7d55cb5f054cfa9ec8cf7db25ea89985dbc82..868cd319a5afbc0261f5bb23d1647cbb56eeff71 100644
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/kurento.js
@@ -53,6 +53,11 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
       sessionToken
     };
 
+    this.media = {
+      inputDevice: {},
+    };
+
+
     this.internalMeetingID = meetingId;
     this.voiceBridge = voiceBridge;
   }
@@ -61,7 +66,7 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
     window.kurentoExitAudio();
   }
 
-  joinAudio({ isListenOnly }, callback) {
+  joinAudio({ isListenOnly, inputStream }, callback) {
     return new Promise(async (resolve, reject) => {
       this.callback = callback;
       let iceServers = [];
@@ -78,15 +83,21 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
           caleeName: `${GLOBAL_AUDIO_PREFIX}${this.voiceBridge}`,
           iceServers,
           logger: modLogger,
+          inputStream,
         };
 
         const onSuccess = ack => resolve(this.callback({ status: this.baseCallStates.started }));
 
-        const onFail = error => resolve(this.callback({
-          status: this.baseCallStates.failed,
-          error: this.baseErrorCodes.CONNECTION_ERROR,
-          bridgeError: error,
-        }));
+        const onFail = error => {
+          const { reason } = error;
+          this.callback({
+            status: this.baseCallStates.failed,
+            error: this.baseErrorCodes.CONNECTION_ERROR,
+            bridgeError: reason,
+          })
+
+          reject(reason);
+        };
 
         if (!isListenOnly) {
           return reject("Invalid bridge option");
@@ -105,6 +116,22 @@ export default class KurentoAudioBridge extends BaseAudioBridge {
     });
   }
 
+  async changeOutputDevice(value) {
+    const audioContext = document.querySelector('#'+MEDIA_TAG);
+    if (audioContext.setSinkId) {
+      try {
+        await audioContext.setSinkId(value);
+        this.media.outputDeviceId = value;
+      } catch (err) {
+        logger.error(err);
+        throw new Error(this.baseErrorCodes.MEDIA_ERROR);
+      }
+    }
+
+    return this.media.outputDeviceId || value;
+  }
+
+
   exitAudio() {
     return new Promise((resolve, reject) => {
       window.kurentoExitAudio();
diff --git a/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js b/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js
index ae7d8a3efc67cc242a1dd722d753a0ce7ba5d62b..d6306b06a5e66dfbd0f249b3998998217bf75215 100755
--- a/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js
+++ b/bigbluebutton-html5/imports/api/log-client/server/methods/logClient.js
@@ -1,13 +1,14 @@
 import Logger from '/imports/startup/server/logger';
 import Users from '/imports/api/users';
 
-const logClient = function (type, log, fullInfo) {
+const logClient = function (type, log, fullInfo = {}) {
   const SERVER_CONN_ID = this.connection.id;
   const User = Users.findOne({ connectionId: SERVER_CONN_ID });
   const logContents = { fullInfo };
 
   if (User) {
-    if (User.meetingId === fullInfo.credentials.meetingId) {
+    if ((fullInfo.credentials && User.meetingId === fullInfo.credentials.meetingId) ||
+      ((fullInfo.meetingId && User.meetingId === fullInfo.meetingId))) {
       logContents.validUser = 'valid';
     } else {
       logContents.validUser = 'invalid';
diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
index f266589972c617ca45545ae285d67ee46193a956..3c22b70b55a99d69445c888bd25c39d6a9c08a8a 100755
--- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
+++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
@@ -3,6 +3,7 @@ import Auth from '/imports/ui/services/auth';
 import BridgeService from './service';
 import { fetchWebRTCMappedStunTurnServers } from '/imports/utils/fetchStunTurnServers';
 import logger from '/imports/startup/client/logger';
+import { notify } from '/imports/ui/services/notification';
 
 const SFU_CONFIG = Meteor.settings.public.kurento;
 const SFU_URL = SFU_CONFIG.wsUrl;
@@ -14,6 +15,8 @@ const SCREENSHARE_VIDEO_TAG = 'screenshareVideo';
 
 const CHROME_EXTENSION_KEY = CHROME_CUSTOM_EXTENSION_KEY === 'KEY' ? CHROME_DEFAULT_EXTENSION_KEY : CHROME_CUSTOM_EXTENSION_KEY;
 
+const ICE_CONNECTION_FAILED = 'ICE connection failed';
+
 const getUserId = () => Auth.userID;
 
 const getMeetingId = () => Auth.meetingID;
@@ -28,17 +31,17 @@ const logFunc = (type, message, options) => {
 
   const topic = options.topic || 'screenshare';
 
-  logger[type]({obj: Object.assign(options, {userId, userName, topic})}, `[${topic}] ${message}`);
+  logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`);
 };
 
 const modLogger = {
-  info: function (message, options = {}) {
+  info(message, options = {}) {
     logFunc('info', message, options);
   },
-  error: function (message, options = {}) {
+  error(message, options = {}) {
     logFunc('error', message, options);
   },
-  debug: function (message, options = {}) {
+  debug(message, options = {}) {
     logFunc('debug', message, options);
   },
   warn: (message, options = {}) => {
@@ -58,7 +61,7 @@ export default class KurentoScreenshareBridge {
       const options = {
         wsUrl: SFU_URL,
         iceServers,
-        logger: modLogger
+        logger: modLogger,
       };
 
       window.kurentoWatchVideo(
@@ -68,16 +71,16 @@ export default class KurentoScreenshareBridge {
         getMeetingId(),
         null,
         null,
-        options
+        options,
       );
-    };
+    }
   }
 
   kurentoExitVideo() {
     window.kurentoExitVideo();
   }
 
-  async kurentoShareScreen() {
+  async kurentoShareScreen(onFail) {
     let iceServers = [];
     try {
       iceServers = await fetchWebRTCMappedStunTurnServers(getSessionToken());
@@ -92,15 +95,14 @@ export default class KurentoScreenshareBridge {
         iceServers,
         logger: modLogger,
       };
-
       window.kurentoShareScreen(
         SCREENSHARE_VIDEO_TAG,
         BridgeService.getConferenceBridge(),
         getUserId(),
         getMeetingId(),
+        onFail,
         null,
-        null,
-        options
+        options,
       );
     }
   }
diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
index 5b0a672f2c30fc67be96bc3764efa3a05643f187..56f5a25debdbbadc4e8049acf7349683cf339ab5 100755
--- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
+++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js
@@ -1,12 +1,14 @@
 import { check } from 'meteor/check';
 import Logger from '/imports/startup/server/logger';
 import Users from '/imports/api/users';
+import Meetings from '/imports/api/meetings';
 
 import stringHash from 'string-hash';
 import flat from 'flat';
 
 import addVoiceUser from '/imports/api/voice-users/server/modifiers/addVoiceUser';
 import changeRole from '/imports/api/users/server/modifiers/changeRole';
+import setApprovedStatus from '/imports/api/users/server/modifiers/setApprovedStatus';
 
 const COLOR_LIST = [
   '#7b1fa2', '#6a1b9a', '#4a148c', '#5e35b1', '#512da8', '#4527a0',
@@ -47,7 +49,9 @@ export default function addUser(meetingId, user) {
   const ROLE_VIEWER = USER_CONFIG.role_viewer;
   const APP_CONFIG = Meteor.settings.public.app;
   const ALLOW_HTML5_MODERATOR = APP_CONFIG.allowHTML5Moderator;
+  const GUEST_ALWAYS_ACCEPT = 'ALWAYS_ACCEPT';
 
+  const Meeting = Meetings.findOne({ meetingId });
   // override moderator status of html5 client users, depending on a system flag
   const dummyUser = Users.findOne(selector);
   let userRole = user.role;
@@ -104,6 +108,10 @@ export default function addUser(meetingId, user) {
       changeRole(ROLE_MODERATOR, true, userId, meetingId);
     }
 
+    if (Meeting.usersProp.guestPolicy === GUEST_ALWAYS_ACCEPT) {
+      setApprovedStatus(meetingId, userId, true);
+    }
+
     const { insertedId } = numChanged;
     if (insertedId) {
       return Logger.info(`Added user id=${userId} meeting=${meetingId}`);
diff --git a/bigbluebutton-html5/imports/startup/client/auth.js b/bigbluebutton-html5/imports/startup/client/auth.js
index 4a0abfb71666ab6f45287c7248f6ce80afe2698a..2621468a393f665ea7078bdc833a52e3eb65169a 100755
--- a/bigbluebutton-html5/imports/startup/client/auth.js
+++ b/bigbluebutton-html5/imports/startup/client/auth.js
@@ -17,7 +17,7 @@ export function joinRouteHandler(nextState, replace, callback) {
     replace({ pathname: '/error/404' });
     callback();
   }
-  
+
   // Old credentials stored in memory were being used when joining a new meeting
   Auth.clearCredentials();
 
@@ -66,7 +66,7 @@ export function joinRouteHandler(nextState, replace, callback) {
           const key = Object.keys(data).shift();
 
           const handledHTML5Parameters = [
-            'html5recordingbot'
+            'html5recordingbot',
           ];
           if (handledHTML5Parameters.indexOf(key) === -1) {
             return acc;
@@ -79,7 +79,7 @@ export function joinRouteHandler(nextState, replace, callback) {
             log('error', `Caught: ${e.message}`);
           }
 
-          return { ...acc, [key]: value};
+          return { ...acc, [key]: value };
         }, {}) : {};
 
       SessionStorage.setItem(METADATA_KEY, metakeys);
@@ -108,7 +108,7 @@ export function joinRouteHandler(nextState, replace, callback) {
 
       replace({ pathname: path });
 
-      logger.info(JSON.stringify(clientInfo));
+      logger.info(clientInfo);
 
       return callback();
     });
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 6dd9e895ddc444b37b95b5e4b21fc69d37bea2e1..afb5e63c05a41ab7bc16ff6ed9341991322c074c 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -26,6 +26,7 @@ const propTypes = {
   subscriptionsReady: PropTypes.bool.isRequired,
   locale: PropTypes.string,
   endedCode: PropTypes.string,
+  approved: PropTypes.bool,
 };
 
 const defaultProps = {
@@ -33,6 +34,7 @@ const defaultProps = {
   errorCode: undefined,
   locale: undefined,
   endedCode: undefined,
+  approved: undefined,
 };
 
 class Base extends Component {
@@ -82,6 +84,7 @@ class Base extends Component {
     }
 
     if (error || errorCode) {
+      logger.error(`User could not log in HTML5, hit ${errorCode}`);
       return (<ErrorScreen code={errorCode}>{error}</ErrorScreen>);
     }
 
@@ -90,6 +93,10 @@ class Base extends Component {
     }
     // this.props.annotationsHandler.stop();
 
+    if (subscriptionsReady) {
+      logger.info('Client loaded successfully');
+    }
+
     return (<AppContainer {...this.props} baseControls={stateControls} />);
   }
 
diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx
index 5b5c1b7e6a9821291c8ffc209902373c699b63f6..3602f33a625a184b04068b8f11fbb8c8d49c2d0f 100644
--- a/bigbluebutton-html5/imports/startup/client/intl.jsx
+++ b/bigbluebutton-html5/imports/startup/client/intl.jsx
@@ -2,13 +2,10 @@ import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import { IntlProvider } from 'react-intl';
 import Settings from '/imports/ui/services/settings';
+import LoadingScreen from '/imports/ui/components/loading-screen/component';
 
 const propTypes = {
-  locale: PropTypes.string.isRequired,
-  baseControls: PropTypes.shape({
-    updateErrorState: PropTypes.func.isRequired,
-    updateLoadingState: PropTypes.func.isRequired,
-  }).isRequired,
+  locale: PropTypes.string,
   children: PropTypes.element.isRequired,
 };
 
@@ -24,7 +21,8 @@ class IntlStartup extends Component {
 
     this.state = {
       messages: {},
-      locale: DEFAULT_LANGUAGE,
+      normalizedLocale: null,
+      fetching: false,
     };
 
     this.fetchLocalizedMessages = this.fetchLocalizedMessages.bind(this);
@@ -34,7 +32,9 @@ class IntlStartup extends Component {
   }
 
   componentWillUpdate(nextProps) {
-    if (nextProps.locale && this.props.locale !== nextProps.locale) {
+    if (!this.state.fetching
+      && this.state.normalizedLocale
+      && nextProps.locale.toLowerCase() !== this.state.normalizedLocale.toLowerCase()) {
       this.fetchLocalizedMessages(nextProps.locale);
     }
   }
@@ -42,37 +42,34 @@ class IntlStartup extends Component {
   fetchLocalizedMessages(locale) {
     const url = `/html5client/locale?locale=${locale}`;
 
-    const { baseControls } = this.props;
+    this.setState({ fetching: true }, () => {
+      fetch(url)
+        .then((response) => {
+          if (!response.ok) {
+            return Promise.reject();
+          }
 
-    baseControls.updateLoadingState(true);
-    fetch(url)
-      .then((response) => {
-        if (!response.ok) {
-          return Promise.reject();
-        }
-
-        return response.json();
-      })
-      .then(({ messages, normalizedLocale }) => {
-        const dasherizedLocale = normalizedLocale.replace('_', '-')
-        this.setState({ messages, locale: dasherizedLocale }, () => {
-          Settings.application.locale = dasherizedLocale;
-          Settings.save();
-          baseControls.updateLoadingState(false);
-        });
-      })
-      .catch((messages) => {
-        this.setState({ locale: DEFAULT_LANGUAGE }, () => {
-          Settings.application.locale = DEFAULT_LANGUAGE;
-          Settings.save();
-          baseControls.updateLoadingState(false);
+          return response.json();
+        })
+        .then(({ messages, normalizedLocale }) => {
+          const dasherizedLocale = normalizedLocale.replace('_', '-');
+          this.setState({ messages, fetching: false, normalizedLocale: dasherizedLocale }, () => {
+            Settings.application.locale = dasherizedLocale;
+            Settings.save();
+          });
+        })
+        .catch(() => {
+          this.setState({ fetching: false, normalizedLocale: null }, () => {
+            Settings.application.locale = DEFAULT_LANGUAGE;
+            Settings.save();
+          });
         });
-      });
+    });
   }
 
   render() {
-    return (
-      <IntlProvider locale={this.state.locale} messages={this.state.messages}>
+    return this.state.fetching ? <LoadingScreen /> : (
+      <IntlProvider locale={this.state.normalizedLocale} messages={this.state.messages}>
         {this.props.children}
       </IntlProvider>
     );
diff --git a/bigbluebutton-html5/imports/startup/client/logger.js b/bigbluebutton-html5/imports/startup/client/logger.js
index a84e70bc4e064589cedb6841fe683df8d27ea6b1..6b8dc02b9be3021b9a7c282b68db1eb7c12fc4a6 100755
--- a/bigbluebutton-html5/imports/startup/client/logger.js
+++ b/bigbluebutton-html5/imports/startup/client/logger.js
@@ -16,25 +16,31 @@ import { nameFromLevel } from '@browser-bunyan/levels';
 // externalURL is the end-point that logs will be sent to
 // Call the logger by doing a function call with the level name, I.e, logger.warn('Hi on warn')
 
-const LOG_CONFIG = Meteor.settings.public.log || {};
-const { fullInfo } = Auth;
+const LOG_CONFIG = Meteor.settings.public.clientLog || { console: { enabled: true, level: 'info' } };
 
 // Custom stream that logs to an end-point
 class ServerLoggerStream extends ServerStream {
   write(rec) {
+    const { fullInfo } = Auth;
+
+    this.rec = rec;
     if (fullInfo.meetingId != null) {
-      rec.clientInfo = fullInfo;
+      this.rec.clientInfo = fullInfo;
     }
-    return super.write(rec);
+    return super.write(this.rec);
   }
 }
+
 // Custom stream to log to the meteor server
 class MeteorStream {
   write(rec) {
+    const { fullInfo } = Auth;
+
+    this.rec = rec;
     if (fullInfo.meetingId != null) {
-      Meteor.call('logClient', nameFromLevel[rec.level], rec.msg, fullInfo);
+      Meteor.call('logClient', nameFromLevel[this.rec.level], this.rec.msg, fullInfo);
     } else {
-      Meteor.call('logClient', nameFromLevel[rec.level], rec.msg);
+      Meteor.call('logClient', nameFromLevel[this.rec.level], this.rec.msg);
     }
   }
 }
@@ -55,16 +61,23 @@ function createStreamForTarget(target, options) {
     case TARGET_SERVER:
       Stream = MeteorStream;
       break;
+    default:
+      Stream = ConsoleFormattedStream;
   }
 
   return new Stream(options);
 }
 
 function generateLoggerStreams(config) {
-  return config.map(({ target, level, ...streamOptions }) => ({
-    level,
-    stream: createStreamForTarget(target, streamOptions),
-  }));
+  let result = [];
+  Object.keys(config).forEach((key) => {
+    const logOption = config[key];
+    if (logOption && logOption.enabled) {
+      const { level, ...streamOptions } = logOption;
+      result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
+    }
+  });
+  return result;
 }
 
 // Creates the logger with the array of streams of the chosen targets
diff --git a/bigbluebutton-html5/imports/startup/server/logger.js b/bigbluebutton-html5/imports/startup/server/logger.js
old mode 100644
new mode 100755
index 98e3d2f12e9ccc447c402275ff46874556232f25..e9eacdd43fe8fdf5ad7697befcedc016ad245d80
--- a/bigbluebutton-html5/imports/startup/server/logger.js
+++ b/bigbluebutton-html5/imports/startup/server/logger.js
@@ -17,7 +17,7 @@ Logger.configure({
 });
 
 Meteor.startup(() => {
-  const LOG_CONFIG = Meteor.settings.private.log || {};
+  const LOG_CONFIG = Meteor.settings.private.serverLog || {};
   const { level } = LOG_CONFIG;
 
   // console logging
@@ -28,7 +28,6 @@ Meteor.startup(() => {
     handleExceptions: true,
     level,
   });
-
 });
 
 export default Logger;
diff --git a/bigbluebutton-html5/imports/startup/server/redis.js b/bigbluebutton-html5/imports/startup/server/redis.js
index 613c28bd740f5f86610d01b7a46bb348d4260fe3..768402d784b1963a8e939ae64762b3c9e397ebce 100644
--- a/bigbluebutton-html5/imports/startup/server/redis.js
+++ b/bigbluebutton-html5/imports/startup/server/redis.js
@@ -105,8 +105,9 @@ class RedisPubSub {
     this.config = config;
 
     this.didSendRequestEvent = false;
-    this.pub = Redis.createClient();
-    this.sub = Redis.createClient();
+    const redisHost = process.env.REDIS_HOST || Meteor.settings.private.redis.host;
+    this.pub = Redis.createClient(Meteor.settings.private.redis.port, redisHost);
+    this.sub = Redis.createClient(Meteor.settings.private.redis.port, redisHost);
     this.emitter = new EventEmitter2();
     this.mettingsQueues = {};
 
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index 207091601ecdcda6be570cfe415db4d02d11ab6b..ed7a2222bbd15a22a0202518f367b1e330951c40 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -1,7 +1,6 @@
 import React from 'react';
 import cx from 'classnames';
 import { styles } from './styles.scss';
-import EmojiSelect from './emoji-select/component';
 import DesktopShare from './desktop-share/component';
 import ActionsDropdown from './actions-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
@@ -16,9 +15,6 @@ class ActionsBar extends React.PureComponent {
       handleShareScreen,
       handleUnshareScreen,
       isVideoBroadcasting,
-      emojiList,
-      emojiSelected,
-      handleEmojiChange,
       isUserModerator,
       recordSettingsList,
       toggleRecording,
@@ -55,13 +51,12 @@ class ActionsBar extends React.PureComponent {
               handleCloseVideo={handleExitVideo}
             />
             : null}
-          <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} />
           <DesktopShare {...{
-handleShareScreen,
-            handleUnshareScreen,
-            isVideoBroadcasting,
-            isUserPresenter,
-}}
+              handleShareScreen,
+              handleUnshareScreen,
+              isVideoBroadcasting,
+              isUserPresenter,
+            }}
           />
         </div>
       </div>
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
index ca2f85b1c1a5f6a6ba9688fdb452254e9f6375e8..ce90a94c153c5783f6e67c2fb1f101597baed895 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -10,12 +10,9 @@ const ActionsBarContainer = props => <ActionsBar {...props} />;
 export default withTracker(() => ({
   isUserPresenter: Service.isUserPresenter(),
   isUserModerator: Service.isUserModerator(),
-  emojiList: Service.getEmojiList(),
-  emojiSelected: Service.getEmoji(),
-  handleEmojiChange: Service.setEmoji,
   handleExitVideo: () => VideoService.exitVideo(),
   handleJoinVideo: () => VideoService.joinVideo(),
-  handleShareScreen: () => shareScreen(),
+  handleShareScreen: (onFail) => shareScreen(onFail),
   handleUnshareScreen: () => unshareScreen(),
   isVideoBroadcasting: isVideoBroadcasting(),
   recordSettingsList: Service.recordSettingsList(),
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
index 86e977deb875bdbdb329e7346a0aef9ccf8a22ae..7329d4774cc7afead29ce78a3198c97533577952 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/desktop-share/component.jsx
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, intlShape } from 'react-intl';
 import browser from 'browser-detect';
 import Button from '/imports/ui/components/button/component';
+import logger from '/imports/startup/client/logger';
+import { notify } from '/imports/ui/services/notification';
 import { styles } from '../styles';
 
 const propTypes = {
@@ -30,13 +32,19 @@ const intlMessages = defineMessages({
     id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc',
     description: 'adds context to stop desktop share option',
   },
-
+  iceConnectionStateError: {
+    id: 'app.deskshare.iceConnectionStateError',
+    description: 'Error message for ice connection state failure',
+  },
 });
 
 const BROWSER_RESULTS = browser();
-const isMobileBrowser = BROWSER_RESULTS.mobile ||
-  BROWSER_RESULTS.os.includes('Android'); // mobile flag doesn't always work
+const isMobileBrowser = (BROWSER_RESULTS ? BROWSER_RESULTS.mobile : false) ||
+  (BROWSER_RESULTS && BROWSER_RESULTS.os ?
+    BROWSER_RESULTS.os.includes('Android') : // mobile flag doesn't always work
+    false);
 const screenSharingCheck = Meteor.settings.public.kurento.enableScreensharing;
+const ICE_CONNECTION_FAILED = 'ICE connection failed';
 
 const DesktopShare = ({
   intl,
@@ -44,25 +52,36 @@ const DesktopShare = ({
   handleUnshareScreen,
   isVideoBroadcasting,
   isUserPresenter,
-}) => (
-  (screenSharingCheck && !isMobileBrowser && isUserPresenter ?
+}) => {
+  const onFail = (error) => {
+    switch (error) {
+      case ICE_CONNECTION_FAILED:
+        kurentoExitScreenShare();
+        logger.error('Ice connection state error');
+        notify(intl.formatMessage(intlMessages.iceConnectionStateError), 'error', 'desktop');
+        break;
+      default:
+        logger.error(error || 'Default error handler');
+    }
+  };
+  return (screenSharingCheck && !isMobileBrowser && isUserPresenter ?
     <Button
       className={styles.button}
-      icon="desktop"
+      icon={isVideoBroadcasting ? 'desktop_off' : 'desktop'}
       label={intl.formatMessage(isVideoBroadcasting ?
           intlMessages.stopDesktopShareLabel : intlMessages.desktopShareLabel)}
       description={intl.formatMessage(isVideoBroadcasting ?
           intlMessages.stopDesktopShareDesc : intlMessages.desktopShareDesc)}
-      color="primary"
+      color={isVideoBroadcasting ? 'danger' : 'primary'}
       ghost={false}
       hideLabel
       circle
       size="lg"
-      onClick={isVideoBroadcasting ? handleUnshareScreen : handleShareScreen}
+      onClick={isVideoBroadcasting ? handleUnshareScreen : () => handleShareScreen(onFail)}
       id={isVideoBroadcasting ? 'unshare-screen-button' : 'share-screen-button'}
     />
-    : null)
-);
+    : null);
+};
 
 DesktopShare.propTypes = propTypes;
 export default injectIntl(DesktopShare);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx
deleted file mode 100755
index 59931acdf3361ae3730f37d48e0211fd6cd77aec..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/emoji-select/component.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { defineMessages, intlShape, injectIntl } from 'react-intl';
-
-import Button from '/imports/ui/components/button/component';
-import Dropdown from '/imports/ui/components/dropdown/component';
-import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
-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 { styles } from '../styles';
-
-const intlMessages = defineMessages({
-  statusTriggerLabel: {
-    id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
-    description: 'Emoji status button label',
-  },
-  changeStatusLabel: {
-    id: 'app.actionsBar.changeStatusLabel',
-    description: 'Aria-label for emoji status button',
-  },
-  currentStatusDesc: {
-    id: 'app.actionsBar.currentStatusDesc',
-    description: 'Aria description for status button',
-  },
-});
-
-const propTypes = {
-  intl: intlShape.isRequired,
-  options: PropTypes.objectOf(PropTypes.string).isRequired,
-  selected: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};
-
-const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
-const OPEN_STATUS_AK = SHORTCUTS_CONFIG.openStatus.accesskey;
-
-const EmojiSelect = ({
-  intl,
-  options,
-  selected,
-  onChange,
-}) => {
-  const statuses = Object.keys(options);
-  const lastStatus = statuses.pop();
-
-  const statusLabel = intl.formatMessage(intlMessages.statusTriggerLabel);
-
-  return (
-    <Dropdown autoFocus>
-      <DropdownTrigger tabIndex={0}>
-        <Button
-          className={styles.button}
-          label={statusLabel}
-          aria-label={statusLabel}
-          aria-describedby="currentStatus"
-          icon={options[selected !== lastStatus ? selected : statuses[1]]}
-          ghost={false}
-          hideLabel
-          circle
-          size="lg"
-          color="primary"
-          onClick={() => null}
-          accessKey={OPEN_STATUS_AK}
-        >
-          <div id="currentStatus" hidden>
-            { intl.formatMessage(intlMessages.currentStatusDesc, { 0: selected }) }
-          </div>
-        </Button>
-      </DropdownTrigger>
-      <DropdownContent placement="top left">
-        <DropdownList>
-          {
-            statuses.map(status => (
-              <DropdownListItem
-                key={status}
-                className={status === selected ? styles.emojiSelected : null}
-                icon={options[status]}
-                label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` })}
-                description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Desc` })}
-                onClick={() => onChange(status)}
-                tabIndex={-1}
-              />
-            ))
-            .concat(
-              <DropdownListSeparator key={-1} />,
-              <DropdownListItem
-                key={lastStatus}
-                icon={options[lastStatus]}
-                label={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Label` })}
-                description={intl.formatMessage({ id: `app.actionsBar.emojiMenu.${lastStatus}Desc` })}
-                onClick={() => onChange(lastStatus)}
-                tabIndex={-1}
-              />,
-            )
-          }
-        </DropdownList>
-      </DropdownContent>
-    </Dropdown>
-  );
-};
-
-EmojiSelect.propTypes = propTypes;
-export default injectIntl(EmojiSelect);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
index 844b7f296634a5539db020ae6e945591f891c511..3eb815607df72101052fe4369cc746cb133dbbc1 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/service.js
@@ -1,14 +1,10 @@
 import Auth from '/imports/ui/services/auth';
 import Users from '/imports/api/users';
 import { makeCall } from '/imports/ui/services/api';
-import { EMOJI_STATUSES } from '/imports/utils/statuses';
 import Meetings from '/imports/api/meetings';
 
 export default {
   isUserPresenter: () => Users.findOne({ userId: Auth.userID }).presenter,
-  getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
-  setEmoji: status => makeCall('setEmojiStatus', Auth.userID, status),
-  getEmojiList: () => EMOJI_STATUSES,
   isUserModerator: () => Users.findOne({ userId: Auth.userID }).moderator,
   recordSettingsList: () => Meetings.findOne({ meetingId: Auth.meetingID }).recordProp,
   toggleRecording: () => makeCall('toggleRecording'),
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
index 5abb5ba57052801eddefe421d197374aad621f8e..4aa485dd4231b76a85cab34a97918feac94412f2 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
@@ -44,9 +44,3 @@
     box-shadow: 0 2px 5px 0 rgb(0, 0, 0);
   }
 }
-
-.emojiSelected {
-  span, i {
-    color: $color-primary;
-  }
-}
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index b958607b2ac56863c791dc74894bf392639f03b9..d317789f84d750057cdab5436f3ddce2b5c5fe16 100755
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -10,7 +10,7 @@ import ToastContainer from '../toast/container';
 import ModalContainer from '../modal/container';
 import NotificationsBarContainer from '../notifications-bar/container';
 import AudioContainer from '../audio/container';
-import ChatNotificationContainer from '../chat/notification/container';
+import ChatAlertContainer from '../chat/alert/container';
 import { styles } from './styles';
 
 const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
@@ -81,8 +81,12 @@ class App extends Component {
 
     const BROWSER_RESULTS = browser();
     const body = document.getElementsByTagName('body')[0];
-    body.classList.add(`browser-${BROWSER_RESULTS.name}`);
-    body.classList.add(`os-${BROWSER_RESULTS.os.split(' ').shift().toLowerCase()}`);
+    if (BROWSER_RESULTS && BROWSER_RESULTS.name) {
+      body.classList.add(`browser-${BROWSER_RESULTS.name}`);
+    }
+    if (BROWSER_RESULTS && BROWSER_RESULTS.os) {
+      body.classList.add(`os-${BROWSER_RESULTS.os.split(' ').shift().toLowerCase()}`);
+    }
 
     this.handleWindowResize();
     window.addEventListener('resize', this.handleWindowResize, false);
@@ -315,7 +319,7 @@ class App extends Component {
         <ModalContainer />
         <AudioContainer />
         <ToastContainer />
-        <ChatNotificationContainer currentChatID={params.chatID} />
+        <ChatAlertContainer currentChatID={params.chatID} />
       </main>
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index 8548d1969ad90f0cf8a033166a50268c7bc91a8e..2f64c668357fc59503ffc4e67fe99a522e7934ee 100755
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -7,6 +7,7 @@ import Auth from '/imports/ui/services/auth';
 import Users from '/imports/api/users';
 import Breakouts from '/imports/api/breakouts';
 import Meetings from '/imports/api/meetings';
+import logger from '/imports/startup/client/logger';
 
 import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container';
 
@@ -73,6 +74,8 @@ export default withRouter(injectIntl(withModalMounter(withTracker(({ router, int
     baseControls.updateLoadingState(intl.formatMessage(intlMessages.waitingApprovalMessage));
   }
 
+  logger.info('User joined meeting and subscribed to data successfully');
+
   // Check if user is removed out of the session
   Users.find({ userId: Auth.userID }).observeChanges({
     changed(id, fields) {
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 bd23bdf1e930ae4644b503a53cd9699846bd6bb2..cc9e6a3c83d84f9d7ef8ec705452f51e4ae50469 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
@@ -324,12 +324,15 @@ class AudioModal extends Component {
     }
     if (this.skipAudioOptions()) {
       return (
-        <span className={styles.connecting} role="alert">
-          {!isEchoTest ?
+        <div className={styles.connecting} role="alert">
+          <span>
+            {!isEchoTest ?
               intl.formatMessage(intlMessages.connecting) :
               intl.formatMessage(intlMessages.connectingEchoTest)
             }
-        </span>
+          </span>
+          <span className={styles.connectingAnimation} />
+        </div>
       );
     }
     return content ? this.contents[content].component() : this.renderAudioOptions();
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 b6b40d1bdcbb5aefc284232177cd145bf6258e7d..7c76c02db3939da9c0539310cbf5e61412550504 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
@@ -132,25 +132,19 @@
   font-size: 2rem;
 }
 
-.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;
-}
-
-@keyframes ellipsis {
-  to {
-    width: 1.25em;
-    margin-right: 0;
+.connectingAnimation{
+  &:after {
+    overflow: hidden;
+    display: inline-block;
+    vertical-align: bottom;
+    animation: ellipsis steps(4,end) 900ms infinite;
+    content: "\2026"; /* ascii code for the ellipsis character */
+    width: 0;
+    margin-right: 1.25em;
   }
 }
 
-@-webkit-keyframes ellipsis {
+@keyframes ellipsis {
   to {
     width: 1.25em;
     margin-right: 0;
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx
similarity index 88%
rename from bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx
rename to bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx
index 5895a2833a906c77196d62005308cf0e41706945..fad03755afaabd844b4edb50d6dd687b4779f683 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/audio-notification/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/alert/audio-alert/component.jsx
@@ -7,7 +7,7 @@ const propTypes = {
   count: PropTypes.number.isRequired,
 };
 
-class ChatAudioNotification extends React.Component {
+class ChatAudioAlert extends React.Component {
   constructor(props) {
     super(props);
     this.audio = new Audio(`${Meteor.settings.public.app.basename}/resources/sounds/notify.mp3`);
@@ -43,6 +43,6 @@ class ChatAudioNotification extends React.Component {
     return null;
   }
 }
-ChatAudioNotification.propTypes = propTypes;
+ChatAudioAlert.propTypes = propTypes;
 
-export default ChatAudioNotification;
+export default ChatAudioAlert;
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx
similarity index 94%
rename from bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
rename to bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx
index 2772a808dc4d2977a262ee6c4a0f334063b99893..b84d4ed0c471afc4fa527e700dfbcf7b2962f543 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/alert/component.jsx
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 import _ from 'lodash';
 import UnreadMessages from '/imports/ui/services/unread-messages';
-import ChatAudioNotification from './audio-notification/component';
-import ChatPushNotification from './push-notification/component';
+import ChatAudioAlert from './audio-alert/component';
+import ChatPushAlert from './push-alert/component';
 import Service from '../service';
 import { styles } from '../styles';
 
@@ -36,7 +36,7 @@ const intlMessages = defineMessages({
 const PUBLIC_KEY = 'public';
 const PRIVATE_KEY = 'private';
 
-class ChatNotification extends Component {
+class ChatAlert extends Component {
   constructor(props) {
     super(props);
     this.state = {
@@ -157,7 +157,7 @@ class ChatNotification extends Component {
             .map(msg => this.createMessage(name, msg.content)));
           const limitingMessages = flatMessages;
 
-          return (<ChatPushNotification
+          return (<ChatPushAlert
             key={id}
             chatId={id}
             content={limitingMessages}
@@ -210,7 +210,7 @@ class ChatNotification extends Component {
       <span>
         {
         chatsNotify.map(({ sender, time, content }) =>
-            (<ChatPushNotification
+            (<ChatPushAlert
               key={time}
               chatId={PUBLIC_KEY}
               name={sender.name}
@@ -249,13 +249,13 @@ class ChatNotification extends Component {
 
     return (
       <span>
-        <ChatAudioNotification play={shouldPlayAudio} count={unreadMessagesCount} />
+        <ChatAudioAlert play={shouldPlayAudio} count={unreadMessagesCount} />
         { this.notifyPublicChat() }
         { this.notifyPrivateChat() }
       </span>
     );
   }
 }
-ChatNotification.propTypes = propTypes;
+ChatAlert.propTypes = propTypes;
 
-export default injectIntl(ChatNotification);
+export default injectIntl(ChatAlert);
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx
similarity index 62%
rename from bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx
rename to bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx
index 89cf326bf3f41cc1568f561cb1fa41ad4264a15f..b0bee3dd6be4cc6ed32f43f4d3adb455ef42f4a0 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/alert/container.jsx
@@ -2,10 +2,10 @@ import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
 import UserListService from '/imports/ui/components/user-list/service';
 import Settings from '/imports/ui/services/settings';
-import ChatNotification from './component';
+import ChatAlert from './component';
 
-const ChatNotificationContainer = props => (
-  <ChatNotification {...props} />
+const ChatAlertContainer = props => (
+  <ChatAlert {...props} />
 );
 
 export default withTracker(() => {
@@ -13,9 +13,9 @@ export default withTracker(() => {
   const openChats = UserListService.getOpenChats();
 
   return {
-    disableAudio: !AppSettings.chatAudioNotifications,
-    disableNotify: !AppSettings.chatPushNotifications,
+    disableAudio: !AppSettings.chatAudioAlerts,
+    disableNotify: !AppSettings.chatPushAlerts,
     openChats,
     publicUserId: Meteor.settings.public.chat.public_group_id,
   };
-})(ChatNotificationContainer);
+})(ChatAlertContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx
similarity index 64%
rename from bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx
rename to bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx
index 78ad2d0e091d826f75989b1050f24d031b11c3a4..19a0ddc66cee0dd7181a2969b0ecd1bfb2a01d2d 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/notification/push-notification/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/alert/push-alert/component.jsx
@@ -5,15 +5,15 @@ import injectNotify from '/imports/ui/components/toast/inject-notify/component';
 import { Link } from 'react-router';
 import { styles } from '../../styles.scss';
 
-const NOTIFICATION_INTERVAL = 2000; // 2 seconds
-const NOTIFICATION_LIFETIME = 4000; // 4 seconds
+const ALERT_INTERVAL = 2000; // 2 seconds
+const ALERT_LIFETIME = 4000; // 4 seconds
 
 const propTypes = {
   notify: PropTypes.func.isRequired,
   onOpen: PropTypes.func.isRequired,
 };
 
-class ChatPushNotification extends React.Component {
+class ChatPushAlert extends React.Component {
   static link(message, chatId) {
     return (
       <Link className={styles.link} to={`/users/chat/${chatId}`}>
@@ -24,7 +24,7 @@ class ChatPushNotification extends React.Component {
 
   constructor(props) {
     super(props);
-    this.showNotify = _.debounce(this.showNotify.bind(this), NOTIFICATION_INTERVAL);
+    this.showNotify = _.debounce(this.showNotify.bind(this), ALERT_INTERVAL);
 
     this.componentDidMount = this.showNotify;
     this.componentDidUpdate = this.showNotify;
@@ -40,11 +40,11 @@ class ChatPushNotification extends React.Component {
     } = this.props;
 
     return notify(
-      ChatPushNotification.link(message, chatId),
+      ChatPushAlert.link(message, chatId),
       'info',
       'chat',
-      { onOpen, autoClose: NOTIFICATION_LIFETIME },
-      ChatPushNotification.link(content, chatId),
+      { onOpen, autoClose: ALERT_LIFETIME },
+      ChatPushAlert.link(content, chatId),
       true,
     );
   }
@@ -53,6 +53,6 @@ class ChatPushNotification extends React.Component {
     return null;
   }
 }
-ChatPushNotification.propTypes = propTypes;
+ChatPushAlert.propTypes = propTypes;
 
-export default injectNotify(ChatPushNotification);
+export default injectNotify(ChatPushAlert);
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
index 3666f2d5d4dee7fc7180e7ac299827ccb5410294..4fb0147dbee1df87b3570f6551576e1bc2bd90f5 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
@@ -1,7 +1,7 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
 .item {
-  font-size: $font-size-base * .90;
+  font-size: $font-size-base;
   margin-bottom: $line-height-computed;
 
   &:last-child {
diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js
index 633ba2847a8b89cab30c6d5f711ab7833e37d2c9..a203aab4981330bb88a28a8dd318b410005ab6c4 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/service.js
+++ b/bigbluebutton-html5/imports/ui/components/chat/service.js
@@ -115,15 +115,14 @@ const isChatLocked = (receiverID) => {
   const isPublic = receiverID === PUBLIC_CHAT_ID;
 
   const meeting = Meetings.findOne({});
-  const user = Users.findOne({});
+  const user = Users.findOne({ userId: Auth.userID });
 
   if (meeting.lockSettingsProp !== undefined) {
     const isPubChatLocked = meeting.lockSettingsProp.disablePubChat;
     const isPrivChatLocked = meeting.lockSettingsProp.disablePrivChat;
-    const isViewer = user.role === 'VIEWER';
 
-    return (isPublic && isPubChatLocked && isViewer && user.locked)
-      || (!isPublic && isPrivChatLocked && isViewer && user.locked);
+    return mapUser(user).isLocked &&
+      ((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
   }
 
   return false;
diff --git a/bigbluebutton-html5/imports/ui/components/chat/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/styles.scss
index e33eb5c6fe36e53cbc9fdb064ad67e6520354eea..8889401573e40d5e2d2c7449052493ddaa670344 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/styles.scss
@@ -44,6 +44,7 @@ $background-active: darken($color-white, 5%);
   flex-direction: column;
   justify-content: space-around;
   overflow: hidden;
+  height: 100vh;
 }
 
 .header {
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx
index bc6d9d243d32d8affcb811e435877ca9651f6b45..bd868773200d9f6cff8a63d8bff52a56c8ef6c5c 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/component.jsx
@@ -73,15 +73,15 @@ class Dropdown extends Component {
     return nextState.isOpen ? screenreaderTrap.trap(this.dropdown) : screenreaderTrap.untrap();
   }
 
-
   componentDidUpdate(prevProps, prevState) {
-    if (this.state.isOpen && !prevState.isOpen) {
-      this.props.onShow();
-    }
+    const {
+      onShow,
+      onHide,
+    } = this.props;
 
-    if (!this.state.isOpen && prevState.isOpen) {
-      this.props.onHide();
-    }
+    if (this.state.isOpen && !prevState.isOpen) { onShow(); }
+
+    if (!this.state.isOpen && prevState.isOpen) { onHide(); }
   }
 
   handleShow() {
@@ -98,14 +98,17 @@ class Dropdown extends Component {
     });
   }
 
-  handleWindowClick(event) {
+  handleWindowClick() {
     const triggerElement = findDOMNode(this.trigger);
+    const contentElement = findDOMNode(this.content);
+    const closeDropdown = this.props.isOpen && this.state.isOpen && triggerElement.contains(event.target);
+    const preventHide = this.props.isOpen && contentElement.contains(event.target) || !triggerElement;
 
-    if (!triggerElement) return;
+    if (closeDropdown) {
+      return this.props.onHide();
+    }
 
-    if (!this.state.isOpen
-      || triggerElement === event.target
-      || triggerElement.contains(event.target)) {
+    if (contentElement && preventHide) {
       return;
     }
 
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx
index 744331f4197d0bc0d77609c0ed57deebee15aed3..95496e1bad2d39b88a75fd830c92a45cb8405884 100755
--- a/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/component.jsx
@@ -6,7 +6,6 @@ import { styles } from './styles';
 import ListItem from './item/component';
 import ListSeparator from './separator/component';
 import ListTitle from './title/component';
-import UserActions from '../../user-list/user-list-content/user-participants/user-list-item/user-action/component';
 
 const propTypes = {
   /*  We should recheck this proptype, sometimes we need to create an container and send to dropdown,
@@ -15,8 +14,7 @@ const propTypes = {
   children: PropTypes.arrayOf((propValue, key, componentName, location, propFullName) => {
     if (propValue[key].type !== ListItem &&
       propValue[key].type !== ListSeparator &&
-      propValue[key].type !== ListTitle &&
-      propValue[key].type !== UserActions) {
+      propValue[key].type !== ListTitle) {
       return new Error(`Invalid prop \`${propFullName}\` supplied to` +
         ` \`${componentName}\`. Validation failed.`);
     }
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx
index cb7f49b57ee9b11d10c8c1431fe3ce57161e4826..41c158f9dadd9f22fd97153df0ccdcec7119ab51 100644
--- a/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/item/component.jsx
@@ -26,11 +26,12 @@ export default class DropdownListItem extends Component {
   }
 
   renderDefault() {
-    const { icon, label } = this.props;
+    const { icon, label, iconRight } = this.props;
 
     return [
       (icon ? <Icon iconName={icon} key="icon" className={styles.itemIcon} /> : null),
       (<span className={styles.itemLabel} key="label">{label}</span>),
+      (iconRight ? <Icon iconName={iconRight} key="iconRight" className={styles.iconRight} /> : null),
     ];
   }
 
diff --git a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
index 991fc52f73855432ee8a0f46c0e3df155130b888..06f65f09c8bbb9d8525a60b2f68aeb60cc6b75fd 100755
--- a/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/dropdown/list/styles.scss
@@ -1,5 +1,8 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
+$more-icon-font-size: 12px;
+$more-icon-line-height: 16px;
+
 %list {
   list-style: none;
   font-size: $font-size-base;
@@ -107,6 +110,7 @@
       border-radius: 0.2rem;
     }
 
+    .iconRight,
     .itemIcon,
     .itemLabel {
       color: inherit;
@@ -118,12 +122,20 @@
   }
 }
 
+.iconRight,
 .itemIcon {
   margin-right: ($line-height-computed / 2);
   color: $color-text;
   flex: 0 0;
 }
 
+.iconRight {
+  margin-right: -$indicator-padding-left;
+  margin-left: $sm-padding-x;
+  font-size: $more-icon-font-size;
+  line-height: $more-icon-line-height;
+}
+
 .itemLabel {
   color: $color-gray-dark;
   font-size: 90%;
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss
index b569419dc2a30586a3be5378971080b33be7d62b..1be766e2959a60b4efd4f25707cb060ef251f6ea 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/recording-indicator/styles.scss
@@ -30,6 +30,8 @@
   overflow: hidden;
   text-overflow: ellipsis;
   vertical-align: middle;
+  max-width: 20vw;
+ 
 }
 
 .recordIndicator {
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
index 66d9ff5458ca768fe6520dbc0a5eb6cc8499eb0e..50f3859de848779befcd699a07df2e32c40a09ca 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/component.jsx
@@ -73,6 +73,14 @@ const intlMessages = defineMessages({
     id: 'app.navBar.settingsDropdown.hotkeysDesc',
     description: 'Describes hotkeys option',
   },
+  helpLabel: {
+    id: 'app.navBar.settingsDropdown.helpLabel',
+    description: 'Help options label',
+  },
+  helpDesc: {
+    id: 'app.navBar.settingsDropdown.helpDesc',
+    description: 'Describes help option',
+  },
 });
 
 const SHORTCUTS_CONFIG = Meteor.settings.public.app.shortcuts;
@@ -93,8 +101,9 @@ class SettingsDropdown extends Component {
   componentWillMount() {
     const { intl, mountModal, isAndroid } = this.props;
     const { fullscreenLabel, fullscreenDesc, fullscreenIcon } = this.checkFullscreen(this.props);
+    const { showHelpButton: helpButton } = Meteor.settings.public.app;
 
-    this.menuItems = [(<DropdownListItem
+    this.menuItems =_.compact( [(<DropdownListItem
       key={_.uniqueId('list-item-')}
       icon={fullscreenIcon}
       label={fullscreenLabel}
@@ -115,9 +124,17 @@ class SettingsDropdown extends Component {
         description={intl.formatMessage(intlMessages.aboutDesc)}
         onClick={() => mountModal(<AboutContainer />)}
       />),
+      !helpButton ? null :
       (<DropdownListItem
         key={_.uniqueId('list-item-')}
-        icon="about"
+        icon="help"
+        label={intl.formatMessage(intlMessages.helpLabel)}
+        description={intl.formatMessage(intlMessages.helpDesc)}
+        onClick={() => window.open('https://bigbluebutton.org/videos/')}
+      />),
+      (<DropdownListItem
+        key={_.uniqueId('list-item-')}
+        icon="shortcuts"
         label={intl.formatMessage(intlMessages.hotkeysLabel)}
         description={intl.formatMessage(intlMessages.hotkeysDesc)}
         onClick={() => mountModal(<ShortcutHelpComponent />)}
@@ -130,12 +147,14 @@ class SettingsDropdown extends Component {
         description={intl.formatMessage(intlMessages.leaveSessionDesc)}
         onClick={() => mountModal(<LogoutConfirmationContainer />)}
       />),
-    ];
+    ])
 
     // Removes fullscreen button if not on Android
     if (!isAndroid) {
       this.menuItems.shift();
     }
+
+
   }
 
   componentWillReceiveProps(nextProps) {
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx
index b068f1faa22bb7df870e252dbb0444ebd516a2ff..6a13b39a6e9d209806b96997dfa2f84dd4bff391 100755
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/settings-dropdown/container.jsx
@@ -53,7 +53,7 @@ export default class SettingsDropdownContainer extends Component {
     const handleToggleFullscreen = toggleFullScreen;
     const isFullScreen = this.state.isFullScreen;
     const result = browser();
-    const isAndroid = result.os.includes('Android');
+    const isAndroid = (result && result.os) ? result.os.includes('Android') : false;
 
     return (
       <SettingsDropdown
diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss
old mode 100644
new mode 100755
index d9f228f7a6491a93d54a30e8b300cde869a921bf..473924509538585cf89c471cb247a757f0f7eeaa
--- a/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/nav-bar/styles.scss
@@ -33,7 +33,8 @@
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
-
+  max-width: 30vw;
+  
   > [class^="icon-bbb-"] {
     font-size: 75%;
   }
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss
index d9d863df263d716d28f08487c3be46bb9bc5a6f2..63b7cadc87464cc58a02465a02e1429fd0f99b09 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss
@@ -3,6 +3,7 @@
 
 $controls-color: $color-gray !default;
 $controls-background: $color-white !default;
+$toolbar-button-border-radius: 5px;
 
 .presentationToolbarWrapper,
 .zoomWrapper {
@@ -16,6 +17,7 @@ $controls-background: $color-white !default;
   position: absolute;
   bottom: .8rem;
   box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25);
+  border-radius: $toolbar-button-border-radius;
   align-self: center;
   justify-content: center;
   z-index: 1;
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
index 5c7965dcaa919e317b4784ea75a5634b8a5926a0..99e61aae0b1506660d33dcb707793e1756b87852 100644
--- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js
@@ -13,8 +13,9 @@ const isVideoBroadcasting = () => {
     return false;
   }
 
+  const hasStream = ds.screenshare.stream ? true : false;
   // TODO commented out isPresenter to enable screen viewing to the presenter
-  return ds.screenshare.stream; // && !PresentationService.isPresenter();
+  return hasStream; // && !PresentationService.isPresenter();
 }
 
 // if remote screenshare has been ended disconnect and hide the video stream
@@ -32,8 +33,8 @@ const presenterScreenshareHasStarted = () => {
   KurentoBridge.kurentoWatchVideo();
 }
 
-const shareScreen = () => {
-  KurentoBridge.kurentoShareScreen();
+const shareScreen = (onFail) => {
+  KurentoBridge.kurentoShareScreen(onFail);
 }
 
 const unshareScreen = () => {
diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
index 010bed8fb264748c57e2727aba33ef6ed2a5d8c6..cbd75e98268a1b093451823559d97b2102a84837 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/application/component.jsx
@@ -14,12 +14,12 @@ const intlMessages = defineMessages({
     id: 'app.submenu.application.applicationSectionTitle',
     description: 'Application section title',
   },
-  audioNotifyLabel: {
-    id: 'app.submenu.application.audioNotifyLabel',
+  audioAlertLabel: {
+    id: 'app.submenu.application.audioAlertLabel',
     description: 'audio notification label',
   },
-  pushNotifyLabel: {
-    id: 'app.submenu.application.pushNotifyLabel',
+  pushAlertLabel: {
+    id: 'app.submenu.application.pushAlertLabel',
     description: 'push notifiation label',
   },
   fontSizeControlLabel: {
@@ -117,14 +117,6 @@ class ApplicationMenu extends BaseMenu {
     this.handleUpdateSettings('application', obj.settings);
   }
 
-  // Adjust the locale format to be able to display the locale names properly in the client
-  formatLocale(locale) {
-    return locale
-      .split('-')
-      .map((val, idx) => (idx == 1 ? val.toUpperCase() : val))
-      .join('_');
-  }
-
   render() {
     const { availableLocales, intl } = this.props;
     const { isLargestFontSize, isSmallestFontSize } = this.state;
@@ -141,7 +133,7 @@ class ApplicationMenu extends BaseMenu {
             <div className={styles.col} aria-hidden="true">
               <div className={styles.formElement}>
                 <label className={styles.label}>
-                  {intl.formatMessage(intlMessages.audioNotifyLabel)}
+                  {intl.formatMessage(intlMessages.audioAlertLabel)}
                 </label>
               </div>
             </div>
@@ -149,9 +141,9 @@ class ApplicationMenu extends BaseMenu {
               <div className={cx(styles.formElement, styles.pullContentRight)}>
                 <Toggle
                   icons={false}
-                  defaultChecked={this.state.settings.chatAudioNotifications}
-                  onChange={() => this.handleToggle('chatAudioNotifications')}
-                  ariaLabel={intl.formatMessage(intlMessages.audioNotifyLabel)}
+                  defaultChecked={this.state.settings.chatAudioAlerts}
+                  onChange={() => this.handleToggle('chatAudioAlerts')}
+                  ariaLabel={intl.formatMessage(intlMessages.audioAlertLabel)}
                 />
               </div>
             </div>
@@ -161,7 +153,7 @@ class ApplicationMenu extends BaseMenu {
             <div className={styles.col} aria-hidden="true">
               <div className={styles.formElement}>
                 <label className={styles.label}>
-                  {intl.formatMessage(intlMessages.pushNotifyLabel)}
+                  {intl.formatMessage(intlMessages.pushAlertLabel)}
                 </label>
               </div>
             </div>
@@ -169,9 +161,9 @@ class ApplicationMenu extends BaseMenu {
               <div className={cx(styles.formElement, styles.pullContentRight)}>
                 <Toggle
                   icons={false}
-                  defaultChecked={this.state.settings.chatPushNotifications}
-                  onChange={() => this.handleToggle('chatPushNotifications')}
-                  ariaLabel={intl.formatMessage(intlMessages.pushNotifyLabel)}
+                  defaultChecked={this.state.settings.chatPushAlerts}
+                  onChange={() => this.handleToggle('chatPushAlerts')}
+                  ariaLabel={intl.formatMessage(intlMessages.pushAlertLabel)}
                 />
               </div>
             </div>
diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx
old mode 100644
new mode 100755
index 3db4cf1e280ddb0a9261d3fe0c3391b6f7689337..a0ab4b21c1ce9ca26945f538a120a0705488fd97
--- a/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-avatar/component.jsx
@@ -38,27 +38,34 @@ const UserAvatar = ({
   voice,
   className,
 }) => (
-  <div
-    aria-hidden="true"
-    data-test="userAvatar"
-    className={cx(styles.avatar, {
-      [styles.moderator]: moderator,
-      [styles.presenter]: presenter,
-      [styles.muted]: muted,
-      [styles.listenOnly]: listenOnly,
-      [styles.talking]: (talking && !muted),
-      [styles.voice]: voice,
-    }, className)}
-    style={{
-      backgroundColor: color,
-      color, // We need the same color on both for the border
-    }}
-  >
-    <div className={styles.content}>
-      {children}
+
+    <div
+      aria-hidden="true"
+      data-test="userAvatar"
+      className={cx(styles.avatar, {
+        [styles.moderator]: moderator,
+        [styles.presenter]: presenter,
+        [styles.muted]: muted,
+        [styles.listenOnly]: listenOnly,
+        [styles.voice]: voice,
+      }, className)}
+      style={{
+        backgroundColor: color,
+        color, // We need the same color on both for the border
+      }}
+    >
+
+      <div className={cx({
+        [styles.talking]: (talking && !muted),
+      })}
+      />
+
+
+      <div className={styles.content}>
+        {children}
+      </div>
     </div>
-  </div>
-);
+  );
 
 UserAvatar.propTypes = propTypes;
 UserAvatar.defaultProps = defaultProps;
diff --git a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
index 2dd42cbc15353c2d365e5f761d358058204102f7..77f652bd10f9468a070c5a6fbcc90aa84ddde85b 100755
--- a/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-avatar/styles.scss
@@ -46,22 +46,30 @@ $user-color: currentColor; //picks the current color reference in the class
     text-align: center;
     vertical-align: middle;
     letter-spacing: -.65rem;
+    z-index: 1;
   }
 }
 
 .talking {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background-color: $user-color;
+  border-radius: inherit;
   animation: pulse 1s infinite ease-in;
 }
 
+
 @keyframes pulse {
   0% {
-    box-shadow: 0 0 0 0 $user-color;
-  }
-  70% {
-    box-shadow: 0 0 0 0.5625rem transparent;
+    opacity: 1;
+    transform: scale(1);
   }
   100% {
-    box-shadow: 0 0 0 0 transparent;
+    opacity: 0;
+    transform: scale(1.5);
   }
 }
 
@@ -92,6 +100,7 @@ $user-color: currentColor; //picks the current color reference in the class
   }
 }
 
+
 .listenOnly {
   &:after {
     content: "\00a0\e90c\00a0";
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
index 2bcd6de25444f9535fadc261060d000f98277579..948345a9546702533ab75175250fdfef0b25edb7 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
@@ -64,6 +64,9 @@ class UserList extends Component {
       roving,
       CustomLogoUrl,
       getGroupChatPrivate,
+      handleEmojiChange,
+      getEmojiList,
+      getEmoji,
     } = this.props;
 
     return (
@@ -94,6 +97,9 @@ class UserList extends Component {
           isPublicChat,
           roving,
           getGroupChatPrivate,
+          handleEmojiChange,
+          getEmojiList,
+          getEmoji,
         }
       }
         />}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
index a21bd8419cbe0279b2b3e6998877f98e324c157e..6681437009194483abc1b3b59fe424bf3acae5b7 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
@@ -48,4 +48,7 @@ export default withTracker(({ chatID, compact }) => ({
   CustomLogoUrl: Service.getCustomLogoUrl(),
   compact,
   getGroupChatPrivate: Service.getGroupChatPrivate,
+  handleEmojiChange: Service.setEmojiStatus,
+  getEmojiList: Service.getEmojiList(),
+  getEmoji: Service.getEmoji(),
 }))(UserListContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 5922bb0220eb4e1f693bd19a0ab0206c31b53ef1..b54412be33cc08a3d24abe26550e0583bf685222 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -61,15 +61,15 @@ const sortUsersByEmoji = (a, b) => {
   const emojiA = statusA in EMOJI_STATUSES ? EMOJI_STATUSES[statusA] : statusA;
   const emojiB = statusB in EMOJI_STATUSES ? EMOJI_STATUSES[statusB] : statusB;
 
-  if (emojiA && emojiB && (emojiA !== EMOJI_STATUSES.none && emojiB !== EMOJI_STATUSES.none)) {
+  if (emojiA && emojiB && (emojiA !== 'none' && emojiB !== 'none')) {
     if (a.emoji.changedAt < b.emoji.changedAt) {
       return -1;
     } else if (a.emoji.changedAt > b.emoji.changedAt) {
       return 1;
     }
-  } else if (emojiA && emojiA !== EMOJI_STATUSES.none) {
+  } else if (emojiA && emojiA !== 'none') {
     return -1;
-  } else if (emojiB && emojiB !== EMOJI_STATUSES.none) {
+  } else if (emojiB && emojiB !== 'none') {
     return 1;
   }
   return 0;
@@ -297,6 +297,8 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
       && user.isModerator
       && !isDialInUser;
 
+  const allowedToChangeStatus = user.isCurrent;
+
   return {
     allowedToChatPrivately,
     allowedToMuteAudio,
@@ -306,6 +308,7 @@ const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
     allowedToSetPresenter,
     allowedToPromote,
     allowedToDemote,
+    allowedToChangeStatus,
   };
 };
 
@@ -338,7 +341,13 @@ const isMeetingLocked = (id) => {
   return isLocked;
 };
 
-const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none'); };
+const setEmojiStatus = (data) => {
+  const statusAvailable = (Object.keys(EMOJI_STATUSES).includes(data));
+
+  return statusAvailable
+    ? makeCall('setEmojiStatus', Auth.userID, data)
+    : makeCall('setEmojiStatus', data, 'none');
+};
 
 const assignPresenter = (userId) => { makeCall('assignPresenter', userId); };
 
@@ -438,4 +447,7 @@ export default {
   setCustomLogoUrl,
   getCustomLogoUrl,
   getGroupChatPrivate,
+  getEmojiList: () => EMOJI_STATUSES,
+  getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
 };
+
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
index 227f2159bad4028958d49c33f2ee1209ff9ea36f..231a8df908535c2dda4b5a337925b877241b70c1 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import { styles } from './styles';
 import UserParticipants from './user-participants/component';
@@ -35,8 +35,32 @@ const defaultProps = {
   meeting: {},
 };
 
-class UserContent extends Component {
+class UserContent extends React.PureComponent {
   render() {
+    const {
+      users,
+      compact,
+      intl,
+      currentUser,
+      meeting,
+      isBreakoutRoom,
+      setEmojiStatus,
+      assignPresenter,
+      removeUser,
+      toggleVoice,
+      changeRole,
+      getAvailableActions,
+      normalizeEmojiName,
+      isMeetingLocked,
+      roving,
+      handleEmojiChange,
+      getEmojiList,
+      getEmoji,
+      isPublicChat,
+      openChats,
+      getGroupChatPrivate,
+    } = this.props;
+
     return (
       <div
         data-test="userListContent"
@@ -44,29 +68,36 @@ class UserContent extends Component {
         role="complementary"
       >
         <UserMessages
-          isPublicChat={this.props.isPublicChat}
-          openChats={this.props.openChats}
-          compact={this.props.compact}
-          intl={this.props.intl}
-          roving={this.props.roving}
+          {...{
+            isPublicChat,
+            openChats,
+            compact,
+            intl,
+            roving,
+          }}
         />
         <UserParticipants
-          users={this.props.users}
-          compact={this.props.compact}
-          intl={this.props.intl}
-          currentUser={this.props.currentUser}
-          meeting={this.props.meeting}
-          isBreakoutRoom={this.props.isBreakoutRoom}
-          setEmojiStatus={this.props.setEmojiStatus}
-          assignPresenter={this.props.assignPresenter}
-          removeUser={this.props.removeUser}
-          toggleVoice={this.props.toggleVoice}
-          changeRole={this.props.changeRole}
-          getAvailableActions={this.props.getAvailableActions}
-          normalizeEmojiName={this.props.normalizeEmojiName}
-          isMeetingLocked={this.props.isMeetingLocked}
-          roving={this.props.roving}
-          getGroupChatPrivate={this.props.getGroupChatPrivate}
+          {...{
+            users,
+            compact,
+            intl,
+            currentUser,
+            meeting,
+            isBreakoutRoom,
+            setEmojiStatus,
+            assignPresenter,
+            removeUser,
+            toggleVoice,
+            changeRole,
+            getAvailableActions,
+            normalizeEmojiName,
+            isMeetingLocked,
+            roving,
+            handleEmojiChange,
+            getEmojiList,
+            getEmoji,
+            getGroupChatPrivate,
+          }}
         />
       </div>
     );
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
index ab97d5a7805af7d65026c9c83106ad616a5f7bbc..3d45555ebee6588132879a835b2915bfd6ed4e4c 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from 'react';
 import { TransitionGroup, CSSTransition } from 'react-transition-group';
-import PropTypes from 'prop-types';
 import { defineMessages } from 'react-intl';
+import PropTypes from 'prop-types';
 import cx from 'classnames';
 import { styles } from '/imports/ui/components/user-list/user-list-content/styles';
 import UserListItem from './user-list-item/component';
@@ -48,38 +48,6 @@ const intlMessages = defineMessages({
     id: 'app.userList.usersTitle',
     description: 'Title for the Header',
   },
-  ChatLabel: {
-    id: 'app.userList.menu.chat.label',
-    description: 'Save the changes and close the settings menu',
-  },
-  ClearStatusLabel: {
-    id: 'app.userList.menu.clearStatus.label',
-    description: 'Clear the emoji status of this user',
-  },
-  MakePresenterLabel: {
-    id: 'app.userList.menu.makePresenter.label',
-    description: 'Set this user to be the presenter in this meeting',
-  },
-  RemoveUserLabel: {
-    id: 'app.userList.menu.removeUser.label',
-    description: 'Forcefully remove this user from the meeting',
-  },
-  MuteUserAudioLabel: {
-    id: 'app.userList.menu.muteUserAudio.label',
-    description: 'Forcefully mute this user',
-  },
-  UnmuteUserAudioLabel: {
-    id: 'app.userList.menu.unmuteUserAudio.label',
-    description: 'Forcefully unmute this user',
-  },
-  PromoteUserLabel: {
-    id: 'app.userList.menu.promoteUser.label',
-    description: 'Forcefully promote this viewer to a moderator',
-  },
-  DemoteUserLabel: {
-    id: 'app.userList.menu.demoteUser.label',
-    description: 'Forcefully demote this moderator to a viewer',
-  },
 });
 
 class UserParticipants extends Component {
@@ -136,62 +104,17 @@ class UserParticipants extends Component {
       normalizeEmojiName,
       isMeetingLocked,
       users,
-      intl,
       changeRole,
       assignPresenter,
       setEmojiStatus,
       removeUser,
       toggleVoice,
-      getGroupChatPrivate,
+      getGroupChatPrivate, //// TODO check if this is used
+      handleEmojiChange, //// TODO add to props validation
+      getEmojiList,
+      getEmoji,
     } = this.props;
 
-    const userActions =
-    {
-      openChat: {
-        label: () => intl.formatMessage(intlMessages.ChatLabel),
-        handler: (router, user) => {
-          getGroupChatPrivate(currentUser, user);
-          router.push(`/users/chat/${user.id}`);
-        },
-        icon: 'chat',
-      },
-      clearStatus: {
-        label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
-        handler: user => setEmojiStatus(user.id, 'none'),
-        icon: 'clear_status',
-      },
-      setPresenter: {
-        label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
-        handler: user => assignPresenter(user.id),
-        icon: 'presentation',
-      },
-      remove: {
-        label: user => intl.formatMessage(intlMessages.RemoveUserLabel, { 0: user.name }),
-        handler: user => removeUser(user.id),
-        icon: 'circle_close',
-      },
-      mute: {
-        label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
-        handler: user => toggleVoice(user.id),
-        icon: 'mute',
-      },
-      unmute: {
-        label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
-        handler: user => toggleVoice(user.id),
-        icon: 'unmute',
-      },
-      promote: {
-        label: () => intl.formatMessage(intlMessages.PromoteUserLabel),
-        handler: user => changeRole(user.id, 'MODERATOR'),
-        icon: 'promote',
-      },
-      demote: {
-        label: () => intl.formatMessage(intlMessages.DemoteUserLabel),
-        handler: user => changeRole(user.id, 'VIEWER'),
-        icon: 'user',
-      },
-    };
-
     let index = -1;
 
     return users.map(user => (
@@ -207,15 +130,24 @@ class UserParticipants extends Component {
       >
         <div ref={(node) => { this.userRefs[index += 1] = node; }}>
           <UserListItem
-            compact={compact}
-            isBreakoutRoom={isBreakoutRoom}
-            user={user}
-            currentUser={currentUser}
-            userActions={userActions}
-            meeting={meeting}
-            getAvailableActions={getAvailableActions}
-            normalizeEmojiName={normalizeEmojiName}
-            isMeetingLocked={isMeetingLocked}
+            {...{
+              user,
+              currentUser,
+              compact,
+              isBreakoutRoom,
+              meeting,
+              getAvailableActions,
+              normalizeEmojiName,
+              isMeetingLocked,
+              handleEmojiChange,
+              getEmojiList,
+              getEmoji,
+              setEmojiStatus,
+              assignPresenter,
+              removeUser,
+              toggleVoice,
+              changeRole,
+            }}
             getScrollContainerRef={this.getScrollContainerRef}
           />
         </div>
@@ -224,9 +156,7 @@ class UserParticipants extends Component {
   }
 
   focusUserItem(index) {
-    if (!this.userRefs[index]) {
-      return;
-    }
+    if (!this.userRefs[index]) return;
 
     this.userRefs[index].firstChild.focus();
   }
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
index 26d2c09dc9acf5647b53ce5f085359ab838b4be4..e3e4fc585745629138d818f7b08b6c898dabe522 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/component.jsx
@@ -2,9 +2,7 @@ import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import { withRouter } from 'react-router';
 import { injectIntl } from 'react-intl';
-import _ from 'lodash';
-import UserListContent from './user-list-content/component';
-import UserAction from './user-action/component';
+import UserDropdown from './user-dropdown/component';
 
 const propTypes = {
   user: PropTypes.shape({
@@ -23,7 +21,6 @@ const propTypes = {
   intl: PropTypes.shape({
     formatMessage: PropTypes.func.isRequired,
   }).isRequired,
-  userActions: PropTypes.shape({}).isRequired,
   router: PropTypes.shape({}).isRequired,
   isBreakoutRoom: PropTypes.bool,
   getAvailableActions: PropTypes.func.isRequired,
@@ -38,64 +35,6 @@ const defaultProps = {
 };
 
 class UserListItem extends Component {
-  static createAction(action, ...options) {
-    return (
-      <UserAction
-        key={_.uniqueId('action-item-')}
-        icon={action.icon}
-        label={action.label(...options)}
-        handler={action.handler}
-        options={[...options]}
-      />
-    );
-  }
-
-  getUsersActions() {
-    const {
-      currentUser,
-      user,
-      userActions,
-      router,
-      isBreakoutRoom,
-      getAvailableActions,
-    } = this.props;
-
-    const {
-      openChat,
-      clearStatus,
-      setPresenter,
-      remove,
-      mute,
-      unmute,
-      promote,
-      demote,
-    } = userActions;
-
-    const actions = getAvailableActions(currentUser, user, router, isBreakoutRoom);
-
-    const {
-      allowedToChatPrivately,
-      allowedToMuteAudio,
-      allowedToUnmuteAudio,
-      allowedToResetStatus,
-      allowedToRemove,
-      allowedToSetPresenter,
-      allowedToPromote,
-      allowedToDemote,
-    } = actions;
-
-    return _.compact([
-      (allowedToChatPrivately ? UserListItem.createAction(openChat, router, user) : null),
-      (allowedToMuteAudio ? UserListItem.createAction(mute, user) : null),
-      (allowedToUnmuteAudio ? UserListItem.createAction(unmute, user) : null),
-      (allowedToResetStatus ? UserListItem.createAction(clearStatus, user) : null),
-      (allowedToSetPresenter ? UserListItem.createAction(setPresenter, user) : null),
-      (allowedToRemove ? UserListItem.createAction(remove, user) : null),
-      (allowedToPromote ? UserListItem.createAction(promote, user) : null),
-      (allowedToDemote ? UserListItem.createAction(demote, user) : null),
-    ]);
-  }
-
   render() {
     const {
       compact,
@@ -105,19 +44,42 @@ class UserListItem extends Component {
       isMeetingLocked,
       normalizeEmojiName,
       getScrollContainerRef,
+      assignPresenter,
+      removeUser,
+      toggleVoice,
+      changeRole,
+      setEmojiStatus,
+      currentUser,
+      router,
+      isBreakoutRoom,
+      getAvailableActions,
+      handleEmojiChange,
+      getEmojiList,
+      getEmoji,
     } = this.props;
 
-    const actions = this.getUsersActions();
-
-    const contents = (<UserListContent
-      compact={compact}
-      user={user}
-      intl={intl}
-      normalizeEmojiName={normalizeEmojiName}
-      actions={actions}
-      meeting={meeting}
-      isMeetingLocked={isMeetingLocked}
-      getScrollContainerRef={getScrollContainerRef}
+    const contents = (<UserDropdown
+      {...{
+        compact,
+        user,
+        intl,
+        normalizeEmojiName,
+        meeting,
+        isMeetingLocked,
+        getScrollContainerRef,
+        assignPresenter,
+        removeUser,
+        toggleVoice,
+        changeRole,
+        setEmojiStatus,
+        currentUser,
+        router,
+        isBreakoutRoom,
+        getAvailableActions,
+        handleEmojiChange,
+        getEmojiList,
+        getEmoji,
+      }}
     />);
 
     return contents;
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx
deleted file mode 100755
index 955f5a5468184548c4848947f14793c7d86f621e..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-action/component.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
-
-const propTypes = {
-  icon: PropTypes.string.isRequired,
-  label: PropTypes.string.isRequired,
-  handler: PropTypes.func.isRequired,
-  options: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
-};
-
-export default class UserActions extends React.PureComponent {
-  render() {
-    const {
-      key, icon, label, handler, options,
-    } = this.props;
-
-    return (
-      <DropdownListItem
-        key={key}
-        icon={icon}
-        label={label}
-        defaultMessage={label}
-        onClick={() => handler.call(this, ...options)}
-      />
-    );
-  }
-}
-
-UserActions.propTypes = propTypes;
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
similarity index 52%
rename from bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx
rename to bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
index 1ba403fe4b4ce88dd032aa60ee87f614431aa4b5..b2992e16021b07f2ce42783e1bf181a6b69cce6b 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
@@ -8,8 +8,9 @@ import Dropdown from '/imports/ui/components/dropdown/component';
 import DropdownTrigger from '/imports/ui/components/dropdown/trigger/component';
 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 DropdownListTitle from '/imports/ui/components/dropdown/list/title/component';
+import _ from 'lodash';
 import { styles } from './styles';
 import UserName from './../user-name/component';
 import UserIcons from './../user-icons/component';
@@ -39,6 +40,46 @@ const messages = defineMessages({
     id: 'app.userList.userAriaLabel',
     description: 'aria label for each user in the userlist',
   },
+  statusTriggerLabel: {
+    id: 'app.actionsBar.emojiMenu.statusTriggerLabel',
+    description: 'label for option to show emoji menu',
+  },
+  backTriggerLabel: {
+    id: 'app.audio.backLabel',
+    description: 'label for option to hide emoji menu',
+  },
+  ChatLabel: {
+    id: 'app.userList.menu.chat.label',
+    description: 'Save the changes and close the settings menu',
+  },
+  ClearStatusLabel: {
+    id: 'app.userList.menu.clearStatus.label',
+    description: 'Clear the emoji status of this user',
+  },
+  MakePresenterLabel: {
+    id: 'app.userList.menu.makePresenter.label',
+    description: 'Set this user to be the presenter in this meeting',
+  },
+  RemoveUserLabel: {
+    id: 'app.userList.menu.removeUser.label',
+    description: 'Forcefully remove this user from the meeting',
+  },
+  MuteUserAudioLabel: {
+    id: 'app.userList.menu.muteUserAudio.label',
+    description: 'Forcefully mute this user',
+  },
+  UnmuteUserAudioLabel: {
+    id: 'app.userList.menu.unmuteUserAudio.label',
+    description: 'Forcefully unmute this user',
+  },
+  PromoteUserLabel: {
+    id: 'app.userList.menu.promoteUser.label',
+    description: 'Forcefully promote this viewer to a moderator',
+  },
+  DemoteUserLabel: {
+    id: 'app.userList.menu.demoteUser.label',
+    description: 'Forcefully demote this moderator to a viewer',
+  },
 });
 
 const propTypes = {
@@ -48,14 +89,12 @@ const propTypes = {
     formatMessage: PropTypes.func.isRequired,
   }).isRequired,
   normalizeEmojiName: PropTypes.func.isRequired,
-  actions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
   meeting: PropTypes.shape({}).isRequired,
   isMeetingLocked: PropTypes.func.isRequired,
   getScrollContainerRef: PropTypes.func.isRequired,
 };
 
-
-class UserListContent extends Component {
+class UserDropdown extends Component {
   /**
    * Return true if the content fit on the screen, false otherwise.
    *
@@ -75,6 +114,7 @@ class UserListContent extends Component {
       dropdownOffset: 0,
       dropdownDirection: 'top',
       dropdownVisible: false,
+      showNestedOptions: false,
     };
 
     this.handleScroll = this.handleScroll.bind(this);
@@ -82,6 +122,8 @@ class UserListContent extends Component {
     this.onActionsHide = this.onActionsHide.bind(this);
     this.getDropdownMenuParent = this.getDropdownMenuParent.bind(this);
     this.renderUserAvatar = this.renderUserAvatar.bind(this);
+    this.resetMenuState = this.resetMenuState.bind(this);
+    this.makeDropdownItem = this.makeDropdownItem.bind(this);
   }
 
   componentWillMount() {
@@ -89,30 +131,201 @@ class UserListContent extends Component {
     this.seperator = _.uniqueId('action-separator-');
   }
 
-  componentDidUpdate() {
+  componentDidUpdate(prevProps, prevState) {
+    if (!this.state.isActionsOpen && this.state.showNestedOptions) {
+      return this.resetMenuState();
+    }
+
     this.checkDropdownDirection();
   }
 
-  onActionsShow() {
-    const dropdown = this.getDropdownMenuParent();
-    const scrollContainer = this.props.getScrollContainerRef();
-    const dropdownTrigger = dropdown.children[0];
-
-    const list = findDOMNode(this.list);
-    const children = [].slice.call(list.children);
-    children.find(child => child.getAttribute('role') === 'menuitem').focus();
+  makeDropdownItem(key, label, onClick, icon = null, iconRight = null) {
+    return (
+      <DropdownListItem
+        {...{
+          key,
+          label,
+          onClick,
+          icon,
+          iconRight,
+        }}
+        className={key === this.props.getEmoji ? styles.emojiSelected : null}
+      />
+    );
+  }
 
-    this.setState({
-      isActionsOpen: true,
-      dropdownVisible: false,
-      dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
+  resetMenuState() {
+    return this.setState({
+      isActionsOpen: false,
+      dropdownOffset: 0,
       dropdownDirection: 'top',
+      dropdownVisible: false,
+      showNestedOptions: false,
     });
+  }
+
+  getUsersActions() {
+    const {
+      intl,
+      currentUser,
+      user,
+      router,
+      isBreakoutRoom,
+      getAvailableActions,
+      handleEmojiChange,
+      getEmojiList,
+      setEmojiStatus,
+      assignPresenter,
+      removeUser,
+      toggleVoice,
+      changeRole,
+    } = this.props;
+
+    const actionPermissions = getAvailableActions(currentUser, user, router, isBreakoutRoom);
+    const actions = [];
+
+    const {
+      allowedToChatPrivately,
+      allowedToMuteAudio,
+      allowedToUnmuteAudio,
+      allowedToResetStatus,
+      allowedToRemove,
+      allowedToSetPresenter,
+      allowedToPromote,
+      allowedToDemote,
+      allowedToChangeStatus,
+    } = actionPermissions;
+
+    if (this.state.showNestedOptions) {
+      if (allowedToChangeStatus) {
+        actions.push(this.makeDropdownItem(
+          'back',
+          intl.formatMessage(messages.backTriggerLabel),
+          () => this.setState({ showNestedOptions: false, isActionsOpen: true }),
+          'left_arrow',
+        ));
+      }
+
+      actions.push(<DropdownListSeparator key={_.uniqueId('list-separator-')} />);
+
+      const statuses = Object.keys(getEmojiList);
+      statuses.map(status => actions.push(this.makeDropdownItem(
+        status,
+        intl.formatMessage({ id: `app.actionsBar.emojiMenu.${status}Label` }),
+        () => { handleEmojiChange(status); this.resetMenuState(); },
+        getEmojiList[status],
+      )));
 
-    scrollContainer.addEventListener('scroll', this.handleScroll, false);
+      return actions;
+    }
+
+    if (allowedToChangeStatus) {
+      actions.push(this.makeDropdownItem(
+        'setstatus',
+        intl.formatMessage(messages.statusTriggerLabel),
+        () => this.setState({ showNestedOptions: true, isActionsOpen: true }),
+        'user',
+        'right_arrow',
+      ));
+    }
+
+    if (allowedToChatPrivately) {
+      actions.push(this.makeDropdownItem(
+        'openChat',
+        intl.formatMessage(messages.ChatLabel),
+        () => this.onActionsHide(router.push(`/users/chat/${user.id}`)),
+        'chat',
+      ));
+    }
+
+    if (allowedToResetStatus && user.emoji.status !== 'none') {
+      actions.push(this.makeDropdownItem(
+        'clearStatus',
+        intl.formatMessage(messages.ClearStatusLabel),
+        () => this.onActionsHide(setEmojiStatus(user.id, 'none')),
+        'clear_status',
+      ));
+    }
+
+    if (allowedToMuteAudio) {
+      actions.push(this.makeDropdownItem(
+        'mute',
+        intl.formatMessage(messages.MuteUserAudioLabel),
+        () => this.onActionsHide(toggleVoice(user.id)),
+        'mute',
+      ));
+    }
+
+    if (allowedToUnmuteAudio) {
+      actions.push(this.makeDropdownItem(
+        'unmute',
+        intl.formatMessage(messages.UnmuteUserAudioLabel),
+        () => this.onActionsHide(toggleVoice(user.id)),
+        'unmute',
+      ));
+    }
+
+    if (allowedToSetPresenter) {
+      actions.push(this.makeDropdownItem(
+        'setPresenter',
+        intl.formatMessage(messages.MakePresenterLabel),
+        () => this.onActionsHide(assignPresenter(user.id)),
+        'presentation',
+      ));
+    }
+
+    if (allowedToRemove) {
+      actions.push(this.makeDropdownItem(
+        'remove',
+        intl.formatMessage(messages.RemoveUserLabel, { 0: user.name }),
+        () => this.onActionsHide(removeUser(user.id)),
+        'circle_close',
+      ));
+    }
+
+    if (allowedToPromote) {
+      actions.push(this.makeDropdownItem(
+        'promote',
+        intl.formatMessage(messages.PromoteUserLabel),
+        () => this.onActionsHide(changeRole(user.id, 'MODERATOR')),
+        'promote',
+      ));
+    }
+
+    if (allowedToDemote) {
+      actions.push(this.makeDropdownItem(
+        'demote',
+        intl.formatMessage(messages.DemoteUserLabel),
+        () => this.onActionsHide(changeRole(user.id, 'VIEWER')),
+        'user',
+      ));
+    }
+
+    return actions;
+  }
+
+  onActionsShow() {
+    const dropdown = this.getDropdownMenuParent();
+    const scrollContainer = this.props.getScrollContainerRef();
+
+    if (dropdown && scrollContainer) {
+      const dropdownTrigger = dropdown.children[0];
+      const list = findDOMNode(this.list);
+      const children = [].slice.call(list.children);
+      children.find(child => child.getAttribute('role') === 'menuitem').focus();
+
+      this.setState({
+        isActionsOpen: true,
+        dropdownVisible: false,
+        dropdownOffset: dropdownTrigger.offsetTop - scrollContainer.scrollTop,
+        dropdownDirection: 'top',
+      });
+
+      scrollContainer.addEventListener('scroll', this.handleScroll, false);
+    }
   }
 
-  onActionsHide() {
+  onActionsHide(callback) {
     this.setState({
       isActionsOpen: false,
       dropdownVisible: false,
@@ -120,6 +333,10 @@ class UserListContent extends Component {
 
     const scrollContainer = this.props.getScrollContainerRef();
     scrollContainer.removeEventListener('scroll', this.handleScroll, false);
+
+    if (callback) {
+      return callback;
+    }
   }
 
   getDropdownMenuParent() {
@@ -127,9 +344,7 @@ class UserListContent extends Component {
   }
 
   handleScroll() {
-    this.setState({
-      isActionsOpen: false,
-    });
+    this.setState({ isActionsOpen: false });
   }
 
   /**
@@ -148,7 +363,7 @@ class UserListContent extends Component {
       };
 
       const isDropdownVisible =
-        UserListContent.checkIfDropdownIsVisible(
+        UserDropdown.checkIfDropdownIsVisible(
           dropdownContent.offsetTop,
           dropdownContent.offsetHeight,
         );
@@ -211,7 +426,6 @@ class UserListContent extends Component {
       compact,
       user,
       intl,
-      actions,
       isMeetingLocked,
       meeting,
     } = this.props;
@@ -223,6 +437,8 @@ class UserListContent extends Component {
       dropdownOffset,
     } = this.state;
 
+    const actions = this.getUsersActions();
+
     const userItemContentsStyle = {};
 
     userItemContentsStyle[styles.userItemContentsCompact] = compact;
@@ -255,25 +471,27 @@ class UserListContent extends Component {
             { this.renderUserAvatar() }
           </div>
           {<UserName
-            user={user}
-            compact={compact}
-            intl={intl}
-            meeting={meeting}
-            isMeetingLocked={isMeetingLocked}
-            userAriaLabel={userAriaLabel}
-            isActionsOpen={isActionsOpen}
+            {...{
+              user,
+              compact,
+              intl,
+              meeting,
+              isMeetingLocked,
+              userAriaLabel,
+              isActionsOpen,
+            }}
           />}
           {<UserIcons
-            user={user}
-            compact={compact}
+            {...{
+              user,
+              compact,
+            }}
           />}
         </div>
       </div>
     );
 
-    if (!actions.length) {
-      return contents;
-    }
+    if (!actions.length) return contents;
 
     return (
       <Dropdown
@@ -298,24 +516,12 @@ class UserListContent extends Component {
           className={styles.dropdownContent}
           placement={`right ${dropdownDirection}`}
         >
-
           <DropdownList
             ref={(ref) => { this.list = ref; }}
             getDropdownMenuParent={this.getDropdownMenuParent}
             onActionsHide={this.onActionsHide}
           >
-            {
-              [
-                (
-                  <DropdownListTitle
-                    description={intl.formatMessage(messages.menuTitleContext)}
-                    key={this.title}
-                  >
-                    {user.name}
-                  </DropdownListTitle>),
-                (<DropdownListSeparator key={this.seperator} />),
-              ].concat(actions)
-            }
+            {actions}
           </DropdownList>
         </DropdownContent>
       </Dropdown>
@@ -323,5 +529,5 @@ class UserListContent extends Component {
   }
 }
 
-UserListContent.propTypes = propTypes;
-export default UserListContent;
+UserDropdown.propTypes = propTypes;
+export default UserDropdown;
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss
similarity index 96%
rename from bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss
rename to bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss
index 441d4377ac6b264410884aa1bf0738d5fdb6036c..e6e8b750e51a95e4d044ad7f847b3f4069ed25d7 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-list-content/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/styles.scss
@@ -103,3 +103,9 @@
   max-width: 100%;
   overflow: visible;
 }
+
+.emojiSelected {
+  span, i {
+    color: $color-primary;
+  }
+}
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
index bc2c5ded07c7960f8580cdf13fda80309a014bca..7959f3ed2f160bfebc6d3129530eae142cad842a 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
@@ -9,6 +9,8 @@ import logger from '/imports/startup/client/logger';
 import VideoService from './service';
 import VideoList from './video-list/component';
 
+const VIDEO_CONSTRAINTS = Meteor.settings.public.kurento.cameraConstraints;
+
 const intlClientErrors = defineMessages({
   iceCandidateError: {
     id: 'app.video.iceCandidateError',
@@ -46,6 +48,10 @@ const intlClientErrors = defineMessages({
     id: 'app.video.notReadableError',
     description: 'error message When the webcam is being used by other software',
   },
+  iceConnectionStateError: {
+    id: 'app.video.iceConnectionStateError',
+    description: 'Error message for ice connection state being failed',
+  },
 });
 
 const intlSFUErrors = defineMessages({
@@ -63,7 +69,7 @@ const intlSFUErrors = defineMessages({
   },
   2003: {
     id: 'app.sfu.mediaServerRequestTimeout2003',
-    description: "Error message fired when requests are timing out due to lack of resources",
+    description: 'Error message fired when requests are timing out due to lack of resources',
   },
   2021: {
     id: 'app.sfu.serverIceGatheringFailed2021',
@@ -80,7 +86,7 @@ const intlSFUErrors = defineMessages({
   2203: {
     id: 'app.sfu.noAvailableCodec2203',
     description: 'Error message fired when the server has no available codec for the client',
-  }
+  },
 });
 
 const CAMERA_SHARE_FAILED_WAIT_TIME = 15000;
@@ -123,10 +129,10 @@ class VideoProvider extends Component {
   }
 
   logger(type, message, options = {}) {
-    const {userId, userName} = this.props;
+    const { userId, userName } = this.props;
     const topic = options.topic || 'video';
 
-    logger[type]({obj: Object.assign(options, {userId, userName, topic})}, `[${topic}] ${message}`);
+    logger[type]({ obj: Object.assign(options, { userId, userName, topic }) }, `[${topic}] ${message}`);
   }
 
   _sendPauseStream(id, role, state) {
@@ -215,7 +221,7 @@ class VideoProvider extends Component {
   }
 
   onWsOpen() {
-    this.logger('debug', '------ Websocket connection opened.', {topic: 'ws'});
+    this.logger('debug', '------ Websocket connection opened.', { topic: 'ws' });
 
     // -- Resend queued messages that happened when socket was not connected
     while (this.wsQueue.length > 0) {
@@ -228,7 +234,7 @@ class VideoProvider extends Component {
   }
 
   onWsClose(error) {
-    this.logger('debug', '------ Websocket connection closed.', {topic: 'ws'});
+    this.logger('debug', '------ Websocket connection closed.', { topic: 'ws' });
 
     this.stopWebRTCPeer(this.props.userId);
     clearInterval(this.pingInterval);
@@ -246,7 +252,7 @@ class VideoProvider extends Component {
   onWsMessage(msg) {
     const parsedMessage = JSON.parse(msg.data);
 
-    this.logger('debug', `Received new message '${parsedMessage.id}'`, {topic: 'ws', message: parsedMessage});
+    this.logger('debug', `Received new message '${parsedMessage.id}'`, { topic: 'ws', message: parsedMessage });
 
     switch (parsedMessage.id) {
       case 'startResponse':
@@ -266,7 +272,7 @@ class VideoProvider extends Component {
         break;
 
       case 'pong':
-        this.logger('debug', 'Received pong from server', {topic: 'ws'});
+        this.logger('debug', 'Received pong from server', { topic: 'ws' });
         break;
 
       case 'error':
@@ -281,10 +287,10 @@ class VideoProvider extends Component {
 
     if (this.connectedToMediaServer()) {
       const jsonMessage = JSON.stringify(message);
-      this.logger('debug', `Sending message '${message.id}'`, {topic: 'ws', message});
+      this.logger('debug', `Sending message '${message.id}'`, { topic: 'ws', message });
       ws.send(jsonMessage, (error) => {
         if (error) {
-          this.logger(`client: Websocket error '${error}' on message '${message.id}'`, {topic: 'ws'});
+          this.logger(`client: Websocket error '${error}' on message '${message.id}'`, { topic: 'ws' });
         }
       });
     } else {
@@ -303,12 +309,12 @@ class VideoProvider extends Component {
     const id = message.cameraId;
     const peer = this.webRtcPeers[id];
 
-    this.logger('debug', 'SDP answer received from server. Processing ...', {cameraId: id, sdpAnswer: message.sdpAnswer});
+    this.logger('debug', 'SDP answer received from server. Processing ...', { cameraId: id, sdpAnswer: message.sdpAnswer });
 
     if (peer) {
       peer.processAnswer(message.sdpAnswer, (error) => {
         if (error) {
-          return this.logger('debug', JSON.stringify(error), {cameraId: id});
+          return this.logger('debug', JSON.stringify(error), { cameraId: id });
         }
       });
     } else {
@@ -319,13 +325,13 @@ class VideoProvider extends Component {
   handleIceCandidate(message) {
     const webRtcPeer = this.webRtcPeers[message.cameraId];
 
-    this.logger('debug', 'Received remote ice candidate', {topic: 'ice', candidate: message.candidate})
+    this.logger('debug', 'Received remote ice candidate', { topic: 'ice', candidate: message.candidate });
 
     if (webRtcPeer) {
       if (webRtcPeer.didSDPAnswered) {
         webRtcPeer.addIceCandidate(message.candidate, (err) => {
           if (err) {
-            return this.logger('error', `Error adding candidate: ${err}`, {cameraId: message.cameraId});
+            return this.logger('error', `Error adding candidate: ${err}`, { cameraId: message.cameraId });
           }
         });
       } else {
@@ -335,15 +341,21 @@ class VideoProvider extends Component {
         webRtcPeer.iceQueue.push(message.candidate);
       }
     } else {
-      this.logger('warn', ' [iceCandidate] Message arrived after the peer was already thrown out, discarding it...', {cameraId: message.cameraId});
+      this.logger('warn', ' [iceCandidate] Message arrived after the peer was already thrown out, discarding it...', { cameraId: message.cameraId });
     }
   }
 
   stopWebRTCPeer(id) {
-    this.logger('info', 'Stopping webcam', {cameraId: id});
+    this.logger('info', 'Stopping webcam', { cameraId: id });
     const { userId } = this.props;
     const shareWebcam = id === userId;
 
+    // in this case, 'closed' state is not caused by an error;
+    // we stop listening to prevent this from being treated as an error
+    if (this.webRtcPeers[id]) {
+      this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = null;
+    }
+
     if (shareWebcam) {
       this.unshareWebcam();
     }
@@ -365,11 +377,11 @@ class VideoProvider extends Component {
   destroyWebRTCPeer(id) {
     const webRtcPeer = this.webRtcPeers[id];
     if (webRtcPeer) {
-      this.logger('info', 'Stopping WebRTC peer', {cameraId: id});
+      this.logger('info', 'Stopping WebRTC peer', { cameraId: id });
       webRtcPeer.dispose();
       delete this.webRtcPeers[id];
     } else {
-      this.logger('warn', 'No WebRTC peer to stop (not an error)', {cameraId: id});
+      this.logger('warn', 'No WebRTC peer to stop (not an error)', { cameraId: id });
     }
   }
 
@@ -382,25 +394,10 @@ class VideoProvider extends Component {
     } catch (error) {
       this.logger('error', 'Video provider failed to fetch ice servers, using default');
     } finally {
-      const videoConstraints = {
-        width: {
-          min: 320,
-          max: 640,
-        },
-        height: {
-          min: 180,
-          max: 480,
-        },
-      };
-
-      if (!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) {
-        videoConstraints.frameRate = { min: 5, ideal: 10 };
-      }
-
       const options = {
         mediaConstraints: {
           audio: false,
-          video: videoConstraints,
+          video: VIDEO_CONSTRAINTS,
         },
         onicecandidate: this._getOnIceCandidateCallback(id, shareWebcam),
       };
@@ -436,7 +433,7 @@ class VideoProvider extends Component {
             return this._webRTCOnError(errorGenOffer, id, shareWebcam);
           }
 
-          this.logger('debug', `Invoking SDP offer callback function ${location.host}`, {cameraId: id, offerSdp});
+          this.logger('debug', `Invoking SDP offer callback function ${location.host}`, { cameraId: id, offerSdp });
 
           const message = {
             type: 'video',
@@ -453,6 +450,8 @@ class VideoProvider extends Component {
           peer.didSDPAnswered = true;
         });
       });
+      this.webRtcPeers[id].peerConnection.oniceconnectionstatechange =
+        this._getOnIceConnectionStateChangeCallback(id);
     }
   }
 
@@ -460,7 +459,7 @@ class VideoProvider extends Component {
     const { intl } = this.props;
 
     return () => {
-      this.logger('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`, {cameraId: id});
+      this.logger('error', `Camera share has not suceeded in ${CAMERA_SHARE_FAILED_WAIT_TIME}`, { cameraId: id });
 
       if (this.props.userId === id) {
         this.notifyError(intl.formatMessage(intlClientErrors.sharingError));
@@ -489,7 +488,7 @@ class VideoProvider extends Component {
       peer.addIceCandidate(candidate, (err) => {
         if (err) {
           this.notifyError(intl.formatMessage(intlClientErrors.iceCandidateError));
-          return this.logger('error', `Error adding candidate: ${err}`, {cameraId});
+          return this.logger('error', `Error adding candidate: ${err}`, { cameraId });
         }
       });
     }
@@ -510,7 +509,7 @@ class VideoProvider extends Component {
 
     this.stopWebRTCPeer(id);
 
-    return this.logger('error', errorMessage, {cameraId: id});
+    return this.logger('error', errorMessage, { cameraId: id });
   }
 
   _getOnIceCandidateCallback(id, shareWebcam) {
@@ -521,11 +520,11 @@ class VideoProvider extends Component {
       if (!this.restartTimeout[id]) {
         this.restartTimer[id] = this.restartTimer[id] || CAMERA_SHARE_FAILED_WAIT_TIME;
 
-        this.logger('debug', `Setting a camera connection restart in ${this.restartTimer[id]}`, {cameraId: id});
+        this.logger('debug', `Setting a camera connection restart in ${this.restartTimer[id]}`, { cameraId: id });
         this.restartTimeout[id] = setTimeout(this._getWebRTCStartTimeout(id, shareWebcam, peer), this.restartTimer[id]);
       }
 
-      this.logger('debug', 'Generated local ice candidate', {topic: 'ice', candidate})
+      this.logger('debug', 'Generated local ice candidate', { topic: 'ice', candidate });
 
       const message = {
         type: 'video',
@@ -538,6 +537,24 @@ class VideoProvider extends Component {
     };
   }
 
+  _getOnIceConnectionStateChangeCallback(id) {
+    const { intl } = this.props;
+    const peer = this.webRtcPeers[id];
+
+    return (event) => {
+      const connectionState = peer.peerConnection.iceConnectionState;
+      if (connectionState === 'failed' || connectionState === 'closed') {
+
+        // prevent the same error from being detected multiple times
+        peer.peerConnection.oniceconnectionstatechange = null;
+
+        this.logger('error', 'ICE connection state', id);
+        this.stopWebRTCPeer(id);
+        this.notifyError(intl.formatMessage(intlClientErrors.iceConnectionStateError));
+      }
+    };
+  }
+
   attachVideoStream(id) {
     const video = this.videoTags[id];
     if (video == null) {
@@ -706,7 +723,7 @@ class VideoProvider extends Component {
 
   monitorTrackStart(peer, track, local, callback) {
     const that = this;
-    this.logger('info', 'Starting stats monitoring on', {cameraId: track.id});
+    this.logger('info', 'Starting stats monitoring on', { cameraId: track.id });
     const getStatsInterval = 2000;
 
     const callGetStats = () => {
@@ -772,10 +789,10 @@ class VideoProvider extends Component {
   }
 
   handlePlayStop(message) {
-    const {cameraId} = message;
+    const { cameraId } = message;
 
-    this.logger('info', 'Handle play stop for camera', {cameraId});
-    this.stopWebRTCPeer(id);
+    this.logger('info', 'Handle play stop for camera', { cameraId });
+    this.stopWebRTCPeer(cameraId);
   }
 
   handlePlayStart(message) {
@@ -784,7 +801,7 @@ class VideoProvider extends Component {
     const videoTag = this.videoTags[id];
 
     if (peer) {
-      this.logger('info', 'Handle play start for camera', {cameraId: id});
+      this.logger('info', 'Handle play start for camera', { cameraId: id });
 
       // Clear camera shared timeout when camera succesfully starts
       clearTimeout(this.restartTimeout[id]);
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss
index 2e2fe1980c0503c49356360c41056f07a878307e..e5a9a27f7b650f3d9a85facd2385c1821cf80788 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/styles.scss
@@ -1,5 +1,9 @@
 @import "/imports/ui/stylesheets/variables/_all";
 
+$cam-dropdown-width: 70%;
+$audio-indicator-width: 1.12rem;
+$audio-indicator-fs: 75%;
+
 .videoCanvas {
   position: absolute;
   top: 0;
@@ -119,6 +123,7 @@
   flex: 1;
   display: flex;
   outline: none !important;
+  width: $cam-dropdown-width;
 
   @include mq($medium-up) {
     > [aria-expanded] {
@@ -164,13 +169,15 @@
 
 .muted, .voice {
   display: inline-block;
-  width: 1.25rem;
-  height: 1.25rem;
+  width: $audio-indicator-width;
+  height: $audio-indicator-width;
+  min-width: $audio-indicator-width;
+  min-height: $audio-indicator-width;
   color: $color-white;
   border-radius: 50%;
 
   &::before {
-    font-size: 75%;
+    font-size: $audio-indicator-fs;
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-factory/reactive-annotation/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/annotation-factory/reactive-annotation/component.jsx
old mode 100644
new mode 100755
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/pencil/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/pencil/component.jsx
old mode 100644
new mode 100755
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js
index 796262fca76e4b12323418281c0c8c16bc06b94c..727e0306784275f4954e69d41f62ef2268fa5891 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js
@@ -7,6 +7,14 @@ import logger from '/imports/startup/client/logger';
 import { isEqual } from 'lodash';
 
 const Annotations = new Mongo.Collection(null);
+const ANNOTATION_CONFIG = Meteor.settings.public.whiteboard.annotations;
+const DRAW_START = ANNOTATION_CONFIG.status.start;
+const DRAW_END = ANNOTATION_CONFIG.status.end;
+const discardedList = [];
+
+export function addAnnotationToDiscardedList(annotation) {
+  if (!discardedList.includes(annotation)) discardedList.push(annotation);
+}
 
 function clearFakeAnnotations() {
   Annotations.remove({ id: /-fake/g });
@@ -25,18 +33,17 @@ function handleAddedAnnotation({
 
   const fakeAnnotation = Annotations.findOne({ id: `${annotation.id}-fake` });
   const fakePoints = fakeAnnotation.annotationInfo.points;
-  const lastPoints = annotation.annotationInfo.points;
+  const { points: lastPoints } = annotation.annotationInfo;
 
   if (annotation.annotationType !== 'pencil') {
     Annotations.update(fakeAnnotation._id, {
       $set: {
         position: annotation.position,
-        'annotationInfo.color': isEqual(fakePoints, lastPoints) || annotation.status === 'DRAW_END' ?
+        'annotationInfo.color': isEqual(fakePoints, lastPoints) || annotation.status === DRAW_END ?
           annotation.annotationInfo.color : fakeAnnotation.annotationInfo.color,
       },
       $inc: { version: 1 }, // TODO: Remove all this version stuff
     });
-
     return;
   }
 
@@ -47,12 +54,12 @@ function handleAddedAnnotation({
     }
 
     // Remove fake annotation for pencil on draw end
-    if (annotation.status === 'DRAW_END') {
+    if (annotation.status === DRAW_END) {
       Annotations.remove({ id: `${annotation.id}-fake` });
       return;
     }
 
-    if (annotation.status === 'DRAW_START') {
+    if (annotation.status === DRAW_START) {
       Annotations.update(fakeAnnotation._id, {
         $set: {
           position: annotation.position - 1,
@@ -68,6 +75,8 @@ function handleRemovedAnnotation({
 }) {
   const query = { meetingId, whiteboardId };
 
+  addAnnotationToDiscardedList(shapeId);
+
   if (userId) {
     query.userId = userId;
   }
@@ -82,7 +91,10 @@ function handleRemovedAnnotation({
 AnnotationsStreamer.on('removed', handleRemovedAnnotation);
 
 AnnotationsStreamer.on('added', ({ annotations }) => {
-  annotations.forEach(annotation => handleAddedAnnotation(annotation));
+  // Call handleAddedAnnotation when this annotation is not in discardedList
+  annotations
+    .filter(({ annotation }) => !discardedList.includes(annotation.id))
+    .forEach(annotation => handleAddedAnnotation(annotation));
 });
 
 function increaseBrightness(realHex, percent) {
@@ -125,7 +137,10 @@ const proccessAnnotationsQueue = () => {
   }
 
   // console.log('annotationQueue.length', annotationsQueue, annotationsQueue.length);
-  AnnotationsStreamer.emit('publish', { credentials: Auth.credentials, payload: annotationsQueue });
+  AnnotationsStreamer.emit('publish', {
+    credentials: Auth.credentials,
+    payload: annotationsQueue.filter(({ id }) => !discardedList.includes(id)),
+  });
   annotationsQueue = [];
   // ask tiago
   const delayPerc =
@@ -143,7 +158,7 @@ export function sendAnnotation(annotation) {
   if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin);
 
   // skip optimistic for draw end since the smoothing is done in akka
-  if (annotation.status === 'DRAW_END') return;
+  if (annotation.status === DRAW_END) return;
 
   const { position, ...relevantAnotation } = annotation;
   const queryFake = addAnnotationQuery(
@@ -168,7 +183,6 @@ WhiteboardMultiUser.find({ meetingId: Auth.meetingID }).observeChanges({
 
 Users.find({ userId: Auth.userID }).observeChanges({
   changed(id, { presenter }) {
-    console.log(presenter);
     if (presenter === false) clearFakeAnnotations();
   },
 });
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx
old mode 100644
new mode 100755
index afd48151ad422840f59d147df7d86998e780d42d..ef017649c4ca9e49d4820ffcc49e5340b951ab0c
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/component.jsx
@@ -127,6 +127,9 @@ export default class WhiteboardOverlay extends Component {
       sendAnnotation,
       resetTextShapeSession,
       setTextShapeActiveId,
+      contextMenuHandler,
+      addAnnotationToDiscardedList,
+      undoAnnotation,
     } = this.props;
     const { tool } = drawSettings;
     const actions = {
@@ -140,6 +143,9 @@ export default class WhiteboardOverlay extends Component {
       sendAnnotation,
       resetTextShapeSession,
       setTextShapeActiveId,
+      contextMenuHandler,
+      addAnnotationToDiscardedList,
+      undoAnnotation,
     };
 
     if (tool === 'triangle' || tool === 'rectangle' || tool === 'ellipse' || tool === 'line') {
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx
old mode 100644
new mode 100755
index d9580c9a8143fb4578c51d748c540478fdbbc041..eee2d97002cfa5eca59c7ca129f150827148a6d6
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/container.jsx
@@ -2,6 +2,7 @@ import React from 'react';
 import { withTracker } from 'meteor/react-meteor-data';
 import PropTypes from 'prop-types';
 import WhiteboardOverlayService from './service';
+import WhiteboardToolbarService from '../whiteboard-toolbar/service';
 import WhiteboardOverlay from './component';
 
 const WhiteboardOverlayContainer = (props) => {
@@ -14,7 +15,10 @@ const WhiteboardOverlayContainer = (props) => {
 };
 
 export default withTracker(() => ({
+  undoAnnotation: WhiteboardToolbarService.undoAnnotation,
+  contextMenuHandler: WhiteboardOverlayService.contextMenuHandler,
   sendAnnotation: WhiteboardOverlayService.sendAnnotation,
+  addAnnotationToDiscardedList: WhiteboardOverlayService.addAnnotationToDiscardedList,
   setTextShapeActiveId: WhiteboardOverlayService.setTextShapeActiveId,
   resetTextShapeSession: WhiteboardOverlayService.resetTextShapeSession,
   drawSettings: WhiteboardOverlayService.getWhiteboardToolbarValues(),
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx
index 69d42fc5fe590fff5529adfddc504b73228dcce8..57c59ea6088941258565cb113f5a769004153259 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/pencil-draw-listener/component.jsx
@@ -24,6 +24,7 @@ export default class PencilDrawListener extends Component {
     this.handleTouchMove = this.handleTouchMove.bind(this);
     this.handleTouchEnd = this.handleTouchEnd.bind(this);
     this.handleTouchCancel = this.handleTouchCancel.bind(this);
+    this.discardAnnotation = this.discardAnnotation.bind(this);
   }
 
   componentDidMount() {
@@ -117,17 +118,23 @@ export default class PencilDrawListener extends Component {
 
   // main mouse down handler
   mouseDownHandler(event) {
+    const isLeftClick = event.button === 0;
+    const isRightClick = event.button === 2;
+
     if (!this.isDrawing) {
-      window.addEventListener('mouseup', this.mouseUpHandler);
-      window.addEventListener('mousemove', this.mouseMoveHandler, true);
+      if (isLeftClick) {
+        window.addEventListener('mouseup', this.mouseUpHandler);
+        window.addEventListener('mousemove', this.mouseMoveHandler, true);
 
-      const { clientX, clientY } = event;
-      this.commonDrawStartHandler(clientX, clientY);
+        const { clientX, clientY } = event;
+        this.commonDrawStartHandler(clientX, clientY);
+      }
 
     // if you switch to a different window using Alt+Tab while mouse is down and release it
     // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
-    } else {
+    } else if (isRightClick) {
       this.sendLastMessage();
+      this.discardAnnotation();
     }
   }
 
@@ -172,7 +179,7 @@ export default class PencilDrawListener extends Component {
       position: 0,
     };
 
-    // dimensions are added to the 'DRAW_END', last message
+      // dimensions are added to the 'DRAW_END', last message
     if (dimensions) {
       annotation.annotationInfo.dimensions = dimensions;
     }
@@ -208,6 +215,14 @@ export default class PencilDrawListener extends Component {
     window.removeEventListener('touchcancel', this.handleTouchCancel, true);
   }
 
+  discardAnnotation() {
+    const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions;
+    const { whiteboardId } = this.props;
+
+    undoAnnotation(whiteboardId);
+    addAnnotationToDiscardedList(getCurrentShapeId());
+  }
+
   render() {
     const baseName = Meteor.settings.public.app.basename;
     const pencilDrawStyle = {
@@ -217,12 +232,14 @@ export default class PencilDrawListener extends Component {
       zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping
       cursor: `url('${baseName}/resources/images/whiteboard-cursor/pencil.png') 2 22, default`,
     };
+    const { contextMenuHandler } = this.props.actions;
     return (
       <div
         onTouchStart={this.handleTouchStart}
         role="presentation"
         style={pencilDrawStyle}
         onMouseDown={this.mouseDownHandler}
+        onContextMenu={contextMenuHandler}
       />
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js
index 8e61e2333db1b16470cb94c26bb682fdefa0f372..423b8965ea3fddc1f60ad98a492c845d11e3cf42 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/service.js
@@ -1,8 +1,6 @@
-import { makeCall } from '/imports/ui/services/api';
 import Storage from '/imports/ui/services/storage/session';
 import Auth from '/imports/ui/services/auth';
-
-import { sendAnnotation } from '/imports/ui/components/whiteboard/service';
+import { sendAnnotation, addAnnotationToDiscardedList } from '/imports/ui/components/whiteboard/service';
 
 const DRAW_SETTINGS = 'drawSettings';
 
@@ -49,11 +47,14 @@ const setTextShapeActiveId = (id) => {
 
 const getCurrentUserId = () => Auth.userID;
 
+const contextMenuHandler = event => event.preventDefault();
 
 export default {
+  addAnnotationToDiscardedList,
   sendAnnotation,
   getWhiteboardToolbarValues,
   setTextShapeActiveId,
   resetTextShapeSession,
   getCurrentUserId,
+  contextMenuHandler,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx
index 07ec6a396e101ec03a8951fdd7cf87360de3fa45..8c17c485a7637efa1879c7e1c644ef9f89f08431 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/shape-draw-listener/component.jsx
@@ -148,17 +148,25 @@ export default class ShapeDrawListener extends Component {
 
   // main mouse down handler
   handleMouseDown(event) {
-    // Sometimes when you Alt+Tab while drawing it can happen that your mouse is up,
-    // but the browser didn't catch it. So check it here.
-    if (this.isDrawing) {
-      return this.sendLastMessage();
-    }
+    const isLeftClick = event.button === 0;
+    const isRightClick = event.button === 2;
 
-    window.addEventListener('mouseup', this.handleMouseUp);
-    window.addEventListener('mousemove', this.handleMouseMove, true);
+    if (!this.isDrawing) {
+      if (isLeftClick) {
+        window.addEventListener('mouseup', this.handleMouseUp);
+        window.addEventListener('mousemove', this.handleMouseMove, true);
 
-    const { clientX, clientY } = event;
-    return this.commonDrawStartHandler(clientX, clientY);
+        const { clientX, clientY } = event;
+        this.commonDrawStartHandler(clientX, clientY);
+      }
+
+    // if you switch to a different window using Alt+Tab while mouse is down and release it
+    // it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
+    } else if (isRightClick) {
+      // this.isDrawing = false;
+      this.sendLastMessage();
+      this.discardAnnotation();
+    }
   }
 
   // main mouse move handler
@@ -280,6 +288,14 @@ export default class ShapeDrawListener extends Component {
     sendAnnotation(annotation, whiteboardId);
   }
 
+  discardAnnotation() {
+    const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions;
+    const { whiteboardId } = this.props;
+
+    undoAnnotation(whiteboardId);
+    addAnnotationToDiscardedList(getCurrentShapeId());
+  }
+
   render() {
     const { tool } = this.props.drawSettings;
     const baseName = Meteor.settings.public.app.basename;
@@ -290,12 +306,14 @@ export default class ShapeDrawListener extends Component {
       zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping
       cursor: `url('${baseName}/resources/images/whiteboard-cursor/${tool !== 'rectangle' ? tool : 'square'}.png'), default`,
     };
+    const { contextMenuHandler } = this.props.actions;
     return (
       <div
         onTouchStart={this.handleTouchStart}
         role="presentation"
         style={shapeDrawStyle}
         onMouseDown={this.handleMouseDown}
+        onContextMenu={contextMenuHandler}
       />
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
index 2f155dbe71040df82c8fcc86141c22792e846042..b7d136ab9bd5dfc1bb79b5d00cb06733d453cea3 100755
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-overlay/text-draw-listener/component.jsx
@@ -172,22 +172,30 @@ export default class TextDrawListener extends Component {
 
   // main mouse down handler
   handleMouseDown(event) {
+    const isLeftClick = event.button === 0;
+    const isRightClick = event.button === 2;
+
     if (this.hasBeenTouchedRecently) {
       return;
     }
 
     // if our current drawing state is not drawing the box and not writing the text
     if (!this.state.isDrawing && !this.state.isWritingText) {
-      window.addEventListener('mouseup', this.handleMouseUp);
-      window.addEventListener('mousemove', this.handleMouseMove, true);
+      if (isLeftClick) {
+        window.addEventListener('mouseup', this.handleMouseUp);
+        window.addEventListener('mousemove', this.handleMouseMove, true);
 
-      const { clientX, clientY } = event;
-      this.commonDrawStartHandler(clientX, clientY);
+        const { clientX, clientY } = event;
+        this.commonDrawStartHandler(clientX, clientY);
+      }
 
     // second case is when a user finished writing the text and publishes the final result
     } else {
       // publishing the final shape and resetting the state
       this.sendLastMessage();
+      if (isRightClick) {
+        this.discardAnnotation();
+      }
     }
   }
 
@@ -370,6 +378,14 @@ export default class TextDrawListener extends Component {
     sendAnnotation(annotation, whiteboardId);
   }
 
+  discardAnnotation() {
+    const { getCurrentShapeId, addAnnotationToDiscardedList, undoAnnotation } = this.props.actions;
+    const { whiteboardId } = this.props;
+
+    undoAnnotation(whiteboardId);
+    addAnnotationToDiscardedList(getCurrentShapeId());
+  }
+
   render() {
     const baseName = Meteor.settings.public.app.basename;
     const textDrawStyle = {
@@ -379,12 +395,14 @@ export default class TextDrawListener extends Component {
       zIndex: 2 ** 31 - 1, // maximun value of z-index to prevent other things from overlapping
       cursor: `url('${baseName}/resources/images/whiteboard-cursor/text.png'), default`,
     };
+    const { contextMenuHandler } = this.props.actions;
     return (
       <div
         role="presentation"
         style={textDrawStyle}
         onMouseDown={this.handleMouseDown}
         onTouchStart={this.handleTouchStart}
+        onContextMenu={contextMenuHandler}
       >
         {this.state.isDrawing ?
           <svg
diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss
index 2878fd57bcbdfd2b2ead3c263cc4103b371b590d..60d9d7fbd1c6167322a98595736f153a27b0cc8b 100644
--- a/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/whiteboard/whiteboard-toolbar/styles.scss
@@ -34,6 +34,7 @@ $toolbar-list-color: $color-gray;
   align-items: center;
   justify-content: center;
   box-shadow: $toolbar-box-shadow;
+  border-radius: $toolbar-button-border-radius;
   pointer-events: all;
 
   .buttonWrapper > .toolbarButton {
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 15873ba13aa7478c98e0b2b69d86fa04db414621..cb50880e2f6838d8f753a537dbb0d85e18be3a27 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -9,9 +9,11 @@ import logger from '/imports/startup/client/logger';
 import { notify } from '/imports/ui/services/notification';
 
 const MEDIA = Meteor.settings.public.media;
+const MEDIA_TAG = MEDIA.mediaTag;
 const USE_SIP = MEDIA.useSIPAudio;
 const USE_KURENTO = Meteor.settings.public.kurento.enableListenOnly;
 const ECHO_TEST_NUMBER = MEDIA.echoTestNumber;
+const MAX_LISTEN_ONLY_RETRIES = 2;
 
 const CALL_STATES = {
   STARTED: 'started',
@@ -127,7 +129,7 @@ class AudioManager {
       .then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
   }
 
-  joinListenOnly() {
+  joinListenOnly(retries = 0) {
     this.isListenOnly = true;
     this.isEchoTest = false;
     // The kurento bridge isn't a full audio bridge yet, so we have to differ it
@@ -145,24 +147,20 @@ class AudioManager {
       setTimeout(reject, 12000, iceGatheringErr);
     });
 
-
-    // Workaround to circumvent the WebKit autoplay policy without prompting
-    // the user to do some action again. A silent stream is played.
-    this.playFakeAudio();
-
     return this.onAudioJoining()
       .then(() => Promise.race([
         bridge.joinAudio(callOptions, this.callStateCallback.bind(this)),
         iceGatheringTimeout,
       ]))
       .catch((err) => {
-        // If theres a iceGathering timeout we retry to join after asking device permissions
-        if (err === iceGatheringErr) {
-          return this.askDevicesPermissions()
-            .then(() => this.joinListenOnly());
+        // If theres a iceGathering timeout we retry to join until MAX_LISTEN_ONLY_RETRIES
+        if (err === iceGatheringErr && retries < MAX_LISTEN_ONLY_RETRIES) {
+          this.joinListenOnly(++retries);
+        } else {
+          clearTimeout(iceGatheringTimeout);
+          logger.error('Listen only error:', err);
+          throw err;
         }
-
-        throw err;
       });
   }
 
@@ -218,8 +216,6 @@ class AudioManager {
       });
     }
 
-    clearInterval(this.fakeAudioInterval);
-
     if (!this.isEchoTest) {
       this.notify(this.messages.info.JOINED_AUDIO);
     }
@@ -269,7 +265,6 @@ class AudioManager {
       } else if (status === FAILED) {
         this.error = error;
         this.notify(this.messages.error[error] || this.messages.error.GENERIC_ERROR, true);
-        makeCall('failed callStateCallback audio', response);
         logger.error('Audio Error:', error, bridgeError);
         this.exitAudio();
         this.onAudioExit();
@@ -286,7 +281,21 @@ class AudioManager {
       new window.AudioContext() :
       new window.webkitAudioContext();
 
-    return this.listenOnlyAudioContext.createMediaStreamDestination().stream;
+    // Create a placeholder buffer to upstart audio context
+    const pBuffer = this.listenOnlyAudioContext.createBuffer(2, this.listenOnlyAudioContext.sampleRate * 3, this.listenOnlyAudioContext.sampleRate);
+
+    var dest = this.listenOnlyAudioContext.createMediaStreamDestination();
+
+    let audio = document.querySelector(MEDIA_TAG);
+
+    // Play bogus silent audio to try to circumvent autoplay policy on Safari
+    audio.src = 'resources/sounds/silence.mp3'
+
+    audio.play().catch(e => {
+      logger.warn('Error on playing test audio:', e);
+    });
+
+    return dest.stream;
   }
 
   isUsingAudio() {
@@ -359,19 +368,6 @@ class AudioManager {
       this.isListenOnly ? 'audio_on' : 'unmute',
     );
   }
-
-  playFakeAudio() {
-    const outputDeviceId = this.outputDeviceId;
-    const sound = new Audio('resources/sounds/silence.mp3');
-    if (outputDeviceId && sound.setSinkId) {
-      sound.setSinkId(outputDeviceId);
-    }
-    // Hack within the hack: haven't got time to get the right timing to play
-    // the audio on stock listen only, but I'll get back to it - prlanzarin
-    this.fakeAudioInterval = setInterval(() => {
-      sound.play();
-    }, 1000);
-  }
 }
 
 const audioManager = new AudioManager();
diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js
index d714597b58901dce4974b868daf0bf9d836a1887..d950defbd6f4164b26aaad269262c0a201f73f77 100755
--- a/bigbluebutton-html5/imports/ui/services/auth/index.js
+++ b/bigbluebutton-html5/imports/ui/services/auth/index.js
@@ -10,6 +10,17 @@ const CONNECTION_TIMEOUT = Meteor.settings.public.app.connectionTimeout;
 
 class Auth {
   constructor() {
+    this._loggedIn = {
+      value: false,
+      tracker: new Tracker.Dependency(),
+    };
+
+    const queryParams = new URLSearchParams(document.location.search);
+    if (queryParams.has('sessionToken')
+      && queryParams.get('sessionToken') !== Session.get('sessionToken')) {
+      return;
+    }
+
     this._meetingID = Storage.getItem('meetingID');
     this._userID = Storage.getItem('userID');
     this._authToken = Storage.getItem('authToken');
@@ -18,10 +29,6 @@ class Auth {
     this._confname = Storage.getItem('confname');
     this._externUserID = Storage.getItem('externUserID');
     this._fullname = Storage.getItem('fullname');
-    this._loggedIn = {
-      value: false,
-      tracker: new Tracker.Dependency(),
-    };
   }
 
   get meetingID() {
@@ -115,7 +122,7 @@ class Auth {
       sessionToken: this.sessionToken,
       fullname: this.fullname,
       externUserID: this.externUserID,
-      confname: this.confname
+      confname: this.confname,
     };
   }
 
@@ -149,7 +156,7 @@ class Auth {
     this.logoutURL = null;
     this.sessionToken = null;
     this.fullname = null;
-    this.externUserID = null
+    this.externUserID = null;
     this.confname = null;
     return Promise.resolve(...args);
   }
diff --git a/bigbluebutton-html5/imports/utils/statuses.js b/bigbluebutton-html5/imports/utils/statuses.js
index 9aa785f63997e224beb0e8ab4492bdfbc6833410..848c521c4788f787de3e1c763de21cc659dfe31b 100644
--- a/bigbluebutton-html5/imports/utils/statuses.js
+++ b/bigbluebutton-html5/imports/utils/statuses.js
@@ -9,7 +9,6 @@ export const EMOJI_STATUSES = {
   applause: 'applause',
   thumbsUp: 'thumbs_up',
   thumbsDown: 'thumbs_down',
-  none: 'clear_status',
 };
 
 export default { EMOJI_STATUSES };
diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json
index 7978989134a65c2d40b688fcce1f71bb759d520c..9f5fb78ceedb463b63fc4a886ae756ea02995b55 100755
--- a/bigbluebutton-html5/private/config/settings-development.json
+++ b/bigbluebutton-html5/private/config/settings-development.json
@@ -19,8 +19,8 @@
       "askForFeedbackOnLogout": false,
       "defaultSettings": {
         "application": {
-          "chatAudioNotifications": false,
-          "chatPushNotifications": false,
+          "chatAudioAlerts": false,
+          "chatPushAlerts": false,
           "fontSize": "16px",
           "fallbackLocale": "en"
         },
@@ -67,7 +67,8 @@
       "allowHTML5Moderator": true,
       "allowModeratorToUnmuteAudio": true,
       "httpsConnection": false,
-      "connectionTimeout": 60000
+      "connectionTimeout": 60000,
+      "showHelpButton": true
     },
 
     "kurento": {
@@ -78,6 +79,14 @@
       "chromeExtensionLink": "LINK",
       "chromeScreenshareSources": ["window", "screen"],
       "firefoxScreenshareSource": "window",
+      "cameraConstraints": {
+        "width": {
+          "max": 640
+        },
+        "height": {
+          "max": 480
+        }
+      },
       "enableScreensharing": false,
       "enableVideo": false,
       "enableVideoStats": false,
@@ -348,11 +357,12 @@
           }
         ]
       }
-      
     },
-    "log": [
-      { "target": "server", "level": "info" }
-    ]
+    "clientLog": {
+      "server": { "enabled": true, "level": "info" },
+      "console": { "enabled": false, "level": "debug" },
+      "external": { "enabled": false, "level": "info", "url": "https://LOG_HOST/html5Log", "method": "POST" }
+    }
   },
 
   "private": {
@@ -383,7 +393,7 @@
       ]
     },
 
-    "log": {
+    "serverLog": {
       "level": "info"
     }
   }
diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json
index 30a9d373a3366011fd946d906487f2ecf34c270f..93dbb95c08520accc3cb18ba300adfc14c1b22c1 100755
--- a/bigbluebutton-html5/private/config/settings-production.json
+++ b/bigbluebutton-html5/private/config/settings-production.json
@@ -19,8 +19,8 @@
       "askForFeedbackOnLogout": false,
       "defaultSettings": {
         "application": {
-          "chatAudioNotifications": false,
-          "chatPushNotifications": false,
+          "chatAudioAlerts": false,
+          "chatPushAlerts": false,
           "fontSize": "16px",
           "fallbackLocale": "en"
         },
@@ -67,7 +67,8 @@
       "allowHTML5Moderator": true,
       "allowModeratorToUnmuteAudio": true,
       "httpsConnection": true,
-      "connectionTimeout": 10000
+      "connectionTimeout": 10000,
+      "showHelpButton": true
     },
 
     "kurento": {
@@ -78,6 +79,14 @@
       "chromeExtensionLink": "LINK",
       "chromeScreenshareSources": ["window", "screen"],
       "firefoxScreenshareSource": "window",
+      "cameraConstraints": {
+        "width": {
+          "max": 640
+        },
+        "height": {
+          "max": 480
+        }
+      },
       "enableScreensharing": false,
       "enableVideo": false,
       "enableVideoStats": false,
@@ -349,9 +358,11 @@
         ]
       }
     },
-    "log": [
-      { "target": "server", "level": "info" }
-    ]
+    "clientLog": {
+      "server": { "enabled": true, "level": "info" },
+      "console": { "enabled": false, "level": "debug" },
+      "external": { "enabled": false, "level": "info", "url": "https://LOG_HOST/html5Log", "method": "POST" }
+    }
   },
 
   "private": {
@@ -382,7 +393,7 @@
       ]
     },
 
-    "log": {
+    "serverLog": {
       "level": "info"
     }
   }
diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json
index b7a1218f7a8fc4b0048404bee047a55b71c0b755..294b61f0f2aea2f22298ce0d88b48d1aa510b362 100644
--- a/bigbluebutton-html5/private/locales/de.json
+++ b/bigbluebutton-html5/private/locales/de.json
@@ -97,6 +97,8 @@
     "app.navBar.settingsDropdown.exitFullscreenDesc": "Vollbildmodus beenden",
     "app.navBar.settingsDropdown.hotkeysLabel": "Tastaturkürzel",
     "app.navBar.settingsDropdown.hotkeysDesc": "Liste verfügbarer Tastaturkürzel",
+    "app.navBar.settingsDropdown.helpLabel": "Hilfe",
+    "app.navBar.settingsDropdown.helpDesc": "Verlinkt zu den Videoanleitungen",
     "app.navBar.userListToggleBtnLabel": "Teilnehmerliste umschalten",
     "app.navBar.toggleUserList.ariaLabel": "Teilnehmer/Nachrichten-Umschalter",
     "app.navBar.toggleUserList.newMessages": "mit Benachrichtigung für neue Nachrichten",
@@ -126,8 +128,8 @@
     "app.actionsBar.raiseLabel": "Hand heben",
     "app.actionsBar.label": "Aktionsleiste",
     "app.submenu.application.applicationSectionTitle": "Anwendung",
-    "app.submenu.application.audioNotifyLabel": "Audiobenachrichtigungen für Chat",
-    "app.submenu.application.pushNotifyLabel": "Push-Benachrichtigungen für Chat",
+    "app.submenu.application.audioAlertLabel": "Audiowarnungen für Chat",
+    "app.submenu.application.pushAlertLabel": "Popupwarnungen für Chat",
     "app.submenu.application.fontSizeControlLabel": "Schriftgröße",
     "app.submenu.application.increaseFontBtnLabel": "Schriftgröße erhöhen",
     "app.submenu.application.decreaseFontBtnLabel": "Schriftgröße verringern",
@@ -320,6 +322,7 @@
     "app.video.joinVideo": "Webcam freigeben",
     "app.video.leaveVideo": "Webcam stoppen",
     "app.video.iceCandidateError": "Fehler beim Hinzufügen vom ice candidate",
+    "app.video.iceConnectionStateError": "Fehler 1107: ICE Übertragung fehlgeschlagen",
     "app.video.permissionError": "Fehler bei Freigabe der Webcam. Bitte Berechtigungen prüfen",
     "app.video.sharingError": "Fehler bei Freigabe der Webcam",
     "app.video.notFoundError": "Konnte keine Webcam finden. Stellen Sie sicher, dass sie angeschlossen ist",
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 38f308196217e82dbcdf6e433f04659feb83a4d5..f4a01c30d4a5840d9763a1269c6cdab8c71975a8 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -97,6 +97,8 @@
     "app.navBar.settingsDropdown.exitFullscreenDesc": "Exit fullscreen mode",
     "app.navBar.settingsDropdown.hotkeysLabel": "Hotkeys",
     "app.navBar.settingsDropdown.hotkeysDesc": "Listing of available hotkeys",
+    "app.navBar.settingsDropdown.helpLabel": "Help",
+    "app.navBar.settingsDropdown.helpDesc": "Links user to video tutorials",
     "app.navBar.userListToggleBtnLabel": "User List Toggle",
     "app.navBar.toggleUserList.ariaLabel": "Users and Messages Toggle",
     "app.navBar.toggleUserList.newMessages": "with new message notification",
@@ -126,8 +128,8 @@
     "app.actionsBar.raiseLabel": "Raise",
     "app.actionsBar.label": "Actions Bar",
     "app.submenu.application.applicationSectionTitle": "Application",
-    "app.submenu.application.audioNotifyLabel": "Audio notifications for chat",
-    "app.submenu.application.pushNotifyLabel": "Push notifications for chat",
+    "app.submenu.application.audioAlertLabel": "Audio Alerts for Chat",
+    "app.submenu.application.pushAlertLabel": "Popup Alerts for Chat",
     "app.submenu.application.fontSizeControlLabel": "Font size",
     "app.submenu.application.increaseFontBtnLabel": "Increase Application Font Size",
     "app.submenu.application.decreaseFontBtnLabel": "Decrease Application Font Size",
@@ -200,7 +202,7 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with",
     "app.actionsBar.actionsDropdown.startRecording": "Start recording",
     "app.actionsBar.actionsDropdown.stopRecording": "Stop recording",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "Set a Status",
+    "app.actionsBar.emojiMenu.statusTriggerLabel": "Set Status",
     "app.actionsBar.emojiMenu.awayLabel": "Away",
     "app.actionsBar.emojiMenu.awayDesc": "Change your status to away",
     "app.actionsBar.emojiMenu.raiseHandLabel": "Raise",
@@ -320,6 +322,7 @@
     "app.video.joinVideo": "Share Webcam",
     "app.video.leaveVideo": "Unshare Webcam",
     "app.video.iceCandidateError": "Error on adding ice candidate",
+    "app.video.iceConnectionStateError": "Error 1107: ICE negotiation failed",
     "app.video.permissionError": "Error on sharing webcam. Please check permissions",
     "app.video.sharingError": "Error on sharing webcam",
     "app.video.notFoundError": "Could not find webcam. Please make sure it's connected",
@@ -346,6 +349,7 @@
     "app.video.stats.rtt": "RTT",
     "app.video.stats.encodeUsagePercent": "Encode usage",
     "app.video.stats.currentDelay": "Current delay",
+    "app.deskshare.iceConnectionStateError": "Error 1108: ICE connection failed when sharing screen",
     "app.sfu.mediaServerConnectionError2000": "Error 2000: Unable to connect to media server",
     "app.sfu.mediaServerOffline2001": "Error 2001: Media server is offline. Please try again later.",
     "app.sfu.mediaServerNoResources2002": "Error 2002: Media server has no available resources",
diff --git a/bigbluebutton-html5/private/locales/es_ES.json b/bigbluebutton-html5/private/locales/es_ES.json
index 65b630baf85c8892ab5786d947ad49df704b28c6..91c9a9370cbee1044dae7d52077bc0522103aba8 100644
--- a/bigbluebutton-html5/private/locales/es_ES.json
+++ b/bigbluebutton-html5/private/locales/es_ES.json
@@ -84,8 +84,6 @@
     "app.actionsBar.raiseLabel": "Alzar",
     "app.actionsBar.label": "Barra de acciones",
     "app.submenu.application.applicationSectionTitle": "Aplicación",
-    "app.submenu.application.audioNotifyLabel": "Notificación de audio para el chat",
-    "app.submenu.application.pushNotifyLabel": "Empujar notificaciones para el chat",
     "app.submenu.application.fontSizeControlLabel": "Tamaño de fuente",
     "app.submenu.application.increaseFontBtnLabel": "Incrementar tamaño de fuente",
     "app.submenu.application.decreaseFontBtnLabel": "Reducir tamaño de fuente",
diff --git a/bigbluebutton-html5/private/locales/fa_IR.json b/bigbluebutton-html5/private/locales/fa_IR.json
new file mode 100644
index 0000000000000000000000000000000000000000..4136ce2d4c1f7786e942c26c490eaaa35b9d6b2f
--- /dev/null
+++ b/bigbluebutton-html5/private/locales/fa_IR.json
@@ -0,0 +1,396 @@
+{
+    "app.home.greeting": "خوش آمدید{0}. ارائه شما به زودی آغاز میشود.",
+    "app.chat.submitLabel": "ارسال پیام",
+    "app.chat.errorMinMessageLength": "پیام شما {0} کاراکتر(0) است، برای ارسال، کوتاه است.",
+    "app.chat.errorMaxMessageLength": "پیام شما {0} کارکتر(0) است  و برای ارسال خیلی طولانیست.",
+    "app.chat.inputLabel": "ورودی پیام برای چت{0}",
+    "app.chat.inputPlaceholder": "پیام{0}",
+    "app.chat.titlePublic": "گفتگوی عمومی",
+    "app.chat.titlePrivate": "گفتگوی خصوصی با{0}",
+    "app.chat.partnerDisconnected": "{0}کلاس را ترک کرد.",
+    "app.chat.closeChatLabel": "خروج{0}",
+    "app.chat.hideChatLabel": "مخفی کردن{0}",
+    "app.chat.moreMessages": "مشاهده ادامه پیام های پایین",
+    "app.chat.dropdown.options": "گزینه های گفتگو",
+    "app.chat.dropdown.clear": "پاک سازی",
+    "app.chat.dropdown.copy": "کپی کردن",
+    "app.chat.dropdown.save": "ذخیره کردن",
+    "app.chat.label": "گفتگو",
+    "app.chat.emptyLogLabel": "پاک کردن سابقه گفتگو",
+    "app.chat.clearPublicChatMessage": "سابقه گفتگوها توسط مدیر حذف گردید.",
+    "app.userList.usersTitle": "دانش آموزان",
+    "app.userList.participantsTitle": "دانش آموزان",
+    "app.userList.messagesTitle": "پیام ها",
+    "app.userList.presenter": "استاد",
+    "app.userList.you": "شما",
+    "app.userList.locked": "قفل شده",
+    "app.userList.label": "لیست دانش آموزان",
+    "app.userList.toggleCompactView.label": "مخفی کردن نوار ابزار",
+    "app.userList.guest": "مهمان",
+    "app.userList.menuTitleContext": "گزینه های قابل اعمال",
+    "app.userList.chatListItem.unreadSingular": "{0} پیام جدید",
+    "app.userList.chatListItem.unreadPlural": "{0} پیام های جدید",
+    "app.userList.menu.chat.label": "گفتگو",
+    "app.userList.menu.clearStatus.label": "پاک کردن وضعیت",
+    "app.userList.menu.makePresenter.label": "تبدیل به استاد",
+    "app.userList.menu.removeUser.label": "حذف کاربر",
+    "app.userList.menu.muteUserAudio.label": "بستن صدای کاربر",
+    "app.userList.menu.unmuteUserAudio.label": "فعال سازی میکروفن کاربر",
+    "app.userList.userAriaLabel": "{0} {1} {2} وضعیت {3}",
+    "app.userList.menu.promoteUser.label": "ارتقا نقش به مدیر",
+    "app.userList.menu.demoteUser.label": "تبدیل به دانش آموز",
+    "app.media.label": "مدیا",
+    "app.media.screenshare.start": "اشتراک صفحه نمایش شروع شد",
+    "app.media.screenshare.end": "اشتراک صفحه نمایش به پایان رسید.",
+    "app.media.screenshare.safariNotSupported": "در حال حاضر اشتراک صفحه نمایش در مرورگر سافاری پشتیبانی نمیشد، لطفا از کروم یا فایرفاکس استفاده کنید.",
+    "app.meeting.ended": "جلسه پایان یافت",
+    "app.meeting.endedMessage": "شما در حال انتقال به صفحه اصلی هستید.",
+    "app.presentation.presentationToolbar.prevSlideLabel": "اسلاید قبلی",
+    "app.presentation.presentationToolbar.prevSlideDesc": "تغییر ارائه به اسلاید قبلی",
+    "app.presentation.presentationToolbar.nextSlideLabel": "اسلاید بعدی",
+    "app.presentation.presentationToolbar.nextSlideDesc": "تغییر ارائه به اسلاید بعدی",
+    "app.presentation.presentationToolbar.skipSlideLabel": "بیرون رفتن از اسلاید",
+    "app.presentation.presentationToolbar.skipSlideDesc": "تغییر ارائه به یک اسلاید خاص",
+    "app.presentation.presentationToolbar.fitWidthLabel": "اندازه تصویر را متناسب با عرض صفحه کن.",
+    "app.presentation.presentationToolbar.fitWidthDesc": "کلیه عرض اسلاید را نمایش بده",
+    "app.presentation.presentationToolbar.fitScreenLabel": "ابعاد را به اندازه تمام  صفحه کن",
+    "app.presentation.presentationToolbar.fitScreenDesc": "کل اسلاید را نمایش بده",
+    "app.presentation.presentationToolbar.zoomLabel": "بزرگ نمایی",
+    "app.presentation.presentationToolbar.zoomDesc": "تغییر مقدار زوم ارائه",
+    "app.presentation.presentationToolbar.goToSlide": "اسلاید {0}",
+    "app.presentationUploder.title": "ارائه",
+    "app.presentationUploder.message": "شما به عنوان عضوی از کلاس میتوانید جزوه خود را با فرمت فایل های Office آپلود کنید. پیشنهاد ما فایل های PDF است.",
+    "app.presentationUploder.confirmLabel": "شروع",
+    "app.presentationUploder.confirmDesc": "تغییرات خود را ذخیره کنید و ارائه را آغاز نمایید.",
+    "app.presentationUploder.dismissLabel": "کنسل",
+    "app.presentationUploder.dismissDesc": "بستن پنجره و حذف تغییرات شما",
+    "app.presentationUploder.dropzoneLabel": "فایل های خود را برای آپلود به اینجا بکشید.",
+    "app.presentationUploder.browseFilesLabel": "یا برای انتخاب فایل کلیک کنید.",
+    "app.presentationUploder.fileToUpload": "فایلهایی که آپلود خواهند شد.",
+    "app.presentationUploder.currentBadge": "جاری",
+    "app.presentationUploder.genericError": "اشتباهی اتفاق افتاد.",
+    "app.presentationUploder.upload.progress": "در حال آپلود ({0}%)",
+    "app.presentationUploder.upload.413": "حجم فایل خیلی زیاد است.",
+    "app.presentationUploder.conversion.conversionProcessingSlides": "در حال پردازش صفحه {0} از {1}",
+    "app.presentationUploder.conversion.genericConversionStatus": "در حال تبدیل فایل",
+    "app.presentationUploder.conversion.generatingThumbnail": "در حال ساخت تصاویر کوچک از فایل",
+    "app.presentationUploder.conversion.generatedSlides": "اسلاید ها ساخته شدند.",
+    "app.presentationUploder.conversion.generatingSvg": "در حال ساخت تصاویر SVG",
+    "app.presentationUploder.conversion.pageCountExceeded": "تعداد صفحات از حد اکثر صفحات مورد پشتیبانی بیشتر است.",
+    "app.presentationUploder.conversion.timeout": "تبدیل فایل بیش از حد طول کشید.",
+    "app.polling.pollingTitle": "گزینه های نظرسنجی.",
+    "app.polling.pollAnswerLabel": "پاسخ نظرسنجی {0}",
+    "app.polling.pollAnswerDesc": "این گزینه را برای رای دادن به {0} انتخاب کنید.",
+    "app.failedMessage": "عذر خواهی میکنیم. مشکلی در ارتباط با سرور وجود دارد.",
+    "app.connectingMessage": "در حال اتصال...",
+    "app.waitingMessage": "ارتباط قطع شد. در حال تلاش مجدد در {0} ثانیه",
+    "app.navBar.settingsDropdown.optionsLabel": "گزینه ها",
+    "app.navBar.settingsDropdown.fullscreenLabel": "تمام صفحه",
+    "app.navBar.settingsDropdown.settingsLabel": "تنظیمات",
+    "app.navBar.settingsDropdown.aboutLabel": "درباره",
+    "app.navBar.settingsDropdown.leaveSessionLabel": "خروج",
+    "app.navBar.settingsDropdown.exitFullscreenLabel": "بستن حالت تمام صفحه",
+    "app.navBar.settingsDropdown.fullscreenDesc": "منوی تنظیمات را تمام صفحه کن",
+    "app.navBar.settingsDropdown.settingsDesc": "تغییر تنظیمات معمول",
+    "app.navBar.settingsDropdown.aboutDesc": "نمایش اطلاعات دانش آموز",
+    "app.navBar.settingsDropdown.leaveSessionDesc": "ترک جلسه",
+    "app.navBar.settingsDropdown.exitFullscreenDesc": "خروج از حالت تمام صفحه",
+    "app.navBar.settingsDropdown.hotkeysLabel": "کلیدهای میانبر",
+    "app.navBar.settingsDropdown.hotkeysDesc": "لیست کلیدهای میانبر",
+    "app.navBar.userListToggleBtnLabel": "نمایش لیست کاربران",
+    "app.navBar.toggleUserList.ariaLabel": "نمایش گفتگوی دانش آموزان و اساتید",
+    "app.navBar.toggleUserList.newMessages": "با اعلان پیام جدید",
+    "app.navBar.recording": "جلسه در حال ضبط شدن است.",
+    "app.navBar.recording.on": "در حال ضبط",
+    "app.navBar.recording.off": "ضبط نمیشود.",
+    "app.leaveConfirmation.title": "ترک جلسه",
+    "app.leaveConfirmation.message": "آیا میخواهید جلسه را ترک کنید؟",
+    "app.leaveConfirmation.confirmLabel": "ترک",
+    "app.leaveConfirmation.confirmDesc": "پیام های شما بیرون از جلسه",
+    "app.leaveConfirmation.dismissLabel": "کنسل",
+    "app.leaveConfirmation.dismissDesc": "ترک جلسه را رد کرد و آن را بست.",
+    "app.leaveConfirmation.endMeetingLabel": "بله و بستن جلسه",
+    "app.leaveConfirmation.endMeetingAriaLabel": "ترک و پایان جلسه",
+    "app.leaveConfirmation.endMeetingDesc": "تایید خروج از جلسه را بست.",
+    "app.about.title": "درباره",
+    "app.about.version": "نسخه کاربر",
+    "app.about.copyright": "کپی رایت",
+    "app.about.confirmLabel": "تایید",
+    "app.about.confirmDesc": "تایید",
+    "app.about.dismissLabel": "کنسل",
+    "app.about.dismissDesc": "بستن قسمت درباره کاربر",
+    "app.actionsBar.changeStatusLabel": "تغییر وضعیت دانش آموز",
+    "app.actionsBar.muteLabel": "حالت بی صدا",
+    "app.actionsBar.unmuteLabel": "فعال سازی صدا",
+    "app.actionsBar.camOffLabel": "بستن دوربین",
+    "app.actionsBar.raiseLabel": "اجازه گرفتن از استاد",
+    "app.actionsBar.label": "منوی امکانات",
+    "app.submenu.application.applicationSectionTitle": "نرم افزار",
+    "app.submenu.application.fontSizeControlLabel": "اندازه متن",
+    "app.submenu.application.increaseFontBtnLabel": "افزایش اندازه متن نرم افزار",
+    "app.submenu.application.decreaseFontBtnLabel": "کاهش اندازه متن نرم افزار",
+    "app.submenu.application.languageLabel": "زبان نرم افزار",
+    "app.submenu.application.ariaLanguageLabel": "تغییر زبان نرم افزار",
+    "app.submenu.application.languageOptionLabel": "انتخاب زبان",
+    "app.submenu.application.noLocaleOptionLabel": "هیچ زبان فعالی وجود ندارد",
+    "app.submenu.audio.micSourceLabel": "ورودی صدای میکروفن",
+    "app.submenu.audio.speakerSourceLabel": "صدای خروجی",
+    "app.submenu.audio.streamVolumeLabel": "ولوم صدای میکروفن شما",
+    "app.submenu.video.title": "تصویر کلاس",
+    "app.submenu.video.videoSourceLabel": "منبع تصویر",
+    "app.submenu.video.videoOptionLabel": "انتخاب منبع تصویر",
+    "app.submenu.video.videoQualityLabel": "کیفیت تصویر",
+    "app.submenu.video.qualityOptionLabel": "انتخاب کیفیت تصویر",
+    "app.submenu.video.participantsCamLabel": "مشاهده دوربین کاربران",
+    "app.submenu.closedCaptions.closedCaptionsLabel": "کپشن های بسته شده",
+    "app.submenu.closedCaptions.takeOwnershipLabel": "بدست گرفتن کنترل",
+    "app.submenu.closedCaptions.languageLabel": "زبان",
+    "app.submenu.closedCaptions.localeOptionLabel": "انتخاب زبان",
+    "app.submenu.closedCaptions.noLocaleOptionLabel": "هیچ زبان فعالی وجود ندارد.",
+    "app.submenu.closedCaptions.fontFamilyLabel": "فونت",
+    "app.submenu.closedCaptions.fontFamilyOptionLabel": "انتخاب فونت",
+    "app.submenu.closedCaptions.fontSizeLabel": "اندازه فونت",
+    "app.submenu.closedCaptions.fontSizeOptionLabel": "انتخاب اندازه فونت",
+    "app.submenu.closedCaptions.backgroundColorLabel": "رنگ پس زمینه",
+    "app.submenu.closedCaptions.fontColorLabel": "رنگ متن",
+    "app.submenu.participants.muteAllLabel": "غیر فعال سازی صدای کاربران به جز اساتید",
+    "app.submenu.participants.lockAllLabel": "قفل کردن کابران",
+    "app.submenu.participants.lockItemLabel": "شرکت کنندگان{0}",
+    "app.submenu.participants.lockMicDesc": "غیر فعال سازی میکروفن شرکت کنندگان قفل شده",
+    "app.submenu.participants.lockCamDesc": "غیر فعال سازی دوربین کاربران قفل شده.",
+    "app.submenu.participants.lockPublicChatDesc": "غیرفعال سازی گفتگوی عمومی کابران قفل شده",
+    "app.submenu.participants.lockPrivateChatDesc": "غیرفعال سازی گفتگوی خصوصی برای کابران قفل شده.",
+    "app.submenu.participants.lockLayoutDesc": "قفل کردن چینش صفحه کابران قفل شده",
+    "app.submenu.participants.lockMicAriaLabel": "قفل کردن میکروفن",
+    "app.submenu.participants.lockCamAriaLabel": "قفل کردن دوربین",
+    "app.submenu.participants.lockPublicChatAriaLabel": "قفل کردن گفتگوی عمومی",
+    "app.submenu.participants.lockPrivateChatAriaLabel": "قفل کردن گفتگوی خصوصی",
+    "app.submenu.participants.lockLayoutAriaLabel": "قفل کردن چینش تصویر",
+    "app.submenu.participants.lockMicLabel": "میکروفن",
+    "app.submenu.participants.lockCamLabel": "دوربین",
+    "app.submenu.participants.lockPublicChatLabel": "گفتگوی عمومی",
+    "app.submenu.participants.lockPrivateChatLabel": "گفتگوی خصوصی",
+    "app.submenu.participants.lockLayoutLabel": "چینش تصویر",
+    "app.settings.applicationTab.label": "نرم افزار",
+    "app.settings.audioTab.label": "صدا",
+    "app.settings.videoTab.label": "تصویر کلاس",
+    "app.settings.closedcaptionTab.label": "کپشن های بسته شده",
+    "app.settings.usersTab.label": "شرکت کنندگان در کلاس",
+    "app.settings.main.label": "تنظیمات",
+    "app.settings.main.cancel.label": "کنسل",
+    "app.settings.main.cancel.label.description": "حذف تغییرات و بستن منوی تنظیمات",
+    "app.settings.main.save.label": "ذخیره",
+    "app.settings.main.save.label.description": "ذخیره تغییرات و بستن منوی تنظیمات",
+    "app.settings.dataSavingTab.label": "کاهش مصرف اینترنت",
+    "app.settings.dataSavingTab.webcam": "فعال سازی دوربین کلاس",
+    "app.settings.dataSavingTab.screenShare": "فعال سازی اشتراک گذاری تصویر کامپیوتر",
+    "app.settings.dataSavingTab.description": "برای صرفه جویی در مصرف اینترنت آیتم هایی که باید نمایش داده شوند را انتخاب کنید.",
+    "app.switch.onLabel": "روشن",
+    "app.switch.offLabel": "خاموش",
+    "app.actionsBar.actionsDropdown.actionsLabel": "فعالیت ها",
+    "app.actionsBar.actionsDropdown.presentationLabel": "آپلود فایل ارائه",
+    "app.actionsBar.actionsDropdown.initPollLabel": "ایجاد نظرسنجی",
+    "app.actionsBar.actionsDropdown.desktopShareLabel": "اشتراک صفحه نمایش شما",
+    "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "متوقف کردن اشتراک گذاری دسکتاپ شما",
+    "app.actionsBar.actionsDropdown.presentationDesc": "آپلود فایل ارائه شما",
+    "app.actionsBar.actionsDropdown.initPollDesc": "ایجاد نظرسنجی",
+    "app.actionsBar.actionsDropdown.desktopShareDesc": "اشتراک گذاری تصویر کامپیوتر شما با دیگران",
+    "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "توقف اشتراک گذاری تصویر کامپیوتر شما با دیگران",
+    "app.actionsBar.actionsDropdown.startRecording": "شروع ضبط کلاس",
+    "app.actionsBar.actionsDropdown.stopRecording": "توقف ضبط کلاس",
+    "app.actionsBar.emojiMenu.awayLabel": "عدم حضور در کلاس",
+    "app.actionsBar.emojiMenu.awayDesc": "تغییر وضعیت شما به عدم حضور در کلاس",
+    "app.actionsBar.emojiMenu.raiseHandLabel": "اجازه گرفتن از استاد",
+    "app.actionsBar.emojiMenu.raiseHandDesc": "دست خود را بالا ببرید تا از استاد سوال بپرسید",
+    "app.actionsBar.emojiMenu.neutralLabel": "تصمیم گیری نشده",
+    "app.actionsBar.emojiMenu.neutralDesc": "تغییر وضعیت شما به تصمیم گیری نشده",
+    "app.actionsBar.emojiMenu.confusedLabel": "کامل نفهمیدم",
+    "app.actionsBar.emojiMenu.confusedDesc": "تغییر وضعیت شما به وضعیت کامل نفهمیدم",
+    "app.actionsBar.emojiMenu.sadLabel": "ناراحت",
+    "app.actionsBar.emojiMenu.sadDesc": "تغییر حالت شما به ناراحت",
+    "app.actionsBar.emojiMenu.happyLabel": "شاد",
+    "app.actionsBar.emojiMenu.happyDesc": "تغییر وضعیت شما به خوشحال",
+    "app.actionsBar.emojiMenu.noneLabel": "پاک کردن وضعیت",
+    "app.actionsBar.emojiMenu.noneDesc": "پاک کردن وضعیت شما",
+    "app.actionsBar.emojiMenu.applauseLabel": "تحسین کردن.",
+    "app.actionsBar.emojiMenu.applauseDesc": "تغییر وضعیت شما به تحسین کردن",
+    "app.actionsBar.emojiMenu.thumbsUpLabel": "عالی",
+    "app.actionsBar.emojiMenu.thumbsUpDesc": "تغییر وضعیت شما به عالی",
+    "app.actionsBar.emojiMenu.thumbsDownLabel": "نه چندان خوب",
+    "app.actionsBar.emojiMenu.thumbsDownDesc": "تغییر وضعیت شما به نه چندان خوب",
+    "app.actionsBar.currentStatusDesc": "وضعیت فعلی شما{0}",
+    "app.audioNotification.audioFailedError1001": "خطای 1001: سوکت شما قطع شد",
+    "app.audioNotification.audioFailedError1002": "خطای 1002: امکان ارتباط با سوکت برای شما فراهم نیست، وضعیت نت خود را بررسی کنید یا آن را تغییر دهید.",
+    "app.audioNotification.audioFailedError1003": "خطای 1003: نسخه مرورگر شما برای کلاس مناسب نیست.",
+    "app.audioNotification.audioFailedError1004": "خطای 1004: خطا در تماس",
+    "app.audioNotification.audioFailedError1005": "خطای 1005: تماس به صورت نامشخصی قطع شد",
+    "app.audioNotification.audioFailedError1006": "خطای 1006: تماس بسیار طولانی شد",
+    "app.audioNotification.audioFailedError1007": "خطای 1007: خطای ارتباط ice",
+    "app.audioNotification.audioFailedError1008": "خطای 1008: انتقال میسر نشد",
+    "app.audioNotification.audioFailedError1009": "خطای 1009: امکان پیدا کردن STUN/TURN وجود ندارد.",
+    "app.audioNotification.audioFailedError1010": "خطای 1010: ICE برقرار نشد اینترنت خود را چک کنید.",
+    "app.audioNotification.audioFailedError1011": "خطای 1011: ICE برقرار نشد",
+    "app.audioNotification.audioFailedMessage": "امکان اتصال صدای شما فراهم نیست.",
+    "app.audioNotification.mediaFailedMessage": "امکان گرفتن صدای میکروفن کاربر وجود ندارد.",
+    "app.audioNotification.closeLabel": "بستن",
+    "app.audioNotificaion.reconnectingAsListenOnly": "صدای میکروفن برای مدیران فقط فعال است، شما فقط شنونده هستید.",
+    "app.breakoutJoinConfirmation.title": "رفتن به اتاق استراحت",
+    "app.breakoutJoinConfirmation.message": "آیا میخواهید وارد شوید",
+    "app.breakoutJoinConfirmation.confirmLabel": "پیوستن",
+    "app.breakoutJoinConfirmation.confirmDesc": "پیوستن به اتاق استراحت",
+    "app.breakoutJoinConfirmation.dismissLabel": "کنسل",
+    "app.breakoutJoinConfirmation.dismissDesc": "بستن و رد کردن پیوستن به اتاق استراحت",
+    "app.breakoutJoinConfirmation.freeJoinMessage": "انتخاب اتاق استراحت برای پیوستن",
+    "app.breakoutTimeRemainingMessage": "زمان باقی مانده از اتاق استراحت: {0}",
+    "app.breakoutWillCloseMessage": "زمان پایان یافت. اتاق استراحت به زودی بسته میشود",
+    "app.calculatingBreakoutTimeRemaining": "محاسبه زمان باقی مانده...",
+    "app.audioModal.microphoneLabel": "گوینده هم باشم.",
+    "app.audioModal.listenOnlyLabel": "فقط شنونده باشم.",
+    "app.audioModal.audioChoiceLabel": "چگونه میخواهید صدا را دریافت کنید.",
+    "app.audioModal.iOSBrowser": "صدا/تصویر پشتیبانی نمیشود.",
+    "app.audioModal.iOSErrorDescription": "در حال حاضر صدا و تصویر در کروم iOS پشتیبانی نمیشود.",
+    "app.audioModal.iOSErrorRecommendation": "پیشنهاد ما استفاده از سافاری است.",
+    "app.audioModal.audioChoiceDesc": "انتخاب کنید چگونه صدا را در این جلسه میخواهید دریافت کنید",
+    "app.audioModal.closeLabel": "بستن",
+    "app.audioModal.yes": "بله",
+    "app.audioModal.no": "خیر",
+    "app.audioModal.yes.arialabel" : "اکو شنیده میشود",
+    "app.audioModal.no.arialabel" : "اکو شنیده نمیشود",
+    "app.audioModal.echoTestTitle": "این یک تست اکو خصوصی است.چند کلمه صحبت کنید.آیا صدایتان را میشنوید؟",
+    "app.audioModal.settingsTitle": "تغییر تنظیمات صدا",
+    "app.audioModal.helpTitle": "یک مشکلی در سخت افزار شما وجود دارد",
+    "app.audioModal.helpText": "آیا به مرورگر خود در قسمت تنظیمات اجازه دسترسی به میکروفن را داده اید؟ اگر اینطور نیست مطابق دستورالعمل مرورگرتان عمل کرده و اجازه دسترسی به میکروفن را فعال کنید.",
+    "app.audioModal.connecting": "در حال اتصال...",
+    "app.audioModal.connectingEchoTest": "اتصال به تست اکو",
+    "app.audioManager.joinedAudio": "شما به جلسه صوتی وارد شده اید",
+    "app.audioManager.joinedEcho": "شما به تست اکو پیوسته اید",
+    "app.audioManager.leftAudio": "شما جلسه صوتی را ترک کرده اید",
+    "app.audioManager.genericError": "خطا: مشکلی پیش آمده است لطفا مجدد سعی کنید.",
+    "app.audioManager.connectionError": "خطا: خطای ارتباط",
+    "app.audioManager.requestTimeout": "خطا: خطای طول کشیدن بیش از اندازه در درخواست.",
+    "app.audioManager.invalidTarget": "خطا: تلاش برای درخواست چیزی غیر صحیح",
+    "app.audioManager.mediaError": "خطا: مشکلی در اتصال سخت افزار شما با کلاس موجود است.",
+    "app.audio.joinAudio": "پیوستن به صدا",
+    "app.audio.leaveAudio": "ترک صدا",
+    "app.audio.enterSessionLabel": "ورود به جلسه",
+    "app.audio.playSoundLabel": "پخش صدا",
+    "app.audio.backLabel": "بازگشت",
+    "app.audio.audioSettings.titleLabel": "انتخاب تنظیمات صدای شما",
+    "app.audio.audioSettings.descriptionLabel": "لطفا توجه کنید، یک پیام در مرورگر شما ظاهر میشود و از شما میخواهد اجازه اشتراک میکروفن خود را بدهید.",
+    "app.audio.audioSettings.microphoneSourceLabel": "منبع ورودی میکروفن",
+    "app.audio.audioSettings.speakerSourceLabel": "منبع خروجی صدا",
+    "app.audio.audioSettings.microphoneStreamLabel": "ولوم صدای میکروفن شما",
+    "app.audio.audioSettings.retryLabel": "تلاش مجدد",
+    "app.audio.listenOnly.backLabel": "بازگشت",
+    "app.audio.listenOnly.closeLabel": "بستن",
+    "app.audio.permissionsOverlay.title": "اجازه دهید کلاس به میکروفن  شما دسترسی داشته باشد.",
+    "app.audio.permissionsOverlay.hint": "برای پیوستن شما به کنفرانس صوتی نیاز به اجازه شما می باشد. :)",
+    "app.error.removed": "شما از کنفرانس کنار گذاشته شده اید.",
+    "app.error.meeting.ended": "شما از کنفرانس خارج شده اید",
+    "app.dropdown.close": "بستن",
+    "app.error.500": "خطای پیش آمده است.",
+    "app.error.404": "پیدا نشد",
+    "app.error.401": "شناسایی نشد",
+    "app.error.403": "ممنوع",
+    "app.error.leaveLabel": "ورود مجدد",
+    "app.guest.waiting": "منتظر تایید جهت پیوستن",
+    "app.toast.breakoutRoomEnded": "اتاق استراحت پایان یافت. لطفا مجدد به کنفرانس صوتی بپیوندید.",
+    "app.toast.chat.public": "پیام جدید در گفتگوی عمومی",
+    "app.toast.chat.private": "پیام جدید در گفتگوی خصوصی",
+    "app.toast.chat.system": "سیستم",
+    "app.notification.recordingStart": "جلسه در حال ضبط شدن است",
+    "app.notification.recordingStop": "جلسه دیگر ضبط نمیشود.",
+    "app.shortcut-help.title": "کلید های میانبر",
+    "app.shortcut-help.accessKeyNotAvailable": "کلیدهای دسترسی موجود نیستند.",
+    "app.shortcut-help.comboLabel": "کمبو",
+    "app.shortcut-help.functionLabel": "تابع",
+    "app.shortcut-help.closeLabel": "بستن",
+    "app.shortcut-help.closeDesc": "بستن صفحه کلیدهای میانبر",
+    "app.shortcut-help.openOptions": "باز کردن گزینه ها",
+    "app.shortcut-help.toggleUserList": "مشاهده لیست دانش آموزان",
+    "app.shortcut-help.toggleMute": "حالت بی صدا/با صدا",
+    "app.shortcut-help.togglePublicChat": "نمایش گفتگوی عمومی(لیتس کاربران باید فعال باشد)",
+    "app.shortcut-help.hidePrivateChat": "پنهان کردن گفتگوی خصوصی",
+    "app.shortcut-help.closePrivateChat": "بستن گفتگوی خصوصی",
+    "app.shortcut-help.openActions": "باز کردن منوی کارها",
+    "app.shortcut-help.openStatus": "باز کردن منوی وضعیت",
+    "app.video.joinVideo": "اشتراک گذاری دوربین",
+    "app.video.leaveVideo": "بستن دوربین",
+    "app.video.iceCandidateError": "خطا در افزودن ICE",
+    "app.video.permissionError": "خطا در اشتراک گذاری دوربین. لطفا دسترسی ها را چک کنید.",
+    "app.video.sharingError": "خطا در اشتراک دوربین",
+    "app.video.notFoundError": "دوربین یافت نشد. لطفا مطمئن شوید که دوربین متصل است.",
+    "app.video.notAllowed": "اجازه دسترسی مرورگر به دوربین داده نشده است. لطفا دسترسی های مرورگر را چک کنید.",
+    "app.video.notSupportedError": "مطمئن شوید SSL فعال است.",
+    "app.video.notReadableError": "عدم امکان دسترسی به داده های دوربین، مطمئن شوید دوربین شما جای دیگری درگیر نیست.",
+    "app.video.swapCam": "جابجا کردن",
+    "app.video.swapCamDesc": "جابجا کردن موقعیت تصویر دوربین ها",
+    "app.video.videoMenu": "منوی دوربین",
+    "app.video.videoMenuDisabled": "امکان مشاهده دوربین در تنظیمات شما فعال نیست.لطفا به تنظیمات مراجعه کنید.",
+    "app.video.videoMenuDesc": "منوی تنظیمات دوربین",
+    "app.video.chromeExtensionError": "شما باید نصب کنید.",
+    "app.video.chromeExtensionErrorLink": "این افزونه کروم را",
+    "app.video.stats.title": "وضعیت اتصال",
+    "app.video.stats.packetsReceived": "بسته های دریافت شده.",
+    "app.video.stats.packetsSent": "بسته های ارسال شده",
+    "app.video.stats.packetsLost": "بسته های از دست رفته",
+    "app.video.stats.bitrate": "بیت ریت",
+    "app.video.stats.lostPercentage": "درصد از دست رفته",
+    "app.video.stats.lostRecentPercentage": "درصد از دست رفته اخیر",
+    "app.video.stats.dimensions": "ابعاد",
+    "app.video.stats.codec": "کدک",
+    "app.video.stats.decodeDelay": "تاخیر بازگشایی",
+    "app.video.stats.rtt": "RTT",
+    "app.video.stats.encodeUsagePercent": "استفاده ENCOD",
+    "app.video.stats.currentDelay": "تاخیر جاری",
+    "app.sfu.mediaServerConnectionError2000": "خطای 2000: عدم امکان ارتباط با سرور",
+    "app.sfu.mediaServerOffline2001": "خطای 2001: سرور مدیا اینترنت ندارد. لطفا بعدا چک کنید.",
+    "app.sfu.mediaServerNoResources2002": "خطای 2002: سرور مدیا منابع قابل دسترسی ندارد.",
+    "app.sfu.mediaServerRequestTimeout2003": "خطای 2003: درخواست های سرور مدیا TIMEOUT میشود",
+    "app.sfu.serverIceGatheringFailed2021": "خطای 2021: سرور مدیا ICE ندارد.",
+    "app.sfu.serverIceGatheringFailed2022": "خطای 2022: سرور مدیا اتصال ICE ندارد.",
+    "app.sfu.invalidSdp2202":"خطای 2202: کاربر SDP اشتباه ایجاد کرده است.",
+    "app.sfu.noAvailableCodec2203": "خطای 2203: سرور کدک متناسب را پیدا نمیکند",
+
+    "app.meeting.endNotification.ok.label": "باشه",
+    "app.whiteboard.toolbar.tools": "ابزارها",
+    "app.whiteboard.toolbar.tools.pointer": "نشانگر",
+    "app.whiteboard.toolbar.tools.pencil": "مداد",
+    "app.whiteboard.toolbar.tools.rectangle": "چهارگوش",
+    "app.whiteboard.toolbar.tools.triangle": "سه گوش",
+    "app.whiteboard.toolbar.tools.ellipse": "بیضی",
+    "app.whiteboard.toolbar.tools.line": "خط",
+    "app.whiteboard.toolbar.tools.text": "متن",
+    "app.whiteboard.toolbar.thickness": "ضخامت خط",
+    "app.whiteboard.toolbar.thicknessDisabled": "ضخامت خط غیر فعال است.",
+    "app.whiteboard.toolbar.color": "رنگ ها",
+    "app.whiteboard.toolbar.colorDisabled": "رنگ ها غیر فعالند",
+    "app.whiteboard.toolbar.color.black": "مشکی",
+    "app.whiteboard.toolbar.color.white": "سفید",
+    "app.whiteboard.toolbar.color.red": "قرمز",
+    "app.whiteboard.toolbar.color.orange": "نارنجی",
+    "app.whiteboard.toolbar.color.eletricLime": "زرد آبی",
+    "app.whiteboard.toolbar.color.lime": "لیمویی",
+    "app.whiteboard.toolbar.color.cyan": "آبی روشن",
+    "app.whiteboard.toolbar.color.dodgerBlue": "آبی تیره",
+    "app.whiteboard.toolbar.color.blue": "آبی",
+    "app.whiteboard.toolbar.color.violet": "بنفش",
+    "app.whiteboard.toolbar.color.magenta": "قرمز",
+    "app.whiteboard.toolbar.color.silver": "نقره ای",
+    "app.whiteboard.toolbar.undo": "دوباره سازی حاشیه نویسی",
+    "app.whiteboard.toolbar.clear": "پاک کردن همه حاشیه نویسی ها",
+    "app.whiteboard.toolbar.multiUserOn": "فعال کردن حالت چند کابره",
+    "app.whiteboard.toolbar.multiUserOff": "خاموش کردن حالت چند کاربره",
+    "app.whiteboard.toolbar.fontSize": "لیست اندازه فونت",
+    "app.feedback.title": "شما از کنفرانس بیرون رفته اید",
+    "app.feedback.subtitle": "بسیار ممنون میشویم نظر خود را در خصوص کلاس بفرمایید(نظر شما)",
+    "app.feedback.textarea": "چگونه میتوان کلاس را بهتر کرد؟",
+    "app.feedback.sendFeedback": "ارسال پیشنهاد و انتقاد",
+    "app.feedback.sendFeedbackDesc": "ارسال پیشنهاد و انتقاد و ترک جلسه",
+    "app.videoDock.webcamFocusLabel": "تمرکز",
+    "app.videoDock.webcamFocusDesc": "تمرکز در دوربین انتخاب شده",
+    "app.videoDock.webcamUnfocusLabel": "خروج از حالت تمرکز",
+    "app.videoDock.webcamUnfocusDesc": "خروج از حالت تمرکز در دوربین انتخاب شده"
+
+}
+
diff --git a/bigbluebutton-html5/private/locales/fr.json b/bigbluebutton-html5/private/locales/fr.json
index 4196a593edb919614bd90a8378f8adeac91a645f..3503d3841a9c160890ce2e32322bc50a7e5150f2 100644
--- a/bigbluebutton-html5/private/locales/fr.json
+++ b/bigbluebutton-html5/private/locales/fr.json
@@ -126,8 +126,6 @@
     "app.actionsBar.raiseLabel": "Lever la main",
     "app.actionsBar.label": "Barre d'actions",
     "app.submenu.application.applicationSectionTitle": "Application",
-    "app.submenu.application.audioNotifyLabel": "Notifications audio pour la discussion",
-    "app.submenu.application.pushNotifyLabel": "Pousser les notifications de la discussion",
     "app.submenu.application.fontSizeControlLabel": "Taille des caractères",
     "app.submenu.application.increaseFontBtnLabel": "Augmenter la Taille de la Police",
     "app.submenu.application.decreaseFontBtnLabel": "Diminuer la Taille de la Police",
@@ -200,7 +198,6 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Cesser de partager votre écran avec",
     "app.actionsBar.actionsDropdown.startRecording": "Commencer à enregistrer",
     "app.actionsBar.actionsDropdown.stopRecording": "Arrêter d'enregistrer",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "Définir un statut",
     "app.actionsBar.emojiMenu.awayLabel": "Éloigné",
     "app.actionsBar.emojiMenu.awayDesc": "Passer votre état à éloigné",
     "app.actionsBar.emojiMenu.raiseHandLabel": "Lever la main",
diff --git a/bigbluebutton-html5/private/locales/id.json b/bigbluebutton-html5/private/locales/id.json
index 427b46af95449f39fe9cf5d5ed5185c71e55070a..d2a85c265e5a1293f6b0b15f25aca6774b804448 100644
--- a/bigbluebutton-html5/private/locales/id.json
+++ b/bigbluebutton-html5/private/locales/id.json
@@ -96,8 +96,6 @@
     "app.actionsBar.camOffLabel": "Kamera Off",
     "app.actionsBar.raiseLabel": "Naikan",
     "app.submenu.application.applicationSectionTitle": "Aplikasi",
-    "app.submenu.application.audioNotifyLabel": "Notifikasi audio untuk Chat",
-    "app.submenu.application.pushNotifyLabel": "Notifikasi otomatis untuk Chat",
     "app.submenu.application.fontSizeControlLabel": "Ukuran Huruf",
     "app.submenu.application.increaseFontBtnLabel": "Besarkan Ukuran Huruf Aplikasi",
     "app.submenu.application.decreaseFontBtnLabel": "Kecilkan Ukuran Huruf Aplikasi",
diff --git a/bigbluebutton-html5/private/locales/ja.json b/bigbluebutton-html5/private/locales/ja.json
index b41d221c4782ac544d425cc37c94b3cb20e64783..43a6025d427cb8894d0846379fd0f62aee6aac6f 100644
--- a/bigbluebutton-html5/private/locales/ja.json
+++ b/bigbluebutton-html5/private/locales/ja.json
@@ -122,8 +122,6 @@
     "app.actionsBar.raiseLabel": "手をあげる",
     "app.actionsBar.label": "アクションメニュー",
     "app.submenu.application.applicationSectionTitle": "アプリケーション",
-    "app.submenu.application.audioNotifyLabel": "チャットの音声通知",
-    "app.submenu.application.pushNotifyLabel": "チャットのプッシュ通知",
     "app.submenu.application.fontSizeControlLabel": "フォントサイズ",
     "app.submenu.application.increaseFontBtnLabel": "アプリケーションのフォントサイズを大きくする",
     "app.submenu.application.decreaseFontBtnLabel": "アプリケーションのフォントサイズを小さくする",
@@ -196,7 +194,6 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "との画面共有を止める",
     "app.actionsBar.actionsDropdown.startRecording": "録画開始",
     "app.actionsBar.actionsDropdown.stopRecording": "録画停止",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "ステータスを設定する",
     "app.actionsBar.emojiMenu.awayLabel": "不在",
     "app.actionsBar.emojiMenu.awayDesc": "スタータスを「不在」にする",
     "app.actionsBar.emojiMenu.raiseHandLabel": "挙手",
diff --git a/bigbluebutton-html5/private/locales/ja_JP.json b/bigbluebutton-html5/private/locales/ja_JP.json
index b41d221c4782ac544d425cc37c94b3cb20e64783..43a6025d427cb8894d0846379fd0f62aee6aac6f 100644
--- a/bigbluebutton-html5/private/locales/ja_JP.json
+++ b/bigbluebutton-html5/private/locales/ja_JP.json
@@ -122,8 +122,6 @@
     "app.actionsBar.raiseLabel": "手をあげる",
     "app.actionsBar.label": "アクションメニュー",
     "app.submenu.application.applicationSectionTitle": "アプリケーション",
-    "app.submenu.application.audioNotifyLabel": "チャットの音声通知",
-    "app.submenu.application.pushNotifyLabel": "チャットのプッシュ通知",
     "app.submenu.application.fontSizeControlLabel": "フォントサイズ",
     "app.submenu.application.increaseFontBtnLabel": "アプリケーションのフォントサイズを大きくする",
     "app.submenu.application.decreaseFontBtnLabel": "アプリケーションのフォントサイズを小さくする",
@@ -196,7 +194,6 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "との画面共有を止める",
     "app.actionsBar.actionsDropdown.startRecording": "録画開始",
     "app.actionsBar.actionsDropdown.stopRecording": "録画停止",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "ステータスを設定する",
     "app.actionsBar.emojiMenu.awayLabel": "不在",
     "app.actionsBar.emojiMenu.awayDesc": "スタータスを「不在」にする",
     "app.actionsBar.emojiMenu.raiseHandLabel": "挙手",
diff --git a/bigbluebutton-html5/private/locales/km.json b/bigbluebutton-html5/private/locales/km.json
index 1aa8193acc51ac6578402f06b5eaf3517910aa26..3239abcaa143d599a0c35c8c96404518e403fadd 100644
--- a/bigbluebutton-html5/private/locales/km.json
+++ b/bigbluebutton-html5/private/locales/km.json
@@ -86,8 +86,6 @@
     "app.actionsBar.raiseLabel": "លើកដៃ",
     "app.actionsBar.label": "របារ​សកម្មភាព",
     "app.submenu.application.applicationSectionTitle": "កម្មវិធី",
-    "app.submenu.application.audioNotifyLabel": "ការ​ជូន​ដំណឹង​ជា​សម្លេង​សម្រាប់​ការ​ជជែក",
-    "app.submenu.application.pushNotifyLabel": "ការ​ជូន​ដំណឹង​ដែល​លេច​ឡើង​សម្រាប់​ការ​ជជែក",
     "app.submenu.application.fontSizeControlLabel": "ទំហំ​អក្សរ​",
     "app.submenu.application.increaseFontBtnLabel": "បង្កើន​ទំហំ​អក្សរ​ក្នុង​កម្មវិធី",
     "app.submenu.application.decreaseFontBtnLabel": "បន្ថយ​ទំហំ​អក្សរ​ក្នុង​កម្មវិធី",
diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json
index 9cb1608b09f81e9217cf5f91c85f29aeb6a0850a..ae8ae20035844c8ea05a64214e30b355c116aa8b 100644
--- a/bigbluebutton-html5/private/locales/pt_BR.json
+++ b/bigbluebutton-html5/private/locales/pt_BR.json
@@ -59,7 +59,7 @@
     "app.presentation.presentationToolbar.zoomDesc": "Alterar o nível de zoom da apresentação",
     "app.presentation.presentationToolbar.goToSlide": "Slide {0}",
     "app.presentationUploder.title": "Apresentação",
-    "app.presentationUploder.message": "Como apresentador no BigBlueButton, você tem a capacidade de carregar qualquer documento do Office ou arquivo PDF. Para melhores resultados, recomendamos que se carregue arquivos em PDF.",
+    "app.presentationUploder.message": "Como apresentador, você tem a capacidade de carregar qualquer documento do Office ou arquivo PDF. Para melhores resultados, recomendamos que se carregue arquivos em PDF.",
     "app.presentationUploder.confirmLabel": "Iniciar",
     "app.presentationUploder.confirmDesc": "Salve as alterações e inicie a apresentação",
     "app.presentationUploder.dismissLabel": "Cancelar",
@@ -97,6 +97,8 @@
     "app.navBar.settingsDropdown.exitFullscreenDesc": "Sair do modo de tela cheia",
     "app.navBar.settingsDropdown.hotkeysLabel": "Atalhos de teclado",
     "app.navBar.settingsDropdown.hotkeysDesc": "Lista de atalhos disponíveis",
+    "app.navBar.settingsDropdown.helpLabel": "Ajuda",
+    "app.navBar.settingsDropdown.helpDesc": "Vídeo tutoriais",
     "app.navBar.userListToggleBtnLabel": "Alternar lista de usuários",
     "app.navBar.toggleUserList.ariaLabel": "Alternar Usuários e Mensagens",
     "app.navBar.toggleUserList.newMessages": "com notificação para novas mensagens",
@@ -126,8 +128,6 @@
     "app.actionsBar.raiseLabel": "Levantar a mão",
     "app.actionsBar.label": "Barra de ações",
     "app.submenu.application.applicationSectionTitle": "Aplicação",
-    "app.submenu.application.audioNotifyLabel": "Notificações de áudio para o bate-papo",
-    "app.submenu.application.pushNotifyLabel": "Notificações para o bate-papo",
     "app.submenu.application.fontSizeControlLabel": "Tamanho da fonte",
     "app.submenu.application.increaseFontBtnLabel": "Aumentar o tamanho da fonte da aplicação",
     "app.submenu.application.decreaseFontBtnLabel": "Diminuir o tamanho da fonte da aplicação",
@@ -262,7 +262,7 @@
     "app.audioModal.echoTestTitle": "Este é um teste privado de áudio. Fale algumas palavras. Você consegue ouvir sua voz?",
     "app.audioModal.settingsTitle": "Alterar as configurações de áudio",
     "app.audioModal.helpTitle": "Houve um problema com seus dispositivos de mídia",
-    "app.audioModal.helpText": "Você autorizou o BigBlueButton a acessar seu microfone? Observe que uma caixa de diálogo deve abrir assim que você tentar participar da conferência de áudio, pedindo as permissões de acesso aos dispositivos de mídia, por favor conceda essa permissão de acesso para participar da conferência. Se isso não funcionar, tente alterar a permissão do microfone nas configurações do seu navegador.",
+    "app.audioModal.helpText": "Você autorizou o acesso ao seu microfone? Observe que uma caixa de diálogo deve abrir assim que você tentar participar da conferência de áudio, pedindo as permissões de acesso aos dispositivos de mídia, por favor conceda essa permissão de acesso para participar da conferência. Se isso não funcionar, tente alterar a permissão do microfone nas configurações do seu navegador.",
     "app.audioModal.connecting": "Conectando",
     "app.audioModal.connectingEchoTest": "Conectando ao teste de áudio",
     "app.audioManager.joinedAudio": "Você se juntou à conferência de áudio",
@@ -346,6 +346,14 @@
     "app.video.stats.rtt": "RTT",
     "app.video.stats.encodeUsagePercent": "Uso de codificação",
     "app.video.stats.currentDelay": "Atraso atual",
+    "app.sfu.mediaServerConnectionError2000": "Erro 2000: Não foi possível conectar ao servidor de mídia",
+    "app.sfu.mediaServerOffline2001": "Erro 2001: Servidor de mídia está offline. Por favor, tente novamente mais tarde.",
+    "app.sfu.mediaServerNoResources2002": "Erro 2002: Servidor de mídia não possui recursos disponíveis",
+    "app.sfu.mediaServerRequestTimeout2003": "Erro 2003: Tempo limite para solicitações ao servidor de mídia",
+    "app.sfu.serverIceGatheringFailed2021": "Erro 2021: O servidor de mídia não pode capturar candidatos ICE",
+    "app.sfu.serverIceGatheringFailed2022": "Erro 2022: Falha na conexão ICE do servidor de mídia",
+    "app.sfu.invalidSdp2202":"Erro 2202: Cliente gerou um SDP inválido",
+    "app.sfu.noAvailableCodec2203": "Erro 2203: O servidor não encontrou um codec apropriado",
 
     "app.meeting.endNotification.ok.label": "OK",
     "app.whiteboard.toolbar.tools": "Ferramentas",
diff --git a/bigbluebutton-html5/private/locales/ru_RU.json b/bigbluebutton-html5/private/locales/ru_RU.json
index ea7ce626272e42482d6518e0e6c472126a6c14ac..eb533f56578f899ec3bf12dd4c8e8abc170264e4 100644
--- a/bigbluebutton-html5/private/locales/ru_RU.json
+++ b/bigbluebutton-html5/private/locales/ru_RU.json
@@ -37,6 +37,8 @@
     "app.userList.menu.muteUserAudio.label": "Выключить микрофон пользователя",
     "app.userList.menu.unmuteUserAudio.label": "Включить микрофон пользователя",
     "app.userList.userAriaLabel": "{0} {1} {2}  Статус {3}",
+    "app.userList.menu.promoteUser.label": "Повысить до модератора",
+    "app.userList.menu.demoteUser.label": "Понизить до зрителя",
     "app.media.label": "Медиа",
     "app.media.screenshare.start": "Демонстрация экрана началась",
     "app.media.screenshare.end": "Демонстрация экрана закончилась",
@@ -95,6 +97,8 @@
     "app.navBar.settingsDropdown.exitFullscreenDesc": "Выйти из полноэкранного режима",
     "app.navBar.settingsDropdown.hotkeysLabel": "Быстрые клавиши",
     "app.navBar.settingsDropdown.hotkeysDesc": "Список доступных быстрых клавиш",
+    "app.navBar.settingsDropdown.helpLabel": "Помощь",
+    "app.navBar.settingsDropdown.helpDesc": "Ссылки на видеоуроки",
     "app.navBar.userListToggleBtnLabel": "Включить/выключить список пользователей",
     "app.navBar.toggleUserList.ariaLabel": "Включить/выключить Пользователей и Сообщения",
     "app.navBar.toggleUserList.newMessages": "с уведомлением о новых сообщениях",
@@ -124,8 +128,8 @@
     "app.actionsBar.raiseLabel": "Поднять",
     "app.actionsBar.label": "Панель действий",
     "app.submenu.application.applicationSectionTitle": "Приложение",
-    "app.submenu.application.audioNotifyLabel": "Звуковые уведомления чата",
-    "app.submenu.application.pushNotifyLabel": "Push-уведомления для чата",
+    "app.submenu.application.audioAlertLabel": "Звуковые оповещения для чата",
+    "app.submenu.application.pushAlertLabel": "Всплывающие оповещения для чата",
     "app.submenu.application.fontSizeControlLabel": "Размер шрифта",
     "app.submenu.application.increaseFontBtnLabel": "Увеличить шрифт приложения",
     "app.submenu.application.decreaseFontBtnLabel": "Уменьшить шрифт приложения",
@@ -234,12 +238,14 @@
     "app.audioNotification.audioFailedMessage": "Не удалось установить аудио-соединение",
     "app.audioNotification.mediaFailedMessage": "Ошибка getUserMicMedia, разрешены только безопасные источники",
     "app.audioNotification.closeLabel": "Закрыть",
+    "app.audioNotificaion.reconnectingAsListenOnly": "Микрофон доступен только модератору, вы будете присоединены в режиме прослушивания",
     "app.breakoutJoinConfirmation.title": "Присоединиться к комнате групповой работы",
     "app.breakoutJoinConfirmation.message": "Вы хотите присоединиться к ",
     "app.breakoutJoinConfirmation.confirmLabel": "Присоединиться",
     "app.breakoutJoinConfirmation.confirmDesc": "Присоединяет вас к конференции",
     "app.breakoutJoinConfirmation.dismissLabel": "Отмена",
     "app.breakoutJoinConfirmation.dismissDesc": "Закрывает и отклоняет присоединение к конференции",
+    "app.breakoutJoinConfirmation.freeJoinMessage": "Выберите конференцию, к которой хотите подключиться",
     "app.breakoutTimeRemainingMessage": "Оставшееся время конференции: {0}",
     "app.breakoutWillCloseMessage": "Время вышло. Комната групповой работы скоро закроется.",
     "app.calculatingBreakoutTimeRemaining": "Подсчёт оставшегося времени...",
diff --git a/bigbluebutton-html5/private/locales/tr_TR.json b/bigbluebutton-html5/private/locales/tr_TR.json
index 22b7b63ab73d68512b044fa655e5e83f3b728b57..42bd9a8ba97ecd2812193316ca898cdf7386d0fc 100644
--- a/bigbluebutton-html5/private/locales/tr_TR.json
+++ b/bigbluebutton-html5/private/locales/tr_TR.json
@@ -121,8 +121,6 @@
     "app.actionsBar.raiseLabel": "El Kaldır",
     "app.actionsBar.label": "Eylem Çubuğu",
     "app.submenu.application.applicationSectionTitle": "Uygulama",
-    "app.submenu.application.audioNotifyLabel": "Sohbet sesli bildirimleri",
-    "app.submenu.application.pushNotifyLabel": "Sohbet anlık bildirimleri",
     "app.submenu.application.fontSizeControlLabel": "Yazı büyüklüğü",
     "app.submenu.application.increaseFontBtnLabel": "Uygulama Yazı Büyüklüğünü Artır",
     "app.submenu.application.decreaseFontBtnLabel": "Uygulama Yazı Büyüklüğünü Azalt",
@@ -195,7 +193,6 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Ekran genişliği paylaşımını sonlandır",
     "app.actionsBar.actionsDropdown.startRecording": "Kaydı başlat",
     "app.actionsBar.actionsDropdown.stopRecording": "Kaydı durdur",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "Durum Belirt",
     "app.actionsBar.emojiMenu.awayLabel": "Dışarıda",
     "app.actionsBar.emojiMenu.awayDesc": "Durumunu dışarıda yap",
     "app.actionsBar.emojiMenu.raiseHandLabel": "El Kaldır",
diff --git a/bigbluebutton-html5/private/locales/uk_UA.json b/bigbluebutton-html5/private/locales/uk_UA.json
index 51cafc940f77d74d0ab57a5ae42c898fb3badd14..c41d86330e8b62ef12bdde4c3aeac71a1bc444f5 100644
--- a/bigbluebutton-html5/private/locales/uk_UA.json
+++ b/bigbluebutton-html5/private/locales/uk_UA.json
@@ -124,8 +124,6 @@
     "app.actionsBar.raiseLabel": "Підняти",
     "app.actionsBar.label": "Панель дій",
     "app.submenu.application.applicationSectionTitle": "Застосунок",
-    "app.submenu.application.audioNotifyLabel": "Аудіо сповіщення чату",
-    "app.submenu.application.pushNotifyLabel": "Push-сповіщення для чату",
     "app.submenu.application.fontSizeControlLabel": "Розмір шрифту",
     "app.submenu.application.increaseFontBtnLabel": "Збільшити шрифт застосунку",
     "app.submenu.application.decreaseFontBtnLabel": "Зменшити шрифт застосунку",
@@ -198,7 +196,6 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Зупинити демонстрацію екрану",
     "app.actionsBar.actionsDropdown.startRecording": "Почати запис",
     "app.actionsBar.actionsDropdown.stopRecording": "Зупинити запис",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "Встановити статус",
     "app.actionsBar.emojiMenu.awayLabel": "Відійшов",
     "app.actionsBar.emojiMenu.awayDesc": "Змінює ваш статус на \"Відійшов\"",
     "app.actionsBar.emojiMenu.raiseHandLabel": "Підняти руку",
diff --git a/bigbluebutton-html5/private/locales/zh_CN.json b/bigbluebutton-html5/private/locales/zh_CN.json
index fe09c30e61f26995a3deb3e43786653e15a8f46d..339bc296fa9f15ff7da06c6c17d2d4d9c8950194 100644
--- a/bigbluebutton-html5/private/locales/zh_CN.json
+++ b/bigbluebutton-html5/private/locales/zh_CN.json
@@ -124,8 +124,6 @@
     "app.actionsBar.raiseLabel": "举手",
     "app.actionsBar.label": "操作栏",
     "app.submenu.application.applicationSectionTitle": "应用",
-    "app.submenu.application.audioNotifyLabel": "聊天声音提醒",
-    "app.submenu.application.pushNotifyLabel": "聊天推送提醒",
     "app.submenu.application.fontSizeControlLabel": "字号",
     "app.submenu.application.increaseFontBtnLabel": "增大界面字号",
     "app.submenu.application.decreaseFontBtnLabel": "减小界面字号",
@@ -198,7 +196,6 @@
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "停止分享您的桌面演示给",
     "app.actionsBar.actionsDropdown.startRecording": "开始录制",
     "app.actionsBar.actionsDropdown.stopRecording": "停止录制",
-    "app.actionsBar.emojiMenu.statusTriggerLabel": "设置状态",
     "app.actionsBar.emojiMenu.awayLabel": "离开",
     "app.actionsBar.emojiMenu.awayDesc": "更改您的状态为离开",
     "app.actionsBar.emojiMenu.raiseHandLabel": "举手",
diff --git a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot
index 002765657c7b16bb5cbb69a1dc02c1236cd98ede..35752e9e8f20fe49a49d45810f179d1df2ecc403 100644
Binary files a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot and b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.eot differ
diff --git a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg
index 7352885f9dacb11fd95c9d23c0eaa29ba16cb5bb..6d628ddf266de26a6bf1b8ee0d7b18143b8ae92c 100644
--- a/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg
+++ b/bigbluebutton-html5/public/fonts/BbbIcons/bbb-icons.svg
@@ -170,8 +170,8 @@ d="M870 385c31 0 59 -24 59 -61v-450c0 -34 -28 -59 -62 -59h-713c-31 0 -62 25 -62
 d="M251 645h722c22 0 34 -18 34 -36s-12 -36 -34 -36h-722c-22 0 -34 18 -34 36s12 36 34 36zM251 135h231c22 0 34 -18 34 -36s-12 -36 -34 -36h-231c-22 0 -34 18 -34 36s12 36 34 36zM251 385h484c22 0 34 -18 34 -36s-12 -36 -34 -36h-484c-22 0 -34 18 -34 36
 s12 36 34 36zM12 609c0 40 30 72 68 72s69 -32 69 -72s-31 -72 -69 -72s-68 32 -68 72zM15 349c0 38 29 69 65 69s65 -31 65 -69s-29 -68 -65 -68s-65 30 -65 68zM12 97c0 40 30 72 68 72s69 -32 69 -72s-31 -72 -69 -72s-68 32 -68 72z" />
     <glyph glyph-name="desktop" unicode="&#xe928;" 
-d="M925 751c48 0 85 -38 85 -82v-598c0 -44 -38 -81 -82 -81h-269v-72h89c14 0 27 -13 27 -27s-10 -28 -27 -28h-468c-17 0 -27 14 -27 28s10 27 27 27h89v72h-270c-44 0 -82 37 -82 81v598c0 44 38 82 82 82h826zM601 -79v72h-177v-72h177zM952 75v594c0 14 -10 24 -24 24
-h-829c-14 0 -24 -10 -24 -24v-594c0 -14 10 -24 24 -24h826c14 0 27 10 27 24z" />
+d="M855 674c36 -1 66 -31 67 -67v-491c0 -37 -30 -67 -67 -68h-223v-62h73c13 0 23 -11 23 -24s-10 -23 -23 -23h-386c-13 0 -23 10 -23 23s10 24 23 24h75v60h-223c-38 0 -69 30 -69 68v493c1 37 32 67 69 67h684zM585 -12v60h-146v-60h146zM876 607c0 11 -9 19 -19 19
+h-686c-11 0 -19 -8 -19 -19v-493c0 -10 8 -19 19 -19h684c11 0 19 9 19 19z" />
     <glyph glyph-name="fit_to_screen" unicode="&#xe929;" 
 d="M594 -34c5 5 11 8 18 8s14 -3 19 -8c8 -8 13 -18 13 -27c0 -6 -2 -12 -6 -18l-102 -102c-5 -7 -12 -11 -20 -11s-17 4 -24 11l-106 109c-5 5 -7 12 -7 18c0 10 5 20 14 26c6 4 12 6 18 6c9 0 18 -4 26 -12l48 -48v246c0 17 13 27 30 27s31 -10 31 -27v-246zM631 645
 c-5 -5 -12 -7 -19 -7s-13 2 -18 7l-48 48v-246c0 -17 -14 -27 -31 -27s-30 10 -30 27v246l-48 -48c-8 -8 -17 -12 -27 -12c-6 0 -12 1 -17 5c-9 6 -14 17 -14 27c0 6 2 12 7 17l106 110c7 7 15 10 22 10c8 0 16 -4 22 -10l102 -103c4 -6 6 -11 6 -17c0 -9 -5 -19 -13 -27z
@@ -332,6 +332,10 @@ c0 -3 0 -7 -1 -10l-46 -269l242 126c8 3 18 5 28 5s20 -2 28 -6z" />
     <glyph glyph-name="star_filled" unicode="&#xe952;" 
 d="M243 -167c-33 1 -60 28 -60 61c0 2 1 7 1 10l46 266l-196 192c-10 9 -18 29 -18 42c0 29 24 56 52 60l269 39l121 244c9 19 33 34 54 34s45 -15 54 -34l120 -243l270 -40c28 -4 52 -31 52 -60c0 -13 -8 -33 -18 -42l-196 -192l46 -268c0 -3 1 -7 1 -10
 c0 -17 -11 -39 -25 -49c-9 -6 -24 -11 -35 -11c-8 0 -21 3 -28 7l-241 127l-241 -127c-7 -3 -19 -6 -27 -6h-1z" />
+    <glyph glyph-name="desktop_off" unicode="&#xe953;" 
+d="M928 707c4 -4 7 -12 7 -18s-4 -14 -8 -18l-29 -28c13 -12 23 -34 24 -51v-494c-1 -35 -31 -65 -67 -66h-223v-59h76c13 0 24 -11 24 -24s-11 -24 -24 -24h-392c-13 0 -24 11 -24 24s11 24 24 24h78v60h-128l-131 -126c-4 -4 -12 -8 -18 -8s-14 4 -18 8s-8 12 -8 18
+s4 14 8 18l92 89h-20c-37 0 -68 30 -69 66v494c2 36 32 66 69 66h669l52 49c4 4 12 8 18 8s14 -4 18 -8zM171 79h68l552 532h-620c-11 0 -19 -9 -19 -19v-494c0 -10 8 -19 19 -19zM586 -28l-1 60h-146v-60h147zM874 592c0 7 -5 14 -11 17l-551 -530h543c11 0 19 9 19 19v494
+z" />
     <glyph glyph-name="minus" unicode="&#xe954;" 
 d="M934 352c24 0 45 -20 45 -45s-21 -45 -45 -45h-844c-24 0 -45 21 -45 45c0 25 21 45 45 45h844z" />
   </font>
diff --git a/bigbluebutton-web/.dockerignore b/bigbluebutton-web/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a
--- /dev/null
+++ b/bigbluebutton-web/.dockerignore
@@ -0,0 +1 @@
+Dockerfile
diff --git a/bigbluebutton-web/Dockerfile b/bigbluebutton-web/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..a980f59723ee51b198144ccc7e16ca46f36322f3
--- /dev/null
+++ b/bigbluebutton-web/Dockerfile
@@ -0,0 +1,64 @@
+FROM bbb-common-web AS builder
+
+RUN mkdir -p /root/tools \
+ && cd /root/tools \
+ && wget http://services.gradle.org/distributions/gradle-2.12-bin.zip \
+ && unzip gradle-2.12-bin.zip \
+ && ln -s gradle-2.12 gradle
+
+RUN mkdir -p /root/tools \
+ && cd /root/tools \
+ && wget https://github.com/grails/grails-core/releases/download/v2.5.2/grails-2.5.2.zip \
+ && unzip grails-2.5.2.zip \
+ && ln -s grails-2.5.2 grails
+
+ENV PATH="/root/tools/gradle/bin:/root/tools/grails/bin:${PATH}"
+
+ARG COMMON_VERSION=0.0.1-SNAPSHOT
+
+COPY . /source
+
+RUN cd /source \
+ && find -name build.gradle -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-message[^:]*\):.*|\1:$COMMON_VERSION'|g" {} \; \
+ && find -name build.gradle -exec sed -i "s|\(.*org.bigbluebutton.*bbb-common-web[^:]*\):.*|\1:$COMMON_VERSION'|g" {} \;
+
+RUN cd /source \
+ && gradle resolveDeps \
+ && grails war
+
+FROM tomcat:7-jre8
+
+WORKDIR $CATALINA_HOME
+
+ENV DOCKERIZE_VERSION v0.6.1
+RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+  && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+  && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
+
+ENV DEBIAN_FRONTEND noninteractive
+RUN apt-get update \
+ && apt-get -y install imagemagick xpdf-utils netcat libreoffice ttf-liberation fonts-crosextra-carlito fonts-crosextra-caladea unzip procps \
+ && wget http://ftp.us.debian.org/debian/pool/main/libj/libjpeg8/libjpeg8_8d-1+deb7u1_amd64.deb \
+ && dpkg -i libjpeg8*.deb \
+ && rm libjpeg8*.deb
+
+RUN echo "deb http://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main " | tee /etc/apt/sources.list.d/bigbluebutton.list \
+ && wget http://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - \
+ && apt-get update \
+ && apt-get -y install bbb-swftools
+
+# clean default webapps
+RUN rm -r webapps/*
+
+COPY --from=builder /source/target/bigbluebutton-0.9.0.war webapps/bigbluebutton.war
+
+RUN unzip -q webapps/bigbluebutton.war -d webapps/bigbluebutton \
+ && rm webapps/bigbluebutton.war
+
+COPY turn-stun-servers.xml.tmpl turn-stun-servers.xml.tmpl
+
+COPY docker-entrypoint.sh /usr/local/bin/
+
+CMD [ "dockerize", \
+  "-template", "turn-stun-servers.xml.tmpl:webapps/bigbluebutton/WEB-INF/spring/turn-stun-servers.xml", \
+  "docker-entrypoint.sh" ]
diff --git a/bigbluebutton-web/docker-entrypoint.sh b/bigbluebutton-web/docker-entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..460ef6c5197aac1d08440c500359895ac7325d55
--- /dev/null
+++ b/bigbluebutton-web/docker-entrypoint.sh
@@ -0,0 +1,17 @@
+#!/bin/bash -xe
+
+mkdir -p /var/bigbluebutton/recording/raw
+mkdir -p /var/bigbluebutton/recording/process
+mkdir -p /var/bigbluebutton/recording/publish
+mkdir -p /var/bigbluebutton/recording/status/recorded
+mkdir -p /var/bigbluebutton/recording/status/archived
+mkdir -p /var/bigbluebutton/recording/status/processed
+mkdir -p /var/bigbluebutton/recording/status/sanity
+mkdir -p /var/bigbluebutton/published
+mkdir -p /var/bigbluebutton/deleted
+mkdir -p /var/bigbluebutton/unpublished
+
+export JAVA_OPTS="${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -DsecuritySalt=${SHARED_SECRET} -Dredis.host=redis -DredisHost=redis -Dbigbluebutton.web.serverURL=https://${SERVER_DOMAIN} -DattendeesJoinViaHTML5Client=true -DmoderatorsJoinViaHTML5Client=true -DsvgImagesRequired=true"
+sed -i "s|^securerandom\.source=.*|securerandom.source=file:/dev/urandom|g" ${JAVA_HOME}/lib/security/java.security
+
+catalina.sh run
diff --git a/bigbluebutton-web/grails-app/conf/Config.groovy b/bigbluebutton-web/grails-app/conf/Config.groovy
index cd9d36a9d343d06031c92165dfa34f6729eeaa8a..230e3f8feb790666eb17745f327a5065cce133a6 100755
--- a/bigbluebutton-web/grails-app/conf/Config.groovy
+++ b/bigbluebutton-web/grails-app/conf/Config.groovy
@@ -130,5 +130,7 @@ log4j = {
            'org.hibernate',
            'net.sf.ehcache.hibernate'
 
-    debug  'org.bigbluebutton'
+    debug  'org.bigbluebutton',
+           'grails.app.controllers',
+           'grails.app.services'
 }
diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
index 2e423db06c9b1c02ac0766fd9943c7e8cd9573f4..7dfef5366601b2aafe8f9287a93e08bc14867719 100755
--- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
+++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
@@ -425,28 +425,10 @@ class ApiController {
       }
     } else {
       Config conf = meeting.getDefaultConfig();
-      if (conf == null) {
-        // BEGIN - backward compatibility
-        invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE);
-        return
-        // END - backward compatibility
-
-        errors.noConfigFound();
-        respondWithErrors(errors);
-      } else {
-        configxml = conf.config;
-      }
+      configxml = conf.config;
     }
 
-    if (StringUtils.isEmpty(configxml)) {
-      // BEGIN - backward compatibility
-      invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE);
-      return
-      // END - backward compatibility
-
-      errors.noConfigFound();
-      respondWithErrors(errors);
-    }
+    // Do not fail if there's no default config.xml, needed for an HTML5 client only scenario
 
     String guestStatusVal = meeting.calcGuestStatus(role, guest, authenticated)
 
@@ -1239,6 +1221,16 @@ class ApiController {
 
         String defConfigXML = paramsProcessorUtil.getDefaultConfigXML();
 
+        if (StringUtils.isEmpty(defConfigXML)) {
+          // BEGIN - backward compatibility
+          invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE);
+          return
+          // END - backward compatibility
+
+          errors.noConfigFound();
+          respondWithErrors(errors);
+        }
+
         response.addHeader("Cache-Control", "no-cache")
         render text: defConfigXML, contentType: 'text/xml'
     }
@@ -1280,6 +1272,16 @@ class ApiController {
         }
       }
     } else {
+      if (StringUtils.isEmpty(us.configXML)) {
+        // BEGIN - backward compatibility
+        invalid("noConfigFound","We could not find a config for this request.", REDIRECT_RESPONSE);
+        return
+        // END - backward compatibility
+
+        errors.noConfigFound();
+        respondWithErrors(errors);
+      }
+
       Map<String, Object> logData = new HashMap<String, Object>();
       logData.put("meetingId", us.meetingID);
       logData.put("externalMeetingId", us.externMeetingID);
diff --git a/bigbluebutton-web/turn-stun-servers.xml.tmpl b/bigbluebutton-web/turn-stun-servers.xml.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..534aff29501418b0e92c77d58c4ba5616a9cd93f
--- /dev/null
+++ b/bigbluebutton-web/turn-stun-servers.xml.tmpl
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+
+Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+
+This program is free software; you can redistribute it and/or modify it under the
+terms of the GNU Lesser General Public License as published by the Free Software
+Foundation; either version 3.0 of the License, or (at your option) any later
+version.
+
+BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans
+            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
+            ">
+
+    <bean id="stun1" class="org.bigbluebutton.web.services.turn.StunServer">
+        <constructor-arg index="0" value="stun:{{ .Env.TURN_DOMAIN }}:3478"/>
+    </bean>
+
+    <bean id="turn1" class="org.bigbluebutton.web.services.turn.TurnServer">
+        <constructor-arg index="0" value="{{ .Env.TURN_SECRET }}"/>
+        <constructor-arg index="1" value="turn:{{ .Env.TURN_DOMAIN }}:3478"/>
+        <constructor-arg index="2" value="86400"/>
+    </bean>
+
+    <bean id="turn2" class="org.bigbluebutton.web.services.turn.TurnServer">
+        <constructor-arg index="0" value="{{ .Env.TURN_SECRET }}"/>
+        <constructor-arg index="1" value="turn:{{ .Env.TURN_DOMAIN }}:3478?transport=tcp"/>
+        <constructor-arg index="2" value="86400"/>
+    </bean>
+
+    <bean id="stunTurnService" class="org.bigbluebutton.web.services.turn.StunTurnService">
+        <property name="stunServers">
+            <set>
+                <ref bean="stun1" />
+            </set>
+        </property>
+        <property name="turnServers">
+            <set>
+                <ref bean="turn1" />
+                <ref bean="turn2" />
+            </set>
+        </property>
+        <property name="remoteIceCandidates">
+            <set/>
+        </property>
+    </bean>
+</beans>
diff --git a/labs/bbb-webrtc-sfu/Dockerfile b/labs/bbb-webrtc-sfu/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..ebba290ee6887c5df9ec7f5424a8fe6deb40fb77
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/Dockerfile
@@ -0,0 +1,17 @@
+FROM node:8
+
+ADD . /source
+RUN cp /source/config/default.example.yml /source/config/production.yml
+
+ENV NODE_ENV production
+
+RUN cd /source \
+ && mv docker-entrypoint.sh /usr/local/bin/ \
+ && npm install \
+ && npm cache clear --force
+
+WORKDIR /source
+
+EXPOSE 3008
+
+CMD [ "docker-entrypoint.sh" ]
diff --git a/labs/bbb-webrtc-sfu/config/custom-environment-variables.yml b/labs/bbb-webrtc-sfu/config/custom-environment-variables.yml
new file mode 100644
index 0000000000000000000000000000000000000000..074de09626d59cbb357795d1c39eb81d35f51044
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/config/custom-environment-variables.yml
@@ -0,0 +1,7 @@
+kurentoUrl: KURENTO_URL
+kurentoIp: KURENTO_IP
+redisHost: REDIS_HOST
+freeswitch:
+    ip: FREESWITCH_IP
+log:
+    level: LOG_LEVEL
diff --git a/labs/bbb-webrtc-sfu/config/default.example.yml b/labs/bbb-webrtc-sfu/config/default.example.yml
index 565d12ea97d7aca479ddb84eeb91d9125cba45f6..7c63f131cbd443fbc9421c78f950b63283848264 100644
--- a/labs/bbb-webrtc-sfu/config/default.example.yml
+++ b/labs/bbb-webrtc-sfu/config/default.example.yml
@@ -18,9 +18,14 @@ to-akka: "to-akka-apps-redis-channel"
 from-akka: "from-akka-apps-redis-channel"
 common-message-version: "2.x"
 webcam-force-h264: true
+webcam-preferred-h264-profile: "42e01f"
+# Target bitrate (kbps) for webcams. Value 0 leaves it unconstrained.
+webcam-target-bitrate: 300
 screenshare-force-h264: true
 screenshare-preferred-h264-profile: "42e01f"
 screenshareKeyframeInterval: 2
+# Target bitrate (kbps) for screenshare. Value 0 leaves it unconstrained.
+screenshare-target-bitrate: 0
 
 recordScreenSharing: true
 recordWebcams: false
@@ -32,7 +37,7 @@ recordingFormat: 'mkv'
 redisExpireTime: 1209600 # 14 days as per the akka keys
 
 freeswitch:
-    ip: ''
+    ip: 'FREESWITCH_IP'
     port: '5066'
 
 log:
diff --git a/labs/bbb-webrtc-sfu/docker-entrypoint.sh b/labs/bbb-webrtc-sfu/docker-entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d424e511a15e8814d6dbb9fad53b62c07729b2d6
--- /dev/null
+++ b/labs/bbb-webrtc-sfu/docker-entrypoint.sh
@@ -0,0 +1,11 @@
+#!/bin/bash -e
+
+CONTAINER_IP=$(hostname -I | awk '{print $1}')
+
+sed -i "s|^\(localIpAddress\):.*|\1: \"$CONTAINER_IP\"|g" config/production.yml
+
+if [ ! -z "$KURENTO_NAME" ]; then
+    export KURENTO_IP=$(getent hosts $KURENTO_NAME | awk '{ print $1 }')
+fi
+
+npm start
diff --git a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js
index c72815e3f33f5d72863306488c21d4b3ec44de17..8997af06d0b77288cdf28bcbcc369eb8a8b90680 100755
--- a/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js
+++ b/labs/bbb-webrtc-sfu/lib/audio/AudioManager.js
@@ -48,7 +48,7 @@ module.exports = class AudioManager extends BaseManager {
     }
   }
 
-  _onMessage(message) {
+  async _onMessage(message) {
     Logger.debug(this._logPrefix, 'Received message [' + message.id + '] from connection', message.connectionId);
     let session;
 
@@ -62,24 +62,27 @@ module.exports = class AudioManager extends BaseManager {
 
     switch (message.id) {
       case 'start':
+        try {
+          if (!session) {
+            session = new Audio(this._bbbGW, connectionId, voiceBridge);
+          }
 
-        if (!session) {
-          session = new Audio(this._bbbGW, connectionId, voiceBridge);
-        }
+          this._meetings[message.internalMeetingId] = sessionId;
+          this._sessions[sessionId] = session;
 
-        this._meetings[message.internalMeetingId] = sessionId;
-        this._sessions[sessionId] = session;
+          const { sdpOffer, caleeName, userId, userName } = message;
 
-        // starts audio session by sending sessionID, websocket and sdpoffer
-        session.start(sessionId, connectionId, message.sdpOffer, message.caleeName, message.userId, message.userName, (error, sdpAnswer) => {
+          // starts audio session by sending sessionID, websocket and sdpoffer
+          const sdpAnswer = await session.start(sessionId, connectionId, sdpOffer, caleeName, userId, userName);
           Logger.info(this._logPrefix, "Started presenter ", sessionId, " for connection", connectionId);
-          Logger.debug(this._logPrefix, "SDP answer was", sdpAnswer);
-          if (error) {
-            const errorMessage = this._handleError(this._logPrefix, connectionId, callerName, C.RECV_ROLE, error);
-            return this._bbbGW.publish(JSON.stringify({
-              ...errorMessage
+
+          session.once(C.MEDIA_SERVER_OFFLINE, async (event) => {
+            const errorMessage = this._handleError(this._logPrefix, connectionId, caleeName, C.RECV_ROLE, errors.MEDIA_SERVER_OFFLINE);
+            errorMessage.id = 'webRTCAudioError';
+            this._bbbGW.publish(JSON.stringify({
+              ...errorMessage,
             }), C.FROM_AUDIO);
-          }
+          });
 
           this._bbbGW.publish(JSON.stringify({
             connectionId: connectionId,
@@ -90,7 +93,13 @@ module.exports = class AudioManager extends BaseManager {
           }), C.FROM_AUDIO);
 
           Logger.info(this._logPrefix, "Sending startResponse to user", sessionId, "for connection", session._id);
-        });
+        } catch (err) {
+          const errorMessage = this._handleError(this._logPrefix, connectionId, message.caleeName, C.RECV_ROLE, err);
+          errorMessage.id = 'webRTCAudioError';
+          return this._bbbGW.publish(JSON.stringify({
+            ...errorMessage
+          }), C.FROM_AUDIO);
+        }
         break;
 
       case 'stop':
diff --git a/labs/bbb-webrtc-sfu/lib/audio/audio.js b/labs/bbb-webrtc-sfu/lib/audio/audio.js
index 8e6667e65adcd386940cdf5c6961b3e421b3f8be..3f877245b6b1f468fcf84ff046c283da97d8fd64 100644
--- a/labs/bbb-webrtc-sfu/lib/audio/audio.js
+++ b/labs/bbb-webrtc-sfu/lib/audio/audio.js
@@ -155,53 +155,56 @@ module.exports = class Audio extends BaseProvider {
     }
   }
 
-  async start (sessionId, connectionId, sdpOffer, caleeName, userId, userName, callback) {
-    Logger.info(LOG_PREFIX, "Starting audio instance for", this.id);
-    let sdpAnswer;
+  start (sessionId, connectionId, sdpOffer, caleeName, userId, userName) {
+    return new Promise(async (resolve, reject) => {
+      Logger.info(LOG_PREFIX, "Starting audio instance for", this.id);
+      let sdpAnswer;
 
-    // Storing the user data to be used by the pub calls
-    let user = {userId: userId, userName: userName};
-    this.addUser(connectionId, user);
+      // Storing the user data to be used by the pub calls
+      let user = {userId: userId, userName: userName};
+      this.addUser(connectionId, user);
 
-    try {
-      if (!this.sourceAudioStarted) {
-        this.userId = await this.mcs.join(this.voiceBridge, 'SFU', {});
-        Logger.info(LOG_PREFIX, "MCS join for", this.id, "returned", this.userId);
+      try {
+        if (!this.sourceAudioStarted) {
+          this.userId = await this.mcs.join(this.voiceBridge, 'SFU', {});
+          Logger.info(LOG_PREFIX, "MCS join for", this.id, "returned", this.userId);
 
-        const ret = await this.mcs.publish(this.userId,
+          const ret = await this.mcs.publish(this.userId,
             this.voiceBridge,
             'RtpEndpoint',
             {descriptor: sdpOffer, adapter: 'Freeswitch', name: caleeName});
 
-        this.sourceAudio = ret.sessionId;
-        this.mcs.on('MediaEvent' + this.sourceAudio, this.mediaState.bind(this));
-        this.sourceAudioStarted = true;
+          this.sourceAudio = ret.sessionId;
+          this.mcs.on('MediaEvent' + this.sourceAudio, this.mediaState.bind(this));
+          this.mcs.on('ServerState' + this.sourceAudio, this.serverState.bind(this));
+          this.sourceAudioStarted = true;
 
-        Logger.info(LOG_PREFIX, "MCS publish for user", this.userId, "returned", this.sourceAudio);
-      }
+          Logger.info(LOG_PREFIX, "MCS publish for user", this.userId, "returned", this.sourceAudio);
+        }
 
-      const retSubscribe  = await this.mcs.subscribe(this.userId,
+        const retSubscribe  = await this.mcs.subscribe(this.userId,
           this.sourceAudio,
           'WebRtcEndpoint',
           {descriptor: sdpOffer, adapter: 'Kurento'});
 
-      this.audioEndpoints[connectionId] = retSubscribe.sessionId;
+        this.audioEndpoints[connectionId] = retSubscribe.sessionId;
 
-      sdpAnswer = retSubscribe.answer;
-      this.flushCandidatesQueue(connectionId);
+        sdpAnswer = retSubscribe.answer;
+        this.flushCandidatesQueue(connectionId);
 
-      this.mcs.on('MediaEvent' + retSubscribe.sessionId, (event) => {
-        this.mediaStateWebRtc(event, connectionId)
-      });
+        this.mcs.on('MediaEvent' + retSubscribe.sessionId, (event) => {
+          this.mediaStateWebRtc(event, connectionId)
+        });
 
-      Logger.info(LOG_PREFIX, "MCS subscribe for user", this.userId, "returned", retSubscribe.sessionId);
+        Logger.info(LOG_PREFIX, "MCS subscribe for user", this.userId, "returned", retSubscribe.sessionId);
 
-      return callback(null, sdpAnswer);
-    }
-    catch (err) {
-      return callback(this._handleError(LOG_PREFIX, err, "recv", userId));
-    }
-  };
+        return resolve(sdpAnswer);
+      }
+      catch (err) {
+        return reject(this._handleError(LOG_PREFIX, err, "recv", userId));
+      }
+    });
+  }
 
   async stopListener(id) {
     const listener = this.audioEndpoints[id];
@@ -255,7 +258,7 @@ module.exports = class Audio extends BaseProvider {
       return Promise.resolve();
     }
     catch (err) {
-      reject(this._handleError(LOG_PREFIX, err, "recv", this.userId));
+      throw (this._handleError(LOG_PREFIX, err, "recv", this.userId));
     }
   };
 
@@ -310,4 +313,17 @@ module.exports = class Audio extends BaseProvider {
     }), C.FROM_AUDIO);
     this.removeUser(connectionId);
   };
+
+  serverState (event) {
+    const { eventTag: { code }  } = { ...event };
+    switch (code) {
+      case C.MEDIA_SERVER_OFFLINE:
+        Logger.error(LOG_PREFIX, "Provider received MEDIA_SERVER_OFFLINE event");
+        this.emit(C.MEDIA_SERVER_OFFLINE, event);
+        break;
+
+      default:
+        Logger.warn(LOG_PREFIX, "Unknown server state", event);
+    }
+  }
 };
diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
index 0d57aa7c47b4a38ac2444f6493d9147698596cae..f55ba1153d91ced626e16620200bcb63c17a2a58 100644
--- a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
+++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
@@ -157,9 +157,4 @@ module.exports = class BigBlueButtonGW extends EventEmitter {
   setEventEmitter (emitter) {
     this.emitter = emitter;
   }
-
-  _onServerResponse(data) {
-    // Here this is the 'ws' instance
-    this.sendMessage(data);
-  }
 }
diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js
index 8b1727351088dada9d62c1efd02229a5a44e6ef6..dafc0040732956e8f9f2e280e0762a9a4db49e7a 100644
--- a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js
+++ b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js
@@ -7,25 +7,6 @@ const Logger = require('../utils/Logger');
 // ID counter
 let connectionIDCounter = 0;
 
-ws.prototype.sendMessage = function(json) {
-
-  let websocket = this;
-
-  if (this._closeCode === 1000) {
-    Logger.error("[WebsocketConnectionManager] Websocket closed, not sending");
-    this._errorCallback("[WebsocketConnectionManager] Error: not opened");
-  }
-
-  return this.send(JSON.stringify(json), function(error) {
-    if(error) {
-      Logger.error('[WebsocketConnectionManager] Websocket error "' + error + '" on message "' + json.id + '"');
-
-      websocket._errorCallback(error);
-    }
-  });
-
-};
-
 module.exports = class WebsocketConnectionManager {
   constructor (server, path) {
     this.wss = new ws.Server({
@@ -48,7 +29,7 @@ module.exports = class WebsocketConnectionManager {
     const connectionId = data? data.connectionId : null;
     const ws = this.webSockets[connectionId];
     if (ws) {
-      ws.sendMessage(data);
+      this.sendMessage(ws, data);
     }
   }
 
@@ -77,7 +58,7 @@ module.exports = class WebsocketConnectionManager {
       message = JSON.parse(data);
 
       if (message.id === 'ping') {
-        ws.sendMessage({id: 'pong'});
+        this.sendMessage(ws, {id: 'pong'});
         return;
       }
 
@@ -134,4 +115,20 @@ module.exports = class WebsocketConnectionManager {
 
     delete this.webSockets[ws.id];
   }
+
+  sendMessage (ws, json) {
+
+    if (ws._closeCode === 1000) {
+      Logger.error("[WebsocketConnectionManager] Websocket closed, not sending");
+      this._onError(ws, "[WebsocketConnectionManager] Error: not opened");
+    }
+
+    return ws.send(JSON.stringify(json), (error) => {
+      if(error) {
+        Logger.error('[WebsocketConnectionManager] Websocket error "' + error + '" on message "' + json.id + '"');
+
+        this._onError(ws, error);
+      }
+    });
+  }
 }
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js
index e456e87412fec5b869a00e8bb742632a396221cb..e5028f2a4b0bd0b2519d8a7af356f09b5c2a5fd7 100644
--- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/AudioHandler.js
@@ -1,7 +1,7 @@
 'use strict';
-
 const Logger = require('../../../../utils/Logger');
 const config = require('config');
+const KURENTO_IP = config.get('kurentoIp');
 
 var kmh = function(sdp) {
   this.endpointSdp = sdp;
@@ -77,6 +77,7 @@ kmh.prototype.AudioHandler.prototype = {
     if(this.endpointSdp === null) {
       Logger.info("[mcs-audio-handler] Processing SDP for Kurento RTP endpoint", this.rtp);
       this.endpointSdp = await this.Kurento.processOffer(this.rtp, this.remote_sdp);
+      this.endpointSdp = this.endpointSdp.replace(/(IP4\s[0-9.]*)/g, 'IP4 ' + KURENTO_IP);
     }
     this.sdp = this.endpointSdp;
     this.timeout = setTimeout(function () {
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js
index 9a8028bcbcc19b339fbec693d8f16de3453adb69..10f8e5c254f82842aaf81658583e1e5d3dc8332b 100644
--- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/adapters/freeswitch/freeswitch.js
@@ -24,6 +24,10 @@ module.exports = class Freeswitch extends EventEmitter {
       this._sessions = {};
       this._rtpConverters = {};
       this._Kurento = new Kurento(config.get('kurentoUrl'));
+      this._Kurento.on(C.ERROR.MEDIA_SERVER_OFFLINE, () => {
+        this.emit(C.ERROR.MEDIA_SERVER_OFFLINE);
+      });
+
       instance = this;
     }
 
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
index a1eac49832889e4b81062107c9d48264a8b88617..7b1345fdb7ccf5e13e95513d6a84f1c70db8b325 100644
--- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
@@ -60,13 +60,19 @@ module.exports = class SdpSession extends MediaSession {
           return reject(this._handleError(C.ERROR.MEDIA_NO_AVAILABLE_CODEC));
         }
 
+        const { targetBitrate } = this._options;
+
+        if (answer && targetBitrate && targetBitrate !== '0') {
+          this._answer.addBandwidth('video', targetBitrate);
+        }
+
         if (this._type !== 'WebRtcEndpoint') {
           this._offer.replaceServerIpv4(kurentoIp);
-          return resolve(answer);
+          return resolve(this._answer? this._answer._plainSdp : null);
         }
 
         await this._MediaServer.gatherCandidates(this._mediaElement);
-        resolve(answer);
+        resolve(this._answer._plainSdp);
       }
       catch (err) {
         return reject(this._handleError(err));
diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
index 6f35e3b7c7d64b083b752d6a62c8652da012d0b6..a19912bdba9d2002543a2f01d5e57d050c1c6169 100644
--- a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
+++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
@@ -54,6 +54,20 @@ module.exports = class SdpWrapper {
     return this._mediaCapabilities.hasAvailableAudioCodec;
   }
 
+  addBandwidth (type, bw) {
+    // Bandwidth format
+    // { type: 'TIAS or AS', limit: 2048000 }
+    for(var ml of this._jsonSdp.media) {
+      if(ml.type === type ) {
+        ml['bandwidth'] = [];
+        ml.bandwidth.push({ type: 'TIAS', limit: (bw >>> 0) * 1000 });
+        ml.bandwidth.push({ type: 'AS', limit: bw });
+      }
+    }
+
+    this._plainSdp = transform.write(this._jsonSdp);
+  }
+
   /**
    * Given a SDP, test if there is an audio description in it
    * @return {boolean}    true if there is more than one video description, else false
diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
index f2eaa8cc957533ac649516f195c1612846a281ce..aeab9fc3afc1e2860d09a8697cdbcc0f2b12952c 100644
--- a/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
+++ b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js
@@ -21,6 +21,7 @@ const kurentoIp = config.get('kurentoIp');
 const localIpAddress = config.get('localIpAddress');
 const FORCE_H264 = config.get('screenshare-force-h264');
 const PREFERRED_H264_PROFILE = config.get('screenshare-preferred-h264-profile');
+const SCREENSHARE_TARGET_BITRATE = config.get('screenshare-target-bitrate');
 const SHOULD_RECORD = config.get('recordScreenSharing');
 const KEYFRAME_INTERVAL = config.get('screenshareKeyframeInterval');
 const LOG_PREFIX = "[screenshare]";
@@ -274,7 +275,7 @@ module.exports = class Screenshare extends BaseProvider {
   _startPresenter (sdpOffer) {
     return new Promise(async (resolve, reject) => {
       try {
-        const retSource = await this.mcs.publish(this.mcsUserId, this._meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer});
+        const retSource = await this.mcs.publish(this.mcsUserId, this._meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer, targetBitrate: SCREENSHARE_TARGET_BITRATE });
 
         this._presenterEndpoint = retSource.sessionId;
         sharedScreens[this._voiceBridge] = this._presenterEndpoint;
diff --git a/labs/bbb-webrtc-sfu/lib/video/video.js b/labs/bbb-webrtc-sfu/lib/video/video.js
index 9250d86d90774f12d12590a6f0526ffe9b28171e..41e2f7ccbb34eecdaba18e1a2f2208345e24dd61 100644
--- a/labs/bbb-webrtc-sfu/lib/video/video.js
+++ b/labs/bbb-webrtc-sfu/lib/video/video.js
@@ -7,8 +7,10 @@ const C = require('../bbb/messages/Constants');
 const Logger = require('../utils/Logger');
 const Messaging = require('../bbb/messages/Messaging');
 const h264_sdp = require('../h264-sdp');
+const PREFERRED_H264_PROFILE = config.get('webcam-preferred-h264-profile');
 const BaseProvider = require('../base/BaseProvider');
 const FORCE_H264 = config.get('webcam-force-h264');
+const WEBCAM_TARGET_BITRATE = config.get('webcam-target-bitrate');
 const SHOULD_RECORD = config.get('recordWebcams');
 const LOG_PREFIX = "[video]";
 
@@ -230,7 +232,7 @@ module.exports = class Video extends BaseProvider {
 
       // Force H264
       if (FORCE_H264) {
-        sdpOffer = h264_sdp.transform(sdpOffer);
+        sdpOffer = h264_sdp.transform(sdpOffer, PREFERRED_H264_PROFILE);
       }
 
       // Start the recording process
@@ -259,7 +261,7 @@ module.exports = class Video extends BaseProvider {
     return new Promise(async (resolve, reject) => {
       try {
         if (this.shared) {
-          let { answer, sessionId } = await this.mcs.publish(this.userId, this.meetingId, type, { descriptor });
+          let { answer, sessionId } = await this.mcs.publish(this.userId, this.meetingId, type, { descriptor, targetBitrate: WEBCAM_TARGET_BITRATE });
           this.mediaId = sessionId;
           sharedWebcams[this.id] = this.mediaId;
           return resolve(answer);
diff --git a/labs/bbb-webrtc-sfu/package-lock.json b/labs/bbb-webrtc-sfu/package-lock.json
index 590ed3dd4556f3d7a6513cc917e19214583c3ddc..667d138dcc4c024982bf66cb8ff9ac482ea1586c 100644
--- a/labs/bbb-webrtc-sfu/package-lock.json
+++ b/labs/bbb-webrtc-sfu/package-lock.json
@@ -142,32 +142,45 @@
         "inherits": "2.0.3",
         "kurento-client-core": "git+https://github.com/mconf/kurento-client-core-js.git#3bfcff9cb21430a4f451239100b4c306b9705757",
         "kurento-client-elements": "git+https://github.com/mconf/kurento-client-elements-js.git#8db04d5a31c299c9c6bacdfbc8fb358ad1c80fbb",
-        "kurento-client-filters": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0",
+        "kurento-client-filters": "github:Kurento/kurento-client-filters-js#67d43d4fca03c6002015448973c3e6d82e14cb3c",
         "kurento-jsonrpc": "github:Kurento/kurento-jsonrpc-js#1cdb36884ca8f096d21c335f28129a5449214e7b",
         "minimist": "1.2.0",
         "promise": "7.1.1",
         "promisecallback": "0.0.4",
         "reconnect-ws": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a"
-      }
-    },
-    "kurento-client-core": {
-      "version": "git+https://github.com/mconf/kurento-client-core-js.git#3bfcff9cb21430a4f451239100b4c306b9705757"
-    },
-    "kurento-client-elements": {
-      "version": "git+https://github.com/mconf/kurento-client-elements-js.git#8db04d5a31c299c9c6bacdfbc8fb358ad1c80fbb"
-    },
-    "kurento-client-filters": {
-      "version": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0"
-    },
-    "kurento-jsonrpc": {
-      "version": "github:Kurento/kurento-jsonrpc-js#1cdb36884ca8f096d21c335f28129a5449214e7b",
-      "requires": {
-        "bufferutil": "1.2.1",
-        "inherits": "2.0.3",
-        "utf-8-validate": "1.2.2",
-        "ws": "1.1.5"
       },
       "dependencies": {
+        "kurento-client-core": {
+          "version": "git+https://github.com/mconf/kurento-client-core-js.git#3bfcff9cb21430a4f451239100b4c306b9705757"
+        },
+        "kurento-client-elements": {
+          "version": "git+https://github.com/mconf/kurento-client-elements-js.git#8db04d5a31c299c9c6bacdfbc8fb358ad1c80fbb"
+        },
+        "kurento-client-filters": {
+          "version": "github:Kurento/kurento-client-filters-js#67d43d4fca03c6002015448973c3e6d82e14cb3c"
+        },
+        "kurento-jsonrpc": {
+          "version": "github:Kurento/kurento-jsonrpc-js#1cdb36884ca8f096d21c335f28129a5449214e7b",
+          "requires": {
+            "bufferutil": "1.2.1",
+            "inherits": "2.0.3",
+            "utf-8-validate": "1.2.2",
+            "ws": "1.1.5"
+          }
+        },
+        "reconnect-core": {
+          "version": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734",
+          "requires": {
+            "backoff": "2.3.0"
+          }
+        },
+        "reconnect-ws": {
+          "version": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a",
+          "requires": {
+            "reconnect-core": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734",
+            "websocket-stream": "0.5.1"
+          }
+        },
         "ws": {
           "version": "1.1.5",
           "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz",
@@ -256,19 +269,6 @@
         "nanoid": "1.0.1"
       }
     },
-    "reconnect-core": {
-      "version": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734",
-      "requires": {
-        "backoff": "2.3.0"
-      }
-    },
-    "reconnect-ws": {
-      "version": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a",
-      "requires": {
-        "reconnect-core": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734",
-        "websocket-stream": "0.5.1"
-      }
-    },
     "redis": {
       "version": "2.8.0",
       "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
diff --git a/labs/docker/Makefile b/labs/docker/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..63d7fb33b4acdddd21284585f94c651199b5ecf5
--- /dev/null
+++ b/labs/docker/Makefile
@@ -0,0 +1,57 @@
+SHELL=/bin/bash
+
+# in order to build images for bigbluebutton/bigbluebutton-docker, run:
+# IMAGE_ACCOUNT=bigbluebutton IMAGE_REPO=bigbluebutton-docker make release
+
+# build options
+BUILD_REVISION=`git rev-parse --short HEAD`
+BUILD_DIR_BASE=`git rev-parse --git-dir`/..
+BUILD_VERSION?=
+BUILD_IMAGE=0
+IMAGE_ACCOUNT?=
+IMAGE_REPO?=
+IMAGE_TAG=latest
+TAG_REVISION?=0
+
+all: release
+
+image:
+	-cd $(DIR) && docker build -t $(IMAGE_NAME):$(IMAGE_TAG) $(BUILD_ARGS) .
+	if [ "$(BUILD_IMAGE)" == "0" ]; then \
+		if [ "$(IMAGE_ACCOUNT)" != "" ]; then \
+			if [ "$(IMAGE_REPO)" != "" ]; then \
+				docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_ACCOUNT)/$(IMAGE_REPO):$(IMAGE_NAME); \
+				if [ "$(TAG_REVISION)" == "1" ]; then \
+					docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_ACCOUNT)/$(IMAGE_REPO):$(IMAGE_NAME)-$(BUILD_REVISION); \
+				fi \
+			fi \
+		else \
+			if [ "$(IMAGE_REPO)" != "" ]; then \
+				docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO):$(IMAGE_NAME); \
+				if [ "$(TAG_REVISION)" == "1" ]; then \
+					docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_REPO):$(IMAGE_NAME)-$(BUILD_REVISION); \
+				fi \
+			else \
+				if [ "$(TAG_REVISION)" == "1" ]; then \
+					docker tag $(IMAGE_NAME):$(IMAGE_TAG) $(IMAGE_NAME):$(BUILD_REVISION); \
+				fi \
+			fi \
+		fi \
+	fi
+
+release:
+	make image DIR=$(BUILD_DIR_BASE)/labs/docker/sbt IMAGE_NAME=sbt IMAGE_TAG=0.13.8 BUILD_IMAGE=1
+	make image DIR=$(BUILD_DIR_BASE)/bbb-common-message IMAGE_NAME=bbb-common-message BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" BUILD_IMAGE=1
+	make image DIR=$(BUILD_DIR_BASE)/bbb-common-web IMAGE_NAME=bbb-common-web BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" BUILD_IMAGE=1
+	make image DIR=$(BUILD_DIR_BASE)/bbb-fsesl-client IMAGE_NAME=bbb-fsesl-client BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT" BUILD_IMAGE=1
+	make image DIR=$(BUILD_DIR_BASE)/akka-bbb-apps IMAGE_NAME=bbb-apps-akka BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT"
+	make image DIR=$(BUILD_DIR_BASE)/akka-bbb-fsesl IMAGE_NAME=bbb-fsesl-akka BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT"
+	make image DIR=$(BUILD_DIR_BASE)/bigbluebutton-web IMAGE_NAME=bbb-web BUILD_ARGS="--build-arg COMMON_VERSION=0.0.1-SNAPSHOT"
+	make image DIR=$(BUILD_DIR_BASE)/bigbluebutton-html5 IMAGE_NAME=bbb-html5
+	make image DIR=$(BUILD_DIR_BASE)/labs/bbb-webrtc-sfu IMAGE_NAME=bbb-webrtc-sfu
+	make image DIR=$(BUILD_DIR_BASE)/bbb-webhooks IMAGE_NAME=bbb-webhooks
+	make image DIR=$(BUILD_DIR_BASE)/labs/docker/kurento IMAGE_NAME=bbb-kurento
+	make image DIR=$(BUILD_DIR_BASE)/labs/docker/freeswitch IMAGE_NAME=bbb-freeswitch
+	make image DIR=$(BUILD_DIR_BASE)/labs/docker/nginx IMAGE_NAME=bbb-nginx
+	make image DIR=$(BUILD_DIR_BASE)/labs/docker/coturn IMAGE_NAME=bbb-coturn
+	make image DIR=$(BUILD_DIR_BASE)/bbb-lti IMAGE_NAME=bbb-lti
diff --git a/labs/docker/README.md b/labs/docker/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..654d2201a6633e6810e84b566893f62e4d4db29b
--- /dev/null
+++ b/labs/docker/README.md
@@ -0,0 +1,274 @@
+IMPORTANT: this is a work in progress!
+
+# Purpose
+
+The purpose of this repo is to get BigBlueButton working in a multi-container Docker configuration over a single port, then to deploy and scale it using Kubernetes
+
+# Launching BBB via Docker
+
+## Prerequisites
+
+#### Ensure you have the latest version of Docker-CE by following the install steps
+
+Ubuntu: https://docs.docker.com/install/linux/docker-ce/ubuntu/
+
+Fedora: https://docs.docker.com/install/linux/docker-ce/fedora/
+
+#### Make sure to also do the post install steps
+
+https://docs.docker.com/install/linux/linux-postinstall/
+
+#### Install docker-compose
+
+Ubuntu: 
+```
+sudo apt-get install docker-compose
+```
+
+Fedora:
+```
+sudo dnf install docker-compose
+```
+
+## Build all docker images
+
+#### Build all docker images with one command
+```
+cd labs/docker/
+make release
+```
+
+#### Verify that you have all the necessary images
+```
+docker images
+```
+
+You should see:
+* sbt
+* bbb-common-message
+* bbb-common-web
+* bbb-fsesl-client
+* bbb-apps-akka
+* bbb-fsesl-akka
+* bbb-web
+* bbb-html5
+* bbb-webrtc-sfu
+* bbb-webhooks
+* bbb-kurento
+* bbb-freeswitch
+* bbb-nginx
+* bbb-coturn
+* bbb-lti
+
+
+In the event that any of the above images are missing, you'll need to build them individually
+
+## Build images individually
+
+sbt is needed to build the Scala components
+```
+cd labs/docker/sbt/
+docker build -t 'sbt:0.13.8' .
+```
+
+Build libraries
+```
+cd bbb-common-message/
+docker build -t 'bbb-common-message' --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+
+cd bbb-common-web/
+docker build -t 'bbb-common-web' --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+
+cd bbb-fsesl-client/
+docker build -t 'bbb-fsesl-client' --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+```
+
+Build akka components
+```
+cd akka-bbb-apps/
+docker build -t bbb-apps-akka --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+
+# Not needed since we're setting up HTML5 only
+cd akka-bbb-transcode/
+docker build -t bbb-transcode --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+
+cd akka-bbb-fsesl/
+docker build -t bbb-fsesl-akka --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+```
+
+Build bbb-web
+```
+cd bigbluebutton-web/
+docker build -t bbb-web --build-arg COMMON_VERSION=0.0.1-SNAPSHOT .
+```
+
+Build bbb-html5
+```
+cd bigbluebutton-html5/
+docker build -t bbb-html5 .
+```
+
+Build bbb-webrtc-sfu
+```
+cd labs/bbb-webrtc-sfu/
+docker build -t bbb-webrtc-sfu .
+```
+
+Build bbb-webhooks
+```
+cd bbb-webhooks/
+docker build -t bbb-webhooks .
+```
+
+Build Kurento Media Server
+```
+cd labs/docker/kurento/
+docker build -t bbb-kurento .
+```
+
+Build FreeSWITCH
+```
+cd labs/docker/freeswitch/
+docker build -t bbb-freeswitch .
+```
+
+Build nginx
+```
+cd labs/docker/nginx/
+docker build -t bbb-nginx .
+```
+
+Build coturn
+```
+cd labs/docker/coturn
+docker build -t bbb-coturn .
+```
+
+(Optional) Build bbb-lti
+```
+cd bbb-lti/
+docker build -t bbb-lti .
+```
+
+## Setup
+
+#### Export your configuration as environment variables
+NOTE: replace the example SERVER_DOMAIN's value with your own FQDN
+```
+export SERVER_DOMAIN=docker.bigbluebutton.org
+export EXTERNAL_IP=$(dig +short $SERVER_DOMAIN | grep '^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$' | head -n 1)
+export SHARED_SECRET=`openssl rand -hex 16`
+export COTURN_REST_SECRET=`openssl rand -hex 16`
+export SECRET_KEY_BASE=`openssl rand -hex 64`
+export SCREENSHARE_EXTENSION_KEY=akgoaoikmbmhcopjgakkcepdgdgkjfbc
+export SCREENSHARE_EXTENSION_LINK=https://chrome.google.com/webstore/detail/bigbluebutton-screenshare/akgoaoikmbmhcopjgakkcepdgdgkjfbc
+export TAG_PREFIX=
+export TAG_SUFFIX=
+```
+
+#### Create a volume for the SSL certs
+```
+docker volume create docker_ssl-conf	
+```	
+
+#### Generate SSL certs	
+```	
+docker run --rm -p 80:80 -v docker_ssl-conf:/etc/letsencrypt -it certbot/certbot certonly --non-interactive --register-unsafely-without-email --agree-tos --expand --domain $SERVER_DOMAIN --standalone	
+
+# certificate path: docker_ssl-conf/live/$SERVER_DOMAIN/fullchain.pem	
+# key path: docker_ssl-conf/live/$SERVER_DOMAIN/privkey.pem	
+```
+NOTE: If running on AWS, you won't be able to use the default Public DNS for your SERVER_DOMAIN as Let's Encrypt doesn't allow generating SSL certs from any *.amazonaws.com domain. Alternatively, you can create a PTR record that goes from a non-AWS FQDN to the AWS FQDN.
+
+#### Create a volume for the static files (optional)
+```
+docker volume create docker_static
+cd bigbluebutton-config/web/
+docker run -d --rm --name nginx -v docker_static:/var/www/bigbluebutton-default nginx tail -f /dev/null
+docker cp . nginx:/var/www/bigbluebutton-default
+docker exec -it nginx chown -R www-data:www-data /var/www/bigbluebutton-default
+docker stop nginx
+```
+
+#### Ensure the following ports are open
+* TCP/UDP 3478
+* TCP 80
+* TCP 443
+
+## Run
+
+#### Launch everything with docker compose
+```
+cd labs/docker/
+docker-compose up
+```
+
+#### Access your server via greenlight and create meetings
+
+https://<your_fqdn_here>/b
+
+#### To shut down and exit gracefully
+```
+CTRL+C
+```
+
+
+# Setting up a Kubernetes Cluster
+
+## Prerequisites
+
+#### Install kubeadm, kubelet, and kubectl
+
+https://kubernetes.io/docs/setup/independent/install-kubeadm/
+
+#### Disable swap by commenting out the "swap" line in /etc/fstab, then do a reboot
+```
+sudo vi /etc/fstab
+sudo systemctl reboot
+```
+
+#### Verify swap is disabled
+```
+sudo free -h
+```
+
+#### Install Minikube
+
+https://kubernetes.io/docs/tasks/tools/install-minikube/
+
+#### Install VirtualBox Manager
+
+Ubuntu:
+```
+sudo apt-get install virtualbox
+```
+
+Fedora:
+```
+sudo dnf install virtualbox
+```
+
+## Setup
+
+#### The following kernel modules are required to avoid preflight errors and warnings during cluster setup
+* ip_vs
+* ip_vs_rr
+* ip_vs_wrr
+* ip_vs_sh
+
+#### Check if kernel modules are already loaded
+```
+lsmod | grep ip_vs
+```
+
+#### Add the kernel modules (if not already loaded)
+```
+sudo modprobe ip_vs
+sudo modprobe ip_vs_rr
+sudo modprobe ip_vs_wrr
+sudo modprobe ip_vs_sh
+```
+
+#### Create a single master cluster with kubeadm
+
+https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/
diff --git a/labs/docker/coturn/Dockerfile b/labs/docker/coturn/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..aa4912ae026922b53caff6ecbef5e988f5606b1b
--- /dev/null
+++ b/labs/docker/coturn/Dockerfile
@@ -0,0 +1,14 @@
+FROM ubuntu:16.04
+
+RUN apt-get update && apt-get install -y coturn wget
+
+ENV DOCKERIZE_VERSION v0.6.1
+RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+  && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+  && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
+
+COPY ./turnserver.conf.tmpl /etc/turnserver.conf.tmpl
+
+CMD [ "dockerize", \
+  "-template", "/etc/turnserver.conf.tmpl:/etc/turnserver.conf", \
+  "turnserver", "--syslog" ]
diff --git a/labs/docker/coturn/turnserver.conf.tmpl b/labs/docker/coturn/turnserver.conf.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..c9810fb8229d2ab440e91ffea1f56cfd73dc0fee
--- /dev/null
+++ b/labs/docker/coturn/turnserver.conf.tmpl
@@ -0,0 +1,14 @@
+listening-port={{ .Env.PORT }}
+min-port=49152
+max-port=65535
+fingerprint
+lt-cred-mech
+realm={{ .Env.SERVER_DOMAIN }}
+external-ip={{ .Env.EXTERNAL_IP }}
+
+{{ if isTrue .Env.ENABLE_REST_API }}
+use-auth-secret
+static-auth-secret={{ .Env.SECRET }}
+{{ else }}
+user={{ .Env.LT_USER }}:{{ .Env.LT_SECRET }}
+{{ end }}
diff --git a/labs/docker/docker-compose.yml b/labs/docker/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..08d4ecb6644d5a3561dfcd465481a320e3cea6ab
--- /dev/null
+++ b/labs/docker/docker-compose.yml
@@ -0,0 +1,183 @@
+version: '2'
+
+services:
+  mongo:
+    image: mongo:3.4
+    restart: unless-stopped
+
+  redis:
+    image: redis
+    restart: unless-stopped
+
+  bbb-html5:
+    image: ${TAG_PREFIX}bbb-html5${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - mongo
+      - redis
+    environment:
+      MONGO_URL: mongodb://mongo/bbbhtml5
+      METEOR_SETTINGS_MODIFIER: ".public.kurento.wsUrl = \"wss://${SERVER_DOMAIN}/bbb-webrtc-sfu\" | .public.kurento.enableVideo = true | .public.kurento.enableScreensharing = true | .public.kurento.chromeDefaultExtensionKey = \"${SCREENSHARE_EXTENSION_KEY}\" | .public.kurento.chromeDefaultExtensionLink = \"${SCREENSHARE_EXTENSION_LINK}\" | .public.kurento.enableVideoStats = true | .public.kurento.enableListenOnly = true"
+      REDIS_HOST: redis
+      ROOT_URL: http://127.0.0.1/html5client
+    labels:
+      - "traefik.backend=bbb-html5"
+      - "traefik.frontend.rule=PathPrefix: /html5client,/_timesync"
+
+  bbb-webhooks:
+    image: ${TAG_PREFIX}bbb-webhooks${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - redis
+    environment:
+      REDIS_HOST: redis
+      SHARED_SECRET: ${SHARED_SECRET}
+      BEARER_AUTH: 1
+      SERVER_DOMAIN: ${SERVER_DOMAIN}
+    labels:
+      - "traefik.backend=bbb-webhooks"
+      - "traefik.frontend.rule=PathPrefix: /bigbluebutton/api/hooks"
+
+  bbb-freeswitch:
+    image: ${TAG_PREFIX}bbb-freeswitch${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - coturn
+    volumes:
+      - media-audio:/var/freeswitch/meetings
+
+  bbb-webrtc-sfu:
+    image: ${TAG_PREFIX}bbb-webrtc-sfu${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - redis
+      - kurento
+      - bbb-freeswitch
+    environment:
+      KURENTO_NAME: kurento
+      KURENTO_URL: ws://kurento:8888/kurento
+      REDIS_HOST: redis
+      FREESWITCH_IP: bbb-freeswitch
+      LOG_LEVEL: debug
+    labels:
+      - "traefik.backend=bbb-webrtc-sfu"
+      - "traefik.frontend.rule=PathPrefix: /bbb-webrtc-sfu"
+
+  coturn:
+    image: ${TAG_PREFIX}bbb-coturn${TAG_SUFFIX}
+    restart: unless-stopped
+    environment:
+      TURN_DOMAIN: ${SERVER_DOMAIN}
+      SECRET: ${COTURN_REST_SECRET}
+      EXTERNAL_IP: ${EXTERNAL_IP}
+      ENABLE_REST_API: 1
+      PORT: 3478
+    ports:
+      - 3478:3478/udp
+      - 3478:3478/tcp
+
+  kurento:
+    image: ${TAG_PREFIX}bbb-kurento${TAG_SUFFIX}
+    restart: unless-stopped
+    volumes:
+      - media-video:/var/kurento/recordings
+      - media-screenshare:/var/kurento/screenshare
+    environment:
+      KMS_STUN_IP: ${EXTERNAL_IP}
+      KMS_STUN_PORT: 3478
+
+  bbb-apps-akka:
+    image: ${TAG_PREFIX}bbb-apps-akka${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - redis
+    environment:
+      JAVA_OPTS: -Dredis.host=redis
+
+  bbb-fsesl-akka:
+    image: ${TAG_PREFIX}bbb-fsesl-akka${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - bbb-freeswitch
+      - redis
+    command: ["wait-for-it.sh", "bbb-freeswitch:8021", "--timeout=60", "--strict", "--", "/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"]
+    environment:
+      JAVA_OPTS: -Dredis.host=redis -Dfreeswitch.esl.host=bbb-freeswitch
+
+  bbb-web:
+    image: ${TAG_PREFIX}bbb-web${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - redis
+    volumes:
+      - bigbluebutton:/var/bigbluebutton
+    environment:
+      SERVER_DOMAIN: ${SERVER_DOMAIN}
+      SHARED_SECRET: ${SHARED_SECRET}
+      TURN_DOMAIN: ${SERVER_DOMAIN}
+      TURN_SECRET: ${COTURN_REST_SECRET}
+    labels:
+      - "traefik.backend=bbb-web"
+      - "traefik.frontend.rule=PathPrefix: /bigbluebutton"
+
+  bbb-greenlight:
+    image: bigbluebutton/greenlight:v2
+    restart: unless-stopped
+    volumes:
+      - greenlight_db:/usr/src/app/db/production
+      - greenlight_logs:/usr/src/app/log
+    environment:
+      BIGBLUEBUTTON_ENDPOINT: https://${SERVER_DOMAIN}/bigbluebutton/
+      BIGBLUEBUTTON_SECRET: ${SHARED_SECRET}
+      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
+      ALLOW_GREENLIGHT_ACCOUNTS: "true"
+    labels:
+      - "traefik.backend=bbb-greenlight"
+      - "traefik.frontend.rule=PathPrefix: /b"
+
+  # when we're able to setup traefik properly for wss, nginx is no longer needed
+  nginx:
+    image: ${TAG_PREFIX}bbb-nginx${TAG_SUFFIX}
+    restart: unless-stopped
+    depends_on:
+      - bbb-freeswitch
+    environment:
+      SERVER_DOMAIN: ${SERVER_DOMAIN}
+    labels:
+      - "traefik.backend=bbb-freeswitch"
+      - "traefik.frontend.rule=PathPrefix: /ws"
+
+  traefik:
+    image: traefik
+    restart: unless-stopped
+    ports:
+      - 80:80
+      - 8080:8080
+      - 443:443
+    command: traefik
+      - --docker
+      - --logLevel=INFO
+      - --acme
+      - --acme.httpchallenge
+      - --acme.httpchallenge.entrypoint=http
+      - --acme.acmelogging
+      - --acme.storage=/etc/traefik/acme/acme.json
+      - --acme.email=felipe@mconf.com
+      - --acme.entrypoint=https
+      - --acme.domains=${SERVER_DOMAIN}
+      - --defaultentrypoints=http,https
+      - --entryPoints='Name:http Address::80 Redirect.EntryPoint:https'
+      - --entryPoints='Name:https Address::443 TLS'
+    volumes:
+      - traefik-acme:/etc/traefik/acme/
+      - /var/run/docker.sock:/var/run/docker.sock
+
+volumes:
+  traefik-acme:
+  static:
+  bigbluebutton:
+  media-audio:
+  media-video:
+  media-screenshare:
+  greenlight_db:
+  greenlight_logs:
diff --git a/labs/docker/freeswitch/Dockerfile b/labs/docker/freeswitch/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..86865a8fb847ec0af679b46ce674cd386697d596
--- /dev/null
+++ b/labs/docker/freeswitch/Dockerfile
@@ -0,0 +1,24 @@
+FROM ubuntu:16.04
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update \
+ && apt-get -y install wget libedit-dev xmlstarlet
+
+RUN echo "deb http://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main " | tee /etc/apt/sources.list.d/bigbluebutton.list \
+ && wget http://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - \
+ && apt-get update \
+ && apt-get -y install bbb-freeswitch-core \
+ && find /opt/freeswitch/conf/sip_profiles/ -name "*ipv6*" -prune -exec rm -rf "{}" \;
+
+EXPOSE 7443
+
+COPY docker-entrypoint.sh /usr/local/bin/
+COPY event_socket_conf.xml /opt/freeswitch/conf/autoload_configs/event_socket.conf.xml
+
+RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64 \
+ && chmod +x /usr/local/bin/dumb-init
+
+ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
+
+CMD [ "docker-entrypoint.sh" ]
diff --git a/labs/docker/freeswitch/docker-entrypoint.sh b/labs/docker/freeswitch/docker-entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..90e82ba379894259f46797fe6e166406a4ad0bfb
--- /dev/null
+++ b/labs/docker/freeswitch/docker-entrypoint.sh
@@ -0,0 +1,14 @@
+#!/bin/bash -xe
+
+IP=$(hostname -I | cut -d' ' -f1)
+
+xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml
+xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_sip_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml
+xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "local_ip_v4=")]/@data' --value "${IP}" /opt/freeswitch/conf/vars.xml
+# add wss-binding element
+xmlstarlet edit --inplace --subnode '//settings' --type elem --name 'newsubnode' --value '' /opt/freeswitch/conf/sip_profiles/external.xml
+xmlstarlet edit --inplace --insert '//newsubnode' --type attr --name 'name' --value 'wss-binding'  /opt/freeswitch/conf/sip_profiles/external.xml
+xmlstarlet edit --inplace --insert '//newsubnode' --type attr --name 'value' --value ':7443'  /opt/freeswitch/conf/sip_profiles/external.xml
+xmlstarlet edit --inplace --rename '//newsubnode' --value 'param'  /opt/freeswitch/conf/sip_profiles/external.xml
+
+/opt/freeswitch/bin/freeswitch
diff --git a/labs/docker/freeswitch/event_socket_conf.xml b/labs/docker/freeswitch/event_socket_conf.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c10b16fb1ba4d1ecc42921913373ac049a9347b1
--- /dev/null
+++ b/labs/docker/freeswitch/event_socket_conf.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<configuration name="event_socket.conf" description="Socket Client">
+  <settings>
+    <param name="nat-map" value="false"/>
+    <param name="listen-ip" value="0.0.0.0"/>
+    <param name="listen-port" value="8021"/>
+    <param name="password" value="ClueCon"/>
+    <param name="apply-inbound-acl" value="localnet.auto"/>
+  </settings>
+</configuration>
diff --git a/labs/docker/k8s/bbb-apps-akka.yaml b/labs/docker/k8s/bbb-apps-akka.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d21e1d44abe6651aac809d3f5b4040e2bc5237e4
--- /dev/null
+++ b/labs/docker/k8s/bbb-apps-akka.yaml
@@ -0,0 +1,45 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-apps-akka
+    version: latest
+  name: bbb-apps-akka
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-apps-akka
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-apps-akka
+        version: latest
+      name: bbb-apps-akka
+    spec:
+      containers:
+      - env:
+        - name: JAVA_OPTS
+          value: -Dredis.host=redis
+        image: fcecagno/bigbluebutton:bbb-apps-akka
+        imagePullPolicy: Always
+        name: bbb-apps-akka
+        ports:
+        - containerPort: 8080
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
diff --git a/labs/docker/k8s/bbb-coturn.yaml b/labs/docker/k8s/bbb-coturn.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f68eb0cca329a7de723b63db8ce773c79ef93fea
--- /dev/null
+++ b/labs/docker/k8s/bbb-coturn.yaml
@@ -0,0 +1,71 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-coturn
+    version: latest
+  name: bbb-coturn
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-coturn
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-coturn
+        version: latest
+      name: bbb-coturn
+    spec:
+      containers:
+      - env:
+        - name: PORT
+          value: "3478"
+        - name: SERVER_DOMAIN
+          value: bigbluebutton.rocket.chat
+        - name: SECRET
+          value: "54321"
+        - name: EXTERNAL_IP
+          value: "35.185.19.180"
+        - name: ENABLE_REST_API
+          value: "1"
+        image: fcecagno/bigbluebutton:bbb-coturn
+        imagePullPolicy: Always
+        name: bbb-coturn
+        ports:
+        - containerPort: 3478
+          protocol: TCP
+        - containerPort: 3478
+          protocol: UDP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-coturn
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 3478
+    port: 3478
+  selector:
+    app: bbb-coturn
diff --git a/labs/docker/k8s/bbb-freeswitch-nginx.yaml b/labs/docker/k8s/bbb-freeswitch-nginx.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d550783745d6b122972de76f1b4ef0bf9da861f0
--- /dev/null
+++ b/labs/docker/k8s/bbb-freeswitch-nginx.yaml
@@ -0,0 +1,79 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-freeswitch-nginx
+    version: latest
+  name: bbb-freeswitch-nginx
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-freeswitch-nginx
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-freeswitch-nginx
+        version: latest
+      name: bbb-freeswitch-nginx
+    spec:
+      containers:
+      - env:
+        - name: SERVER_DOMAIN
+          value: bigbluebutton.rocket.chat
+        image: fcecagno/bigbluebutton:bbb-nginx
+        imagePullPolicy: Always
+        name: bbb-freeswitch-nginx
+        ports:
+        - containerPort: 80
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-freeswitch-nginx
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 80
+    port: 80
+  selector:
+    app: bbb-freeswitch-nginx
+
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: bbb-freeswitch-nginx
+  namespace: bigbluebutton
+  annotations:
+    kubernetes.io/ingress.class: "traefik"
+spec:
+  rules:
+  - host: bigbluebutton.rocket.chat
+    http:
+      paths:
+      - path: /ws
+        backend:
+          serviceName: bbb-freeswitch-nginx
+          servicePort: 80
diff --git a/labs/docker/k8s/bbb-freeswitch.yaml b/labs/docker/k8s/bbb-freeswitch.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6c78e391a0217f34ef3b9385374e06e9851b4282
--- /dev/null
+++ b/labs/docker/k8s/bbb-freeswitch.yaml
@@ -0,0 +1,58 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-freeswitch
+    version: latest
+  name: bbb-freeswitch
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-freeswitch
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-freeswitch
+        version: latest
+      name: bbb-freeswitch
+    spec:
+      containers:
+      - image: fcecagno/bigbluebutton:bbb-freeswitch
+        imagePullPolicy: Always
+        name: bbb-freeswitch
+        ports:
+        - containerPort: 7443
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-freeswitch
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 7443
+    port: 7443
+  selector:
+    app: bbb-freeswitch
diff --git a/labs/docker/k8s/bbb-fsesl-akka.yaml b/labs/docker/k8s/bbb-fsesl-akka.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b37ba7d94fee4c938cc732ce01e8c7cb93441712
--- /dev/null
+++ b/labs/docker/k8s/bbb-fsesl-akka.yaml
@@ -0,0 +1,43 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-fsesl-akka
+    version: latest
+  name: bbb-fsesl-akka
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-fsesl-akka
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-fsesl-akka
+        version: latest
+      name: bbb-fsesl-akka
+    spec:
+      containers:
+      - env:
+        - name: JAVA_OPTS
+          value: -Dredis.host=redis -Dfreeswitch.esl.host=bbb-freeswitch
+        image: fcecagno/bigbluebutton:bbb-fsesl-akka
+        imagePullPolicy: Always
+        name: bbb-fsesl-akka
+        command: ["wait-for-it.sh", "bbb-freeswitch:8021", "--timeout=60", "--strict", "--", "/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka"]
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
diff --git a/labs/docker/k8s/bbb-html5.yaml b/labs/docker/k8s/bbb-html5.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..cf8fdafcf3b0c3977909121d5de4c1ba2db5f141
--- /dev/null
+++ b/labs/docker/k8s/bbb-html5.yaml
@@ -0,0 +1,91 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-html5
+    version: latest
+  name: bbb-html5
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-html5
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-html5
+        version: latest
+      name: bbb-html5
+    spec:
+      containers:
+      - env:
+        - name: MONGO_URL
+          value: mongodb://mongo:27017/
+        - name: MONGO_DB
+          value: bbbhtml5
+        - name: METEOR_SETTINGS_MODIFIER
+          value: ".public.kurento.wsUrl = \"wss://bigbluebutton.rocket.chat/bbb-webrtc-sfu\" | .public.kurento.enableVideo = true | .public.kurento.enableScreensharing = true | .public.kurento.enableVideoStats = true | .public.kurento.enableListenOnly = true"
+        - name: REDIS_HOST
+          value: redis
+        - name: ROOT_URL
+          value: http://127.0.0.1/html5client
+        image: fcecagno/bigbluebutton:bbb-html5
+        imagePullPolicy: Always
+        name: bbb-html5
+        ports:
+        - containerPort: 3000
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-html5
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 3000
+    port: 3000
+  selector:
+    app: bbb-html5
+
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: bbb-html5
+  namespace: bigbluebutton
+  annotations:
+    kubernetes.io/ingress.class: "traefik"
+spec:
+  rules:
+  - host: bigbluebutton.rocket.chat
+    http:
+      paths:
+      - path: /html5client
+        backend:
+          serviceName: bbb-html5
+          servicePort: 3000
+      - path: /_timesync
+        backend:
+          serviceName: bbb-html5
+          servicePort: 3000
diff --git a/labs/docker/k8s/bbb-kurento.yaml b/labs/docker/k8s/bbb-kurento.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..3c2a0ac6e9fb64b43f7ef32be6a580ecdd5f6607
--- /dev/null
+++ b/labs/docker/k8s/bbb-kurento.yaml
@@ -0,0 +1,69 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-kurento
+    version: latest
+  name: bbb-kurento
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-kurento
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-kurento
+        version: latest
+      name: bbb-kurento
+    spec:
+      containers:
+      - env:
+        - name: KMS_STUN_IP
+        # TODO: how to get this IP?
+          value: "35.185.19.180"
+        - name: KMS_STUN_PORT
+          value: "3478"
+        image: fcecagno/bigbluebutton:bbb-kurento
+        imagePullPolicy: Always
+        name: bbb-kurento
+        ports:
+        - containerPort: 8888
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-kurento
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 8888
+    port: 8888
+  selector:
+    app: bbb-kurento
+
+
+    # volumes:
+    #   - media-video:/var/kurento/recordings
+    #   - media-screenshare:/var/kurento/screenshare
diff --git a/labs/docker/k8s/bbb-web.yaml b/labs/docker/k8s/bbb-web.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..784ae337eb475c6f17da7844160ad063166cb394
--- /dev/null
+++ b/labs/docker/k8s/bbb-web.yaml
@@ -0,0 +1,83 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-web
+    version: latest
+  name: bbb-web
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-web
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-web
+        version: latest
+      name: bbb-web
+    spec:
+      containers:
+      - env:
+        - name: SERVER_DOMAIN
+          value: bigbluebutton.rocket.chat
+        - name: SHARED_SECRET
+          value: "12345"
+        - name: TURN_SECRET
+          value: "54321"
+        image: fcecagno/bigbluebutton:bbb-web
+        imagePullPolicy: Always
+        name: bbb-web
+        ports:
+        - containerPort: 8080
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-web
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 8080
+    port: 8080
+  selector:
+    app: bbb-web
+
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: bbb-web
+  namespace: bigbluebutton
+  annotations:
+    kubernetes.io/ingress.class: "traefik"
+spec:
+  rules:
+  - host: bigbluebutton.rocket.chat
+    http:
+      paths:
+      - path: /bigbluebutton
+        backend:
+          serviceName: bbb-web
+          servicePort: 8080
diff --git a/labs/docker/k8s/bbb-webhooks.yaml b/labs/docker/k8s/bbb-webhooks.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9239ce01e43844f471ce19b3c8657df49b0d3111
--- /dev/null
+++ b/labs/docker/k8s/bbb-webhooks.yaml
@@ -0,0 +1,81 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-webhooks
+    version: latest
+  name: bbb-webhooks
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-webhooks
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-webhooks
+        version: latest
+      name: bbb-webhooks
+    spec:
+      containers:
+      - env:
+        - name: REDIS_HOST
+          value: redis
+        - name: SHARED_SECRET
+          value: "12345"
+        image: fcecagno/bigbluebutton:bbb-webhooks
+        imagePullPolicy: Always
+        name: bbb-webhooks
+        ports:
+        - containerPort: 3005
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-webhooks
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 3005
+    port: 3005
+  selector:
+    app: bbb-webhooks
+
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: bbb-webhooks
+  namespace: bigbluebutton
+  annotations:
+    kubernetes.io/ingress.class: "traefik"
+spec:
+  rules:
+  - host: bigbluebutton.rocket.chat
+    http:
+      paths:
+      - path: /bigbluebutton/api/hooks
+        backend:
+          serviceName: bbb-webhooks
+          servicePort: 3005
diff --git a/labs/docker/k8s/bbb-webrtc-sfu.yaml b/labs/docker/k8s/bbb-webrtc-sfu.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..323622f023a8e7137a6eb53e0abafb1fb39481e3
--- /dev/null
+++ b/labs/docker/k8s/bbb-webrtc-sfu.yaml
@@ -0,0 +1,87 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: bbb-webrtc-sfu
+    version: latest
+  name: bbb-webrtc-sfu
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: bbb-webrtc-sfu
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: bbb-webrtc-sfu
+        version: latest
+      name: bbb-webrtc-sfu
+    spec:
+      containers:
+      - env:
+        - name: KURENTO_NAME
+          value: kurento
+        - name: KURENTO_URL
+          value: ws://kurento:8888/kurento
+        - name: REDIS_HOST
+          value: redis
+        - name: FREESWITCH_IP
+          value: bbb-freeswitch
+        - name: LOG_LEVEL
+          value: debug
+        image: fcecagno/bigbluebutton:bbb-webrtc-sfu
+        imagePullPolicy: Always
+        name: bbb-webrtc-sfu
+        ports:
+        - containerPort: 3008
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bbb-webrtc-sfu
+  namespace: bigbluebutton
+  annotations:
+    traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
+spec:
+  ports:
+  - name: http
+    targetPort: 3008
+    port: 3008
+  selector:
+    app: bbb-webrtc-sfu
+
+---
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: bbb-webrtc-sfu
+  namespace: bigbluebutton
+  annotations:
+    kubernetes.io/ingress.class: "traefik"
+spec:
+  rules:
+  - host: bigbluebutton.rocket.chat
+    http:
+      paths:
+      - path: /bbb-webrtc-sfu
+        backend:
+          serviceName: bbb-webrtc-sfu
+          servicePort: 3008
diff --git a/labs/docker/k8s/bigbluebutton-ns.yaml b/labs/docker/k8s/bigbluebutton-ns.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d66b7579ed4aae4a45759e7f1ecb3fa3a7d4df8b
--- /dev/null
+++ b/labs/docker/k8s/bigbluebutton-ns.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: bigbluebutton
diff --git a/labs/docker/k8s/mongo.yaml b/labs/docker/k8s/mongo.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5a09cdd532b9a840e16bb612914a7cd8f2650933
--- /dev/null
+++ b/labs/docker/k8s/mongo.yaml
@@ -0,0 +1,55 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: mongo
+    version: latest
+  name: mongo
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: mongo
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: mongo
+        version: latest
+      name: mongo
+    spec:
+      containers:
+      - image: mongo:3.6
+        imagePullPolicy: Always
+        name: mongo
+        ports:
+        - containerPort: 27017
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mongo
+  namespace: bigbluebutton
+spec:
+  ports:
+  - targetPort: 27017
+    port: 27017
+  selector:
+    app: mongo
diff --git a/labs/docker/k8s/redis.yaml b/labs/docker/k8s/redis.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..fe12f4eb193aed1043b39c905d0539f7af0709c2
--- /dev/null
+++ b/labs/docker/k8s/redis.yaml
@@ -0,0 +1,55 @@
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  labels:
+    app: redis
+    version: latest
+  name: redis
+  namespace: bigbluebutton
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: redis
+      version: latest
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+    type: RollingUpdate
+  template:
+    metadata:
+      labels:
+        app: redis
+        version: latest
+      name: redis
+    spec:
+      containers:
+      - image: redis:latest
+        imagePullPolicy: Always
+        name: redis
+        ports:
+        - containerPort: 6379
+          protocol: TCP
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      nodeSelector:
+        role: 'bigbluebutton'
+      tolerations:
+      - key: role
+        operator: Equal
+        value: 'bigbluebutton'
+        effect: NoSchedule
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: redis
+  namespace: bigbluebutton
+spec:
+  ports:
+  - targetPort: 6379
+    port: 6379
+  selector:
+    app: redis
diff --git a/labs/docker/kurento/Dockerfile b/labs/docker/kurento/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..97dbf889259eeca09b76fdb364bd280d9b317c37
--- /dev/null
+++ b/labs/docker/kurento/Dockerfile
@@ -0,0 +1,28 @@
+FROM ubuntu:16.04
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update \
+ && apt-get -y dist-upgrade \
+ && apt-get install -y software-properties-common curl wget apt-transport-https
+
+RUN wget https://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - \
+ && add-apt-repository "deb https://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main" \
+ && apt-get update \
+ && apt-get -y install kurento-media-server bzip2 jq \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN apt-get update \
+ && apt-get install -y --download-only openh264-gst-plugins-bad-1.5
+
+COPY ./docker-entrypoint.sh /usr/local/bin/
+COPY ./healthchecker.sh /healthchecker.sh
+
+HEALTHCHECK --start-period=15s --interval=30s --timeout=3s --retries=1 CMD /healthchecker.sh
+
+ENV GST_DEBUG=Kurento*:5
+ENV PORT=8888
+
+ENTRYPOINT ["docker-entrypoint.sh"]
+CMD ["/usr/bin/kurento-media-server"]
diff --git a/labs/docker/kurento/docker-entrypoint.sh b/labs/docker/kurento/docker-entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..4d1d3a444ebd238994df8c48195f5027b68f6d4e
--- /dev/null
+++ b/labs/docker/kurento/docker-entrypoint.sh
@@ -0,0 +1,24 @@
+#!/bin/bash -e
+
+apt-get install -y openh264-gst-plugins-bad-1.5
+
+rm -f /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
+touch /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
+
+if [ -n "$KMS_TURN_URL" ]; then
+  echo "turnURL=$KMS_TURN_URL" >> /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
+fi
+
+if [ -n "$KMS_STUN_IP" -a -n "$KMS_STUN_PORT" ]; then
+  # Generate WebRtcEndpoint configuration
+  echo "stunServerAddress=$KMS_STUN_IP" >> /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
+  echo "stunServerPort=$KMS_STUN_PORT" >> /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
+fi
+
+KMS_CONFIG=$(cat /etc/kurento/kurento.conf.json | sed '/^[ ]*\/\//d' | jq ".mediaServer.net.websocket.port = $PORT")
+echo $KMS_CONFIG > /etc/kurento/kurento.conf.json
+
+# Remove ipv6 local loop until ipv6 is supported
+cat /etc/hosts | sed '/::1/d' | tee /etc/hosts > /dev/null
+
+exec "$@"
diff --git a/labs/docker/kurento/healthchecker.sh b/labs/docker/kurento/healthchecker.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c7f6ce82faf2c3667e2ce65030963ab02bff83c9
--- /dev/null
+++ b/labs/docker/kurento/healthchecker.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+[[ "$(curl -w '%{http_code}' -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: 127.0.0.1:8888" -H "Origin: 127.0.0.1" http://127.0.0.1:8888/kurento)" == 500 ]] && exit 0 || exit 1
diff --git a/labs/docker/nginx/Dockerfile b/labs/docker/nginx/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..56fc11c4192341c49542f0584f8c8273db0e870a
--- /dev/null
+++ b/labs/docker/nginx/Dockerfile
@@ -0,0 +1,14 @@
+FROM nginx
+
+RUN apt-get update && apt-get install -y wget
+
+ENV DOCKERIZE_VERSION v0.6.1
+RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+  && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
+  && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
+
+COPY ./nginx.conf.tmpl /etc/nginx/nginx.conf.tmpl
+
+CMD [ "dockerize", \
+  "-template", "/etc/nginx/nginx.conf.tmpl:/etc/nginx/nginx.conf", \
+  "nginx", "-g", "daemon off;" ]
diff --git a/labs/docker/nginx/nginx.conf.tmpl b/labs/docker/nginx/nginx.conf.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..0210fc399b1ed6e9d5bb3a30173982f30961a688
--- /dev/null
+++ b/labs/docker/nginx/nginx.conf.tmpl
@@ -0,0 +1,66 @@
+user www-data;
+worker_processes auto;
+pid /run/nginx.pid;
+
+events {
+    worker_connections 768;
+}
+
+http {
+    ##
+    # Basic Settings
+    ##
+
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+
+    include /etc/nginx/mime.types;
+    default_type application/octet-stream;
+
+    ##
+    # SSL Settings
+    ##
+
+    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
+    ssl_prefer_server_ciphers on;
+
+    ##
+    # Logging Settings
+    ##
+
+    access_log /var/log/nginx/access.log;
+    error_log /var/log/nginx/error.log;
+
+    ##
+    # Gzip Settings
+    ##
+
+    gzip on;
+    gzip_disable "msie6";
+
+    ##
+    # Virtual Host Configs
+    ##
+
+    server {
+        listen   80;
+        listen [::]:80;
+        server_name {{ .Env.SERVER_DOMAIN }};
+
+        access_log  /var/log/nginx/bigbluebutton.access.log;
+
+        location /ws {
+            proxy_pass https://bbb-freeswitch:7443;
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "Upgrade";
+            proxy_read_timeout 6h;
+            proxy_send_timeout 6h;
+            client_body_timeout 6h;
+            send_timeout 6h;
+        }
+    }
+}
diff --git a/labs/docker/sbt/Dockerfile b/labs/docker/sbt/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..99e285c61de4892f93bc2a40fa267d28da9e737b
--- /dev/null
+++ b/labs/docker/sbt/Dockerfile
@@ -0,0 +1,14 @@
+FROM openjdk:8
+
+ARG SBT_VERSION=0.13.8
+
+RUN curl -L -o sbt-$SBT_VERSION.deb https://dl.bintray.com/sbt/debian/sbt-$SBT_VERSION.deb \
+ && dpkg -i sbt-$SBT_VERSION.deb \
+ && rm sbt-$SBT_VERSION.deb \
+ && apt-get update \
+ && apt-get install sbt \
+ && sbt sbtVersion
+
+RUN echo 'resolvers += "Artima Maven Repository" at "http://repo.artima.com/releases"' | tee -a ~/.sbt/0.13/global.sbt
+
+WORKDIR /root
diff --git a/record-and-playback/presentation/scripts/process/presentation.rb b/record-and-playback/presentation/scripts/process/presentation.rb
index 43894728edd578a5b51f8496dd3548372dfcb5b8..d7d594eb839720174d67b132c752fec34a297419 100755
--- a/record-and-playback/presentation/scripts/process/presentation.rb
+++ b/record-and-playback/presentation/scripts/process/presentation.rb
@@ -228,13 +228,13 @@ if not FileTest.directory?(target_dir)
       end
 
       processed_audio_file = BigBlueButton::AudioProcessor.get_processed_audio_file("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio")
-      BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, webcam_width, webcam_height, presentation_props['audio_offset'], processed_audio_file)
+      BigBlueButton.process_webcam_videos(target_dir, temp_dir, meeting_id, webcam_width, webcam_height, presentation_props['audio_offset'], processed_audio_file, presentation_props['video_formats'])
     end
 
     if !Dir["#{raw_archive_dir}/deskshare/*"].empty? and presentation_props['include_deskshare']
       deskshare_width = presentation_props['deskshare_output_width']
       deskshare_height = presentation_props['deskshare_output_height']
-      BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, deskshare_width, deskshare_height)
+      BigBlueButton.process_deskshare_videos(target_dir, temp_dir, meeting_id, deskshare_width, deskshare_height, presentation_props['video_formats'])
     end
 
     process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w")