diff --git a/package.json b/package.json
index 033fc274b7e8b599ffd39c09c4cb1e399e4548e1..5baab2abb42b937e578c0b379d96fcc81f9f9e39 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,9 @@
     "prettier": "prettier --write src/**/*.js"
   },
   "dependencies": {
+    "@fortawesome/fontawesome": "^1.1.5",
+    "@fortawesome/fontawesome-free-solid": "^5.0.9",
+    "@fortawesome/react-fontawesome": "^0.0.18",
     "aframe-billboard-component": "^1.0.0",
     "aframe-extras": "^3.12.4",
     "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array",
diff --git a/src/assets/avatars/BotBobo_Avatar.glb b/src/assets/avatars/BotBobo_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..66862adb28f35fb3bb1ca4954d840a1f9a91bb96
Binary files /dev/null and b/src/assets/avatars/BotBobo_Avatar.glb differ
diff --git a/src/assets/avatars/BotBobo_Avatar_Unlit.glb b/src/assets/avatars/BotBobo_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..e4a939a242fc930dc52c7d60aa145b7b86023330
Binary files /dev/null and b/src/assets/avatars/BotBobo_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb
index 9c72e3d2fc170d6b0782d741eca441a62780daad..098a042ead7b919170c6c4c6a63ba40f62a1b81b 100644
Binary files a/src/assets/avatars/BotDom_Avatar.glb and b/src/assets/avatars/BotDom_Avatar.glb differ
diff --git a/src/assets/avatars/BotDom_Avatar_Unlit.glb b/src/assets/avatars/BotDom_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..88c830ae9f73581683050705ad75f321bdeecb37
Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotGreg_Avatar.glb b/src/assets/avatars/BotGreg_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..33ca3ee66249ed267688353ef0ab82fd97dfc8bc
Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar.glb differ
diff --git a/src/assets/avatars/BotGreg_Avatar_Unlit.glb b/src/assets/avatars/BotGreg_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..ed0a5291790f79029b6c7e624a45253c5f6caeb3
Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotGuest_Avatar.glb b/src/assets/avatars/BotGuest_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..e0be8e9cbad1a1d6167c0c0ccf333285d0857472
Binary files /dev/null and b/src/assets/avatars/BotGuest_Avatar.glb differ
diff --git a/src/assets/avatars/BotGuest_Avatar_Unlit.glb b/src/assets/avatars/BotGuest_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..3bc50b18e3ffbf9f9007a6f7790a8512284949a7
Binary files /dev/null and b/src/assets/avatars/BotGuest_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotJim_Avatar.glb b/src/assets/avatars/BotJim_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..34c1a8cf274d0ed62b2bc22b51d573f6bafc1728
Binary files /dev/null and b/src/assets/avatars/BotJim_Avatar.glb differ
diff --git a/src/assets/avatars/BotJim_Avatar_Unlit.glb b/src/assets/avatars/BotJim_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..534356f80deb17b8808bc778db2db048811baf6a
Binary files /dev/null and b/src/assets/avatars/BotJim_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotKev_Avatar.glb b/src/assets/avatars/BotKev_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..a20a54bcc42926a79b623e360a4ed9ddb2380695
Binary files /dev/null and b/src/assets/avatars/BotKev_Avatar.glb differ
diff --git a/src/assets/avatars/BotKev_Avatar_Unlit.glb b/src/assets/avatars/BotKev_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..d70196b7a3f83f25445cef27cf0fbc7493805cd2
Binary files /dev/null and b/src/assets/avatars/BotKev_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotPinky_Avatar.glb b/src/assets/avatars/BotPinky_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..da14ccd6ada339e7d28c1dfcd636a0810d45907c
Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar.glb differ
diff --git a/src/assets/avatars/BotPinky_Avatar_Unlit.glb b/src/assets/avatars/BotPinky_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..4029c8c6feb3dfe137d932ff0e7eb9223c60b6d1
Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotRobert_Avatar.glb b/src/assets/avatars/BotRobert_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..68d28e5a19616eae6e5e5c8694182efe02643c65
Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar.glb differ
diff --git a/src/assets/avatars/BotRobert_Avatar_Unlit.glb b/src/assets/avatars/BotRobert_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..505eae50b5604bdbe3a7076421dabd1847e561f1
Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotWoody_Avatar.glb b/src/assets/avatars/BotWoody_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..0a205ee9311b5c73f96f5fdf562970bd362b1a21
Binary files /dev/null and b/src/assets/avatars/BotWoody_Avatar.glb differ
diff --git a/src/assets/avatars/BotWoody_Avatar_Unlit.glb b/src/assets/avatars/BotWoody_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..bd330d47f269ccaae436fbb3a33aa85b51eee6fd
Binary files /dev/null and b/src/assets/avatars/BotWoody_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/avatars.js b/src/assets/avatars/avatars.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7a5500048710c5bf0910e5bc781000764871312
--- /dev/null
+++ b/src/assets/avatars/avatars.js
@@ -0,0 +1,65 @@
+export const avatars = [
+  {
+    "id": "botdefault",
+    "models": {
+      "low": `${ require("./BotDefault_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotDefault_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botbobo",
+    "models": {
+      "low": `${ require("./BotBobo_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotBobo_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botdom",
+    "models": {
+      "low": `${ require("./BotDom_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotDom_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botgreg",
+    "models": {
+      "low": `${ require("./BotGreg_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotGreg_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botguest",
+    "models": {
+      "low": `${ require("./BotGuest_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotGuest_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botjim",
+    "models": {
+      "low": `${ require("./BotJim_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotJim_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botpinky",
+    "models": {
+      "low": `${ require("./BotPinky_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotPinky_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botrobert",
+    "models": {
+      "low": `${ require("./BotRobert_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotRobert_Avatar.glb") }`
+    }
+  },
+  {
+    "id": "botwoody",
+    "models": {
+      "low": `${ require("./BotWoody_Avatar_Unlit.glb") }`,
+      "high": `${ require("./BotWoody_Avatar.glb") }`
+    }
+  }
+];
diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1663ec5b6ba92ef81c1b7fc9955f98dd0788272b
--- /dev/null
+++ b/src/assets/stylesheets/avatar-selector.scss
@@ -0,0 +1,45 @@
+@import 'fonts';
+@import 'shared';
+
+#selector-root {
+  height: 100%;
+}
+.avatar-selector {
+  overflow: hidden;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+
+  &__previous-button:focus, &__next-button:focus {
+    outline: none;
+  }
+  &__previous-button:active, &__next-button:active {
+    color: grey;
+  }
+  &__previous-button, &__next-button {
+    position: absolute;
+    top: 50%;
+    margin-top: -0.5em;
+    appearance: none;
+    -moz-appearance: none;
+    -webkit-appearance: none;
+    background: transparent;
+    color: white;
+    border: none;
+    font-size: 64pt;
+  }
+  &__previous-button {
+    left: 0.2em;
+  }
+  &__next-button {
+    right: 0.2em;
+  }
+  &__loading {
+    @extend %default-font;
+    display: block;
+    position: absolute;
+    top: 50%;
+    text-align: center;
+    color: white;
+  }
+}
diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss
index d30ffc98bc332d450d24dc29f3e2ebbcf5d21772..2915f56da8f98f3fc79bda2b147d3750179c5dbc 100644
--- a/src/assets/stylesheets/entry.scss
+++ b/src/assets/stylesheets/entry.scss
@@ -18,6 +18,29 @@
   flex-direction: column;
   flex: 10 1 auto;
   justify-content: center;
+
+  &__screen-sharing {
+	font-size: 1.4em;
+	margin-left: 2.95em;
+	margin-top: 0.6em;
+  }
+
+  &__screen-sharing-checkbox {
+	appearance: none;
+	-moz-appearance: none;
+	-webkit-appearance: none;
+	width: 2em;
+	height: 2em;
+	border: 3px solid white;
+	border-radius: 9px;
+	vertical-align: sub;
+	margin: 0 0.6em
+  }
+  &__screen-sharing-checkbox:checked {
+	border: 9px double white;
+	outline: 9px solid white;
+	outline-offset: -18px;
+  }
 }
 
 .entry-panel__secondary {
diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss
index 05b2e5d801f42cee41a9e245eb57daed200798cc..9b45afd389dd5006d1596ee11d94f41a1ee59650 100644
--- a/src/assets/stylesheets/profile.scss
+++ b/src/assets/stylesheets/profile.scss
@@ -8,17 +8,27 @@
   align-items: center;
   display: flex;
   pointer-events: auto;
+
+  &__avatar-selector {
+    border: none;
+    width: 95%;
+    height: 100%;
+    margin: 1em 0;
+  }
 }
 
 .profile-entry__box {
-  height: 150px;
   border-radius: 8px;
   display: flex;
   flex-direction: column;
   justify-content: space-between;
   align-items: center;
-  padding: 25px;
+  padding: 15px;
   flex: 1 1 100%;
+  width: 60vw;
+  min-width: 300px;
+  max-width: 700px;
+  height: 500px
 }
 
 .profile-entry__box--darkened {
@@ -42,6 +52,7 @@
   line-height: 2.0em;
   padding-left: 1.25em;
   padding-right: 1.25em;
+  margin: 0.5em 0;
 }
 
 .profile-entry__form-submit {
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 4251de47fb601c4b3c33c9297e419db38894dbc6..043290de7491a39a1d8f114be22913d915d1f06f 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -12,9 +12,11 @@
     "entry.daydream-prefix": "Enter on ",
     "entry.daydream-medium": "Daydream",
     "entry.daydream-via-chrome": "Using Google Chrome",
+    "entry.enable-screen-sharing": "Share my desktop",
     "profile.save": "SAVE",
     "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32",
     "profile.header": "Your identity",
+    "profile.avatar-selector.loading": "Loading Avatars...",
     "audio.title": "Test your audio",
     "audio.subtitle-desktop": "Confirm HMD speaker output",
     "audio.subtitle-mobile": "Earphones are recommended",
diff --git a/src/avatar-selector.html b/src/avatar-selector.html
new file mode 100644
index 0000000000000000000000000000000000000000..8496ef94d7f95ec90d29ae5c7e3c030c15659238
--- /dev/null
+++ b/src/avatar-selector.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+  <% if(NODE_ENV === "production") { %>
+    <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script>
+  <% } else { %>
+    <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.js"></script>
+  <% } %>
+</head>
+
+<body>
+  <div id="selector-root"></div>
+</body>
+
+</html>
diff --git a/src/avatar-selector.js b/src/avatar-selector.js
new file mode 100644
index 0000000000000000000000000000000000000000..6acfe154caebb1c9f799631940aeb918e945fd7e
--- /dev/null
+++ b/src/avatar-selector.js
@@ -0,0 +1,53 @@
+import ReactDOM from "react-dom";
+import React from "react";
+import queryString from "query-string";
+import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
+import en from "react-intl/locale-data/en";
+
+import "./assets/stylesheets/avatar-selector.scss";
+import "./vendor/GLTFLoader";
+
+import "./components/animation-mixer";
+import "./components/audio-feedback";
+import "./components/loop-animation";
+import "./elements/a-progressive-asset";
+import "./gltf-component-mappings";
+import { avatars } from "./assets/avatars/avatars.js";
+import { avatarIds } from "./utils/identity";
+
+import { App } from "./App";
+import AvatarSelector from "./react-components/avatar-selector";
+import localeData from "./assets/translations.data.json";
+
+window.APP = new App();
+const hash = queryString.parse(location.hash);
+const isMobile = AFRAME.utils.device.isMobile();
+if (hash.quality) {
+  window.APP.quality = hash.quality;
+} else {
+  window.APP.quality = isMobile ? "low" : "high";
+}
+
+const lang = ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage)
+  .toLowerCase()
+  .split(/[_-]+/)[0];
+addLocaleData([...en]);
+const messages = localeData[lang] || localeData.en;
+
+function postAvatarIdToParent(newAvatarId) {
+  window.parent.postMessage({ avatarId: newAvatarId }, location.origin);
+}
+
+function mountUI() {
+  const hash = queryString.parse(location.hash);
+  const avatarId = hash.avatar_id;
+  ReactDOM.render(
+    <IntlProvider locale={lang} messages={messages}>
+      <AvatarSelector {...{ avatars, avatarId, onChange: postAvatarIdToParent }} />
+    </IntlProvider>,
+    document.getElementById("selector-root")
+  );
+}
+
+window.addEventListener("hashchange", mountUI);
+document.addEventListener("DOMContentLoaded", mountUI);
diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js
index 03cfba4f6d7a835ac825804724a4109d16690cdb..51adcf2f7f2ddd6d0023c54442e03627ea952c70 100644
--- a/src/components/networked-video-player.js
+++ b/src/components/networked-video-player.js
@@ -1,3 +1,5 @@
+import queryString from "query-string";
+
 import styles from "./networked-video-player.css";
 
 const nafConnected = function() {
@@ -9,14 +11,6 @@ const nafConnected = function() {
 AFRAME.registerComponent("networked-video-player", {
   schema: {},
   async init() {
-    let container = document.getElementById("nvp-debug-container");
-    if (!container) {
-      container = document.createElement("div");
-      container.id = "nvp-debug-container";
-      container.classList.add(styles.container);
-      document.body.appendChild(container);
-    }
-
     await nafConnected();
 
     const networkedEl = await NAF.utils.getNetworkedEntity(this.el);
@@ -25,6 +19,24 @@ AFRAME.registerComponent("networked-video-player", {
     }
 
     const ownerId = networkedEl.components.networked.data.owner;
+
+    const qs = queryString.parse(location.search);
+    const rejectScreenShares = qs.accept_screen_shares === undefined;
+    if (ownerId !== NAF.clientId && rejectScreenShares) {
+      // Toggle material visibility since object visibility is network-synced
+      // TODO: There ought to be a better way to disable network syncs on a remote entity
+      this.el.setAttribute("material", {visible: false});
+      return;
+    }
+
+    let container = document.getElementById("nvp-debug-container");
+    if (!container) {
+      container = document.createElement("div");
+      container.id = "nvp-debug-container";
+      container.classList.add(styles.container);
+      document.body.appendChild(container);
+    }
+
     const stream = await NAF.connection.adapter.getMediaStream(ownerId, "video");
     if (!stream) {
       return;
diff --git a/src/components/player-info.js b/src/components/player-info.js
index 8cb265a67b9c67ac26510d6b8a03efb59703c931..ac2fc6b55fcdaf0215e26327368c07dd67e88fcc 100644
--- a/src/components/player-info.js
+++ b/src/components/player-info.js
@@ -1,7 +1,7 @@
 AFRAME.registerComponent("player-info", {
   schema: {
     displayName: { type: "string" },
-    avatar: { type: "string" }
+    avatarSrc: { type: "string" }
   },
   init() {
     this.applyProperties = this.applyProperties.bind(this);
@@ -24,8 +24,8 @@ AFRAME.registerComponent("player-info", {
     }
 
     const modelEl = this.el.querySelector(".model");
-    if (this.data.avatar && modelEl) {
-      modelEl.setAttribute("src", this.data.avatar);
+    if (this.data.avatarSrc && modelEl) {
+      modelEl.setAttribute("src", this.data.avatarSrc);
     }
   }
 });
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
index e88f7a76bf6ee4218f37997d9497e576b3bed55a..33489e2bc1792aa2b9dd4bddca360caaa39c509b 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/elements/a-gltf-entity.js
@@ -2,6 +2,9 @@ const GLTFCache = {};
 
 AFRAME.AGLTFEntity = {
   defaultInflator(el, componentName, componentData) {
+    if (!AFRAME.components[componentName]) {
+      throw new Error(`Inflator failed. "${componentName}" component does not exist.`);
+    }
     if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) {
       for (let i = 0; i < componentData.length; i++) {
         el.setAttribute(componentName + "__" + i, componentData[i]);
@@ -220,7 +223,10 @@ AFRAME.registerElement("a-gltf-entity", {
         this.querySelectorAll(":scope > template").forEach(templateEl =>
           this.templates.push({
             selector: templateEl.getAttribute("data-selector"),
-            templateRoot: document.importNode(templateEl.content.firstElementChild, true)
+            templateRoot: document.importNode(
+              templateEl.firstElementChild || templateEl.content.firstElementChild,
+              true
+            )
           })
         );
       }
@@ -232,6 +238,10 @@ AFRAME.registerElement("a-gltf-entity", {
           // If the src attribute is a selector, get the url from the asset item.
           if (src && src.charAt(0) === "#") {
             const assetEl = document.getElementById(src.substring(1));
+            if (!assetEl) { 
+              console.warn(`Attempted to use non-existent asset ${src} as src for`, this);
+              return;
+            }
 
             const fallbackSrc = assetEl.getAttribute("src");
             const highSrc = assetEl.getAttribute("high-src");
@@ -282,8 +292,7 @@ AFRAME.registerElement("a-gltf-entity", {
 
           this.emit("model-loaded", { format: "gltf", model: this.model });
         } catch (e) {
-          const message = (e && e.message) || "Failed to load glTF model";
-          console.error(message);
+          console.error("Failed to load glTF model", e.message, this);
           this.emit("model-error", { format: "gltf", src });
         }
       }
@@ -303,7 +312,6 @@ AFRAME.registerElement("a-gltf-entity", {
         if (attr === "src") {
           this.applySrc(newVal);
         }
-        AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal);
       }
     },
 
diff --git a/src/elements/a-progressive-asset.js b/src/elements/a-progressive-asset.js
index a367e374aece30163bc73802ab8cf11950f28c1a..8bf922aa17b10ce6eec09baf413e93dcf6d871f1 100644
--- a/src/elements/a-progressive-asset.js
+++ b/src/elements/a-progressive-asset.js
@@ -9,17 +9,17 @@ AFRAME.registerElement("a-progressive-asset", {
       value() {
         this.data = null;
         this.isAssetItem = true;
+      }
+    },
 
+    attachedCallback: {
+      value() {
         if (!this.parentNode.fileLoader) {
           throw new Error("a-progressive-asset must be the child of an a-assets element.");
         }
 
         this.fileLoader = this.parentNode.fileLoader;
-      }
-    },
 
-    attachedCallback: {
-      value() {
         const self = this;
         const fallbackSrc = this.getAttribute("src");
         const highSrc = this.getAttribute("high-src");
diff --git a/src/hub.html b/src/hub.html
index 6a66bb7ac9dbb44ec6018b391df1ead0a0ea3f06..fd276abaec43666e9f57842b8e96a0da8c6b1ead 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -30,13 +30,69 @@
             <img id="avatar"  src="./assets/hud/avatar.jpg" >
 
             <a-progressive-asset
-                id="bot-skinned-mesh"
+                id="botdefault"
                 response-type="arraybuffer"
                 src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
                 high-src="./assets/avatars/BotDefault_Avatar.glb"
                 low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
             ></a-progressive-asset>
-            <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item>
+            <a-progressive-asset
+                id="botbobo"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotBobo_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotBobo_Avatar.glb"
+                low-src="./assets/avatars/BotBobo_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botdom"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotDom_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotDom_Avatar.glb"
+                low-src="./assets/avatars/BotDom_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botgreg"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotGreg_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotGreg_Avatar.glb"
+                low-src="./assets/avatars/BotGreg_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botguest"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotGuest_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotGuest_Avatar.glb"
+                low-src="./assets/avatars/BotGuest_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botjim"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotJim_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotJim_Avatar.glb"
+                low-src="./assets/avatars/BotJim_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botpinky"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotPinky_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotPinky_Avatar.glb"
+                low-src="./assets/avatars/BotPinky_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botrobert"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotRobert_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotRobert_Avatar.glb"
+                low-src="./assets/avatars/BotRobert_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            <a-progressive-asset
+                id="botwoody"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotWoody_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotWoody_Avatar.glb"
+                low-src="./assets/avatars/BotWoody_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+
             <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item>
             <a-asset-item id="interactable-duck" response-type="arraybuffer" src="./assets/interactables/duck/DuckyMesh.glb"></a-asset-item>
 
@@ -45,7 +101,7 @@
             <!-- Templates -->
 
             <template id="video-template">
-                <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity>
+                <a-entity class="video" geometry="primitive: plane;" material="side: double; shader: flat;" networked-video-player></a-entity>
             </template>
 
             <template id="remote-avatar-template">
@@ -120,7 +176,7 @@
         <!-- Interactables -->
         <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity>
 
-        <a-entity 
+        <a-entity
             gltf-model="#interactable-duck"
             scale="2 2 2"
             class="interactable" 
@@ -211,7 +267,7 @@
             <a-gltf-entity class="model" inflate="true">
                 <template data-selector=".RootScene">
                     <a-entity
-                        ik-controller 
+                        ik-controller
                         animated-robot-hands
                         animation-mixer
                     ></a-entity>
@@ -287,12 +343,12 @@
             xr="ar: false"
         ></a-entity>
 
-        <a-cylinder 
-            position="0 0.45 0" 
-            material="visible: false" 
-            height="1" radius="3.1" 
-            segments-radial="12" 
-            static-body 
+        <a-cylinder
+            position="0 0.45 0"
+            material="visible: false"
+            height="1" radius="3.1"
+            segments-radial="12"
+            static-body
             class="collidable"
         ></a-cylinder>
 
diff --git a/src/hub.js b/src/hub.js
index b0899d5dda41acbe5aa2a88582f91556dba4648b..887e470250fd224c616ec66e8f53e2ceaea703fe 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -87,6 +87,12 @@ import { generateDefaultProfile } from "./utils/identity.js";
 import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
 import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 
+function qsTruthy(param) {
+  const val = qs[param];
+  // if the param exists but is not set (e.g. "?foo&bar"), its value is null.
+  return val === null || /1|on|true/i.test(val);
+}
+
 registerTelemetry();
 
 AFRAME.registerInputBehaviour("vive_trackpad_dpad4", vive_trackpad_dpad4);
@@ -103,44 +109,17 @@ concurrentLoadDetector.start();
 // Always layer in any new default profile bits
 store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } });
 
-async function shareMedia(audio, video) {
-  const constraints = {
-    audio: !!audio,
-    video: video ? { mediaSource: "screen", height: 720, frameRate: 30 } : false
-  };
-  const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
-  NAF.connection.adapter.setLocalMediaStream(mediaStream);
-
-  const id = `${NAF.clientId}-screen`;
-  let entity = document.getElementById(id);
-  if (entity) {
-    entity.setAttribute("visible", !!video);
-  } else if (video) {
-    const sceneEl = document.querySelector("a-scene");
-    entity = document.createElement("a-entity");
-    entity.id = id;
-    entity.setAttribute("offset-relative-to", {
-      target: "#player-camera",
-      offset: "0 0 -2",
-      on: "action_share_screen"
-    });
-    entity.setAttribute("networked", { template: "#video-template" });
-    sceneEl.appendChild(entity);
-  }
-}
-
 async function exitScene() {
   const scene = document.querySelector("a-scene");
   scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
   document.body.removeChild(scene);
 }
 
-function updatePlayerInfoFromStore() {
+function applyProfileFromStore(playerRig) {
   const displayName = store.state.profile.display_name;
-  const playerRig = document.querySelector("#player-rig");
   playerRig.setAttribute("player-info", {
     displayName,
-    avatar: qs.avatar || "#bot-skinned-mesh"
+    avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault")
   });
   document.querySelector("a-scene").emit("username-changed", { username: displayName });
 }
@@ -148,7 +127,6 @@ function updatePlayerInfoFromStore() {
 async function enterScene(mediaStream, enterInVR, janusRoomId) {
   const scene = document.querySelector("a-scene");
   const playerRig = document.querySelector("#player-rig");
-
   document.querySelector("a-scene canvas").classList.remove("blurred");
   registerNetworkSchemas();
 
@@ -169,32 +147,46 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
     serverURL: process.env.JANUS_SERVER
   });
 
-  if (!qs.stats || !/off|false|0/.test(qs.stats)) {
+  if (!qsTruthy("no_stats")) {
     scene.setAttribute("stats", true);
   }
 
-  if (isMobile || qs.mobile) {
+  if (isMobile || qsTruthy(qs.mobile)) {
     playerRig.setAttribute("virtual-gamepad-controls", {});
   }
 
-  updatePlayerInfoFromStore();
-  store.addEventListener("statechanged", updatePlayerInfoFromStore);
+  const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig);
+  applyProfileOnPlayerRig();
+  store.addEventListener("statechanged", applyProfileOnPlayerRig);
 
-  const avatarScale = parseInt(qs.avatarScale, 10);
+  const avatarScale = parseInt(qs.avatar_scale, 10);
 
   if (avatarScale) {
     playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale });
   }
 
-  let sharingScreen = false;
+  const videoTracks = mediaStream.getVideoTracks();
+  let sharingScreen = videoTracks.length > 0;
+
+  const screenEntityId = `${NAF.clientId}-screen`;
+  let screenEntity = document.getElementById(screenEntityId);
 
-  // TODO remove
   scene.addEventListener("action_share_screen", () => {
     sharingScreen = !sharingScreen;
-    shareMedia(true, sharingScreen);
+    if (sharingScreen) {
+      for (const track of videoTracks) {
+        mediaStream.addTrack(track);
+      }
+    } else {
+      for (const track of mediaStream.getVideoTracks()) {
+        mediaStream.removeTrack(track);
+      }
+    }
+    NAF.connection.adapter.setLocalMediaStream(mediaStream);
+    screenEntity.setAttribute("visible", sharingScreen);
   });
 
-  if (qs.offline) {
+  if (qsTruthy("offline")) {
     onConnect();
   } else {
     document.body.addEventListener("connected", onConnect);
@@ -204,23 +196,19 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
     if (mediaStream) {
       NAF.connection.adapter.setLocalMediaStream(mediaStream);
 
-      const hasVideo = !!(mediaStream.getVideoTracks().length > 0);
-
-      const id = `${NAF.clientId}-screen`;
-      let entity = document.getElementById(id);
-      if (entity) {
-        entity.setAttribute("visible", hasVideo);
-      } else if (hasVideo) {
+      if (screenEntity) {
+        screenEntity.setAttribute("visible", sharingScreen);
+      } else if (sharingScreen) {
         const sceneEl = document.querySelector("a-scene");
-        entity = document.createElement("a-entity");
-        entity.id = id;
-        entity.setAttribute("offset-relative-to", {
-          target: "#head",
+        screenEntity = document.createElement("a-entity");
+        screenEntity.id = screenEntityId;
+        screenEntity.setAttribute("offset-relative-to", {
+          target: "#player-camera",
           offset: "0 0 -2",
           on: "action_share_screen"
         });
-        entity.setAttribute("networked", { template: "#video-template" });
-        sceneEl.appendChild(entity);
+        screenEntity.setAttribute("networked", { template: "#video-template" });
+        sceneEl.appendChild(screenEntity);
       }
     }
   }
@@ -229,13 +217,9 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
 function onConnect() {}
 
 function mountUI(scene) {
-  const disableAutoExitOnConcurrentLoad = qs.allow_multi === "true";
-
-  let forcedVREntryType = null;
-
-  if (qs.vr_entry_type) {
-    forcedVREntryType = qs.vr_entry_type;
-  }
+  const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
+  const forcedVREntryType = qs.vr_entry_type || null;
+  const enableScreenSharing = qsTruthy("enable_screen_sharing");
 
   const uiRoot = ReactDOM.render(
     <UIRoot
@@ -246,6 +230,7 @@ function mountUI(scene) {
         concurrentLoadDetector,
         disableAutoExitOnConcurrentLoad,
         forcedVREntryType,
+        enableScreenSharing,
         store
       }}
     />,
diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js
new file mode 100644
index 0000000000000000000000000000000000000000..7cf71b7c5e130c2bbd16a54e5b247f3a1cea74d5
--- /dev/null
+++ b/src/react-components/avatar-selector.js
@@ -0,0 +1,127 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { injectIntl, FormattedMessage } from 'react-intl';
+import FontAwesomeIcon from '@fortawesome/react-fontawesome';
+import faAngleLeft from '@fortawesome/fontawesome-free-solid/faAngleLeft';
+import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight';
+import meetingSpace from '../assets/environments/MeetingSpace1_mesh.glb';
+
+class AvatarSelector extends Component {
+  static propTypes = {
+    avatars: PropTypes.array,
+    avatarId: PropTypes.string,
+    onChange: PropTypes.func,
+  }
+
+  getAvatarIndex = (direction=0) => {
+    const currAvatarIndex = this.props.avatars.findIndex(avatar => avatar.id === this.props.avatarId);
+    const numAvatars = this.props.avatars.length;
+    return ((currAvatarIndex + direction) % numAvatars + numAvatars) % numAvatars;
+  }
+  nextAvatarIndex = () => this.getAvatarIndex(1)
+  previousAvatarIndex = () => this.getAvatarIndex(-1)
+
+  emitChangeToNext = () => {
+    const nextAvatarId = this.props.avatars[this.nextAvatarIndex()].id;
+    this.props.onChange(nextAvatarId);
+  }
+
+  emitChangeToPrevious = () => {
+    const previousAvatarId = this.props.avatars[this.previousAvatarIndex()].id;
+    this.props.onChange(previousAvatarId);
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.avatarId !== prevProps.avatarId) { 
+      // HACK - a-animation ought to restart the animation when the `to` attribute changes, but it doesn't
+      // so we need to force it here.
+      const currRot = this.animation.parentNode.getAttribute('rotation');
+      this.animation.setAttribute('from', `${currRot.x} ${currRot.y} ${currRot.z}`);
+      this.animation.stop();
+      this.animation.handleMixinUpdate();
+      this.animation.start();
+    }
+  }
+
+  render () {
+    const avatarAssets = this.props.avatars.map(avatar => (
+      <a-progressive-asset
+        id={avatar.id}
+        key={avatar.id}
+        response-type="arraybuffer"
+        high-src={`${avatar.models.high}`}
+        low-src={`${avatar.models.low}`}
+      ></a-progressive-asset>
+    ));
+
+    const avatarEntities = this.props.avatars.map((avatar, i) => (
+      <a-entity key={avatar.id} position="0 0 0" rotation={`0 ${360 * -i / this.props.avatars.length} 0`}>
+        <a-gltf-entity position="0 0 5" rotation="0 0 0" src={'#' + avatar.id} inflate="true">
+          <template data-selector=".RootScene">
+            <a-entity animation-mixer></a-entity>
+          </template>
+          <a-animation
+            attribute="rotation"
+            dur="2000"
+            to={`0 ${this.getAvatarIndex() === i ? 360 : 0} 0`}
+            repeat="indefinite">
+          </a-animation>
+        </a-gltf-entity>
+      </a-entity>
+    ));
+
+    return (
+      <div className="avatar-selector">
+      <span className="avatar-selector__loading">
+        <FormattedMessage id="profile.avatar-selector.loading"/>
+      </span>
+      <a-scene vr-mode-ui="enabled: false" ref={sce => this.scene = sce}>
+        <a-assets>
+          {avatarAssets}
+          <a-asset-item
+            id="meeting-space1-mesh"
+            response-type="arraybuffer"
+            src={meetingSpace}
+          ></a-asset-item>
+        </a-assets>
+
+        <a-entity>
+          <a-animation
+            ref={anm => this.animation = anm}
+            attribute="rotation"
+            dur="1000"
+            easing="ease-out"
+            to={`0 ${360 * this.getAvatarIndex() / this.props.avatars.length + 180} 0`}>
+          </a-animation>
+          {avatarEntities}
+        </a-entity>
+
+        <a-entity position="0 1.5 -5.6" rotation="-10 180 0" camera></a-entity>
+
+        <a-entity
+          hide-when-quality="low"
+          light="type: directional; color: #F9FFCE; intensity: 0.6"
+          position="0 5 -15"
+        ></a-entity>
+        <a-entity
+          hide-when-quality="low"
+          light="type: ambient; color: #FFF"
+        ></a-entity>
+        <a-gltf-entity
+          id="meeting-space"
+          src="#meeting-space1-mesh"
+          position="0 0 0"
+        ></a-gltf-entity>
+      </a-scene>
+      <button className="avatar-selector__previous-button" onClick={this.emitChangeToPrevious}>
+        <FontAwesomeIcon icon={faAngleLeft} />
+      </button>
+      <button className="avatar-selector__next-button" onClick={this.emitChangeToNext}>
+        <FontAwesomeIcon icon={faAngleRight} />
+      </button>
+      </div>
+    );
+  }
+}
+
+export default injectIntl(AvatarSelector);
diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js
index 28c8cd7d9ce8c8bf9902f6a654c835bbd7fe95ea..91044c27cab1955c7e35698322c618b907a3f95d 100644
--- a/src/react-components/profile-entry-panel.js
+++ b/src/react-components/profile-entry-panel.js
@@ -13,29 +13,50 @@ class ProfileEntryPanel extends Component {
   constructor(props) {
     super(props);
     window.store = this.props.store;
-    this.state = {name: this.props.store.state.profile.display_name};
+    this.state = {
+      display_name: this.props.store.state.profile.display_name,
+      avatar_id: this.props.store.state.profile.avatar_id,
+    };
     this.props.store.addEventListener("statechanged", this.storeUpdated);
   }
 
   storeUpdated = () => {
-    this.setState({name: this.props.store.state.profile.display_name});
+    const { avatar_id, display_name } = this.props.store.state.profile;
+    this.setState({ avatar_id, display_name });
   }
 
-  saveName = (e) => {
+  saveStateAndFinish = (e) => {
     e.preventDefault();
-    this.props.store.update({ profile: { display_name: this.nameInput.value } });
+    this.props.store.update({profile: {
+      display_name: this.state.display_name,
+      avatar_id: this.state.avatar_id
+    }});
     this.props.finished();
   }
 
+  stopPropagation = (e) => {
+    e.stopPropagation();
+  }
+
+  setAvatarStateFromIframeMessage = (e) => {
+    if (e.source !== this.avatarSelector.contentWindow) { return; }
+    this.setState({avatar_id: e.data.avatarId});
+  }
+
   componentDidMount() {
     // stop propagation so that avatar doesn't move when wasd'ing during text input.
-    this.nameInput.addEventListener('keydown', e => e.stopPropagation());
-    this.nameInput.addEventListener('keypress', e => e.stopPropagation());
-    this.nameInput.addEventListener('keyup', e => e.stopPropagation());
+    this.nameInput.addEventListener('keydown', this.stopPropagation);
+    this.nameInput.addEventListener('keypress', this.stopPropagation);
+    this.nameInput.addEventListener('keyup', this.stopPropagation);
+    window.addEventListener('message', this.setAvatarStateFromIframeMessage);
   }
-  
+
   componentWillUnmount() {
     this.props.store.removeEventListener('statechanged', this.storeUpdated);
+    this.nameInput.removeEventListener('keydown', this.stopPropagation);
+    this.nameInput.removeEventListener('keypress', this.stopPropagation);
+    this.nameInput.removeEventListener('keyup', this.stopPropagation);
+    window.removeEventListener('message', this.setAvatarStateFromIframeMessage);
   }
 
   render () {
@@ -43,19 +64,26 @@ class ProfileEntryPanel extends Component {
 
     return (
       <div className="profile-entry">
-        <form onSubmit={this.saveName}>
+        <form onSubmit={this.saveStateAndFinish}>
         <div className="profile-entry__box profile-entry__box--darkened">
           <div className="profile-entry__subtitle">
             <FormattedMessage id="profile.header"/>
           </div>
           <input
             className="profile-entry__form-field-text"
-            value={this.state.name} onChange={(e) => this.setState({name: e.target.value})}
+            value={this.state.display_name} onChange={(e) => this.setState({display_name: e.target.value})}
             required pattern={SCHEMA.definitions.profile.properties.display_name.pattern}
             title={formatMessage({ id: "profile.display_name.validation_warning" })}
             ref={inp => this.nameInput = inp}/>
+          <iframe
+            className="profile-entry__avatar-selector"
+            src={
+              /* HACK: Have to account for the smoke test server like this. Feels wrong though. */
+              `${/smoke/i.test(location.hostname) ? 'smoke-' : ''}avatar-selector.html#avatar_id=${this.state.avatar_id}`
+            }
+            ref={ifr => this.avatarSelector = ifr}></iframe>
           <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" }) }/>
-          </div>
+        </div>
         </form>
       </div>
     );
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index f2d6f9102d23ccb25375abf75bf1733bb2b28375..7cca41e23076270371bb33ed3ce37d7b08fa5a38 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -41,11 +41,6 @@ async function grantedMicLabels() {
   return mediaDevices.filter(d => d.label && d.kind === "audioinput").map(d => d.label);
 }
 
-async function hasGrantedMicPermissions() {
-  const micLabels = await grantedMicLabels();
-  return micLabels.length > 0;
-}
-
 // This is a list of regexes that match the microphone labels of HMDs.
 //
 // If entering VR mode, and if any of these regexes match an audio device,
@@ -64,6 +59,7 @@ class UIRoot extends Component {
     concurrentLoadDetector: PropTypes.object,
     disableAutoExitOnConcurrentLoad: PropTypes.bool,
     forcedVREntryType: PropTypes.string,
+    enableScreenSharing: PropTypes.bool,
     store: PropTypes.object,
     scene: PropTypes.object
   };
@@ -74,7 +70,10 @@ class UIRoot extends Component {
     enterInVR: false,
 
     shareScreen: false,
+    requestedScreen: false,
     mediaStream: null,
+    videoTrack: null,
+    audioTrack: null,
 
     toneInterval: null,
     tonePlaying: false,
@@ -212,12 +211,27 @@ class UIRoot extends Component {
     });
   };
 
+  hasGrantedMicPermissions = async () => {
+    if (this.state.requestedScreen) {
+      // There is no way to tell if you've granted mic permissions in a previous session if we've 
+      // already prompted for screen sharing permissions, so we have to assume that we've never granted permissions.
+      // Fortunately, if you *have* granted permissions permanently, there won't be a second browser prompt, but we 
+      // can't determine that before hand.
+      // See https://bugzilla.mozilla.org/show_bug.cgi?id=1449783 for a potential solution in the future.
+      return false;
+    }
+    else {
+      // If we haven't requested the screen in this session, check if we've granted permissions in a previous session.
+      return (await grantedMicLabels()).length > 0;
+    }
+  }
+
   performDirectEntryFlow = async enterInVR => {
     this.startTestTone();
 
     this.setState({ enterInVR });
 
-    const hasGrantedMic = await hasGrantedMicPermissions();
+    const hasGrantedMic = await this.hasGrantedMicPermissions();
 
     if (hasGrantedMic) {
       await this.setMediaStreamToDefault();
@@ -271,37 +285,60 @@ class UIRoot extends Component {
     }
   };
 
-  mediaVideoConstraint = () => {
-    return this.state.shareScreen ? { mediaSource: "screen", height: 720, frameRate: 30 } : false;
-  };
-
   micDeviceChanged = async ev => {
-    const constraints = { audio: { deviceId: { exact: [ev.target.value] } }, video: this.mediaVideoConstraint() };
-    await this.setupNewMediaStream(constraints);
-  };
+    const constraints = { audio: { deviceId: { exact: [ev.target.value] } } };
+    await this.fetchAudioTrack(constraints);
+    await this.setupNewMediaStream();
+  }
 
   setMediaStreamToDefault = async () => {
-    await this.setupNewMediaStream({ audio: true, video: false });
-  };
-
-  setupNewMediaStream = async constraints => {
-    const AudioContext = window.AudioContext || window.webkitAudioContext;
-    const audioContext = new AudioContext();
+    await this.fetchAudioTrack({ audio: true });
+    await this.setupNewMediaStream();
+  }
 
-    if (this.state.mediaStream) {
-      clearInterval(this.state.micUpdateInterval);
+  setStateAndRequestScreen = async e => {
+    const checked = e.target.checked;
+    await this.setState({ requestedScreen: true, shareScreen: checked });
+    if (checked) {
+      this.fetchVideoTrack({ video: {
+        mediaSource: "screen", 
+        // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything
+        // other than your current monitor that has a different aspect ratio.
+        width: screen.width / screen.height * 720, 
+        height: 720,
+        frameRate: 30 
+      } });
+    }
+    else {
+      this.setState({ videoTrack: null });
+    }
+  }
 
-      const previousStream = this.state.mediaStream;
+  fetchVideoTrack = async constraints => {
+    const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
+    this.setState({ videoTrack: mediaStream.getVideoTracks()[0] });
+  }
 
-      for (const tracks of [previousStream.getAudioTracks(), previousStream.getVideoTracks()]) {
-        for (const track of tracks) {
-          track.stop();
-        }
-      }
+  fetchAudioTrack = async constraints => {
+    if (this.state.audioTrack) {
+      this.state.audioTrack.stop();
     }
-
     const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
+    this.setState({ audioTrack: mediaStream.getAudioTracks()[0] });
+  }
+
+  setupNewMediaStream = async constraints => {
+    const mediaStream = new MediaStream();
+
+    // we should definitely have an audioTrack at this point. 
+    mediaStream.addTrack(this.state.audioTrack);
 
+    if (this.state.videoTrack) {
+      mediaStream.addTrack(this.state.videoTrack);
+    }
+
+    const AudioContext = window.AudioContext || window.webkitAudioContext;
+    const audioContext = new AudioContext();
     const source = audioContext.createMediaStreamSource(mediaStream);
     const analyzer = audioContext.createAnalyser();
     const levels = new Uint8Array(analyzer.fftSize);
@@ -346,8 +383,10 @@ class UIRoot extends Component {
 
   fetchMicDevices = async () => {
     const mediaDevices = await navigator.mediaDevices.enumerateDevices();
-    this.setState({
-      micDevices: mediaDevices.filter(d => d.kind === "audioinput").map(d => ({ deviceId: d.deviceId, label: d.label }))
+    this.setState({ 
+      micDevices: mediaDevices.
+        filter(d => d.kind === "audioinput").
+        map(d => ({ deviceId: d.deviceId, label: d.label }))
     });
   };
 
@@ -430,29 +469,47 @@ class UIRoot extends Component {
 
     const daydreamMaybeSubtitle = messages["entry.daydream-via-chrome"];
 
-    const entryPanel =
+    // Only show this in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and
+    // will attempt to share your webcam instead!
+    const screenSharingCheckbox = ( 
+      this.props.enableScreenSharing &&
+      !mobiledetect.mobile() && 
+      /firefox/i.test(navigator.userAgent) &&
+      (
+        <label className="entry-panel__screen-sharing">
+          <input className="entry-panel__screen-sharing-checkbox" type="checkbox"
+            value={this.state.shareScreen}
+            onChange={this.setStateAndRequestScreen}
+          />
+          <FormattedMessage id="entry.enable-screen-sharing" />
+        </label>
+      ) 
+    );
+
+    const entryPanel = 
       this.state.entryStep === ENTRY_STEPS.start ? (
         <div className="entry-panel">
           <TwoDEntryButton onClick={this.enter2D} />
-          {this.state.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
-            <GenericEntryButton onClick={this.enterVR} />
+          { this.state.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
+            <GenericEntryButton onClick={this.enterVR} /> 
           )}
-          {this.state.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && (
-            <GearVREntryButton onClick={this.enterGearVR} />
+          { this.state.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && (
+            <GearVREntryButton onClick={this.enterGearVR} /> 
           )}
-          {this.state.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && (
+          { this.state.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && (
             <DaydreamEntryButton
               onClick={this.enterDaydream}
               subtitle={
-                this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : ""
+                this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" 
               }
-            />
+            /> 
           )}
           {this.state.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
             <div className="entry-panel__secondary" onClick={this.enterVR}>
               <FormattedMessage id="entry.cardboard" />
             </div>
           )}
+          { screenSharingCheckbox }
         </div>
       ) : null;
 
diff --git a/src/storage/store.js b/src/storage/store.js
index 5b00b7ac8f5570c28651587997ae4c0546a3b722..037e27bf1d9603a210423baa5ee6543c2ee77fdb 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -17,6 +17,7 @@ export const SCHEMA = {
       additionalProperties: false,
       properties: {
         display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" },
+        avatar_id: { type: "string" },
       }
     }
   },
diff --git a/src/utils/identity.js b/src/utils/identity.js
index 72cabdf008bc2574b4408d62c8f7f17f30fc9a59..118fe30812c1013cb64c47a335c24465a12f6636 100644
--- a/src/utils/identity.js
+++ b/src/utils/identity.js
@@ -1,3 +1,5 @@
+import { avatars } from "../assets/avatars/avatars.js";
+
 const names = [
   "albattani",
   "allen",
@@ -161,7 +163,16 @@ const names = [
   "yonath"
 ];
 
+function selectRandom(arr) {
+   return arr[Math.floor(Math.random() * arr.length)]
+}
+
+export const avatarIds = avatars.map(av => av.id);
+
 export function generateDefaultProfile() {
-  const name = names[Math.floor(Math.random() * names.length)];
-  return { display_name: name.replace(/^./, name[0].toUpperCase()) };
+  const name = selectRandom(names);
+  return {
+    display_name: name.replace(/^./, name[0].toUpperCase()) ,
+    avatar_id: selectRandom(avatarIds)
+  };
 }
diff --git a/webpack.config.js b/webpack.config.js
index a2fa5d229a890fb45261b6093305c725f11ed647..1fdc22406d4a3ab182b072c6aca32e96cb4d6b23 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -77,7 +77,8 @@ class LodashTemplatePlugin {
 const config = {
   entry: {
     index: path.join(__dirname, "src", "index.js"),
-    hub: path.join(__dirname, "src", "hub.js")
+    hub: path.join(__dirname, "src", "hub.js"),
+    "avatar-selector": path.join(__dirname, "src", "avatar-selector.js")
   },
   output: {
     path: path.join(__dirname, "public"),
@@ -205,6 +206,12 @@ const config = {
           chunks: []
         })
     ),
+    new HTMLWebpackPlugin({
+      filename: "avatar-selector.html",
+      template: path.join(__dirname, "src", "avatar-selector.html"),
+      chunks: ["avatar-selector"],
+      inject: "head"
+    }),
     // Extract required css and add a content hash.
     new ExtractTextPlugin("assets/stylesheets/[name]-[contenthash].css", {
       disable: process.env.NODE_ENV !== "production"
diff --git a/yarn.lock b/yarn.lock
index cce545356bdd3a422811544e84dc33aafcbf63db..18f7727e7dd7217a0e581df22c2ddfc12a8ff16a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -78,6 +78,28 @@
     lodash "^4.2.0"
     to-fast-properties "^2.0.0"
 
+"@fortawesome/fontawesome-common-types@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.3.tgz#8475e0f2d1ad1f858c4ec2e76ed9a2456a09ad83"
+
+"@fortawesome/fontawesome-free-solid@^5.0.9":
+  version "5.0.9"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-solid/-/fontawesome-free-solid-5.0.9.tgz#456155a1cd82a0342ffe6a869d5a54fdadd78548"
+  dependencies:
+    "@fortawesome/fontawesome-common-types" "^0.1.3"
+
+"@fortawesome/fontawesome@^1.1.5":
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome/-/fontawesome-1.1.5.tgz#c7cfafdd3364245626293cc670357f9fb8487170"
+  dependencies:
+    "@fortawesome/fontawesome-common-types" "^0.1.3"
+
+"@fortawesome/react-fontawesome@^0.0.18":
+  version "0.0.18"
+  resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.0.18.tgz#4e0eb1cf9797715a67bb7705ae084fa0a410f185"
+  dependencies:
+    humps "^2.0.1"
+
 JSONStream@^1.0.3:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
@@ -3932,6 +3954,10 @@ https-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
 
+humps@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa"
+
 iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
   version "0.4.19"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"