/*! @name mpd-parser @version 1.3.1 @license Apache-2.0 */
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@xmldom/xmldom')) :
  typeof define === 'function' && define.amd ? define(['exports', '@xmldom/xmldom'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mpdParser = {}, global.window));
}(this, (function (exports, xmldom) { 'use strict';

  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'
  };

  var urlToolkit = {exports: {}};

  (function (module, exports) {
    // see https://tools.ietf.org/html/rfc1808
    (function (root) {
      var URL_REGEX = /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/;
      var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/;
      var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
      var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g;
      var URLToolkit = {
        // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
        // E.g
        // With opts.alwaysNormalize = false (default, spec compliant)
        // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
        // With opts.alwaysNormalize = true (not spec compliant)
        // http://a.com/b/cd + /e/f/../g => http://a.com/e/g
        buildAbsoluteURL: function (baseURL, relativeURL, opts) {
          opts = opts || {}; // remove any remaining space and CRLF

          baseURL = baseURL.trim();
          relativeURL = relativeURL.trim();

          if (!relativeURL) {
            // 2a) If the embedded URL is entirely empty, it inherits the
            // entire base URL (i.e., is set equal to the base URL)
            // and we are done.
            if (!opts.alwaysNormalize) {
              return baseURL;
            }

            var basePartsForNormalise = URLToolkit.parseURL(baseURL);

            if (!basePartsForNormalise) {
              throw new Error('Error trying to parse base URL.');
            }

            basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
            return URLToolkit.buildURLFromParts(basePartsForNormalise);
          }

          var relativeParts = URLToolkit.parseURL(relativeURL);

          if (!relativeParts) {
            throw new Error('Error trying to parse relative URL.');
          }

          if (relativeParts.scheme) {
            // 2b) If the embedded URL starts with a scheme name, it is
            // interpreted as an absolute URL and we are done.
            if (!opts.alwaysNormalize) {
              return relativeURL;
            }

            relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
            return URLToolkit.buildURLFromParts(relativeParts);
          }

          var baseParts = URLToolkit.parseURL(baseURL);

          if (!baseParts) {
            throw new Error('Error trying to parse base URL.');
          }

          if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
            // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
            // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
            var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
            baseParts.netLoc = pathParts[1];
            baseParts.path = pathParts[2];
          }

          if (baseParts.netLoc && !baseParts.path) {
            baseParts.path = '/';
          }

          var builtParts = {
            // 2c) Otherwise, the embedded URL inherits the scheme of
            // the base URL.
            scheme: baseParts.scheme,
            netLoc: relativeParts.netLoc,
            path: null,
            params: relativeParts.params,
            query: relativeParts.query,
            fragment: relativeParts.fragment
          };

          if (!relativeParts.netLoc) {
            // 3) If the embedded URL's <net_loc> is non-empty, we skip to
            // Step 7.  Otherwise, the embedded URL inherits the <net_loc>
            // (if any) of the base URL.
            builtParts.netLoc = baseParts.netLoc; // 4) If the embedded URL path is preceded by a slash "/", the
            // path is not relative and we skip to Step 7.

            if (relativeParts.path[0] !== '/') {
              if (!relativeParts.path) {
                // 5) If the embedded URL path is empty (and not preceded by a
                // slash), then the embedded URL inherits the base URL path
                builtParts.path = baseParts.path; // 5a) if the embedded URL's <params> is non-empty, we skip to
                // step 7; otherwise, it inherits the <params> of the base
                // URL (if any) and

                if (!relativeParts.params) {
                  builtParts.params = baseParts.params; // 5b) if the embedded URL's <query> is non-empty, we skip to
                  // step 7; otherwise, it inherits the <query> of the base
                  // URL (if any) and we skip to step 7.

                  if (!relativeParts.query) {
                    builtParts.query = baseParts.query;
                  }
                }
              } else {
                // 6) The last segment of the base URL's path (anything
                // following the rightmost slash "/", or the entire path if no
                // slash is present) is removed and the embedded URL's path is
                // appended in its place.
                var baseURLPath = baseParts.path;
                var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
                builtParts.path = URLToolkit.normalizePath(newPath);
              }
            }
          }

          if (builtParts.path === null) {
            builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
          }

          return URLToolkit.buildURLFromParts(builtParts);
        },
        parseURL: function (url) {
          var parts = URL_REGEX.exec(url);

          if (!parts) {
            return null;
          }

          return {
            scheme: parts[1] || '',
            netLoc: parts[2] || '',
            path: parts[3] || '',
            params: parts[4] || '',
            query: parts[5] || '',
            fragment: parts[6] || ''
          };
        },
        normalizePath: function (path) {
          // The following operations are
          // then applied, in order, to the new path:
          // 6a) All occurrences of "./", where "." is a complete path
          // segment, are removed.
          // 6b) If the path ends with "." as a complete path segment,
          // that "." is removed.
          path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); // 6c) All occurrences of "<segment>/../", where <segment> is a
          // complete path segment not equal to "..", are removed.
          // Removal of these path segments is performed iteratively,
          // removing the leftmost matching pattern on each iteration,
          // until no matching pattern remains.
          // 6d) If the path ends with "<segment>/..", where <segment> is a
          // complete path segment not equal to "..", that
          // "<segment>/.." is removed.

          while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {}

          return path.split('').reverse().join('');
        },
        buildURLFromParts: function (parts) {
          return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
        }
      };
      module.exports = URLToolkit;
    })();
  })(urlToolkit);

  var URLToolkit = urlToolkit.exports;

  var DEFAULT_LOCATION = 'http://example.com';

  var resolveUrl = function resolveUrl(baseUrl, relativeUrl) {
    // return early if we don't need to resolve
    if (/^[a-z]+:/i.test(relativeUrl)) {
      return relativeUrl;
    } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location


    if (/^data:/.test(baseUrl)) {
      baseUrl = window.location && window.location.href || '';
    } // IE11 supports URL but not the URL constructor
    // feature detect the behavior we want


    var nativeURL = typeof window.URL === 'function';
    var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node)
    // and if baseUrl isn't an absolute url

    var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location

    if (nativeURL) {
      baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION);
    } else if (!/\/\//i.test(baseUrl)) {
      baseUrl = URLToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl);
    }

    if (nativeURL) {
      var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol
      // and if we're location-less, remove the location
      // otherwise, return the url unmodified

      if (removeLocation) {
        return newUrl.href.slice(DEFAULT_LOCATION.length);
      } else if (protocolLess) {
        return newUrl.href.slice(newUrl.protocol.length);
      }

      return newUrl.href;
    }

    return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
  };

  /**
   * @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(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.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10);
      let endRange = window.BigInt ? window.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.BigInt(endRange) - window.BigInt(startRange) + window.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.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.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.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.BigInt(size) - window.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.BigInt(size);
      } else {
        startIndex += size;
      }

      presentationTime += duration / timescale;
      number++;
    }

    playlist.segments = segments;
    return playlist;
  };

  /**
   * Loops through all supported media groups in master and calls the provided
   * callback for each group
   *
   * @param {Object} master
   *        The parsed master manifest object
   * @param {string[]} groups
   *        The media groups to call the callback for
   * @param {Function} callback
   *        Callback to call for each media group
   */
  var forEachMediaGroup = function forEachMediaGroup(master, groups, callback) {
    groups.forEach(function (mediaType) {
      for (var groupKey in master.mediaGroups[mediaType]) {
        for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
          var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
          callback(mediaProperties, mediaType, groupKey, labelKey);
        }
      }
    });
  };

  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 = [];
    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(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;
    }, {});
  };

  var atob = function atob(s) {
    return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
  };

  function decodeB64ToUint8Array(b64Text) {
    var decodedString = atob(b64Text);
    var array = new Uint8Array(decodedString.length);

    for (var i = 0; i < decodedString.length; i++) {
      array[i] = decodedString.charCodeAt(i);
    }

    return array;
  }

  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(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(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;

  Object.defineProperty(exports, '__esModule', { value: true });

})));
