aframe
Version:
A web framework for building virtual reality experiences.
283 lines (247 loc) • 8.3 kB
JavaScript
/* global customElements */
import * as THREE from 'three';
import { ANode } from './a-node.js';
import { debug } from '../utils/index.js';
var fileLoader = new THREE.FileLoader();
var warn = debug('core:a-assets:warn');
/**
* Asset management system. Handles blocking on asset loading.
*/
class AAssets extends ANode {
constructor () {
super();
this.isAssets = true;
this.fileLoader = fileLoader;
this.timeout = null;
}
doConnectedCallback () {
var self = this;
var i;
var loaded = [];
var mediaEl;
var mediaEls;
var imgEl;
var imgEls;
var timeout;
var children;
super.doConnectedCallback();
if (!this.parentNode.isScene) {
throw new Error('<a-assets> must be a child of a <a-scene>.');
}
// Wait for <img>s.
imgEls = this.querySelectorAll('img');
for (i = 0; i < imgEls.length; i++) {
imgEl = fixUpMediaElement(imgEls[i]);
loaded.push(new Promise(function (resolve, reject) {
// Set in cache because we won't be needing to call three.js loader if we have.
// a loaded media element.
THREE.Cache.add(imgEls[i].getAttribute('src'), imgEl);
if (imgEl.complete) {
resolve();
return;
}
imgEl.onload = resolve;
imgEl.onerror = reject;
}));
}
// Wait for <audio>s and <video>s.
mediaEls = this.querySelectorAll('audio, video');
for (i = 0; i < mediaEls.length; i++) {
mediaEl = fixUpMediaElement(mediaEls[i]);
if (!mediaEl.src && !mediaEl.srcObject) {
warn('Audio/video asset has neither `src` nor `srcObject` attributes.');
}
loaded.push(mediaElementLoaded(mediaEl));
}
// Wait for <a-asset-item>s
children = this.getChildren();
children.forEach(function (child) {
if (!child.isAssetItem || !child.hasAttribute('src')) { return; }
loaded.push(new Promise(function waitForLoaded (resolve, reject) {
if (child.hasLoaded) { return resolve(); }
child.addEventListener('loaded', resolve);
child.addEventListener('error', reject);
}));
});
// Trigger loaded for scene to start rendering.
Promise.allSettled(loaded).then(function () {
// Make sure the timeout didn't occur.
if (self.timeout === null) { return; }
self.load();
});
// Timeout to start loading anyways.
timeout = parseInt(this.getAttribute('timeout'), 10) || 3000;
this.timeout = setTimeout(function () {
// Make sure the loading didn't complete.
if (self.hasLoaded) { return; }
warn('Asset loading timed out in', timeout, 'ms');
self.timeout = null;
self.emit('timeout');
self.load();
}, timeout);
}
disconnectedCallback () {
super.disconnectedCallback();
if (this.timeout) { clearTimeout(this.timeout); }
}
load () {
// Filter out all children, as waiting already took place in doConnectedCallback.
super.load.call(this, null, function () { return false; });
}
}
customElements.define('a-assets', AAssets);
/**
* Preload using XHRLoader for any type of asset.
*/
class AAssetItem extends ANode {
constructor () {
super();
this.data = null;
this.isAssetItem = true;
}
connectedCallback () {
var self = this;
var src = this.getAttribute('src');
fileLoader.setResponseType(
this.getAttribute('response-type') || inferResponseType(src));
fileLoader.load(src, function handleOnLoad (response) {
self.data = response;
ANode.prototype.load.call(self);
}, function handleOnProgress (xhr) {
self.emit('progress', {
loadedBytes: xhr.loaded,
totalBytes: xhr.total,
xhr: xhr
});
}, function handleOnError (xhr) {
self.emit('error', {xhr: xhr}, false);
});
}
}
customElements.define('a-asset-item', AAssetItem);
/**
* Create a Promise that resolves once the media element has finished buffering.
*
* @param {Element} el - HTMLMediaElement.
* @returns {Promise}
*/
function mediaElementLoaded (el) {
if (!el.hasAttribute('autoplay') && el.getAttribute('preload') !== 'auto') {
return;
}
// If media specifies autoplay or preload, wait until media is completely buffered.
return new Promise(function (resolve, reject) {
if (el.readyState === 4) { return resolve(); } // Already loaded.
if (el.error) { return reject(); } // Error.
el.addEventListener('loadeddata', checkProgress, false);
el.addEventListener('progress', checkProgress, false);
el.addEventListener('error', reject, false);
function checkProgress () {
// Add up the seconds buffered.
var secondsBuffered = 0;
for (var i = 0; i < el.buffered.length; i++) {
secondsBuffered += el.buffered.end(i) - el.buffered.start(i);
}
// Compare seconds buffered to media duration.
if (secondsBuffered >= el.duration) {
// Set in cache because we won't be needing to call three.js loader if we have.
// a loaded media element.
// Store video elements only. three.js loader is used for audio elements.
// See assetParse too.
if (el.tagName === 'VIDEO') {
THREE.Cache.add(el.getAttribute('src'), el);
}
resolve();
}
}
});
}
/**
* Automatically add attributes to media elements where convenient.
* crossorigin, playsinline.
*/
function fixUpMediaElement (mediaEl) {
// Cross-origin.
var newMediaEl = setCrossOrigin(mediaEl);
// Plays inline for mobile.
if (newMediaEl.tagName && newMediaEl.tagName.toLowerCase() === 'video') {
newMediaEl.setAttribute('playsinline', '');
newMediaEl.setAttribute('webkit-playsinline', '');
}
if (newMediaEl !== mediaEl) {
mediaEl.parentNode.appendChild(newMediaEl);
mediaEl.parentNode.removeChild(mediaEl);
}
return newMediaEl;
}
/**
* Automatically set `crossorigin` if not defined on the media element.
* If it is not defined, we must create and re-append a new media element <img> and
* have the browser re-request it with `crossorigin` set.
*
* @param {Element} mediaEl - Media element (e.g., <img>, <audio>, <video>).
* @returns {Element} Media element to be used to listen to for loaded events.
*/
function setCrossOrigin (mediaEl) {
var newMediaEl;
var src;
// Already has crossorigin set.
if (mediaEl.hasAttribute('crossorigin')) { return mediaEl; }
src = mediaEl.getAttribute('src');
if (src !== null) {
// Does not have protocol.
if (src.indexOf('://') === -1) { return mediaEl; }
// Determine if cross origin is actually needed.
if (extractDomain(src) === window.location.host) { return mediaEl; }
}
warn('Cross-origin element (e.g., <img>) was requested without `crossorigin` set. ' +
'A-Frame will re-request the asset with `crossorigin` attribute set. ' +
'Please set `crossorigin` on the element (e.g., <img crossorigin="anonymous">)', src);
mediaEl.crossOrigin = 'anonymous';
newMediaEl = mediaEl.cloneNode(true);
return newMediaEl;
}
/**
* Extract domain out of URL.
*
* @param {string} url
* @returns {string}
*/
function extractDomain (url) {
// Find and remove protocol (e.g., http, ftp, etc.) to get domain.
var domain = url.indexOf('://') > -1 ? url.split('/')[2] : url.split('/')[0];
// Find and remove port number.
return domain.substring(0, domain.indexOf(':'));
}
/**
* Infer response-type attribute from src.
* Default is text (default XMLHttpRequest.responseType)
* and arraybuffer for .glb files.
*
* @param {string} src
* @returns {string}
*/
export function inferResponseType (src) {
var fileName = getFileNameFromURL(src);
var dotLastIndex = fileName.lastIndexOf('.');
if (dotLastIndex >= 0) {
var extension = fileName.slice(dotLastIndex, src.search(/\?|#|$/));
if (extension === '.glb') {
return 'arraybuffer';
}
}
return 'text';
}
/**
* Extract filename from URL
*
* @param {string} url
* @returns {string}
*/
export function getFileNameFromURL (url) {
var parser = document.createElement('a');
parser.href = url;
var query = parser.search.replace(/^\?/, '');
var filePath = url.replace(query, '').replace('?', '');
return filePath.substring(filePath.lastIndexOf('/') + 1);
}