UNPKG

13.9 kBJavaScriptView Raw
1import videojs from 'video.js';
2import { createTransferableMessage } from './bin-utils';
3
4export const REQUEST_ERRORS = {
5 FAILURE: 2,
6 TIMEOUT: -101,
7 ABORTED: -102
8};
9
10/**
11 * Turns segment byterange into a string suitable for use in
12 * HTTP Range requests
13 *
14 * @param {Object} byterange - an object with two values defining the start and end
15 * of a byte-range
16 */
17const byterangeStr = function(byterange) {
18 let byterangeStart;
19 let byterangeEnd;
20
21 // `byterangeEnd` is one less than `offset + length` because the HTTP range
22 // header uses inclusive ranges
23 byterangeEnd = byterange.offset + byterange.length - 1;
24 byterangeStart = byterange.offset;
25 return 'bytes=' + byterangeStart + '-' + byterangeEnd;
26};
27
28/**
29 * Defines headers for use in the xhr request for a particular segment.
30 *
31 * @param {Object} segment - a simplified copy of the segmentInfo object
32 * from SegmentLoader
33 */
34const segmentXhrHeaders = function(segment) {
35 let headers = {};
36
37 if (segment.byterange) {
38 headers.Range = byterangeStr(segment.byterange);
39 }
40 return headers;
41};
42
43/**
44 * Abort all requests
45 *
46 * @param {Object} activeXhrs - an object that tracks all XHR requests
47 */
48const abortAll = (activeXhrs) => {
49 activeXhrs.forEach((xhr) => {
50 xhr.abort();
51 });
52};
53
54/**
55 * Gather important bandwidth stats once a request has completed
56 *
57 * @param {Object} request - the XHR request from which to gather stats
58 */
59const getRequestStats = (request) => {
60 return {
61 bandwidth: request.bandwidth,
62 bytesReceived: request.bytesReceived || 0,
63 roundTripTime: request.roundTripTime || 0
64 };
65};
66
67/**
68 * If possible gather bandwidth stats as a request is in
69 * progress
70 *
71 * @param {Event} progressEvent - an event object from an XHR's progress event
72 */
73const getProgressStats = (progressEvent) => {
74 const request = progressEvent.target;
75 const roundTripTime = Date.now() - request.requestTime;
76 const stats = {
77 bandwidth: Infinity,
78 bytesReceived: 0,
79 roundTripTime: roundTripTime || 0
80 };
81
82 stats.bytesReceived = progressEvent.loaded;
83 // This can result in Infinity if stats.roundTripTime is 0 but that is ok
84 // because we should only use bandwidth stats on progress to determine when
85 // abort a request early due to insufficient bandwidth
86 stats.bandwidth = Math.floor((stats.bytesReceived / stats.roundTripTime) * 8 * 1000);
87
88 return stats;
89};
90
91/**
92 * Handle all error conditions in one place and return an object
93 * with all the information
94 *
95 * @param {Error|null} error - if non-null signals an error occured with the XHR
96 * @param {Object} request - the XHR request that possibly generated the error
97 */
98const handleErrors = (error, request) => {
99 if (request.timedout) {
100 return {
101 status: request.status,
102 message: 'HLS request timed-out at URL: ' + request.uri,
103 code: REQUEST_ERRORS.TIMEOUT,
104 xhr: request
105 };
106 }
107
108 if (request.aborted) {
109 return {
110 status: request.status,
111 message: 'HLS request aborted at URL: ' + request.uri,
112 code: REQUEST_ERRORS.ABORTED,
113 xhr: request
114 };
115 }
116
117 if (error) {
118 return {
119 status: request.status,
120 message: 'HLS request errored at URL: ' + request.uri,
121 code: REQUEST_ERRORS.FAILURE,
122 xhr: request
123 };
124 }
125
126 return null;
127};
128
129/**
130 * Handle responses for key data and convert the key data to the correct format
131 * for the decryption step later
132 *
133 * @param {Object} segment - a simplified copy of the segmentInfo object
134 * from SegmentLoader
135 * @param {Function} finishProcessingFn - a callback to execute to continue processing
136 * this request
137 */
138const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => {
139 const response = request.response;
140 const errorObj = handleErrors(error, request);
141
142 if (errorObj) {
143 return finishProcessingFn(errorObj, segment);
144 }
145
146 if (response.byteLength !== 16) {
147 return finishProcessingFn({
148 status: request.status,
149 message: 'Invalid HLS key at URL: ' + request.uri,
150 code: REQUEST_ERRORS.FAILURE,
151 xhr: request
152 }, segment);
153 }
154
155 const view = new DataView(response);
156
157 segment.key.bytes = new Uint32Array([
158 view.getUint32(0),
159 view.getUint32(4),
160 view.getUint32(8),
161 view.getUint32(12)
162 ]);
163 return finishProcessingFn(null, segment);
164};
165
166/**
167 * Handle init-segment responses
168 *
169 * @param {Object} segment - a simplified copy of the segmentInfo object
170 * from SegmentLoader
171 * @param {Function} finishProcessingFn - a callback to execute to continue processing
172 * this request
173 */
174const handleInitSegmentResponse = (segment, finishProcessingFn) => (error, request) => {
175 const response = request.response;
176 const errorObj = handleErrors(error, request);
177
178 if (errorObj) {
179 return finishProcessingFn(errorObj, segment);
180 }
181
182 // stop processing if received empty content
183 if (response.byteLength === 0) {
184 return finishProcessingFn({
185 status: request.status,
186 message: 'Empty HLS segment content at URL: ' + request.uri,
187 code: REQUEST_ERRORS.FAILURE,
188 xhr: request
189 }, segment);
190 }
191
192 segment.map.bytes = new Uint8Array(request.response);
193 return finishProcessingFn(null, segment);
194};
195
196/**
197 * Response handler for segment-requests being sure to set the correct
198 * property depending on whether the segment is encryped or not
199 * Also records and keeps track of stats that are used for ABR purposes
200 *
201 * @param {Object} segment - a simplified copy of the segmentInfo object
202 * from SegmentLoader
203 * @param {Function} finishProcessingFn - a callback to execute to continue processing
204 * this request
205 */
206const handleSegmentResponse = (segment, finishProcessingFn) => (error, request) => {
207 const response = request.response;
208 const errorObj = handleErrors(error, request);
209
210 if (errorObj) {
211 return finishProcessingFn(errorObj, segment);
212 }
213
214 // stop processing if received empty content
215 if (response.byteLength === 0) {
216 return finishProcessingFn({
217 status: request.status,
218 message: 'Empty HLS segment content at URL: ' + request.uri,
219 code: REQUEST_ERRORS.FAILURE,
220 xhr: request
221 }, segment);
222 }
223
224 segment.stats = getRequestStats(request);
225
226 if (segment.key) {
227 segment.encryptedBytes = new Uint8Array(request.response);
228 } else {
229 segment.bytes = new Uint8Array(request.response);
230 }
231
232 return finishProcessingFn(null, segment);
233};
234
235/**
236 * Decrypt the segment via the decryption web worker
237 *
238 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines
239 * @param {Object} segment - a simplified copy of the segmentInfo object
240 * from SegmentLoader
241 * @param {Function} doneFn - a callback that is executed after decryption has completed
242 */
243const decryptSegment = (decrypter, segment, doneFn) => {
244 const decryptionHandler = (event) => {
245 if (event.data.source === segment.requestId) {
246 decrypter.removeEventListener('message', decryptionHandler);
247 const decrypted = event.data.decrypted;
248
249 segment.bytes = new Uint8Array(decrypted.bytes,
250 decrypted.byteOffset,
251 decrypted.byteLength);
252 return doneFn(null, segment);
253 }
254 };
255
256 decrypter.addEventListener('message', decryptionHandler);
257
258 // this is an encrypted segment
259 // incrementally decrypt the segment
260 decrypter.postMessage(createTransferableMessage({
261 source: segment.requestId,
262 encrypted: segment.encryptedBytes,
263 key: segment.key.bytes,
264 iv: segment.key.iv
265 }), [
266 segment.encryptedBytes.buffer,
267 segment.key.bytes.buffer
268 ]);
269};
270
271/**
272 * The purpose of this function is to get the most pertinent error from the
273 * array of errors.
274 * For instance if a timeout and two aborts occur, then the aborts were
275 * likely triggered by the timeout so return that error object.
276 */
277const getMostImportantError = (errors) => {
278 return errors.reduce((prev, err) => {
279 return err.code > prev.code ? err : prev;
280 });
281};
282
283/**
284 * This function waits for all XHRs to finish (with either success or failure)
285 * before continueing processing via it's callback. The function gathers errors
286 * from each request into a single errors array so that the error status for
287 * each request can be examined later.
288 *
289 * @param {Object} activeXhrs - an object that tracks all XHR requests
290 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines
291 * @param {Function} doneFn - a callback that is executed after all resources have been
292 * downloaded and any decryption completed
293 */
294const waitForCompletion = (activeXhrs, decrypter, doneFn) => {
295 let errors = [];
296 let count = 0;
297
298 return (error, segment) => {
299 if (error) {
300 // If there are errors, we have to abort any outstanding requests
301 abortAll(activeXhrs);
302 errors.push(error);
303 }
304 count += 1;
305
306 if (count === activeXhrs.length) {
307 // Keep track of when *all* of the requests have completed
308 segment.endOfAllRequests = Date.now();
309
310 if (errors.length > 0) {
311 const worstError = getMostImportantError(errors);
312
313 return doneFn(worstError, segment);
314 }
315 if (segment.encryptedBytes) {
316 return decryptSegment(decrypter, segment, doneFn);
317 }
318 // Otherwise, everything is ready just continue
319 return doneFn(null, segment);
320 }
321 };
322};
323
324/**
325 * Simple progress event callback handler that gathers some stats before
326 * executing a provided callback with the `segment` object
327 *
328 * @param {Object} segment - a simplified copy of the segmentInfo object
329 * from SegmentLoader
330 * @param {Function} progressFn - a callback that is executed each time a progress event
331 * is received
332 * @param {Event} event - the progress event object from XMLHttpRequest
333 */
334const handleProgress = (segment, progressFn) => (event) => {
335 segment.stats = videojs.mergeOptions(segment.stats, getProgressStats(event));
336
337 // record the time that we receive the first byte of data
338 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) {
339 segment.stats.firstBytesReceivedAt = Date.now();
340 }
341
342 return progressFn(event, segment);
343};
344
345/**
346 * Load all resources and does any processing necessary for a media-segment
347 *
348 * Features:
349 * decrypts the media-segment if it has a key uri and an iv
350 * aborts *all* requests if *any* one request fails
351 *
352 * The segment object, at minimum, has the following format:
353 * {
354 * resolvedUri: String,
355 * [byterange]: {
356 * offset: Number,
357 * length: Number
358 * },
359 * [key]: {
360 * resolvedUri: String
361 * [byterange]: {
362 * offset: Number,
363 * length: Number
364 * },
365 * iv: {
366 * bytes: Uint32Array
367 * }
368 * },
369 * [map]: {
370 * resolvedUri: String,
371 * [byterange]: {
372 * offset: Number,
373 * length: Number
374 * },
375 * [bytes]: Uint8Array
376 * }
377 * }
378 * ...where [name] denotes optional properties
379 *
380 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js
381 * @param {Object} xhrOptions - the base options to provide to all xhr requests
382 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128
383 * decryption routines
384 * @param {Object} segment - a simplified copy of the segmentInfo object
385 * from SegmentLoader
386 * @param {Function} progressFn - a callback that receives progress events from the main
387 * segment's xhr request
388 * @param {Function} doneFn - a callback that is executed only once all requests have
389 * succeeded or failed
390 * @returns {Function} a function that, when invoked, immediately aborts all
391 * outstanding requests
392 */
393export const mediaSegmentRequest = (xhr,
394 xhrOptions,
395 decryptionWorker,
396 segment,
397 progressFn,
398 doneFn) => {
399 const activeXhrs = [];
400 const finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn);
401
402 // optionally, request the decryption key
403 if (segment.key) {
404 const keyRequestOptions = videojs.mergeOptions(xhrOptions, {
405 uri: segment.key.resolvedUri,
406 responseType: 'arraybuffer'
407 });
408 const keyRequestCallback = handleKeyResponse(segment, finishProcessingFn);
409 const keyXhr = xhr(keyRequestOptions, keyRequestCallback);
410
411 activeXhrs.push(keyXhr);
412 }
413
414 // optionally, request the associated media init segment
415 if (segment.map &&
416 !segment.map.bytes) {
417 const initSegmentOptions = videojs.mergeOptions(xhrOptions, {
418 uri: segment.map.resolvedUri,
419 responseType: 'arraybuffer',
420 headers: segmentXhrHeaders(segment.map)
421 });
422 const initSegmentRequestCallback = handleInitSegmentResponse(segment,
423 finishProcessingFn);
424 const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
425
426 activeXhrs.push(initSegmentXhr);
427 }
428
429 const segmentRequestOptions = videojs.mergeOptions(xhrOptions, {
430 uri: segment.resolvedUri,
431 responseType: 'arraybuffer',
432 headers: segmentXhrHeaders(segment)
433 });
434 const segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn);
435 const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback);
436
437 segmentXhr.addEventListener('progress', handleProgress(segment, progressFn));
438 activeXhrs.push(segmentXhr);
439
440 return () => abortAll(activeXhrs);
441};