2020-01-20 12:50:03 +03:00
|
|
|
/*
|
|
|
|
AUTHOR
|
|
|
|
Trek Hopton <trek@ausocean.org>
|
|
|
|
|
|
|
|
LICENSE
|
|
|
|
This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
|
|
|
|
|
|
|
|
It is free software: you can redistribute it and/or modify them
|
|
|
|
under the terms of the GNU General Public License as published by the
|
|
|
|
Free Software Foundation, either version 3 of the License, or (at your
|
|
|
|
option) any later version.
|
|
|
|
|
|
|
|
It is distributed in the hope that it will be useful, but WITHOUT
|
|
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
|
|
for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License in gpl.txt.
|
|
|
|
If not, see http://www.gnu.org/licenses.
|
|
|
|
|
|
|
|
For hls.js Copyright notice and license, see LICENSE file.
|
2020-01-02 11:06:52 +03:00
|
|
|
*/
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
/**
|
|
|
|
* XHR based loader
|
|
|
|
*/
|
2020-01-02 11:06:52 +03:00
|
|
|
|
|
|
|
const { performance, XMLHttpRequest } = window;
|
|
|
|
|
|
|
|
class XhrLoader {
|
2020-01-20 12:50:03 +03:00
|
|
|
constructor(config) {
|
2020-01-02 11:06:52 +03:00
|
|
|
if (config && config.xhrSetup) {
|
|
|
|
this.xhrSetup = config.xhrSetup;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
destroy() {
|
2020-01-02 11:06:52 +03:00
|
|
|
this.abort();
|
|
|
|
this.loader = null;
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
abort() {
|
2020-01-02 11:06:52 +03:00
|
|
|
let loader = this.loader;
|
|
|
|
if (loader && loader.readyState !== 4) {
|
|
|
|
this.stats.aborted = true;
|
|
|
|
loader.abort();
|
|
|
|
}
|
|
|
|
|
|
|
|
window.clearTimeout(this.requestTimeout);
|
|
|
|
this.requestTimeout = null;
|
|
|
|
window.clearTimeout(this.retryTimeout);
|
|
|
|
this.retryTimeout = null;
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
load(context, config, callbacks) {
|
2020-01-02 11:06:52 +03:00
|
|
|
this.context = context;
|
|
|
|
this.config = config;
|
|
|
|
this.callbacks = callbacks;
|
|
|
|
this.stats = { trequest: performance.now(), retry: 0 };
|
|
|
|
this.retryDelay = config.retryDelay;
|
|
|
|
this.loadInternal();
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
loadInternal() {
|
2020-01-02 11:06:52 +03:00
|
|
|
let xhr, context = this.context;
|
|
|
|
xhr = this.loader = new XMLHttpRequest();
|
2020-01-20 12:50:03 +03:00
|
|
|
window.console.log("load internal xhr: " + context.url);
|
|
|
|
|
2020-01-02 11:06:52 +03:00
|
|
|
let stats = this.stats;
|
|
|
|
stats.tfirst = 0;
|
|
|
|
stats.loaded = 0;
|
|
|
|
const xhrSetup = this.xhrSetup;
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (xhrSetup) {
|
|
|
|
try {
|
|
|
|
xhrSetup(xhr, context.url);
|
|
|
|
} catch (e) {
|
|
|
|
// fix xhrSetup: (xhr, url) => {xhr.setRequestHeader("Content-Language", "test");}
|
|
|
|
// not working, as xhr.setRequestHeader expects xhr.readyState === OPEN
|
|
|
|
xhr.open('GET', context.url, true);
|
|
|
|
xhrSetup(xhr, context.url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!xhr.readyState) {
|
|
|
|
xhr.open('GET', context.url, true);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS
|
|
|
|
this.callbacks.onError({ code: xhr.status, text: e.message }, context, xhr);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (context.rangeEnd) {
|
|
|
|
xhr.setRequestHeader('Range', 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
xhr.onreadystatechange = this.readystatechange.bind(this);
|
|
|
|
xhr.onprogress = this.loadprogress.bind(this);
|
|
|
|
xhr.responseType = context.responseType;
|
|
|
|
|
|
|
|
// setup timeout before we perform request
|
|
|
|
this.requestTimeout = window.setTimeout(this.loadtimeout.bind(this), this.config.timeout);
|
|
|
|
xhr.send();
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
readystatechange(event) {
|
2020-01-02 11:06:52 +03:00
|
|
|
let xhr = event.currentTarget,
|
|
|
|
readyState = xhr.readyState,
|
|
|
|
stats = this.stats,
|
|
|
|
context = this.context,
|
|
|
|
config = this.config;
|
|
|
|
|
|
|
|
// don't proceed if xhr has been aborted
|
|
|
|
if (stats.aborted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// >= HEADERS_RECEIVED
|
|
|
|
if (readyState >= 2) {
|
|
|
|
// clear xhr timeout and rearm it if readyState less than 4
|
|
|
|
window.clearTimeout(this.requestTimeout);
|
|
|
|
if (stats.tfirst === 0) {
|
|
|
|
stats.tfirst = Math.max(performance.now(), stats.trequest);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (readyState === 4) {
|
|
|
|
let status = xhr.status;
|
|
|
|
// http status between 200 to 299 are all successful
|
|
|
|
if (status >= 200 && status < 300) {
|
|
|
|
stats.tload = Math.max(stats.tfirst, performance.now());
|
|
|
|
let data, len;
|
|
|
|
if (context.responseType === 'arraybuffer') {
|
|
|
|
data = xhr.response;
|
|
|
|
len = data.byteLength;
|
|
|
|
} else {
|
|
|
|
data = xhr.responseText;
|
|
|
|
len = data.length;
|
|
|
|
}
|
|
|
|
stats.loaded = stats.total = len;
|
|
|
|
let response = { url: xhr.responseURL, data: data };
|
|
|
|
this.callbacks.onSuccess(response, stats, context, xhr);
|
|
|
|
} else {
|
|
|
|
// if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error
|
|
|
|
if (stats.retry >= config.maxRetry || (status >= 400 && status < 499)) {
|
2020-01-20 12:50:03 +03:00
|
|
|
console.error(`${status} while loading ${context.url}`);
|
2020-01-02 11:06:52 +03:00
|
|
|
this.callbacks.onError({ code: status, text: xhr.statusText }, context, xhr);
|
|
|
|
} else {
|
|
|
|
// retry
|
2020-01-20 12:50:03 +03:00
|
|
|
console.warn(`${status} while loading ${context.url}, retrying in ${this.retryDelay}...`);
|
2020-01-02 11:06:52 +03:00
|
|
|
// aborts and resets internal state
|
|
|
|
this.destroy();
|
|
|
|
// schedule retry
|
|
|
|
this.retryTimeout = window.setTimeout(this.loadInternal.bind(this), this.retryDelay);
|
|
|
|
// set exponential backoff
|
|
|
|
this.retryDelay = Math.min(2 * this.retryDelay, config.maxRetryDelay);
|
|
|
|
stats.retry++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet
|
|
|
|
this.requestTimeout = window.setTimeout(this.loadtimeout.bind(this), config.timeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
loadtimeout() {
|
|
|
|
console.warn(`timeout while loading ${this.context.url}`);
|
2020-01-02 11:06:52 +03:00
|
|
|
this.callbacks.onTimeout(this.stats, this.context, null);
|
|
|
|
}
|
|
|
|
|
2020-01-20 12:50:03 +03:00
|
|
|
loadprogress(event) {
|
2020-01-02 11:06:52 +03:00
|
|
|
let xhr = event.currentTarget,
|
|
|
|
stats = this.stats;
|
|
|
|
|
|
|
|
stats.loaded = event.loaded;
|
|
|
|
if (event.lengthComputable) {
|
|
|
|
stats.total = event.total;
|
|
|
|
}
|
|
|
|
|
|
|
|
let onProgress = this.callbacks.onProgress;
|
|
|
|
if (onProgress) {
|
|
|
|
// third arg is to provide on progress data
|
|
|
|
onProgress(stats, this.context, null, xhr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default XhrLoader;
|