2752 lines
		
	
	
		
			89 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2752 lines
		
	
	
		
			89 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/*! @name mpd-parser @version 1.3.1 @license Apache-2.0 */
 | 
						|
'use strict';
 | 
						|
 | 
						|
Object.defineProperty(exports, '__esModule', { value: true });
 | 
						|
 | 
						|
var resolveUrl = require('@videojs/vhs-utils/cjs/resolve-url');
 | 
						|
var window = require('global/window');
 | 
						|
var mediaGroups = require('@videojs/vhs-utils/cjs/media-groups');
 | 
						|
var decodeB64ToUint8Array = require('@videojs/vhs-utils/cjs/decode-b64-to-uint8-array');
 | 
						|
var xmldom = require('@xmldom/xmldom');
 | 
						|
 | 
						|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
 | 
						|
 | 
						|
var resolveUrl__default = /*#__PURE__*/_interopDefaultLegacy(resolveUrl);
 | 
						|
var window__default = /*#__PURE__*/_interopDefaultLegacy(window);
 | 
						|
var decodeB64ToUint8Array__default = /*#__PURE__*/_interopDefaultLegacy(decodeB64ToUint8Array);
 | 
						|
 | 
						|
var version = "1.3.1";
 | 
						|
 | 
						|
const isObject = obj => {
 | 
						|
  return !!obj && typeof obj === 'object';
 | 
						|
};
 | 
						|
 | 
						|
const merge = (...objects) => {
 | 
						|
  return objects.reduce((result, source) => {
 | 
						|
    if (typeof source !== 'object') {
 | 
						|
      return result;
 | 
						|
    }
 | 
						|
 | 
						|
    Object.keys(source).forEach(key => {
 | 
						|
      if (Array.isArray(result[key]) && Array.isArray(source[key])) {
 | 
						|
        result[key] = result[key].concat(source[key]);
 | 
						|
      } else if (isObject(result[key]) && isObject(source[key])) {
 | 
						|
        result[key] = merge(result[key], source[key]);
 | 
						|
      } else {
 | 
						|
        result[key] = source[key];
 | 
						|
      }
 | 
						|
    });
 | 
						|
    return result;
 | 
						|
  }, {});
 | 
						|
};
 | 
						|
const values = o => Object.keys(o).map(k => o[k]);
 | 
						|
 | 
						|
const range = (start, end) => {
 | 
						|
  const result = [];
 | 
						|
 | 
						|
  for (let i = start; i < end; i++) {
 | 
						|
    result.push(i);
 | 
						|
  }
 | 
						|
 | 
						|
  return result;
 | 
						|
};
 | 
						|
const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
 | 
						|
const from = list => {
 | 
						|
  if (!list.length) {
 | 
						|
    return [];
 | 
						|
  }
 | 
						|
 | 
						|
  const result = [];
 | 
						|
 | 
						|
  for (let i = 0; i < list.length; i++) {
 | 
						|
    result.push(list[i]);
 | 
						|
  }
 | 
						|
 | 
						|
  return result;
 | 
						|
};
 | 
						|
const findIndexes = (l, key) => l.reduce((a, e, i) => {
 | 
						|
  if (e[key]) {
 | 
						|
    a.push(i);
 | 
						|
  }
 | 
						|
 | 
						|
  return a;
 | 
						|
}, []);
 | 
						|
/**
 | 
						|
 * Returns a union of the included lists provided each element can be identified by a key.
 | 
						|
 *
 | 
						|
 * @param {Array} list - list of lists to get the union of
 | 
						|
 * @param {Function} keyFunction - the function to use as a key for each element
 | 
						|
 *
 | 
						|
 * @return {Array} the union of the arrays
 | 
						|
 */
 | 
						|
 | 
						|
const union = (lists, keyFunction) => {
 | 
						|
  return values(lists.reduce((acc, list) => {
 | 
						|
    list.forEach(el => {
 | 
						|
      acc[keyFunction(el)] = el;
 | 
						|
    });
 | 
						|
    return acc;
 | 
						|
  }, {}));
 | 
						|
};
 | 
						|
 | 
						|
var errors = {
 | 
						|
  INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
 | 
						|
  INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
 | 
						|
  DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
 | 
						|
  DASH_INVALID_XML: 'DASH_INVALID_XML',
 | 
						|
  NO_BASE_URL: 'NO_BASE_URL',
 | 
						|
  MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
 | 
						|
  SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
 | 
						|
  UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object} SingleUri
 | 
						|
 * @property {string} uri - relative location of segment
 | 
						|
 * @property {string} resolvedUri - resolved location of segment
 | 
						|
 * @property {Object} byterange - Object containing information on how to make byte range
 | 
						|
 *   requests following byte-range-spec per RFC2616.
 | 
						|
 * @property {String} byterange.length - length of range request
 | 
						|
 * @property {String} byterange.offset - byte offset of range request
 | 
						|
 *
 | 
						|
 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
 | 
						|
 * that conforms to how m3u8-parser is structured
 | 
						|
 *
 | 
						|
 * @see https://github.com/videojs/m3u8-parser
 | 
						|
 *
 | 
						|
 * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
 | 
						|
 * @param {string} source - source url for segment
 | 
						|
 * @param {string} range - optional range used for range calls,
 | 
						|
 *   follows  RFC 2616, Clause 14.35.1
 | 
						|
 * @return {SingleUri} full segment information transformed into a format similar
 | 
						|
 *   to m3u8-parser
 | 
						|
 */
 | 
						|
 | 
						|
const urlTypeToSegment = ({
 | 
						|
  baseUrl = '',
 | 
						|
  source = '',
 | 
						|
  range = '',
 | 
						|
  indexRange = ''
 | 
						|
}) => {
 | 
						|
  const segment = {
 | 
						|
    uri: source,
 | 
						|
    resolvedUri: resolveUrl__default['default'](baseUrl || '', source)
 | 
						|
  };
 | 
						|
 | 
						|
  if (range || indexRange) {
 | 
						|
    const rangeStr = range ? range : indexRange;
 | 
						|
    const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible
 | 
						|
 | 
						|
    let startRange = window__default['default'].BigInt ? window__default['default'].BigInt(ranges[0]) : parseInt(ranges[0], 10);
 | 
						|
    let endRange = window__default['default'].BigInt ? window__default['default'].BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER
 | 
						|
 | 
						|
    if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') {
 | 
						|
      startRange = Number(startRange);
 | 
						|
    }
 | 
						|
 | 
						|
    if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') {
 | 
						|
      endRange = Number(endRange);
 | 
						|
    }
 | 
						|
 | 
						|
    let length;
 | 
						|
 | 
						|
    if (typeof endRange === 'bigint' || typeof startRange === 'bigint') {
 | 
						|
      length = window__default['default'].BigInt(endRange) - window__default['default'].BigInt(startRange) + window__default['default'].BigInt(1);
 | 
						|
    } else {
 | 
						|
      length = endRange - startRange + 1;
 | 
						|
    }
 | 
						|
 | 
						|
    if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) {
 | 
						|
      length = Number(length);
 | 
						|
    } // byterange should be inclusive according to
 | 
						|
    // RFC 2616, Clause 14.35.1
 | 
						|
 | 
						|
 | 
						|
    segment.byterange = {
 | 
						|
      length,
 | 
						|
      offset: startRange
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  return segment;
 | 
						|
};
 | 
						|
const byteRangeToString = byterange => {
 | 
						|
  // `endRange` is one less than `offset + length` because the HTTP range
 | 
						|
  // header uses inclusive ranges
 | 
						|
  let endRange;
 | 
						|
 | 
						|
  if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
 | 
						|
    endRange = window__default['default'].BigInt(byterange.offset) + window__default['default'].BigInt(byterange.length) - window__default['default'].BigInt(1);
 | 
						|
  } else {
 | 
						|
    endRange = byterange.offset + byterange.length - 1;
 | 
						|
  }
 | 
						|
 | 
						|
  return `${byterange.offset}-${endRange}`;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * parse the end number attribue that can be a string
 | 
						|
 * number, or undefined.
 | 
						|
 *
 | 
						|
 * @param {string|number|undefined} endNumber
 | 
						|
 *        The end number attribute.
 | 
						|
 *
 | 
						|
 * @return {number|null}
 | 
						|
 *          The result of parsing the end number.
 | 
						|
 */
 | 
						|
 | 
						|
const parseEndNumber = endNumber => {
 | 
						|
  if (endNumber && typeof endNumber !== 'number') {
 | 
						|
    endNumber = parseInt(endNumber, 10);
 | 
						|
  }
 | 
						|
 | 
						|
  if (isNaN(endNumber)) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  return endNumber;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Functions for calculating the range of available segments in static and dynamic
 | 
						|
 * manifests.
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
const segmentRange = {
 | 
						|
  /**
 | 
						|
   * Returns the entire range of available segments for a static MPD
 | 
						|
   *
 | 
						|
   * @param {Object} attributes
 | 
						|
   *        Inheritied MPD attributes
 | 
						|
   * @return {{ start: number, end: number }}
 | 
						|
   *         The start and end numbers for available segments
 | 
						|
   */
 | 
						|
  static(attributes) {
 | 
						|
    const {
 | 
						|
      duration,
 | 
						|
      timescale = 1,
 | 
						|
      sourceDuration,
 | 
						|
      periodDuration
 | 
						|
    } = attributes;
 | 
						|
    const endNumber = parseEndNumber(attributes.endNumber);
 | 
						|
    const segmentDuration = duration / timescale;
 | 
						|
 | 
						|
    if (typeof endNumber === 'number') {
 | 
						|
      return {
 | 
						|
        start: 0,
 | 
						|
        end: endNumber
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    if (typeof periodDuration === 'number') {
 | 
						|
      return {
 | 
						|
        start: 0,
 | 
						|
        end: periodDuration / segmentDuration
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      start: 0,
 | 
						|
      end: sourceDuration / segmentDuration
 | 
						|
    };
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns the current live window range of available segments for a dynamic MPD
 | 
						|
   *
 | 
						|
   * @param {Object} attributes
 | 
						|
   *        Inheritied MPD attributes
 | 
						|
   * @return {{ start: number, end: number }}
 | 
						|
   *         The start and end numbers for available segments
 | 
						|
   */
 | 
						|
  dynamic(attributes) {
 | 
						|
    const {
 | 
						|
      NOW,
 | 
						|
      clientOffset,
 | 
						|
      availabilityStartTime,
 | 
						|
      timescale = 1,
 | 
						|
      duration,
 | 
						|
      periodStart = 0,
 | 
						|
      minimumUpdatePeriod = 0,
 | 
						|
      timeShiftBufferDepth = Infinity
 | 
						|
    } = attributes;
 | 
						|
    const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated
 | 
						|
    // after retrieving UTC server time.
 | 
						|
 | 
						|
    const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock.
 | 
						|
    // Convert the period start time to EPOCH.
 | 
						|
 | 
						|
    const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update.
 | 
						|
 | 
						|
    const periodEndWC = now + minimumUpdatePeriod;
 | 
						|
    const periodDuration = periodEndWC - periodStartWC;
 | 
						|
    const segmentCount = Math.ceil(periodDuration * timescale / duration);
 | 
						|
    const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
 | 
						|
    const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
 | 
						|
    return {
 | 
						|
      start: Math.max(0, availableStart),
 | 
						|
      end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Maps a range of numbers to objects with information needed to build the corresponding
 | 
						|
 * segment list
 | 
						|
 *
 | 
						|
 * @name toSegmentsCallback
 | 
						|
 * @function
 | 
						|
 * @param {number} number
 | 
						|
 *        Number of the segment
 | 
						|
 * @param {number} index
 | 
						|
 *        Index of the number in the range list
 | 
						|
 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
 | 
						|
 *         Object with segment timing and duration info
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a callback for Array.prototype.map for mapping a range of numbers to
 | 
						|
 * information needed to build the segment list.
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *        Inherited MPD attributes
 | 
						|
 * @return {toSegmentsCallback}
 | 
						|
 *         Callback map function
 | 
						|
 */
 | 
						|
 | 
						|
const toSegments = attributes => number => {
 | 
						|
  const {
 | 
						|
    duration,
 | 
						|
    timescale = 1,
 | 
						|
    periodStart,
 | 
						|
    startNumber = 1
 | 
						|
  } = attributes;
 | 
						|
  return {
 | 
						|
    number: startNumber + number,
 | 
						|
    duration: duration / timescale,
 | 
						|
    timeline: periodStart,
 | 
						|
    time: number * duration
 | 
						|
  };
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Returns a list of objects containing segment timing and duration info used for
 | 
						|
 * building the list of segments. This uses the @duration attribute specified
 | 
						|
 * in the MPD manifest to derive the range of segments.
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *        Inherited MPD attributes
 | 
						|
 * @return {{number: number, duration: number, time: number, timeline: number}[]}
 | 
						|
 *         List of Objects with segment timing and duration info
 | 
						|
 */
 | 
						|
 | 
						|
const parseByDuration = attributes => {
 | 
						|
  const {
 | 
						|
    type,
 | 
						|
    duration,
 | 
						|
    timescale = 1,
 | 
						|
    periodDuration,
 | 
						|
    sourceDuration
 | 
						|
  } = attributes;
 | 
						|
  const {
 | 
						|
    start,
 | 
						|
    end
 | 
						|
  } = segmentRange[type](attributes);
 | 
						|
  const segments = range(start, end).map(toSegments(attributes));
 | 
						|
 | 
						|
  if (type === 'static') {
 | 
						|
    const index = segments.length - 1; // section is either a period or the full source
 | 
						|
 | 
						|
    const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration
 | 
						|
 | 
						|
    segments[index].duration = sectionDuration - duration / timescale * index;
 | 
						|
  }
 | 
						|
 | 
						|
  return segments;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Translates SegmentBase into a set of segments.
 | 
						|
 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes.  Each
 | 
						|
 * node should be translated into segment.
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *   Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *   names as keys
 | 
						|
 * @return {Object.<Array>} list of segments
 | 
						|
 */
 | 
						|
 | 
						|
const segmentsFromBase = attributes => {
 | 
						|
  const {
 | 
						|
    baseUrl,
 | 
						|
    initialization = {},
 | 
						|
    sourceDuration,
 | 
						|
    indexRange = '',
 | 
						|
    periodStart,
 | 
						|
    presentationTime,
 | 
						|
    number = 0,
 | 
						|
    duration
 | 
						|
  } = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
 | 
						|
 | 
						|
  if (!baseUrl) {
 | 
						|
    throw new Error(errors.NO_BASE_URL);
 | 
						|
  }
 | 
						|
 | 
						|
  const initSegment = urlTypeToSegment({
 | 
						|
    baseUrl,
 | 
						|
    source: initialization.sourceURL,
 | 
						|
    range: initialization.range
 | 
						|
  });
 | 
						|
  const segment = urlTypeToSegment({
 | 
						|
    baseUrl,
 | 
						|
    source: baseUrl,
 | 
						|
    indexRange
 | 
						|
  });
 | 
						|
  segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source
 | 
						|
  // (since SegmentBase is only for one total segment)
 | 
						|
 | 
						|
  if (duration) {
 | 
						|
    const segmentTimeInfo = parseByDuration(attributes);
 | 
						|
 | 
						|
    if (segmentTimeInfo.length) {
 | 
						|
      segment.duration = segmentTimeInfo[0].duration;
 | 
						|
      segment.timeline = segmentTimeInfo[0].timeline;
 | 
						|
    }
 | 
						|
  } else if (sourceDuration) {
 | 
						|
    segment.duration = sourceDuration;
 | 
						|
    segment.timeline = periodStart;
 | 
						|
  } // If presentation time is provided, these segments are being generated by SIDX
 | 
						|
  // references, and should use the time provided. For the general case of SegmentBase,
 | 
						|
  // there should only be one segment in the period, so its presentation time is the same
 | 
						|
  // as its period start.
 | 
						|
 | 
						|
 | 
						|
  segment.presentationTime = presentationTime || periodStart;
 | 
						|
  segment.number = number;
 | 
						|
  return [segment];
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
 | 
						|
 * according to the sidx information given.
 | 
						|
 *
 | 
						|
 * playlist.sidx has metadadata about the sidx where-as the sidx param
 | 
						|
 * is the parsed sidx box itself.
 | 
						|
 *
 | 
						|
 * @param {Object} playlist the playlist to update the sidx information for
 | 
						|
 * @param {Object} sidx the parsed sidx box
 | 
						|
 * @return {Object} the playlist object with the updated sidx information
 | 
						|
 */
 | 
						|
 | 
						|
const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => {
 | 
						|
  // Retain init segment information
 | 
						|
  const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing
 | 
						|
 | 
						|
  const sourceDuration = playlist.sidx.duration; // Retain source timeline
 | 
						|
 | 
						|
  const timeline = playlist.timeline || 0;
 | 
						|
  const sidxByteRange = playlist.sidx.byterange;
 | 
						|
  const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx
 | 
						|
 | 
						|
  const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes
 | 
						|
 | 
						|
  const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
 | 
						|
  const segments = [];
 | 
						|
  const type = playlist.endList ? 'static' : 'dynamic';
 | 
						|
  const periodStart = playlist.sidx.timeline;
 | 
						|
  let presentationTime = periodStart;
 | 
						|
  let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box
 | 
						|
 | 
						|
  let startIndex; // eslint-disable-next-line
 | 
						|
 | 
						|
  if (typeof sidx.firstOffset === 'bigint') {
 | 
						|
    startIndex = window__default['default'].BigInt(sidxEnd) + sidx.firstOffset;
 | 
						|
  } else {
 | 
						|
    startIndex = sidxEnd + sidx.firstOffset;
 | 
						|
  }
 | 
						|
 | 
						|
  for (let i = 0; i < mediaReferences.length; i++) {
 | 
						|
    const reference = sidx.references[i]; // size of the referenced (sub)segment
 | 
						|
 | 
						|
    const size = reference.referencedSize; // duration of the referenced (sub)segment, in  the  timescale
 | 
						|
    // this will be converted to seconds when generating segments
 | 
						|
 | 
						|
    const duration = reference.subsegmentDuration; // should be an inclusive range
 | 
						|
 | 
						|
    let endIndex; // eslint-disable-next-line
 | 
						|
 | 
						|
    if (typeof startIndex === 'bigint') {
 | 
						|
      endIndex = startIndex + window__default['default'].BigInt(size) - window__default['default'].BigInt(1);
 | 
						|
    } else {
 | 
						|
      endIndex = startIndex + size - 1;
 | 
						|
    }
 | 
						|
 | 
						|
    const indexRange = `${startIndex}-${endIndex}`;
 | 
						|
    const attributes = {
 | 
						|
      baseUrl,
 | 
						|
      timescale,
 | 
						|
      timeline,
 | 
						|
      periodStart,
 | 
						|
      presentationTime,
 | 
						|
      number,
 | 
						|
      duration,
 | 
						|
      sourceDuration,
 | 
						|
      indexRange,
 | 
						|
      type
 | 
						|
    };
 | 
						|
    const segment = segmentsFromBase(attributes)[0];
 | 
						|
 | 
						|
    if (initSegment) {
 | 
						|
      segment.map = initSegment;
 | 
						|
    }
 | 
						|
 | 
						|
    segments.push(segment);
 | 
						|
 | 
						|
    if (typeof startIndex === 'bigint') {
 | 
						|
      startIndex += window__default['default'].BigInt(size);
 | 
						|
    } else {
 | 
						|
      startIndex += size;
 | 
						|
    }
 | 
						|
 | 
						|
    presentationTime += duration / timescale;
 | 
						|
    number++;
 | 
						|
  }
 | 
						|
 | 
						|
  playlist.segments = segments;
 | 
						|
  return playlist;
 | 
						|
};
 | 
						|
 | 
						|
const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen)
 | 
						|
 | 
						|
const TIME_FUDGE = 1 / 60;
 | 
						|
/**
 | 
						|
 * Given a list of timelineStarts, combines, dedupes, and sorts them.
 | 
						|
 *
 | 
						|
 * @param {TimelineStart[]} timelineStarts - list of timeline starts
 | 
						|
 *
 | 
						|
 * @return {TimelineStart[]} the combined and deduped timeline starts
 | 
						|
 */
 | 
						|
 | 
						|
const getUniqueTimelineStarts = timelineStarts => {
 | 
						|
  return union(timelineStarts, ({
 | 
						|
    timeline
 | 
						|
  }) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1);
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Finds the playlist with the matching NAME attribute.
 | 
						|
 *
 | 
						|
 * @param {Array} playlists - playlists to search through
 | 
						|
 * @param {string} name - the NAME attribute to search for
 | 
						|
 *
 | 
						|
 * @return {Object|null} the matching playlist object, or null
 | 
						|
 */
 | 
						|
 | 
						|
const findPlaylistWithName = (playlists, name) => {
 | 
						|
  for (let i = 0; i < playlists.length; i++) {
 | 
						|
    if (playlists[i].attributes.NAME === name) {
 | 
						|
      return playlists[i];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return null;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Gets a flattened array of media group playlists.
 | 
						|
 *
 | 
						|
 * @param {Object} manifest - the main manifest object
 | 
						|
 *
 | 
						|
 * @return {Array} the media group playlists
 | 
						|
 */
 | 
						|
 | 
						|
const getMediaGroupPlaylists = manifest => {
 | 
						|
  let mediaGroupPlaylists = [];
 | 
						|
  mediaGroups.forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
 | 
						|
    mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
 | 
						|
  });
 | 
						|
  return mediaGroupPlaylists;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Updates the playlist's media sequence numbers.
 | 
						|
 *
 | 
						|
 * @param {Object} config - options object
 | 
						|
 * @param {Object} config.playlist - the playlist to update
 | 
						|
 * @param {number} config.mediaSequence - the mediaSequence number to start with
 | 
						|
 */
 | 
						|
 | 
						|
const updateMediaSequenceForPlaylist = ({
 | 
						|
  playlist,
 | 
						|
  mediaSequence
 | 
						|
}) => {
 | 
						|
  playlist.mediaSequence = mediaSequence;
 | 
						|
  playlist.segments.forEach((segment, index) => {
 | 
						|
    segment.number = playlist.mediaSequence + index;
 | 
						|
  });
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists
 | 
						|
 * and a complete list of timeline starts.
 | 
						|
 *
 | 
						|
 * If no matching playlist is found, only the discontinuity sequence number of the playlist
 | 
						|
 * will be updated.
 | 
						|
 *
 | 
						|
 * Since early available timelines are not supported, at least one segment must be present.
 | 
						|
 *
 | 
						|
 * @param {Object} config - options object
 | 
						|
 * @param {Object[]} oldPlaylists - the old playlists to use as a reference
 | 
						|
 * @param {Object[]} newPlaylists - the new playlists to update
 | 
						|
 * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point
 | 
						|
 */
 | 
						|
 | 
						|
const updateSequenceNumbers = ({
 | 
						|
  oldPlaylists,
 | 
						|
  newPlaylists,
 | 
						|
  timelineStarts
 | 
						|
}) => {
 | 
						|
  newPlaylists.forEach(playlist => {
 | 
						|
    playlist.discontinuitySequence = timelineStarts.findIndex(function ({
 | 
						|
      timeline
 | 
						|
    }) {
 | 
						|
      return timeline === playlist.timeline;
 | 
						|
    }); // Playlists NAMEs come from DASH Representation IDs, which are mandatory
 | 
						|
    // (see ISO_23009-1-2012 5.3.5.2).
 | 
						|
    //
 | 
						|
    // If the same Representation existed in a prior Period, it will retain the same NAME.
 | 
						|
 | 
						|
    const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME);
 | 
						|
 | 
						|
    if (!oldPlaylist) {
 | 
						|
      // Since this is a new playlist, the media sequence values can start from 0 without
 | 
						|
      // consequence.
 | 
						|
      return;
 | 
						|
    } // TODO better support for live SIDX
 | 
						|
    //
 | 
						|
    // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD).
 | 
						|
    // This is evident by a playlist only having a single SIDX reference. In a multiperiod
 | 
						|
    // playlist there would need to be multiple SIDX references. In addition, live SIDX is
 | 
						|
    // not supported when the SIDX properties change on refreshes.
 | 
						|
    //
 | 
						|
    // In the future, if support needs to be added, the merging logic here can be called
 | 
						|
    // after SIDX references are resolved. For now, exit early to prevent exceptions being
 | 
						|
    // thrown due to undefined references.
 | 
						|
 | 
						|
 | 
						|
    if (playlist.sidx) {
 | 
						|
      return;
 | 
						|
    } // Since we don't yet support early available timelines, we don't need to support
 | 
						|
    // playlists with no segments.
 | 
						|
 | 
						|
 | 
						|
    const firstNewSegment = playlist.segments[0];
 | 
						|
    const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) {
 | 
						|
      return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE;
 | 
						|
    }); // No matching segment from the old playlist means the entire playlist was refreshed.
 | 
						|
    // In this case the media sequence should account for this update, and the new segments
 | 
						|
    // should be marked as discontinuous from the prior content, since the last prior
 | 
						|
    // timeline was removed.
 | 
						|
 | 
						|
    if (oldMatchingSegmentIndex === -1) {
 | 
						|
      updateMediaSequenceForPlaylist({
 | 
						|
        playlist,
 | 
						|
        mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length
 | 
						|
      });
 | 
						|
      playlist.segments[0].discontinuity = true;
 | 
						|
      playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content.
 | 
						|
      //
 | 
						|
      // If the new playlist's timeline is the same as the last seen segment's timeline,
 | 
						|
      // then a discontinuity can be added to identify that there's potentially missing
 | 
						|
      // content. If there's no missing content, the discontinuity should still be rather
 | 
						|
      // harmless. It's possible that if segment durations are accurate enough, that the
 | 
						|
      // existence of a gap can be determined using the presentation times and durations,
 | 
						|
      // but if the segment timing info is off, it may introduce more problems than simply
 | 
						|
      // adding the discontinuity.
 | 
						|
      //
 | 
						|
      // If the new playlist's timeline is different from the last seen segment's timeline,
 | 
						|
      // then a discontinuity can be added to identify that this is the first seen segment
 | 
						|
      // of a new timeline. However, the logic at the start of this function that
 | 
						|
      // determined the disconinuity sequence by timeline index is now off by one (the
 | 
						|
      // discontinuity of the newest timeline hasn't yet fallen off the manifest...since
 | 
						|
      // we added it), so the disconinuity sequence must be decremented.
 | 
						|
      //
 | 
						|
      // A period may also have a duration of zero, so the case of no segments is handled
 | 
						|
      // here even though we don't yet support early available periods.
 | 
						|
 | 
						|
      if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) {
 | 
						|
        playlist.discontinuitySequence--;
 | 
						|
      }
 | 
						|
 | 
						|
      return;
 | 
						|
    } // If the first segment matched with a prior segment on a discontinuity (it's matching
 | 
						|
    // on the first segment of a period), then the discontinuitySequence shouldn't be the
 | 
						|
    // timeline's matching one, but instead should be the one prior, and the first segment
 | 
						|
    // of the new manifest should be marked with a discontinuity.
 | 
						|
    //
 | 
						|
    // The reason for this special case is that discontinuity sequence shows how many
 | 
						|
    // discontinuities have fallen off of the playlist, and discontinuities are marked on
 | 
						|
    // the first segment of a new "timeline." Because of this, while DASH will retain that
 | 
						|
    // Period while the "timeline" exists, HLS keeps track of it via the discontinuity
 | 
						|
    // sequence, and that first segment is an indicator, but can be removed before that
 | 
						|
    // timeline is gone.
 | 
						|
 | 
						|
 | 
						|
    const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex];
 | 
						|
 | 
						|
    if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) {
 | 
						|
      firstNewSegment.discontinuity = true;
 | 
						|
      playlist.discontinuityStarts.unshift(0);
 | 
						|
      playlist.discontinuitySequence--;
 | 
						|
    }
 | 
						|
 | 
						|
    updateMediaSequenceForPlaylist({
 | 
						|
      playlist,
 | 
						|
      mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number
 | 
						|
    });
 | 
						|
  });
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Given an old parsed manifest object and a new parsed manifest object, updates the
 | 
						|
 * sequence and timing values within the new manifest to ensure that it lines up with the
 | 
						|
 * old.
 | 
						|
 *
 | 
						|
 * @param {Array} oldManifest - the old main manifest object
 | 
						|
 * @param {Array} newManifest - the new main manifest object
 | 
						|
 *
 | 
						|
 * @return {Object} the updated new manifest object
 | 
						|
 */
 | 
						|
 | 
						|
const positionManifestOnTimeline = ({
 | 
						|
  oldManifest,
 | 
						|
  newManifest
 | 
						|
}) => {
 | 
						|
  // Starting from v4.1.2 of the IOP, section 4.4.3.3 states:
 | 
						|
  //
 | 
						|
  // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates."
 | 
						|
  //
 | 
						|
  // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160
 | 
						|
  //
 | 
						|
  // Because of this change, and the difficulty of supporting periods with changing start
 | 
						|
  // times, periods with changing start times are not supported. This makes the logic much
 | 
						|
  // simpler, since periods with the same start time can be considerred the same period
 | 
						|
  // across refreshes.
 | 
						|
  //
 | 
						|
  // To give an example as to the difficulty of handling periods where the start time may
 | 
						|
  // change, if a single period manifest is refreshed with another manifest with a single
 | 
						|
  // period, and both the start and end times are increased, then the only way to determine
 | 
						|
  // if it's a new period or an old one that has changed is to look through the segments of
 | 
						|
  // each playlist and determine the presentation time bounds to find a match. In addition,
 | 
						|
  // if the period start changed to exceed the old period end, then there would be no
 | 
						|
  // match, and it would not be possible to determine whether the refreshed period is a new
 | 
						|
  // one or the old one.
 | 
						|
  const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest));
 | 
						|
  const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that
 | 
						|
  // there's a "memory leak" in that it will never stop growing, in reality, only a couple
 | 
						|
  // of properties are saved for each seen Period. Even long running live streams won't
 | 
						|
  // generate too many Periods, unless the stream is watched for decades. In the future,
 | 
						|
  // this can be optimized by mapping to discontinuity sequence numbers for each timeline,
 | 
						|
  // but it may not become an issue, and the additional info can be useful for debugging.
 | 
						|
 | 
						|
  newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]);
 | 
						|
  updateSequenceNumbers({
 | 
						|
    oldPlaylists,
 | 
						|
    newPlaylists,
 | 
						|
    timelineStarts: newManifest.timelineStarts
 | 
						|
  });
 | 
						|
  return newManifest;
 | 
						|
};
 | 
						|
 | 
						|
const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
 | 
						|
 | 
						|
const mergeDiscontiguousPlaylists = playlists => {
 | 
						|
  // Break out playlists into groups based on their baseUrl
 | 
						|
  const playlistsByBaseUrl = playlists.reduce(function (acc, cur) {
 | 
						|
    if (!acc[cur.attributes.baseUrl]) {
 | 
						|
      acc[cur.attributes.baseUrl] = [];
 | 
						|
    }
 | 
						|
 | 
						|
    acc[cur.attributes.baseUrl].push(cur);
 | 
						|
    return acc;
 | 
						|
  }, {});
 | 
						|
  let allPlaylists = [];
 | 
						|
  Object.values(playlistsByBaseUrl).forEach(playlistGroup => {
 | 
						|
    const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
 | 
						|
      // assuming playlist IDs are the same across periods
 | 
						|
      // TODO: handle multiperiod where representation sets are not the same
 | 
						|
      // across periods
 | 
						|
      const name = playlist.attributes.id + (playlist.attributes.lang || '');
 | 
						|
 | 
						|
      if (!acc[name]) {
 | 
						|
        // First Period
 | 
						|
        acc[name] = playlist;
 | 
						|
        acc[name].attributes.timelineStarts = [];
 | 
						|
      } else {
 | 
						|
        // Subsequent Periods
 | 
						|
        if (playlist.segments) {
 | 
						|
          // first segment of subsequent periods signal a discontinuity
 | 
						|
          if (playlist.segments[0]) {
 | 
						|
            playlist.segments[0].discontinuity = true;
 | 
						|
          }
 | 
						|
 | 
						|
          acc[name].segments.push(...playlist.segments);
 | 
						|
        } // bubble up contentProtection, this assumes all DRM content
 | 
						|
        // has the same contentProtection
 | 
						|
 | 
						|
 | 
						|
        if (playlist.attributes.contentProtection) {
 | 
						|
          acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      acc[name].attributes.timelineStarts.push({
 | 
						|
        // Although they represent the same number, it's important to have both to make it
 | 
						|
        // compatible with HLS potentially having a similar attribute.
 | 
						|
        start: playlist.attributes.periodStart,
 | 
						|
        timeline: playlist.attributes.periodStart
 | 
						|
      });
 | 
						|
      return acc;
 | 
						|
    }, {}));
 | 
						|
    allPlaylists = allPlaylists.concat(mergedPlaylists);
 | 
						|
  });
 | 
						|
  return allPlaylists.map(playlist => {
 | 
						|
    playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
 | 
						|
    return playlist;
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
 | 
						|
  const sidxKey = generateSidxKey(playlist.sidx);
 | 
						|
  const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
 | 
						|
 | 
						|
  if (sidxMatch) {
 | 
						|
    addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri);
 | 
						|
  }
 | 
						|
 | 
						|
  return playlist;
 | 
						|
};
 | 
						|
const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
 | 
						|
  if (!Object.keys(sidxMapping).length) {
 | 
						|
    return playlists;
 | 
						|
  }
 | 
						|
 | 
						|
  for (const i in playlists) {
 | 
						|
    playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
 | 
						|
  }
 | 
						|
 | 
						|
  return playlists;
 | 
						|
};
 | 
						|
const formatAudioPlaylist = ({
 | 
						|
  attributes,
 | 
						|
  segments,
 | 
						|
  sidx,
 | 
						|
  mediaSequence,
 | 
						|
  discontinuitySequence,
 | 
						|
  discontinuityStarts
 | 
						|
}, isAudioOnly) => {
 | 
						|
  const playlist = {
 | 
						|
    attributes: {
 | 
						|
      NAME: attributes.id,
 | 
						|
      BANDWIDTH: attributes.bandwidth,
 | 
						|
      CODECS: attributes.codecs,
 | 
						|
      ['PROGRAM-ID']: 1
 | 
						|
    },
 | 
						|
    uri: '',
 | 
						|
    endList: attributes.type === 'static',
 | 
						|
    timeline: attributes.periodStart,
 | 
						|
    resolvedUri: attributes.baseUrl || '',
 | 
						|
    targetDuration: attributes.duration,
 | 
						|
    discontinuitySequence,
 | 
						|
    discontinuityStarts,
 | 
						|
    timelineStarts: attributes.timelineStarts,
 | 
						|
    mediaSequence,
 | 
						|
    segments
 | 
						|
  };
 | 
						|
 | 
						|
  if (attributes.contentProtection) {
 | 
						|
    playlist.contentProtection = attributes.contentProtection;
 | 
						|
  }
 | 
						|
 | 
						|
  if (attributes.serviceLocation) {
 | 
						|
    playlist.attributes.serviceLocation = attributes.serviceLocation;
 | 
						|
  }
 | 
						|
 | 
						|
  if (sidx) {
 | 
						|
    playlist.sidx = sidx;
 | 
						|
  }
 | 
						|
 | 
						|
  if (isAudioOnly) {
 | 
						|
    playlist.attributes.AUDIO = 'audio';
 | 
						|
    playlist.attributes.SUBTITLES = 'subs';
 | 
						|
  }
 | 
						|
 | 
						|
  return playlist;
 | 
						|
};
 | 
						|
const formatVttPlaylist = ({
 | 
						|
  attributes,
 | 
						|
  segments,
 | 
						|
  mediaSequence,
 | 
						|
  discontinuityStarts,
 | 
						|
  discontinuitySequence
 | 
						|
}) => {
 | 
						|
  if (typeof segments === 'undefined') {
 | 
						|
    // vtt tracks may use single file in BaseURL
 | 
						|
    segments = [{
 | 
						|
      uri: attributes.baseUrl,
 | 
						|
      timeline: attributes.periodStart,
 | 
						|
      resolvedUri: attributes.baseUrl || '',
 | 
						|
      duration: attributes.sourceDuration,
 | 
						|
      number: 0
 | 
						|
    }]; // targetDuration should be the same duration as the only segment
 | 
						|
 | 
						|
    attributes.duration = attributes.sourceDuration;
 | 
						|
  }
 | 
						|
 | 
						|
  const m3u8Attributes = {
 | 
						|
    NAME: attributes.id,
 | 
						|
    BANDWIDTH: attributes.bandwidth,
 | 
						|
    ['PROGRAM-ID']: 1
 | 
						|
  };
 | 
						|
 | 
						|
  if (attributes.codecs) {
 | 
						|
    m3u8Attributes.CODECS = attributes.codecs;
 | 
						|
  }
 | 
						|
 | 
						|
  const vttPlaylist = {
 | 
						|
    attributes: m3u8Attributes,
 | 
						|
    uri: '',
 | 
						|
    endList: attributes.type === 'static',
 | 
						|
    timeline: attributes.periodStart,
 | 
						|
    resolvedUri: attributes.baseUrl || '',
 | 
						|
    targetDuration: attributes.duration,
 | 
						|
    timelineStarts: attributes.timelineStarts,
 | 
						|
    discontinuityStarts,
 | 
						|
    discontinuitySequence,
 | 
						|
    mediaSequence,
 | 
						|
    segments
 | 
						|
  };
 | 
						|
 | 
						|
  if (attributes.serviceLocation) {
 | 
						|
    vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
 | 
						|
  }
 | 
						|
 | 
						|
  return vttPlaylist;
 | 
						|
};
 | 
						|
const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
 | 
						|
  let mainPlaylist;
 | 
						|
  const formattedPlaylists = playlists.reduce((a, playlist) => {
 | 
						|
    const role = playlist.attributes.role && playlist.attributes.role.value || '';
 | 
						|
    const language = playlist.attributes.lang || '';
 | 
						|
    let label = playlist.attributes.label || 'main';
 | 
						|
 | 
						|
    if (language && !playlist.attributes.label) {
 | 
						|
      const roleLabel = role ? ` (${role})` : '';
 | 
						|
      label = `${playlist.attributes.lang}${roleLabel}`;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!a[label]) {
 | 
						|
      a[label] = {
 | 
						|
        language,
 | 
						|
        autoselect: true,
 | 
						|
        default: role === 'main',
 | 
						|
        playlists: [],
 | 
						|
        uri: ''
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);
 | 
						|
    a[label].playlists.push(formatted);
 | 
						|
 | 
						|
    if (typeof mainPlaylist === 'undefined' && role === 'main') {
 | 
						|
      mainPlaylist = playlist;
 | 
						|
      mainPlaylist.default = true;
 | 
						|
    }
 | 
						|
 | 
						|
    return a;
 | 
						|
  }, {}); // if no playlists have role "main", mark the first as main
 | 
						|
 | 
						|
  if (!mainPlaylist) {
 | 
						|
    const firstLabel = Object.keys(formattedPlaylists)[0];
 | 
						|
    formattedPlaylists[firstLabel].default = true;
 | 
						|
  }
 | 
						|
 | 
						|
  return formattedPlaylists;
 | 
						|
};
 | 
						|
const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
 | 
						|
  return playlists.reduce((a, playlist) => {
 | 
						|
    const label = playlist.attributes.label || playlist.attributes.lang || 'text';
 | 
						|
    const language = playlist.attributes.lang || 'und';
 | 
						|
 | 
						|
    if (!a[label]) {
 | 
						|
      a[label] = {
 | 
						|
        language,
 | 
						|
        default: false,
 | 
						|
        autoselect: false,
 | 
						|
        playlists: [],
 | 
						|
        uri: ''
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));
 | 
						|
    return a;
 | 
						|
  }, {});
 | 
						|
};
 | 
						|
 | 
						|
const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => {
 | 
						|
  if (!svc) {
 | 
						|
    return svcObj;
 | 
						|
  }
 | 
						|
 | 
						|
  svc.forEach(service => {
 | 
						|
    const {
 | 
						|
      channel,
 | 
						|
      language
 | 
						|
    } = service;
 | 
						|
    svcObj[language] = {
 | 
						|
      autoselect: false,
 | 
						|
      default: false,
 | 
						|
      instreamId: channel,
 | 
						|
      language
 | 
						|
    };
 | 
						|
 | 
						|
    if (service.hasOwnProperty('aspectRatio')) {
 | 
						|
      svcObj[language].aspectRatio = service.aspectRatio;
 | 
						|
    }
 | 
						|
 | 
						|
    if (service.hasOwnProperty('easyReader')) {
 | 
						|
      svcObj[language].easyReader = service.easyReader;
 | 
						|
    }
 | 
						|
 | 
						|
    if (service.hasOwnProperty('3D')) {
 | 
						|
      svcObj[language]['3D'] = service['3D'];
 | 
						|
    }
 | 
						|
  });
 | 
						|
  return svcObj;
 | 
						|
}, {});
 | 
						|
 | 
						|
const formatVideoPlaylist = ({
 | 
						|
  attributes,
 | 
						|
  segments,
 | 
						|
  sidx,
 | 
						|
  discontinuityStarts
 | 
						|
}) => {
 | 
						|
  const playlist = {
 | 
						|
    attributes: {
 | 
						|
      NAME: attributes.id,
 | 
						|
      AUDIO: 'audio',
 | 
						|
      SUBTITLES: 'subs',
 | 
						|
      RESOLUTION: {
 | 
						|
        width: attributes.width,
 | 
						|
        height: attributes.height
 | 
						|
      },
 | 
						|
      CODECS: attributes.codecs,
 | 
						|
      BANDWIDTH: attributes.bandwidth,
 | 
						|
      ['PROGRAM-ID']: 1
 | 
						|
    },
 | 
						|
    uri: '',
 | 
						|
    endList: attributes.type === 'static',
 | 
						|
    timeline: attributes.periodStart,
 | 
						|
    resolvedUri: attributes.baseUrl || '',
 | 
						|
    targetDuration: attributes.duration,
 | 
						|
    discontinuityStarts,
 | 
						|
    timelineStarts: attributes.timelineStarts,
 | 
						|
    segments
 | 
						|
  };
 | 
						|
 | 
						|
  if (attributes.frameRate) {
 | 
						|
    playlist.attributes['FRAME-RATE'] = attributes.frameRate;
 | 
						|
  }
 | 
						|
 | 
						|
  if (attributes.contentProtection) {
 | 
						|
    playlist.contentProtection = attributes.contentProtection;
 | 
						|
  }
 | 
						|
 | 
						|
  if (attributes.serviceLocation) {
 | 
						|
    playlist.attributes.serviceLocation = attributes.serviceLocation;
 | 
						|
  }
 | 
						|
 | 
						|
  if (sidx) {
 | 
						|
    playlist.sidx = sidx;
 | 
						|
  }
 | 
						|
 | 
						|
  return playlist;
 | 
						|
};
 | 
						|
 | 
						|
const videoOnly = ({
 | 
						|
  attributes
 | 
						|
}) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
 | 
						|
 | 
						|
const audioOnly = ({
 | 
						|
  attributes
 | 
						|
}) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
 | 
						|
 | 
						|
const vttOnly = ({
 | 
						|
  attributes
 | 
						|
}) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
 | 
						|
/**
 | 
						|
 * Contains start and timeline properties denoting a timeline start. For DASH, these will
 | 
						|
 * be the same number.
 | 
						|
 *
 | 
						|
 * @typedef {Object} TimelineStart
 | 
						|
 * @property {number} start - the start time of the timeline
 | 
						|
 * @property {number} timeline - the timeline number
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Adds appropriate media and discontinuity sequence values to the segments and playlists.
 | 
						|
 *
 | 
						|
 * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a
 | 
						|
 * DASH specific attribute used in constructing segment URI's from templates. However, from
 | 
						|
 * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence`
 | 
						|
 * value, which should start at the original media sequence value (or 0) and increment by 1
 | 
						|
 * for each segment thereafter. Since DASH's `startNumber` values are independent per
 | 
						|
 * period, it doesn't make sense to use it for `number`. Instead, assume everything starts
 | 
						|
 * from a 0 mediaSequence value and increment from there.
 | 
						|
 *
 | 
						|
 * Note that VHS currently doesn't use the `number` property, but it can be helpful for
 | 
						|
 * debugging and making sense of the manifest.
 | 
						|
 *
 | 
						|
 * For live playlists, to account for values increasing in manifests when periods are
 | 
						|
 * removed on refreshes, merging logic should be used to update the numbers to their
 | 
						|
 * appropriate values (to ensure they're sequential and increasing).
 | 
						|
 *
 | 
						|
 * @param {Object[]} playlists - the playlists to update
 | 
						|
 * @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
const addMediaSequenceValues = (playlists, timelineStarts) => {
 | 
						|
  // increment all segments sequentially
 | 
						|
  playlists.forEach(playlist => {
 | 
						|
    playlist.mediaSequence = 0;
 | 
						|
    playlist.discontinuitySequence = timelineStarts.findIndex(function ({
 | 
						|
      timeline
 | 
						|
    }) {
 | 
						|
      return timeline === playlist.timeline;
 | 
						|
    });
 | 
						|
 | 
						|
    if (!playlist.segments) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    playlist.segments.forEach((segment, index) => {
 | 
						|
      segment.number = index;
 | 
						|
    });
 | 
						|
  });
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Given a media group object, flattens all playlists within the media group into a single
 | 
						|
 * array.
 | 
						|
 *
 | 
						|
 * @param {Object} mediaGroupObject - the media group object
 | 
						|
 *
 | 
						|
 * @return {Object[]}
 | 
						|
 *         The media group playlists
 | 
						|
 */
 | 
						|
 | 
						|
const flattenMediaGroupPlaylists = mediaGroupObject => {
 | 
						|
  if (!mediaGroupObject) {
 | 
						|
    return [];
 | 
						|
  }
 | 
						|
 | 
						|
  return Object.keys(mediaGroupObject).reduce((acc, label) => {
 | 
						|
    const labelContents = mediaGroupObject[label];
 | 
						|
    return acc.concat(labelContents.playlists);
 | 
						|
  }, []);
 | 
						|
};
 | 
						|
const toM3u8 = ({
 | 
						|
  dashPlaylists,
 | 
						|
  locations,
 | 
						|
  contentSteering,
 | 
						|
  sidxMapping = {},
 | 
						|
  previousManifest,
 | 
						|
  eventStream
 | 
						|
}) => {
 | 
						|
  if (!dashPlaylists.length) {
 | 
						|
    return {};
 | 
						|
  } // grab all main manifest attributes
 | 
						|
 | 
						|
 | 
						|
  const {
 | 
						|
    sourceDuration: duration,
 | 
						|
    type,
 | 
						|
    suggestedPresentationDelay,
 | 
						|
    minimumUpdatePeriod
 | 
						|
  } = dashPlaylists[0].attributes;
 | 
						|
  const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
 | 
						|
  const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
 | 
						|
  const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly));
 | 
						|
  const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean);
 | 
						|
  const manifest = {
 | 
						|
    allowCache: true,
 | 
						|
    discontinuityStarts: [],
 | 
						|
    segments: [],
 | 
						|
    endList: true,
 | 
						|
    mediaGroups: {
 | 
						|
      AUDIO: {},
 | 
						|
      VIDEO: {},
 | 
						|
      ['CLOSED-CAPTIONS']: {},
 | 
						|
      SUBTITLES: {}
 | 
						|
    },
 | 
						|
    uri: '',
 | 
						|
    duration,
 | 
						|
    playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
 | 
						|
  };
 | 
						|
 | 
						|
  if (minimumUpdatePeriod >= 0) {
 | 
						|
    manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000;
 | 
						|
  }
 | 
						|
 | 
						|
  if (locations) {
 | 
						|
    manifest.locations = locations;
 | 
						|
  }
 | 
						|
 | 
						|
  if (contentSteering) {
 | 
						|
    manifest.contentSteering = contentSteering;
 | 
						|
  }
 | 
						|
 | 
						|
  if (type === 'dynamic') {
 | 
						|
    manifest.suggestedPresentationDelay = suggestedPresentationDelay;
 | 
						|
  }
 | 
						|
 | 
						|
  if (eventStream && eventStream.length > 0) {
 | 
						|
    manifest.eventStream = eventStream;
 | 
						|
  }
 | 
						|
 | 
						|
  const isAudioOnly = manifest.playlists.length === 0;
 | 
						|
  const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null;
 | 
						|
  const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null;
 | 
						|
  const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup));
 | 
						|
  const playlistTimelineStarts = formattedPlaylists.map(({
 | 
						|
    timelineStarts
 | 
						|
  }) => timelineStarts);
 | 
						|
  manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts);
 | 
						|
  addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts);
 | 
						|
 | 
						|
  if (organizedAudioGroup) {
 | 
						|
    manifest.mediaGroups.AUDIO.audio = organizedAudioGroup;
 | 
						|
  }
 | 
						|
 | 
						|
  if (organizedVttGroup) {
 | 
						|
    manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup;
 | 
						|
  }
 | 
						|
 | 
						|
  if (captions.length) {
 | 
						|
    manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions);
 | 
						|
  }
 | 
						|
 | 
						|
  if (previousManifest) {
 | 
						|
    return positionManifestOnTimeline({
 | 
						|
      oldManifest: previousManifest,
 | 
						|
      newManifest: manifest
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  return manifest;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Calculates the R (repetition) value for a live stream (for the final segment
 | 
						|
 * in a manifest where the r value is negative 1)
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *        Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *        names as keys
 | 
						|
 * @param {number} time
 | 
						|
 *        current time (typically the total time up until the final segment)
 | 
						|
 * @param {number} duration
 | 
						|
 *        duration property for the given <S />
 | 
						|
 *
 | 
						|
 * @return {number}
 | 
						|
 *        R value to reach the end of the given period
 | 
						|
 */
 | 
						|
const getLiveRValue = (attributes, time, duration) => {
 | 
						|
  const {
 | 
						|
    NOW,
 | 
						|
    clientOffset,
 | 
						|
    availabilityStartTime,
 | 
						|
    timescale = 1,
 | 
						|
    periodStart = 0,
 | 
						|
    minimumUpdatePeriod = 0
 | 
						|
  } = attributes;
 | 
						|
  const now = (NOW + clientOffset) / 1000;
 | 
						|
  const periodStartWC = availabilityStartTime + periodStart;
 | 
						|
  const periodEndWC = now + minimumUpdatePeriod;
 | 
						|
  const periodDuration = periodEndWC - periodStartWC;
 | 
						|
  return Math.ceil((periodDuration * timescale - time) / duration);
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
 | 
						|
 * timing and duration
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *        Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *        names as keys
 | 
						|
 * @param {Object[]} segmentTimeline
 | 
						|
 *        List of objects representing the attributes of each S element contained within
 | 
						|
 *
 | 
						|
 * @return {{number: number, duration: number, time: number, timeline: number}[]}
 | 
						|
 *         List of Objects with segment timing and duration info
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
const parseByTimeline = (attributes, segmentTimeline) => {
 | 
						|
  const {
 | 
						|
    type,
 | 
						|
    minimumUpdatePeriod = 0,
 | 
						|
    media = '',
 | 
						|
    sourceDuration,
 | 
						|
    timescale = 1,
 | 
						|
    startNumber = 1,
 | 
						|
    periodStart: timeline
 | 
						|
  } = attributes;
 | 
						|
  const segments = [];
 | 
						|
  let time = -1;
 | 
						|
 | 
						|
  for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
 | 
						|
    const S = segmentTimeline[sIndex];
 | 
						|
    const duration = S.d;
 | 
						|
    const repeat = S.r || 0;
 | 
						|
    const segmentTime = S.t || 0;
 | 
						|
 | 
						|
    if (time < 0) {
 | 
						|
      // first segment
 | 
						|
      time = segmentTime;
 | 
						|
    }
 | 
						|
 | 
						|
    if (segmentTime && segmentTime > time) {
 | 
						|
      // discontinuity
 | 
						|
      // TODO: How to handle this type of discontinuity
 | 
						|
      // timeline++ here would treat it like HLS discontuity and content would
 | 
						|
      // get appended without gap
 | 
						|
      // E.G.
 | 
						|
      //  <S t="0" d="1" />
 | 
						|
      //  <S d="1" />
 | 
						|
      //  <S d="1" />
 | 
						|
      //  <S t="5" d="1" />
 | 
						|
      // would have $Time$ values of [0, 1, 2, 5]
 | 
						|
      // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
 | 
						|
      // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
 | 
						|
      // does the value of sourceDuration consider this when calculating arbitrary
 | 
						|
      // negative @r repeat value?
 | 
						|
      // E.G. Same elements as above with this added at the end
 | 
						|
      //  <S d="1" r="-1" />
 | 
						|
      //  with a sourceDuration of 10
 | 
						|
      // Would the 2 gaps be included in the time duration calculations resulting in
 | 
						|
      // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
 | 
						|
      // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
 | 
						|
      time = segmentTime;
 | 
						|
    }
 | 
						|
 | 
						|
    let count;
 | 
						|
 | 
						|
    if (repeat < 0) {
 | 
						|
      const nextS = sIndex + 1;
 | 
						|
 | 
						|
      if (nextS === segmentTimeline.length) {
 | 
						|
        // last segment
 | 
						|
        if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) {
 | 
						|
          count = getLiveRValue(attributes, time, duration);
 | 
						|
        } else {
 | 
						|
          // TODO: This may be incorrect depending on conclusion of TODO above
 | 
						|
          count = (sourceDuration * timescale - time) / duration;
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        count = (segmentTimeline[nextS].t - time) / duration;
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      count = repeat + 1;
 | 
						|
    }
 | 
						|
 | 
						|
    const end = startNumber + segments.length + count;
 | 
						|
    let number = startNumber + segments.length;
 | 
						|
 | 
						|
    while (number < end) {
 | 
						|
      segments.push({
 | 
						|
        number,
 | 
						|
        duration: duration / timescale,
 | 
						|
        time,
 | 
						|
        timeline
 | 
						|
      });
 | 
						|
      time += duration;
 | 
						|
      number++;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return segments;
 | 
						|
};
 | 
						|
 | 
						|
const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
 | 
						|
/**
 | 
						|
 * Replaces template identifiers with corresponding values. To be used as the callback
 | 
						|
 * for String.prototype.replace
 | 
						|
 *
 | 
						|
 * @name replaceCallback
 | 
						|
 * @function
 | 
						|
 * @param {string} match
 | 
						|
 *        Entire match of identifier
 | 
						|
 * @param {string} identifier
 | 
						|
 *        Name of matched identifier
 | 
						|
 * @param {string} format
 | 
						|
 *        Format tag string. Its presence indicates that padding is expected
 | 
						|
 * @param {string} width
 | 
						|
 *        Desired length of the replaced value. Values less than this width shall be left
 | 
						|
 *        zero padded
 | 
						|
 * @return {string}
 | 
						|
 *         Replacement for the matched identifier
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a function to be used as a callback for String.prototype.replace to replace
 | 
						|
 * template identifiers
 | 
						|
 *
 | 
						|
 * @param {Obect} values
 | 
						|
 *        Object containing values that shall be used to replace known identifiers
 | 
						|
 * @param {number} values.RepresentationID
 | 
						|
 *        Value of the Representation@id attribute
 | 
						|
 * @param {number} values.Number
 | 
						|
 *        Number of the corresponding segment
 | 
						|
 * @param {number} values.Bandwidth
 | 
						|
 *        Value of the Representation@bandwidth attribute.
 | 
						|
 * @param {number} values.Time
 | 
						|
 *        Timestamp value of the corresponding segment
 | 
						|
 * @return {replaceCallback}
 | 
						|
 *         Callback to be used with String.prototype.replace to replace identifiers
 | 
						|
 */
 | 
						|
 | 
						|
const identifierReplacement = values => (match, identifier, format, width) => {
 | 
						|
  if (match === '$$') {
 | 
						|
    // escape sequence
 | 
						|
    return '$';
 | 
						|
  }
 | 
						|
 | 
						|
  if (typeof values[identifier] === 'undefined') {
 | 
						|
    return match;
 | 
						|
  }
 | 
						|
 | 
						|
  const value = '' + values[identifier];
 | 
						|
 | 
						|
  if (identifier === 'RepresentationID') {
 | 
						|
    // Format tag shall not be present with RepresentationID
 | 
						|
    return value;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!format) {
 | 
						|
    width = 1;
 | 
						|
  } else {
 | 
						|
    width = parseInt(width, 10);
 | 
						|
  }
 | 
						|
 | 
						|
  if (value.length >= width) {
 | 
						|
    return value;
 | 
						|
  }
 | 
						|
 | 
						|
  return `${new Array(width - value.length + 1).join('0')}${value}`;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Constructs a segment url from a template string
 | 
						|
 *
 | 
						|
 * @param {string} url
 | 
						|
 *        Template string to construct url from
 | 
						|
 * @param {Obect} values
 | 
						|
 *        Object containing values that shall be used to replace known identifiers
 | 
						|
 * @param {number} values.RepresentationID
 | 
						|
 *        Value of the Representation@id attribute
 | 
						|
 * @param {number} values.Number
 | 
						|
 *        Number of the corresponding segment
 | 
						|
 * @param {number} values.Bandwidth
 | 
						|
 *        Value of the Representation@bandwidth attribute.
 | 
						|
 * @param {number} values.Time
 | 
						|
 *        Timestamp value of the corresponding segment
 | 
						|
 * @return {string}
 | 
						|
 *         Segment url with identifiers replaced
 | 
						|
 */
 | 
						|
 | 
						|
const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values));
 | 
						|
/**
 | 
						|
 * Generates a list of objects containing timing and duration information about each
 | 
						|
 * segment needed to generate segment uris and the complete segment object
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *        Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *        names as keys
 | 
						|
 * @param {Object[]|undefined} segmentTimeline
 | 
						|
 *        List of objects representing the attributes of each S element contained within
 | 
						|
 *        the SegmentTimeline element
 | 
						|
 * @return {{number: number, duration: number, time: number, timeline: number}[]}
 | 
						|
 *         List of Objects with segment timing and duration info
 | 
						|
 */
 | 
						|
 | 
						|
const parseTemplateInfo = (attributes, segmentTimeline) => {
 | 
						|
  if (!attributes.duration && !segmentTimeline) {
 | 
						|
    // if neither @duration or SegmentTimeline are present, then there shall be exactly
 | 
						|
    // one media segment
 | 
						|
    return [{
 | 
						|
      number: attributes.startNumber || 1,
 | 
						|
      duration: attributes.sourceDuration,
 | 
						|
      time: 0,
 | 
						|
      timeline: attributes.periodStart
 | 
						|
    }];
 | 
						|
  }
 | 
						|
 | 
						|
  if (attributes.duration) {
 | 
						|
    return parseByDuration(attributes);
 | 
						|
  }
 | 
						|
 | 
						|
  return parseByTimeline(attributes, segmentTimeline);
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Generates a list of segments using information provided by the SegmentTemplate element
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *        Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *        names as keys
 | 
						|
 * @param {Object[]|undefined} segmentTimeline
 | 
						|
 *        List of objects representing the attributes of each S element contained within
 | 
						|
 *        the SegmentTimeline element
 | 
						|
 * @return {Object[]}
 | 
						|
 *         List of segment objects
 | 
						|
 */
 | 
						|
 | 
						|
const segmentsFromTemplate = (attributes, segmentTimeline) => {
 | 
						|
  const templateValues = {
 | 
						|
    RepresentationID: attributes.id,
 | 
						|
    Bandwidth: attributes.bandwidth || 0
 | 
						|
  };
 | 
						|
  const {
 | 
						|
    initialization = {
 | 
						|
      sourceURL: '',
 | 
						|
      range: ''
 | 
						|
    }
 | 
						|
  } = attributes;
 | 
						|
  const mapSegment = urlTypeToSegment({
 | 
						|
    baseUrl: attributes.baseUrl,
 | 
						|
    source: constructTemplateUrl(initialization.sourceURL, templateValues),
 | 
						|
    range: initialization.range
 | 
						|
  });
 | 
						|
  const segments = parseTemplateInfo(attributes, segmentTimeline);
 | 
						|
  return segments.map(segment => {
 | 
						|
    templateValues.Number = segment.number;
 | 
						|
    templateValues.Time = segment.time;
 | 
						|
    const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2
 | 
						|
    // - if timescale isn't present on any level, default to 1.
 | 
						|
 | 
						|
    const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
 | 
						|
 | 
						|
    const presentationTimeOffset = attributes.presentationTimeOffset || 0;
 | 
						|
    const presentationTime = // Even if the @t attribute is not specified for the segment, segment.time is
 | 
						|
    // calculated in mpd-parser prior to this, so it's assumed to be available.
 | 
						|
    attributes.periodStart + (segment.time - presentationTimeOffset) / timescale;
 | 
						|
    const map = {
 | 
						|
      uri,
 | 
						|
      timeline: segment.timeline,
 | 
						|
      duration: segment.duration,
 | 
						|
      resolvedUri: resolveUrl__default['default'](attributes.baseUrl || '', uri),
 | 
						|
      map: mapSegment,
 | 
						|
      number: segment.number,
 | 
						|
      presentationTime
 | 
						|
    };
 | 
						|
    return map;
 | 
						|
  });
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
 | 
						|
 * to an object that matches the output of a segment in videojs/mpd-parser
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *   Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *   names as keys
 | 
						|
 * @param {Object} segmentUrl
 | 
						|
 *   <SegmentURL> node to translate into a segment object
 | 
						|
 * @return {Object} translated segment object
 | 
						|
 */
 | 
						|
 | 
						|
const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
 | 
						|
  const {
 | 
						|
    baseUrl,
 | 
						|
    initialization = {}
 | 
						|
  } = attributes;
 | 
						|
  const initSegment = urlTypeToSegment({
 | 
						|
    baseUrl,
 | 
						|
    source: initialization.sourceURL,
 | 
						|
    range: initialization.range
 | 
						|
  });
 | 
						|
  const segment = urlTypeToSegment({
 | 
						|
    baseUrl,
 | 
						|
    source: segmentUrl.media,
 | 
						|
    range: segmentUrl.mediaRange
 | 
						|
  });
 | 
						|
  segment.map = initSegment;
 | 
						|
  return segment;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Generates a list of segments using information provided by the SegmentList element
 | 
						|
 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes.  Each
 | 
						|
 * node should be translated into segment.
 | 
						|
 *
 | 
						|
 * @param {Object} attributes
 | 
						|
 *   Object containing all inherited attributes from parent elements with attribute
 | 
						|
 *   names as keys
 | 
						|
 * @param {Object[]|undefined} segmentTimeline
 | 
						|
 *        List of objects representing the attributes of each S element contained within
 | 
						|
 *        the SegmentTimeline element
 | 
						|
 * @return {Object.<Array>} list of segments
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
const segmentsFromList = (attributes, segmentTimeline) => {
 | 
						|
  const {
 | 
						|
    duration,
 | 
						|
    segmentUrls = [],
 | 
						|
    periodStart
 | 
						|
  } = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR
 | 
						|
  // if both SegmentTimeline and @duration are defined, it is outside of spec.
 | 
						|
 | 
						|
  if (!duration && !segmentTimeline || duration && segmentTimeline) {
 | 
						|
    throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
 | 
						|
  }
 | 
						|
 | 
						|
  const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject));
 | 
						|
  let segmentTimeInfo;
 | 
						|
 | 
						|
  if (duration) {
 | 
						|
    segmentTimeInfo = parseByDuration(attributes);
 | 
						|
  }
 | 
						|
 | 
						|
  if (segmentTimeline) {
 | 
						|
    segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
 | 
						|
  }
 | 
						|
 | 
						|
  const segments = segmentTimeInfo.map((segmentTime, index) => {
 | 
						|
    if (segmentUrlMap[index]) {
 | 
						|
      const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2
 | 
						|
      // - if timescale isn't present on any level, default to 1.
 | 
						|
 | 
						|
      const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
 | 
						|
 | 
						|
      const presentationTimeOffset = attributes.presentationTimeOffset || 0;
 | 
						|
      segment.timeline = segmentTime.timeline;
 | 
						|
      segment.duration = segmentTime.duration;
 | 
						|
      segment.number = segmentTime.number;
 | 
						|
      segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale;
 | 
						|
      return segment;
 | 
						|
    } // Since we're mapping we should get rid of any blank segments (in case
 | 
						|
    // the given SegmentTimeline is handling for more elements than we have
 | 
						|
    // SegmentURLs for).
 | 
						|
 | 
						|
  }).filter(segment => segment);
 | 
						|
  return segments;
 | 
						|
};
 | 
						|
 | 
						|
const generateSegments = ({
 | 
						|
  attributes,
 | 
						|
  segmentInfo
 | 
						|
}) => {
 | 
						|
  let segmentAttributes;
 | 
						|
  let segmentsFn;
 | 
						|
 | 
						|
  if (segmentInfo.template) {
 | 
						|
    segmentsFn = segmentsFromTemplate;
 | 
						|
    segmentAttributes = merge(attributes, segmentInfo.template);
 | 
						|
  } else if (segmentInfo.base) {
 | 
						|
    segmentsFn = segmentsFromBase;
 | 
						|
    segmentAttributes = merge(attributes, segmentInfo.base);
 | 
						|
  } else if (segmentInfo.list) {
 | 
						|
    segmentsFn = segmentsFromList;
 | 
						|
    segmentAttributes = merge(attributes, segmentInfo.list);
 | 
						|
  }
 | 
						|
 | 
						|
  const segmentsInfo = {
 | 
						|
    attributes
 | 
						|
  };
 | 
						|
 | 
						|
  if (!segmentsFn) {
 | 
						|
    return segmentsInfo;
 | 
						|
  }
 | 
						|
 | 
						|
  const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which
 | 
						|
  // must be in seconds. Since we've generated the segment list, we no longer need
 | 
						|
  // @duration to be in @timescale units, so we can convert it here.
 | 
						|
 | 
						|
  if (segmentAttributes.duration) {
 | 
						|
    const {
 | 
						|
      duration,
 | 
						|
      timescale = 1
 | 
						|
    } = segmentAttributes;
 | 
						|
    segmentAttributes.duration = duration / timescale;
 | 
						|
  } else if (segments.length) {
 | 
						|
    // if there is no @duration attribute, use the largest segment duration as
 | 
						|
    // as target duration
 | 
						|
    segmentAttributes.duration = segments.reduce((max, segment) => {
 | 
						|
      return Math.max(max, Math.ceil(segment.duration));
 | 
						|
    }, 0);
 | 
						|
  } else {
 | 
						|
    segmentAttributes.duration = 0;
 | 
						|
  }
 | 
						|
 | 
						|
  segmentsInfo.attributes = segmentAttributes;
 | 
						|
  segmentsInfo.segments = segments; // This is a sidx box without actual segment information
 | 
						|
 | 
						|
  if (segmentInfo.base && segmentAttributes.indexRange) {
 | 
						|
    segmentsInfo.sidx = segments[0];
 | 
						|
    segmentsInfo.segments = [];
 | 
						|
  }
 | 
						|
 | 
						|
  return segmentsInfo;
 | 
						|
};
 | 
						|
const toPlaylists = representations => representations.map(generateSegments);
 | 
						|
 | 
						|
const findChildren = (element, name) => from(element.childNodes).filter(({
 | 
						|
  tagName
 | 
						|
}) => tagName === name);
 | 
						|
const getContent = element => element.textContent.trim();
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts the provided string that may contain a division operation to a number.
 | 
						|
 *
 | 
						|
 * @param {string} value - the provided string value
 | 
						|
 *
 | 
						|
 * @return {number} the parsed string value
 | 
						|
 */
 | 
						|
const parseDivisionValue = value => {
 | 
						|
  return parseFloat(value.split('/').reduce((prev, current) => prev / current));
 | 
						|
};
 | 
						|
 | 
						|
const parseDuration = str => {
 | 
						|
  const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
 | 
						|
  const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
 | 
						|
  const SECONDS_IN_DAY = 24 * 60 * 60;
 | 
						|
  const SECONDS_IN_HOUR = 60 * 60;
 | 
						|
  const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S
 | 
						|
 | 
						|
  const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
 | 
						|
  const match = durationRegex.exec(str);
 | 
						|
 | 
						|
  if (!match) {
 | 
						|
    return 0;
 | 
						|
  }
 | 
						|
 | 
						|
  const [year, month, day, hour, minute, second] = match.slice(1);
 | 
						|
  return parseFloat(year || 0) * SECONDS_IN_YEAR + parseFloat(month || 0) * SECONDS_IN_MONTH + parseFloat(day || 0) * SECONDS_IN_DAY + parseFloat(hour || 0) * SECONDS_IN_HOUR + parseFloat(minute || 0) * SECONDS_IN_MIN + parseFloat(second || 0);
 | 
						|
};
 | 
						|
const parseDate = str => {
 | 
						|
  // Date format without timezone according to ISO 8601
 | 
						|
  // YYY-MM-DDThh:mm:ss.ssssss
 | 
						|
  const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is
 | 
						|
  // expressed by ending with 'Z'
 | 
						|
 | 
						|
  if (dateRegex.test(str)) {
 | 
						|
    str += 'Z';
 | 
						|
  }
 | 
						|
 | 
						|
  return Date.parse(str);
 | 
						|
};
 | 
						|
 | 
						|
const parsers = {
 | 
						|
  /**
 | 
						|
   * Specifies the duration of the entire Media Presentation. Format is a duration string
 | 
						|
   * as specified in ISO 8601
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The duration in seconds
 | 
						|
   */
 | 
						|
  mediaPresentationDuration(value) {
 | 
						|
    return parseDuration(value);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the Segment availability start time for all Segments referred to in this
 | 
						|
   * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
 | 
						|
   * time. Format is a date string as specified in ISO 8601
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The date as seconds from unix epoch
 | 
						|
   */
 | 
						|
  availabilityStartTime(value) {
 | 
						|
    return parseDate(value) / 1000;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the smallest period between potential changes to the MPD. Format is a
 | 
						|
   * duration string as specified in ISO 8601
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The duration in seconds
 | 
						|
   */
 | 
						|
  minimumUpdatePeriod(value) {
 | 
						|
    return parseDuration(value);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the suggested presentation delay. Format is a
 | 
						|
   * duration string as specified in ISO 8601
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The duration in seconds
 | 
						|
   */
 | 
						|
  suggestedPresentationDelay(value) {
 | 
						|
    return parseDuration(value);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * specifices the type of mpd. Can be either "static" or "dynamic"
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   *
 | 
						|
   * @return {string}
 | 
						|
   *         The type as a string
 | 
						|
   */
 | 
						|
  type(value) {
 | 
						|
    return value;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the duration of the smallest time shifting buffer for any Representation
 | 
						|
   * in the MPD. Format is a duration string as specified in ISO 8601
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The duration in seconds
 | 
						|
   */
 | 
						|
  timeShiftBufferDepth(value) {
 | 
						|
    return parseDuration(value);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
 | 
						|
   * Format is a duration string as specified in ISO 8601
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The duration in seconds
 | 
						|
   */
 | 
						|
  start(value) {
 | 
						|
    return parseDuration(value);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the width of the visual presentation
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed width
 | 
						|
   */
 | 
						|
  width(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the height of the visual presentation
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed height
 | 
						|
   */
 | 
						|
  height(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the bitrate of the representation
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed bandwidth
 | 
						|
   */
 | 
						|
  bandwidth(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the frame rate of the representation
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed frame rate
 | 
						|
   */
 | 
						|
  frameRate(value) {
 | 
						|
    return parseDivisionValue(value);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the number of the first Media Segment in this Representation in the Period
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed number
 | 
						|
   */
 | 
						|
  startNumber(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the timescale in units per seconds
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed timescale
 | 
						|
   */
 | 
						|
  timescale(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the presentationTimeOffset.
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of the attribute as a string
 | 
						|
   *
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed presentationTimeOffset
 | 
						|
   */
 | 
						|
  presentationTimeOffset(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the constant approximate Segment duration
 | 
						|
   * NOTE: The <Period> element also contains an @duration attribute. This duration
 | 
						|
   *       specifies the duration of the Period. This attribute is currently not
 | 
						|
   *       supported by the rest of the parser, however we still check for it to prevent
 | 
						|
   *       errors.
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed duration
 | 
						|
   */
 | 
						|
  duration(value) {
 | 
						|
    const parsedValue = parseInt(value, 10);
 | 
						|
 | 
						|
    if (isNaN(parsedValue)) {
 | 
						|
      return parseDuration(value);
 | 
						|
    }
 | 
						|
 | 
						|
    return parsedValue;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the Segment duration, in units of the value of the @timescale.
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed duration
 | 
						|
   */
 | 
						|
  d(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the MPD start time, in @timescale units, the first Segment in the series
 | 
						|
   * starts relative to the beginning of the Period
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed time
 | 
						|
   */
 | 
						|
  t(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the repeat count of the number of following contiguous Segments with the
 | 
						|
   * same duration expressed by the value of @d
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed number
 | 
						|
   */
 | 
						|
  r(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Specifies the presentationTime.
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of the attribute as a string
 | 
						|
   *
 | 
						|
   * @return {number}
 | 
						|
   *         The parsed presentationTime
 | 
						|
   */
 | 
						|
  presentationTime(value) {
 | 
						|
    return parseInt(value, 10);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Default parser for all other attributes. Acts as a no-op and just returns the value
 | 
						|
   * as a string
 | 
						|
   *
 | 
						|
   * @param {string} value
 | 
						|
   *        value of attribute as a string
 | 
						|
   * @return {string}
 | 
						|
   *         Unparsed value
 | 
						|
   */
 | 
						|
  DEFAULT(value) {
 | 
						|
    return value;
 | 
						|
  }
 | 
						|
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Gets all the attributes and values of the provided node, parses attributes with known
 | 
						|
 * types, and returns an object with attribute names mapped to values.
 | 
						|
 *
 | 
						|
 * @param {Node} el
 | 
						|
 *        The node to parse attributes from
 | 
						|
 * @return {Object}
 | 
						|
 *         Object with all attributes of el parsed
 | 
						|
 */
 | 
						|
 | 
						|
const parseAttributes = el => {
 | 
						|
  if (!(el && el.attributes)) {
 | 
						|
    return {};
 | 
						|
  }
 | 
						|
 | 
						|
  return from(el.attributes).reduce((a, e) => {
 | 
						|
    const parseFn = parsers[e.name] || parsers.DEFAULT;
 | 
						|
    a[e.name] = parseFn(e.value);
 | 
						|
    return a;
 | 
						|
  }, {});
 | 
						|
};
 | 
						|
 | 
						|
const keySystemsMap = {
 | 
						|
  'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
 | 
						|
  'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
 | 
						|
  'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
 | 
						|
  'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime',
 | 
						|
  // ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme
 | 
						|
  'urn:mpeg:dash:mp4protection:2011': 'mp4protection'
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Builds a list of urls that is the product of the reference urls and BaseURL values
 | 
						|
 *
 | 
						|
 * @param {Object[]} references
 | 
						|
 *        List of objects containing the reference URL as well as its attributes
 | 
						|
 * @param {Node[]} baseUrlElements
 | 
						|
 *        List of BaseURL nodes from the mpd
 | 
						|
 * @return {Object[]}
 | 
						|
 *         List of objects with resolved urls and attributes
 | 
						|
 */
 | 
						|
 | 
						|
const buildBaseUrls = (references, baseUrlElements) => {
 | 
						|
  if (!baseUrlElements.length) {
 | 
						|
    return references;
 | 
						|
  }
 | 
						|
 | 
						|
  return flatten(references.map(function (reference) {
 | 
						|
    return baseUrlElements.map(function (baseUrlElement) {
 | 
						|
      const initialBaseUrl = getContent(baseUrlElement);
 | 
						|
      const resolvedBaseUrl = resolveUrl__default['default'](reference.baseUrl, initialBaseUrl);
 | 
						|
      const finalBaseUrl = merge(parseAttributes(baseUrlElement), {
 | 
						|
        baseUrl: resolvedBaseUrl
 | 
						|
      }); // If the URL is resolved, we want to get the serviceLocation from the reference
 | 
						|
      // assuming there is no serviceLocation on the initialBaseUrl
 | 
						|
 | 
						|
      if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
 | 
						|
        finalBaseUrl.serviceLocation = reference.serviceLocation;
 | 
						|
      }
 | 
						|
 | 
						|
      return finalBaseUrl;
 | 
						|
    });
 | 
						|
  }));
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Contains all Segment information for its containing AdaptationSet
 | 
						|
 *
 | 
						|
 * @typedef {Object} SegmentInformation
 | 
						|
 * @property {Object|undefined} template
 | 
						|
 *           Contains the attributes for the SegmentTemplate node
 | 
						|
 * @property {Object[]|undefined} segmentTimeline
 | 
						|
 *           Contains a list of atrributes for each S node within the SegmentTimeline node
 | 
						|
 * @property {Object|undefined} list
 | 
						|
 *           Contains the attributes for the SegmentList node
 | 
						|
 * @property {Object|undefined} base
 | 
						|
 *           Contains the attributes for the SegmentBase node
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns all available Segment information contained within the AdaptationSet node
 | 
						|
 *
 | 
						|
 * @param {Node} adaptationSet
 | 
						|
 *        The AdaptationSet node to get Segment information from
 | 
						|
 * @return {SegmentInformation}
 | 
						|
 *         The Segment information contained within the provided AdaptationSet
 | 
						|
 */
 | 
						|
 | 
						|
const getSegmentInformation = adaptationSet => {
 | 
						|
  const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
 | 
						|
  const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
 | 
						|
  const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge({
 | 
						|
    tag: 'SegmentURL'
 | 
						|
  }, parseAttributes(s)));
 | 
						|
  const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
 | 
						|
  const segmentTimelineParentNode = segmentList || segmentTemplate;
 | 
						|
  const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
 | 
						|
  const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
 | 
						|
  const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both
 | 
						|
  // @initialization and an <Initialization> node.  @initialization can be templated,
 | 
						|
  // while the node can have a url and range specified.  If the <SegmentTemplate> has
 | 
						|
  // both @initialization and an <Initialization> subelement we opt to override with
 | 
						|
  // the node, as this interaction is not defined in the spec.
 | 
						|
 | 
						|
  const template = segmentTemplate && parseAttributes(segmentTemplate);
 | 
						|
 | 
						|
  if (template && segmentInitialization) {
 | 
						|
    template.initialization = segmentInitialization && parseAttributes(segmentInitialization);
 | 
						|
  } else if (template && template.initialization) {
 | 
						|
    // If it is @initialization we convert it to an object since this is the format that
 | 
						|
    // later functions will rely on for the initialization segment.  This is only valid
 | 
						|
    // for <SegmentTemplate>
 | 
						|
    template.initialization = {
 | 
						|
      sourceURL: template.initialization
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  const segmentInfo = {
 | 
						|
    template,
 | 
						|
    segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)),
 | 
						|
    list: segmentList && merge(parseAttributes(segmentList), {
 | 
						|
      segmentUrls,
 | 
						|
      initialization: parseAttributes(segmentInitialization)
 | 
						|
    }),
 | 
						|
    base: segmentBase && merge(parseAttributes(segmentBase), {
 | 
						|
      initialization: parseAttributes(segmentInitialization)
 | 
						|
    })
 | 
						|
  };
 | 
						|
  Object.keys(segmentInfo).forEach(key => {
 | 
						|
    if (!segmentInfo[key]) {
 | 
						|
      delete segmentInfo[key];
 | 
						|
    }
 | 
						|
  });
 | 
						|
  return segmentInfo;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Contains Segment information and attributes needed to construct a Playlist object
 | 
						|
 * from a Representation
 | 
						|
 *
 | 
						|
 * @typedef {Object} RepresentationInformation
 | 
						|
 * @property {SegmentInformation} segmentInfo
 | 
						|
 *           Segment information for this Representation
 | 
						|
 * @property {Object} attributes
 | 
						|
 *           Inherited attributes for this Representation
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Maps a Representation node to an object containing Segment information and attributes
 | 
						|
 *
 | 
						|
 * @name inheritBaseUrlsCallback
 | 
						|
 * @function
 | 
						|
 * @param {Node} representation
 | 
						|
 *        Representation node from the mpd
 | 
						|
 * @return {RepresentationInformation}
 | 
						|
 *         Representation information needed to construct a Playlist object
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a callback for Array.prototype.map for mapping Representation nodes to
 | 
						|
 * Segment information and attributes using inherited BaseURL nodes.
 | 
						|
 *
 | 
						|
 * @param {Object} adaptationSetAttributes
 | 
						|
 *        Contains attributes inherited by the AdaptationSet
 | 
						|
 * @param {Object[]} adaptationSetBaseUrls
 | 
						|
 *        List of objects containing resolved base URLs and attributes
 | 
						|
 *        inherited by the AdaptationSet
 | 
						|
 * @param {SegmentInformation} adaptationSetSegmentInfo
 | 
						|
 *        Contains Segment information for the AdaptationSet
 | 
						|
 * @return {inheritBaseUrlsCallback}
 | 
						|
 *         Callback map function
 | 
						|
 */
 | 
						|
 | 
						|
const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => {
 | 
						|
  const repBaseUrlElements = findChildren(representation, 'BaseURL');
 | 
						|
  const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
 | 
						|
  const attributes = merge(adaptationSetAttributes, parseAttributes(representation));
 | 
						|
  const representationSegmentInfo = getSegmentInformation(representation);
 | 
						|
  return repBaseUrls.map(baseUrl => {
 | 
						|
    return {
 | 
						|
      segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
 | 
						|
      attributes: merge(attributes, baseUrl)
 | 
						|
    };
 | 
						|
  });
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Tranforms a series of content protection nodes to
 | 
						|
 * an object containing pssh data by key system
 | 
						|
 *
 | 
						|
 * @param {Node[]} contentProtectionNodes
 | 
						|
 *        Content protection nodes
 | 
						|
 * @return {Object}
 | 
						|
 *        Object containing pssh data by key system
 | 
						|
 */
 | 
						|
 | 
						|
const generateKeySystemInformation = contentProtectionNodes => {
 | 
						|
  return contentProtectionNodes.reduce((acc, node) => {
 | 
						|
    const attributes = parseAttributes(node); // Although it could be argued that according to the UUID RFC spec the UUID string (a-f chars) should be generated
 | 
						|
    // as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system
 | 
						|
    // UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do
 | 
						|
    // .toLowerCase() on the input UUID string from the manifest (at least I could not think of one).
 | 
						|
 | 
						|
    if (attributes.schemeIdUri) {
 | 
						|
      attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase();
 | 
						|
    }
 | 
						|
 | 
						|
    const keySystem = keySystemsMap[attributes.schemeIdUri];
 | 
						|
 | 
						|
    if (keySystem) {
 | 
						|
      acc[keySystem] = {
 | 
						|
        attributes
 | 
						|
      };
 | 
						|
      const psshNode = findChildren(node, 'cenc:pssh')[0];
 | 
						|
 | 
						|
      if (psshNode) {
 | 
						|
        const pssh = getContent(psshNode);
 | 
						|
        acc[keySystem].pssh = pssh && decodeB64ToUint8Array__default['default'](pssh);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return acc;
 | 
						|
  }, {});
 | 
						|
}; // defined in ANSI_SCTE 214-1 2016
 | 
						|
 | 
						|
 | 
						|
const parseCaptionServiceMetadata = service => {
 | 
						|
  // 608 captions
 | 
						|
  if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') {
 | 
						|
    const values = typeof service.value !== 'string' ? [] : service.value.split(';');
 | 
						|
    return values.map(value => {
 | 
						|
      let channel;
 | 
						|
      let language; // default language to value
 | 
						|
 | 
						|
      language = value;
 | 
						|
 | 
						|
      if (/^CC\d=/.test(value)) {
 | 
						|
        [channel, language] = value.split('=');
 | 
						|
      } else if (/^CC\d$/.test(value)) {
 | 
						|
        channel = value;
 | 
						|
      }
 | 
						|
 | 
						|
      return {
 | 
						|
        channel,
 | 
						|
        language
 | 
						|
      };
 | 
						|
    });
 | 
						|
  } else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') {
 | 
						|
    const values = typeof service.value !== 'string' ? [] : service.value.split(';');
 | 
						|
    return values.map(value => {
 | 
						|
      const flags = {
 | 
						|
        // service or channel number 1-63
 | 
						|
        'channel': undefined,
 | 
						|
        // language is a 3ALPHA per ISO 639.2/B
 | 
						|
        // field is required
 | 
						|
        'language': undefined,
 | 
						|
        // BIT 1/0 or ?
 | 
						|
        // default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown
 | 
						|
        'aspectRatio': 1,
 | 
						|
        // BIT 1/0
 | 
						|
        // easy reader flag indicated the text is tailed to the needs of beginning readers
 | 
						|
        // default 0, or off
 | 
						|
        'easyReader': 0,
 | 
						|
        // BIT 1/0
 | 
						|
        // If 3d metadata is present (CEA-708.1) then 1
 | 
						|
        // default 0
 | 
						|
        '3D': 0
 | 
						|
      };
 | 
						|
 | 
						|
      if (/=/.test(value)) {
 | 
						|
        const [channel, opts = ''] = value.split('=');
 | 
						|
        flags.channel = channel;
 | 
						|
        flags.language = value;
 | 
						|
        opts.split(',').forEach(opt => {
 | 
						|
          const [name, val] = opt.split(':');
 | 
						|
 | 
						|
          if (name === 'lang') {
 | 
						|
            flags.language = val; // er for easyReadery
 | 
						|
          } else if (name === 'er') {
 | 
						|
            flags.easyReader = Number(val); // war for wide aspect ratio
 | 
						|
          } else if (name === 'war') {
 | 
						|
            flags.aspectRatio = Number(val);
 | 
						|
          } else if (name === '3D') {
 | 
						|
            flags['3D'] = Number(val);
 | 
						|
          }
 | 
						|
        });
 | 
						|
      } else {
 | 
						|
        flags.language = value;
 | 
						|
      }
 | 
						|
 | 
						|
      if (flags.channel) {
 | 
						|
        flags.channel = 'SERVICE' + flags.channel;
 | 
						|
      }
 | 
						|
 | 
						|
      return flags;
 | 
						|
    });
 | 
						|
  }
 | 
						|
};
 | 
						|
/**
 | 
						|
 * A map callback that will parse all event stream data for a collection of periods
 | 
						|
 * DASH ISO_IEC_23009 5.10.2.2
 | 
						|
 * https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing
 | 
						|
 *
 | 
						|
 * @param {PeriodInformation} period object containing necessary period information
 | 
						|
 * @return a collection of parsed eventstream event objects
 | 
						|
 */
 | 
						|
 | 
						|
const toEventStream = period => {
 | 
						|
  // get and flatten all EventStreams tags and parse attributes and children
 | 
						|
  return flatten(findChildren(period.node, 'EventStream').map(eventStream => {
 | 
						|
    const eventStreamAttributes = parseAttributes(eventStream);
 | 
						|
    const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects
 | 
						|
 | 
						|
    return findChildren(eventStream, 'Event').map(event => {
 | 
						|
      const eventAttributes = parseAttributes(event);
 | 
						|
      const presentationTime = eventAttributes.presentationTime || 0;
 | 
						|
      const timescale = eventStreamAttributes.timescale || 1;
 | 
						|
      const duration = eventAttributes.duration || 0;
 | 
						|
      const start = presentationTime / timescale + period.attributes.start;
 | 
						|
      return {
 | 
						|
        schemeIdUri,
 | 
						|
        value: eventStreamAttributes.value,
 | 
						|
        id: eventAttributes.id,
 | 
						|
        start,
 | 
						|
        end: start + duration / timescale,
 | 
						|
        messageData: getContent(event) || eventAttributes.messageData,
 | 
						|
        contentEncoding: eventStreamAttributes.contentEncoding,
 | 
						|
        presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0
 | 
						|
      };
 | 
						|
    });
 | 
						|
  }));
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Maps an AdaptationSet node to a list of Representation information objects
 | 
						|
 *
 | 
						|
 * @name toRepresentationsCallback
 | 
						|
 * @function
 | 
						|
 * @param {Node} adaptationSet
 | 
						|
 *        AdaptationSet node from the mpd
 | 
						|
 * @return {RepresentationInformation[]}
 | 
						|
 *         List of objects containing Representaion information
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
 | 
						|
 * Representation information objects
 | 
						|
 *
 | 
						|
 * @param {Object} periodAttributes
 | 
						|
 *        Contains attributes inherited by the Period
 | 
						|
 * @param {Object[]} periodBaseUrls
 | 
						|
 *        Contains list of objects with resolved base urls and attributes
 | 
						|
 *        inherited by the Period
 | 
						|
 * @param {string[]} periodSegmentInfo
 | 
						|
 *        Contains Segment Information at the period level
 | 
						|
 * @return {toRepresentationsCallback}
 | 
						|
 *         Callback map function
 | 
						|
 */
 | 
						|
 | 
						|
const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => {
 | 
						|
  const adaptationSetAttributes = parseAttributes(adaptationSet);
 | 
						|
  const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL'));
 | 
						|
  const role = findChildren(adaptationSet, 'Role')[0];
 | 
						|
  const roleAttributes = {
 | 
						|
    role: parseAttributes(role)
 | 
						|
  };
 | 
						|
  let attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes);
 | 
						|
  const accessibility = findChildren(adaptationSet, 'Accessibility')[0];
 | 
						|
  const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility));
 | 
						|
 | 
						|
  if (captionServices) {
 | 
						|
    attrs = merge(attrs, {
 | 
						|
      captionServices
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  const label = findChildren(adaptationSet, 'Label')[0];
 | 
						|
 | 
						|
  if (label && label.childNodes.length) {
 | 
						|
    const labelVal = label.childNodes[0].nodeValue.trim();
 | 
						|
    attrs = merge(attrs, {
 | 
						|
      label: labelVal
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
 | 
						|
 | 
						|
  if (Object.keys(contentProtection).length) {
 | 
						|
    attrs = merge(attrs, {
 | 
						|
      contentProtection
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  const segmentInfo = getSegmentInformation(adaptationSet);
 | 
						|
  const representations = findChildren(adaptationSet, 'Representation');
 | 
						|
  const adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo);
 | 
						|
  return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Contains all period information for mapping nodes onto adaptation sets.
 | 
						|
 *
 | 
						|
 * @typedef {Object} PeriodInformation
 | 
						|
 * @property {Node} period.node
 | 
						|
 *           Period node from the mpd
 | 
						|
 * @property {Object} period.attributes
 | 
						|
 *           Parsed period attributes from node plus any added
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Maps a PeriodInformation object to a list of Representation information objects for all
 | 
						|
 * AdaptationSet nodes contained within the Period.
 | 
						|
 *
 | 
						|
 * @name toAdaptationSetsCallback
 | 
						|
 * @function
 | 
						|
 * @param {PeriodInformation} period
 | 
						|
 *        Period object containing necessary period information
 | 
						|
 * @param {number} periodStart
 | 
						|
 *        Start time of the Period within the mpd
 | 
						|
 * @return {RepresentationInformation[]}
 | 
						|
 *         List of objects containing Representaion information
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of
 | 
						|
 * Representation information objects
 | 
						|
 *
 | 
						|
 * @param {Object} mpdAttributes
 | 
						|
 *        Contains attributes inherited by the mpd
 | 
						|
  * @param {Object[]} mpdBaseUrls
 | 
						|
 *        Contains list of objects with resolved base urls and attributes
 | 
						|
 *        inherited by the mpd
 | 
						|
 * @return {toAdaptationSetsCallback}
 | 
						|
 *         Callback map function
 | 
						|
 */
 | 
						|
 | 
						|
const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
 | 
						|
  const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL'));
 | 
						|
  const periodAttributes = merge(mpdAttributes, {
 | 
						|
    periodStart: period.attributes.start
 | 
						|
  });
 | 
						|
 | 
						|
  if (typeof period.attributes.duration === 'number') {
 | 
						|
    periodAttributes.periodDuration = period.attributes.duration;
 | 
						|
  }
 | 
						|
 | 
						|
  const adaptationSets = findChildren(period.node, 'AdaptationSet');
 | 
						|
  const periodSegmentInfo = getSegmentInformation(period.node);
 | 
						|
  return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Tranforms an array of content steering nodes into an object
 | 
						|
 * containing CDN content steering information from the MPD manifest.
 | 
						|
 *
 | 
						|
 * For more information on the DASH spec for Content Steering parsing, see:
 | 
						|
 * https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
 | 
						|
 *
 | 
						|
 * @param {Node[]} contentSteeringNodes
 | 
						|
 *        Content steering nodes
 | 
						|
 * @param {Function} eventHandler
 | 
						|
 *        The event handler passed into the parser options to handle warnings
 | 
						|
 * @return {Object}
 | 
						|
 *        Object containing content steering data
 | 
						|
 */
 | 
						|
 | 
						|
const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => {
 | 
						|
  // If there are more than one ContentSteering tags, throw an error
 | 
						|
  if (contentSteeringNodes.length > 1) {
 | 
						|
    eventHandler({
 | 
						|
      type: 'warn',
 | 
						|
      message: 'The MPD manifest should contain no more than one ContentSteering tag'
 | 
						|
    });
 | 
						|
  } // Return a null value if there are no ContentSteering tags
 | 
						|
 | 
						|
 | 
						|
  if (!contentSteeringNodes.length) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const infoFromContentSteeringTag = merge({
 | 
						|
    serverURL: getContent(contentSteeringNodes[0])
 | 
						|
  }, parseAttributes(contentSteeringNodes[0])); // Converts `queryBeforeStart` to a boolean, as well as setting the default value
 | 
						|
  // to `false` if it doesn't exist
 | 
						|
 | 
						|
  infoFromContentSteeringTag.queryBeforeStart = infoFromContentSteeringTag.queryBeforeStart === 'true';
 | 
						|
  return infoFromContentSteeringTag;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Gets Period@start property for a given period.
 | 
						|
 *
 | 
						|
 * @param {Object} options
 | 
						|
 *        Options object
 | 
						|
 * @param {Object} options.attributes
 | 
						|
 *        Period attributes
 | 
						|
 * @param {Object} [options.priorPeriodAttributes]
 | 
						|
 *        Prior period attributes (if prior period is available)
 | 
						|
 * @param {string} options.mpdType
 | 
						|
 *        The MPD@type these periods came from
 | 
						|
 * @return {number|null}
 | 
						|
 *         The period start, or null if it's an early available period or error
 | 
						|
 */
 | 
						|
 | 
						|
const getPeriodStart = ({
 | 
						|
  attributes,
 | 
						|
  priorPeriodAttributes,
 | 
						|
  mpdType
 | 
						|
}) => {
 | 
						|
  // Summary of period start time calculation from DASH spec section 5.3.2.1
 | 
						|
  //
 | 
						|
  // A period's start is the first period's start + time elapsed after playing all
 | 
						|
  // prior periods to this one. Periods continue one after the other in time (without
 | 
						|
  // gaps) until the end of the presentation.
 | 
						|
  //
 | 
						|
  // The value of Period@start should be:
 | 
						|
  // 1. if Period@start is present: value of Period@start
 | 
						|
  // 2. if previous period exists and it has @duration: previous Period@start +
 | 
						|
  //    previous Period@duration
 | 
						|
  // 3. if this is first period and MPD@type is 'static': 0
 | 
						|
  // 4. in all other cases, consider the period an "early available period" (note: not
 | 
						|
  //    currently supported)
 | 
						|
  // (1)
 | 
						|
  if (typeof attributes.start === 'number') {
 | 
						|
    return attributes.start;
 | 
						|
  } // (2)
 | 
						|
 | 
						|
 | 
						|
  if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') {
 | 
						|
    return priorPeriodAttributes.start + priorPeriodAttributes.duration;
 | 
						|
  } // (3)
 | 
						|
 | 
						|
 | 
						|
  if (!priorPeriodAttributes && mpdType === 'static') {
 | 
						|
    return 0;
 | 
						|
  } // (4)
 | 
						|
  // There is currently no logic for calculating the Period@start value if there is
 | 
						|
  // no Period@start or prior Period@start and Period@duration available. This is not made
 | 
						|
  // explicit by the DASH interop guidelines or the DASH spec, however, since there's
 | 
						|
  // nothing about any other resolution strategies, it's implied. Thus, this case should
 | 
						|
  // be considered an early available period, or error, and null should suffice for both
 | 
						|
  // of those cases.
 | 
						|
 | 
						|
 | 
						|
  return null;
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Traverses the mpd xml tree to generate a list of Representation information objects
 | 
						|
 * that have inherited attributes from parent nodes
 | 
						|
 *
 | 
						|
 * @param {Node} mpd
 | 
						|
 *        The root node of the mpd
 | 
						|
 * @param {Object} options
 | 
						|
 *        Available options for inheritAttributes
 | 
						|
 * @param {string} options.manifestUri
 | 
						|
 *        The uri source of the mpd
 | 
						|
 * @param {number} options.NOW
 | 
						|
 *        Current time per DASH IOP.  Default is current time in ms since epoch
 | 
						|
 * @param {number} options.clientOffset
 | 
						|
 *        Client time difference from NOW (in milliseconds)
 | 
						|
 * @return {RepresentationInformation[]}
 | 
						|
 *         List of objects containing Representation information
 | 
						|
 */
 | 
						|
 | 
						|
const inheritAttributes = (mpd, options = {}) => {
 | 
						|
  const {
 | 
						|
    manifestUri = '',
 | 
						|
    NOW = Date.now(),
 | 
						|
    clientOffset = 0,
 | 
						|
    // TODO: For now, we are expecting an eventHandler callback function
 | 
						|
    // to be passed into the mpd parser as an option.
 | 
						|
    // In the future, we should enable stream parsing by using the Stream class from vhs-utils.
 | 
						|
    // This will support new features including a standardized event handler.
 | 
						|
    // See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing.
 | 
						|
    // https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9
 | 
						|
    eventHandler = function () {}
 | 
						|
  } = options;
 | 
						|
  const periodNodes = findChildren(mpd, 'Period');
 | 
						|
 | 
						|
  if (!periodNodes.length) {
 | 
						|
    throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
 | 
						|
  }
 | 
						|
 | 
						|
  const locations = findChildren(mpd, 'Location');
 | 
						|
  const mpdAttributes = parseAttributes(mpd);
 | 
						|
  const mpdBaseUrls = buildBaseUrls([{
 | 
						|
    baseUrl: manifestUri
 | 
						|
  }], findChildren(mpd, 'BaseURL'));
 | 
						|
  const contentSteeringNodes = findChildren(mpd, 'ContentSteering'); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
 | 
						|
 | 
						|
  mpdAttributes.type = mpdAttributes.type || 'static';
 | 
						|
  mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
 | 
						|
  mpdAttributes.NOW = NOW;
 | 
						|
  mpdAttributes.clientOffset = clientOffset;
 | 
						|
 | 
						|
  if (locations.length) {
 | 
						|
    mpdAttributes.locations = locations.map(getContent);
 | 
						|
  }
 | 
						|
 | 
						|
  const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to
 | 
						|
  // adding properties that require looking at prior periods is to parse attributes and add
 | 
						|
  // missing ones before toAdaptationSets is called. If more such properties are added, it
 | 
						|
  // may be better to refactor toAdaptationSets.
 | 
						|
 | 
						|
  periodNodes.forEach((node, index) => {
 | 
						|
    const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary
 | 
						|
    // for this period.
 | 
						|
 | 
						|
    const priorPeriod = periods[index - 1];
 | 
						|
    attributes.start = getPeriodStart({
 | 
						|
      attributes,
 | 
						|
      priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null,
 | 
						|
      mpdType: mpdAttributes.type
 | 
						|
    });
 | 
						|
    periods.push({
 | 
						|
      node,
 | 
						|
      attributes
 | 
						|
    });
 | 
						|
  });
 | 
						|
  return {
 | 
						|
    locations: mpdAttributes.locations,
 | 
						|
    contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler),
 | 
						|
    // TODO: There are occurences where this `representationInfo` array contains undesired
 | 
						|
    // duplicates. This generally occurs when there are multiple BaseURL nodes that are
 | 
						|
    // direct children of the MPD node. When we attempt to resolve URLs from a combination of the
 | 
						|
    // parent BaseURL and a child BaseURL, and the value does not resolve,
 | 
						|
    // we end up returning the child BaseURL multiple times.
 | 
						|
    // We need to determine a way to remove these duplicates in a safe way.
 | 
						|
    // See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527
 | 
						|
    representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
 | 
						|
    eventStream: flatten(periods.map(toEventStream))
 | 
						|
  };
 | 
						|
};
 | 
						|
 | 
						|
const stringToMpdXml = manifestString => {
 | 
						|
  if (manifestString === '') {
 | 
						|
    throw new Error(errors.DASH_EMPTY_MANIFEST);
 | 
						|
  }
 | 
						|
 | 
						|
  const parser = new xmldom.DOMParser();
 | 
						|
  let xml;
 | 
						|
  let mpd;
 | 
						|
 | 
						|
  try {
 | 
						|
    xml = parser.parseFromString(manifestString, 'application/xml');
 | 
						|
    mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
 | 
						|
  } catch (e) {// ie 11 throws on invalid xml
 | 
						|
  }
 | 
						|
 | 
						|
  if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
 | 
						|
    throw new Error(errors.DASH_INVALID_XML);
 | 
						|
  }
 | 
						|
 | 
						|
  return mpd;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
 | 
						|
 *
 | 
						|
 * @param {string} mpd
 | 
						|
 *        XML string of the MPD manifest
 | 
						|
 * @return {Object|null}
 | 
						|
 *         Attributes of UTCTiming node specified in the manifest. Null if none found
 | 
						|
 */
 | 
						|
 | 
						|
const parseUTCTimingScheme = mpd => {
 | 
						|
  const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
 | 
						|
 | 
						|
  if (!UTCTimingNode) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const attributes = parseAttributes(UTCTimingNode);
 | 
						|
 | 
						|
  switch (attributes.schemeIdUri) {
 | 
						|
    case 'urn:mpeg:dash:utc:http-head:2014':
 | 
						|
    case 'urn:mpeg:dash:utc:http-head:2012':
 | 
						|
      attributes.method = 'HEAD';
 | 
						|
      break;
 | 
						|
 | 
						|
    case 'urn:mpeg:dash:utc:http-xsdate:2014':
 | 
						|
    case 'urn:mpeg:dash:utc:http-iso:2014':
 | 
						|
    case 'urn:mpeg:dash:utc:http-xsdate:2012':
 | 
						|
    case 'urn:mpeg:dash:utc:http-iso:2012':
 | 
						|
      attributes.method = 'GET';
 | 
						|
      break;
 | 
						|
 | 
						|
    case 'urn:mpeg:dash:utc:direct:2014':
 | 
						|
    case 'urn:mpeg:dash:utc:direct:2012':
 | 
						|
      attributes.method = 'DIRECT';
 | 
						|
      attributes.value = Date.parse(attributes.value);
 | 
						|
      break;
 | 
						|
 | 
						|
    case 'urn:mpeg:dash:utc:http-ntp:2014':
 | 
						|
    case 'urn:mpeg:dash:utc:ntp:2014':
 | 
						|
    case 'urn:mpeg:dash:utc:sntp:2014':
 | 
						|
    default:
 | 
						|
      throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
 | 
						|
  }
 | 
						|
 | 
						|
  return attributes;
 | 
						|
};
 | 
						|
 | 
						|
const VERSION = version;
 | 
						|
/*
 | 
						|
 * Given a DASH manifest string and options, parses the DASH manifest into an object in the
 | 
						|
 * form outputed by m3u8-parser and accepted by videojs/http-streaming.
 | 
						|
 *
 | 
						|
 * For live DASH manifests, if `previousManifest` is provided in options, then the newly
 | 
						|
 * parsed DASH manifest will have its media sequence and discontinuity sequence values
 | 
						|
 * updated to reflect its position relative to the prior manifest.
 | 
						|
 *
 | 
						|
 * @param {string} manifestString - the DASH manifest as a string
 | 
						|
 * @param {options} [options] - any options
 | 
						|
 *
 | 
						|
 * @return {Object} the manifest object
 | 
						|
 */
 | 
						|
 | 
						|
const parse = (manifestString, options = {}) => {
 | 
						|
  const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
 | 
						|
  const playlists = toPlaylists(parsedManifestInfo.representationInfo);
 | 
						|
  return toM3u8({
 | 
						|
    dashPlaylists: playlists,
 | 
						|
    locations: parsedManifestInfo.locations,
 | 
						|
    contentSteering: parsedManifestInfo.contentSteeringInfo,
 | 
						|
    sidxMapping: options.sidxMapping,
 | 
						|
    previousManifest: options.previousManifest,
 | 
						|
    eventStream: parsedManifestInfo.eventStream
 | 
						|
  });
 | 
						|
};
 | 
						|
/**
 | 
						|
 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
 | 
						|
 *
 | 
						|
 * @param {string} manifestString
 | 
						|
 *        XML string of the MPD manifest
 | 
						|
 * @return {Object|null}
 | 
						|
 *         Attributes of UTCTiming node specified in the manifest. Null if none found
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString));
 | 
						|
 | 
						|
exports.VERSION = VERSION;
 | 
						|
exports.addSidxSegmentsToPlaylist = addSidxSegmentsToPlaylist$1;
 | 
						|
exports.generateSidxKey = generateSidxKey;
 | 
						|
exports.inheritAttributes = inheritAttributes;
 | 
						|
exports.parse = parse;
 | 
						|
exports.parseUTCTiming = parseUTCTiming;
 | 
						|
exports.stringToMpdXml = stringToMpdXml;
 | 
						|
exports.toM3u8 = toM3u8;
 | 
						|
exports.toPlaylists = toPlaylists;
 |