UNPKG

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