447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
'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; |