# Source: m3u8/parser.js

    /**
     * @file m3u8/parser.js
     */
    import Stream from '../stream' ;
    import LineStream from './line-stream';
    import ParseStream from './parse-stream';
    import {mergeOptions} from 'video.js';

    /**
     * A parser for M3U8 files. The current interpretation of the input is
     * exposed as a property `manifest` on parser objects. It's just two lines to
     * create and parse a manifest once you have the contents available as a string:
     *
     * ```js
     * var parser = new videojs.m3u8.Parser();
     * parser.push(xhr.responseText);
     * ```
     *
     * New input can later be applied to update the manifest object by calling
     * `push` again.
     *
     * The parser attempts to create a usable manifest object even if the
     * underlying input is somewhat nonsensical. It emits `info` and `warning`
     * events during the parse if it encounters input that seems invalid or
     * requires some property of the manifest object to be defaulted.
     *
     * @class Parser
     * @extends Stream
     */
    export default class Parser extends Stream {
      constructor() {
        super();
        this.lineStream = new LineStream();
        this.parseStream = new ParseStream();
        this.lineStream.pipe(this.parseStream);
        /* eslint-disable consistent-this */
        let self = this;
        /* eslint-enable consistent-this */
        let uris = [];
        let currentUri = {};
        let key;
        let noop = function() {};
        let defaultMediaGroups = {
          'AUDIO': {},
          'VIDEO': {},
          'CLOSED-CAPTIONS': {},
          'SUBTITLES': {}
        };
        // group segments into numbered timelines delineated by discontinuities
        let currentTimeline = 0;

        // the manifest is empty until the parse stream begins delivering data
        this.manifest = {
          allowCache: true,
          discontinuityStarts: []
        };

        // update the manifest with the m3u8 entry from the parse stream
        this.parseStream.on('data', function(entry) {
          let mediaGroup;
          let rendition;

          ({
            tag() {
              // switch based on the tag type
              (({
                'allow-cache'() {
                  this.manifest.allowCache = entry.allowed;
                  if (!('allowed' in entry)) {
                    this.trigger('info', {
                      message: 'defaulting allowCache to YES'
                    });
                    this.manifest.allowCache = true;
                  }
                },
                byterange() {
                  let byterange = {};

                  if ('length' in entry) {
                    currentUri.byterange = byterange;
                    byterange.length = entry.length;

                    if (!('offset' in entry)) {
                      this.trigger('info', {
                        message: 'defaulting offset to zero'
                      });
                      entry.offset = 0;
                    }
                  }
                  if ('offset' in entry) {
                    currentUri.byterange = byterange;
                    byterange.offset = entry.offset;
                  }
                },
                endlist() {
                  this.manifest.endList = true;
                },
                inf() {
                  if (!('mediaSequence' in this.manifest)) {
                    this.manifest.mediaSequence = 0;
                    this.trigger('info', {
                      message: 'defaulting media sequence to zero'
                    });
                  }
                  if (!('discontinuitySequence' in this.manifest)) {
                    this.manifest.discontinuitySequence = 0;
                    this.trigger('info', {
                      message: 'defaulting discontinuity sequence to zero'
                    });
                  }
                  if (entry.duration > 0) {
                    currentUri.duration = entry.duration;
                  }

                  if (entry.duration === 0) {
                    currentUri.duration = 0.01;
                    this.trigger('info', {
                      message: 'updating zero segment duration to a small value'
                    });
                  }

                  this.manifest.segments = uris;
                },
                key() {
                  if (!entry.attributes) {
                    this.trigger('warn', {
                      message: 'ignoring key declaration without attribute list'
                    });
                    return;
                  }
                  // clear the active encryption key
                  if (entry.attributes.METHOD === 'NONE') {
                    key = null;
                    return;
                  }
                  if (!entry.attributes.URI) {
                    this.trigger('warn', {
                      message: 'ignoring key declaration without URI'
                    });
                    return;
                  }
                  if (!entry.attributes.METHOD) {
                    this.trigger('warn', {
                      message: 'defaulting key method to AES-128'
                    });
                  }

                  // setup an encryption key for upcoming segments
                  key = {
                    method: entry.attributes.METHOD || 'AES-128',
                    uri: entry.attributes.URI
                  };

                  if (typeof entry.attributes.IV !== 'undefined') {
                    key.iv = entry.attributes.IV;
                  }
                },
                'media-sequence'() {
                  if (!isFinite(entry.number)) {
                    this.trigger('warn', {
                      message: 'ignoring invalid media sequence: ' + entry.number
                    });
                    return;
                  }
                  this.manifest.mediaSequence = entry.number;
                },
                'discontinuity-sequence'() {
                  if (!isFinite(entry.number)) {
                    this.trigger('warn', {
                      message: 'ignoring invalid discontinuity sequence: ' + entry.number
                    });
                    return;
                  }
                  this.manifest.discontinuitySequence = entry.number;
                  currentTimeline = entry.number;
                },
                'playlist-type'() {
                  if (!(/VOD|EVENT/).test(entry.playlistType)) {
                    this.trigger('warn', {
                      message: 'ignoring unknown playlist type: ' + entry.playlist
                    });
                    return;
                  }
                  this.manifest.playlistType = entry.playlistType;
                },
                'stream-inf'() {
                  this.manifest.playlists = uris;
                  this.manifest.mediaGroups =
                    this.manifest.mediaGroups || defaultMediaGroups;

                  if (!entry.attributes) {
                    this.trigger('warn', {
                      message: 'ignoring empty stream-inf attributes'
                    });
                    return;
                  }

                  if (!currentUri.attributes) {
                    currentUri.attributes = {};
                  }
                  currentUri.attributes = mergeOptions(currentUri.attributes,
                                                       entry.attributes);
                },
                media() {
                  this.manifest.mediaGroups =
                    this.manifest.mediaGroups || defaultMediaGroups;

                  if (!(entry.attributes &&
                        entry.attributes.TYPE &&
                        entry.attributes['GROUP-ID'] &&
                        entry.attributes.NAME)) {
                    this.trigger('warn', {
                      message: 'ignoring incomplete or missing media group'
                    });
                    return;
                  }

                  // find the media group, creating defaults as necessary
                  let mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];

                  mediaGroupType[entry.attributes['GROUP-ID']] =
                    mediaGroupType[entry.attributes['GROUP-ID']] || {};
                  mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];

                  // collect the rendition metadata
                  rendition = {
                    default: (/yes/i).test(entry.attributes.DEFAULT)
                  };
                  if (rendition.default) {
                    rendition.autoselect = true;
                  } else {
                    rendition.autoselect = (/yes/i).test(entry.attributes.AUTOSELECT);
                  }
                  if (entry.attributes.LANGUAGE) {
                    rendition.language = entry.attributes.LANGUAGE;
                  }
                  if (entry.attributes.URI) {
                    rendition.uri = entry.attributes.URI;
                  }

                  // insert the new rendition
                  mediaGroup[entry.attributes.NAME] = rendition;
                },
                discontinuity() {
                  currentTimeline += 1;
                  currentUri.discontinuity = true;
                  this.manifest.discontinuityStarts.push(uris.length);
                },
                targetduration() {
                  if (!isFinite(entry.duration) || entry.duration < 0) {
                    this.trigger('warn', {
                      message: 'ignoring invalid target duration: ' + entry.duration
                    });
                    return;
                  }
                  this.manifest.targetDuration = entry.duration;
                },
                totalduration() {
                  if (!isFinite(entry.duration) || entry.duration < 0) {
                    this.trigger('warn', {
                      message: 'ignoring invalid total duration: ' + entry.duration
                    });
                    return;
                  }
                  this.manifest.totalDuration = entry.duration;
                }
              })[entry.tagType] || noop).call(self);
            },
            uri() {
              currentUri.uri = entry.uri;
              uris.push(currentUri);

              // if no explicit duration was declared, use the target duration
              if (this.manifest.targetDuration &&
                  !('duration' in currentUri)) {
                this.trigger('warn', {
                  message: 'defaulting segment duration to the target duration'
                });
                currentUri.duration = this.manifest.targetDuration;
              }
              // annotate with encryption information, if necessary
              if (key) {
                currentUri.key = key;
              }
              currentUri.timeline = currentTimeline;

              // prepare for the next URI
              currentUri = {};
            },
            comment() {
              // comments are not important for playback
            }
          })[entry.type].call(self);
        });

      }

      /**
       * Parse the input string and update the manifest object.
       *
       * @param {String} chunk a potentially incomplete portion of the manifest
       */
      push(chunk) {
        this.lineStream.push(chunk);
      }

      /**
       * Flush any remaining input. This can be handy if the last line of an M3U8
       * manifest did not contain a trailing newline but the file has been
       * completely received.
       */
      end() {
        // flush any buffered input
        this.lineStream.push('\n');
      }

    }