# Source: media-segment-request.js

    import videojs from 'video.js';
    import { createTransferableMessage } from './bin-utils';

    export const REQUEST_ERRORS = {
      FAILURE: 2,
      TIMEOUT: -101,
      ABORTED: -102
    };

    /**
     * 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
     */
    const byterangeStr = function(byterange) {
      let byterangeStart;
      let byterangeEnd;

      // `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
     */
    const segmentXhrHeaders = function(segment) {
      let headers = {};

      if (segment.byterange) {
        headers.Range = byterangeStr(segment.byterange);
      }
      return headers;
    };

    /**
     * Abort all requests
     *
     * @param {Object} activeXhrs - an object that tracks all XHR requests
     */
    const abortAll = (activeXhrs) => {
      activeXhrs.forEach((xhr) => {
        xhr.abort();
      });
    };

    /**
     * Gather important bandwidth stats once a request has completed
     *
     * @param {Object} request - the XHR request from which to gather stats
     */
    const 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
     */
    const getProgressStats = (progressEvent) => {
      const request = progressEvent.target;
      const roundTripTime = Date.now() - request.requestTime;
      const 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
     */
    const 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
     */
    const handleKeyResponse = (segment, finishProcessingFn) => (error, request) => {
      const response = request.response;
      const 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);
      }

      const 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
     */
    const handleInitSegmentResponse = (segment, finishProcessingFn) => (error, request) => {
      const response = request.response;
      const 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
     */
    const handleSegmentResponse = (segment, finishProcessingFn) => (error, request) => {
      const response = request.response;
      const 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
     */
    const decryptSegment = (decrypter, segment, doneFn) => {
      const decryptionHandler = (event) => {
        if (event.data.source === segment.requestId) {
          decrypter.removeEventListener('message', decryptionHandler);
          const 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(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.
     */
    const getMostImportantError = (errors) => {
      return errors.reduce((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
     */
    const waitForCompletion = (activeXhrs, decrypter, doneFn) => {
      let errors = [];
      let count = 0;

      return (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) {
            const 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
     */
    const handleProgress = (segment, progressFn) => (event) => {
      segment.stats = videojs.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
     */
    export const mediaSegmentRequest = (xhr,
                                        xhrOptions,
                                        decryptionWorker,
                                        segment,
                                        progressFn,
                                        doneFn) => {
      const activeXhrs = [];
      const finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn);

      // optionally, request the decryption key
      if (segment.key) {
        const keyRequestOptions = videojs.mergeOptions(xhrOptions, {
          uri: segment.key.resolvedUri,
          responseType: 'arraybuffer'
        });
        const keyRequestCallback = handleKeyResponse(segment, finishProcessingFn);
        const keyXhr = xhr(keyRequestOptions, keyRequestCallback);

        activeXhrs.push(keyXhr);
      }

      // optionally, request the associated media init segment
      if (segment.map &&
        !segment.map.bytes) {
        const initSegmentOptions = videojs.mergeOptions(xhrOptions, {
          uri: segment.map.resolvedUri,
          responseType: 'arraybuffer',
          headers: segmentXhrHeaders(segment.map)
        });
        const initSegmentRequestCallback = handleInitSegmentResponse(segment,
                                                                     finishProcessingFn);
        const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);

        activeXhrs.push(initSegmentXhr);
      }

      const segmentRequestOptions = videojs.mergeOptions(xhrOptions, {
        uri: segment.resolvedUri,
        responseType: 'arraybuffer',
        headers: segmentXhrHeaders(segment)
      });
      const segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn);
      const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback);

      segmentXhr.addEventListener('progress', handleProgress(segment, progressFn));
      activeXhrs.push(segmentXhr);

      return () => abortAll(activeXhrs);
    };