diff --git a/README.md b/README.md
index 24015624..6344e653 100644
--- a/README.md
+++ b/README.md
@@ -159,13 +159,16 @@ docker run --rm --name my-spreed-webrtc -p 8080:8080 -p 8443:8443 \
 ## Setup Screensharing
 
 ### Chrome
-Chrome should work out of the box.
+
+Chromium-based browsers (e.g. Google Chrome) require the [Spreed.ME screen sharing
+extension](https://www.spreed.me/extension/).
 
 ### Firefox
 
-As of Firefox >= 36 you must append the domain being used to the allowed domains
-to access your screen. You do this by navigating to `about:config`, search for
-'media.getusermedia.screensharing.allowed_domains', and append the domain
+Screensharing with Firefox >= 52 should work out of the box.
+When using Firefox 36 – 51 you must append the domain being used to the allowed
+domains to access your screen. You do this by navigating to `about:config`, search
+for 'media.getusermedia.screensharing.allowed_domains', and append the domain
 to the list of strings. You can edit the field simply by double clicking on it.
 Ensure that you follow the syntax rules of the field. If you are using an `ip:port`
 url, simply append `ip` to the list. Also ensure that you are using `https`,
diff --git a/dependencies.tsv b/dependencies.tsv
index dd07bef1..5d7cf7e9 100644
--- a/dependencies.tsv
+++ b/dependencies.tsv
@@ -3,7 +3,7 @@ github.com/gorilla/context	git	215affda49addc4c8ef7e2534915df2c8c35c6cd	2014-12-
 github.com/gorilla/mux	git	ba336c9cfb43552c90de6cb2ceedd3271c747558	2015-07-17T15:03:03Z
 github.com/gorilla/securecookie	git	aeade84400a85c6875264ae51c7a56ecdcb61751	2015-07-16T23:32:44Z
 github.com/gorilla/websocket	git	a69d25be2fe2923a97c2af6849b2f52426f68fc0	2016-08-02T13:32:03Z
-github.com/longsleep/pkac	git	68bf8859f58dd84332ee41c07eba357fb3818ba3	2014-05-01T18:13:13Z
+github.com/longsleep/pkac	git	302922ac7627196c8cf9e4384cccadf9aa008b3e	2017-02-16T19:00:44Z
 github.com/nats-io/nats	git	355b5b97e0842dc94f1106729aa88e33e06317ca	2015-12-09T21:13:14Z
 github.com/satori/go.uuid	git	879c5887cd475cd7864858769793b2ceb0d44feb	2016-06-07T14:43:47Z
 github.com/strukturag/goacceptlanguageparser	git	68066e68c2940059aadc6e19661610cf428b6647	2014-02-13T13:31:23Z
diff --git a/go/channelling/config.go b/go/channelling/config.go
index 3a40bd85..5fcc65ea 100644
--- a/go/channelling/config.go
+++ b/go/channelling/config.go
@@ -32,7 +32,8 @@ type Config struct {
 	ContentSecurityPolicyReportOnly string                    `json:"-"` // HTML content security policy in report only mode
 	RoomTypeDefault                 string                    `json:"-"` // New rooms default to this type
 	RoomTypes                       map[*regexp.Regexp]string `json:"-"` // Map of regular expression -> room type
-	RoomNameCaseSensitive           bool                      // Wether the room names are case sensitive.
+	RoomNameCaseSensitive           bool                      // Whether the room names are case sensitive.
+	LockedRoomJoinableWithPIN       bool                      // Whether locked rooms should be joinable by providing the PIN the room was locked with
 }
 
 func (config *Config) WithModule(m string) bool {
diff --git a/go/channelling/room_manager.go b/go/channelling/room_manager.go
index bd6b6e76..c0c5d51e 100644
--- a/go/channelling/room_manager.go
+++ b/go/channelling/room_manager.go
@@ -109,6 +109,9 @@ func (rooms *roomManager) setNatsRoomType(msg *roomTypeMessage) {
 		return
 	}
 
+	// TODO(fancycode): Should we use a separate mutex for this?
+	rooms.Lock()
+	defer rooms.Unlock()
 	if msg.Type != "" {
 		log.Printf("Setting room type for %s to %s\n", msg.Path, msg.Type)
 		rooms.roomTypes[msg.Path] = msg.Type
@@ -274,7 +277,11 @@ func (rooms *roomManager) MakeRoomID(roomName, roomType string) string {
 }
 
 func (rooms *roomManager) getConfiguredRoomType(roomName string) string {
-	if roomType, found := rooms.roomTypes[roomName]; found {
+	// TODO(fancycode): Should we use a separate mutex for this?
+	rooms.RLock()
+	roomType, found := rooms.roomTypes[roomName]
+	rooms.RUnlock()
+	if found {
 		// Type of this room was overwritten through NATS.
 		return roomType
 	}
diff --git a/go/channelling/server/config.go b/go/channelling/server/config.go
index 659c1ac7..cfe57da6 100644
--- a/go/channelling/server/config.go
+++ b/go/channelling/server/config.go
@@ -147,6 +147,7 @@ func NewConfig(container phoenix.Container, tokens bool) (*channelling.Config, e
 		RoomTypeDefault:                 defaultRoomType,
 		RoomTypes:                       roomTypes,
 		RoomNameCaseSensitive:           container.GetBoolDefault("app", "caseSensitiveRooms", false),
+		LockedRoomJoinableWithPIN:       container.GetBoolDefault("app", "lockedRoomJoinableWithPIN", true),
 	}, nil
 }
 
diff --git a/go/channelling/turnservice_manager.go b/go/channelling/turnservice_manager.go
index aa36a063..3119c0c5 100644
--- a/go/channelling/turnservice_manager.go
+++ b/go/channelling/turnservice_manager.go
@@ -100,9 +100,17 @@ func (mgr *turnServiceManager) turnData(credentials *turnservicecli.CachedCreden
 
 			if len(turn.Servers) > 0 {
 				// For backwards compatibility with clients which do not
-				// understand turn.Servers, directly deliver the first TURN
-				// server zone URNs.
-				turn.Urls = turn.Servers[0].URNs
+				// understand turn.Servers, directly deliver the TURN
+				// server zone URNs with the lowest priority.
+				minPrio := 0
+				minPrioIdx := -1
+				for idx, server := range turn.Servers {
+					if minPrioIdx == -1 || server.Prio < minPrio {
+						minPrio = server.Prio
+						minPrioIdx = idx
+					}
+				}
+				turn.Urls = turn.Servers[minPrioIdx].URNs
 			}
 		}
 	}
diff --git a/server.conf.in b/server.conf.in
index 80aeff46..1641ee85 100644
--- a/server.conf.in
+++ b/server.conf.in
@@ -97,7 +97,10 @@ encryptionSecret = tne-default-encryption-block-key
 ; Whether a user account is required to create a room. This only has an effect
 ; if user accounts are enabled. Optional, defaults to false.
 ;authorizeRoomCreation = false
-; Wether the pipelines API should be enabled. Optional, defaults to false.
+; Whether locked rooms should be joinable by providing the PIN the room was
+; locked with. Optional, defaults to true.
+;lockedRoomJoinableWithPIN = true
+; Whether the pipelines API should be enabled. Optional, defaults to false.
 ;pipelinesEnabled = false
 ; Server token is a public random string which is used to enhance security of
 ; server generated security tokens. When the serverToken is changed all existing
diff --git a/static/js/directives/audiovideo.js b/static/js/directives/audiovideo.js
index 468391d0..9992c1b0 100644
--- a/static/js/directives/audiovideo.js
+++ b/static/js/directives/audiovideo.js
@@ -146,7 +146,7 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
 							var video = clonedElement.find("video")[0];
 							$window.attachMediaStream(video, stream);
 							// Waiter callbacks also count as connected, as browser support (FireFox 25) is not setting state changes properly.
-							videoWaiter.wait(video, stream, function(withvideo) {
+							videoWaiter.wait(video, stream, function(withvideo, retriggered) {
 								if (scope.destroyed) {
 									console.log("Abort wait for video on destroyed scope.");
 									return;
@@ -163,7 +163,9 @@ define(['jquery', 'underscore', 'text!partials/audiovideo.html', 'text!partials/
 										$scope.onlyaudio = true;
 									});
 								}
-								scope.$emit("active", currentcall);
+								if (!retriggered) {
+									scope.$emit("active", currentcall);
+								}
 								$scope.redraw();
 							}, function() {
 								if (scope.destroyed) {
diff --git a/static/js/directives/socialshare.js b/static/js/directives/socialshare.js
index 8401d0a9..bcda2133 100644
--- a/static/js/directives/socialshare.js
+++ b/static/js/directives/socialshare.js
@@ -31,7 +31,7 @@ define(['text!partials/socialshare.html'], function(template) {
 	};
 
 	// socialShare
-	return ["$window", "translation", "rooms", "alertify", function($window, translation, rooms, alertify) {
+	return ["$window", "translation", "rooms", "roompin", "alertify", function($window, translation, rooms, roompin, alertify) {
 
 		var title = $window.encodeURIComponent($window.document.title);
 		var makeUrl = function(nw, target) {
@@ -71,21 +71,7 @@ define(['text!partials/socialshare.html'], function(template) {
 							//$window.alert("Room link: " + $scope.roomlink);
 							alertify.dialog.notify(translation._("Room link"), '<a href="'+$scope.roomlink+'" rel="external" target="_blank">'+$scope.roomlink+'</a>');
 						} else if (nw === "pin") {
-							if (!$scope.isRoomLocked) {
-								// Lock
-								alertify.dialog.prompt(translation._("Please enter a new Room PIN to lock the room"), function(pin) {
-									rooms.setPIN(pin);
-								}, function() {
-									// Do nothing
-								});
-							} else {
-								// Unlock
-								alertify.dialog.confirm(translation._("Do you want to unlock the room?"), function() {
-									rooms.setPIN("");
-								}, function() {
-									// Do nothing
-								});
-							}
+							roompin.toggleCurrentRoomState(rooms);
 						}
 					}
 				});
diff --git a/static/js/mediastream/peerconnection.js b/static/js/mediastream/peerconnection.js
index 9bdc014d..6236f3a6 100644
--- a/static/js/mediastream/peerconnection.js
+++ b/static/js/mediastream/peerconnection.js
@@ -347,7 +347,7 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
 		if (!this.pc) {
 			return [];
 		}
-		return this.pc.getRemoteStreams.apply(this.pc, arguments);
+		return this.pc.getLocalStreams.apply(this.pc, arguments);
 
 	};
 
diff --git a/static/js/services/roompin.js b/static/js/services/roompin.js
index a407b9a4..5bf760ac 100644
--- a/static/js/services/roompin.js
+++ b/static/js/services/roompin.js
@@ -21,11 +21,16 @@
 
 "use strict";
 define([
-], function() {
+	"moment"
+], function(moment) {
 
-	return ["$window", "$q", "alertify", "translation", "safeMessage", function($window, $q, alertify, translation, safeMessage) {
+	return ["$window", "$q", "globalContext", "alertify", "toastr", "translation", "safeMessage", "randomGen", "localStorage", function($window, $q, context, alertify, toastr, translation, safeMessage, randomGen, localStorage) {
 
 		var pinCache = {};
+		var getLocalStoragePINIDForRoom = function(roomName) {
+			return "room-pin-" + roomName;
+		};
+		var lockedRoomsJoinable = !!context.Cfg.LockedRoomJoinableWithPIN;
 		var roompin = {
 			get: function(roomName) {
 				var cachedPIN = pinCache[roomName];
@@ -33,30 +38,84 @@ define([
 			},
 			clear: function(roomName) {
 				delete pinCache[roomName];
+				localStorage.removeItem(getLocalStoragePINIDForRoom(roomName));
 				console.log("Cleared PIN for", roomName);
 			},
-			update: function(roomName, pin) {
+			update: function(roomName, pin, noAlert) {
 				if (pin) {
 					pinCache[roomName] = pin;
-					alertify.dialog.alert(translation._("PIN for room %s is now '%s'.", safeMessage(roomName), safeMessage(pin)));
+					localStorage.setItem(getLocalStoragePINIDForRoom(roomName), pin);
+					if (!noAlert && lockedRoomsJoinable) {
+						alertify.dialog.alert(translation._("PIN for room %s is now '%s'.", safeMessage(roomName), safeMessage(pin)));
+					}
 				} else {
 					roompin.clear(roomName);
-					alertify.dialog.alert(translation._("PIN lock has been removed from room %s.", safeMessage(roomName)));
+					if (!noAlert && lockedRoomsJoinable) {
+						toastr.info(moment().format("lll"), translation._("PIN lock has been removed from room '%s'", safeMessage(roomName)));
+					}
 				}
 			},
 			requestInteractively: function(roomName) {
 				var deferred = $q.defer();
-				alertify.dialog.prompt(translation._("Enter the PIN for room %s", safeMessage(roomName)), function(pin) {
+				var tryJoinWithStoredPIN = function() {
+					var pin = localStorage.getItem(getLocalStoragePINIDForRoom(roomName));
 					if (pin) {
-						pinCache[roomName] = pin;
+						roompin.update(roomName, pin, true);
 						deferred.resolve();
-					} else {
+						return true;
+					}
+					return false;
+				};
+				if (lockedRoomsJoinable) {
+					if (!tryJoinWithStoredPIN()) {
+						alertify.dialog.prompt(translation._("Enter the PIN for room %s", safeMessage(roomName)), function(pin) {
+							if (pin) {
+								roompin.update(roomName, pin);
+								deferred.resolve();
+							} else {
+								deferred.reject();
+							}
+						}, function() {
+							deferred.reject();
+						});
+					}
+				} else {
+					if (!tryJoinWithStoredPIN()) {
+						alertify.dialog.error(
+							translation._("Can't join locked room '%s'.", safeMessage(roomName)),
+							translation._("Room '%s' is locked. This server is configured to not let anyone join locked rooms.", safeMessage(roomName))
+						);
 						deferred.reject();
 					}
+				}
+				return deferred.promise;
+			},
+			// Passing in "rooms" is a bit of a hack to prevent circular dependencies
+			toggleCurrentRoomState: function(rooms) {
+				if (!rooms.isLocked()) {
+					// Lock
+					if (lockedRoomsJoinable) {
+						alertify.dialog.prompt(translation._("Please enter a new Room PIN to lock the room"), function(pin) {
+							rooms.setPIN(pin);
+						}, function() {
+							// Do nothing
+						});
+					} else {
+						alertify.dialog.confirm(translation._("Do you want to lock the room?"), function() {
+							var pin = randomGen.random({hex: true});
+							rooms.setPIN(pin);
+						}, function() {
+							// Do nothing
+						});
+					}
+					return;
+				}
+				// Unlock
+				alertify.dialog.confirm(translation._("Do you want to unlock the room?"), function() {
+					rooms.setPIN("");
 				}, function() {
-					deferred.reject();
+					// Do nothing
 				});
-				return deferred.promise;
 			}
 		};
 
diff --git a/static/js/services/videowaiter.js b/static/js/services/videowaiter.js
index fb37cc6d..9f0ad12d 100644
--- a/static/js/services/videowaiter.js
+++ b/static/js/services/videowaiter.js
@@ -24,39 +24,61 @@ define(["underscore"], function(_) {
 
 	return ["$window", function($window) {
 
-		var Waiter = function() {
+		var Waiter = function(video, stream, cb, err_cb) {
 			this.stop = false;
+			this.triggered = false;
 			this.count = 0;
 			this.retries = 100;
+
+			this.video = video;
+			this.stream = stream;
+			this.cb = cb;
+			this.err_cb = err_cb;
+		};
+		Waiter.prototype.trigger = function() {
+			var oldTriggered = this.triggered;
+			this.triggered = true;
+			return oldTriggered;
+		};
+		Waiter.prototype.error = function() {
+			var triggered = this.trigger();
+			if (this.err_cb) {
+				this.err_cb(this.video, this.stream, triggered);
+			}
 		};
-		Waiter.prototype.start = function(video, stream, cb, err_cb) {
+		Waiter.prototype.found = function(withvideo) {
+			var triggered = this.trigger();
+			this.cb(withvideo, triggered);
+		};
+		Waiter.prototype.start = function() {
 			if (this.stop) {
-				if (err_cb) {
-					err_cb(video, stream);
-				}
+				this.error();
 				return;
 			}
-			var videoTracks = stream && stream.getVideoTracks() || [];
+			var recheck = _.bind(this.start, this);
+			var videoTracks = this.stream && this.stream.getVideoTracks() || [];
 			//console.log("wait for video", videoTracks.length, video.currentTime, video.videoHeight, video);
 			if (videoTracks.length === 0 && this.count >= 10) {
-				cb(false, video, stream);
-			} else if (video.currentTime > 0 && video.videoHeight > 0) {
-				cb(true, video, stream);
+				this.found(false);
+			} else if (this.video.currentTime > 0 && this.video.videoHeight > 0) {
+				this.found(true);
 			} else {
 				if (videoTracks.length > 0 && this.count >= 10) {
 					var videoTrack = videoTracks[0];
 					if (videoTrack.enabled === true && videoTrack.muted === true) {
-						cb(false, video, stream);
+						videoTrack.onunmute = function() {
+							videoTrack.onunmute = undefined;
+							_.defer(recheck);
+						};
+						this.found(false);
 						return;
 					}
 				}
 				this.count++;
 				if (this.count < this.retries) {
-					$window.setTimeout(_.bind(this.start, this, video, stream, cb, err_cb), 100);
+					$window.setTimeout(recheck, 100);
 				} else {
-					if (err_cb) {
-						err_cb(video, stream);
-					}
+					this.error();
 				}
 			}
 		};
@@ -67,15 +89,16 @@ define(["underscore"], function(_) {
 		// videoWaiter wait
 		return {
 			wait: function(video, stream, cb, err_cb) {
-				var waiter = new Waiter();
+				var waiter = new Waiter(video, stream, cb, err_cb);
 				_.defer(function() {
-					waiter.start(video, stream, cb, err_cb);
+					waiter.start();
 				});
-				return waiter;
+				return {
+					stop: waiter.stop,
+				};
 			}
 		}
 
 	}]
 
-
 });