diff --git a/package-lock.json b/package-lock.json
index b6cc7770d591bbbc02bab06845da018763d8669d..d34036b8666bbc53b52294a633bdc23fcf512a8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -587,10 +587,6 @@
       "version": "github:mozillareality/aframe-teleport-controls#14f296cad85cea6d15ee5ba08b142526ff9573f4",
       "from": "github:mozillareality/aframe-teleport-controls#hubs/master"
     },
-    "aframe-xr": {
-      "version": "github:brianpeiris/aframe-xr#3162aed9f054b5a604e46a74a4f8599d4ac2ad58",
-      "from": "github:brianpeiris/aframe-xr#3162aed"
-    },
     "after": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
@@ -4183,6 +4179,11 @@
         "es6-symbol": "^3.1.1"
       }
     },
+    "es6-promise": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+      "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y="
+    },
     "es6-symbol": {
       "version": "3.1.1",
       "resolved": "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz",
@@ -5254,14 +5255,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -5276,20 +5275,17 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -5406,8 +5402,7 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -5419,7 +5414,6 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -5434,7 +5428,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -5442,14 +5435,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -5468,7 +5459,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -5549,8 +5539,7 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5562,7 +5551,6 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5684,7 +5672,6 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -6696,6 +6683,11 @@
       "integrity": "sha1-Cpf7h2mG6AgcYxFg+PnziRV/AEM=",
       "dev": true
     },
+    "immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+    },
     "import-lazy": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz",
@@ -7633,6 +7625,48 @@
         "array-includes": "^3.0.3"
       }
     },
+    "jszip": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
+      "integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
+      "requires": {
+        "core-js": "~2.3.0",
+        "es6-promise": "~3.0.2",
+        "lie": "~3.1.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.0.6"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+          "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU="
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+          "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+        },
+        "readable-stream": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+          "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~1.0.6",
+            "string_decoder": "~0.10.x",
+            "util-deprecate": "~1.0.1"
+          }
+        }
+      }
+    },
     "keyv": {
       "version": "3.0.0",
       "resolved": "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz",
@@ -7691,6 +7725,14 @@
         "type-check": "~0.3.2"
       }
     },
+    "lie": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
+      "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
+      "requires": {
+        "immediate": "~3.0.5"
+      }
+    },
     "listr": {
       "version": "0.14.1",
       "resolved": "https://registry.yarnpkg.com/listr/-/listr-0.14.1.tgz",
@@ -9471,8 +9513,7 @@
     "pako": {
       "version": "1.0.6",
       "resolved": "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz",
-      "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg=",
-      "dev": true
+      "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg="
     },
     "parallel-transform": {
       "version": "1.1.0",
@@ -13108,8 +13149,7 @@
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
-      "dev": true
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
     },
     "util.promisify": {
       "version": "1.0.0",
diff --git a/package.json b/package.json
index f31e683cdb704e79b8d4016b2865bc057ecc8803..631190d49cb0fdd1e75c4b566a84d91654de8ca2 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
     "doc": "node ./scripts/doc/build.js",
     "prettier": "prettier --write '*.js' 'src/**/*.js'",
     "lint:js": "eslint '*.js' 'scripts/**/*.js' 'src/**/*.js'",
-    "lint:html": "node ./scripts/lint-html.js 'src/**/*.html'",
+    "lint:html": "htmlhint 'src/**/*.html'",
     "lint": "npm run lint:js && npm run lint:html"
   },
   "dependencies": {
@@ -36,7 +36,6 @@
     "aframe-rounded": "^1.0.3",
     "aframe-slice9-component": "^1.0.0",
     "aframe-teleport-controls": "github:mozillareality/aframe-teleport-controls#hubs/master",
-    "aframe-xr": "github:brianpeiris/aframe-xr#3162aed",
     "classnames": "^2.2.5",
     "copy-to-clipboard": "^3.0.8",
     "deepmerge": "^2.1.1",
@@ -44,6 +43,7 @@
     "event-target-shim": "^3.0.1",
     "form-urlencoded": "^2.0.4",
     "jsonschema": "^1.2.2",
+    "jszip": "^3.1.5",
     "moving-average": "^1.0.0",
     "naf-janus-adapter": "^0.10.1",
     "networked-aframe": "github:mozillareality/networked-aframe#mr-social-client/master",
@@ -80,7 +80,6 @@
     "html-loader": "^0.5.5",
     "html-webpack-plugin": "^3.1.0",
     "htmlhint": "^0.9.13",
-    "lodash": "^4.17.5",
     "node-sass": "^4.7.2",
     "prettier": "^1.7.0",
     "rimraf": "^2.6.2",
diff --git a/scripts/lint-html.js b/scripts/lint-html.js
deleted file mode 100644
index d8891e836d3e9ed9cf0a8b7dea0aee107188a6fe..0000000000000000000000000000000000000000
--- a/scripts/lint-html.js
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env node
-
-const { promisify } = require("util");
-const fs = require("fs");
-const mkdtemp = promisify(fs.mkdtemp);
-const path = require("path");
-const os = require("os");
-const shell = require("shelljs");
-
-(async function() {
-  function lintFile(tempDir, arg, file) {
-    const out = path.join(tempDir, file);
-    shell.mkdir("-p", path.dirname(out));
-    shell.sed(/<%.+%>/, "", file).to(out);
-    const result = shell.exec(`node_modules/.bin/htmlhint ${arg} --config=.htmlhintrc ${out}`);
-    return result.code;
-  }
-
-  let result = 0;
-  if (process.argv.length > 2) {
-    const tempDir = await mkdtemp(path.join(os.tmpdir(), "lint-html-"));
-    let files;
-    let arg = "";
-    if (process.argv.length === 4) {
-      arg = process.argv[2];
-      files = process.argv[3];
-    } else {
-      files = process.argv[2];
-    }
-    const results = shell.ls(files).map(lintFile.bind(null, tempDir, arg));
-    result = results.reduce((a, r) => a + r, 0);
-    shell.rm("-r", tempDir);
-  }
-
-  shell.exit(result);
-})();
diff --git a/src/components/audio-feedback.js b/src/components/audio-feedback.js
index cdae3ef89a7b42c4745e2227a08836bc5388380b..71798ba4e05f81398f73942176fa1a94671bfef6 100644
--- a/src/components/audio-feedback.js
+++ b/src/components/audio-feedback.js
@@ -8,11 +8,12 @@ AFRAME.registerComponent("networked-audio-analyser", {
     this.volume = 0;
     this.prevVolume = 0;
     this.smoothing = 0.3;
+    this.threshold = 0.01;
     this.el.addEventListener("sound-source-set", event => {
       const ctx = THREE.AudioContext.getContext();
       this.analyser = ctx.createAnalyser();
       this.analyser.fftSize = 32;
-      this.levels = new Float32Array(this.analyser.frequencyBinCount);
+      this.levels = new Uint8Array(this.analyser.frequencyBinCount);
       event.detail.soundSource.connect(this.analyser);
     });
   },
@@ -20,14 +21,19 @@ AFRAME.registerComponent("networked-audio-analyser", {
   tick: function() {
     if (!this.analyser) return;
 
-    this.analyser.getFloatTimeDomainData(this.levels);
+    // take care with compatibility, e.g. safari doesn't support getFloatTimeDomainData
+    this.analyser.getByteTimeDomainData(this.levels);
 
     let sum = 0;
     for (let i = 0; i < this.levels.length; i++) {
-      const amplitude = this.levels[i];
+      const amplitude = (this.levels[i] - 128) / 128;
       sum += amplitude * amplitude;
     }
-    this.volume = this.smoothing * Math.sqrt(sum / this.levels.length) + (1 - this.smoothing) * this.prevVolume;
+    let currVolume = Math.sqrt(sum / this.levels.length);
+    if (currVolume < this.threshold) {
+      currVolume = 0;
+    }
+    this.volume = this.smoothing * currVolume + (1 - this.smoothing) * this.prevVolume;
     this.prevVolume = this.volume;
   }
 });
diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js
index 5e54e729e38c6d2360059e04f8fa4cb827d7b86f..b5904f0fc36c9695908f56017772449ee99dd7e3 100644
--- a/src/components/gltf-model-plus.js
+++ b/src/components/gltf-model-plus.js
@@ -1,3 +1,5 @@
+import SketchfabZipWorker from "../workers/sketchfab-zip.worker.js";
+
 const GLTFCache = {};
 
 AFRAME.GLTFModelPlus = {
@@ -181,14 +183,41 @@ function nextTick() {
   });
 }
 
-function cachedLoadGLTF(src, basePath, preferredTechnique, onProgress) {
-  // Load the gltf model from the cache if it exists.
+function getFilesFromSketchfabZip(src) {
+  return new Promise((resolve, reject) => {
+    const worker = new SketchfabZipWorker();
+    worker.onmessage = e => {
+      const [success, fileMapOrError] = e.data;
+      (success ? resolve : reject)(fileMapOrError);
+    };
+    worker.postMessage(src);
+  });
+}
+
+function cachedLoadGLTF(src, basePath, contentType, preferredTechnique, onProgress) {
   if (!GLTFCache[src]) {
-    GLTFCache[src] = new Promise((resolve, reject) => {
-      const gltfLoader = new THREE.GLTFLoader();
-      gltfLoader.path = basePath;
-      gltfLoader.preferredTechnique = preferredTechnique;
-      gltfLoader.load(src, resolve, onProgress, reject);
+    GLTFCache[src] = new Promise(async (resolve, reject) => {
+      try {
+        let gltfUrl = src;
+        let onLoad = resolve;
+        if (contentType === "model/gltf+zip") {
+          const fileMap = await getFilesFromSketchfabZip(src);
+          gltfUrl = fileMap["scene.gtlf"];
+          onLoad = model => {
+            // The GLTF is now cached as a THREE object, we can get rid of the original blobs
+            Object.keys(fileMap).forEach(URL.revokeObjectURL);
+            resolve(model);
+          };
+        }
+
+        const gltfLoader = new THREE.GLTFLoader();
+        gltfLoader.path = basePath;
+        gltfLoader.preferredTechnique = preferredTechnique;
+        gltfLoader.load(gltfUrl, onLoad, onProgress, reject);
+      } catch (e) {
+        reject(e);
+        delete GLTFCache[src];
+      }
     });
   }
   return GLTFCache[src].then(cloneGltf);
@@ -203,6 +232,7 @@ function cachedLoadGLTF(src, basePath, preferredTechnique, onProgress) {
 AFRAME.registerComponent("gltf-model-plus", {
   schema: {
     src: { type: "string" },
+    contentType: { type: "string" },
     basePath: { type: "string", default: undefined },
     inflate: { default: false }
   },
@@ -244,7 +274,7 @@ AFRAME.registerComponent("gltf-model-plus", {
       }
 
       const gltfPath = THREE.LoaderUtils.extractUrlBase(src);
-      const model = await cachedLoadGLTF(src, this.data.basePath, this.preferredTechnique);
+      const model = await cachedLoadGLTF(src, this.data.basePath, this.data.contentType, this.preferredTechnique);
 
       // If we started loading something else already
       // TODO: there should be a way to cancel loading instead
diff --git a/src/components/media-loader.js b/src/components/media-loader.js
index ef8ea23cc33f5fb077bddf8ce6d0f269b6d04a36..544d4443a4304e0ed1d3ad75523c89cafa46c979 100644
--- a/src/components/media-loader.js
+++ b/src/components/media-loader.js
@@ -1,5 +1,5 @@
 import { getBox, getScaleCoefficient } from "../utils/auto-box-collider";
-import { resolveFarsparkUrl } from "../utils/media-utils";
+import { resolveMedia } from "../utils/media-utils";
 
 const fetchContentType = async url => fetch(url, { method: "HEAD" }).then(r => r.headers.get("content-type"));
 
@@ -53,7 +53,7 @@ AFRAME.registerComponent("media-loader", {
         this.setShapeAndScale(true);
       }, 100);
 
-      const { raw, origin, meta } = await resolveFarsparkUrl(url);
+      const { raw, origin, meta } = await resolveMedia(url);
       console.log("resolved", url, raw, origin, meta);
 
       const contentType = (meta && meta.expected_content_type) || (await fetchContentType(raw));
@@ -79,6 +79,7 @@ AFRAME.registerComponent("media-loader", {
         this.el.addEventListener("model-error", this.onError, { once: true });
         this.el.setAttribute("gltf-model-plus", {
           src: raw,
+          contentType,
           basePath: THREE.LoaderUtils.extractUrlBase(origin),
           inflate: true
         });
diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js
index cc22ef521f2ee74281f2de9e636d867c21d89b1d..835107d15deed8178150f914ec3bd4e076aaba2e 100644
--- a/src/gltf-component-mappings.js
+++ b/src/gltf-component-mappings.js
@@ -17,7 +17,6 @@ AFRAME.GLTFModelPlus.registerComponent("point-light", "light");
 AFRAME.GLTFModelPlus.registerComponent("skybox", "skybox");
 AFRAME.GLTFModelPlus.registerComponent("layers", "layers");
 AFRAME.GLTFModelPlus.registerComponent("shadow", "shadow");
-AFRAME.GLTFModelPlus.registerComponent("xr", "xr");
 AFRAME.GLTFModelPlus.registerComponent("water", "water");
 AFRAME.GLTFModelPlus.registerComponent("scale-audio-feedback", "scale-audio-feedback");
 AFRAME.GLTFModelPlus.registerComponent("animation-mixer", "animation-mixer");
diff --git a/src/hub.html b/src/hub.html
index 9c7db93b00999bb07fe95a44536c925610cdbe1d..45a849b2fc267bb1b35cdf84fe8be64be50852a7 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -5,7 +5,6 @@
     <!-- DO NOT REMOVE/EDIT THIS COMMENT - HUB_META_TAGS -->
 
     <meta charset="utf-8">
-    <meta http-equiv="origin-trial" data-feature="WebVR (For Chrome M62+)" data-expires="<%= ORIGIN_TRIAL_EXPIRES %>" content="<%= ORIGIN_TRIAL_TOKEN %>">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
 
     <link rel="shortcut icon" type="image/png" href="/favicon.ico">
@@ -27,7 +26,7 @@
     <a-scene
         renderer="antialias: true"
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
-        physics="gravity: -6;"
+        physics="gravity: -6; debug: false;"
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
         freeze-controller="toggleEvent: action_freeze"
         personal-space-bubble="debug: false;"
diff --git a/src/hub.js b/src/hub.js
index 3b4744abf0bd38a7361d6640aa3b5a6d878ccd5d..564462417517e2ac606c2c2a371957605230194f 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -6,8 +6,6 @@ import "aframe";
 import { patchWebGLRenderingContext } from "./utils/webgl";
 patchWebGLRenderingContext();
 
-import "aframe-xr";
-
 import "./vendor/GLTFLoader";
 import "networked-aframe/src/index";
 import "naf-janus-adapter";
diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js
index e4f812b59dd04d2402905e97e8a6e0f29f4e85f9..e864bfc9fd979ef42b4fd4083cd67325e3465b3a 100644
--- a/src/utils/media-utils.js
+++ b/src/utils/media-utils.js
@@ -1,20 +1,25 @@
 const whitelistedHosts = [/^.*\.reticulum\.io$/, /^.*hubs\.mozilla\.com$/, /^hubs\.local$/];
 const isHostWhitelisted = hostname => !!whitelistedHosts.filter(r => r.test(hostname)).length;
-let resolveMediaUrl = "/api/v1/media";
+let mediaAPIEndpoint = "/api/v1/media";
 if (process.env.NODE_ENV === "development") {
-  resolveMediaUrl = `https://${process.env.DEV_RETICULUM_SERVER}${resolveMediaUrl}`;
+  mediaAPIEndpoint = `https://${process.env.DEV_RETICULUM_SERVER}${mediaAPIEndpoint}`;
 }
 
-export const resolveFarsparkUrl = async url => {
+const resolveMediaCache = new Map();
+export const resolveMedia = async url => {
   const parsedUrl = new URL(url);
-  if ((parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") || isHostWhitelisted(parsedUrl.hostname))
-    return { raw: url, origin: url };
+  if (resolveMediaCache.has(url)) return resolveMediaCache.get(url);
 
-  return await fetch(resolveMediaUrl, {
-    method: "POST",
-    headers: { "Content-Type": "application/json" },
-    body: JSON.stringify({ media: { url } })
-  }).then(r => r.json());
+  const resolved =
+    (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") || isHostWhitelisted(parsedUrl.hostname)
+      ? { raw: url, origin: url }
+      : await fetch(mediaAPIEndpoint, {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({ media: { url } })
+        }).then(r => r.json());
+  resolveMediaCache.set(url, resolved);
+  return resolved;
 };
 
 let interactableId = 0;
diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js
index 5e23d9250e1f917f1206398fa361115e4be77d28..d92e1773df03db6a76566333fd77867c1a49342f 100644
--- a/src/vendor/GLTFLoader.js
+++ b/src/vendor/GLTFLoader.js
@@ -10,7 +10,7 @@
  * @author netpro2k / https://github.com/netpro2k
  */
 
- import { resolveFarsparkUrl } from "../utils/media-utils"
+ import { resolveMedia } from "../utils/media-utils"
 
 THREE.GLTFLoader = ( function () {
 
@@ -40,7 +40,7 @@ THREE.GLTFLoader = ( function () {
 
 			loader.setResponseType( 'arraybuffer' );
 
-			var farsparkURL = (await resolveFarsparkUrl(url)).raw;
+			var farsparkURL = (await resolveMedia(url)).raw;
 
 			loader.load( farsparkURL, function ( data ) {
 
@@ -1623,7 +1623,7 @@ THREE.GLTFLoader = ( function () {
 
 		var options = this.options;
 
-		var farsparkURL = (await resolveFarsparkUrl(resolveURL(bufferDef.uri, options.path))).raw;
+		var farsparkURL = (await resolveMedia(resolveURL(bufferDef.uri, options.path))).raw;
 
 		return new Promise( function ( resolve, reject ) {
 
@@ -1823,7 +1823,7 @@ THREE.GLTFLoader = ( function () {
 
     var urlToLoad = resolveURL(sourceURI, options.path);
     if (!hasBufferView){
-      urlToLoad = (await resolveFarsparkUrl(urlToLoad)).raw;
+      urlToLoad = (await resolveMedia(urlToLoad)).raw;
     }
 
 		return Promise.resolve( sourceURI ).then( function ( sourceURI ) {
diff --git a/src/workers/gifparsing.worker.js b/src/workers/gifparsing.worker.js
index 643a95ab98e52c048ef551a6249e3ef937191389..1d1400cde35215bc65eba23863c8baa978bcb799 100644
--- a/src/workers/gifparsing.worker.js
+++ b/src/workers/gifparsing.worker.js
@@ -63,10 +63,12 @@ self.onmessage = e => {
     new Uint8Array(e.data),
     (delays, loopcnt, frames, disposals) => {
       self.postMessage([true, frames, delays, disposals]);
+      delete self.onmessage;
     },
     err => {
       console.error("Error in gif parsing worker", err);
       self.postMessage([false, err]);
+      delete self.onmessage;
     }
   );
 };
diff --git a/src/workers/sketchfab-zip.worker.js b/src/workers/sketchfab-zip.worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..9108cbb5597669d99f6c62bcfed4798946458fa1
--- /dev/null
+++ b/src/workers/sketchfab-zip.worker.js
@@ -0,0 +1,33 @@
+import JSZip from "jszip";
+
+async function fetchZipAndGetBlobs(src) {
+  const zip = await fetch(src)
+    .then(r => r.blob())
+    .then(JSZip.loadAsync);
+
+  // Rewrite any url refferences in the GLTF to blob urls
+  const fileMap = {};
+  const files = Object.values(zip.files);
+  const fileBlobs = await Promise.all(files.map(f => f.async("blob")));
+  for (let i = 0; i < fileBlobs.length; i++) {
+    fileMap[files[i].name] = URL.createObjectURL(fileBlobs[i]);
+  }
+
+  const gltfJson = JSON.parse(await zip.file("scene.gltf").async("text"));
+  gltfJson.buffers && gltfJson.buffers.forEach(b => (b.uri = fileMap[b.uri]));
+  gltfJson.images && gltfJson.images.forEach(i => (i.uri = fileMap[i.uri]));
+
+  fileMap["scene.gtlf"] = URL.createObjectURL(new Blob([JSON.stringify(gltfJson, null, 2)], { type: "text/plain" }));
+
+  return fileMap;
+}
+
+self.onmessage = async e => {
+  try {
+    const fileMap = await fetchZipAndGetBlobs(e.data);
+    self.postMessage([true, fileMap]);
+  } catch (e) {
+    self.postMessage([false, e.message]);
+  }
+  delete self.onmessage;
+};
diff --git a/webpack.config.js b/webpack.config.js
index 5cb651c3a3fc91f8c1266b35f35b913cb6ade56c..07dcf72f66e6d9a1165b693a602a5cc4c18aac04 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -9,7 +9,6 @@ const HTMLWebpackPlugin = require("html-webpack-plugin");
 const ExtractTextPlugin = require("extract-text-webpack-plugin");
 const CopyWebpackPlugin = require("copy-webpack-plugin");
 const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
-const _ = require("lodash");
 
 function createHTTPSConfig() {
   // Generate certs for the local webpack-dev-server.
@@ -58,21 +57,6 @@ function createHTTPSConfig() {
   }
 }
 
-class LodashTemplatePlugin {
-  constructor(options) {
-    this.options = options;
-  }
-
-  apply(compiler) {
-    compiler.plugin("compilation", compilation => {
-      compilation.plugin("html-webpack-plugin-before-html-processing", async data => {
-        data.html = _.template(data.html, this.options)();
-        return data;
-      });
-    });
-  }
-}
-
 module.exports = (env, argv) => ({
   entry: {
     index: path.join(__dirname, "src", "index.js"),
@@ -118,9 +102,7 @@ module.exports = (env, argv) => ({
         loader: "html-loader",
         options: {
           // <a-asset-item>'s src property is overwritten with the correct transformed asset url.
-          attrs: ["img:src", "a-asset-item:src", "audio:src", "source:src"],
-          // You can get transformed asset urls in an html template using ${require("pathToFile.ext")}
-          interpolate: "require"
+          attrs: ["img:src", "a-asset-item:src", "audio:src", "source:src"]
         }
       },
       {
@@ -219,7 +201,15 @@ module.exports = (env, argv) => ({
       filename: "hub.html",
       template: path.join(__dirname, "src", "hub.html"),
       chunks: ["vendor", "engine", "hub"],
-      inject: "head"
+      inject: "head",
+      meta: [
+        {
+          "http-equiv": "origin-trial",
+          "data-feature": "WebVR (For Chrome M62+)",
+          "data-expires": process.env.ORIGIN_TRIAL_EXPIRES,
+          content: process.env.ORIGIN_TRIAL_TOKEN
+        }
+      ]
     }),
     new HTMLWebpackPlugin({
       filename: "link.html",
@@ -249,16 +239,6 @@ module.exports = (env, argv) => ({
       filename: "assets/stylesheets/[name]-[md5:contenthash:hex:20].css",
       disable: argv.mode !== "production"
     }),
-    // Transform the output of the html-loader using _.template
-    // before passing the result to html-webpack-plugin
-    new LodashTemplatePlugin({
-      // expose these variables to the lodash template
-      // ex: <%= ORIGIN_TRIAL_TOKEN %>
-      imports: {
-        ORIGIN_TRIAL_EXPIRES: process.env.ORIGIN_TRIAL_EXPIRES,
-        ORIGIN_TRIAL_TOKEN: process.env.ORIGIN_TRIAL_TOKEN
-      }
-    }),
     // Define process.env variables in the browser context.
     new webpack.DefinePlugin({
       "process.env": JSON.stringify({