diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
index 41f0d8c104d7ebf2e7201add9518386366149c7c..63cd05048d733abbcfae9951feddd8af8ad7e626 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
@@ -8,6 +8,8 @@ import VideoService from './service';
 import VideoList from './video-list/component';
 import { fetchWebRTCMappedStunTurnServers } from '/imports/utils/fetchStunTurnServers';
 
+const VIDEO_CONSTRAINTS = Meteor.settings.public.kurento.cameraConstraints;
+
 const intlClientErrors = defineMessages({
   iceCandidateError: {
     id: 'app.video.iceCandidateError',
@@ -350,7 +352,9 @@ class VideoProvider extends Component {
 
     // in this case, 'closed' state is not caused by an error;
     // we stop listening to prevent this from being treated as an error
-    this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = null;
+    if (this.webRtcPeers[id]) {
+      this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = null;
+    }
 
     if (shareWebcam) {
       this.unshareWebcam();
@@ -390,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),
       };
@@ -803,7 +792,7 @@ class VideoProvider extends Component {
     const { cameraId } = message;
 
     this.logger('info', 'Handle play stop for camera', { cameraId });
-    this.stopWebRTCPeer(id);
+    this.stopWebRTCPeer(cameraId);
   }
 
   handlePlayStart(message) {
diff --git a/bigbluebutton-html5/private/config/settings-development.json b/bigbluebutton-html5/private/config/settings-development.json
index 20f055a90f4fc9e59adf64e1bfb5143cc48bfdfe..5352e0e72acef339e2118634d595e4cfb9b377b3 100755
--- a/bigbluebutton-html5/private/config/settings-development.json
+++ b/bigbluebutton-html5/private/config/settings-development.json
@@ -79,6 +79,14 @@
       "chromeExtensionLink": "LINK",
       "chromeScreenshareSources": ["window", "screen"],
       "firefoxScreenshareSource": "window",
+      "cameraConstraints": {
+        "width": {
+          "max": 640
+        },
+        "height": {
+          "max": 480
+        }
+      },
       "enableScreensharing": false,
       "enableVideo": false,
       "enableVideoStats": false,
@@ -344,9 +352,9 @@
       }
     },
     "clientLog": [
-      { 
-        "target": "server", 
-        "level": "info" 
+      {
+        "target": "server",
+        "level": "info"
       }
     ]
   },
@@ -379,7 +387,6 @@
       ]
     },
 
-
     "serverLog": {
       "level": "info"
     }
diff --git a/bigbluebutton-html5/private/config/settings-production.json b/bigbluebutton-html5/private/config/settings-production.json
index b493a3687f9122261fdb6c09f4aab641b077de7e..be718710186555c9f165ee6bb2b369a55da3756d 100755
--- a/bigbluebutton-html5/private/config/settings-production.json
+++ b/bigbluebutton-html5/private/config/settings-production.json
@@ -79,6 +79,14 @@
       "chromeExtensionLink": "LINK",
       "chromeScreenshareSources": ["window", "screen"],
       "firefoxScreenshareSource": "window",
+      "cameraConstraints": {
+        "width": {
+          "max": 640
+        },
+        "height": {
+          "max": 480
+        }
+      },
       "enableScreensharing": false,
       "enableVideo": false,
       "enableVideoStats": false,
@@ -344,9 +352,9 @@
       }
     },
     "clientLog": [
-      { 
-        "target": "server", 
-        "level": "info" 
+      {
+        "target": "server",
+        "level": "info"
       }
     ]
   },
diff --git a/labs/bbb-webrtc-sfu/config/default.example.yml b/labs/bbb-webrtc-sfu/config/default.example.yml
index 565d12ea97d7aca479ddb84eeb91d9125cba45f6..47a91a68650199d22fdecf4f3c6ed040f03f741a 100644
--- a/labs/bbb-webrtc-sfu/config/default.example.yml
+++ b/labs/bbb-webrtc-sfu/config/default.example.yml
@@ -18,9 +18,13 @@ to-akka: "to-akka-apps-redis-channel"
 from-akka: "from-akka-apps-redis-channel"
 common-message-version: "2.x"
 webcam-force-h264: true
+# 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
diff --git a/labs/bbb-webrtc-sfu/lib/audio/audio.js b/labs/bbb-webrtc-sfu/lib/audio/audio.js
index 8e6667e65adcd386940cdf5c6961b3e421b3f8be..385c7d8e304a0d3beee3695649d10bde8721e810 100644
--- a/labs/bbb-webrtc-sfu/lib/audio/audio.js
+++ b/labs/bbb-webrtc-sfu/lib/audio/audio.js
@@ -255,7 +255,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));
     }
   };
 
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..be068de8beda19dafd62708f32faac32df79c4c3 100644
--- a/labs/bbb-webrtc-sfu/lib/video/video.js
+++ b/labs/bbb-webrtc-sfu/lib/video/video.js
@@ -9,6 +9,7 @@ const Messaging = require('../bbb/messages/Messaging');
 const h264_sdp = require('../h264-sdp');
 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]";
 
@@ -259,7 +260,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);