forked from video-dev/hls.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
xhr-loader.js
165 lines (144 loc) · 5 KB
/
xhr-loader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
* XHR based logger
*/
import { logger } from '../utils/logger';
class XhrLoader {
constructor (config) {
if (config && config.xhrSetup) {
this.xhrSetup = config.xhrSetup;
}
}
destroy () {
this.abort();
this.loader = null;
}
abort () {
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;
}
load (context, config, callbacks) {
this.context = context;
this.config = config;
this.callbacks = callbacks;
this.stats = { trequest: performance.now(), retry: 0 };
this.retryDelay = config.retryDelay;
this.loadInternal();
}
loadInternal () {
let xhr, context = this.context;
xhr = this.loader = new XMLHttpRequest();
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();
}
readystatechange (event) {
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)) {
logger.error(`${status} while loading ${context.url}`);
this.callbacks.onError({ code: status, text: xhr.statusText }, context, xhr);
} else {
// retry
logger.warn(`${status} while loading ${context.url}, retrying in ${this.retryDelay}...`);
// 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);
}
}
}
loadtimeout () {
logger.warn(`timeout while loading ${this.context.url}`);
this.callbacks.onTimeout(this.stats, this.context, null);
}
loadprogress (event) {
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;