'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _videoJs = require('video.js'); var _videoJs2 = _interopRequireDefault(_videoJs); var _binUtils = require('./bin-utils'); var REQUEST_ERRORS = { FAILURE: 2, TIMEOUT: -101, ABORTED: -102 }; exports.REQUEST_ERRORS = REQUEST_ERRORS; /** * Turns segment byterange into a string suitable for use in * HTTP Range requests * * @param {Object} byterange - an object with two values defining the start and end * of a byte-range */ var byterangeStr = function byterangeStr(byterange) { var byterangeStart = undefined; var byterangeEnd = undefined; // `byterangeEnd` is one less than `offset + length` because the HTTP range // header uses inclusive ranges byterangeEnd = byterange.offset + byterange.length - 1; byterangeStart = byterange.offset; return 'bytes=' + byterangeStart + '-' + byterangeEnd; }; /** * Defines headers for use in the xhr request for a particular segment. * * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader */ var segmentXhrHeaders = function segmentXhrHeaders(segment) { var headers = {}; if (segment.byterange) { headers.Range = byterangeStr(segment.byterange); } return headers; }; /** * Abort all requests * * @param {Object} activeXhrs - an object that tracks all XHR requests */ var abortAll = function abortAll(activeXhrs) { activeXhrs.forEach(function (xhr) { xhr.abort(); }); }; /** * Gather important bandwidth stats once a request has completed * * @param {Object} request - the XHR request from which to gather stats */ var getRequestStats = function getRequestStats(request) { return { bandwidth: request.bandwidth, bytesReceived: request.bytesReceived || 0, roundTripTime: request.roundTripTime || 0 }; }; /** * If possible gather bandwidth stats as a request is in * progress * * @param {Event} progressEvent - an event object from an XHR's progress event */ var getProgressStats = function getProgressStats(progressEvent) { var request = progressEvent.target; var roundTripTime = Date.now() - request.requestTime; var stats = { bandwidth: Infinity, bytesReceived: 0, roundTripTime: roundTripTime || 0 }; stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok // because we should only use bandwidth stats on progress to determine when // abort a request early due to insufficient bandwidth stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); return stats; }; /** * Handle all error conditions in one place and return an object * with all the information * * @param {Error|null} error - if non-null signals an error occured with the XHR * @param {Object} request - the XHR request that possibly generated the error */ var handleErrors = function handleErrors(error, request) { if (request.timedout) { return { status: request.status, message: 'HLS request timed-out at URL: ' + request.uri, code: REQUEST_ERRORS.TIMEOUT, xhr: request }; } if (request.aborted) { return { status: request.status, message: 'HLS request aborted at URL: ' + request.uri, code: REQUEST_ERRORS.ABORTED, xhr: request }; } if (error) { return { status: request.status, message: 'HLS request errored at URL: ' + request.uri, code: REQUEST_ERRORS.FAILURE, xhr: request }; } return null; }; /** * Handle responses for key data and convert the key data to the correct format * for the decryption step later * * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader * @param {Function} finishProcessingFn - a callback to execute to continue processing * this request */ var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn) { return function (error, request) { var response = request.response; var errorObj = handleErrors(error, request); if (errorObj) { return finishProcessingFn(errorObj, segment); } if (response.byteLength !== 16) { return finishProcessingFn({ status: request.status, message: 'Invalid HLS key at URL: ' + request.uri, code: REQUEST_ERRORS.FAILURE, xhr: request }, segment); } var view = new DataView(response); segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); return finishProcessingFn(null, segment); }; }; /** * Handle init-segment responses * * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader * @param {Function} finishProcessingFn - a callback to execute to continue processing * this request */ var handleInitSegmentResponse = function handleInitSegmentResponse(segment, finishProcessingFn) { return function (error, request) { var response = request.response; var errorObj = handleErrors(error, request); if (errorObj) { return finishProcessingFn(errorObj, segment); } // stop processing if received empty content if (response.byteLength === 0) { return finishProcessingFn({ status: request.status, message: 'Empty HLS segment content at URL: ' + request.uri, code: REQUEST_ERRORS.FAILURE, xhr: request }, segment); } segment.map.bytes = new Uint8Array(request.response); return finishProcessingFn(null, segment); }; }; /** * Response handler for segment-requests being sure to set the correct * property depending on whether the segment is encryped or not * Also records and keeps track of stats that are used for ABR purposes * * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader * @param {Function} finishProcessingFn - a callback to execute to continue processing * this request */ var handleSegmentResponse = function handleSegmentResponse(segment, finishProcessingFn) { return function (error, request) { var response = request.response; var errorObj = handleErrors(error, request); if (errorObj) { return finishProcessingFn(errorObj, segment); } // stop processing if received empty content if (response.byteLength === 0) { return finishProcessingFn({ status: request.status, message: 'Empty HLS segment content at URL: ' + request.uri, code: REQUEST_ERRORS.FAILURE, xhr: request }, segment); } segment.stats = getRequestStats(request); if (segment.key) { segment.encryptedBytes = new Uint8Array(request.response); } else { segment.bytes = new Uint8Array(request.response); } return finishProcessingFn(null, segment); }; }; /** * Decrypt the segment via the decryption web worker * * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader * @param {Function} doneFn - a callback that is executed after decryption has completed */ var decryptSegment = function decryptSegment(decrypter, segment, doneFn) { var decryptionHandler = function decryptionHandler(event) { if (event.data.source === segment.requestId) { decrypter.removeEventListener('message', decryptionHandler); var decrypted = event.data.decrypted; segment.bytes = new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength); return doneFn(null, segment); } }; decrypter.addEventListener('message', decryptionHandler); // this is an encrypted segment // incrementally decrypt the segment decrypter.postMessage((0, _binUtils.createTransferableMessage)({ source: segment.requestId, encrypted: segment.encryptedBytes, key: segment.key.bytes, iv: segment.key.iv }), [segment.encryptedBytes.buffer, segment.key.bytes.buffer]); }; /** * The purpose of this function is to get the most pertinent error from the * array of errors. * For instance if a timeout and two aborts occur, then the aborts were * likely triggered by the timeout so return that error object. */ var getMostImportantError = function getMostImportantError(errors) { return errors.reduce(function (prev, err) { return err.code > prev.code ? err : prev; }); }; /** * This function waits for all XHRs to finish (with either success or failure) * before continueing processing via it's callback. The function gathers errors * from each request into a single errors array so that the error status for * each request can be examined later. * * @param {Object} activeXhrs - an object that tracks all XHR requests * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines * @param {Function} doneFn - a callback that is executed after all resources have been * downloaded and any decryption completed */ var waitForCompletion = function waitForCompletion(activeXhrs, decrypter, doneFn) { var errors = []; var count = 0; return function (error, segment) { if (error) { // If there are errors, we have to abort any outstanding requests abortAll(activeXhrs); errors.push(error); } count += 1; if (count === activeXhrs.length) { // Keep track of when *all* of the requests have completed segment.endOfAllRequests = Date.now(); if (errors.length > 0) { var worstError = getMostImportantError(errors); return doneFn(worstError, segment); } if (segment.encryptedBytes) { return decryptSegment(decrypter, segment, doneFn); } // Otherwise, everything is ready just continue return doneFn(null, segment); } }; }; /** * Simple progress event callback handler that gathers some stats before * executing a provided callback with the `segment` object * * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader * @param {Function} progressFn - a callback that is executed each time a progress event * is received * @param {Event} event - the progress event object from XMLHttpRequest */ var handleProgress = function handleProgress(segment, progressFn) { return function (event) { segment.stats = _videoJs2['default'].mergeOptions(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { segment.stats.firstBytesReceivedAt = Date.now(); } return progressFn(event, segment); }; }; /** * Load all resources and does any processing necessary for a media-segment * * Features: * decrypts the media-segment if it has a key uri and an iv * aborts *all* requests if *any* one request fails * * The segment object, at minimum, has the following format: * { * resolvedUri: String, * [byterange]: { * offset: Number, * length: Number * }, * [key]: { * resolvedUri: String * [byterange]: { * offset: Number, * length: Number * }, * iv: { * bytes: Uint32Array * } * }, * [map]: { * resolvedUri: String, * [byterange]: { * offset: Number, * length: Number * }, * [bytes]: Uint8Array * } * } * ...where [name] denotes optional properties * * @param {Function} xhr - an instance of the xhr wrapper in xhr.js * @param {Object} xhrOptions - the base options to provide to all xhr requests * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 * decryption routines * @param {Object} segment - a simplified copy of the segmentInfo object * from SegmentLoader * @param {Function} progressFn - a callback that receives progress events from the main * segment's xhr request * @param {Function} doneFn - a callback that is executed only once all requests have * succeeded or failed * @returns {Function} a function that, when invoked, immediately aborts all * outstanding requests */ var mediaSegmentRequest = function mediaSegmentRequest(xhr, xhrOptions, decryptionWorker, segment, progressFn, doneFn) { var activeXhrs = []; var finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn); // optionally, request the decryption key if (segment.key) { var keyRequestOptions = _videoJs2['default'].mergeOptions(xhrOptions, { uri: segment.key.resolvedUri, responseType: 'arraybuffer' }); var keyRequestCallback = handleKeyResponse(segment, finishProcessingFn); var keyXhr = xhr(keyRequestOptions, keyRequestCallback); activeXhrs.push(keyXhr); } // optionally, request the associated media init segment if (segment.map && !segment.map.bytes) { var initSegmentOptions = _videoJs2['default'].mergeOptions(xhrOptions, { uri: segment.map.resolvedUri, responseType: 'arraybuffer', headers: segmentXhrHeaders(segment.map) }); var initSegmentRequestCallback = handleInitSegmentResponse(segment, finishProcessingFn); var initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); activeXhrs.push(initSegmentXhr); } var segmentRequestOptions = _videoJs2['default'].mergeOptions(xhrOptions, { uri: segment.resolvedUri, responseType: 'arraybuffer', headers: segmentXhrHeaders(segment) }); var segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn); var segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); segmentXhr.addEventListener('progress', handleProgress(segment, progressFn)); activeXhrs.push(segmentXhr); return function () { return abortAll(activeXhrs); }; }; exports.mediaSegmentRequest = mediaSegmentRequest;