diff --git a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js
index bb17ee6a320488c2b647f8fb8c6b11a2fd39e09a..765bae598df15bac36713eb10c3189a089ae9878 100755
--- a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js
+++ b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js
@@ -534,12 +534,15 @@ function make_call(username, voiceBridge, server, callback, recall, isListenOnly
 	currentSession.mediaHandler.on('iceConnectionConnected', function() {
 		console.log('Received ICE status changed to connected');
 		if (callICEConnected === false) {
-			callICEConnected = true;
-			clearTimeout(iceConnectedTimeout);
-			if (callActive === true) {
-				callback({'status':'started'});
+			// Edge is only ready once the status is 'completed' so we need to skip this step
+			if (!bowser.msedge) {
+				callICEConnected = true;
+				clearTimeout(iceConnectedTimeout);
+				if (callActive === true) {
+					callback({'status':'started'});
+				}
+				clearTimeout(callTimeout);
 			}
-			clearTimeout(callTimeout);
 		}
 	});
 	
diff --git a/bigbluebutton-client/resources/prod/lib/kurento-extension.js b/bigbluebutton-client/resources/prod/lib/kurento-extension.js
index 9f48f5710b5e8464d70f25c0635125a1015a07a6..f52b06bc249bd6b08455f74818e88f36ca07f7d6 100755
--- a/bigbluebutton-client/resources/prod/lib/kurento-extension.js
+++ b/bigbluebutton-client/resources/prod/lib/kurento-extension.js
@@ -3,7 +3,7 @@ const isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
 const isChrome = !!window.chrome && !isOpera;
 const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
 const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
-  || typeof navigator.mediaDevices.getDisplayMedia === 'function');
+  || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
 const kurentoHandler = null;
 const SEND_ROLE = "send";
 const RECV_ROLE = "recv";
diff --git a/bigbluebutton-client/resources/prod/lib/kurento-utils.js b/bigbluebutton-client/resources/prod/lib/kurento-utils.js
index 2a8bfab960387681317951b9428c19110d407a8a..22d8087010b9bef36f97f5da546941b078f42c91 100644
--- a/bigbluebutton-client/resources/prod/lib/kurento-utils.js
+++ b/bigbluebutton-client/resources/prod/lib/kurento-utils.js
@@ -424,7 +424,7 @@ function WebRtcPeer(mode, options, callback) {
                     navigator.getDisplayMedia(recursive.apply(undefined, constraints))
                         .then(gDMCallback)
                         .catch(callback);
-                } else if (typeof navigator.mediaDevices.getDisplayMedia === 'function') {
+                } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
                     navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
                         .then(gDMCallback)
                         .catch(callback);
diff --git a/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js b/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js
deleted file mode 100644
index a29923966fa58015032c35d3daa8f3e8c68d5696..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/client/compatibility/bbb_webrtc_bridge_sip.js
+++ /dev/null
@@ -1,601 +0,0 @@
-
-var userID, callerIdName=null, conferenceVoiceBridge, userAgent=null, userMicMedia, userWebcamMedia, currentSession=null, callTimeout, callActive, callICEConnected, iceConnectedTimeout, callFailCounter, callPurposefullyEnded, uaConnected, transferTimeout, iceGatheringTimeout;
-var inEchoTest = true;
-var html5StunTurn = null;
-
-function webRTCCallback(message) {
-	switch (message.status) {
-		case 'succeded':
-			BBB.webRTCCallSucceeded();
-			break;
-		case 'failed':
-			if (message.errorcode !== 1004) {
-				message.cause = null;
-			}
-			monitorTracksStop();
-			BBB.webRTCCallFailed(inEchoTest, message.errorcode, message.cause);
-			break;
-		case 'ended':
-			monitorTracksStop();
-			BBB.webRTCCallEnded(inEchoTest);
-			break;
-		case 'started':
-			monitorTracksStart();
-			BBB.webRTCCallStarted(inEchoTest);
-			break;
-		case 'connecting':
-			BBB.webRTCCallConnecting(inEchoTest);
-			break;
-		case 'waitingforice':
-			BBB.webRTCCallWaitingForICE(inEchoTest);
-			break;
-		case 'transferring':
-			BBB.webRTCCallTransferring(inEchoTest);
-			break;
-		case 'mediarequest':
-			BBB.webRTCMediaRequest();
-			break;
-		case 'mediasuccess':
-			BBB.webRTCMediaSuccess();
-			break;
-		case 'mediafail':
-			BBB.webRTCMediaFail();
-			break;
-	}
-}
-
-function callIntoConference(voiceBridge, callback, isListenOnly, stunTurn = null) {
-	// root of the call initiation process from the html5 client
-	// Flash will not pass in the listen only field. For html5 it is optional. Assume NOT listen only if no state passed
-	if (isListenOnly == null) {
-		isListenOnly = false;
-	}
-
-	// if additional stun configuration is passed, store the information
-	if (stunTurn != null) {
-		html5StunTurn = {
-			stunServers: stunTurn.stun,
-			turnServers: stunTurn.turn,
-		};
-	}
-
-	// reset callerIdName
-	callerIdName = null;
-	if (!callerIdName) {
-		BBB.getMyUserInfo(function(userInfo) {
-			console.log("User info callback [myUserID=" + userInfo.myUserID
-				+ ",myUsername=" + userInfo.myUsername + ",myAvatarURL=" + userInfo.myAvatarURL
-				+ ",myRole=" + userInfo.myRole + ",amIPresenter=" + userInfo.amIPresenter
-				+ ",dialNumber=" + userInfo.dialNumber + ",voiceBridge=" + userInfo.voiceBridge
-				+ ",isListenOnly=" + isListenOnly + "].");
-			userID = userInfo.myUserID;
-			callerIdName = userInfo.myUserID + "-bbbID-" + userInfo.myUsername;
-			if (isListenOnly) {
-				//prepend the callerIdName so it is recognized as a global audio user
-				callerIdName = "GLOBAL_AUDIO_" + callerIdName;
-			}
-			conferenceVoiceBridge = userInfo.voiceBridge
-			if (voiceBridge === "9196") {
-				voiceBridge = voiceBridge + conferenceVoiceBridge;
-			} else {
-				voiceBridge = conferenceVoiceBridge;
-			}
-			console.log(callerIdName);
-			webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
-		});
-	} else {
-		if (voiceBridge === "9196") {
-			voiceBridge = voiceBridge + conferenceVoiceBridge;
-		} else {
-			voiceBridge = conferenceVoiceBridge;
-		}
-		webrtc_call(callerIdName, voiceBridge, callback, isListenOnly);
-	}
-}
-
-function joinWebRTCVoiceConference() {
-	console.log("Joining to the voice conference directly");
-	inEchoTest = false;
-	// set proper callbacks to previously created user agent
-	if(userAgent) {
-		setUserAgentListeners(webRTCCallback);
-	}
-	callIntoConference(conferenceVoiceBridge, webRTCCallback);
-}
-
-function leaveWebRTCVoiceConference() {
-	console.log("Leaving the voice conference");
-	
-	webrtc_hangup();
-}
-
-function startWebRTCAudioTest(){
-	console.log("Joining the audio test first");
-	inEchoTest = true;
-	callIntoConference("9196", webRTCCallback);
-}
-
-function stopWebRTCAudioTest(){
-	console.log("Stopping webrtc audio test");
-	
-	webrtc_hangup();
-}
-
-function stopWebRTCAudioTestJoinConference(){
-	console.log("Transferring from audio test to conference");
-	
-	webRTCCallback({'status': 'transferring'});
-	
-	transferTimeout = setTimeout( function() {
-		console.log("Call transfer failed. No response after 3 seconds");
-		webRTCCallback({'status': 'failed', 'errorcode': 1008});
-		releaseUserMedia();
-		currentSession = null;
-		if (userAgent != null) {
-			var userAgentTemp = userAgent;
-			userAgent = null;
-			userAgentTemp.stop();
-		}
-	}, 5000);
-	
-	BBB.listen("UserJoinedVoiceEvent", userJoinedVoiceHandler);
-	
-	currentSession.dtmf(1);
-	inEchoTest = false;
-}
-
-function userJoinedVoiceHandler(event) {
-	console.log("UserJoinedVoiceHandler - " + event);
-	if (inEchoTest === false && userID === event.userID) {
-		BBB.unlisten("UserJoinedVoiceEvent", userJoinedVoiceHandler);
-		clearTimeout(transferTimeout);
-		webRTCCallback({'status': 'started'});
-	}
-}
-
-function createUA(username, server, callback, makeCallFunc) {
-	if (userAgent) {
-		console.log("User agent already created");
-		return;
-	}
-
-	console.log("Fetching STUN/TURN server info for user agent");
-
-	console.log(html5StunTurn);
-	if (html5StunTurn != null) {
-		createUAWithStuns(username, server, callback, html5StunTurn, makeCallFunc);
-		return;
-	}
-
-  BBB.getSessionToken(function(sessionToken) {
-  	$.ajax({
-  		dataType: 'json',
-  		url: '/bigbluebutton/api/stuns',
-  		data: {sessionToken:sessionToken}
-  	}).done(function(data) {
-  		var stunsConfig = {};
-  		stunsConfig['stunServers'] = ( data['stunServers'] ? data['stunServers'].map(function(data) {
-  			return data['url'];
-  		}) : [] );
-  		stunsConfig['turnServers'] = ( data['turnServers'] ? data['turnServers'].map(function(data) {
-  			return {
-  				'urls': data['url'],
-  				'username': data['username'],
-  				'password': data['password']
-  			};
-  		}) : [] );
-		//stunsConfig['remoteIceCandidates'] = ( data['remoteIceCandidates'] ? data['remoteIceCandidates'].map(function(data) {
-		//	return data['ip'];
-		//}) : [] );
-  		createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc);
-  	}).fail(function(data, textStatus, errorThrown) {
-  		BBBLog.error("Could not fetch stun/turn servers", {error: textStatus, user: callerIdName, voiceBridge: conferenceVoiceBridge});
-  		callback({'status':'failed', 'errorcode': 1009});
-  	});
-  });
-}
-
-function createUAWithStuns(username, server, callback, stunsConfig, makeCallFunc) {
-	console.log("Creating new user agent");
-
-	/* VERY IMPORTANT 
-	 *	- You must escape the username because spaces will cause the connection to fail
-	 *	- We are connecting to the websocket through an nginx redirect instead of directly to 5066
-	 */
-	var configuration = {
-		uri: 'sip:' + encodeURIComponent(username) + '@' + server,
-		wsServers: ('https:' == document.location.protocol ? 'wss://' : 'ws://')  + server + '/ws',
-		displayName: username,
-		register: false,
-		traceSip: true,
-		autostart: false,
-		userAgentString: "BigBlueButton",
-		stunServers: stunsConfig['stunServers'],
-		turnServers: stunsConfig['turnServers'],
-		//artificialRemoteIceCandidates: stunsConfig['remoteIceCandidates']
-	};
-	
-	uaConnected = false;
-	
-	userAgent = new SIP.UA(configuration);
-	setUserAgentListeners(callback, makeCallFunc);
-	userAgent.start();
-};
-
-function setUserAgentListeners(callback, makeCallFunc) {
-	console.log("resetting UA callbacks");
-	userAgent.removeAllListeners('connected');
-	userAgent.on('connected', function() {
-		uaConnected = true;
-		callback({'status':'succeded'});
-		makeCallFunc();
-	});
-	userAgent.removeAllListeners('disconnected');
-	userAgent.on('disconnected', function() {
-		if (userAgent) {
-			if (userAgent != null) {
-				var userAgentTemp = userAgent;
-				userAgent = null;
-				userAgentTemp.stop();
-			}
-			
-			if (uaConnected) {
-				callback({'status':'failed', 'errorcode': 1001}); // WebSocket disconnected
-			} else {
-				callback({'status':'failed', 'errorcode': 1002}); // Could not make a WebSocket connection
-			}
-		}
-	});
-};
-
-function getUserMicMedia(getUserMicMediaSuccess, getUserMicMediaFailure) {
-	if (userMicMedia == undefined) {
-		if (SIP.WebRTC.isSupported()) {
-			SIP.WebRTC.getUserMedia({audio:true, video:false}, getUserMicMediaSuccess, getUserMicMediaFailure);
-		} else {
-			console.log("getUserMicMedia: webrtc not supported");
-			getUserMicMediaFailure("WebRTC is not supported");
-		}
-	} else {
-		console.log("getUserMicMedia: mic already set");
-		getUserMicMediaSuccess(userMicMedia);
-	}
-};
-
-function webrtc_call(username, voiceBridge, callback, isListenOnly) {
-	if (!isWebRTCAvailable()) {
-		callback({'status': 'failed', 'errorcode': 1003}); // Browser version not supported
-		return;
-	}
-	if (isListenOnly == null) { // assume NOT listen only unless otherwise stated
-		isListenOnly = false;
-	}
-
-	var server = window.document.location.hostname;
-	console.log("user " + username + " calling to " +  voiceBridge);
-
-	var makeCallFunc = function() {
-		// only make the call when both microphone and useragent have been created
-		// for listen only, stating listen only is a viable substitute for acquiring user media control
-		if ((isListenOnly||userMicMedia) && userAgent)
-			make_call(username, voiceBridge, server, callback, false, isListenOnly);
-	};
-
-	// Reset userAgent so we can successfully switch between listenOnly and listen+speak modes
-	userAgent = null;
-	if (!userAgent) {
-		createUA(username, server, callback, makeCallFunc);
-	}
-	// if the user requests to proceed as listen only (does not require media) or media is already acquired,
-	// proceed with making the call
-	if (isListenOnly || userMicMedia != null) {
-		makeCallFunc();
-	} else {
-		callback({'status':'mediarequest'});
-		getUserMicMedia(function(stream) {
-				console.log("getUserMicMedia: success");
-				userMicMedia = stream;
-				callback({'status':'mediasuccess'});
-				makeCallFunc();
-			}, function(e) {
-				console.error("getUserMicMedia: failure - " + e);
-				callback({'status':'mediafail', 'cause': e});
-			}
-		);
-	}
-}
-
-function make_call(username, voiceBridge, server, callback, recall, isListenOnly) {
-	if (isListenOnly == null) {
-		isListenOnly = false;
-	}
-
-	if (userAgent == null) {
-		console.log("userAgent is still null. Delaying call");
-		var callDelayTimeout = setTimeout( function() {
-			make_call(username, voiceBridge, server, callback, recall, isListenOnly);
-		}, 100);
-		return;
-	}
-
-	if (!userAgent.isConnected()) {
-		console.log("Trying to make call, but UserAgent hasn't connected yet. Delaying call");
-		userAgent.once('connected', function() {
-			console.log("UserAgent has now connected, retrying the call");
-			make_call(username, voiceBridge, server, callback, recall, isListenOnly);
-		});
-		return;
-	}
-
-	if (currentSession) {
-		console.log('Active call detected ignoring second make_call');
-		return;
-	}
-
-	// Make an audio/video call:
-	console.log("Setting options.. ");
-
-	var options = {};
-	if (isListenOnly) {
-		// create necessary options for a listen only stream
-		var stream = null;
-		// handle the web browser
-		// create a stream object through the browser separated from user media
-		if (typeof webkitMediaStream !== 'undefined') {
-			// Google Chrome
-			stream = new webkitMediaStream;
-		} else {
-			// Firefox
-			audioContext = new window.AudioContext;
-			stream = audioContext.createMediaStreamDestination().stream;
-		}
-
-		options = {
-			media: {
-				stream: stream, // use the stream created above
-				constraints: {
-					audio: true,
-					video: false
-				},
-				render: {
-					remote: document.getElementById('remote-media')
-				}
-			},
-			// a list of our RTC Connection constraints
-			RTCConstraints: {
-				// our constraints are mandatory. We must received audio and must not receive audio
-				mandatory: {
-					OfferToReceiveAudio: true,
-					OfferToReceiveVideo: false
-				}
-			}
-		};
-	} else {
-		options = {
-			media: {
-				stream: userMicMedia,
-				constraints: {
-					audio: true,
-					video: false
-				},
-				render: {
-					remote: document.getElementById('remote-media')
-				}
-			}
-		};
-	}
-	
-	callTimeout = setTimeout(function() {
-		console.log('Ten seconds without updates sending timeout code');
-		callback({'status':'failed', 'errorcode': 1006}); // Failure on call
-		releaseUserMedia();
-		currentSession = null;
-		if (userAgent != null) {
-			var userAgentTemp = userAgent;
-			userAgent = null;
-			userAgentTemp.stop();
-		}
-	}, 10000);
-	
-	callActive = false;
-	callICEConnected = false;
-	callPurposefullyEnded = false;
-	callFailCounter = 0;
-	console.log("Calling to " + voiceBridge + "....");
-	currentSession = userAgent.invite('sip:' + voiceBridge + '@' + server, options); 
-	
-	// Only send the callback if it's the first try
-	if (recall === false) {
-		console.log('call connecting');
-		callback({'status':'connecting'});
-	} else {
-		console.log('call connecting again');
-	}
-	
-	/*
-	iceGatheringTimeout = setTimeout(function() {
-		console.log('Thirty seconds without ICE gathering finishing');
-		callback({'status':'failed', 'errorcode': 1011}); // ICE Gathering Failed
-		releaseUserMedia();
-		currentSession = null;
-		if (userAgent != null) {
-			var userAgentTemp = userAgent;
-			userAgent = null;
-			userAgentTemp.stop();
-		}
-	}, 30000);
-	*/
-	
-	currentSession.mediaHandler.on('iceGatheringComplete', function() {
-		clearTimeout(iceGatheringTimeout);
-	});
-	
-	// The connecting event fires before the listener can be added
-	currentSession.on('connecting', function(){
-		clearTimeout(callTimeout);
-	});
-	currentSession.on('progress', function(response){
-		console.log('call progress: ' + response);
-		clearTimeout(callTimeout);
-	});
-	currentSession.on('failed', function(response, cause){
-		console.log('call failed with cause: '+ cause);
-		
-		if (currentSession) {
-			releaseUserMedia();
-			if (callActive === false) {
-				callback({'status':'failed', 'errorcode': 1004, 'cause': cause}); // Failure on call
-				currentSession = null;
-				if (userAgent != null) {
-					var userAgentTemp = userAgent;
-					userAgent = null;
-					userAgentTemp.stop();
-				}
-			} else {
-				callActive = false;
-				//currentSession.bye();
-				currentSession = null;
-				if (userAgent != null) {
-					userAgent.stop();
-				}
-			}
-		}
-		clearTimeout(callTimeout);
-	});
-	currentSession.on('bye', function(request){
-		callActive = false;
-		
-		if (currentSession) {
-			console.log('call ended ' + currentSession.endTime);
-			releaseUserMedia();
-			if (callPurposefullyEnded === true) {
-				callback({'status':'ended'});
-			} else {
-				callback({'status':'failed', 'errorcode': 1005}); // Call ended unexpectedly
-			}
-			clearTimeout(callTimeout);
-			currentSession = null;
-		} else {
-			console.log('bye event already received');
-		}
-	});
-	currentSession.on('cancel', function(request) {
-		callActive = false;
-
-		if (currentSession) {
-			console.log('call canceled');
-			releaseUserMedia();
-			clearTimeout(callTimeout);
-			currentSession = null;
-		} else {
-			console.log('cancel event already received');
-		}
-	});
-	currentSession.on('accepted', function(data){
-		callActive = true;
-		console.log('BigBlueButton call accepted');
-		
-		if (callICEConnected === true) {
-			callback({'status':'started'});
-		} else {
-			callback({'status':'waitingforice'});
-			console.log('Waiting for ICE negotiation');
-			iceConnectedTimeout = setTimeout(function() {
-				console.log('5 seconds without ICE finishing');
-				callback({'status':'failed', 'errorcode': 1010}); // ICE negotiation timeout
-				releaseUserMedia();
-				currentSession = null;
-				if (userAgent != null) {
-					var userAgentTemp = userAgent;
-					userAgent = null;
-					userAgentTemp.stop();
-				}
-			}, 5000);
-		}
-		clearTimeout(callTimeout);
-	});
-	currentSession.mediaHandler.on('iceConnectionFailed', function() {
-		console.log('received ice negotiation failed');
-		callback({'status':'failed', 'errorcode': 1007}); // Failure on call
-		releaseUserMedia();
-		currentSession = null;
-		clearTimeout(iceConnectedTimeout);
-		if (userAgent != null) {
-			var userAgentTemp = userAgent;
-			userAgent = null;
-			userAgentTemp.stop();
-		}
-		
-		clearTimeout(callTimeout);
-	});
-	
-	// Some browsers use status of 'connected', others use 'completed', and a couple use both
-	
-	currentSession.mediaHandler.on('iceConnectionConnected', function() {
-		console.log('Received ICE status changed to connected');
-		if (callICEConnected === false) {
-			callICEConnected = true;
-			clearTimeout(iceConnectedTimeout);
-			if (callActive === true) {
-				callback({'status':'started'});
-			}
-			clearTimeout(callTimeout);
-		}
-	});
-	
-	currentSession.mediaHandler.on('iceConnectionCompleted', function() {
-		console.log('Received ICE status changed to completed');
-		if (callICEConnected === false) {
-			callICEConnected = true;
-			clearTimeout(iceConnectedTimeout);
-			if (callActive === true) {
-				callback({'status':'started'});
-			}
-			clearTimeout(callTimeout);
-		}
-	});
-}
-
-function webrtc_hangup(callback) {
-	callPurposefullyEnded = true;
-
-	console.log("Hanging up current session");
-	if (callback) {
-	  currentSession.on('bye', callback);
-	}
-	try {
-		currentSession.bye();
-	} catch (err) {
-		console.log("Forcing to cancel current session");
-		currentSession.cancel();
-	}
-}
-
-function releaseUserMedia() {
-	if (!!userMicMedia) {
-		console.log("Releasing media tracks");
-	
-		userMicMedia.getAudioTracks().forEach(function(track) {
-			track.stop();
-		});
-
-		userMicMedia.getVideoTracks().forEach(function(track) {
-			track.stop();
-		});
-		
-		userMicMedia = null;
-	}
-}
-
-function isWebRTCAvailable() {
-	if (bowser.msedge) {
-		return false;
-	} else {
-		return SIP.WebRTC.isSupported();
-	}
-}
-
-function getCallStatus() {
-	return currentSession;
-}
-
diff --git a/bigbluebutton-html5/client/compatibility/kurento-extension.js b/bigbluebutton-html5/client/compatibility/kurento-extension.js
index 0a181e56cfe3e370016420300b12f28d67ccf32d..8c167fbb790681baabf7e0bdc3eade81cd808501 100644
--- a/bigbluebutton-html5/client/compatibility/kurento-extension.js
+++ b/bigbluebutton-html5/client/compatibility/kurento-extension.js
@@ -4,7 +4,7 @@ const isChrome = !!window.chrome && !isOpera;
 const isSafari = navigator.userAgent.indexOf('Safari') >= 0 && !isChrome;
 const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1;
 const hasDisplayMedia = (typeof navigator.getDisplayMedia === 'function'
-  || typeof navigator.mediaDevices.getDisplayMedia === 'function');
+  || (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function'));
 const kurentoHandler = null;
 
 Kurento = function (
diff --git a/bigbluebutton-html5/client/compatibility/kurento-utils.js b/bigbluebutton-html5/client/compatibility/kurento-utils.js
index 7e118d2d9d17b6a9a30dedc7a6251e75c0f3c40d..bf621aff2c463c0da73f64af7dcc9a8c94b19752 100644
--- a/bigbluebutton-html5/client/compatibility/kurento-utils.js
+++ b/bigbluebutton-html5/client/compatibility/kurento-utils.js
@@ -475,7 +475,7 @@ function WebRtcPeer(mode, options, callback) {
                     navigator.getDisplayMedia(recursive.apply(undefined, constraints))
                         .then(gDMCallback)
                         .catch(callback);
-                } else if (typeof navigator.mediaDevices.getDisplayMedia === 'function') {
+                } else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
                     navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
                         .then(gDMCallback)
                         .catch(callback);
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 6c7492068c871171e9162a321043740dc672d95a..2892e505818075c975b467c9ccf299f97ad9cfd6 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker';
 import BaseAudioBridge from './base';
 import logger from '/imports/startup/client/logger';
 import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers';
-
+import browser from 'browser-detect';
 
 const MEDIA = Meteor.settings.public.media;
 const MEDIA_TAG = MEDIA.mediaTag;
@@ -279,7 +279,14 @@ export default class SIPBridge extends BaseAudioBridge {
     return new Promise((resolve) => {
       const { mediaHandler } = currentSession;
 
-      const connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
+      let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
+      // Edge sends a connected first and then a completed, but the call isn't ready until
+      // the completed comes in. Due to the way that we have the listeners set up, the only
+      // way to ignore one status is to not listen for it.
+      if (browser().name === 'edge') {
+        connectionCompletedEvents  = ['iceConnectionCompleted'];
+      }
+
       const handleConnectionCompleted = () => {
         connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
         // We have to delay notifying that the call is connected because it is sometimes not
diff --git a/bigbluebutton-html5/imports/api/external-videos/index.js b/bigbluebutton-html5/imports/api/external-videos/index.js
index 8653beaffe732de32ca2f5bd0ef48ef14d71d75c..7b2a427b4a8bd3c3ee396be4882ca8837164f338 100644
--- a/bigbluebutton-html5/imports/api/external-videos/index.js
+++ b/bigbluebutton-html5/imports/api/external-videos/index.js
@@ -2,6 +2,4 @@ import { Meteor } from 'meteor/meteor';
 
 const Streamer = new Meteor.Streamer('videos');
 
-export default Streamer
-
-
+export default Streamer;
diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js
index 4310a9fe0b0ab6c10a15f3b056b6879ed20f5e4d..e69961db74dbb8b2f0c01103fbc3e079fd481197 100644
--- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js
+++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js
@@ -17,7 +17,7 @@ export default function addUserSettings(credentials, meetingId, userId, settings
       'forceListenOnly',
       'skipCheck',
       'clientTitle',
-      'lockOnJoin', // NOT IMPLEMENTED YET
+      'lockOnJoin',
       'askForFeedbackOnLogout',
       // BRANDING
       'displayBrandingArea',
diff --git a/bigbluebutton-html5/imports/api/users/server/eventHandlers.js b/bigbluebutton-html5/imports/api/users/server/eventHandlers.js
index 5d9f55fb0e98792130cb21a2144bcd952d1ef31d..de7a3e4d787cb1a54afeab9384f9d646b2f4ce68 100644
--- a/bigbluebutton-html5/imports/api/users/server/eventHandlers.js
+++ b/bigbluebutton-html5/imports/api/users/server/eventHandlers.js
@@ -20,4 +20,3 @@ RedisPubSub.on('GuestsWaitingForApprovalEvtMsg', handleGuestsWaitingForApproval)
 RedisPubSub.on('GuestsWaitingApprovedEvtMsg', handleGuestApproved);
 RedisPubSub.on('UserEjectedFromMeetingEvtMsg', handleUserEjected);
 RedisPubSub.on('UserRoleChangedEvtMsg', handleChangeRole);
-
diff --git a/bigbluebutton-html5/imports/api/users/server/methods.js b/bigbluebutton-html5/imports/api/users/server/methods.js
index 739c8d1dbf6486c08474c5c7e638998cf97e906e..6736d3a1ba081353377d606318719386d0bd21fa 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods.js
@@ -4,6 +4,7 @@ import setEmojiStatus from './methods/setEmojiStatus';
 import assignPresenter from './methods/assignPresenter';
 import changeRole from './methods/changeRole';
 import removeUser from './methods/removeUser';
+import toggleUserLock from './methods/toggleUserLock';
 
 Meteor.methods({
   setEmojiStatus,
@@ -11,4 +12,5 @@ Meteor.methods({
   changeRole,
   removeUser,
   validateAuthToken,
+  toggleUserLock,
 });
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js
new file mode 100644
index 0000000000000000000000000000000000000000..0919194b9261f371210f5981ea3fdca69be95476
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/users/server/methods/toggleUserLock.js
@@ -0,0 +1,29 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import RedisPubSub from '/imports/startup/server/redis';
+import Logger from '/imports/startup/server/logger';
+
+export default function toggleUserLock(credentials, userId, lock) {
+  const REDIS_CONFIG = Meteor.settings.private.redis;
+  const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'LockUserInMeetingCmdMsg';
+
+  const { meetingId, requesterUserId: lockedBy } = credentials;
+
+  check(meetingId, String);
+  check(lockedBy, String);
+  check(userId, String);
+  check(lock, Boolean);
+
+  const payload = {
+    lockedBy,
+    userId,
+    lock,
+  };
+
+  Logger.verbose(`User ${lockedBy} updated lock status from ${userId} to ${lock}
+  in meeting ${meetingId}`);
+
+
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, lockedBy, payload);
+}
diff --git a/bigbluebutton-html5/imports/startup/client/intl.jsx b/bigbluebutton-html5/imports/startup/client/intl.jsx
index b400dc9894ba8f41c0562f66045a49bbebd3ad23..a1407f8d2b7d8afa9c74a9b20843bd2b29d2cc97 100644
--- a/bigbluebutton-html5/imports/startup/client/intl.jsx
+++ b/bigbluebutton-html5/imports/startup/client/intl.jsx
@@ -5,41 +5,43 @@ import Settings from '/imports/ui/services/settings';
 import LoadingScreen from '/imports/ui/components/loading-screen/component';
 
 // currently supported locales.
-import en from 'react-intl/locale-data/en';
-import uk from 'react-intl/locale-data/uk';
-import zh from 'react-intl/locale-data/zh';
-import ru from 'react-intl/locale-data/ru';
+import bg from 'react-intl/locale-data/bg';
 import de from 'react-intl/locale-data/de';
-import fr from 'react-intl/locale-data/fr';
-import pt from 'react-intl/locale-data/pt';
-import fa from 'react-intl/locale-data/fa';
-import tr from 'react-intl/locale-data/tr';
-import ja from 'react-intl/locale-data/ja';
-import km from 'react-intl/locale-data/km';
+import el from 'react-intl/locale-data/el';
+import en from 'react-intl/locale-data/en';
 import es from 'react-intl/locale-data/es';
+import fa from 'react-intl/locale-data/fa';
+import fr from 'react-intl/locale-data/fr';
 import id from 'react-intl/locale-data/id';
-import el from 'react-intl/locale-data/el';
 import it from 'react-intl/locale-data/it';
-import bg from 'react-intl/locale-data/bg';
+import ja from 'react-intl/locale-data/ja';
+import km from 'react-intl/locale-data/km';
+import pl from 'react-intl/locale-data/pl';
+import pt from 'react-intl/locale-data/pt';
+import ru from 'react-intl/locale-data/ru';
+import tr from 'react-intl/locale-data/tr';
+import uk from 'react-intl/locale-data/uk';
+import zh from 'react-intl/locale-data/zh';
 
 
 addLocaleData([
-  ...en,
-  ...uk,
-  ...zh,
-  ...ru,
+  ...bg,
   ...de,
-  ...fr,
-  ...pt,
-  ...fa,
-  ...tr,
-  ...ja,
-  ...km,
+  ...el,
+  ...en,
   ...es,
+  ...fa,
+  ...fr,
   ...id,
-  ...el,
   ...it,
-  ...bg,
+  ...ja,
+  ...km,
+  ...pl,
+  ...pt,
+  ...ru,
+  ...tr,
+  ...uk,
+  ...zh,
 ]);
 
 const propTypes = {
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
index c8aeebdeb2c1ff6f44b4facb3e3437e9215c8048..b69f2f73affe5252b2c4423e7f853b7a03fa6151 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx
@@ -145,8 +145,6 @@ class ActionsDropdown extends Component {
       pollBtnDesc,
       presentationLabel,
       presentationDesc,
-      startRecording,
-      stopRecording,
       createBreakoutRoom,
       createBreakoutRoomDesc,
       invitationItem,
@@ -158,6 +156,10 @@ class ActionsDropdown extends Component {
       formatMessage,
     } = intl;
 
+    const canCreateBreakout = isUserModerator
+    && !meetingIsBreakout
+    && !hasBreakoutRoom;
+
     const canInviteUsers = isUserModerator
     && !meetingIsBreakout
     && hasBreakoutRoom
@@ -209,12 +211,12 @@ class ActionsDropdown extends Component {
           />
         )
         : null),
-      (isUserModerator && !meetingIsBreakout && !hasBreakoutRoom
+      (canCreateBreakout
         ? (
           <DropdownListItem
             icon="rooms"
-            label={intl.formatMessage(intlMessages.createBreakoutRoom)}
-            description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)}
+            label={formatMessage(createBreakoutRoom)}
+            description={formatMessage(createBreakoutRoomDesc)}
             key={this.createBreakoutRoomId}
             onClick={this.onCreateBreakouts}
           />
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
index e6195e3aa32dee3b9cd18a07d560a61f3fa1837e..7614f4c115c68a507cc83d466ba4284e9481d9c7 100755
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx
@@ -4,7 +4,9 @@ import { styles } from './styles.scss';
 import DesktopShare from './desktop-share/component';
 import ActionsDropdown from './actions-dropdown/component';
 import AudioControlsContainer from '../audio/audio-controls/container';
-import JoinVideoOptionsContainer from '../video-provider/video-menu/container';
+import JoinVideoOptionsContainer from '../video-provider/video-button/container';
+
+import PresentationOptionsContainer from './presentation-options/component';
 
 class ActionsBar extends React.PureComponent {
   render() {
@@ -25,6 +27,8 @@ class ActionsBar extends React.PureComponent {
       hasBreakoutRoom,
       meetingName,
       users,
+      isLayoutSwapped,
+      toggleSwapLayout,
       getUsersNotAssigned,
       sendInvitation,
       getBreakouts,
@@ -89,6 +93,16 @@ class ActionsBar extends React.PureComponent {
           }}
           />
         </div>
+        <div className={styles.right}>
+          { isLayoutSwapped
+            ? (
+              <PresentationOptionsContainer
+                toggleSwapLayout={toggleSwapLayout}
+              />
+            )
+            : null
+          }
+        </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 ad7eac73e8ce91b3e711edd68e9c78f0ac3d6c2e..d6bda329699bd5cc16122e788a63c61f396f508c 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx
@@ -9,6 +9,8 @@ import Service from './service';
 import VideoService from '../video-provider/service';
 import { shareScreen, unshareScreen, isVideoBroadcasting } from '../screenshare/service';
 
+import MediaService, { getSwapLayout } from '../media/service';
+
 const ActionsBarContainer = props => <ActionsBar {...props} />;
 
 export default withTracker(() => {
@@ -42,6 +44,8 @@ export default withTracker(() => {
     hasBreakoutRoom: Service.hasBreakoutRoom(),
     meetingName: Service.meetingName(),
     users: Service.users(),
+    isLayoutSwapped: getSwapLayout(),
+    toggleSwapLayout: MediaService.toggleSwapLayout,
     sendInvitation: Service.sendInvitation,
     getBreakouts: Service.getBreakouts,
     getUsersNotAssigned: Service.getUsersNotAssigned,
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
index 22276a96008f85189fc5865c986e7d446e36f5d1..9ba4aceedcdb90864406df5c94514e7b6714e144 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/create-breakout-room/component.jsx
@@ -127,6 +127,10 @@ class BreakoutRoom extends Component {
       preventClosing: true,
       valid: true,
     };
+
+    this.breakoutFormId = _.uniqueId('breakout-form-');
+    this.freeJoinId = _.uniqueId('free-join-check-');
+    this.btnLevelId = _.uniqueId('btn-set-level-');
   }
 
   componentDidMount() {
@@ -155,7 +159,7 @@ class BreakoutRoom extends Component {
       freeJoin,
     } = this.state;
 
-    if (users.length === this.getUserByRoom(0).length) {
+    if (users.length === this.getUserByRoom(0).length && !freeJoin) {
       this.setState({ valid: false });
       return;
     }
@@ -295,7 +299,7 @@ class BreakoutRoom extends Component {
     };
 
     return (
-      <div className={styles.boxContainer}>
+      <div className={styles.boxContainer} key="rooms-grid-">
         <label htmlFor="BreakoutRoom" className={!valid ? styles.changeToWarn : null}>
           <p
             className={styles.freeJoinLabel}
@@ -340,7 +344,7 @@ class BreakoutRoom extends Component {
     if (isInvitation) return null;
 
     return (
-      <div className={styles.breakoutSettings}>
+      <div className={styles.breakoutSettings} key={this.breakoutFormId}>
         <label htmlFor="numberOfRooms">
           <p className={styles.labelText}>{intl.formatMessage(intlMessages.numberOfRooms)}</p>
           <select
@@ -422,7 +426,7 @@ class BreakoutRoom extends Component {
     if (isInvitation) return null;
     const { freeJoin } = this.state;
     return (
-      <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel}>
+      <label htmlFor="freeJoinCheckbox" className={styles.freeJoinLabel} key={this.freeJoinId}>
         <input
           type="checkbox"
           className={styles.freeJoinCheckbox}
@@ -534,6 +538,7 @@ class BreakoutRoom extends Component {
         size="lg"
         label={label}
         onClick={() => this.setState({ formFillLevel: level })}
+        key={this.btnLevelId}
       />
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..f46adc2b4b44114862917f84441e73c51311a932
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
+import Button from '/imports/ui/components/button/component';
+import { styles } from '../styles';
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  toggleSwapLayout: PropTypes.func.isRequired,
+};
+
+const intlMessages = defineMessages({
+  restorePresentationLabel: {
+    id: 'app.actionsBar.actionsDropdown.restorePresentationLabel',
+    description: 'Restore Presentation option label',
+  },
+  restorePresentationDesc: {
+    id: 'app.actionsBar.actionsDropdown.restorePresentationDesc',
+    description: 'button to restore presentation after it has been closed',
+  },
+});
+
+const PresentationOptionsContainer = ({ intl, toggleSwapLayout }) => (
+  <Button
+    className={styles.button}
+    icon="presentation"
+    label={intl.formatMessage(intlMessages.restorePresentationLabel)}
+    description={intl.formatMessage(intlMessages.restorePresentationDesc)}
+    color="primary"
+    hideLabel
+    circle
+    size="lg"
+    onClick={toggleSwapLayout}
+    id="restore-presentation"
+  />
+);
+
+PresentationOptionsContainer.propTypes = propTypes;
+export default injectIntl(PresentationOptionsContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
index 7619bf7adca27ee4fba95d6505aed73b602ed7be..31a39e40089a87cef80de64cf3a5f25d942efffd 100644
--- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss
@@ -2,13 +2,15 @@
 
 .actionsbar,
 .left,
-.center {
+.center,
+.right {
   display: flex;
   flex-direction: row;
 }
 
 .left,
-.center {
+.center,
+.right {
   flex: 1;
   justify-content: center;
 
@@ -29,6 +31,12 @@
   }
 }
 
+.right {
+  position: absolute;
+  bottom: var(--sm-padding-x);
+  right: var(--sm-padding-x);
+}
+
 .centerWithActions {
   @include mq($xsmall-only) {
     justify-content: flex-end;
@@ -57,4 +65,4 @@
   span:hover {
     border: 1.5px solid rgba(255,255,255, .5) !important;
   }
-}
\ No newline at end of file
+}
diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx
index 435c131a01bacec52b51747ddcd91bf00fd131c8..1a7b13e38e989abf15758a600ad6ddcfcc18db10 100755
--- a/bigbluebutton-html5/imports/ui/components/app/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx
@@ -194,7 +194,7 @@ class App extends Component {
 
   render() {
     const {
-      customStyle, customStyleUrl, micsLocked, openPanel,
+      customStyle, customStyleUrl, openPanel,
     } = this.props;
 
     return (
@@ -211,7 +211,7 @@ class App extends Component {
         </section>
         <PollingContainer />
         <ModalContainer />
-        {micsLocked ? null : <AudioContainer />}
+        <AudioContainer />
         <ToastContainer />
         <ChatAlertContainer />
         {customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx
index a289c016da41c7b4223d0759631d5548842497b8..999b09ea160d0cd0f9ce65d3b9ccb3406de1d40f 100755
--- a/bigbluebutton-html5/imports/ui/components/app/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx
@@ -116,7 +116,6 @@ export default injectIntl(withModalMounter(withTracker(({ intl, baseControls })
     chatIsOpen: Session.equals('openPanel', 'chat'),
     openPanel: Session.get('openPanel'),
     userListIsOpen: !Session.equals('openPanel', ''),
-    micsLocked: (currentUserIsLocked && meeting.lockSettingsProp.disableMic),
   };
 })(AppContainer)));
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
index 11cf83edb83fdbec7114f39fc599dbf03d27d205..3c97857f9e30eba8d2de72eede71e7add3946469 100644
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-controls/container.jsx
@@ -44,12 +44,7 @@ export default withModalMounter(withTracker(({ mountModal }) => ({
   glow: Service.isTalking() && !Service.isMuted(),
   handleToggleMuteMicrophone: () => Service.toggleMuteMicrophone(),
   handleJoinAudio: () => {
-    const meeting = Meetings.findOne({ meetingId: Auth.meetingID });
-    const currentUser = Users.findOne({ userId: Auth.userID });
-    const currentUserIsLocked = mapUser(currentUser).isLocked;
-    const micsLocked = (currentUserIsLocked && meeting.lockSettingsProp.disableMic);
-
-    return micsLocked ? Service.joinListenOnly() : mountModal(<AudioModalContainer />);
+    return Service.isConnected() ? Service.joinListenOnly() : mountModal(<AudioModalContainer />);
   },
   handleLeaveAudio: () => Service.exitAudio(),
 }))(AudioControlsContainer));
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 d06f46b3838985f73343da7e388d2769b10e0a59..5bd10c2965b2006f9eb136c71ae85c20fbd5ad7e 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
@@ -129,6 +129,7 @@ class AudioModal extends Component {
       joinFullAudioImmediately,
       joinFullAudioEchoTest,
       forceListenOnlyAttendee,
+      audioLocked,
     } = this.props;
 
     if (joinFullAudioImmediately) {
@@ -139,7 +140,7 @@ class AudioModal extends Component {
       this.handleGoToEchoTest();
     }
 
-    if (forceListenOnlyAttendee) {
+    if (forceListenOnlyAttendee || audioLocked) {
       this.handleJoinListenOnly();
     }
   }
@@ -266,9 +267,11 @@ class AudioModal extends Component {
       audioLocked,
     } = this.props;
 
+    const showMicrophone = forceListenOnlyAttendee || audioLocked;
+
     return (
       <span className={styles.audioOptions}>
-        {!forceListenOnlyAttendee
+        {!showMicrophone
           ? (
             <Button
               className={styles.audioBtn}
diff --git a/bigbluebutton-html5/imports/ui/components/button/styles.scss b/bigbluebutton-html5/imports/ui/components/button/styles.scss
index 85e292eaf0d7fa2385bef9a14a5ad40457965085..a0bb51b40dbf86e75bb556a147c70dc444fd1e9b 100644
--- a/bigbluebutton-html5/imports/ui/components/button/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/button/styles.scss
@@ -260,7 +260,7 @@
 }
 
 .circle {
-  --btn-sm-padding-x: calc(var(--lg-padding-x) / 2.75);
+  --btn-sm-padding-x: calc(var(--sm-padding-x) / 2.75);
   --btn-md-padding-x: calc(var(--md-padding-x) / 2.75);
   --btn-lg-padding-x: calc(var(--lg-padding-x) / 2.75);
   --btn-jumbo-padding-x: calc(var(--jumbo-padding-x) / 2.75);
diff --git a/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx b/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx
index 1a5952544ea418c0b50287cd494e99dd0ec525ac..31d7219095ac83ed88a3ea5eea6dc8887b0f7066 100644
--- a/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/closed-captions/component.jsx
@@ -1,9 +1,17 @@
-import React, { Component } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
 import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
 import { styles } from './styles.scss';
 
-class ClosedCaptions extends Component {
+const intlMessages = defineMessages({
+  noLocaleSelected: {
+    id: 'app.submenu.closedCaptions.noLocaleSelected',
+    description: 'label for selected language for closed captions',
+  },
+});
+
+class ClosedCaptions extends React.PureComponent {
   constructor(props) {
     super(props);
 
@@ -25,13 +33,14 @@ class ClosedCaptions extends Component {
   }
 
   renderCaptions(caption) {
+    const { fontFamily, fontSize, fontColor } = this.props;
     const text = caption.captions;
     const captionStyles = {
       whiteSpace: 'pre-wrap',
       wordWrap: 'break-word',
-      fontFamily: this.props.fontFamily,
-      fontSize: this.props.fontSize,
-      color: this.props.fontColor,
+      fontFamily,
+      fontSize,
+      color: fontColor,
     };
 
     return (
@@ -48,12 +57,15 @@ class ClosedCaptions extends Component {
       locale,
       captions,
       backgroundColor,
+      intl,
     } = this.props;
 
     return (
       <div disabled className={styles.ccbox}>
         <div className={styles.title}>
-          <p> {locale} </p>
+          <p>
+            { locale || intl.formatMessage(intlMessages.noLocaleSelected) }
+          </p>
         </div>
         <div
           ref={(ref) => { this.refCCScrollArea = ref; }}
@@ -69,7 +81,7 @@ class ClosedCaptions extends Component {
   }
 }
 
-export default injectWbResizeEvent(ClosedCaptions);
+export default injectIntl(injectWbResizeEvent(ClosedCaptions));
 
 ClosedCaptions.propTypes = {
   backgroundColor: PropTypes.string.isRequired,
@@ -83,12 +95,15 @@ ClosedCaptions.propTypes = {
       ).isRequired,
     }).isRequired,
   ).isRequired,
-  locale: PropTypes.string.isRequired,
+  locale: PropTypes.string,
   fontColor: PropTypes.string.isRequired,
   fontSize: PropTypes.string.isRequired,
   fontFamily: PropTypes.string.isRequired,
+  intl: PropTypes.shape({
+    formatMessage: PropTypes.func.isRequired,
+  }).isRequired,
 };
 
 ClosedCaptions.defaultProps = {
-  locale: 'Locale is not selected',
+  locale: undefined,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/closed-captions/service.js b/bigbluebutton-html5/imports/ui/components/closed-captions/service.js
index da2e0e10fc1d2a79b821cb80bd1aa5c927ce9f26..d2bd52338129dfa6f422f1ab8e09645e246a68a4 100644
--- a/bigbluebutton-html5/imports/ui/components/closed-captions/service.js
+++ b/bigbluebutton-html5/imports/ui/components/closed-captions/service.js
@@ -4,7 +4,7 @@ import Settings from '/imports/ui/services/settings';
 import _ from 'lodash';
 
 const getCCData = () => {
-  const meetingID = Auth.meetingID;
+  const { meetingID } = Auth;
 
   const ccSettings = Settings.cc;
 
diff --git a/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx b/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx
index 261a55ebf214d1555272465f8a7e53a5323dc5a6..604dc502b6a3f9fae624fae8070a72216d46205f 100755
--- a/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/lock-viewers/component.jsx
@@ -50,10 +50,6 @@ const intlMessages = defineMessages({
     id: 'app.lock-viewers.PrivateChatLable',
     description: 'description for close button',
   },
-  layoutLable: {
-    id: 'app.lock-viewers.Layout',
-    description: 'description for close button',
-  },
 });
 
 class LockViewersComponent extends React.PureComponent {
@@ -197,28 +193,6 @@ class LockViewersComponent extends React.PureComponent {
                 </div>
               </div>
             </div>
-            <div className={styles.row}>
-              <div className={styles.col} aria-hidden="true">
-                <div className={styles.formElement}>
-                  <div className={styles.label}>
-                    {intl.formatMessage(intlMessages.layoutLable)}
-                  </div>
-                </div>
-              </div>
-              <div className={styles.col}>
-                <div className={cx(styles.formElement, styles.pullContentRight)}>
-                  <Toggle
-                    icons={false}
-                    defaultChecked={meeting.lockSettingsProp.lockedLayout}
-                    onChange={() => {
-                      meeting.lockSettingsProp.lockedLayout = !meeting.lockSettingsProp.lockedLayout;
-                      toggleLockSettings(meeting);
-                    }}
-                    ariaLabel={intl.formatMessage(intlMessages.layoutLable)}
-                  />
-                </div>
-              </div>
-            </div>
           </div>
         </div>
       </Modal>
diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx
index ccf3135ba9d79227b73b1b20ddb4ab04e54d046c..451af4b68500f1a4892a662bfdd6cd91f56a65cb 100755
--- a/bigbluebutton-html5/imports/ui/components/media/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx
@@ -131,7 +131,11 @@ export default withModalMounter(withTracker(() => {
 
   if (data.swapLayout) {
     data.floatingOverlay = true;
-    data.hideOverlay = hidePresentation;
+    data.hideOverlay = true;
+  }
+
+  if (data.isScreensharing) {
+    data.floatingOverlay = false;
   }
 
   if (MediaService.shouldShowExternalVideo()) {
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
index 889ba8f50d13bf20fe40ac0e8f0ffc3fb916c72a..1455624126947e13405c2f733f3aa0b967753cc3 100644
--- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx
@@ -10,6 +10,9 @@ import PresentationToolbarContainer from './presentation-toolbar/container';
 import PresentationOverlayContainer from './presentation-overlay/container';
 import Slide from './slide/component';
 import { styles } from './styles.scss';
+import MediaService from '../media/service';
+import PresentationCloseButton from './presentation-close-button/component';
+import FullscreenButton from '../video-provider/fullscreen-button/component';
 
 export default class PresentationArea extends Component {
   constructor() {
@@ -171,9 +174,7 @@ export default class PresentationArea extends Component {
 
   // renders the whole presentation area
   renderPresentationArea() {
-    // sometimes tomcat publishes the slide url, but the actual file is not accessible (why?)
-    if (!this.props.currentSlide
-        || !this.props.currentSlide.calculatedData) {
+    if (!this.isPresentationAccessible()) {
       return null;
     }
     // to control the size of the svg wrapper manually
@@ -182,6 +183,9 @@ export default class PresentationArea extends Component {
     // a reference to the slide object
     const slideObj = this.props.currentSlide;
 
+    const presentationCloseButton = this.renderPresentationClose();
+    const presentationFullscreenButton = this.renderPresentationFullscreen();
+
     // retrieving the pre-calculated data from the slide object
     const {
       x,
@@ -193,8 +197,10 @@ export default class PresentationArea extends Component {
       imageUri,
     } = slideObj.calculatedData;
     const svgDimensions = this.state.fitToWidth ? {
+      position: 'absolute',
       width: 'inherit',
     } : {
+      position: 'absolute',
       width: adjustedSizes.width,
       height: adjustedSizes.height,
     };
@@ -202,6 +208,8 @@ export default class PresentationArea extends Component {
       <div
         style={svgDimensions}
       >
+        {presentationCloseButton}
+        {presentationFullscreenButton}
         <TransitionGroup>
           <CSSTransition
             key={slideObj.id}
@@ -314,6 +322,31 @@ export default class PresentationArea extends Component {
     );
   }
 
+  isPresentationAccessible() {
+    // sometimes tomcat publishes the slide url, but the actual file is not accessible (why?)
+    return this.props.currentSlide && this.props.currentSlide.calculatedData;
+  };
+
+  isFullscreen() {
+    return document.fullscreenElement !== null;
+  }
+
+  renderPresentationClose() {
+    if (!MediaService.shouldEnableSwapLayout() || this.isFullscreen()) {
+      return null;
+    }
+    return <PresentationCloseButton toggleSwapLayout={MediaService.toggleSwapLayout} />;
+  };
+
+  renderPresentationFullscreen() {
+    if (this.isFullscreen()) {
+      return null;
+    }
+    const full = () => this.refPresentationContainer.requestFullscreen();
+
+    return <FullscreenButton handleFullscreen={full} dark />;
+  }
+
   renderPresentationToolbar() {
     if (!this.props.currentSlide) {
       return null;
@@ -332,8 +365,7 @@ export default class PresentationArea extends Component {
   }
 
   renderWhiteboardToolbar() {
-    if (!this.props.currentSlide
-        || !this.props.currentSlide.calculatedData) {
+    if (!this.isPresentationAccessible()) {
       return null;
     }
 
@@ -348,7 +380,9 @@ export default class PresentationArea extends Component {
 
   render() {
     return (
-      <div className={styles.presentationContainer}>
+      <div
+        ref={(ref) => { this.refPresentationContainer = ref; }}
+        className={styles.presentationContainer}>
         <div
           ref={(ref) => { this.refPresentationArea = ref; }}
           className={styles.presentationArea}
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..8ea9abbaad32f7e08e445d9f2a14f5e7d38fe785
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/component.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import Button from '/imports/ui/components/button/component';
+import { styles } from './styles';
+
+const intlMessages = defineMessages({
+  closePresentationLabel: {
+    id: 'app.presentation.close',
+    description: 'Close presentation label',
+  },
+});
+
+const ClosePresentationComponent = ({ intl, toggleSwapLayout }) => (
+  <Button
+    role="button"
+    aria-labelledby="closeLabel"
+    aria-describedby="closeDesc"
+    color="primary"
+    icon="close"
+    size="sm"
+    onClick={toggleSwapLayout}
+    label={intl.formatMessage(intlMessages.closePresentationLabel)}
+    hideLabel
+    className={styles.button}
+  />
+);
+
+export default injectIntl(ClosePresentationComponent);
diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2c3384c4cd6fd8ddf103a4a3068f6a820f2779e3
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-close-button/styles.scss
@@ -0,0 +1,7 @@
+
+.button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  cursor: pointer;
+}
diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
index 906e66c6aa1cd91f31516ad6b00f9f676b488239..6eb1a2c6d30e2dc3970b5ac46273f48267cd9567 100755
--- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx
@@ -1,5 +1,5 @@
 import React from 'react';
-
+import FullscreenButton from '../video-provider/fullscreen-button/component';
 import { styles } from './styles';
 
 export default class ScreenshareComponent extends React.Component {
@@ -11,26 +11,57 @@ export default class ScreenshareComponent extends React.Component {
 
     this.onVideoLoad = this.onVideoLoad.bind(this);
   }
+
   componentDidMount() {
     this.props.presenterScreenshareHasStarted();
   }
+
   componentWillReceiveProps(nextProps) {
     if (this.props.isPresenter && !nextProps.isPresenter) {
       this.props.unshareScreen();
     }
   }
+
   componentWillUnmount() {
     this.props.presenterScreenshareHasEnded();
     this.props.unshareScreen();
   }
+
   onVideoLoad() {
     this.setState({ loaded: true });
   }
 
+  renderFullscreenButton() {
+    const full = () => {
+      if (!this.videoTag) {
+        return;
+      }
+
+      this.videoTag.requestFullscreen();
+    };
+    return <FullscreenButton handleFullscreen={full} />;
+  }
+
   render() {
+    const style = {
+      right: 0,
+      bottom: 0,
+    };
+
     return (
-      [!this.state.loaded ? (<div key="screenshareArea" className={styles.connecting} />) : null,
-        (<video key="screenshareVideo" id="screenshareVideo" style={{ maxHeight: '100%', width: '100%' }} autoPlay playsInline onLoadedData={this.onVideoLoad} />)]
+      [!this.state.loaded ? (<div key="screenshareArea" innerStyle={style} className={styles.connecting} />) : null,
+        this.renderFullscreenButton(),
+        (
+          <video
+            key="screenshareVideo"
+            id="screenshareVideo"
+            style={{ maxHeight: '100%', width: '100%' }}
+            autoPlay
+            playsInline
+            onLoadedData={this.onVideoLoad}
+            ref={(ref) => { this.videoTag = ref; }}
+          />
+        )]
     );
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/settings/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
index fd0dc75ec88c25c000e0c89c15c9cf126c798c44..8aeda4d68d75b7585baddd425c8fa4f5fe95913e 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/component.jsx
@@ -148,13 +148,17 @@ class Settings extends Component {
   }
 
   renderModalContent() {
-    const { intl } = this.props;
+    const {
+      intl,
+      locales,
+    } = this.props;
+
     const {
       selectedTab,
       availableLocales,
       current,
-      locales,
     } = this.state;
+
     return (
       <Tabs
         className={styles.tabs}
diff --git a/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx b/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx
index 12f4f34be11a35682c57dba80f741c3160fb8a82..c227d044f4d17b59a3d5267416914e1e0f470542 100644
--- a/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/settings/submenus/closed-captions/component.jsx
@@ -198,15 +198,15 @@ class ClosedCaptionsMenu extends BaseMenu {
                       <select
                         defaultValue={locales ? locales.indexOf(this.state.settings.locale) : -1}
                         className={styles.select}
-                        onChange={this.handleSelectChange.bind(this, 'locale', this.props.locales)}
+                        onChange={this.handleSelectChange.bind(this, 'locale', locales)}
                       >
                         <option>
-                          { this.props.locales
-                          && this.props.locales.length
+                          { locales
+                          && locales.length
                             ? intl.formatMessage(intlMessages.localeOptionLabel)
                             : intl.formatMessage(intlMessages.noLocaleOptionLabel) }
                         </option>
-                        {this.props.locales ? this.props.locales.map((locale, index) => (
+                        {locales ? locales.map((locale, index) => (
                           <option key={index} value={index}>
                             {locale}
                           </option>
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
index 1a00e4a275eac3378a64129e9e994821ad69c6df..04e126d05a577801d78b8de110645ab982624b0a 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
@@ -31,6 +31,7 @@ const propTypes = {
   roving: PropTypes.func.isRequired,
   getGroupChatPrivate: PropTypes.func.isRequired,
   showBranding: PropTypes.bool.isRequired,
+  toggleUserLock: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -67,6 +68,7 @@ class UserList extends PureComponent {
       hasBreakoutRoom,
       getUsersId,
       hasPrivateChatBetweenUsers,
+      toggleUserLock,
     } = this.props;
 
     return (
@@ -103,6 +105,7 @@ class UserList extends PureComponent {
             hasBreakoutRoom,
             getUsersId,
             hasPrivateChatBetweenUsers,
+            toggleUserLock,
           }
           }
         />}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
index c2cf35d53fa9712170fdfec55dda34d682e3624d..8fcc687aaed1d69a9e0d8a30d1b6ed3b98848559 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
@@ -24,6 +24,7 @@ const propTypes = {
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
   getGroupChatPrivate: PropTypes.func.isRequired,
+  toggleUserLock: PropTypes.func.isRequired,
 };
 
 const UserListContainer = props => <UserList {...props} />;
@@ -56,4 +57,5 @@ export default withTracker(({ chatID, compact }) => ({
   getEmoji: Service.getEmoji(),
   showBranding: getFromUserSettings('displayBrandingArea', Meteor.settings.public.app.branding.displayBrandingArea),
   hasPrivateChatBetweenUsers: Service.hasPrivateChatBetweenUsers,
+  toggleUserLock: Service.toggleUserLock,
 }))(UserListContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 17fde8ed15e66d4956ca2a526b4c5aeb372aed30..487997f5a3ecab798ccfbbd54983b2210554b603 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -260,6 +260,24 @@ const getActiveChats = (chatID) => {
 
 const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
 
+const isMeetingLocked = (id) => {
+  const meeting = Meetings.findOne({ meetingId: id });
+  let isLocked = false;
+
+  if (meeting.lockSettingsProp !== undefined) {
+    const lockSettings = meeting.lockSettingsProp;
+
+    if (lockSettings.disableCam
+      || lockSettings.disableMic
+      || lockSettings.disablePrivChat
+      || lockSettings.disablePubChat) {
+      isLocked = true;
+    }
+  }
+
+  return isLocked;
+};
+
 const getAvailableActions = (currentUser, user, isBreakoutRoom) => {
   const isDialInUser = isVoiceOnlyUser(user.id) || user.isPhoneUser;
 
@@ -301,6 +319,9 @@ const getAvailableActions = (currentUser, user, isBreakoutRoom) => {
 
   const allowedToChangeStatus = user.isCurrent;
 
+  const allowedToChangeUserLockStatus = currentUser.isModerator
+    && !user.isModerator && isMeetingLocked(Auth.meetingID);
+
   return {
     allowedToChatPrivately,
     allowedToMuteAudio,
@@ -311,6 +332,7 @@ const getAvailableActions = (currentUser, user, isBreakoutRoom) => {
     allowedToPromote,
     allowedToDemote,
     allowedToChangeStatus,
+    allowedToChangeUserLockStatus,
   };
 };
 
@@ -325,24 +347,6 @@ const normalizeEmojiName = emoji => (
   emoji in EMOJI_STATUSES ? EMOJI_STATUSES[emoji] : emoji
 );
 
-const isMeetingLocked = (id) => {
-  const meeting = Meetings.findOne({ meetingId: id });
-  let isLocked = false;
-
-  if (meeting.lockSettingsProp !== undefined) {
-    const lockSettings = meeting.lockSettingsProp;
-
-    if (lockSettings.disableCam
-      || lockSettings.disableMic
-      || lockSettings.disablePrivChat
-      || lockSettings.disablePubChat) {
-      isLocked = true;
-    }
-  }
-
-  return isLocked;
-};
-
 const setEmojiStatus = (data) => {
   const statusAvailable = (Object.keys(EMOJI_STATUSES).includes(data));
 
@@ -448,6 +452,10 @@ const isUserModerator = (userId) => {
   return u ? u.moderator : false;
 };
 
+const toggleUserLock = (userId, lockStatus) => {
+  makeCall('toggleUserLock', userId, lockStatus);
+};
+
 export default {
   setEmojiStatus,
   assignPresenter,
@@ -473,4 +481,5 @@ export default {
   getEmojiList: () => EMOJI_STATUSES,
   getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
   hasPrivateChatBetweenUsers,
+  toggleUserLock,
 };
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 9a12a071955681f3e6fd014c00c9c1f00a0dff32..2332e2d8258be41b126cef2ff8c77d8acb1046f0 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
@@ -32,6 +32,7 @@ const propTypes = {
   getUsersId: PropTypes.func.isRequired,
   pollIsOpen: PropTypes.bool.isRequired,
   forcePollOpen: PropTypes.bool.isRequired,
+  toggleUserLock: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -68,6 +69,7 @@ class UserContent extends PureComponent {
       hasBreakoutRoom,
       getUsersId,
       hasPrivateChatBetweenUsers,
+      toggleUserLock,
     } = this.props;
 
     return (
@@ -121,6 +123,7 @@ class UserContent extends PureComponent {
             getGroupChatPrivate,
             getUsersId,
             hasPrivateChatBetweenUsers,
+            toggleUserLock,
           }}
         />
       </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 aa2276dad51ae0a7db4386af9e8dbc2758ee5f91..d77c8e349feffb8541da330ff6b8d739019b850b 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
@@ -31,6 +31,7 @@ const propTypes = {
   normalizeEmojiName: PropTypes.func.isRequired,
   isMeetingLocked: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
+  toggleUserLock: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -126,6 +127,7 @@ class UserParticipants extends Component {
       getEmoji,
       users,
       hasPrivateChatBetweenUsers,
+      toggleUserLock,
     } = this.props;
 
     let index = -1;
@@ -161,6 +163,7 @@ class UserParticipants extends Component {
               changeRole,
               getGroupChatPrivate,
               hasPrivateChatBetweenUsers,
+              toggleUserLock,
             }}
             userId={u}
             getScrollContainerRef={this.getScrollContainerRef}
@@ -200,20 +203,24 @@ class UserParticipants extends Component {
               <div className={styles.container}>
                 <h2 className={styles.smallTitle}>
                   {intl.formatMessage(intlMessages.usersTitle)}
-                &nbsp;(
+                  &nbsp;(
                   {users.length}
-)
-
+                  )
                 </h2>
-                <UserOptionsContainer {...{
-                  users,
-                  muteAllUsers,
-                  muteAllExceptPresenter,
-                  setEmojiStatus,
-                  meeting,
-                  currentUser,
-                }}
-                />
+                {currentUser.isModerator
+                  ? (
+                    <UserOptionsContainer {...{
+                      users,
+                      muteAllUsers,
+                      muteAllExceptPresenter,
+                      setEmojiStatus,
+                      meeting,
+                      currentUser,
+                    }}
+                    />
+                  ) : null
+                }
+
               </div>
             )
             : <hr className={styles.separator} />
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx
index b5ef848a0d23e708ebcd76bfe65add95a2980a35..2ccb85ca97353c3b63f2e1927dcd6a5f85f8760c 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/container.jsx
@@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
 import Meetings from '/imports/api/meetings';
 import UserParticipants from './component';
 
-const UserParticipantsContainer = ({ ...props }) => <UserParticipants {...props} />;
+const UserParticipantsContainer = props => <UserParticipants {...props} />;
 
 export default withTracker(({ getUsersId }) => ({
   meeting: Meetings.findOne({}),
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 701d0d6168e5e6409c8fc5cb154681f716ee1e02..2ffad3e01ade1d39633325289cbf0ba68cdc5c5d 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
@@ -18,6 +18,7 @@ const propTypes = {
   isMeetingLocked: PropTypes.func.isRequired,
   normalizeEmojiName: PropTypes.func.isRequired,
   getScrollContainerRef: PropTypes.func.isRequired,
+  toggleUserLock: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -47,6 +48,7 @@ class UserListItem extends PureComponent {
       setEmojiStatus,
       toggleVoice,
       hasPrivateChatBetweenUsers,
+      toggleUserLock,
     } = this.props;
 
     const { meetingId, lockSettingsProp } = meeting;
@@ -75,6 +77,7 @@ class UserListItem extends PureComponent {
           toggleVoice,
           user,
           hasPrivateChatBetweenUsers,
+          toggleUserLock,
         }}
       />
     );
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
index c5f5422c631728f5aa5b0506ec8bee3e41869168..7ab54c5fa60c0c3eacdd080ca6a2bac95b7918c0 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-dropdown/component.jsx
@@ -81,6 +81,14 @@ const messages = defineMessages({
     id: 'app.userList.menu.demoteUser.label',
     description: 'Forcefully demote this moderator to a viewer',
   },
+  UnlockUserLabel: {
+    id: 'app.userList.menu.unlockUser.label',
+    description: 'Unlock individual user',
+  },
+  LockUserLabel: {
+    id: 'app.userList.menu.lockUser.label',
+    description: 'Lock a unlocked user',
+  },
 });
 
 const propTypes = {
@@ -92,6 +100,7 @@ const propTypes = {
   normalizeEmojiName: PropTypes.func.isRequired,
   isMeetingLocked: PropTypes.func.isRequired,
   getScrollContainerRef: PropTypes.func.isRequired,
+  toggleUserLock: PropTypes.func.isRequired,
 };
 
 class UserDropdown extends PureComponent {
@@ -196,6 +205,7 @@ class UserDropdown extends PureComponent {
       changeRole,
       lockSettingsProp,
       hasPrivateChatBetweenUsers,
+      toggleUserLock,
     } = this.props;
 
     const { showNestedOptions } = this.state;
@@ -213,6 +223,7 @@ class UserDropdown extends PureComponent {
       allowedToPromote,
       allowedToDemote,
       allowedToChangeStatus,
+      allowedToChangeUserLockStatus,
     } = actionPermissions;
 
     const { disablePrivChat } = lockSettingsProp;
@@ -331,6 +342,16 @@ class UserDropdown extends PureComponent {
       ));
     }
 
+    if (allowedToChangeUserLockStatus) {
+      actions.push(this.makeDropdownItem(
+        'unlockUser',
+        user.isLocked ? intl.formatMessage(messages.UnlockUserLabel, { 0: user.name })
+          : intl.formatMessage(messages.LockUserLabel, { 0: user.name }),
+        () => this.onActionsHide(toggleUserLock(user.id, !user.isLocked)),
+        user.isLocked ? 'unlock' : 'lock',
+      ));
+    }
+
     return actions;
   }
 
@@ -438,7 +459,7 @@ class UserDropdown extends PureComponent {
         voice={user.isVoiceUser}
         color={user.color}
       >
-        {isVoiceOnly ? iconVoiceOnlyUser : iconUser }
+        {isVoiceOnly ? iconVoiceOnlyUser : iconUser}
       </UserAvatar>
     );
   }
@@ -491,7 +512,7 @@ class UserDropdown extends PureComponent {
       >
         <div className={styles.userItemContents}>
           <div className={styles.userAvatar}>
-            { this.renderUserAvatar() }
+            {this.renderUserAvatar()}
           </div>
           {<UserName
             {...{
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx
index 0db0a225fdb68c2733af006b26b875f7e7561395..26229f2cf00ec0988b265413ceea44b0c2c19cad 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/component.jsx
@@ -10,6 +10,7 @@ import DropdownContent from '/imports/ui/components/dropdown/content/component';
 import DropdownList from '/imports/ui/components/dropdown/list/component';
 import DropdownListItem from '/imports/ui/components/dropdown/list/item/component';
 import LockViewersContainer from '/imports/ui/components/lock-viewers/container';
+import BreakoutRoom from '/imports/ui/components/actions-bar/create-breakout-room/component';
 import { styles } from './styles';
 
 const propTypes = {
@@ -21,6 +22,11 @@ const propTypes = {
   toggleMuteAllUsersExceptPresenter: PropTypes.func.isRequired,
   toggleStatus: PropTypes.func.isRequired,
   mountModal: PropTypes.func.isRequired,
+  users: PropTypes.arrayOf(Object).isRequired,
+  meetingName: PropTypes.string.isRequired,
+  createBreakoutRoom: PropTypes.func.isRequired,
+  meetingIsBreakout: PropTypes.bool.isRequired,
+  hasBreakoutRoom: PropTypes.bool.isRequired,
 };
 
 const intlMessages = defineMessages({
@@ -68,6 +74,18 @@ const intlMessages = defineMessages({
     id: 'app.userList.userOptions.muteAllExceptPresenterDesc',
     description: 'Mute all except presenter description',
   },
+  createBreakoutRoom: {
+    id: 'app.actionsBar.actionsDropdown.createBreakoutRoom',
+    description: 'Create breakout room option',
+  },
+  createBreakoutRoomDesc: {
+    id: 'app.actionsBar.actionsDropdown.createBreakoutRoomDesc',
+    description: 'Description of create breakout room option',
+  },
+  invitationItem: {
+    id: 'app.invitation.title',
+    description: 'invitation to breakout title',
+  },
 });
 
 class UserOptions extends PureComponent {
@@ -78,55 +96,19 @@ class UserOptions extends PureComponent {
       isUserOptionsOpen: false,
     };
 
+    this.clearStatusId = _.uniqueId('list-item-');
+    this.muteId = _.uniqueId('list-item-');
+    this.muteAllId = _.uniqueId('list-item-');
+    this.lockId = _.uniqueId('list-item-');
+    this.createBreakoutId = _.uniqueId('list-item-');
+
     this.onActionsShow = this.onActionsShow.bind(this);
     this.onActionsHide = this.onActionsHide.bind(this);
     this.alterMenu = this.alterMenu.bind(this);
-  }
-
-  componentWillMount() {
-    const {
-      intl,
-      isMeetingMuted,
-      mountModal,
-      toggleStatus,
-      toggleMuteAllUsers,
-      toggleMuteAllUsersExceptPresenter,
-    } = this.props;
-
-    this.menuItems = _.compact([
-      (<DropdownListItem
-        key={_.uniqueId('list-item-')}
-        icon="clear_status"
-        label={intl.formatMessage(intlMessages.clearAllLabel)}
-        description={intl.formatMessage(intlMessages.clearAllDesc)}
-        onClick={toggleStatus}
-      />),
-      (<DropdownListItem
-        key={_.uniqueId('list-item-')}
-        icon="mute"
-        label={intl.formatMessage(intlMessages.muteAllLabel)}
-        description={intl.formatMessage(intlMessages.muteAllDesc)}
-        onClick={toggleMuteAllUsers}
-      />),
-      (<DropdownListItem
-        key={_.uniqueId('list-item-')}
-        icon="mute"
-        label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)}
-        description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)}
-        onClick={toggleMuteAllUsersExceptPresenter}
-      />),
-      (<DropdownListItem
-        key={_.uniqueId('list-item-')}
-        icon="lock"
-        label={intl.formatMessage(intlMessages.lockViewersLabel)}
-        description={intl.formatMessage(intlMessages.lockViewersDesc)}
-        onClick={() => mountModal(<LockViewersContainer />)}
-      />),
-    ]);
-
-    if (isMeetingMuted) {
-      this.alterMenu();
-    }
+    this.handleCreateBreakoutRoomClick = this.handleCreateBreakoutRoomClick.bind(this);
+    this.onCreateBreakouts = this.onCreateBreakouts.bind(this);
+    this.onInvitationUsers = this.onInvitationUsers.bind(this);
+    this.renderMenuItems = this.renderMenuItems.bind(this);
   }
 
   componentDidUpdate(prevProps) {
@@ -148,6 +130,40 @@ class UserOptions extends PureComponent {
     });
   }
 
+  onCreateBreakouts() {
+    return this.handleCreateBreakoutRoomClick(false);
+  }
+
+  onInvitationUsers() {
+    return this.handleCreateBreakoutRoomClick(true);
+  }
+
+  handleCreateBreakoutRoomClick(isInvitation) {
+    const {
+      createBreakoutRoom,
+      mountModal,
+      meetingName,
+      users,
+      getUsersNotAssigned,
+      getBreakouts,
+      sendInvitation,
+    } = this.props;
+
+    return mountModal(
+      <BreakoutRoom
+        {...{
+          createBreakoutRoom,
+          meetingName,
+          users,
+          getUsersNotAssigned,
+          isInvitation,
+          getBreakouts,
+          sendInvitation,
+        }}
+      />,
+    );
+  }
+
   alterMenu() {
     const {
       intl,
@@ -186,9 +202,92 @@ class UserOptions extends PureComponent {
     }
   }
 
+  renderMenuItems() {
+    const {
+      intl,
+      isMeetingMuted,
+      mountModal,
+      toggleStatus,
+      toggleMuteAllUsers,
+      toggleMuteAllUsersExceptPresenter,
+      meetingIsBreakout,
+      hasBreakoutRoom,
+      getUsersNotAssigned,
+      isUserModerator,
+      users,
+    } = this.props;
+
+    const canCreateBreakout = isUserModerator
+    && !meetingIsBreakout
+    && !hasBreakoutRoom;
+
+    const canInviteUsers = isUserModerator
+    && !meetingIsBreakout
+    && hasBreakoutRoom
+    && getUsersNotAssigned(users).length;
+
+    this.menuItems = _.compact([
+      (<DropdownListItem
+        key={this.clearStatusId}
+        icon="clear_status"
+        label={intl.formatMessage(intlMessages.clearAllLabel)}
+        description={intl.formatMessage(intlMessages.clearAllDesc)}
+        onClick={toggleStatus}
+      />),
+      (<DropdownListItem
+        key={this.muteAllId}
+        icon="mute"
+        label={intl.formatMessage(intlMessages.muteAllLabel)}
+        description={intl.formatMessage(intlMessages.muteAllDesc)}
+        onClick={toggleMuteAllUsers}
+      />),
+      (<DropdownListItem
+        key={this.muteId}
+        icon="mute"
+        label={intl.formatMessage(intlMessages.muteAllExceptPresenterLabel)}
+        description={intl.formatMessage(intlMessages.muteAllExceptPresenterDesc)}
+        onClick={toggleMuteAllUsersExceptPresenter}
+      />),
+      (<DropdownListItem
+        key={this.lockId}
+        icon="lock"
+        label={intl.formatMessage(intlMessages.lockViewersLabel)}
+        description={intl.formatMessage(intlMessages.lockViewersDesc)}
+        onClick={() => mountModal(<LockViewersContainer />)}
+      />),
+      (canCreateBreakout
+        ? (
+          <DropdownListItem
+            key={this.createBreakoutId}
+            icon="rooms"
+            label={intl.formatMessage(intlMessages.createBreakoutRoom)}
+            description={intl.formatMessage(intlMessages.createBreakoutRoomDesc)}
+            onClick={this.onCreateBreakouts}
+          />
+        ) : null
+      ),
+      (canInviteUsers
+        ? (
+          <DropdownListItem
+            icon="rooms"
+            label={intl.formatMessage(intlMessages.invitationItem)}
+            key={this.createBreakoutId}
+            onClick={this.onInvitationUsers}
+          />
+        )
+        : null),
+    ]);
+
+    if (isMeetingMuted) {
+      this.alterMenu();
+    }
+
+    return this.menuItems;
+  }
+
   render() {
-    const { intl } = this.props;
     const { isUserOptionsOpen } = this.state;
+    const { intl } = this.props;
 
     return (
       <Dropdown
@@ -217,7 +316,7 @@ class UserOptions extends PureComponent {
         >
           <DropdownList>
             {
-              this.menuItems
+              this.renderMenuItems()
             }
           </DropdownList>
         </DropdownContent>
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx
index 24d71cfeff48042756b4a29637520d2aa5f2f5af..472655c338b7fe12a5f9dcabea9e718312d4c06a 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-options/container.jsx
@@ -1,67 +1,47 @@
-import React, { PureComponent } from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
 import PropTypes from 'prop-types';
 import Auth from '/imports/ui/services/auth';
+import Service from '/imports/ui/components/actions-bar/service';
 import UserOptions from './component';
 
-
 const propTypes = {
   users: PropTypes.arrayOf(Object).isRequired,
   muteAllUsers: PropTypes.func.isRequired,
   muteAllExceptPresenter: PropTypes.func.isRequired,
   setEmojiStatus: PropTypes.func.isRequired,
   meeting: PropTypes.shape({}).isRequired,
+  currentUser: PropTypes.shape({
+    isModerator: PropTypes.bool.isRequired,
+  }).isRequired,
 };
 
-export default class UserOptionsContainer extends PureComponent {
-  constructor(props) {
-    super(props);
-
-    const { meeting } = this.props;
-
-    this.state = {
-      meetingMuted: meeting.voiceProp.muteOnStart,
-    };
-
-    this.muteMeeting = this.muteMeeting.bind(this);
-    this.muteAllUsersExceptPresenter = this.muteAllUsersExceptPresenter.bind(this);
-    this.handleClearStatus = this.handleClearStatus.bind(this);
-  }
-
-  muteMeeting() {
-    const { muteAllUsers } = this.props;
-    muteAllUsers(Auth.userID);
-  }
-
-  muteAllUsersExceptPresenter() {
-    const { muteAllExceptPresenter } = this.props;
-    muteAllExceptPresenter(Auth.userID);
-  }
-
-  handleClearStatus() {
-    const { users, setEmojiStatus } = this.props;
-
-    users.forEach((id) => {
-      setEmojiStatus(id, 'none');
-    });
-  }
-
-  render() {
-    const { currentUser } = this.props;
-    const currentUserIsModerator = currentUser.isModerator;
-
-    const { meetingMuted } = this.state;
-
-    return (
-      currentUserIsModerator
-        ? (
-          <UserOptions
-            toggleMuteAllUsers={this.muteMeeting}
-            toggleMuteAllUsersExceptPresenter={this.muteAllUsersExceptPresenter}
-            toggleStatus={this.handleClearStatus}
-            isMeetingMuted={meetingMuted}
-          />) : null
-    );
-  }
-}
+const UserOptionsContainer = withTracker((props) => {
+  const {
+    meeting,
+    users,
+    setEmojiStatus,
+    muteAllExceptPresenter,
+    muteAllUsers,
+  } = props;
+
+  return {
+    toggleMuteAllUsers: () => muteAllUsers(Auth.userID),
+    toggleMuteAllUsersExceptPresenter: () => muteAllExceptPresenter(Auth.userID),
+    toggleStatus: () => users.forEach(id => setEmojiStatus(id, 'none')),
+    isMeetingMuted: meeting.voiceProp.muteOnStart,
+    isUserPresenter: Service.isUserPresenter(),
+    isUserModerator: Service.isUserModerator(),
+    createBreakoutRoom: Service.createBreakoutRoom,
+    meetingIsBreakout: Service.meetingIsBreakout(),
+    hasBreakoutRoom: Service.hasBreakoutRoom(),
+    meetingName: Service.meetingName(),
+    users: Service.users(),
+    getBreakouts: Service.getBreakouts,
+    sendInvitation: Service.sendInvitation,
+    getUsersNotAssigned: Service.getUsersNotAssigned,
+  };
+})(UserOptions);
 
 UserOptionsContainer.propTypes = propTypes;
+
+export default UserOptionsContainer;
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..2305f3bf0538a2fc3b46cb16072567a3c562f57a
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/component.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { defineMessages, injectIntl } from 'react-intl';
+import Button from '/imports/ui/components/button/component';
+import cx from 'classnames';
+import { styles } from './styles';
+
+const intlMessages = defineMessages({
+  fullscreenButton: {
+    id: 'app.fullscreenButton.label',
+    description: 'Fullscreen label',
+  },
+});
+
+const FullscreenButtonComponent = ({ intl, handleFullscreen, dark }) => (
+  <div className={cx(styles.wrapper, dark ? styles.dark : null)}>
+    <Button
+      role="button"
+      aria-labelledby="fullscreenButtonLabel"
+      aria-describedby="fullscreenButtonDesc"
+      color="default"
+      icon="fullscreen"
+      size="sm"
+      onClick={handleFullscreen}
+      label={intl.formatMessage(intlMessages.fullscreenButton)}
+      hideLabel
+      circle
+      className={styles.button}
+    />
+  </div>
+);
+
+export default injectIntl(FullscreenButtonComponent);
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..644ed81a284e53a221634581d189ff504ea03ba7
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/fullscreen-button/styles.scss
@@ -0,0 +1,34 @@
+:root {
+  --color-transparent: #ff000000;
+  ::-webkit-media-controls {
+    display:none !important;
+  }
+}
+
+.wrapper {
+  position: absolute;
+  right: 0;
+  background-color: var(--color-transparent);
+  cursor: pointer;
+  border: none !important;
+}
+
+.button {
+  span, span:active, span:hover {
+    background-color: var(--color-transparent) !important;
+    border: none !important;
+    i {
+      border: none !important;
+      background-color: var(--color-transparent) !important;
+      font-weight: bold !important;
+    }
+  }
+}
+
+.dark {
+  bottom: 0;
+}
+
+.dark .button span i {
+  color: var(--color-black);
+}
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..f56bad50e5c61bf47c03b3a85c223325d2be8a45
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import cx from 'classnames';
+import Button from '/imports/ui/components/button/component';
+import { defineMessages, injectIntl, intlShape } from 'react-intl';
+import { styles } from './styles';
+
+const intlMessages = defineMessages({
+  joinVideo: {
+    id: 'app.video.joinVideo',
+    description: 'Join video button label',
+  },
+  leaveVideo: {
+    id: 'app.video.leaveVideo',
+    description: 'Leave video button label',
+  },
+  videoButtonDesc: {
+    id: 'app.video.videoButtonDesc',
+    description: 'video button description',
+  },
+  videoDisabled: {
+    id: 'app.video.videoDisabled',
+    description: 'video disabled label',
+  },
+});
+
+
+const propTypes = {
+  intl: intlShape.isRequired,
+  isSharingVideo: PropTypes.bool.isRequired,
+};
+
+const JoinVideoButton = ({
+  intl,
+  isSharingVideo,
+  isDisabled,
+  handleJoinVideo,
+  handleCloseVideo,
+}) => {
+
+  return (
+    <Button
+      label={isDisabled ?
+        intl.formatMessage(intlMessages.videoDisabled)
+        :
+        (isSharingVideo ?
+          intl.formatMessage(intlMessages.leaveVideo)
+          :
+          intl.formatMessage(intlMessages.joinVideo)
+        )
+      }
+      className={cx(styles.button, isSharingVideo || styles.ghostButton)}
+      onClick={isSharingVideo ? handleCloseVideo : handleJoinVideo}
+      hideLabel
+      aria-label={intl.formatMessage(intlMessages.videoButtonDesc)}
+      color={isSharingVideo ? 'primary' : 'default'}
+      icon={isSharingVideo ? 'video' : 'video_off'}
+      ghost={!isSharingVideo}
+      size="lg"
+      circle
+      disabled={isDisabled}
+    />
+  );
+};
+JoinVideoButton.propTypes = propTypes;
+export default injectIntl(JoinVideoButton);
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..b21d680e1f9c8995e529ffae76a5871180d959cc
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { withTracker } from 'meteor/react-meteor-data';
+import { injectIntl } from 'react-intl';
+import { withModalMounter } from '/imports/ui/components/modal/service';
+import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
+import JoinVideoButton from './component';
+import VideoButtonService from './service';
+
+const JoinVideoOptionsContainer = (props) => {
+  const {
+    isSharingVideo,
+    isDisabled,
+    handleJoinVideo,
+    handleCloseVideo,
+    baseName,
+    intl,
+    mountModal,
+    ...restProps
+  } = props;
+
+  const mountPreview = () => { mountModal(<VideoPreviewContainer />); };
+
+  return <JoinVideoButton {...{ handleJoinVideo: mountPreview, handleCloseVideo, isSharingVideo, isDisabled, ...restProps }} />;
+};
+
+export default withModalMounter(injectIntl(withTracker(() => ({
+  baseName: VideoButtonService.baseName,
+  isSharingVideo: VideoButtonService.isSharingVideo(),
+  isDisabled: VideoButtonService.isDisabled(),
+  videoShareAllowed: VideoButtonService.videoShareAllowed(),
+}))(JoinVideoOptionsContainer)));
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/service.js
similarity index 86%
rename from bigbluebutton-html5/imports/ui/components/video-provider/video-menu/service.js
rename to bigbluebutton-html5/imports/ui/components/video-provider/video-button/service.js
index f17126feb08859f34f89af554897e86bc721addb..a25e62ccf4197c2ced022eba5c9212b2b2e0a855 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/service.js
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/service.js
@@ -2,7 +2,6 @@ import Settings from '/imports/ui/services/settings';
 import mapUser from '/imports/ui/services/user/mapUser';
 import Auth from '/imports/ui/services/auth';
 import Users from '/imports/api/users/';
-import MediaService from '/imports/ui/components/media/service';
 import VideoService from '../service';
 
 const baseName = Meteor.settings.public.app.basename;
@@ -37,7 +36,5 @@ export default {
   isSharingVideo,
   isDisabled,
   baseName,
-  toggleSwapLayout: MediaService.toggleSwapLayout,
-  swapLayoutAllowed: MediaService.shouldEnableSwapLayout,
   videoShareAllowed,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/styles.scss b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/styles.scss
similarity index 100%
rename from bigbluebutton-html5/imports/ui/components/video-provider/video-menu/styles.scss
rename to bigbluebutton-html5/imports/ui/components/video-provider/video-button/styles.scss
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
index 92c434dc45388958c4ea20a21c0b647e2d24b300..497a95c1b88756b1294cc1643fc70e96602f5f66 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-list/video-list-item/component.jsx
@@ -11,6 +11,7 @@ import DropdownListItem from '/imports/ui/components/dropdown/list/item/componen
 import Icon from '/imports/ui/components/icon/component';
 import Button from '/imports/ui/components/button/component';
 import VideoListItemStats from './video-list-item-stats/component';
+import FullscreenButton from '../../fullscreen-button/component';
 import { styles } from '../styles';
 
 const intlMessages = defineMessages({
@@ -96,6 +97,13 @@ class VideoListItem extends Component {
     ]);
   }
 
+  renderFullscreenButton() {
+    const full = () => {
+      this.videoTag.requestFullscreen();
+    };
+    return <FullscreenButton handleFullscreen={full} />;
+  }
+
   render() {
     const { showStats, stats } = this.state;
     const { user } = this.props;
@@ -131,6 +139,7 @@ class VideoListItem extends Component {
           { user.isListenOnly ? <Icon className={styles.voice} iconName="listen" /> : null }
         </div>
         { showStats ? <VideoListItemStats toggleStats={this.toggleStats} stats={stats} /> : null }
+        { this.renderFullscreenButton() }
       </div>
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/component.jsx
deleted file mode 100755
index 88ed1f14a20b9de2aa2fec811ccc76b07f1eaa65..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/component.jsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import _ from 'lodash';
-import cx from 'classnames';
-import Button from '/imports/ui/components/button/component';
-import { defineMessages, injectIntl, intlShape } from 'react-intl';
-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 { styles } from './styles';
-
-const intlMessages = defineMessages({
-  videoMenu: {
-    id: 'app.video.videoMenu',
-    description: 'video menu label',
-  },
-  videoMenuDesc: {
-    id: 'app.video.videoMenuDesc',
-    description: 'video menu description',
-  },
-  videoMenuDisabled: {
-    id: 'app.video.videoMenuDisabled',
-    description: 'video menu label',
-  },
-});
-
-
-const propTypes = {
-  intl: intlShape.isRequired,
-  isSharingVideo: PropTypes.bool.isRequired,
-  videoItems: PropTypes.arrayOf(PropTypes.object).isRequired,
-};
-
-const JoinVideoOptions = ({
-  intl,
-  isSharingVideo,
-  videoItems,
-  videoShareAllowed,
-}) => {
-  const menuItems = videoItems
-    .filter(item => !item.disabled)
-    .map(item =>
-      (
-        <DropdownListItem
-          key={_.uniqueId('video-menu-')}
-          className={styles.item}
-          description={item.description}
-          onClick={item.click}
-          tabIndex={-1}
-          id={item.id}
-        >
-          <img src={item.iconPath} className={styles.imageSize} alt="video menu icon" />
-          <span className={styles.label}>{item.label}</span>
-        </DropdownListItem>
-      ));
-  return (
-    <Dropdown
-      autoFocus
-    >
-      <DropdownTrigger tabIndex={0}>
-        <Button
-          label={!videoShareAllowed ?
-            intl.formatMessage(intlMessages.videoMenuDisabled)
-            : intl.formatMessage(intlMessages.videoMenu)
-          }
-          className={cx(styles.button, isSharingVideo || styles.ghostButton)}
-          onClick={() => null}
-          hideLabel
-          aria-label={intl.formatMessage(intlMessages.videoMenuDesc)}
-          color={isSharingVideo ? 'primary' : 'default'}
-          icon={isSharingVideo ? 'video' : 'video_off'}
-          ghost={!isSharingVideo}
-          size="lg"
-          circle
-          disabled={!videoShareAllowed}
-        />
-      </DropdownTrigger>
-      <DropdownContent placement="top" >
-        <DropdownList horizontal>
-          {menuItems}
-        </DropdownList>
-      </DropdownContent>
-    </Dropdown>
-  );
-};
-JoinVideoOptions.propTypes = propTypes;
-export default injectIntl(JoinVideoOptions);
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/container.jsx
deleted file mode 100755
index 8e453a95ec9d0916cfb65b827475d652a04ca48c..0000000000000000000000000000000000000000
--- a/bigbluebutton-html5/imports/ui/components/video-provider/video-menu/container.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-import { withTracker } from 'meteor/react-meteor-data';
-import { defineMessages, injectIntl } from 'react-intl';
-import { withModalMounter } from '/imports/ui/components/modal/service';
-import VideoPreviewContainer from '/imports/ui/components/video-preview/container';
-import JoinVideoOptions from './component';
-import VideoMenuService from './service';
-
-const intlMessages = defineMessages({
-  joinVideo: {
-    id: 'app.video.joinVideo',
-    description: 'Join video button label',
-  },
-  leaveVideo: {
-    id: 'app.video.leaveVideo',
-    description: 'Leave video button label',
-  },
-  swapCam: {
-    id: 'app.video.swapCam',
-    description: 'Swap cam button label',
-  },
-  swapCamDesc: {
-    id: 'app.video.swapCamDesc',
-    description: 'Swap cam button description',
-  },
-});
-
-const JoinVideoOptionsContainer = (props) => {
-  const {
-    isSharingVideo,
-    isDisabled,
-    handleJoinVideo,
-    handleCloseVideo,
-    toggleSwapLayout,
-    swapLayoutAllowed,
-    baseName,
-    intl,
-    mountModal,
-    ...restProps
-  } = props;
-  const videoItems = [
-    {
-      iconPath: `${baseName}/resources/images/video-menu/icon-swap.svg`,
-      description: intl.formatMessage(intlMessages.swapCamDesc),
-      label: intl.formatMessage(intlMessages.swapCam),
-      disabled: !swapLayoutAllowed,
-      click: toggleSwapLayout,
-      id: 'swap-button',
-    },
-    {
-      iconPath: `${baseName}/resources/images/video-menu/icon-webcam-off.svg`,
-      description: intl.formatMessage(intlMessages[isSharingVideo ? 'leaveVideo' : 'joinVideo']),
-      label: intl.formatMessage(intlMessages[isSharingVideo ? 'leaveVideo' : 'joinVideo']),
-      disabled: isDisabled && !isSharingVideo,
-      click: isSharingVideo ? handleCloseVideo : () => { mountModal(<VideoPreviewContainer />); },
-      id: isSharingVideo ? 'leave-video-button' : 'join-video-button',
-    },
-  ];
-
-  return <JoinVideoOptions {...{ videoItems, isSharingVideo, ...restProps }} />;
-};
-
-export default withModalMounter(injectIntl(withTracker(() => ({
-  baseName: VideoMenuService.baseName,
-  isSharingVideo: VideoMenuService.isSharingVideo(),
-  isDisabled: VideoMenuService.isDisabled(),
-  videoShareAllowed: VideoMenuService.videoShareAllowed(),
-  toggleSwapLayout: VideoMenuService.toggleSwapLayout,
-  swapLayoutAllowed: VideoMenuService.swapLayoutAllowed(),
-}))(JoinVideoOptionsContainer)));
diff --git a/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js b/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js
index 7a5a550b3105c162ea59cfb6f6ada89ae0a1c610..f4f8cc0eac8ab93e34037163ec3062b86f91b96c 100644
--- a/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js
+++ b/bigbluebutton-html5/imports/utils/ios-webview-audio-polyfills.js
@@ -4,7 +4,7 @@ const iosWebviewAudioPolyfills = function () {
       window.RTCPeerConnection.prototype.getRemoteStreams = function () {
         return this._remoteStreams ? this._remoteStreams : [];
       };
-    } 
+    }
     if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
       Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
         get: function get() {
diff --git a/bigbluebutton-html5/imports/utils/safari-webrtc.js b/bigbluebutton-html5/imports/utils/safari-webrtc.js
index 524ceb737bacf40d57f0ab7c945d7ed3be7ffe48..e7dcb725584bf2824ee6cfd61e43b317f2b81977 100644
--- a/bigbluebutton-html5/imports/utils/safari-webrtc.js
+++ b/bigbluebutton-html5/imports/utils/safari-webrtc.js
@@ -37,7 +37,7 @@ export function canGenerateIceCandidates() {
       resolve();
       return;
     }
-    
+
     getIceServersList().catch((e) => {
       reject();
     }).then((iceServersReceived) => {
@@ -53,16 +53,16 @@ export function canGenerateIceCandidates() {
           Session.set('canGenerateIceCandidates', true);
           resolve();
         }
-      }
+      };
 
       pc.onicegatheringstatechange = function (e) {
         if (e.currentTarget.iceGatheringState == 'complete' && countIceCandidates == 0) reject();
-      }
+      };
 
-      setTimeout(function () {
+      setTimeout(() => {
         pc.close();
         if (!countIceCandidates) reject();
-    }, 5000);
+      }, 5000);
 
       const p = pc.createOffer({ offerToReceiveVideo: true });
       p.then((answer) => { pc.setLocalDescription(answer); });
@@ -75,7 +75,7 @@ export function tryGenerateIceCandidates() {
     canGenerateIceCandidates().then((ok) => {
       resolve();
     }).catch((e) => {
-      navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(function (stream) {
+      navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => {
         canGenerateIceCandidates().then((ok) => {
           resolve();
         }).catch((e) => {
@@ -86,4 +86,4 @@ export function tryGenerateIceCandidates() {
       });
     });
   });
-}
\ No newline at end of file
+}
diff --git a/bigbluebutton-html5/private/locales/de.json b/bigbluebutton-html5/private/locales/de.json
index ba68eac7d1a9682f9256a7642f5e619567320e68..5e074a700f48436718f61ce12aaefcab0f6dd6e2 100644
--- a/bigbluebutton-html5/private/locales/de.json
+++ b/bigbluebutton-html5/private/locales/de.json
@@ -18,9 +18,13 @@
     "app.chat.label": "Chat",
     "app.chat.emptyLogLabel": "Chat-Log ist leer",
     "app.chat.clearPublicChatMessage": "Der öffentliche Chatverlauf wurde durch einen Moderator gelöscht",
+    "app.note.title": "Geteilte Notizen",
+    "app.note.label": "Notiz",
+    "app.note.hideNoteLabel": "Notiz verbergen",
     "app.userList.usersTitle": "Teilnehmer",
     "app.userList.participantsTitle": "Teilnehmer",
     "app.userList.messagesTitle": "Nachrichten",
+    "app.userList.notesTitle": "Notizen",
     "app.userList.presenter": "Präsentator",
     "app.userList.you": "Sie",
     "app.userList.locked": "Gesperrt",
@@ -39,6 +43,17 @@
     "app.userList.userAriaLabel": "{0} {1} {2}  Status {3}",
     "app.userList.menu.promoteUser.label": "Zum Moderator befördern",
     "app.userList.menu.demoteUser.label": "Zum Zuschauer zurückstufen",
+    "app.userList.userOptions.manageUsersLabel": "Teilnehmer verwalten",
+    "app.userList.userOptions.muteAllLabel": "Alle Teilnehmer stummschalten",
+    "app.userList.userOptions.muteAllDesc": "Schaltet alle Teilnehmer der Konferenz stumm",
+    "app.userList.userOptions.clearAllLabel": "Lösche alle Statusicons",
+    "app.userList.userOptions.clearAllDesc": "Löscht alle Statusicons der Teilnehmer",
+    "app.userList.userOptions.muteAllExceptPresenterLabel": "Schalte alle Teilnehmer außer den Präsentator stumm",
+    "app.userList.userOptions.muteAllExceptPresenterDesc": "Schaltet alle Teilnehmer außer den Präsentator stumm",
+    "app.userList.userOptions.unmuteAllLabel": "Konferenz-Stummschaltung aufheben",
+    "app.userList.userOptions.unmuteAllDesc": "Hebt die Konferenz-Stummschaltung auf",
+    "app.userList.userOptions.lockViewersLabel": "Zuschauerrechte einschränken",
+    "app.userList.userOptions.lockViewersDesc": "Schränkt bestimmte Funktionen der Konferenzteilnehmer ein",
     "app.media.label": "Media",
     "app.media.screenshare.start": "Bildschirmfreigabe wurde gestartet",
     "app.media.screenshare.end": "Bildschirmfreigabe wurde gestoppt",
@@ -57,6 +72,11 @@
     "app.presentation.presentationToolbar.fitScreenDesc": "Gesamte Folie darstellen",
     "app.presentation.presentationToolbar.zoomLabel": "Vergrößerungsgrad",
     "app.presentation.presentationToolbar.zoomDesc": "Vergrößerungsstufe der Präsentation ändern",
+    "app.presentation.presentationToolbar.zoomInLabel": "Reinzoomen",
+    "app.presentation.presentationToolbar.zoomInDesc": "In die Präsentation hinein zoomen",
+    "app.presentation.presentationToolbar.zoomOutLabel": "Rauszoomen",
+    "app.presentation.presentationToolbar.zoomOutDesc": "Aus der Präsentation heraus zoomen",
+    "app.presentation.presentationToolbar.zoomIndicator": "Zoomstufe anzeigen",
     "app.presentation.presentationToolbar.fitToWidth": "An Breite anpassen",
     "app.presentation.presentationToolbar.goToSlide": "Folie {0}",
     "app.presentationUploder.title": "Präsentation",
@@ -66,10 +86,13 @@
     "app.presentationUploder.dismissLabel": "Abbrechen",
     "app.presentationUploder.dismissDesc": "Fenster schließen und Änderungen verwerfen",
     "app.presentationUploder.dropzoneLabel": "Hochzuladende Dateien hier hin ziehen",
+    "app.presentationUploder.dropzoneImagesLabel": "Bilder hier hinziehen, um sie hochzuladen",
     "app.presentationUploder.browseFilesLabel": "oder nach Dateien suchen",
+    "app.presentationUploder.browseImagesLabel": "oder auf der Festplatte nach Bildern suchen",
     "app.presentationUploder.fileToUpload": "Hochzuladende Datei...",
     "app.presentationUploder.currentBadge": "Aktuell",
     "app.presentationUploder.genericError": "Ups, irgendwas ist schief gelaufen",
+    "app.presentationUploder.rejectedError": "Einige der ausgewählten Dateien wurden zurückgewiesen. Bitte prüfen Sie die Datei-Mime-Typen",
     "app.presentationUploder.upload.progress": "Hochladen ({0}%)",
     "app.presentationUploder.upload.413": "Die Datei ist zu groß",
     "app.presentationUploder.conversion.conversionProcessingSlides": "Verarbeite Seite {0} von {1}",
@@ -79,8 +102,25 @@
     "app.presentationUploder.conversion.generatingSvg": "Erzeuge SVG Bilder...",
     "app.presentationUploder.conversion.pageCountExceeded": "Ups, die Seitenanzahl überschreitet das Limit",
     "app.presentationUploder.conversion.timeout": "Ups, die Konvertierung dauert zu lange",
+    "app.poll.pollPaneTitle": "Umfrage",
+    "app.poll.hidePollDesc": "Versteckt das Umfragemenü",
+    "app.poll.customPollInstruction": "Um selbst erstellte Umfrage zu erstellen, klicken Sie die untenstehende Schaltfläche und geben Sie Ihre Optionen ein",
+    "app.poll.quickPollInstruction": "Wählen Sie eine Schnellumfrageoption aus und starten Sie die Umfrage.",
+    "app.poll.customPollLabel": "Selbst erstellte Umfrage",
+    "app.poll.startCustomLabel": "Selbst erstellte Umfrage starten",
+    "app.poll.activePollInstruction": "Lassen Sie dieses Fenster solange geöffnet, bis alle Antworten abgegeben wurden. Wählen Sie 'Umfrage veröffentlichen' oder navigieren Sie einen Schritt zurück, um die Umfrage zu beenden.",
+    "app.poll.publishLabel": "Umfrage veröffentlichen",
+    "app.poll.backLabel": "Zurück zu den Umfrageoptionen",
     "app.poll.closeLabel": "Schließen",
+    "app.poll.customPlaceholder": "Umfrageoption hinzufügen",
+    "app.poll.tf": "Richtig / Falsch",
+    "app.poll.yn": "Ja / Nein",
+    "app.poll.a2": "A / B",
+    "app.poll.a3": "A / B / C",
+    "app.poll.a4": "A / B / C / D",
+    "app.poll.a5": "A / B / C / D / E",
     "app.poll.liveResult.usersTitle": "Teilnehmer",
+    "app.poll.liveResult.responsesTitle": "Antwort",
     "app.polling.pollingTitle": "Umfrage Optionen",
     "app.polling.pollAnswerLabel": "Umfrageantwort {0}",
     "app.polling.pollAnswerDesc": "Diese Option auswählen für Umfrage {0}",
@@ -102,6 +142,8 @@
     "app.navBar.settingsDropdown.hotkeysDesc": "Liste verfügbarer Tastaturkürzel",
     "app.navBar.settingsDropdown.helpLabel": "Hilfe",
     "app.navBar.settingsDropdown.helpDesc": "Verlinkt zu den Videoanleitungen",
+    "app.navBar.settingsDropdown.endMeetingDesc": "Beendet die aktuelle Konferenz",
+    "app.navBar.settingsDropdown.endMeetingLabel": "Beende Konferenz",
     "app.navBar.userListToggleBtnLabel": "Teilnehmerliste umschalten",
     "app.navBar.toggleUserList.ariaLabel": "Teilnehmer/Nachrichten-Umschalter",
     "app.navBar.toggleUserList.newMessages": "mit Benachrichtigung für neue Nachrichten",
@@ -110,6 +152,8 @@
     "app.navBar.recording.off": "Keine Aufnahme",
     "app.leaveConfirmation.confirmLabel": "Verlassen",
     "app.leaveConfirmation.confirmDesc": "Hiermit verlassen Sie Konferenz",
+    "app.endMeeting.title": "Beende Konferenz",
+    "app.endMeeting.description": "Sind Sie sicher, dass Sie die Konferenz beenden wollen?",
     "app.endMeeting.yesLabel": "Ja",
     "app.endMeeting.noLabel": "Nein",
     "app.about.title": "Versionsinfo",
@@ -198,6 +242,12 @@
     "app.actionsBar.actionsDropdown.initPollDesc": "Eine Umfrage starten",
     "app.actionsBar.actionsDropdown.desktopShareDesc": "Ihren Bildschirm mit anderen teilen",
     "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Bildschirmfreigabe beenden mit",
+    "app.actionsBar.actionsDropdown.pollBtnLabel": "Umfrage starten",
+    "app.actionsBar.actionsDropdown.pollBtnDesc": "Umschalten des Umfragemenüs",
+    "app.actionsBar.actionsDropdown.createBreakoutRoom": "Breakout-Räume erstellen",
+    "app.actionsBar.actionsDropdown.createBreakoutRoomDesc": "ermöglicht die aktuelle Konferenz in mehrere Räume aufzuteilen",
+    "app.actionsBar.actionsDropdown.takePresenter": "Rolle des Präsentators übernehmen",
+    "app.actionsBar.actionsDropdown.takePresenterDesc": "Sich selber zum neuen Präsentator machen",
     "app.actionsBar.emojiMenu.statusTriggerLabel": "Status setzen",
     "app.actionsBar.emojiMenu.awayLabel": "Abwesend",
     "app.actionsBar.emojiMenu.awayDesc": "Ihren Status auf abwesend setzen",
@@ -293,6 +343,7 @@
     "app.error.404": "Nicht gefunden",
     "app.error.401": "Nicht erlaubt",
     "app.error.403": "Verboten",
+    "app.error.400": "Ungültige Anfrage",
     "app.error.leaveLabel": "Erneut einloggen",
     "app.guest.waiting": "Warte auf Erlaubnis zur Konferenzteilnahme",
     "app.toast.breakoutRoomEnded": "Breakout Raum wurde beendet. Bitte klicken Sie aufs Mikrofon-Icon um wieder der Audiokonferenz im Hauptraum beizutreten",
@@ -315,13 +366,28 @@
     "app.shortcut-help.closePrivateChat": "Privaten Chat schließen",
     "app.shortcut-help.openActions": "Aktionsmenü öffnen",
     "app.shortcut-help.openStatus": "Statusmenü öffnen",
+    "app.lock-viewers.title": "Zuschauerfunktionen einschränken",
+    "app.lock-viewers.description": "Diese Optionen erlauben es Ihnen, bestimmte den Zuschauern zur Verfügung stehende Funktionen einzuschränken - so kann beispielsweise der private Chat unterbunden werden (Diese Einschränkungen gelten nicht für Moderatoren)",
+    "app.lock-viewers.featuresLable": "Funktion",
+    "app.lock-viewers.lockStatusLabel": "Aktiviert",
     "app.lock-viewers.webcamLabel": "Webcam",
+    "app.lock-viewers.otherViewersWebcamLabel": "Nur Moderatoren sehen Webcams",
     "app.lock-viewers.microphoneLable": "Mit Mikrofon",
     "app.lock-viewers.PublicChatLabel": "Öffentlicher Chat",
     "app.lock-viewers.PrivateChatLable": "Privater Chat",
     "app.lock-viewers.Layout": "Layout",
+    "app.recording.startTitle": "Aufzeichnung starten",
+    "app.recording.stopTitle": "Aufzeichnung stoppen",
+    "app.recording.startDescription": "Klicken Sie erneut auf das Icon um die Aufzeichnung zu beenden.",
+    "app.recording.stopDescription": "Sind Sie sicher, dass Sie die Aufzeichnung der Konferenz beenden wollen? Sie können die Aufzeichnung jederzeit durch erneutes Klicken auf das Icon fortsetzen.",
+    "app.videoPreview.cameraLabel": "Kamera",
     "app.videoPreview.cancelLabel": "Abbrechen",
     "app.videoPreview.closeLabel": "Schließen",
+    "app.videoPreview.startSharingLabel": "Kamerafreigabe starten",
+    "app.videoPreview.webcamOptionLabel": "Webcam auswählen",
+    "app.videoPreview.webcamPreviewLabel": "Webcamvorschau",
+    "app.videoPreview.webcamSettingsTitle": "Webcam-Einstellungen",
+    "app.videoPreview.webcamNotFoundLabel": "Keine Webcam gefunden",
     "app.video.joinVideo": "Webcam freigeben",
     "app.video.leaveVideo": "Webcam stoppen",
     "app.video.iceCandidateError": "Fehler beim Hinzufügen vom ice candidate",
@@ -332,6 +398,7 @@
     "app.video.notAllowed": "Freigabeerlaubnis für die Webcam nicht erteilt, prüfen Sie Ihre Browerberechtigungen",
     "app.video.notSupportedError": "Webcam kann nur über sichere Verbindung freigegeben werden, stellen Sie sicher, dass das SSL-Zertifikat gültig ist",
     "app.video.notReadableError": "Konnte nicht auf die Webcam zugreifen. Stellen Sie sicher, dass kein anderes Programm auf die Webcam zugreift",
+    "app.video.mediaFlowTimeout1020": "Fehler 1020: Medienverbindung zum Server konnte nicht hergestellt werden",
     "app.video.swapCam": "Wechseln",
     "app.video.swapCamDesc": "Ausrichtung der Webcams wechseln",
     "app.video.videoMenu": "Videomenü",
@@ -359,10 +426,12 @@
     "app.sfu.mediaServerRequestTimeout2003": "Fehler 2003: Zeitüberschreitung bei Anfragen an den Mediaserver",
     "app.sfu.serverIceGatheringFailed2021": "Fehler 2021: Mediaserver kann ICE Kandidaten nicht erfassen",
     "app.sfu.serverIceGatheringFailed2022": "Fehler 2022: Mediaserver ICE Verbindung fehlgeschlagen",
+    "app.sfu.mediaGenericError2200": "Fehler 2200: Medienserver konnte die Anfrage nicht verarbeiten",
     "app.sfu.invalidSdp2202":"Fehler 2202: Client hat ungültigen SDP generiert",
     "app.sfu.noAvailableCodec2203": "Fehler 2203: Server konnte keinen passenden Codec finden",
     "app.meeting.endNotification.ok.label": "OK",
     "app.whiteboard.toolbar.tools": "Werkzeuge",
+    "app.whiteboard.toolbar.tools.hand": "Verschieben",
     "app.whiteboard.toolbar.tools.pencil": "Stift",
     "app.whiteboard.toolbar.tools.rectangle": "Rechteck",
     "app.whiteboard.toolbar.tools.triangle": "Dreieck",
@@ -399,8 +468,37 @@
     "app.videoDock.webcamFocusDesc": "Ausgewählte Webcam vergrößern",
     "app.videoDock.webcamUnfocusLabel": "Normalgröße",
     "app.videoDock.webcamUnfocusDesc": "Ausgewählte Webcam auf Normalgröße verkleinern",
+    "app.invitation.title": "Breakoutraum-Einladung",
+    "app.invitation.confirm": "Einladen",
+    "app.createBreakoutRoom.title": "Breakout-Räume",
+    "app.createBreakoutRoom.breakoutRoomLabel": "Breakout-Räume {0}",
+    "app.createBreakoutRoom.generatingURL": "Erzeuge URL",
+    "app.createBreakoutRoom.generatedURL": "Erzeugt",
+    "app.createBreakoutRoom.duration": "Dauer {0}",
+    "app.createBreakoutRoom.room": "Raum {0}",
+    "app.createBreakoutRoom.notAssigned": "Nicht zugewiesen ({0})",
+    "app.createBreakoutRoom.join": "Raum beitreten",
     "app.createBreakoutRoom.joinAudio": "Audio starten",
-    "app.externalVideo.close": "Schließen"
+    "app.createBreakoutRoom.returnAudio": "Return audio",
+    "app.createBreakoutRoom.confirm": "Erstellen",
+    "app.createBreakoutRoom.numberOfRooms": "Anzahl der Räume",
+    "app.createBreakoutRoom.durationInMinutes": "Dauer (Minuten)",
+    "app.createBreakoutRoom.randomlyAssign": "Zufällige Raumzuweisung",
+    "app.createBreakoutRoom.endAllBreakouts": "Alle Breakout-Räume beenden",
+    "app.createBreakoutRoom.roomName": "{0} (Raum - {1})",
+    "app.createBreakoutRoom.doneLabel": "Fertig",
+    "app.createBreakoutRoom.nextLabel": "Nächster",
+    "app.createBreakoutRoom.addParticipantLabel": "+ Teilnehmer hinzufügen",
+    "app.createBreakoutRoom.freeJoin": "Den Teilnehmern erlauben, sich selber einen Breakout-Raum auszusuchen.",
+    "app.createBreakoutRoom.leastOneWarnBreakout": "Jedem Breakout-Raum muss wenigstens ein Teilnehmer zugeordnet sein.",
+    "app.createBreakoutRoom.modalDesc": "Vervollständigen Sie die folgenden Schritte um Breakout-Räume zu erzeugen. Verteilen Sie die Teilnehmer auf die Räume.",
+    "app.externalVideo.start": "Neues Video teilen",
+    "app.externalVideo.stop": "Teilen des Videos beenden",
+    "app.externalVideo.title": "Youtube-Video teilen",
+    "app.externalVideo.input": "Youtube-Video URL",
+    "app.externalVideo.urlError": "Dies ist kein gültiges Youtube-Video",
+    "app.externalVideo.close": "Schließen",
+    "app.actionsBar.actionsDropdown.shareExternalVideo": "Youtube-Video teilen"
 
 }
 
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 25487bd2f9dcec89dfb1da44cd435a2eac848a51..533031230db2f8837cb6d9cfad03c46f980ee5a8 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -43,6 +43,8 @@
     "app.userList.userAriaLabel": "{0} {1} {2}  Status {3}",
     "app.userList.menu.promoteUser.label": "Promote to moderator",
     "app.userList.menu.demoteUser.label": "Demote to viewer",
+    "app.userList.menu.unlockUser.label": "Unlock {0}",
+    "app.userList.menu.lockUser.label": "Lock {0}",
     "app.userList.userOptions.manageUsersLabel": "Manage users",
     "app.userList.userOptions.muteAllLabel": "Mute all users",
     "app.userList.userOptions.muteAllDesc": "Mutes all users in the meeting",
@@ -60,6 +62,7 @@
     "app.media.screenshare.safariNotSupported": "Screenshare is currently not supported by Safari. Please, use Firefox or Google Chrome.",
     "app.meeting.ended": "This session has ended",
     "app.meeting.endedMessage": "You will be forwarded back to the home screen",
+    "app.presentation.close": "Close presentation",
     "app.presentation.presentationToolbar.prevSlideLabel": "Previous slide",
     "app.presentation.presentationToolbar.prevSlideDesc": "Change the presentation to the previous slide",
     "app.presentation.presentationToolbar.nextSlideLabel": "Next slide",
@@ -169,6 +172,8 @@
     "app.actionsBar.camOffLabel": "Cam Off",
     "app.actionsBar.raiseLabel": "Raise",
     "app.actionsBar.label": "Actions Bar",
+    "app.actionsBar.actionsDropdown.restorePresentationLabel": "Restore Presentation",
+    "app.actionsBar.actionsDropdown.restorePresentationDesc": "Button to restore presentation after it has been closed",
     "app.submenu.application.applicationSectionTitle": "Application",
     "app.submenu.application.animationsLabel": "Animations",
     "app.submenu.application.audioAlertLabel": "Audio Alerts for Chat",
@@ -200,6 +205,7 @@
     "app.submenu.closedCaptions.fontSizeOptionLabel": "Choose Font size",
     "app.submenu.closedCaptions.backgroundColorLabel": "Background color",
     "app.submenu.closedCaptions.fontColorLabel": "Font color",
+    "app.submenu.closedCaptions.noLocaleSelected": "Locale is not selected",
     "app.submenu.participants.muteAllLabel": "Mute all except the presenter",
     "app.submenu.participants.lockAllLabel": "Lock all participants",
     "app.submenu.participants.lockItemLabel": "Participants {0}",
@@ -402,6 +408,8 @@
     "app.video.mediaFlowTimeout1020": "Error 1020: media could not reach the server",
     "app.video.swapCam": "Swap",
     "app.video.swapCamDesc": "swap the direction of webcams",
+    "app.video.videoDisabled": "Webcam is disabled",
+    "app.video.videoButtonDesc": "Join video button",
     "app.video.videoMenu": "Video menu",
     "app.video.videoMenuDisabled": "Video menu Webcam is disabled in Settings",
     "app.video.videoMenuDesc": "Open video menu dropdown",
@@ -420,6 +428,7 @@
     "app.video.stats.rtt": "RTT",
     "app.video.stats.encodeUsagePercent": "Encode usage",
     "app.video.stats.currentDelay": "Current delay",
+    "app.fullscreenButton.label": "Make element fullscreen",
     "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.",
diff --git a/bigbluebutton-html5/private/locales/es.json b/bigbluebutton-html5/private/locales/es.json
new file mode 100644
index 0000000000000000000000000000000000000000..35e0cfa99cc9cf9669609b9770605665b6bfb964
--- /dev/null
+++ b/bigbluebutton-html5/private/locales/es.json
@@ -0,0 +1,297 @@
+{
+    "app.home.greeting": "¡Bienvenido {0}! Tu presentación iniciará en breve...",
+    "app.chat.submitLabel": "Enviar mensaje",
+    "app.chat.errorMinMessageLength": "El mensaje es {0} caracteres mas corto de lo esperado",
+    "app.chat.errorMaxMessageLength": "El mensaje es {0} caracteres mas largo de lo esperado",
+    "app.chat.inputLabel": "Entrada de mensaje para chat {0}",
+    "app.chat.inputPlaceholder": "Mensaje {0}",
+    "app.chat.titlePublic": "Chat público",
+    "app.chat.titlePrivate": "Chat privado con {0}",
+    "app.chat.partnerDisconnected": "{0} ha abandonado la reunión",
+    "app.chat.closeChatLabel": "Cerrar {0}",
+    "app.chat.hideChatLabel": "Ocultar  {0}",
+    "app.chat.moreMessages": "Más mensajes abajo",
+    "app.chat.dropdown.options": "Opciones de chat",
+    "app.chat.dropdown.clear": "Limpiar",
+    "app.chat.dropdown.copy": "Copiar",
+    "app.chat.dropdown.save": "Guardar",
+    "app.chat.label": "Chat",
+    "app.chat.emptyLogLabel": "Registro de chat vacío",
+    "app.chat.clearPublicChatMessage": "El chat publico fue borrado por un moderador",
+    "app.note.title": "Notas compartidas",
+    "app.note.label": "Nota",
+    "app.note.hideNoteLabel": "Ocultar nota",
+    "app.userList.usersTitle": "Usuarios",
+    "app.userList.participantsTitle": "Participantes",
+    "app.userList.messagesTitle": "Mensajes",
+    "app.userList.notesTitle": "Notas",
+    "app.userList.presenter": "Presentador",
+    "app.userList.you": "Tu",
+    "app.userList.locked": "Bloqueado",
+    "app.userList.label": "Lista de usuarios",
+    "app.userList.toggleCompactView.label": "Cambiar a modo de vista compacta",
+    "app.userList.guest": "Huesped",
+    "app.userList.menuTitleContext": "Opciones disponibles",
+    "app.userList.chatListItem.unreadSingular": "{0} Nuevo Mensaje",
+    "app.userList.chatListItem.unreadPlural": "{0} Nuevos mensajes",
+    "app.userList.menu.chat.label": "Chat",
+    "app.userList.menu.clearStatus.label": "Limpiar estátus",
+    "app.userList.menu.makePresenter.label": "Hacer presentador",
+    "app.userList.menu.removeUser.label": "Eliminar usuario",
+    "app.userList.menu.muteUserAudio.label": "Silenciar usuario",
+    "app.userList.menu.unmuteUserAudio.label": "Activar sonido de usuario",
+    "app.userList.userAriaLabel": "{0} {1} {2} estado {3}",
+    "app.userList.menu.promoteUser.label": "Promover a moderador",
+    "app.userList.menu.demoteUser.label": "Degradar a espectador",
+    "app.userList.userOptions.manageUsersLabel": "Manejar usuarios",
+    "app.userList.userOptions.muteAllLabel": "Deshabilitar audio a todos los usuarios",
+    "app.userList.userOptions.muteAllDesc": "Deshabilitar audio a todos los usuarios en la sesión",
+    "app.userList.userOptions.clearAllLabel": "Borrar todos los iconos de estado",
+    "app.userList.userOptions.clearAllDesc": "Borrar todos los iconos de estado de usuarios",
+    "app.userList.userOptions.muteAllExceptPresenterLabel": "Silenciar a todos los usuarios excepto a presentador",
+    "app.userList.userOptions.muteAllExceptPresenterDesc": "Silenciar a todos los usuarios en la sesión excepto a presentador",
+    "app.userList.userOptions.unmuteAllLabel": "Desactivar función de silenciar",
+    "app.userList.userOptions.unmuteAllDesc": "Habilitar audio en la sesión",
+    "app.userList.userOptions.lockViewersLabel": "Bloquear espectadores",
+    "app.media.label": "Media",
+    "app.media.screenshare.start": "Compartir pantalla ha iniciado",
+    "app.media.screenshare.end": "Compartir pantalla ha finalizado",
+    "app.media.screenshare.safariNotSupported": "Compartir pantalla actualmente no es soportada por Safari. Por favor usilize Firefox o Google Chrome.",
+    "app.meeting.ended": "La sesión ha finalizado",
+    "app.meeting.endedMessage": "Serás enviado a la pantalla de inicio.",
+    "app.presentation.presentationToolbar.prevSlideLabel": "Diapositiva anterior",
+    "app.presentation.presentationToolbar.prevSlideDesc": "Cambiar presentación a diapositiva anterior",
+    "app.presentation.presentationToolbar.nextSlideLabel": "Siguiente diapositiva",
+    "app.presentation.presentationToolbar.nextSlideDesc": "Cambiar presentación a diapositiva siguiente",
+    "app.presentation.presentationToolbar.skipSlideLabel": "Brincar diapositiva",
+    "app.presentation.presentationToolbar.skipSlideDesc": "Cambiar presentación a diapositiva específica",
+    "app.presentation.presentationToolbar.fitWidthLabel": "Ajustar a lo ancho",
+    "app.presentation.presentationToolbar.fitWidthDesc": "Mostrar diapositiva a todo lo ancho",
+    "app.presentation.presentationToolbar.fitScreenLabel": "Ajustar a la pantalla",
+    "app.presentation.presentationToolbar.fitScreenDesc": "Mostrar toda la diapositiva",
+    "app.presentation.presentationToolbar.zoomLabel": "Zoom",
+    "app.presentation.presentationToolbar.zoomDesc": "Cambiar el nivel de Zoom de la presentación",
+    "app.presentation.presentationToolbar.zoomInLabel": "Acercarse",
+    "app.presentation.presentationToolbar.zoomInDesc": "Acercarse en presentación",
+    "app.presentation.presentationToolbar.zoomOutLabel": "Alejarse",
+    "app.presentation.presentationToolbar.zoomOutDesc": "Alejarse en presentación",
+    "app.presentation.presentationToolbar.zoomIndicator": "Mostrar el porcentaje de acercamiento",
+    "app.presentation.presentationToolbar.fitToWidth": "Ajustar a lo ancho",
+    "app.presentation.presentationToolbar.goToSlide": "Diapositiva {0}",
+    "app.presentationUploder.title": "Presentación",
+    "app.presentationUploder.confirmLabel": "Iniciar",
+    "app.presentationUploder.confirmDesc": "Grardar los cambios e iniciar la presentación",
+    "app.presentationUploder.dismissLabel": "Cancelar",
+    "app.presentationUploder.dismissDesc": "Cerrar la ventana modal y descartar cambios.",
+    "app.presentationUploder.dropzoneLabel": "Arrastrar archivo aqui para cargarlo",
+    "app.presentationUploder.dropzoneImagesLabel": "Arrastrar imagenes aqui para cargarlas",
+    "app.presentationUploder.browseFilesLabel": "o buscar archivos",
+    "app.presentationUploder.browseImagesLabel": "o buscar imagenes",
+    "app.presentationUploder.fileToUpload": "Esperando ser cargado...",
+    "app.presentationUploder.currentBadge": "Acual",
+    "app.presentationUploder.genericError": "Ups, algo salio mal",
+    "app.presentationUploder.rejectedError": "Algunos de los archivos seleccionados fueron rechazados. Por favor verifique el mime type de los archivo",
+    "app.presentationUploder.upload.progress": "Cargando ({0}%)",
+    "app.presentationUploder.upload.413": "Erchivo demasiado grande",
+    "app.presentationUploder.conversion.conversionProcessingSlides": "Procesando página {0} de {1}",
+    "app.presentationUploder.conversion.genericConversionStatus": "Convirtiendo archivo...",
+    "app.presentationUploder.conversion.generatingThumbnail": "Generando miniaturas...",
+    "app.presentationUploder.conversion.generatedSlides": "Diapositivas generadas...",
+    "app.presentationUploder.conversion.generatingSvg": "Generando imágenes SVG...",
+    "app.poll.startCustomLabel": "Iniciar encuesta personalizada",
+    "app.poll.publishLabel": "Publicar encuesta",
+    "app.poll.closeLabel": "Cerrar",
+    "app.poll.tf": "Verdadero / Falseo",
+    "app.poll.yn": "Si / No",
+    "app.poll.a2": "A / B",
+    "app.poll.a3": "A / B / C",
+    "app.poll.a4": "A / B / C / D",
+    "app.poll.a5": "A / B / C / D / E",
+    "app.poll.liveResult.usersTitle": "Usuarios",
+    "app.poll.liveResult.responsesTitle": "Respuesta",
+    "app.polling.pollingTitle": "Respuestas de encuesta",
+    "app.polling.pollAnswerLabel": "Respuesta de encuesta {0}",
+    "app.polling.pollAnswerDesc": "Seleccione esta opcion para responder {0}",
+    "app.failedMessage": "Disculpas, problemas conectando al servidor.",
+    "app.connectingMessage": "Conectando...",
+    "app.waitingMessage": "Desconectado. Intentando reconectar en {0} segundos...",
+    "app.navBar.settingsDropdown.optionsLabel": "Opciones",
+    "app.navBar.settingsDropdown.fullscreenLabel": "Desplegar a pantalla completa",
+    "app.navBar.settingsDropdown.settingsLabel": "Abrir configuración",
+    "app.navBar.settingsDropdown.aboutLabel": "Acerca de",
+    "app.navBar.settingsDropdown.leaveSessionLabel": "Cerrar sesión",
+    "app.navBar.settingsDropdown.exitFullscreenLabel": "Salir de pantalla completa",
+    "app.navBar.settingsDropdown.fullscreenDesc": "Hacer el menú de configuración a pantalla completa",
+    "app.navBar.settingsDropdown.settingsDesc": "Cambiar la configuración general",
+    "app.navBar.settingsDropdown.aboutDesc": "Mostrar información acerca del cliente",
+    "app.navBar.settingsDropdown.leaveSessionDesc": "Abandonar la reunión",
+    "app.navBar.settingsDropdown.exitFullscreenDesc": "Salir del modo de pantalla completa",
+    "app.navBar.settingsDropdown.hotkeysLabel": "Acceso rápido",
+    "app.navBar.settingsDropdown.hotkeysDesc": "Lista de accesos rápidos",
+    "app.navBar.settingsDropdown.helpLabel": "Ayuda",
+    "app.navBar.settingsDropdown.helpDesc": "Enlaces a videos tutoriales",
+    "app.navBar.userListToggleBtnLabel": "Alternar lista de usuarios",
+    "app.navBar.toggleUserList.newMessages": "con nueva notificación de mensaje ",
+    "app.leaveConfirmation.confirmLabel": "Salir",
+    "app.leaveConfirmation.confirmDesc": "Te desconecta de la reunión",
+    "app.about.title": "Acerca de",
+    "app.about.version": "Construcción del cliente:",
+    "app.about.copyright": "Derechos de autor:",
+    "app.about.confirmLabel": "OK",
+    "app.about.confirmDesc": "OK",
+    "app.about.dismissLabel": "Cancelar",
+    "app.about.dismissDesc": "Cerrar información acerca del cliente",
+    "app.actionsBar.changeStatusLabel": "Cambiar estátus",
+    "app.actionsBar.muteLabel": "Silenciar",
+    "app.actionsBar.unmuteLabel": "De-silenciar",
+    "app.actionsBar.camOffLabel": "Cámara Apagada",
+    "app.actionsBar.raiseLabel": "Alzar",
+    "app.actionsBar.label": "Barra de acciones",
+    "app.submenu.application.applicationSectionTitle": "Aplicación",
+    "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",
+    "app.submenu.application.languageLabel": "Lenguaje de aplicación",
+    "app.submenu.application.ariaLanguageLabel": "Cambiar lenguaje de aplicación",
+    "app.submenu.application.languageOptionLabel": "Seleccionar lenguaje",
+    "app.submenu.application.noLocaleOptionLabel": "No hay locales activos",
+    "app.submenu.audio.micSourceLabel": "Fuente de micrófono",
+    "app.submenu.audio.speakerSourceLabel": "Fuente de altavoces",
+    "app.submenu.audio.streamVolumeLabel": "Volumen del flujo de audio",
+    "app.submenu.video.title": "Video",
+    "app.submenu.video.videoSourceLabel": "Fuente del video",
+    "app.submenu.video.videoOptionLabel": "Escoger ver fuente",
+    "app.submenu.video.videoQualityLabel": "Calidad del Video",
+    "app.submenu.video.qualityOptionLabel": "Escoger calidad del video",
+    "app.submenu.video.participantsCamLabel": "Viendo webcams de participantes",
+    "app.submenu.closedCaptions.closedCaptionsLabel": "Subtítulos",
+    "app.submenu.closedCaptions.takeOwnershipLabel": "Tomar el control",
+    "app.submenu.closedCaptions.languageLabel": "Lenguaje",
+    "app.submenu.closedCaptions.localeOptionLabel": "Seleccionar lenguaje",
+    "app.submenu.closedCaptions.noLocaleOptionLabel": "No hay locales activos",
+    "app.submenu.closedCaptions.fontFamilyLabel": "Familia de fuente",
+    "app.submenu.closedCaptions.fontFamilyOptionLabel": "Seleccionar familia de fuente",
+    "app.submenu.closedCaptions.fontSizeLabel": "Tamaño de fuente",
+    "app.submenu.closedCaptions.fontSizeOptionLabel": "Seleccionar tamaño de fuente",
+    "app.submenu.closedCaptions.backgroundColorLabel": "Color de fondo",
+    "app.submenu.closedCaptions.fontColorLabel": "Color de fuente",
+    "app.submenu.participants.muteAllLabel": "Silenciar a todos menos al presentador",
+    "app.submenu.participants.lockAllLabel": "Bloquear a todos los participantes",
+    "app.submenu.participants.lockItemLabel": "Participantes {0}",
+    "app.submenu.participants.lockMicDesc": "Deshabilita micrófono a todos los participantes bloqueados",
+    "app.submenu.participants.lockCamDesc": "Deshabilita el webcam a todos los participantes bloqueados",
+    "app.submenu.participants.lockPublicChatDesc": "Deshabilita el chat público a todos los usuarios bloqueados",
+    "app.submenu.participants.lockPrivateChatDesc": "Deshabilita el chat privado a todos los usuarios bloqueados",
+    "app.submenu.participants.lockLayoutDesc": "Bloquea el diseño a todos los usuarios bloqueados",
+    "app.submenu.participants.lockMicAriaLabel": "Bloquea micrófono",
+    "app.submenu.participants.lockCamAriaLabel": "Bloquea webcam",
+    "app.submenu.participants.lockPublicChatAriaLabel": "Bloquea el chat público",
+    "app.submenu.participants.lockPrivateChatAriaLabel": "Bloquea el chat privado",
+    "app.submenu.participants.lockLayoutAriaLabel": "Bloquea el diseño",
+    "app.submenu.participants.lockMicLabel": "Micrófono",
+    "app.submenu.participants.lockCamLabel": "Webcam",
+    "app.submenu.participants.lockPublicChatLabel": "Chat público",
+    "app.submenu.participants.lockPrivateChatLabel": "Chat privado",
+    "app.submenu.participants.lockLayoutLabel": "Diseño",
+    "app.settings.applicationTab.label": "Aplicación",
+    "app.settings.audioTab.label": "Audio",
+    "app.settings.videoTab.label": "Video",
+    "app.settings.closedcaptionTab.label": "Subtítulos",
+    "app.settings.usersTab.label": "Participantes",
+    "app.settings.main.label": "Configuración",
+    "app.settings.main.cancel.label": "Cancela",
+    "app.settings.main.cancel.label.description": "Deshecha los cambios y cierra el menú de configuración",
+    "app.settings.main.save.label": "Guardar",
+    "app.settings.main.save.label.description": "Gurada cambios y cierra el menú de configuración",
+    "app.actionsBar.actionsDropdown.actionsLabel": "Acciones",
+    "app.actionsBar.actionsDropdown.presentationLabel": "Subir una presentación",
+    "app.actionsBar.actionsDropdown.initPollLabel": "Iniciar una encuesta",
+    "app.actionsBar.actionsDropdown.desktopShareLabel": "Compartir tu pantalla",
+    "app.actionsBar.actionsDropdown.presentationDesc": "Subir tu presentación",
+    "app.actionsBar.actionsDropdown.initPollDesc": "Iniciar una encuesta",
+    "app.actionsBar.actionsDropdown.desktopShareDesc": "Compartir tu pantalla con otros",
+    "app.actionsBar.emojiMenu.awayLabel": "Ausente",
+    "app.actionsBar.emojiMenu.awayDesc": "Cambiar tu estatus a ausente",
+    "app.actionsBar.emojiMenu.raiseHandLabel": "Alzar",
+    "app.actionsBar.emojiMenu.raiseHandDesc": "Alzar la mano para preguntar",
+    "app.actionsBar.emojiMenu.neutralLabel": "Indeciso",
+    "app.actionsBar.emojiMenu.neutralDesc": "Cambiar tu estatus a indeciso",
+    "app.actionsBar.emojiMenu.confusedLabel": "Confundido",
+    "app.actionsBar.emojiMenu.confusedDesc": "Cambiar tu estatus a confundido",
+    "app.actionsBar.emojiMenu.sadLabel": "Triste",
+    "app.actionsBar.emojiMenu.sadDesc": "Cambiar tu estatus a triste",
+    "app.actionsBar.emojiMenu.happyLabel": "Feliz",
+    "app.actionsBar.emojiMenu.happyDesc": "Cambiar tu estatus a feliz",
+    "app.actionsBar.emojiMenu.noneLabel": "Limpiar estátus",
+    "app.actionsBar.emojiMenu.noneDesc": "Limpia tu estatus",
+    "app.actionsBar.emojiMenu.applauseLabel": "Aplausos",
+    "app.actionsBar.emojiMenu.applauseDesc": "Cambiar tu estatus a aplausos",
+    "app.actionsBar.emojiMenu.thumbsUpLabel": "Señal de aprobación",
+    "app.actionsBar.emojiMenu.thumbsUpDesc": "Cambiar tu estatus a señal de aprobación",
+    "app.actionsBar.emojiMenu.thumbsDownLabel": "Señal de desaprobación",
+    "app.actionsBar.emojiMenu.thumbsDownDesc": "Cambia tu estatus a señal de desaprobación",
+    "app.actionsBar.currentStatusDesc": "estatus actual {0}",
+    "app.audioNotification.audioFailedError1001": "Error 1001: WebSocket desconectado",
+    "app.audioNotification.audioFailedError1002": "Error 1002: No puede establecerse conexión de WebSocket",
+    "app.audioNotification.audioFailedError1003": "Error 1003: Versión de navegador no soportado ",
+    "app.audioNotification.audioFailedError1004": "Error 1004: Falla en llamada",
+    "app.audioNotification.audioFailedError1005": "Error 1005: La llamada terminó inesperadamente",
+    "app.audioNotification.audioFailedError1006": "Error 1006: Tiempo de espera de llamada agotado",
+    "app.audioNotification.audioFailedError1007": "Error 1007: Fallo de negociación ICE",
+    "app.audioNotification.audioFailedError1008": "Error 1008: Fallo de transferencia",
+    "app.audioNotification.audioFailedError1009": "Error 1009: No se pudo obtener información de servidor STUN/TURN",
+    "app.audioNotification.audioFailedError1010": "Error 1010: Se acabó tiempo de negociación ICE  ",
+    "app.audioNotification.audioFailedError1011": "Error 1011: Se acabó tiempo recolectando ICE",
+    "app.audioNotification.audioFailedMessage": "Tu conexión de audio falló en conectarse",
+    "app.audioNotification.mediaFailedMessage": "getUserMicMedia falló, Solo orígenes seguros son admitidos",
+    "app.audioNotification.closeLabel": "Cerrar",
+    "app.breakoutJoinConfirmation.title": "Unirse a una sala de grupo",
+    "app.breakoutJoinConfirmation.message": "Quieres unirte",
+    "app.breakoutJoinConfirmation.confirmLabel": "Unirte",
+    "app.breakoutJoinConfirmation.confirmDesc": "Unirte a sala de grupo",
+    "app.breakoutJoinConfirmation.dismissLabel": "Cancelar",
+    "app.breakoutJoinConfirmation.dismissDesc": "Cierra y rechaza Unirse a Sala de Grupo",
+    "app.breakoutTimeRemainingMessage": "Tiempo restante en Sala de Grupo",
+    "app.breakoutWillCloseMessage": "Tiempo transcurrido. Sala de Grupo de cerrará en breve.",
+    "app.calculatingBreakoutTimeRemaining": "Calculando tiempo restante...",
+    "app.audioModal.microphoneLabel": "Micrófono",
+    "app.audioModal.listenOnlyLabel": "Solo escuchar",
+    "app.audioModal.audioChoiceLabel": "¿Como quieres unirte al audio?",
+    "app.audioModal.audioChoiceDesc": "Selecciona como unirse al audio en esta reunión",
+    "app.audioModal.closeLabel": "Cerrar",
+    "app.audio.joinAudio": "Unirse al audio",
+    "app.audio.leaveAudio": "Abandonar audio",
+    "app.audio.enterSessionLabel": "Entrar a la sesión",
+    "app.audio.playSoundLabel": "Reproducir sonido",
+    "app.audio.backLabel": "Atrás",
+    "app.audio.audioSettings.titleLabel": "Seleccionar tu configuración de audio",
+    "app.audio.audioSettings.descriptionLabel": "Ten en cuenta que aparecerá un cuadro de diálogo en tu navegador, que te pide a aceptar compartir tu micrófono.",
+    "app.audio.audioSettings.microphoneSourceLabel": "Fuente del micrófono",
+    "app.audio.audioSettings.speakerSourceLabel": "Fuente de altavoz",
+    "app.audio.audioSettings.microphoneStreamLabel": "Tu volúmen del flujo de audio",
+    "app.audio.listenOnly.backLabel": "Atrás",
+    "app.audio.listenOnly.closeLabel": "Cerrar",
+    "app.error.meeting.ended": "Haz salido de la conferencia",
+    "app.dropdown.close": "Cerrar",
+    "app.error.500": "Ups, algo salio mal",
+    "app.error.404": "No se encontró",
+    "app.error.401": "No autorizado",
+    "app.error.403": "Prohibido",
+    "app.error.leaveLabel": "Ingresa de nuevo",
+    "app.guest.waiting": "Esperando aprobación para unirse",
+    "app.shortcut-help.title": "Atajos",
+    "app.shortcut-help.closeLabel": "Cerrar",
+    "app.lock-viewers.webcamLabel": "Webcam",
+    "app.lock-viewers.microphoneLable": "Micrófono",
+    "app.lock-viewers.PublicChatLabel": "Chat público",
+    "app.lock-viewers.PrivateChatLable": "Chat privado",
+    "app.lock-viewers.Layout": "Diseño",
+    "app.videoPreview.cancelLabel": "Cancelar",
+    "app.videoPreview.closeLabel": "Cerrar",
+    "app.meeting.endNotification.ok.label": "OK",
+    "app.feedback.title": "Haz salido de la conferencia",
+    "app.createBreakoutRoom.joinAudio": "Unirse al audio",
+    "app.externalVideo.close": "Cerrar"
+
+}
+
diff --git a/bigbluebutton-html5/private/locales/pl_PL.json b/bigbluebutton-html5/private/locales/pl_PL.json
new file mode 100644
index 0000000000000000000000000000000000000000..794aafd2d8b3acb224cda350d04feb1631b82f2d
--- /dev/null
+++ b/bigbluebutton-html5/private/locales/pl_PL.json
@@ -0,0 +1,41 @@
+{
+    "app.chat.submitLabel": "Wyślij wiadomość",
+    "app.chat.titlePrivate": "Czat prywatny z {0}",
+    "app.chat.closeChatLabel": "Zamknij {0}",
+    "app.chat.hideChatLabel": "Ukryj {0}",
+    "app.chat.dropdown.options": "Opcje Czatu",
+    "app.chat.dropdown.clear": "Wyczyść",
+    "app.chat.dropdown.copy": "Kopiuj",
+    "app.chat.dropdown.save": "Zapisz",
+    "app.chat.label": "Czat",
+    "app.note.title": "Udostępnione notatki",
+    "app.note.label": "Notatka",
+    "app.note.hideNoteLabel": "Ukryj notatkÄ™",
+    "app.userList.usersTitle": "Użytkownicy",
+    "app.userList.participantsTitle": "Uczestnicy",
+    "app.userList.messagesTitle": "Wiadomości",
+    "app.userList.notesTitle": "Notatki",
+    "app.userList.presenter": "Prezenter",
+    "app.userList.you": "Ty",
+    "app.userList.label": "Lista użytkowników",
+    "app.userList.guest": "Gość",
+    "app.userList.menuTitleContext": "Dostępne opcje",
+    "app.userList.chatListItem.unreadSingular": "{0} Nowa Wiadomość",
+    "app.userList.menu.chat.label": "Czat",
+    "app.userList.menu.clearStatus.label": "Wyczyść status",
+    "app.userList.menu.removeUser.label": "Usuń użytkownika",
+    "app.poll.closeLabel": "Zamknij",
+    "app.poll.liveResult.usersTitle": "Użytkownicy",
+    "app.settings.usersTab.label": "Uczestincy",
+    "app.settings.main.save.label": "Zapisz",
+    "app.actionsBar.emojiMenu.noneLabel": "Wyczyść status",
+    "app.audioNotification.closeLabel": "Zamknij",
+    "app.audioModal.closeLabel": "Zamknij",
+    "app.audio.listenOnly.closeLabel": "Zamknij",
+    "app.dropdown.close": "Zamknij",
+    "app.shortcut-help.closeLabel": "Zamknij",
+    "app.videoPreview.closeLabel": "Zamknij",
+    "app.externalVideo.close": "Zamknij"
+
+}
+