media-segment-request.js 14 KB

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