PageSourceSearch

https://leicestershireccc.co.uk/assets/js/videojs-http-streaming.min.js?h=1790937032

js leicestershireccc.co.uk collected 2026-10-02 10:32:15 UTC 1,252,981 bytes, 37,013 lines download raw bytes

1/*! @name @videojs/http-streaming @version 3.17.2
vendor: 8,545 bytes, lines 1-297
1 @license Apache-2.0 */
2(function (global, factory) {
3  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('video.js'), require('@xmldom/xmldom')) :
4  typeof define === 'function' && define.amd ? define(['exports', 'video.js', '@xmldom/xmldom'], factory) :
5  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.httpStreaming = {}, global.videojs, global.window));
6})(this, (function (exports, videojs, xmldom) { 'use strict';
7
8  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
10  var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
11
12  function _extends() {
13    _extends = Object.assign || function (target) {
14      for (var i = 1; i < arguments.length; i++) {
15        var source = arguments[i];
16
17        for (var key in source) {
18          if (Object.prototype.hasOwnProperty.call(source, key)) {
19            target[key] = source[key];
20          }
21        }
22      }
23
24      return target;
25    };
26
27    return _extends.apply(this, arguments);
28  }
29
30  var DEFAULT_LOCATION = 'https://example.com';
31
32  var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) {
33    // return early if we don't need to resolve
34    if (/^[a-z]+:/i.test(relativeUrl)) {
35      return relativeUrl;
36    } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location
37
38
39    if (/^data:/.test(baseUrl)) {
40      baseUrl = window.location && window.location.href || '';
41    }
42
43    var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node)
44    // and if baseUrl isn't an absolute url
45
46    var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location
47
48    baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION);
49    var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol
50    // and if we're location-less, remove the location
51    // otherwise, return the url unmodified
52
53    if (removeLocation) {
54      return newUrl.href.slice(DEFAULT_LOCATION.length);
55    } else if (protocolLess) {
56      return newUrl.href.slice(newUrl.protocol.length);
57    }
58
59    return newUrl.href;
60  };
61
62  /**
63   * @file resolve-url.js - Handling how URLs are resolved and manipulated
64   */
65  const resolveUrl = resolveUrl$1;
66  /**
67   * If the xhr request was redirected, return the responseURL, otherwise,
68   * return the original url.
69   *
70   * @api private
71   *
72   * @param  {string} url - an url being requested
73   * @param  {XMLHttpRequest} req - xhr request result
74   *
75   * @return {string}
76   */
77
78  const resolveManifestRedirect = (url, req) => {
79    // To understand how the responseURL below is set and generated:
80    // - https://fetch.spec.whatwg.org/#concept-response-url
81    // - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
82    if (req && req.responseURL && url !== req.responseURL) {
83      return req.responseURL;
84    }
85
86    return url;
87  };
88
89  const logger = source => {
90    if (videojs__default["default"].log.debug) {
91      return videojs__default["default"].log.debug.bind(videojs__default["default"], 'VHS:', `${source} >`);
92    }
93
94    return function () {};
95  };
96
97  /**
98   * @file stream.js
99   */
100
101  /**
102   * A lightweight readable stream implemention that handles event dispatching.
103   *
104   * @class Stream
105   */
106  var Stream = /*#__PURE__*/function () {
107    function Stream() {
108      this.listeners = {};
109    }
110    /**
111     * Add a listener for a specified event type.
112     *
113     * @param {string} type the event name
114     * @param {Function} listener the callback to be invoked when an event of
115     * the specified type occurs
116     */
117
118
119    var _proto = Stream.prototype;
120
121    _proto.on = function on(type, listener) {
122      if (!this.listeners[type]) {
123        this.listeners[type] = [];
124      }
125
126      this.listeners[type].push(listener);
127    }
128    /**
129     * Remove a listener for a specified event type.
130     *
131     * @param {string} type the event name
132     * @param {Function} listener  a function previously registered for this
133     * type of event through `on`
134     * @return {boolean} if we could turn it off or not
135     */
136    ;
137
138    _proto.off = function off(type, listener) {
139      if (!this.listeners[type]) {
140        return false;
141      }
142
143      var index = this.listeners[type].indexOf(listener); // TODO: which is better?
144      // In Video.js we slice listener functions
145      // on trigger so that it does not mess up the order
146      // while we loop through.
147      //
148      // Here we slice on off so that the loop in trigger
149      // can continue using it's old reference to loop without
150      // messing up the order.
151
152      this.listeners[type] = this.listeners[type].slice(0);
153      this.listeners[type].splice(index, 1);
154      return index > -1;
155    }
156    /**
157     * Trigger an event of the specified type on this stream. Any additional
158     * arguments to this function are passed as parameters to event listeners.
159     *
160     * @param {string} type the event name
161     */
162    ;
163
164    _proto.trigger = function trigger(type) {
165      var callbacks = this.listeners[type];
166
167      if (!callbacks) {
168        return;
169      } // Slicing the arguments on every invocation of this method
170      // can add a significant amount of overhead. Avoid the
171      // intermediate object creation for the common case of a
172      // single callback argument
173
174
175      if (arguments.length === 2) {
176        var length = callbacks.length;
177
178        for (var i = 0; i < length; ++i) {
179          callbacks[i].call(this, arguments[1]);
180        }
181      } else {
182        var args = Array.prototype.slice.call(arguments, 1);
183        var _length = callbacks.length;
184
185        for (var _i = 0; _i < _length; ++_i) {
186          callbacks[_i].apply(this, args);
187        }
188      }
189    }
190    /**
191     * Destroys the stream and cleans up.
192     */
193    ;
194
195    _proto.dispose = function dispose() {
196      this.listeners = {};
197    }
198    /**
199     * Forwards all `data` events on this stream to the destination stream. The
200     * destination stream should provide a method `push` to receive the data
201     * events as they arrive.
202     *
203     * @param {Stream} destination the stream that will receive all `data` events
204     * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
205     */
206    ;
207
208    _proto.pipe = function pipe(destination) {
209      this.on('data', function (data) {
210        destination.push(data);
211      });
212    };
213
214    return Stream;
215  }();
216
217  var atob = function atob(s) {
218    return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
219  };
220
221  function decodeB64ToUint8Array(b64Text) {
222    var decodedString = atob(b64Text);
223    var array = new Uint8Array(decodedString.length);
224
225    for (var i = 0; i < decodedString.length; i++) {
226      array[i] = decodedString.charCodeAt(i);
227    }
228
229    return array;
230  }
231
232  /*! @name m3u8-parser @version 7.2.0 @license Apache-2.0 */
233  /**
234   * @file m3u8/line-stream.js
235   */
236
237  /**
238   * A stream that buffers string input and generates a `data` event for each
239   * line.
240   *
241   * @class LineStream
242   * @extends Stream
243   */
244
245  class LineStream extends Stream {
246    constructor() {
247      super();
248      this.buffer = '';
249    }
250    /**
251     * Add new data to be parsed.
252     *
253     * @param {string} data the text to process
254     */
255
256
257    push(data) {
258      let nextNewline;
259      this.buffer += data;
260      nextNewline = this.buffer.indexOf('\n');
261
262      for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
263        this.trigger('data', this.buffer.substring(0, nextNewline));
264        this.buffer = this.buffer.substring(nextNewline + 1);
265      }
266    }
267
268  }
269
270  const TAB = String.fromCharCode(0x09);
271
272  const parseByterange = function (byterangeString) {
273    // optionally match and capture 0+ digits before `@`
274    // optionally match and capture 0+ digits after `@`
275    const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
276    const result = {};
277
278    if (match[1]) {
279      result.length = parseInt(match[1], 10);
280    }
281
282    if (match[2]) {
283      result.offset = parseInt(match[2], 10);
284    }
285
286    return result;
287  };
288  /**
289   * "forgiving" attribute list psuedo-grammar:
290   * attributes -> keyvalue (',' keyvalue)*
291   * keyvalue   -> key '=' value
292   * key        -> [^=]*
293   * value      -> '"' [^"]* '"' | [^,]*
294   */
295
296
297  const attributeSeparator = function () {
vendor: 64,075 bytes, lines 298-2194
298    const key = '[^=]*';
299    const value = '"[^"]*"|[^,]*';
300    const keyvalue = '(?:' + key + ')=(?:' + value + ')';
301    return new RegExp('(?:^|,)(' + keyvalue + ')');
302  };
303  /**
304   * Parse attributes from a line given the separator
305   *
306   * @param {string} attributes the attribute line to parse
307   */
308
309
310  const parseAttributes$1 = function (attributes) {
311    const result = {};
312
313    if (!attributes) {
314      return result;
315    } // split the string using attributes as the separator
316
317
318    const attrs = attributes.split(attributeSeparator());
319    let i = attrs.length;
320    let attr;
321
322    while (i--) {
323      // filter out unmatched portions of the string
324      if (attrs[i] === '') {
325        continue;
326      } // split the key and value
327
328
329      attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value
330
331      attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
332      attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
333      attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
334      result[attr[0]] = attr[1];
335    }
336
337    return result;
338  };
339  /**
340   * Converts a string into a resolution object
341   *
342   * @param {string} resolution a string such as 3840x2160
343   *
344   * @return {Object} An object representing the resolution
345   *
346   */
347
348
349  const parseResolution = resolution => {
350    const split = resolution.split('x');
351    const result = {};
352
353    if (split[0]) {
354      result.width = parseInt(split[0], 10);
355    }
356
357    if (split[1]) {
358      result.height = parseInt(split[1], 10);
359    }
360
361    return result;
362  };
363  /**
364   * A line-level M3U8 parser event stream. It expects to receive input one
365   * line at a time and performs a context-free parse of its contents. A stream
366   * interpretation of a manifest can be useful if the manifest is expected to
367   * be too large to fit comfortably into memory or the entirety of the input
368   * is not immediately available. Otherwise, it's probably much easier to work
369   * with a regular `Parser` object.
370   *
371   * Produces `data` events with an object that captures the parser's
372   * interpretation of the input. That object has a property `tag` that is one
373   * of `uri`, `comment`, or `tag`. URIs only have a single additional
374   * property, `line`, which captures the entirety of the input without
375   * interpretation. Comments similarly have a single additional property
376   * `text` which is the input without the leading `#`.
377   *
378   * Tags always have a property `tagType` which is the lower-cased version of
379   * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
380   * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
381   * tags are given the tag type `unknown` and a single additional property
382   * `data` with the remainder of the input.
383   *
384   * @class ParseStream
385   * @extends Stream
386   */
387
388
389  class ParseStream extends Stream {
390    constructor() {
391      super();
392      this.customParsers = [];
393      this.tagMappers = [];
394    }
395    /**
396     * Parses an additional line of input.
397     *
398     * @param {string} line a single line of an M3U8 file to parse
399     */
400
401
402    push(line) {
403      let match;
404      let event; // strip whitespace
405
406      line = line.trim();
407
408      if (line.length === 0) {
409        // ignore empty lines
410        return;
411      } // URIs
412
413
414      if (line[0] !== '#') {
415        this.trigger('data', {
416          type: 'uri',
417          uri: line
418        });
419        return;
420      } // map tags
421
422
423      const newLines = this.tagMappers.reduce((acc, mapper) => {
424        const mappedLine = mapper(line); // skip if unchanged
425
426        if (mappedLine === line) {
427          return acc;
428        }
429
430        return acc.concat([mappedLine]);
431      }, [line]);
432      newLines.forEach(newLine => {
433        for (let i = 0; i < this.customParsers.length; i++) {
434          if (this.customParsers[i].call(this, newLine)) {
435            return;
436          }
437        } // Comments
438
439
440        if (newLine.indexOf('#EXT') !== 0) {
441          this.trigger('data', {
442            type: 'comment',
443            text: newLine.slice(1)
444          });
445          return;
446        } // strip off any carriage returns here so the regex matching
447        // doesn't have to account for them.
448
449
450        newLine = newLine.replace('\r', ''); // Tags
451
452        match = /^#EXTM3U/.exec(newLine);
453
454        if (match) {
455          this.trigger('data', {
456            type: 'tag',
457            tagType: 'm3u'
458          });
459          return;
460        }
461
462        match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine);
463
464        if (match) {
465          event = {
466            type: 'tag',
467            tagType: 'inf'
468          };
469
470          if (match[1]) {
471            event.duration = parseFloat(match[1]);
472          }
473
474          if (match[2]) {
475            event.title = match[2];
476          }
477
478          this.trigger('data', event);
479          return;
480        }
481
482        match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine);
483
484        if (match) {
485          event = {
486            type: 'tag',
487            tagType: 'targetduration'
488          };
489
490          if (match[1]) {
491            event.duration = parseInt(match[1], 10);
492          }
493
494          this.trigger('data', event);
495          return;
496        }
497
498        match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine);
499
500        if (match) {
501          event = {
502            type: 'tag',
503            tagType: 'version'
504          };
505
506          if (match[1]) {
507            event.version = parseInt(match[1], 10);
508          }
509
510          this.trigger('data', event);
511          return;
512        }
513
514        match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
515
516        if (match) {
517          event = {
518            type: 'tag',
519            tagType: 'media-sequence'
520          };
521
522          if (match[1]) {
523            event.number = parseInt(match[1], 10);
524          }
525
526          this.trigger('data', event);
527          return;
528        }
529
530        match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
531
532        if (match) {
533          event = {
534            type: 'tag',
535            tagType: 'discontinuity-sequence'
536          };
537
538          if (match[1]) {
539            event.number = parseInt(match[1], 10);
540          }
541
542          this.trigger('data', event);
543          return;
544        }
545
546        match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine);
547
548        if (match) {
549          event = {
550            type: 'tag',
551            tagType: 'playlist-type'
552          };
553
554          if (match[1]) {
555            event.playlistType = match[1];
556          }
557
558          this.trigger('data', event);
559          return;
560        }
561
562        match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine);
563
564        if (match) {
565          event = _extends(parseByterange(match[1]), {
566            type: 'tag',
567            tagType: 'byterange'
568          });
569          this.trigger('data', event);
570          return;
571        }
572
573        match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine);
574
575        if (match) {
576          event = {
577            type: 'tag',
578            tagType: 'allow-cache'
579          };
580
581          if (match[1]) {
582            event.allowed = !/NO/.test(match[1]);
583          }
584
585          this.trigger('data', event);
586          return;
587        }
588
589        match = /^#EXT-X-MAP:(.*)$/.exec(newLine);
590
591        if (match) {
592          event = {
593            type: 'tag',
594            tagType: 'map'
595          };
596
597          if (match[1]) {
598            const attributes = parseAttributes$1(match[1]);
599
600            if (attributes.URI) {
601              event.uri = attributes.URI;
602            }
603
604            if (attributes.BYTERANGE) {
605              event.byterange = parseByterange(attributes.BYTERANGE);
606            }
607          }
608
609          this.trigger('data', event);
610          return;
611        }
612
613        match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine);
614
615        if (match) {
616          event = {
617            type: 'tag',
618            tagType: 'stream-inf'
619          };
620
621          if (match[1]) {
622            event.attributes = parseAttributes$1(match[1]);
623
624            if (event.attributes.RESOLUTION) {
625              event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION);
626            }
627
628            if (event.attributes.BANDWIDTH) {
629              event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
630            }
631
632            if (event.attributes['FRAME-RATE']) {
633              event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']);
634            }
635
636            if (event.attributes['PROGRAM-ID']) {
637              event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
638            }
639          }
640
641          this.trigger('data', event);
642          return;
643        }
644
645        match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine);
646
647        if (match) {
648          event = {
649            type: 'tag',
650            tagType: 'media'
651          };
652
653          if (match[1]) {
654            event.attributes = parseAttributes$1(match[1]);
655          }
656
657          this.trigger('data', event);
658          return;
659        }
660
661        match = /^#EXT-X-ENDLIST/.exec(newLine);
662
663        if (match) {
664          this.trigger('data', {
665            type: 'tag',
666            tagType: 'endlist'
667          });
668          return;
669        }
670
671        match = /^#EXT-X-DISCONTINUITY/.exec(newLine);
672
673        if (match) {
674          this.trigger('data', {
675            type: 'tag',
676            tagType: 'discontinuity'
677          });
678          return;
679        }
680
681        match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine);
682
683        if (match) {
684          event = {
685            type: 'tag',
686            tagType: 'program-date-time'
687          };
688
689          if (match[1]) {
690            event.dateTimeString = match[1];
691            event.dateTimeObject = new Date(match[1]);
692          }
693
694          this.trigger('data', event);
695          return;
696        }
697
698        match = /^#EXT-X-KEY:(.*)$/.exec(newLine);
699
700        if (match) {
701          event = {
702            type: 'tag',
703            tagType: 'key'
704          };
705
706          if (match[1]) {
707            event.attributes = parseAttributes$1(match[1]); // parse the IV string into a Uint32Array
708
709            if (event.attributes.IV) {
710              if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
711                event.attributes.IV = event.attributes.IV.substring(2);
712              }
713
714              event.attributes.IV = event.attributes.IV.match(/.{8}/g);
715              event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
716              event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
717              event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
718              event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
719              event.attributes.IV = new Uint32Array(event.attributes.IV);
720            }
721          }
722
723          this.trigger('data', event);
724          return;
725        }
726
727        match = /^#EXT-X-START:(.*)$/.exec(newLine);
728
729        if (match) {
730          event = {
731            type: 'tag',
732            tagType: 'start'
733          };
734
735          if (match[1]) {
736            event.attributes = parseAttributes$1(match[1]);
737            event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']);
738            event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE);
739          }
740
741          this.trigger('data', event);
742          return;
743        }
744
745        match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine);
746
747        if (match) {
748          event = {
749            type: 'tag',
750            tagType: 'cue-out-cont'
751          };
752
753          if (match[1]) {
754            event.data = match[1];
755          } else {
756            event.data = '';
757          }
758
759          this.trigger('data', event);
760          return;
761        }
762
763        match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine);
764
765        if (match) {
766          event = {
767            type: 'tag',
768            tagType: 'cue-out'
769          };
770
771          if (match[1]) {
772            event.data = match[1];
773          } else {
774            event.data = '';
775          }
776
777          this.trigger('data', event);
778          return;
779        }
780
781        match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(newLine);
782
783        if (match) {
784          event = {
785            type: 'tag',
786            tagType: 'cue-in'
787          };
788
789          if (match[1]) {
790            event.data = match[1];
791          } else {
792            event.data = '';
793          }
794
795          this.trigger('data', event);
796          return;
797        }
798
799        match = /^#EXT-X-SKIP:(.*)$/.exec(newLine);
800
801        if (match && match[1]) {
802          event = {
803            type: 'tag',
804            tagType: 'skip'
805          };
806          event.attributes = parseAttributes$1(match[1]);
807
808          if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) {
809            event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10);
810          }
811
812          if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) {
813            event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB);
814          }
815
816          this.trigger('data', event);
817          return;
818        }
819
820        match = /^#EXT-X-PART:(.*)$/.exec(newLine);
821
822        if (match && match[1]) {
823          event = {
824            type: 'tag',
825            tagType: 'part'
826          };
827          event.attributes = parseAttributes$1(match[1]);
828          ['DURATION'].forEach(function (key) {
829            if (event.attributes.hasOwnProperty(key)) {
830              event.attributes[key] = parseFloat(event.attributes[key]);
831            }
832          });
833          ['INDEPENDENT', 'GAP'].forEach(function (key) {
834            if (event.attributes.hasOwnProperty(key)) {
835              event.attributes[key] = /YES/.test(event.attributes[key]);
836            }
837          });
838
839          if (event.attributes.hasOwnProperty('BYTERANGE')) {
840            event.attributes.byterange = parseByterange(event.attributes.BYTERANGE);
841          }
842
843          this.trigger('data', event);
844          return;
845        }
846
847        match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine);
848
849        if (match && match[1]) {
850          event = {
851            type: 'tag',
852            tagType: 'server-control'
853          };
854          event.attributes = parseAttributes$1(match[1]);
855          ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) {
856            if (event.attributes.hasOwnProperty(key)) {
857              event.attributes[key] = parseFloat(event.attributes[key]);
858            }
859          });
860          ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) {
861            if (event.attributes.hasOwnProperty(key)) {
862              event.attributes[key] = /YES/.test(event.attributes[key]);
863            }
864          });
865          this.trigger('data', event);
866          return;
867        }
868
869        match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine);
870
871        if (match && match[1]) {
872          event = {
873            type: 'tag',
874            tagType: 'part-inf'
875          };
876          event.attributes = parseAttributes$1(match[1]);
877          ['PART-TARGET'].forEach(function (key) {
878            if (event.attributes.hasOwnProperty(key)) {
879              event.attributes[key] = parseFloat(event.attributes[key]);
880            }
881          });
882          this.trigger('data', event);
883          return;
884        }
885
886        match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine);
887
888        if (match && match[1]) {
889          event = {
890            type: 'tag',
891            tagType: 'preload-hint'
892          };
893          event.attributes = parseAttributes$1(match[1]);
894          ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) {
895            if (event.attributes.hasOwnProperty(key)) {
896              event.attributes[key] = parseInt(event.attributes[key], 10);
897              const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset';
898              event.attributes.byterange = event.attributes.byterange || {};
899              event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object.
900
901              delete event.attributes[key];
902            }
903          });
904          this.trigger('data', event);
905          return;
906        }
907
908        match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine);
909
910        if (match && match[1]) {
911          event = {
912            type: 'tag',
913            tagType: 'rendition-report'
914          };
915          event.attributes = parseAttributes$1(match[1]);
916          ['LAST-MSN', 'LAST-PART'].forEach(function (key) {
917            if (event.attributes.hasOwnProperty(key)) {
918              event.attributes[key] = parseInt(event.attributes[key], 10);
919            }
920          });
921          this.trigger('data', event);
922          return;
923        }
924
925        match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine);
926
927        if (match && match[1]) {
928          event = {
929            type: 'tag',
930            tagType: 'daterange'
931          };
932          event.attributes = parseAttributes$1(match[1]);
933          ['ID', 'CLASS'].forEach(function (key) {
934            if (event.attributes.hasOwnProperty(key)) {
935              event.attributes[key] = String(event.attributes[key]);
936            }
937          });
938          ['START-DATE', 'END-DATE'].forEach(function (key) {
939            if (event.attributes.hasOwnProperty(key)) {
940              event.attributes[key] = new Date(event.attributes[key]);
941            }
942          });
943          ['DURATION', 'PLANNED-DURATION'].forEach(function (key) {
944            if (event.attributes.hasOwnProperty(key)) {
945              event.attributes[key] = parseFloat(event.attributes[key]);
946            }
947          });
948          ['END-ON-NEXT'].forEach(function (key) {
949            if (event.attributes.hasOwnProperty(key)) {
950              event.attributes[key] = /YES/i.test(event.attributes[key]);
951            }
952          });
953          ['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) {
954            if (event.attributes.hasOwnProperty(key)) {
955              event.attributes[key] = event.attributes[key].toString(16);
956            }
957          });
958          const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/;
959
960          for (const key in event.attributes) {
961            if (!clientAttributePattern.test(key)) {
962              continue;
963            }
964
965            const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]);
966            const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]);
967            event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]);
968          }
969
970          this.trigger('data', event);
971          return;
972        }
973
974        match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine);
975
976        if (match) {
977          this.trigger('data', {
978            type: 'tag',
979            tagType: 'independent-segments'
980          });
981          return;
982        }
983
984        match = /^#EXT-X-I-FRAMES-ONLY/.exec(newLine);
985
986        if (match) {
987          this.trigger('data', {
988            type: 'tag',
989            tagType: 'i-frames-only'
990          });
991          return;
992        }
993
994        match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine);
995
996        if (match) {
997          event = {
998            type: 'tag',
999            tagType: 'content-steering'
1000          };
1001          event.attributes = parseAttributes$1(match[1]);
1002          this.trigger('data', event);
1003          return;
1004        }
1005
1006        match = /^#EXT-X-I-FRAME-STREAM-INF:(.*)$/.exec(newLine);
1007
1008        if (match) {
1009          event = {
1010            type: 'tag',
1011            tagType: 'i-frame-playlist'
1012          };
1013          event.attributes = parseAttributes$1(match[1]);
1014
1015          if (event.attributes.URI) {
1016            event.uri = event.attributes.URI;
1017          }
1018
1019          if (event.attributes.BANDWIDTH) {
1020            event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
1021          }
1022
1023          if (event.attributes.RESOLUTION) {
1024            event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION);
1025          }
1026
1027          if (event.attributes['AVERAGE-BANDWIDTH']) {
1028            event.attributes['AVERAGE-BANDWIDTH'] = parseInt(event.attributes['AVERAGE-BANDWIDTH'], 10);
1029          }
1030
1031          if (event.attributes['FRAME-RATE']) {
1032            event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']);
1033          }
1034
1035          this.trigger('data', event);
1036          return;
1037        }
1038
1039        match = /^#EXT-X-DEFINE:(.*)$/.exec(newLine);
1040
1041        if (match) {
1042          event = {
1043            type: 'tag',
1044            tagType: 'define'
1045          };
1046          event.attributes = parseAttributes$1(match[1]);
1047          this.trigger('data', event);
1048          return;
1049        } // unknown tag type
1050
1051
1052        this.trigger('data', {
1053          type: 'tag',
1054          data: newLine.slice(4)
1055        });
1056      });
1057    }
1058    /**
1059     * Add a parser for custom headers
1060     *
1061     * @param {Object}   options              a map of options for the added parser
1062     * @param {RegExp}   options.expression   a regular expression to match the custom header
1063     * @param {string}   options.customType   the custom type to register to the output
1064     * @param {Function} [options.dataParser] function to parse the line into an object
1065     * @param {boolean}  [options.segment]    should tag data be attached to the segment object
1066     */
1067
1068
1069    addParser({
1070      expression,
1071      customType,
1072      dataParser,
1073      segment
1074    }) {
1075      if (typeof dataParser !== 'function') {
1076        dataParser = line => line;
1077      }
1078
1079      this.customParsers.push(line => {
1080        const match = expression.exec(line);
1081
1082        if (match) {
1083          this.trigger('data', {
1084            type: 'custom',
1085            data: dataParser(line),
1086            customType,
1087            segment
1088          });
1089          return true;
1090        }
1091      });
1092    }
1093    /**
1094     * Add a custom header mapper
1095     *
1096     * @param {Object}   options
1097     * @param {RegExp}   options.expression   a regular expression to match the custom header
1098     * @param {Function} options.map          function to translate tag into a different tag
1099     */
1100
1101
1102    addTagMapper({
1103      expression,
1104      map
1105    }) {
1106      const mapFn = line => {
1107        if (expression.test(line)) {
1108          return map(line);
1109        }
1110
1111        return line;
1112      };
1113
1114      this.tagMappers.push(mapFn);
1115    }
1116
1117  }
1118
1119  const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase());
1120
1121  const camelCaseKeys = function (attributes) {
1122    const result = {};
1123    Object.keys(attributes).forEach(function (key) {
1124      result[camelCase(key)] = attributes[key];
1125    });
1126    return result;
1127  }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration
1128  // we need this helper because defaults are based upon targetDuration and
1129  // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before
1130  // target durations are set.
1131
1132
1133  const setHoldBack = function (manifest) {
1134    const {
1135      serverControl,
1136      targetDuration,
1137      partTargetDuration
1138    } = manifest;
1139
1140    if (!serverControl) {
1141      return;
1142    }
1143
1144    const tag = '#EXT-X-SERVER-CONTROL';
1145    const hb = 'holdBack';
1146    const phb = 'partHoldBack';
1147    const minTargetDuration = targetDuration && targetDuration * 3;
1148    const minPartDuration = partTargetDuration && partTargetDuration * 2;
1149
1150    if (targetDuration && !serverControl.hasOwnProperty(hb)) {
1151      serverControl[hb] = minTargetDuration;
1152      this.trigger('info', {
1153        message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).`
1154      });
1155    }
1156
1157    if (minTargetDuration && serverControl[hb] < minTargetDuration) {
1158      this.trigger('warn', {
1159        message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})`
1160      });
1161      serverControl[hb] = minTargetDuration;
1162    } // default no part hold back to part target duration * 3
1163
1164
1165    if (partTargetDuration && !serverControl.hasOwnProperty(phb)) {
1166      serverControl[phb] = partTargetDuration * 3;
1167      this.trigger('info', {
1168        message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).`
1169      });
1170    } // if part hold back is too small default it to part target duration * 2
1171
1172
1173    if (partTargetDuration && serverControl[phb] < minPartDuration) {
1174      this.trigger('warn', {
1175        message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).`
1176      });
1177      serverControl[phb] = minPartDuration;
1178    }
1179  };
1180  /**
1181   * A parser for M3U8 files. The current interpretation of the input is
1182   * exposed as a property `manifest` on parser objects. It's just two lines to
1183   * create and parse a manifest once you have the contents available as a string:
1184   *
1185   * ```js
1186   * var parser = new m3u8.Parser();
1187   * parser.push(xhr.responseText);
1188   * ```
1189   *
1190   * New input can later be applied to update the manifest object by calling
1191   * `push` again.
1192   *
1193   * The parser attempts to create a usable manifest object even if the
1194   * underlying input is somewhat nonsensical. It emits `info` and `warning`
1195   * events during the parse if it encounters input that seems invalid or
1196   * requires some property of the manifest object to be defaulted.
1197   *
1198   * @class Parser
1199   * @param {Object} [opts] Options for the constructor, needed for substitutions
1200   * @param {string} [opts.uri] URL to check for query params
1201   * @param {Object} [opts.mainDefinitions] Definitions on main playlist that can be imported
1202   * @extends Stream
1203   */
1204
1205
1206  class Parser extends Stream {
1207    constructor(opts = {}) {
1208      super();
1209      this.lineStream = new LineStream();
1210      this.parseStream = new ParseStream();
1211      this.lineStream.pipe(this.parseStream);
1212      this.mainDefinitions = opts.mainDefinitions || {};
1213      this.params = new URL(opts.uri, 'https://a.com').searchParams;
1214      this.lastProgramDateTime = null;
1215      /* eslint-disable consistent-this */
1216
1217      const self = this;
1218      /* eslint-enable consistent-this */
1219
1220      const uris = [];
1221      let currentUri = {}; // if specified, the active EXT-X-MAP definition
1222
1223      let currentMap; // if specified, the active decryption key
1224
1225      let key;
1226      let hasParts = false;
1227
1228      const noop = function () {};
1229
1230      const defaultMediaGroups = {
1231        'AUDIO': {},
1232        'VIDEO': {},
1233        'CLOSED-CAPTIONS': {},
1234        'SUBTITLES': {}
1235      }; // This is the Widevine UUID from DASH IF IOP. The same exact string is
1236      // used in MPDs with Widevine encrypted streams.
1237
1238      const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities
1239
1240      let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data
1241
1242      this.manifest = {
1243        allowCache: true,
1244        discontinuityStarts: [],
1245        dateRanges: [],
1246        iFramePlaylists: [],
1247        segments: []
1248      }; // keep track of the last seen segment's byte range end, as segments are not required
1249      // to provide the offset, in which case it defaults to the next byte after the
1250      // previous segment
1251
1252      let lastByterangeEnd = 0; // keep track of the last seen part's byte range end.
1253
1254      let lastPartByterangeEnd = 0;
1255      const dateRangeTags = {};
1256      this.on('end', () => {
1257        // only add preloadSegment if we don't yet have a uri for it.
1258        // and we actually have parts/preloadHints
1259        if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) {
1260          return;
1261        }
1262
1263        if (!currentUri.map && currentMap) {
1264          currentUri.map = currentMap;
1265        }
1266
1267        if (!currentUri.key && key) {
1268          currentUri.key = key;
1269        }
1270
1271        if (!currentUri.timeline && typeof currentTimeline === 'number') {
1272          currentUri.timeline = currentTimeline;
1273        }
1274
1275        this.manifest.preloadSegment = currentUri;
1276      }); // update the manifest with the m3u8 entry from the parse stream
1277
1278      this.parseStream.on('data', function (entry) {
1279        let mediaGroup;
1280        let rendition; // Replace variables in uris and attributes as defined in #EXT-X-DEFINE tags
1281
1282        if (self.manifest.definitions) {
1283          for (const def in self.manifest.definitions) {
1284            if (entry.uri) {
1285              entry.uri = entry.uri.replace(`{$${def}}`, self.manifest.definitions[def]);
1286            }
1287
1288            if (entry.attributes) {
1289              for (const attr in entry.attributes) {
1290                if (typeof entry.attributes[attr] === 'string') {
1291                  entry.attributes[attr] = entry.attributes[attr].replace(`{$${def}}`, self.manifest.definitions[def]);
1292                }
1293              }
1294            }
1295          }
1296        }
1297
1298        ({
1299          tag() {
1300            // switch based on the tag type
1301            (({
1302              version() {
1303                if (entry.version) {
1304                  this.manifest.version = entry.version;
1305                }
1306              },
1307
1308              'allow-cache'() {
1309                this.manifest.allowCache = entry.allowed;
1310
1311                if (!('allowed' in entry)) {
1312                  this.trigger('info', {
1313                    message: 'defaulting allowCache to YES'
1314                  });
1315                  this.manifest.allowCache = true;
1316                }
1317              },
1318
1319              byterange() {
1320                const byterange = {};
1321
1322                if ('length' in entry) {
1323                  currentUri.byterange = byterange;
1324                  byterange.length = entry.length;
1325
1326                  if (!('offset' in entry)) {
1327                    /*
1328                     * From the latest spec (as of this writing):
1329                     * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2
1330                     *
1331                     * Same text since EXT-X-BYTERANGE's introduction in draft 7:
1332                     * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1)
1333                     *
1334                     * "If o [offset] is not present, the sub-range begins at the next byte
1335                     * following the sub-range of the previous media segment."
1336                     */
1337                    entry.offset = lastByterangeEnd;
1338                  }
1339                }
1340
1341                if ('offset' in entry) {
1342                  currentUri.byterange = byterange;
1343                  byterange.offset = entry.offset;
1344                }
1345
1346                lastByterangeEnd = byterange.offset + byterange.length;
1347              },
1348
1349              endlist() {
1350                this.manifest.endList = true;
1351              },
1352
1353              inf() {
1354                if (!('mediaSequence' in this.manifest)) {
1355                  this.manifest.mediaSequence = 0;
1356                  this.trigger('info', {
1357                    message: 'defaulting media sequence to zero'
1358                  });
1359                }
1360
1361                if (!('discontinuitySequence' in this.manifest)) {
1362                  this.manifest.discontinuitySequence = 0;
1363                  this.trigger('info', {
1364                    message: 'defaulting discontinuity sequence to zero'
1365                  });
1366                }
1367
1368                if (entry.title) {
1369                  currentUri.title = entry.title;
1370                }
1371
1372                if (entry.duration > 0) {
1373                  currentUri.duration = entry.duration;
1374                }
1375
1376                if (entry.duration === 0) {
1377                  currentUri.duration = 0.01;
1378                  this.trigger('info', {
1379                    message: 'updating zero segment duration to a small value'
1380                  });
1381                }
1382
1383                this.manifest.segments = uris;
1384              },
1385
1386              key() {
1387                if (!entry.attributes) {
1388                  this.trigger('warn', {
1389                    message: 'ignoring key declaration without attribute list'
1390                  });
1391                  return;
1392                } // clear the active encryption key
1393
1394
1395                if (entry.attributes.METHOD === 'NONE') {
1396                  key = null;
1397                  return;
1398                }
1399
1400                if (!entry.attributes.URI) {
1401                  this.trigger('warn', {
1402                    message: 'ignoring key declaration without URI'
1403                  });
1404                  return;
1405                }
1406
1407                if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') {
1408                  this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
1409
1410                  this.manifest.contentProtection['com.apple.fps.1_0'] = {
1411                    attributes: entry.attributes
1412                  };
1413                  return;
1414                }
1415
1416                if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') {
1417                  this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
1418
1419                  this.manifest.contentProtection['com.microsoft.playready'] = {
1420                    uri: entry.attributes.URI
1421                  };
1422                  return;
1423                } // check if the content is encrypted for Widevine
1424                // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
1425
1426
1427                if (entry.attributes.KEYFORMAT === widevineUuid) {
1428                  const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];
1429
1430                  if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) {
1431                    this.trigger('warn', {
1432                      message: 'invalid key method provided for Widevine'
1433                    });
1434                    return;
1435                  }
1436
1437                  if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') {
1438                    this.trigger('warn', {
1439                      message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead'
1440                    });
1441                  }
1442
1443                  if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') {
1444                    this.trigger('warn', {
1445                      message: 'invalid key URI provided for Widevine'
1446                    });
1447                    return;
1448                  }
1449
1450                  if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) {
1451                    this.trigger('warn', {
1452                      message: 'invalid key ID provided for Widevine'
1453                    });
1454                    return;
1455                  } // if Widevine key attributes are valid, store them as `contentProtection`
1456                  // on the manifest to emulate Widevine tag structure in a DASH mpd
1457
1458
1459                  this.manifest.contentProtection = this.manifest.contentProtection || {};
1460                  this.manifest.contentProtection['com.widevine.alpha'] = {
1461                    attributes: {
1462                      schemeIdUri: entry.attributes.KEYFORMAT,
1463                      // remove '0x' from the key id string
1464                      keyId: entry.attributes.KEYID.substring(2)
1465                    },
1466                    // decode the base64-encoded PSSH box
1467                    pssh: decodeB64ToUint8Array(entry.attributes.URI.split(',')[1])
1468                  };
1469                  return;
1470                }
1471
1472                if (!entry.attributes.METHOD) {
1473                  this.trigger('warn', {
1474                    message: 'defaulting key method to AES-128'
1475                  });
1476                } // setup an encryption key for upcoming segments
1477
1478
1479                key = {
1480                  method: entry.attributes.METHOD || 'AES-128',
1481                  uri: entry.attributes.URI
1482                };
1483
1484                if (typeof entry.attributes.IV !== 'undefined') {
1485                  key.iv = entry.attributes.IV;
1486                }
1487              },
1488
1489              'media-sequence'() {
1490                if (!isFinite(entry.number)) {
1491                  this.trigger('warn', {
1492                    message: 'ignoring invalid media sequence: ' + entry.number
1493                  });
1494                  return;
1495                }
1496
1497                this.manifest.mediaSequence = entry.number;
1498              },
1499
1500              'discontinuity-sequence'() {
1501                if (!isFinite(entry.number)) {
1502                  this.trigger('warn', {
1503                    message: 'ignoring invalid discontinuity sequence: ' + entry.number
1504                  });
1505                  return;
1506                }
1507
1508                this.manifest.discontinuitySequence = entry.number;
1509                currentTimeline = entry.number;
1510              },
1511
1512              'playlist-type'() {
1513                if (!/VOD|EVENT/.test(entry.playlistType)) {
1514                  this.trigger('warn', {
1515                    message: 'ignoring unknown playlist type: ' + entry.playlist
1516                  });
1517                  return;
1518                }
1519
1520                this.manifest.playlistType = entry.playlistType;
1521              },
1522
1523              map() {
1524                currentMap = {};
1525
1526                if (entry.uri) {
1527                  currentMap.uri = entry.uri;
1528                }
1529
1530                if (entry.byterange) {
1531                  currentMap.byterange = entry.byterange;
1532                }
1533
1534                if (key) {
1535                  currentMap.key = key;
1536                }
1537              },
1538
1539              'stream-inf'() {
1540                this.manifest.playlists = uris;
1541                this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
1542
1543                if (!entry.attributes) {
1544                  this.trigger('warn', {
1545                    message: 'ignoring empty stream-inf attributes'
1546                  });
1547                  return;
1548                }
1549
1550                if (!currentUri.attributes) {
1551                  currentUri.attributes = {};
1552                }
1553
1554                _extends(currentUri.attributes, entry.attributes);
1555              },
1556
1557              media() {
1558                this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
1559
1560                if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
1561                  this.trigger('warn', {
1562                    message: 'ignoring incomplete or missing media group'
1563                  });
1564                  return;
1565                } // find the media group, creating defaults as necessary
1566
1567
1568                const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
1569                mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
1570                mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata
1571
1572                rendition = {
1573                  default: /yes/i.test(entry.attributes.DEFAULT)
1574                };
1575
1576                if (rendition.default) {
1577                  rendition.autoselect = true;
1578                } else {
1579                  rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
1580                }
1581
1582                if (entry.attributes.LANGUAGE) {
1583                  rendition.language = entry.attributes.LANGUAGE;
1584                }
1585
1586                if (entry.attributes.URI) {
1587                  rendition.uri = entry.attributes.URI;
1588                }
1589
1590                if (entry.attributes['INSTREAM-ID']) {
1591                  rendition.instreamId = entry.attributes['INSTREAM-ID'];
1592                }
1593
1594                if (entry.attributes.CHARACTERISTICS) {
1595                  rendition.characteristics = entry.attributes.CHARACTERISTICS;
1596                }
1597
1598                if (entry.attributes.FORCED) {
1599                  rendition.forced = /yes/i.test(entry.attributes.FORCED);
1600                } // insert the new rendition
1601
1602
1603                mediaGroup[entry.attributes.NAME] = rendition;
1604              },
1605
1606              discontinuity() {
1607                currentTimeline += 1;
1608                currentUri.discontinuity = true;
1609                this.manifest.discontinuityStarts.push(uris.length);
1610              },
1611
1612              'program-date-time'() {
1613                if (typeof this.manifest.dateTimeString === 'undefined') {
1614                  // PROGRAM-DATE-TIME is a media-segment tag, but for backwards
1615                  // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag
1616                  // to the manifest object
1617                  // TODO: Consider removing this in future major version
1618                  this.manifest.dateTimeString = entry.dateTimeString;
1619                  this.manifest.dateTimeObject = entry.dateTimeObject;
1620                }
1621
1622                currentUri.dateTimeString = entry.dateTimeString;
1623                currentUri.dateTimeObject = entry.dateTimeObject;
1624                const {
1625                  lastProgramDateTime
1626                } = this;
1627                this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence.
1628                // Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference.
1629
1630                if (lastProgramDateTime === null) {
1631                  // Extrapolate Program Date Time backward
1632                  // Since it is first program date time occurrence we're assuming that
1633                  // all this.manifest.segments have no program date time info
1634                  this.manifest.segments.reduceRight((programDateTime, segment) => {
1635                    segment.programDateTime = programDateTime - segment.duration * 1000;
1636                    return segment.programDateTime;
1637                  }, this.lastProgramDateTime);
1638                }
1639              },
1640
1641              targetduration() {
1642                if (!isFinite(entry.duration) || entry.duration < 0) {
1643                  this.trigger('warn', {
1644                    message: 'ignoring invalid target duration: ' + entry.duration
1645                  });
1646                  return;
1647                }
1648
1649                this.manifest.targetDuration = entry.duration;
1650                setHoldBack.call(this, this.manifest);
1651              },
1652
1653              start() {
1654                if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) {
1655                  this.trigger('warn', {
1656                    message: 'ignoring start declaration without appropriate attribute list'
1657                  });
1658                  return;
1659                }
1660
1661                this.manifest.start = {
1662                  timeOffset: entry.attributes['TIME-OFFSET'],
1663                  precise: entry.attributes.PRECISE
1664                };
1665              },
1666
1667              'cue-out'() {
1668                currentUri.cueOut = entry.data;
1669              },
1670
1671              'cue-out-cont'() {
1672                currentUri.cueOutCont = entry.data;
1673              },
1674
1675              'cue-in'() {
1676                currentUri.cueIn = entry.data;
1677              },
1678
1679              'skip'() {
1680                this.manifest.skip = camelCaseKeys(entry.attributes);
1681                this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']);
1682              },
1683
1684              'part'() {
1685                hasParts = true; // parts are always specifed before a segment
1686
1687                const segmentIndex = this.manifest.segments.length;
1688                const part = camelCaseKeys(entry.attributes);
1689                currentUri.parts = currentUri.parts || [];
1690                currentUri.parts.push(part);
1691
1692                if (part.byterange) {
1693                  if (!part.byterange.hasOwnProperty('offset')) {
1694                    part.byterange.offset = lastPartByterangeEnd;
1695                  }
1696
1697                  lastPartByterangeEnd = part.byterange.offset + part.byterange.length;
1698                }
1699
1700                const partIndex = currentUri.parts.length - 1;
1701                this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']);
1702
1703                if (this.manifest.renditionReports) {
1704                  this.manifest.renditionReports.forEach((r, i) => {
1705                    if (!r.hasOwnProperty('lastPart')) {
1706                      this.trigger('warn', {
1707                        message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART`
1708                      });
1709                    }
1710                  });
1711                }
1712              },
1713
1714              'server-control'() {
1715                const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes);
1716
1717                if (!attrs.hasOwnProperty('canBlockReload')) {
1718                  attrs.canBlockReload = false;
1719                  this.trigger('info', {
1720                    message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false'
1721                  });
1722                }
1723
1724                setHoldBack.call(this, this.manifest);
1725
1726                if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) {
1727                  this.trigger('warn', {
1728                    message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set'
1729                  });
1730                }
1731              },
1732
1733              'preload-hint'() {
1734                // parts are always specifed before a segment
1735                const segmentIndex = this.manifest.segments.length;
1736                const hint = camelCaseKeys(entry.attributes);
1737                const isPart = hint.type && hint.type === 'PART';
1738                currentUri.preloadHints = currentUri.preloadHints || [];
1739                currentUri.preloadHints.push(hint);
1740
1741                if (hint.byterange) {
1742                  if (!hint.byterange.hasOwnProperty('offset')) {
1743                    // use last part byterange end or zero if not a part.
1744                    hint.byterange.offset = isPart ? lastPartByterangeEnd : 0;
1745
1746                    if (isPart) {
1747                      lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length;
1748                    }
1749                  }
1750                }
1751
1752                const index = currentUri.preloadHints.length - 1;
1753                this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']);
1754
1755                if (!hint.type) {
1756                  return;
1757                } // search through all preload hints except for the current one for
1758                // a duplicate type.
1759
1760
1761                for (let i = 0; i < currentUri.preloadHints.length - 1; i++) {
1762                  const otherHint = currentUri.preloadHints[i];
1763
1764                  if (!otherHint.type) {
1765                    continue;
1766                  }
1767
1768                  if (otherHint.type === hint.type) {
1769                    this.trigger('warn', {
1770                      message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}`
1771                    });
1772                  }
1773                }
1774              },
1775
1776              'rendition-report'() {
1777                const report = camelCaseKeys(entry.attributes);
1778                this.manifest.renditionReports = this.manifest.renditionReports || [];
1779                this.manifest.renditionReports.push(report);
1780                const index = this.manifest.renditionReports.length - 1;
1781                const required = ['LAST-MSN', 'URI'];
1782
1783                if (hasParts) {
1784                  required.push('LAST-PART');
1785                }
1786
1787                this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required);
1788              },
1789
1790              'part-inf'() {
1791                this.manifest.partInf = camelCaseKeys(entry.attributes);
1792                this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']);
1793
1794                if (this.manifest.partInf.partTarget) {
1795                  this.manifest.partTargetDuration = this.manifest.partInf.partTarget;
1796                }
1797
1798                setHoldBack.call(this, this.manifest);
1799              },
1800
1801              'daterange'() {
1802                this.manifest.dateRanges.push(camelCaseKeys(entry.attributes));
1803                const index = this.manifest.dateRanges.length - 1;
1804                this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']);
1805                const dateRange = this.manifest.dateRanges[index];
1806
1807                if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) {
1808                  this.trigger('warn', {
1809                    message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
1810                  });
1811                }
1812
1813                if (dateRange.duration && dateRange.duration < 0) {
1814                  this.trigger('warn', {
1815                    message: 'EXT-X-DATERANGE DURATION must not be negative'
1816                  });
1817                }
1818
1819                if (dateRange.plannedDuration && dateRange.plannedDuration < 0) {
1820                  this.trigger('warn', {
1821                    message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
1822                  });
1823                }
1824
1825                const endOnNextYes = !!dateRange.endOnNext;
1826
1827                if (endOnNextYes && !dateRange.class) {
1828                  this.trigger('warn', {
1829                    message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
1830                  });
1831                }
1832
1833                if (endOnNextYes && (dateRange.duration || dateRange.endDate)) {
1834                  this.trigger('warn', {
1835                    message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
1836                  });
1837                }
1838
1839                if (dateRange.duration && dateRange.endDate) {
1840                  const startDate = dateRange.startDate;
1841                  const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000;
1842                  this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds);
1843                }
1844
1845                if (!dateRangeTags[dateRange.id]) {
1846                  dateRangeTags[dateRange.id] = dateRange;
1847                } else {
1848                  for (const attribute in dateRangeTags[dateRange.id]) {
1849                    if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) {
1850                      this.trigger('warn', {
1851                        message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
1852                      });
1853                      break;
1854                    }
1855                  } // if tags with the same ID do not have conflicting attributes, merge them
1856
1857
1858                  const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id);
1859                  this.manifest.dateRanges[dateRangeWithSameId] = _extends(this.manifest.dateRanges[dateRangeWithSameId], dateRange);
1860                  dateRangeTags[dateRange.id] = _extends(dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last
1861
1862                  this.manifest.dateRanges.pop();
1863                }
1864              },
1865
1866              'independent-segments'() {
1867                this.manifest.independentSegments = true;
1868              },
1869
1870              'i-frames-only'() {
1871                this.manifest.iFramesOnly = true;
1872                this.requiredCompatibilityversion(this.manifest.version, 4);
1873              },
1874
1875              'content-steering'() {
1876                this.manifest.contentSteering = camelCaseKeys(entry.attributes);
1877                this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']);
1878              },
1879
1880              /** @this {Parser} */
1881              define() {
1882                this.manifest.definitions = this.manifest.definitions || {};
1883
1884                const addDef = (n, v) => {
1885                  if (n in this.manifest.definitions) {
1886                    // An EXT-X-DEFINE tag MUST NOT specify the same Variable Name as any other
1887                    // EXT-X-DEFINE tag in the same Playlist.  Parsers that encounter duplicate
1888                    // Variable Name declarations MUST fail to parse the Playlist.
1889                    this.trigger('error', {
1890                      message: `EXT-X-DEFINE: Duplicate name ${n}`
1891                    });
1892                    return;
1893                  }
1894
1895                  this.manifest.definitions[n] = v;
1896                };
1897
1898                if ('QUERYPARAM' in entry.attributes) {
1899                  if ('NAME' in entry.attributes || 'IMPORT' in entry.attributes) {
1900                    // An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a
1901                    // QUERYPARAM attribute, but only one of the three.  Otherwise, the
1902                    // client MUST fail to parse the Playlist.
1903                    this.trigger('error', {
1904                      message: 'EXT-X-DEFINE: Invalid attributes'
1905                    });
1906                    return;
1907                  }
1908
1909                  const val = this.params.get(entry.attributes.QUERYPARAM);
1910
1911                  if (!val) {
1912                    // If the QUERYPARAM attribute value does not match any query parameter in
1913                    // the URI or the matching parameter has no associated value, the parser
1914                    // MUST fail to parse the Playlist.  If more than one parameter matches,
1915                    // any of the associated values MAY be used.
1916                    this.trigger('error', {
1917                      message: `EXT-X-DEFINE: No query param ${entry.attributes.QUERYPARAM}`
1918                    });
1919                    return;
1920                  }
1921
1922                  addDef(entry.attributes.QUERYPARAM, decodeURIComponent(val));
1923                  return;
1924                }
1925
1926                if ('NAME' in entry.attributes) {
1927                  if ('IMPORT' in entry.attributes) {
1928                    // An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a
1929                    // QUERYPARAM attribute, but only one of the three.  Otherwise, the
1930                    // client MUST fail to parse the Playlist.
1931                    this.trigger('error', {
1932                      message: 'EXT-X-DEFINE: Invalid attributes'
1933                    });
1934                    return;
1935                  }
1936
1937                  if (!('VALUE' in entry.attributes) || typeof entry.attributes.VALUE !== 'string') {
1938                    // This attribute is REQUIRED if the EXT-X-DEFINE tag has a NAME attribute.
1939                    // The quoted-string MAY be empty.
1940                    this.trigger('error', {
1941                      message: `EXT-X-DEFINE: No value for ${entry.attributes.NAME}`
1942                    });
1943                    return;
1944                  }
1945
1946                  addDef(entry.attributes.NAME, entry.attributes.VALUE);
1947                  return;
1948                }
1949
1950                if ('IMPORT' in entry.attributes) {
1951                  if (!this.mainDefinitions[entry.attributes.IMPORT]) {
1952                    // Covers two conditions, as mainDefinitions will always be empty on main
1953                    //
1954                    // EXT-X-DEFINE tags containing the IMPORT attribute MUST NOT occur in
1955                    // Multivariant Playlists; they are only allowed in Media Playlists.
1956                    //
1957                    // If the IMPORT attribute value does not match any Variable Name in the
1958                    // Multivariant Playlist, or if the Media Playlist loaded from a
1959                    // Multivariant Playlist, the parser MUST fail the Playlist.
1960                    this.trigger('error', {
1961                      message: `EXT-X-DEFINE: No value ${entry.attributes.IMPORT} to import, or IMPORT used on main playlist`
1962                    });
1963                    return;
1964                  }
1965
1966                  addDef(entry.attributes.IMPORT, this.mainDefinitions[entry.attributes.IMPORT]);
1967                  return;
1968                } // An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a QUERYPARAM
1969                // attribute, but only one of the three.  Otherwise, the client MUST fail to
1970                // parse the Playlist.
1971
1972
1973                this.trigger('error', {
1974                  message: 'EXT-X-DEFINE: No attribute'
1975                });
1976              },
1977
1978              'i-frame-playlist'() {
1979                this.manifest.iFramePlaylists.push({
1980                  attributes: entry.attributes,
1981                  uri: entry.uri,
1982                  timeline: currentTimeline
1983                });
1984                this.warnOnMissingAttributes_('#EXT-X-I-FRAME-STREAM-INF', entry.attributes, ['BANDWIDTH', 'URI']);
1985              }
1986
1987            })[entry.tagType] || noop).call(self);
1988          },
1989
1990          uri() {
1991            currentUri.uri = entry.uri;
1992            uris.push(currentUri); // if no explicit duration was declared, use the target duration
1993
1994            if (this.manifest.targetDuration && !('duration' in currentUri)) {
1995              this.trigger('warn', {
1996                message: 'defaulting segment duration to the target duration'
1997              });
1998              currentUri.duration = this.manifest.targetDuration;
1999            } // annotate with encryption information, if necessary
2000
2001
2002            if (key) {
2003              currentUri.key = key;
2004            }
2005
2006            currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary
2007
2008            if (currentMap) {
2009              currentUri.map = currentMap;
2010            } // reset the last byterange end as it needs to be 0 between parts
2011
2012
2013            lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward
2014
2015            if (this.lastProgramDateTime !== null) {
2016              currentUri.programDateTime = this.lastProgramDateTime;
2017              this.lastProgramDateTime += currentUri.duration * 1000;
2018            } // prepare for the next URI
2019
2020
2021            currentUri = {};
2022          },
2023
2024          comment() {// comments are not important for playback
2025          },
2026
2027          custom() {
2028            // if this is segment-level data attach the output to the segment
2029            if (entry.segment) {
2030              currentUri.custom = currentUri.custom || {};
2031              currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object
2032            } else {
2033              this.manifest.custom = this.manifest.custom || {};
2034              this.manifest.custom[entry.customType] = entry.data;
2035            }
2036          }
2037
2038        })[entry.type].call(self);
2039      });
2040    }
2041
2042    requiredCompatibilityversion(currentVersion, targetVersion) {
2043      if (currentVersion < targetVersion || !currentVersion) {
2044        this.trigger('warn', {
2045          message: `manifest must be at least version ${targetVersion}`
2046        });
2047      }
2048    }
2049
2050    warnOnMissingAttributes_(identifier, attributes, required) {
2051      const missing = [];
2052      required.forEach(function (key) {
2053        if (!attributes.hasOwnProperty(key)) {
2054          missing.push(key);
2055        }
2056      });
2057
2058      if (missing.length) {
2059        this.trigger('warn', {
2060          message: `${identifier} lacks required attribute(s): ${missing.join(', ')}`
2061        });
2062      }
2063    }
2064    /**
2065     * Parse the input string and update the manifest object.
2066     *
2067     * @param {string} chunk a potentially incomplete portion of the manifest
2068     */
2069
2070
2071    push(chunk) {
2072      this.lineStream.push(chunk);
2073    }
2074    /**
2075     * Flush any remaining input. This can be handy if the last line of an M3U8
2076     * manifest did not contain a trailing newline but the file has been
2077     * completely received.
2078     */
2079
2080
2081    end() {
2082      // flush any buffered input
2083      this.lineStream.push('\n');
2084
2085      if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) {
2086        this.trigger('warn', {
2087          message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
2088        });
2089      }
2090
2091      this.lastProgramDateTime = null;
2092      this.trigger('end');
2093    }
2094    /**
2095     * Add an additional parser for non-standard tags
2096     *
2097     * @param {Object}   options              a map of options for the added parser
2098     * @param {RegExp}   options.expression   a regular expression to match the custom header
2099     * @param {string}   options.customType   the custom type to register to the output
2100     * @param {Function} [options.dataParser] function to parse the line into an object
2101     * @param {boolean}  [options.segment]    should tag data be attached to the segment object
2102     */
2103
2104
2105    addParser(options) {
2106      this.parseStream.addParser(options);
2107    }
2108    /**
2109     * Add a custom header mapper
2110     *
2111     * @param {Object}   options
2112     * @param {RegExp}   options.expression   a regular expression to match the custom header
2113     * @param {Function} options.map          function to translate tag into a different tag
2114     */
2115
2116
2117    addTagMapper(options) {
2118      this.parseStream.addTagMapper(options);
2119    }
2120
2121  }
2122
2123  var regexs = {
2124    // to determine mime types
2125    mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v|stpp.ttml.im1t)/,
2126    webm: /^(vp0?[89]|av0?1|opus|vorbis)/,
2127    ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/,
2128    // to determine if a codec is audio or video
2129    video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/,
2130    audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3|speex|aac)/,
2131    text: /^(stpp.ttml.im1t)/,
2132    // mux.js support regex
2133    muxerVideo: /^(avc0?1)/,
2134    muxerAudio: /^(mp4a)/,
2135    // match nothing as muxer does not support text right now.
2136    // there cannot never be a character before the start of a string
2137    // so this matches nothing.
2138    muxerText: /a^/
2139  };
2140  var mediaTypes = ['video', 'audio', 'text'];
2141  var upperMediaTypes = ['Video', 'Audio', 'Text'];
2142  /**
2143   * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard
2144   * `avc1.<hhhhhh>`
2145   *
2146   * @param {string} codec
2147   *        Codec string to translate
2148   * @return {string}
2149   *         The translated codec string
2150   */
2151
2152  var translateLegacyCodec = function translateLegacyCodec(codec) {
2153    if (!codec) {
2154      return codec;
2155    }
2156
2157    return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) {
2158      var profileHex = ('00' + Number(profile).toString(16)).slice(-2);
2159      var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2);
2160      return 'avc1.' + profileHex + '00' + avcLevelHex;
2161    });
2162  };
2163  /**
2164   * @typedef {Object} ParsedCodecInfo
2165   * @property {number} codecCount
2166   *           Number of codecs parsed
2167   * @property {string} [videoCodec]
2168   *           Parsed video codec (if found)
2169   * @property {string} [videoObjectTypeIndicator]
2170   *           Video object type indicator (if found)
2171   * @property {string|null} audioProfile
2172   *           Audio profile
2173   */
2174
2175  /**
2176   * Parses a codec string to retrieve the number of codecs specified, the video codec and
2177   * object type indicator, and the audio profile.
2178   *
2179   * @param {string} [codecString]
2180   *        The codec string to parse
2181   * @return {ParsedCodecInfo}
2182   *         Parsed codec info
2183   */
2184
2185  var parseCodecs = function parseCodecs(codecString) {
2186    if (codecString === void 0) {
2187      codecString = '';
2188    }
2189
2190    var codecs = codecString.split(',');
2191    var result = [];
2192    codecs.forEach(function (codec) {
2193      codec = codec.trim();
2194      var codecType;
vendor: 134,050 bytes, lines 2195-6357
2195      mediaTypes.forEach(function (name) {
2196        var match = regexs[name].exec(codec.toLowerCase());
2197
2198        if (!match || match.length <= 1) {
2199          return;
2200        }
2201
2202        codecType = name; // maintain codec case
2203
2204        var type = codec.substring(0, match[1].length);
2205        var details = codec.replace(type, '');
2206        result.push({
2207          type: type,
2208          details: details,
2209          mediaType: name
2210        });
2211      });
2212
2213      if (!codecType) {
2214        result.push({
2215          type: codec,
2216          details: '',
2217          mediaType: 'unknown'
2218        });
2219      }
2220    });
2221    return result;
2222  };
2223  /**
2224   * Returns a ParsedCodecInfo object for the default alternate audio playlist if there is
2225   * a default alternate audio playlist for the provided audio group.
2226   *
2227   * @param {Object} master
2228   *        The master playlist
2229   * @param {string} audioGroupId
2230   *        ID of the audio group for which to find the default codec info
2231   * @return {ParsedCodecInfo}
2232   *         Parsed codec info
2233   */
2234
2235  var codecsFromDefault = function codecsFromDefault(master, audioGroupId) {
2236    if (!master.mediaGroups.AUDIO || !audioGroupId) {
2237      return null;
2238    }
2239
2240    var audioGroup = master.mediaGroups.AUDIO[audioGroupId];
2241
2242    if (!audioGroup) {
2243      return null;
2244    }
2245
2246    for (var name in audioGroup) {
2247      var audioType = audioGroup[name];
2248
2249      if (audioType.default && audioType.playlists) {
2250        // codec should be the same for all playlists within the audio type
2251        return parseCodecs(audioType.playlists[0].attributes.CODECS);
2252      }
2253    }
2254
2255    return null;
2256  };
2257  var isAudioCodec = function isAudioCodec(codec) {
2258    if (codec === void 0) {
2259      codec = '';
2260    }
2261
2262    return regexs.audio.test(codec.trim().toLowerCase());
2263  };
2264  var isTextCodec = function isTextCodec(codec) {
2265    if (codec === void 0) {
2266      codec = '';
2267    }
2268
2269    return regexs.text.test(codec.trim().toLowerCase());
2270  };
2271  var getMimeForCodec = function getMimeForCodec(codecString) {
2272    if (!codecString || typeof codecString !== 'string') {
2273      return;
2274    }
2275
2276    var codecs = codecString.toLowerCase().split(',').map(function (c) {
2277      return translateLegacyCodec(c.trim());
2278    }); // default to video type
2279
2280    var type = 'video'; // only change to audio type if the only codec we have is
2281    // audio
2282
2283    if (codecs.length === 1 && isAudioCodec(codecs[0])) {
2284      type = 'audio';
2285    } else if (codecs.length === 1 && isTextCodec(codecs[0])) {
2286      // text uses application/<container> for now
2287      type = 'application';
2288    } // default the container to mp4
2289
2290
2291    var container = 'mp4'; // every codec must be able to go into the container
2292    // for that container to be the correct one
2293
2294    if (codecs.every(function (c) {
2295      return regexs.mp4.test(c);
2296    })) {
2297      container = 'mp4';
2298    } else if (codecs.every(function (c) {
2299      return regexs.webm.test(c);
2300    })) {
2301      container = 'webm';
2302    } else if (codecs.every(function (c) {
2303      return regexs.ogg.test(c);
2304    })) {
2305      container = 'ogg';
2306    }
2307
2308    return type + "/" + container + ";codecs=\"" + codecString + "\"";
2309  };
2310  /**
2311   * Tests whether the codec is supported by MediaSource. Optionally also tests ManagedMediaSource.
2312   *
2313   * @param {string} codecString
2314   *        Codec to test
2315   * @param {boolean} [withMMS]
2316   *        Whether to check if ManagedMediaSource supports it
2317   * @return {boolean}
2318   *          Codec is supported
2319   */
2320
2321  var browserSupportsCodec = function browserSupportsCodec(codecString, withMMS) {
2322    if (codecString === void 0) {
2323      codecString = '';
2324    }
2325
2326    if (withMMS === void 0) {
2327      withMMS = false;
2328    }
2329
2330    return window.MediaSource && window.MediaSource.isTypeSupported && window.MediaSource.isTypeSupported(getMimeForCodec(codecString)) || withMMS && window.ManagedMediaSource && window.ManagedMediaSource.isTypeSupported && window.ManagedMediaSource.isTypeSupported(getMimeForCodec(codecString)) || false;
2331  };
2332  var muxerSupportsCodec = function muxerSupportsCodec(codecString) {
2333    if (codecString === void 0) {
2334      codecString = '';
2335    }
2336
2337    return codecString.toLowerCase().split(',').every(function (codec) {
2338      codec = codec.trim(); // any match is supported.
2339
2340      for (var i = 0; i < upperMediaTypes.length; i++) {
2341        var type = upperMediaTypes[i];
2342
2343        if (regexs["muxer" + type].test(codec)) {
2344          return true;
2345        }
2346      }
2347
2348      return false;
2349    });
2350  };
2351  var DEFAULT_AUDIO_CODEC = 'mp4a.40.2';
2352  var DEFAULT_VIDEO_CODEC = 'avc1.4d400d';
2353
2354  /**
2355   * Provides a compatibility layer between Video.js 7 and 8 API changes for VHS.
2356   */
2357  /**
2358   * Delegates to videojs.obj.merge (Video.js 8) or
2359   * videojs.mergeOptions (Video.js 7).
2360   */
2361
2362  function merge$1(...args) {
2363    const context = videojs__default["default"].obj || videojs__default["default"];
2364    const fn = context.merge || context.mergeOptions;
2365    return fn.apply(context, args);
2366  }
2367  /**
2368   * Delegates to videojs.time.createTimeRanges (Video.js 8) or
2369   * videojs.createTimeRanges (Video.js 7).
2370   */
2371
2372  function createTimeRanges(...args) {
2373    const context = videojs__default["default"].time || videojs__default["default"];
2374    const fn = context.createTimeRanges || context.createTimeRanges;
2375    return fn.apply(context, args);
2376  }
2377  /**
2378   * Converts provided buffered ranges to a descriptive string
2379   *
2380   * @param {TimeRanges} buffered - received buffered time ranges
2381   *
2382   * @return {string} - descriptive string
2383   */
2384
2385  function bufferedRangesToString(buffered) {
2386    if (buffered.length === 0) {
2387      return 'Buffered Ranges are empty';
2388    }
2389
2390    let bufferedRangesStr = 'Buffered Ranges: \n';
2391
2392    for (let i = 0; i < buffered.length; i++) {
2393      const start = buffered.start(i);
2394      const end = buffered.end(i);
2395      bufferedRangesStr += `${start} --> ${end}. Duration (${end - start})\n`;
2396    }
2397
2398    return bufferedRangesStr;
2399  }
2400
2401  /**
2402   * ranges
2403   *
2404   * Utilities for working with TimeRanges.
2405   *
2406   */
2407
2408  const TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range
2409  // can be misleading because of precision differences or when the current media has poorly
2410  // aligned audio and video, which can cause values to be slightly off from what you would
2411  // expect. This value is what we consider to be safe to use in such comparisons to account
2412  // for these scenarios.
2413
2414  const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
2415
2416  const filterRanges = function (timeRanges, predicate) {
2417    const results = [];
2418    let i;
2419
2420    if (timeRanges && timeRanges.length) {
2421      // Search for ranges that match the predicate
2422      for (i = 0; i < timeRanges.length; i++) {
2423        if (predicate(timeRanges.start(i), timeRanges.end(i))) {
2424          results.push([timeRanges.start(i), timeRanges.end(i)]);
2425        }
2426      }
2427    }
2428
2429    return createTimeRanges(results);
2430  };
2431  /**
2432   * Attempts to find the buffered TimeRange that contains the specified
2433   * time.
2434   *
2435   * @param {TimeRanges} buffered - the TimeRanges object to query
2436   * @param {number} time  - the time to filter on.
2437   * @return {TimeRanges} a new TimeRanges object
2438   */
2439
2440
2441  const findRange = function (buffered, time) {
2442    return filterRanges(buffered, function (start, end) {
2443      return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time;
2444    });
2445  };
2446  /**
2447   * Returns the TimeRanges that begin later than the specified time.
2448   *
2449   * @param {TimeRanges} timeRanges - the TimeRanges object to query
2450   * @param {number} time - the time to filter on.
2451   * @return {TimeRanges} a new TimeRanges object.
2452   */
2453
2454  const findNextRange = function (timeRanges, time) {
2455    return filterRanges(timeRanges, function (start) {
2456      return start - TIME_FUDGE_FACTOR >= time;
2457    });
2458  };
2459  /**
2460   * Returns gaps within a list of TimeRanges
2461   *
2462   * @param {TimeRanges} buffered - the TimeRanges object
2463   * @return {TimeRanges} a TimeRanges object of gaps
2464   */
2465
2466  const findGaps = function (buffered) {
2467    if (buffered.length < 2) {
2468      return createTimeRanges();
2469    }
2470
2471    const ranges = [];
2472
2473    for (let i = 1; i < buffered.length; i++) {
2474      const start = buffered.end(i - 1);
2475      const end = buffered.start(i);
2476      ranges.push([start, end]);
2477    }
2478
2479    return createTimeRanges(ranges);
2480  };
2481  /**
2482   * Calculate the intersection of two TimeRanges
2483   *
2484   * @param {TimeRanges} bufferA
2485   * @param {TimeRanges} bufferB
2486   * @return {TimeRanges} The interesection of `bufferA` with `bufferB`
2487   */
2488
2489  const bufferIntersection = function (bufferA, bufferB) {
2490    let start = null;
2491    let end = null;
2492    let arity = 0;
2493    const extents = [];
2494    const ranges = [];
2495
2496    if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
2497      return createTimeRanges();
2498    } // Handle the case where we have both buffers and create an
2499    // intersection of the two
2500
2501
2502    let count = bufferA.length; // A) Gather up all start and end times
2503
2504    while (count--) {
2505      extents.push({
2506        time: bufferA.start(count),
2507        type: 'start'
2508      });
2509      extents.push({
2510        time: bufferA.end(count),
2511        type: 'end'
2512      });
2513    }
2514
2515    count = bufferB.length;
2516
2517    while (count--) {
2518      extents.push({
2519        time: bufferB.start(count),
2520        type: 'start'
2521      });
2522      extents.push({
2523        time: bufferB.end(count),
2524        type: 'end'
2525      });
2526    } // B) Sort them by time
2527
2528
2529    extents.sort(function (a, b) {
2530      return a.time - b.time;
2531    }); // C) Go along one by one incrementing arity for start and decrementing
2532    //    arity for ends
2533
2534    for (count = 0; count < extents.length; count++) {
2535      if (extents[count].type === 'start') {
2536        arity++; // D) If arity is ever incremented to 2 we are entering an
2537        //    overlapping range
2538
2539        if (arity === 2) {
2540          start = extents[count].time;
2541        }
2542      } else if (extents[count].type === 'end') {
2543        arity--; // E) If arity is ever decremented to 1 we leaving an
2544        //    overlapping range
2545
2546        if (arity === 1) {
2547          end = extents[count].time;
2548        }
2549      } // F) Record overlapping ranges
2550
2551
2552      if (start !== null && end !== null) {
2553        ranges.push([start, end]);
2554        start = null;
2555        end = null;
2556      }
2557    }
2558
2559    return createTimeRanges(ranges);
2560  };
2561  /**
2562   * Gets a human readable string for a TimeRange
2563   *
2564   * @param {TimeRange} range
2565   * @return {string} a human readable string
2566   */
2567
2568  const printableRange = range => {
2569    const strArr = [];
2570
2571    if (!range || !range.length) {
2572      return '';
2573    }
2574
2575    for (let i = 0; i < range.length; i++) {
2576      strArr.push(range.start(i) + ' => ' + range.end(i));
2577    }
2578
2579    return strArr.join(', ');
2580  };
2581  /**
2582   * Calculates the amount of time left in seconds until the player hits the end of the
2583   * buffer and causes a rebuffer
2584   *
2585   * @param {TimeRange} buffered
2586   *        The state of the buffer
2587   * @param {Numnber} currentTime
2588   *        The current time of the player
2589   * @param {number} playbackRate
2590   *        The current playback rate of the player. Defaults to 1.
2591   * @return {number}
2592   *         Time until the player has to start rebuffering in seconds.
2593   * @function timeUntilRebuffer
2594   */
2595
2596  const timeUntilRebuffer = function (buffered, currentTime, playbackRate = 1) {
2597    const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
2598    return (bufferedEnd - currentTime) / playbackRate;
2599  };
2600  /**
2601   * Converts a TimeRanges object into an array representation
2602   *
2603   * @param {TimeRanges} timeRanges
2604   * @return {Array}
2605   */
2606
2607  const timeRangesToArray = timeRanges => {
2608    const timeRangesList = [];
2609
2610    for (let i = 0; i < timeRanges.length; i++) {
2611      timeRangesList.push({
2612        start: timeRanges.start(i),
2613        end: timeRanges.end(i)
2614      });
2615    }
2616
2617    return timeRangesList;
2618  };
2619  /**
2620   * Determines if two time range objects are different.
2621   *
2622   * @param {TimeRange} a
2623   *        the first time range object to check
2624   *
2625   * @param {TimeRange} b
2626   *        the second time range object to check
2627   *
2628   * @return {Boolean}
2629   *         Whether the time range objects differ
2630   */
2631
2632  const isRangeDifferent = function (a, b) {
2633    // same object
2634    if (a === b) {
2635      return false;
2636    } // one or the other is undefined
2637
2638
2639    if (!a && b || !b && a) {
2640      return true;
2641    } // length is different
2642
2643
2644    if (a.length !== b.length) {
2645      return true;
2646    } // see if any start/end pair is different
2647
2648
2649    for (let i = 0; i < a.length; i++) {
2650      if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) {
2651        return true;
2652      }
2653    } // if the length and every pair is the same
2654    // this is the same time range
2655
2656
2657    return false;
2658  };
2659  const lastBufferedEnd = function (a) {
2660    if (!a || !a.length || !a.end) {
2661      return;
2662    }
2663
2664    return a.end(a.length - 1);
2665  };
2666  /**
2667   * A utility function to add up the amount of time in a timeRange
2668   * after a specified startTime.
2669   * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0
2670   *     would return 40 as there are 40s seconds after 0 in the timeRange
2671   *
2672   * @param {TimeRange} range
2673   *        The range to check against
2674   * @param {number} startTime
2675   *        The time in the time range that you should start counting from
2676   *
2677   * @return {number}
2678   *          The number of seconds in the buffer passed the specified time.
2679   */
2680
2681  const timeAheadOf = function (range, startTime) {
2682    let time = 0;
2683
2684    if (!range || !range.length) {
2685      return time;
2686    }
2687
2688    for (let i = 0; i < range.length; i++) {
2689      const start = range.start(i);
2690      const end = range.end(i); // startTime is after this range entirely
2691
2692      if (startTime > end) {
2693        continue;
2694      } // startTime is within this range
2695
2696
2697      if (startTime > start && startTime <= end) {
2698        time += end - startTime;
2699        continue;
2700      } // startTime is before this range.
2701
2702
2703      time += end - start;
2704    }
2705
2706    return time;
2707  };
2708
2709  /**
2710   * @file playlist.js
2711   *
2712   * Playlist related utilities.
2713   */
2714  /**
2715   * Get the duration of a segment, with special cases for
2716   * llhls segments that do not have a duration yet.
2717   *
2718   * @param {Object} playlist
2719   *        the playlist that the segment belongs to.
2720   * @param {Object} segment
2721   *        the segment to get a duration for.
2722   *
2723   * @return {number}
2724   *          the segment duration
2725   */
2726
2727  const segmentDurationWithParts = (playlist, segment) => {
2728    // if this isn't a preload segment
2729    // then we will have a segment duration that is accurate.
2730    if (!segment.preload) {
2731      return segment.duration;
2732    } // otherwise we have to add up parts and preload hints
2733    // to get an up to date duration.
2734
2735
2736    let result = 0;
2737    (segment.parts || []).forEach(function (p) {
2738      result += p.duration;
2739    }); // for preload hints we have to use partTargetDuration
2740    // as they won't even have a duration yet.
2741
2742    (segment.preloadHints || []).forEach(function (p) {
2743      if (p.type === 'PART') {
2744        result += playlist.partTargetDuration;
2745      }
2746    });
2747    return result;
2748  };
2749  /**
2750   * A function to get a combined list of parts and segments with durations
2751   * and indexes.
2752   *
2753   * @param {Playlist} playlist the playlist to get the list for.
2754   *
2755   * @return {Array} The part/segment list.
2756   */
2757
2758  const getPartsAndSegments = playlist => (playlist.segments || []).reduce((acc, segment, si) => {
2759    if (segment.parts) {
2760      segment.parts.forEach(function (part, pi) {
2761        acc.push({
2762          duration: part.duration,
2763          segmentIndex: si,
2764          partIndex: pi,
2765          part,
2766          segment
2767        });
2768      });
2769    } else {
2770      acc.push({
2771        duration: segment.duration,
2772        segmentIndex: si,
2773        partIndex: null,
2774        segment,
2775        part: null
2776      });
2777    }
2778
2779    return acc;
2780  }, []);
2781  const getLastParts = media => {
2782    const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1];
2783    return lastSegment && lastSegment.parts || [];
2784  };
2785  const getKnownPartCount = ({
2786    preloadSegment
2787  }) => {
2788    if (!preloadSegment) {
2789      return;
2790    }
2791
2792    const {
2793      parts,
2794      preloadHints
2795    } = preloadSegment;
2796    let partCount = (preloadHints || []).reduce((count, hint) => count + (hint.type === 'PART' ? 1 : 0), 0);
2797    partCount += parts && parts.length ? parts.length : 0;
2798    return partCount;
2799  };
2800  /**
2801   * Get the number of seconds to delay from the end of a
2802   * live playlist.
2803   *
2804   * @param {Playlist} main the main playlist
2805   * @param {Playlist} media the media playlist
2806   * @return {number} the hold back in seconds.
2807   */
2808
2809  const liveEdgeDelay = (main, media) => {
2810    if (media.endList) {
2811      return 0;
2812    } // dash suggestedPresentationDelay trumps everything
2813
2814
2815    if (main && main.suggestedPresentationDelay) {
2816      return main.suggestedPresentationDelay;
2817    }
2818
2819    const hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first
2820
2821    if (hasParts && media.serverControl && media.serverControl.partHoldBack) {
2822      return media.serverControl.partHoldBack;
2823    } else if (hasParts && media.partTargetDuration) {
2824      return media.partTargetDuration * 3; // finally look for full segment delays
2825    } else if (media.serverControl && media.serverControl.holdBack) {
2826      return media.serverControl.holdBack;
2827    } else if (media.targetDuration) {
2828      return media.targetDuration * 3;
2829    }
2830
2831    return 0;
2832  };
2833  /**
2834   * walk backward until we find a duration we can use
2835   * or return a failure
2836   *
2837   * @param {Playlist} playlist the playlist to walk through
2838   * @param {Number} endSequence the mediaSequence to stop walking on
2839   */
2840
2841  const backwardDuration = function (playlist, endSequence) {
2842    let result = 0;
2843    let i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following
2844    // the interval, use it
2845
2846    let segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline
2847    // information that is earlier than endSequence
2848
2849    if (segment) {
2850      if (typeof segment.start !== 'undefined') {
2851        return {
2852          result: segment.start,
2853          precise: true
2854        };
2855      }
2856
2857      if (typeof segment.end !== 'undefined') {
2858        return {
2859          result: segment.end - segment.duration,
2860          precise: true
2861        };
2862      }
2863    }
2864
2865    while (i--) {
2866      segment = playlist.segments[i];
2867
2868      if (typeof segment.end !== 'undefined') {
2869        return {
2870          result: result + segment.end,
2871          precise: true
2872        };
2873      }
2874
2875      result += segmentDurationWithParts(playlist, segment);
2876
2877      if (typeof segment.start !== 'undefined') {
2878        return {
2879          result: result + segment.start,
2880          precise: true
2881        };
2882      }
2883    }
2884
2885    return {
2886      result,
2887      precise: false
2888    };
2889  };
2890  /**
2891   * walk forward until we find a duration we can use
2892   * or return a failure
2893   *
2894   * @param {Playlist} playlist the playlist to walk through
2895   * @param {number} endSequence the mediaSequence to stop walking on
2896   */
2897
2898
2899  const forwardDuration = function (playlist, endSequence) {
2900    let result = 0;
2901    let segment;
2902    let i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline
2903    // information
2904
2905    for (; i < playlist.segments.length; i++) {
2906      segment = playlist.segments[i];
2907
2908      if (typeof segment.start !== 'undefined') {
2909        return {
2910          result: segment.start - result,
2911          precise: true
2912        };
2913      }
2914
2915      result += segmentDurationWithParts(playlist, segment);
2916
2917      if (typeof segment.end !== 'undefined') {
2918        return {
2919          result: segment.end - result,
2920          precise: true
2921        };
2922      }
2923    } // indicate we didn't find a useful duration estimate
2924
2925
2926    return {
2927      result: -1,
2928      precise: false
2929    };
2930  };
2931  /**
2932    * Calculate the media duration from the segments associated with a
2933    * playlist. The duration of a subinterval of the available segments
2934    * may be calculated by specifying an end index.
2935    *
2936    * @param {Object} playlist a media playlist object
2937    * @param {number=} endSequence an exclusive upper boundary
2938    * for the playlist.  Defaults to playlist length.
2939    * @param {number} expired the amount of time that has dropped
2940    * off the front of the playlist in a live scenario
2941    * @return {number} the duration between the first available segment
2942    * and end index.
2943    */
2944
2945
2946  const intervalDuration = function (playlist, endSequence, expired) {
2947    if (typeof endSequence === 'undefined') {
2948      endSequence = playlist.mediaSequence + playlist.segments.length;
2949    }
2950
2951    if (endSequence < playlist.mediaSequence) {
2952      return 0;
2953    } // do a backward walk to estimate the duration
2954
2955
2956    const backward = backwardDuration(playlist, endSequence);
2957
2958    if (backward.precise) {
2959      // if we were able to base our duration estimate on timing
2960      // information provided directly from the Media Source, return
2961      // it
2962      return backward.result;
2963    } // walk forward to see if a precise duration estimate can be made
2964    // that way
2965
2966
2967    const forward = forwardDuration(playlist, endSequence);
2968
2969    if (forward.precise) {
2970      // we found a segment that has been buffered and so it's
2971      // position is known precisely
2972      return forward.result;
2973    } // return the less-precise, playlist-based duration estimate
2974
2975
2976    return backward.result + expired;
2977  };
2978  /**
2979    * Calculates the duration of a playlist. If a start and end index
2980    * are specified, the duration will be for the subset of the media
2981    * timeline between those two indices. The total duration for live
2982    * playlists is always Infinity.
2983    *
2984    * @param {Object} playlist a media playlist object
2985    * @param {number=} endSequence an exclusive upper
2986    * boundary for the playlist. Defaults to the playlist media
2987    * sequence number plus its length.
2988    * @param {number=} expired the amount of time that has
2989    * dropped off the front of the playlist in a live scenario
2990    * @return {number} the duration between the start index and end
2991    * index.
2992    */
2993
2994
2995  const duration = function (playlist, endSequence, expired) {
2996    if (!playlist) {
2997      return 0;
2998    }
2999
3000    if (typeof expired !== 'number') {
3001      expired = 0;
3002    } // if a slice of the total duration is not requested, use
3003    // playlist-level duration indicators when they're present
3004
3005
3006    if (typeof endSequence === 'undefined') {
3007      // if present, use the duration specified in the playlist
3008      if (playlist.totalDuration) {
3009        return playlist.totalDuration;
3010      } // duration should be Infinity for live playlists
3011
3012
3013      if (!playlist.endList) {
3014        return window.Infinity;
3015      }
3016    } // calculate the total duration based on the segment durations
3017
3018
3019    return intervalDuration(playlist, endSequence, expired);
3020  };
3021  /**
3022    * Calculate the time between two indexes in the current playlist
3023    * neight the start- nor the end-index need to be within the current
3024    * playlist in which case, the targetDuration of the playlist is used
3025    * to approximate the durations of the segments
3026    *
3027    * @param {Array} options.durationList list to iterate over for durations.
3028    * @param {number} options.defaultDuration duration to use for elements before or after the durationList
3029    * @param {number} options.startIndex partsAndSegments index to start
3030    * @param {number} options.endIndex partsAndSegments index to end.
3031    * @return {number} the number of seconds between startIndex and endIndex
3032    */
3033
3034  const sumDurations = function ({
3035    defaultDuration,
3036    durationList,
3037    startIndex,
3038    endIndex
3039  }) {
3040    let durations = 0;
3041
3042    if (startIndex > endIndex) {
3043      [startIndex, endIndex] = [endIndex, startIndex];
3044    }
3045
3046    if (startIndex < 0) {
3047      for (let i = startIndex; i < Math.min(0, endIndex); i++) {
3048        durations += defaultDuration;
3049      }
3050
3051      startIndex = 0;
3052    }
3053
3054    for (let i = startIndex; i < endIndex; i++) {
3055      durations += durationList[i].duration;
3056    }
3057
3058    return durations;
3059  };
3060  /**
3061   * Calculates the playlist end time
3062   *
3063   * @param {Object} playlist a media playlist object
3064   * @param {number=} expired the amount of time that has
3065   *                  dropped off the front of the playlist in a live scenario
3066   * @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the
3067   *                        playlist end calculation should consider the safe live end
3068   *                        (truncate the playlist end by three segments). This is normally
3069   *                        used for calculating the end of the playlist's seekable range.
3070   *                        This takes into account the value of liveEdgePadding.
3071   *                        Setting liveEdgePadding to 0 is equivalent to setting this to false.
3072   * @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds.
3073   *                 If this is provided, it is used in the safe live end calculation.
3074   *                 Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent.
3075   *                 Corresponds to suggestedPresentationDelay in DASH manifests.
3076   * @return {number} the end time of playlist
3077   * @function playlistEnd
3078   */
3079
3080  const playlistEnd = function (playlist, expired, useSafeLiveEnd, liveEdgePadding) {
3081    if (!playlist || !playlist.segments) {
3082      return null;
3083    }
3084
3085    if (playlist.endList) {
3086      return duration(playlist);
3087    }
3088
3089    if (expired === null) {
3090      return null;
3091    }
3092
3093    expired = expired || 0;
3094    let lastSegmentEndTime = intervalDuration(playlist, playlist.mediaSequence + playlist.segments.length, expired);
3095
3096    if (useSafeLiveEnd) {
3097      liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist);
3098      lastSegmentEndTime -= liveEdgePadding;
3099    } // don't return a time less than zero
3100
3101
3102    return Math.max(0, lastSegmentEndTime);
3103  };
3104  /**
3105    * Calculates the interval of time that is currently seekable in a
3106    * playlist. The returned time ranges are relative to the earliest
3107    * moment in the specified playlist that is still available. A full
3108    * seekable implementation for live streams would need to offset
3109    * these values by the duration of content that has expired from the
3110    * stream.
3111    *
3112    * @param {Object} playlist a media playlist object
3113    * dropped off the front of the playlist in a live scenario
3114    * @param {number=} expired the amount of time that has
3115    * dropped off the front of the playlist in a live scenario
3116    * @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds.
3117    *        Corresponds to suggestedPresentationDelay in DASH manifests.
3118    * @return {TimeRanges} the periods of time that are valid targets
3119    * for seeking
3120    */
3121
3122  const seekable = function (playlist, expired, liveEdgePadding) {
3123    const useSafeLiveEnd = true;
3124    const seekableStart = expired || 0;
3125    let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding);
3126
3127    if (seekableEnd === null) {
3128      return createTimeRanges();
3129    } // Clamp seekable end since it can not be less than the seekable start
3130
3131
3132    if (seekableEnd < seekableStart) {
3133      seekableEnd = seekableStart;
3134    }
3135
3136    return createTimeRanges(seekableStart, seekableEnd);
3137  };
3138  /**
3139   * Determine the index and estimated starting time of the segment that
3140   * contains a specified playback position in a media playlist.
3141   *
3142   * @param {Object} options.playlist the media playlist to query
3143   * @param {number} options.currentTime The number of seconds since the earliest
3144   * possible position to determine the containing segment for
3145   * @param {number} options.startTime the time when the segment/part starts
3146   * @param {number} options.startingSegmentIndex the segment index to start looking at.
3147   * @param {number?} [options.startingPartIndex] the part index to look at within the segment.
3148   *
3149   * @return {Object} an object with partIndex, segmentIndex, and startTime.
3150   */
3151
3152  const getMediaInfoForTime = function ({
3153    playlist,
3154    currentTime,
3155    startingSegmentIndex,
3156    startingPartIndex,
3157    startTime,
3158    exactManifestTimings
3159  }) {
3160    let time = currentTime - startTime;
3161    const partsAndSegments = getPartsAndSegments(playlist);
3162    let startIndex = 0;
3163
3164    for (let i = 0; i < partsAndSegments.length; i++) {
3165      const partAndSegment = partsAndSegments[i];
3166
3167      if (startingSegmentIndex !== partAndSegment.segmentIndex) {
3168        continue;
3169      } // skip this if part index does not match.
3170
3171
3172      if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) {
3173        continue;
3174      }
3175
3176      startIndex = i;
3177      break;
3178    }
3179
3180    if (time < 0) {
3181      // Walk backward from startIndex in the playlist, adding durations
3182      // until we find a segment that contains `time` and return it
3183      if (startIndex > 0) {
3184        for (let i = startIndex - 1; i >= 0; i--) {
3185          const partAndSegment = partsAndSegments[i];
3186          time += partAndSegment.duration;
3187
3188          if (exactManifestTimings) {
3189            if (time < 0) {
3190              continue;
3191            }
3192          } else if (time + TIME_FUDGE_FACTOR <= 0) {
3193            continue;
3194          }
3195
3196          return {
3197            partIndex: partAndSegment.partIndex,
3198            segmentIndex: partAndSegment.segmentIndex,
3199            startTime: startTime - sumDurations({
3200              defaultDuration: playlist.targetDuration,
3201              durationList: partsAndSegments,
3202              startIndex,
3203              endIndex: i
3204            })
3205          };
3206        }
3207      } // We were unable to find a good segment within the playlist
3208      // so select the first segment
3209
3210
3211      return {
3212        partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
3213        segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
3214        startTime: currentTime
3215      };
3216    } // When startIndex is negative, we first walk forward to first segment
3217    // adding target durations. If we "run out of time" before getting to
3218    // the first segment, return the first segment
3219
3220
3221    if (startIndex < 0) {
3222      for (let i = startIndex; i < 0; i++) {
3223        time -= playlist.targetDuration;
3224
3225        if (time < 0) {
3226          return {
3227            partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
3228            segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
3229            startTime: currentTime
3230          };
3231        }
3232      }
3233
3234      startIndex = 0;
3235    } // Walk forward from startIndex in the playlist, subtracting durations
3236    // until we find a segment that contains `time` and return it
3237
3238
3239    for (let i = startIndex; i < partsAndSegments.length; i++) {
3240      const partAndSegment = partsAndSegments[i];
3241      time -= partAndSegment.duration;
3242      const canUseFudgeFactor = partAndSegment.duration > TIME_FUDGE_FACTOR;
3243      const isExactlyAtTheEnd = time === 0;
3244      const isExtremelyCloseToTheEnd = canUseFudgeFactor && time + TIME_FUDGE_FACTOR >= 0;
3245
3246      if (isExactlyAtTheEnd || isExtremelyCloseToTheEnd) {
3247        // 1) We are exactly at the end of the current segment.
3248        // 2) We are extremely close to the end of the current segment (The difference is less than  1 / 30).
3249        //    We may encounter this situation when
3250        //    we don't have exact match between segment duration info in the manifest and the actual duration of the segment
3251        //    For example:
3252        //    We appended 3 segments 10 seconds each, meaning we should have 30 sec buffered,
3253        //    but we the actual buffered is 29.99999
3254        //
3255        // In both cases:
3256        // if we passed current time -> it means that we already played current segment
3257        // if we passed buffered.end -> it means that this segment is already loaded and buffered
3258        // we should select the next segment if we have one:
3259        if (i !== partsAndSegments.length - 1) {
3260          continue;
3261        }
3262      }
3263
3264      if (exactManifestTimings) {
3265        if (time > 0) {
3266          continue;
3267        }
3268      } else if (time - TIME_FUDGE_FACTOR >= 0) {
3269        continue;
3270      }
3271
3272      return {
3273        partIndex: partAndSegment.partIndex,
3274        segmentIndex: partAndSegment.segmentIndex,
3275        startTime: startTime + sumDurations({
3276          defaultDuration: playlist.targetDuration,
3277          durationList: partsAndSegments,
3278          startIndex,
3279          endIndex: i
3280        })
3281      };
3282    } // We are out of possible candidates so load the last one...
3283
3284
3285    return {
3286      segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex,
3287      partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex,
3288      startTime: currentTime
3289    };
3290  };
3291  /**
3292   * Check whether the playlist is excluded or not.
3293   *
3294   * @param {Object} playlist the media playlist object
3295   * @return {boolean} whether the playlist is excluded or not
3296   * @function isExcluded
3297   */
3298
3299  const isExcluded = function (playlist) {
3300    return playlist.excludeUntil && playlist.excludeUntil > Date.now();
3301  };
3302  /**
3303   * Check whether the playlist is compatible with current playback configuration or has
3304   * been excluded permanently for being incompatible.
3305   *
3306   * @param {Object} playlist the media playlist object
3307   * @return {boolean} whether the playlist is incompatible or not
3308   * @function isIncompatible
3309   */
3310
3311  const isIncompatible = function (playlist) {
3312    return playlist.excludeUntil && playlist.excludeUntil === Infinity;
3313  };
3314  /**
3315   * Check whether the playlist is enabled or not.
3316   *
3317   * @param {Object} playlist the media playlist object
3318   * @return {boolean} whether the playlist is enabled or not
3319   * @function isEnabled
3320   */
3321
3322  const isEnabled = function (playlist) {
3323    const excluded = isExcluded(playlist);
3324    return !playlist.disabled && !excluded;
3325  };
3326  /**
3327   * Check whether the playlist has been manually disabled through the representations api.
3328   *
3329   * @param {Object} playlist the media playlist object
3330   * @return {boolean} whether the playlist is disabled manually or not
3331   * @function isDisabled
3332   */
3333
3334  const isDisabled = function (playlist) {
3335    return playlist.disabled;
3336  };
3337  /**
3338   * Returns whether the current playlist is an AES encrypted HLS stream
3339   *
3340   * @return {boolean} true if it's an AES encrypted HLS stream
3341   */
3342
3343  const isAes = function (media) {
3344    for (let i = 0; i < media.segments.length; i++) {
3345      if (media.segments[i].key) {
3346        return true;
3347      }
3348    }
3349
3350    return false;
3351  };
3352  /**
3353   * Checks if the playlist has a value for the specified attribute
3354   *
3355   * @param {string} attr
3356   *        Attribute to check for
3357   * @param {Object} playlist
3358   *        The media playlist object
3359   * @return {boolean}
3360   *         Whether the playlist contains a value for the attribute or not
3361   * @function hasAttribute
3362   */
3363
3364  const hasAttribute = function (attr, playlist) {
3365    return playlist.attributes && playlist.attributes[attr];
3366  };
3367  /**
3368   * Estimates the time required to complete a segment download from the specified playlist
3369   *
3370   * @param {number} segmentDuration
3371   *        Duration of requested segment
3372   * @param {number} bandwidth
3373   *        Current measured bandwidth of the player
3374   * @param {Object} playlist
3375   *        The media playlist object
3376   * @param {number=} bytesReceived
3377   *        Number of bytes already received for the request. Defaults to 0
3378   * @return {number|NaN}
3379   *         The estimated time to request the segment. NaN if bandwidth information for
3380   *         the given playlist is unavailable
3381   * @function estimateSegmentRequestTime
3382   */
3383
3384  const estimateSegmentRequestTime = function (segmentDuration, bandwidth, playlist, bytesReceived = 0) {
3385    if (!hasAttribute('BANDWIDTH', playlist)) {
3386      return NaN;
3387    }
3388
3389    const size = segmentDuration * playlist.attributes.BANDWIDTH;
3390    return (size - bytesReceived * 8) / bandwidth;
3391  };
3392  /*
3393   * Returns whether the current playlist is the lowest rendition
3394   *
3395   * @return {Boolean} true if on lowest rendition
3396   */
3397
3398  const isLowestEnabledRendition = (main, media) => {
3399    if (main.playlists.length === 1) {
3400      return true;
3401    }
3402
3403    const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE;
3404    return main.playlists.filter(playlist => {
3405      if (!isEnabled(playlist)) {
3406        return false;
3407      }
3408
3409      return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth;
3410    }).length === 0;
3411  };
3412  const playlistMatch = (a, b) => {
3413    // both playlits are null
3414    // or only one playlist is non-null
3415    // no match
3416    if (!a && !b || !a && b || a && !b) {
3417      return false;
3418    } // playlist objects are the same, match
3419
3420
3421    if (a === b) {
3422      return true;
3423    } // first try to use id as it should be the most
3424    // accurate
3425
3426
3427    if (a.id && b.id && a.id === b.id) {
3428      return true;
3429    } // next try to use reslovedUri as it should be the
3430    // second most accurate.
3431
3432
3433    if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) {
3434      return true;
3435    } // finally try to use uri as it should be accurate
3436    // but might miss a few cases for relative uris
3437
3438
3439    if (a.uri && b.uri && a.uri === b.uri) {
3440      return true;
3441    }
3442
3443    return false;
3444  };
3445
3446  const someAudioVariant = function (main, callback) {
3447    const AUDIO = main && main.mediaGroups && main.mediaGroups.AUDIO || {};
3448    let found = false;
3449
3450    for (const groupName in AUDIO) {
3451      for (const label in AUDIO[groupName]) {
3452        found = callback(AUDIO[groupName][label]);
3453
3454        if (found) {
3455          break;
3456        }
3457      }
3458
3459      if (found) {
3460        break;
3461      }
3462    }
3463
3464    return !!found;
3465  };
3466
3467  const isAudioOnly = main => {
3468    // we are audio only if we have no main playlists but do
3469    // have media group playlists.
3470    if (!main || !main.playlists || !main.playlists.length) {
3471      // without audio variants or playlists this
3472      // is not an audio only main.
3473      const found = someAudioVariant(main, variant => variant.playlists && variant.playlists.length || variant.uri);
3474      return found;
3475    } // if every playlist has only an audio codec it is audio only
3476
3477
3478    for (let i = 0; i < main.playlists.length; i++) {
3479      const playlist = main.playlists[i];
3480      const CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist.
3481
3482      if (CODECS && CODECS.split(',').every(c => isAudioCodec(c))) {
3483        continue;
3484      } // playlist is in an audio group it is audio only
3485
3486
3487      const found = someAudioVariant(main, variant => playlistMatch(playlist, variant));
3488
3489      if (found) {
3490        continue;
3491      } // if we make it here this playlist isn't audio and we
3492      // are not audio only
3493
3494
3495      return false;
3496    } // if we make it past every playlist without returning, then
3497    // this is an audio only playlist.
3498
3499
3500    return true;
3501  }; // exports
3502
3503  var Playlist = {
3504    liveEdgeDelay,
3505    duration,
3506    seekable,
3507    getMediaInfoForTime,
3508    isEnabled,
3509    isDisabled,
3510    isExcluded,
3511    isIncompatible,
3512    playlistEnd,
3513    isAes,
3514    hasAttribute,
3515    estimateSegmentRequestTime,
3516    isLowestEnabledRendition,
3517    isAudioOnly,
3518    playlistMatch,
3519    segmentDurationWithParts
3520  };
3521
3522  const {
3523    log
3524  } = videojs__default["default"];
3525  const createPlaylistID = (index, uri) => {
3526    return `${index}-${uri}`;
3527  }; // default function for creating a group id
3528
3529  const groupID = (type, group, label) => {
3530    return `placeholder-uri-${type}-${group}-${label}`;
3531  };
3532  /**
3533   * Parses a given m3u8 playlist
3534   *
3535   * @param {Function} [onwarn]
3536   *        a function to call when the parser triggers a warning event.
3537   * @param {Function} [oninfo]
3538   *        a function to call when the parser triggers an info event.
3539   * @param {string} manifestString
3540   *        The downloaded manifest string
3541   * @param {Object[]} [customTagParsers]
3542   *        An array of custom tag parsers for the m3u8-parser instance
3543   * @param {Object[]} [customTagMappers]
3544   *        An array of custom tag mappers for the m3u8-parser instance
3545   * @param {boolean} [llhls]
3546   *        Whether to keep ll-hls features in the manifest after parsing.
3547   * @return {Object}
3548   *         The manifest object
3549   */
3550
3551  const parseManifest = ({
3552    onwarn,
3553    oninfo,
3554    manifestString,
3555    customTagParsers = [],
3556    customTagMappers = [],
3557    llhls
3558  }) => {
3559    const parser = new Parser();
3560
3561    if (onwarn) {
3562      parser.on('warn', onwarn);
3563    }
3564
3565    if (oninfo) {
3566      parser.on('info', oninfo);
3567    }
3568
3569    customTagParsers.forEach(customParser => parser.addParser(customParser));
3570    customTagMappers.forEach(mapper => parser.addTagMapper(mapper));
3571    parser.push(manifestString);
3572    parser.end();
3573    const manifest = parser.manifest; // remove llhls features from the parsed manifest
3574    // if we don't want llhls support.
3575
3576    if (!llhls) {
3577      ['preloadSegment', 'skip', 'serverControl', 'renditionReports', 'partInf', 'partTargetDuration'].forEach(function (k) {
3578        if (manifest.hasOwnProperty(k)) {
3579          delete manifest[k];
3580        }
3581      });
3582
3583      if (manifest.segments) {
3584        manifest.segments.forEach(function (segment) {
3585          ['parts', 'preloadHints'].forEach(function (k) {
3586            if (segment.hasOwnProperty(k)) {
3587              delete segment[k];
3588            }
3589          });
3590        });
3591      }
3592    }
3593
3594    if (!manifest.targetDuration) {
3595      let targetDuration = 10;
3596
3597      if (manifest.segments && manifest.segments.length) {
3598        targetDuration = manifest.segments.reduce((acc, s) => Math.max(acc, s.duration), 0);
3599      }
3600
3601      if (onwarn) {
3602        onwarn({
3603          message: `manifest has no targetDuration defaulting to ${targetDuration}`
3604        });
3605      }
3606
3607      manifest.targetDuration = targetDuration;
3608    }
3609
3610    const parts = getLastParts(manifest);
3611
3612    if (parts.length && !manifest.partTargetDuration) {
3613      const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0);
3614
3615      if (onwarn) {
3616        onwarn({
3617          message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}`
3618        });
3619        log.error('LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.');
3620      }
3621
3622      manifest.partTargetDuration = partTargetDuration;
3623    }
3624
3625    return manifest;
3626  };
3627  /**
3628   * Loops through all supported media groups in main and calls the provided
3629   * callback for each group
3630   *
3631   * @param {Object} main
3632   *        The parsed main manifest object
3633   * @param {Function} callback
3634   *        Callback to call for each media group
3635   */
3636
3637  const forEachMediaGroup$1 = (main, callback) => {
3638    if (!main.mediaGroups) {
3639      return;
3640    }
3641
3642    ['AUDIO', 'SUBTITLES'].forEach(mediaType => {
3643      if (!main.mediaGroups[mediaType]) {
3644        return;
3645      }
3646
3647      for (const groupKey in main.mediaGroups[mediaType]) {
3648        for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
3649          const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey];
3650          callback(mediaProperties, mediaType, groupKey, labelKey);
3651        }
3652      }
3653    });
3654  };
3655  /**
3656   * Adds properties and attributes to the playlist to keep consistent functionality for
3657   * playlists throughout VHS.
3658   *
3659   * @param {Object} config
3660   *        Arguments object
3661   * @param {Object} config.playlist
3662   *        The media playlist
3663   * @param {string} [config.uri]
3664   *        The uri to the media playlist (if media playlist is not from within a main
3665   *        playlist)
3666   * @param {string} id
3667   *        ID to use for the playlist
3668   */
3669
3670  const setupMediaPlaylist = ({
3671    playlist,
3672    uri,
3673    id
3674  }) => {
3675    playlist.id = id;
3676    playlist.playlistErrors_ = 0;
3677
3678    if (uri) {
3679      // For media playlists, m3u8-parser does not have access to a URI, as HLS media
3680      // playlists do not contain their own source URI, but one is needed for consistency in
3681      // VHS.
3682      playlist.uri = uri;
3683    } // For HLS main playlists, even though certain attributes MUST be defined, the
3684    // stream may still be played without them.
3685    // For HLS media playlists, m3u8-parser does not attach an attributes object to the
3686    // manifest.
3687    //
3688    // To avoid undefined reference errors through the project, and make the code easier
3689    // to write/read, add an empty attributes object for these cases.
3690
3691
3692    playlist.attributes = playlist.attributes || {};
3693  };
3694  /**
3695   * Adds ID, resolvedUri, and attributes properties to each playlist of the main, where
3696   * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to
3697   * playlist references to the playlists array.
3698   *
3699   * @param {Object} main
3700   *        The main playlist
3701   */
3702
3703  const setupMediaPlaylists = main => {
3704    let i = main.playlists.length;
3705
3706    while (i--) {
3707      const playlist = main.playlists[i];
3708      setupMediaPlaylist({
3709        playlist,
3710        id: createPlaylistID(i, playlist.uri)
3711      });
3712      playlist.resolvedUri = resolveUrl(main.uri, playlist.uri);
3713      main.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility
3714
3715      main.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute,
3716      // the stream can be played without it. Although an attributes property may have been
3717      // added to the playlist to prevent undefined references, issue a warning to fix the
3718      // manifest.
3719
3720      if (!playlist.attributes.BANDWIDTH) {
3721        log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
3722      }
3723    }
3724  };
3725  /**
3726   * Adds resolvedUri properties to each media group.
3727   *
3728   * @param {Object} main
3729   *        The main playlist
3730   */
3731
3732  const resolveMediaGroupUris = main => {
3733    forEachMediaGroup$1(main, properties => {
3734      if (properties.uri) {
3735        properties.resolvedUri = resolveUrl(main.uri, properties.uri);
3736      }
3737    });
3738  };
3739  /**
3740   * Creates a main playlist wrapper to insert a sole media playlist into.
3741   *
3742   * @param {Object} media
3743   *        Media playlist
3744   * @param {string} uri
3745   *        The media URI
3746   *
3747   * @return {Object}
3748   *         main playlist
3749   */
3750
3751  const mainForMedia = (media, uri) => {
3752    const id = createPlaylistID(0, uri);
3753    const main = {
3754      mediaGroups: {
3755        'AUDIO': {},
3756        'VIDEO': {},
3757        'CLOSED-CAPTIONS': {},
3758        'SUBTITLES': {}
3759      },
3760      uri: window.location.href,
3761      resolvedUri: window.location.href,
3762      playlists: [{
3763        uri,
3764        id,
3765        resolvedUri: uri,
3766        // m3u8-parser does not attach an attributes property to media playlists so make
3767        // sure that the property is attached to avoid undefined reference errors
3768        attributes: {}
3769      }]
3770    }; // set up ID reference
3771
3772    main.playlists[id] = main.playlists[0]; // URI reference added for backwards compatibility
3773
3774    main.playlists[uri] = main.playlists[0];
3775    return main;
3776  };
3777  /**
3778   * Does an in-place update of the main manifest to add updated playlist URI references
3779   * as well as other properties needed by VHS that aren't included by the parser.
3780   *
3781   * @param {Object} main
3782   *        main manifest object
3783   * @param {string} uri
3784   *        The source URI
3785   * @param {function} createGroupID
3786   *        A function to determine how to create the groupID for mediaGroups
3787   */
3788
3789  const addPropertiesToMain = (main, uri, createGroupID = groupID) => {
3790    main.uri = uri;
3791
3792    for (let i = 0; i < main.playlists.length; i++) {
3793      if (!main.playlists[i].uri) {
3794        // Set up phony URIs for the playlists since playlists are referenced by their URIs
3795        // throughout VHS, but some formats (e.g., DASH) don't have external URIs
3796        // TODO: consider adding dummy URIs in mpd-parser
3797        const phonyUri = `placeholder-uri-${i}`;
3798        main.playlists[i].uri = phonyUri;
3799      }
3800    }
3801
3802    const audioOnlyMain = isAudioOnly(main);
3803    forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => {
3804      // add a playlist array under properties
3805      if (!properties.playlists || !properties.playlists.length) {
3806        // If the manifest is audio only and this media group does not have a uri, check
3807        // if the media group is located in the main list of playlists. If it is, don't add
3808        // placeholder properties as it shouldn't be considered an alternate audio track.
3809        if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) {
3810          for (let i = 0; i < main.playlists.length; i++) {
3811            const p = main.playlists[i];
3812
3813            if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) {
3814              return;
3815            }
3816          }
3817        }
3818
3819        properties.playlists = [_extends({}, properties)];
3820      }
3821
3822      properties.playlists.forEach(function (p, i) {
3823        const groupId = createGroupID(mediaType, groupKey, labelKey, p);
3824        const id = createPlaylistID(i, groupId);
3825
3826        if (p.uri) {
3827          p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri);
3828        } else {
3829          // DEPRECATED, this has been added to prevent a breaking change.
3830          // previously we only ever had a single media group playlist, so
3831          // we mark the first playlist uri without prepending the index as we used to
3832          // ideally we would do all of the playlists the same way.
3833          p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use
3834          // the placeholder again
3835
3836          p.resolvedUri = p.uri;
3837        }
3838
3839        p.id = p.id || id; // add an empty attributes object, all playlists are
3840        // expected to have this.
3841
3842        p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility)
3843
3844        main.playlists[p.id] = p;
3845        main.playlists[p.uri] = p;
3846      });
3847    });
3848    setupMediaPlaylists(main);
3849    resolveMediaGroupUris(main);
3850  };
3851
3852  class DateRangesStorage {
3853    constructor() {
3854      this.offset_ = null;
3855      this.pendingDateRanges_ = new Map();
3856      this.processedDateRanges_ = new Map();
3857    }
3858
3859    setOffset(segments = []) {
3860      // already set
3861      if (this.offset_ !== null) {
3862        return;
3863      } // no segment to process
3864
3865
3866      if (!segments.length) {
3867        return;
3868      }
3869
3870      const [firstSegment] = segments; // no program date time
3871
3872      if (firstSegment.programDateTime === undefined) {
3873        return;
3874      } // Set offset as ProgramDateTime for the very first segment of the very first playlist load:
3875
3876
3877      this.offset_ = firstSegment.programDateTime / 1000;
3878    }
3879
3880    setPendingDateRanges(dateRanges = []) {
3881      if (!dateRanges.length) {
3882        return;
3883      }
3884
3885      const [dateRange] = dateRanges;
3886      const startTime = dateRange.startDate.getTime();
3887      this.trimProcessedDateRanges_(startTime);
3888      this.pendingDateRanges_ = dateRanges.reduce((map, pendingDateRange) => {
3889        map.set(pendingDateRange.id, pendingDateRange);
3890        return map;
3891      }, new Map());
3892    }
3893
3894    processDateRange(dateRange) {
3895      this.pendingDateRanges_.delete(dateRange.id);
3896      this.processedDateRanges_.set(dateRange.id, dateRange);
3897    }
3898
3899    getDateRangesToProcess() {
3900      if (this.offset_ === null) {
3901        return [];
3902      }
3903
3904      const dateRangeClasses = {};
3905      const dateRangesToProcess = [];
3906      this.pendingDateRanges_.forEach((dateRange, id) => {
3907        if (this.processedDateRanges_.has(id)) {
3908          return;
3909        }
3910
3911        dateRange.startTime = dateRange.startDate.getTime() / 1000 - this.offset_;
3912
3913        dateRange.processDateRange = () => this.processDateRange(dateRange);
3914
3915        dateRangesToProcess.push(dateRange);
3916
3917        if (!dateRange.class) {
3918          return;
3919        }
3920
3921        if (dateRangeClasses[dateRange.class]) {
3922          const length = dateRangeClasses[dateRange.class].push(dateRange);
3923          dateRange.classListIndex = length - 1;
3924        } else {
3925          dateRangeClasses[dateRange.class] = [dateRange];
3926          dateRange.classListIndex = 0;
3927        }
3928      });
3929
3930      for (const dateRange of dateRangesToProcess) {
3931        const classList = dateRangeClasses[dateRange.class] || [];
3932
3933        if (dateRange.endDate) {
3934          dateRange.endTime = dateRange.endDate.getTime() / 1000 - this.offset_;
3935        } else if (dateRange.endOnNext && classList[dateRange.classListIndex + 1]) {
3936          dateRange.endTime = classList[dateRange.classListIndex + 1].startTime;
3937        } else if (dateRange.duration) {
3938          dateRange.endTime = dateRange.startTime + dateRange.duration;
3939        } else if (dateRange.plannedDuration) {
3940          dateRange.endTime = dateRange.startTime + dateRange.plannedDuration;
3941        } else {
3942          dateRange.endTime = dateRange.startTime;
3943        }
3944      }
3945
3946      return dateRangesToProcess;
3947    }
3948
3949    trimProcessedDateRanges_(startTime) {
3950      const copy = new Map(this.processedDateRanges_);
3951      copy.forEach((dateRange, id) => {
3952        if (dateRange.startDate.getTime() < startTime) {
3953          this.processedDateRanges_.delete(id);
3954        }
3955      });
3956    }
3957
3958  }
3959
3960  const QUOTA_EXCEEDED_ERR = 22;
3961  const getStreamingNetworkErrorMetadata = ({
3962    requestType,
3963    request,
3964    error,
3965    parseFailure
3966  }) => {
3967    const isBadStatus = request.status < 200 || request.status > 299;
3968    const isFailure = request.status >= 400 && request.status <= 499;
3969    const errorMetadata = {
3970      uri: request.uri,
3971      requestType
3972    };
3973    const isBadStatusOrParseFailure = isBadStatus && !isFailure || parseFailure;
3974
3975    if (error && isFailure) {
3976      // copy original error and add to the metadata.
3977      errorMetadata.error = _extends({}, error);
3978      errorMetadata.errorType = videojs__default["default"].Error.NetworkRequestFailed;
3979    } else if (request.aborted) {
3980      errorMetadata.errorType = videojs__default["default"].Error.NetworkRequestAborted;
3981    } else if (request.timedout) {
3982      errorMetadata.errorType = videojs__default["default"].Error.NetworkRequestTimeout;
3983    } else if (isBadStatusOrParseFailure) {
3984      const errorType = parseFailure ? videojs__default["default"].Error.NetworkBodyParserFailed : videojs__default["default"].Error.NetworkBadStatus;
3985      errorMetadata.errorType = errorType;
3986      errorMetadata.status = request.status;
3987      errorMetadata.headers = request.headers;
3988    }
3989
3990    return errorMetadata;
3991  };
3992
3993  /**
3994   * @file - codecs.js - Handles tasks regarding codec strings such as translating them to
3995   * codec strings, or translating codec strings into objects that can be examined.
3996   */
3997  const logFn$1 = logger('CodecUtils');
3998  /**
3999   * Returns a set of codec strings parsed from the playlist or the default
4000   * codec strings if no codecs were specified in the playlist
4001   *
4002   * @param {Playlist} media the current media playlist
4003   * @return {Object} an object with the video and audio codecs
4004   */
4005
4006  const getCodecs = function (media) {
4007    // if the codecs were explicitly specified, use them instead of the
4008    // defaults
4009    const mediaAttributes = media.attributes || {};
4010
4011    if (mediaAttributes.CODECS) {
4012      return parseCodecs(mediaAttributes.CODECS);
4013    }
4014  };
4015  const isMaat = (main, media) => {
4016    const mediaAttributes = media.attributes || {};
4017    return main && main.mediaGroups && main.mediaGroups.AUDIO && mediaAttributes.AUDIO && main.mediaGroups.AUDIO[mediaAttributes.AUDIO];
4018  };
4019  const isMuxed = (main, media) => {
4020    if (!isMaat(main, media)) {
4021      return true;
4022    }
4023
4024    const mediaAttributes = media.attributes || {};
4025    const audioGroup = main.mediaGroups.AUDIO[mediaAttributes.AUDIO];
4026
4027    for (const groupId in audioGroup) {
4028      // If an audio group has a URI (the case for HLS, as HLS will use external playlists),
4029      // or there are listed playlists (the case for DASH, as the manifest will have already
4030      // provided all of the details necessary to generate the audio playlist, as opposed to
4031      // HLS' externally requested playlists), then the content is demuxed.
4032      if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) {
4033        return true;
4034      }
4035    }
4036
4037    return false;
4038  };
4039  const unwrapCodecList = function (codecList) {
4040    const codecs = {};
4041    codecList.forEach(({
4042      mediaType,
4043      type,
4044      details
4045    }) => {
4046      codecs[mediaType] = codecs[mediaType] || [];
4047      codecs[mediaType].push(translateLegacyCodec(`${type}${details}`));
4048    });
4049    Object.keys(codecs).forEach(function (mediaType) {
4050      if (codecs[mediaType].length > 1) {
4051        logFn$1(`multiple ${mediaType} codecs found as attributes: ${codecs[mediaType].join(', ')}. Setting playlist codecs to null so that we wait for mux.js to probe segments for real codecs.`);
4052        codecs[mediaType] = null;
4053        return;
4054      }
4055
4056      codecs[mediaType] = codecs[mediaType][0];
4057    });
4058    return codecs;
4059  };
4060  const codecCount = function (codecObj) {
4061    let count = 0;
4062
4063    if (codecObj.audio) {
4064      count++;
4065    }
4066
4067    if (codecObj.video) {
4068      count++;
4069    }
4070
4071    return count;
4072  };
4073  /**
4074   * Calculates the codec strings for a working configuration of
4075   * SourceBuffers to play variant streams in a main playlist. If
4076   * there is no possible working configuration, an empty object will be
4077   * returned.
4078   *
4079   * @param main {Object} the m3u8 object for the main playlist
4080   * @param media {Object} the m3u8 object for the variant playlist
4081   * @return {Object} the codec strings.
4082   *
4083   * @private
4084   */
4085
4086  const codecsForPlaylist = function (main, media) {
4087    const mediaAttributes = media.attributes || {};
4088    const codecInfo = unwrapCodecList(getCodecs(media) || []); // HLS with multiple-audio tracks must always get an audio codec.
4089    // Put another way, there is no way to have a video-only multiple-audio HLS!
4090
4091    if (isMaat(main, media) && !codecInfo.audio) {
4092      if (!isMuxed(main, media)) {
4093        // It is possible for codecs to be specified on the audio media group playlist but
4094        // not on the rendition playlist. This is mostly the case for DASH, where audio and
4095        // video are always separate (and separately specified).
4096        const defaultCodecs = unwrapCodecList(codecsFromDefault(main, mediaAttributes.AUDIO) || []);
4097
4098        if (defaultCodecs.audio) {
4099          codecInfo.audio = defaultCodecs.audio;
4100        }
4101      }
4102    }
4103
4104    return codecInfo;
4105  };
4106
4107  const {
4108    EventTarget: EventTarget$1
4109  } = videojs__default["default"];
4110
4111  const addLLHLSQueryDirectives = (uri, media) => {
4112    if (media.endList || !media.serverControl) {
4113      return uri;
4114    }
4115
4116    const parameters = {};
4117
4118    if (media.serverControl.canBlockReload) {
4119      const {
4120        preloadSegment
4121      } = media; // next msn is a zero based value, length is not.
4122
4123      let nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely
4124      // that we are going to request a part of that preload segment.
4125      // the logic below is used to determine that.
4126
4127      if (preloadSegment) {
4128        const parts = preloadSegment.parts || []; // _HLS_part is a zero based index
4129
4130        const nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the
4131        // length of parts, then we know we had part preload hints
4132        // and we need to add the _HLS_part= query
4133
4134        if (nextPart > -1 && nextPart !== parts.length - 1) {
4135          // add existing parts to our preload hints
4136          // eslint-disable-next-line
4137          parameters._HLS_part = nextPart;
4138        } // this if statement makes sure that we request the msn
4139        // of the preload segment if:
4140        // 1. the preload segment had parts (and was not yet a full segment)
4141        //    but was added to our segments array
4142        // 2. the preload segment had preload hints for parts that are not in
4143        //    the manifest yet.
4144        // in all other cases we want the segment after the preload segment
4145        // which will be given by using media.segments.length because it is 1 based
4146        // rather than 0 based.
4147
4148
4149        if (nextPart > -1 || parts.length) {
4150          nextMSN--;
4151        }
4152      } // add _HLS_msn= in front of any _HLS_part query
4153      // eslint-disable-next-line
4154
4155
4156      parameters._HLS_msn = nextMSN;
4157    }
4158
4159    if (media.serverControl && media.serverControl.canSkipUntil) {
4160      // add _HLS_skip= infront of all other queries.
4161      // eslint-disable-next-line
4162      parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES';
4163    }
4164
4165    if (Object.keys(parameters).length) {
4166      const parsedUri = new window.URL(uri);
4167      ['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) {
4168        if (!parameters.hasOwnProperty(name)) {
4169          return;
4170        }
4171
4172        parsedUri.searchParams.set(name, parameters[name]);
4173      });
4174      uri = parsedUri.toString();
4175    }
4176
4177    return uri;
4178  };
4179  /**
4180   * Returns a new segment object with properties and
4181   * the parts array merged.
4182   *
4183   * @param {Object} a the old segment
4184   * @param {Object} b the new segment
4185   *
4186   * @return {Object} the merged segment
4187   */
4188
4189
4190  const updateSegment = (a, b) => {
4191    if (!a) {
4192      return b;
4193    }
4194
4195    const result = merge$1(a, b); // if only the old segment has preload hints
4196    // and the new one does not, remove preload hints.
4197
4198    if (a.preloadHints && !b.preloadHints) {
4199      delete result.preloadHints;
4200    } // if only the old segment has parts
4201    // then the parts are no longer valid
4202
4203
4204    if (a.parts && !b.parts) {
4205      delete result.parts; // if both segments have parts
4206      // copy part propeties from the old segment
4207      // to the new one.
4208    } else if (a.parts && b.parts) {
4209      for (let i = 0; i < b.parts.length; i++) {
4210        if (a.parts && a.parts[i]) {
4211          result.parts[i] = merge$1(a.parts[i], b.parts[i]);
4212        }
4213      }
4214    } // set skipped to false for segments that have
4215    // have had information merged from the old segment.
4216
4217
4218    if (!a.skipped && b.skipped) {
4219      result.skipped = false;
4220    } // set preload to false for segments that have
4221    // had information added in the new segment.
4222
4223
4224    if (a.preload && !b.preload) {
4225      result.preload = false;
4226    }
4227
4228    return result;
4229  };
4230  /**
4231   * Returns a new array of segments that is the result of merging
4232   * properties from an older list of segments onto an updated
4233   * list. No properties on the updated playlist will be ovewritten.
4234   *
4235   * @param {Array} original the outdated list of segments
4236   * @param {Array} update the updated list of segments
4237   * @param {number=} offset the index of the first update
4238   * segment in the original segment list. For non-live playlists,
4239   * this should always be zero and does not need to be
4240   * specified. For live playlists, it should be the difference
4241   * between the media sequence numbers in the original and updated
4242   * playlists.
4243   * @return {Array} a list of merged segment objects
4244   */
4245
4246  const updateSegments = (original, update, offset) => {
4247    const oldSegments = original.slice();
4248    const newSegments = update.slice();
4249    offset = offset || 0;
4250    const result = [];
4251    let currentMap;
4252
4253    for (let newIndex = 0; newIndex < newSegments.length; newIndex++) {
4254      const oldSegment = oldSegments[newIndex + offset];
4255      const newSegment = newSegments[newIndex];
4256
4257      if (oldSegment) {
4258        currentMap = oldSegment.map || currentMap;
4259        result.push(updateSegment(oldSegment, newSegment));
4260      } else {
4261        // carry over map to new segment if it is missing
4262        if (currentMap && !newSegment.map) {
4263          newSegment.map = currentMap;
4264        }
4265
4266        result.push(newSegment);
4267      }
4268    }
4269
4270    return result;
4271  };
4272  const resolveSegmentUris = (segment, baseUri) => {
4273    // preloadSegment will not have a uri at all
4274    // as the segment isn't actually in the manifest yet, only parts
4275    if (!segment.resolvedUri && segment.uri) {
4276      segment.resolvedUri = resolveUrl(baseUri, segment.uri);
4277    }
4278
4279    if (segment.key && !segment.key.resolvedUri) {
4280      segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri);
4281    }
4282
4283    if (segment.map && !segment.map.resolvedUri) {
4284      segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri);
4285    }
4286
4287    if (segment.map && segment.map.key && !segment.map.key.resolvedUri) {
4288      segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri);
4289    }
4290
4291    if (segment.parts && segment.parts.length) {
4292      segment.parts.forEach(p => {
4293        if (p.resolvedUri) {
4294          return;
4295        }
4296
4297        p.resolvedUri = resolveUrl(baseUri, p.uri);
4298      });
4299    }
4300
4301    if (segment.preloadHints && segment.preloadHints.length) {
4302      segment.preloadHints.forEach(p => {
4303        if (p.resolvedUri) {
4304          return;
4305        }
4306
4307        p.resolvedUri = resolveUrl(baseUri, p.uri);
4308      });
4309    }
4310  };
4311
4312  const getAllSegments = function (media) {
4313    const segments = media.segments || [];
4314    const preloadSegment = media.preloadSegment; // a preloadSegment with only preloadHints is not currently
4315    // a usable segment, only include a preloadSegment that has
4316    // parts.
4317
4318    if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) {
4319      // if preloadHints has a MAP that means that the
4320      // init segment is going to change. We cannot use any of the parts
4321      // from this preload segment.
4322      if (preloadSegment.preloadHints) {
4323        for (let i = 0; i < preloadSegment.preloadHints.length; i++) {
4324          if (preloadSegment.preloadHints[i].type === 'MAP') {
4325            return segments;
4326          }
4327        }
4328      } // set the duration for our preload segment to target duration.
4329
4330
4331      preloadSegment.duration = media.targetDuration;
4332      preloadSegment.preload = true;
4333      segments.push(preloadSegment);
4334    }
4335
4336    return segments;
4337  }; // consider the playlist unchanged if the playlist object is the same or
4338  // the number of segments is equal, the media sequence number is unchanged,
4339  // and this playlist hasn't become the end of the playlist
4340
4341
4342  const isPlaylistUnchanged = (a, b) => a === b || a.segments && b.segments && a.segments.length === b.segments.length && a.endList === b.endList && a.mediaSequence === b.mediaSequence && a.preloadSegment === b.preloadSegment;
4343  /**
4344    * Returns a new main playlist that is the result of merging an
4345    * updated media playlist into the original version. If the
4346    * updated media playlist does not match any of the playlist
4347    * entries in the original main playlist, null is returned.
4348    *
4349    * @param {Object} main a parsed main M3U8 object
4350    * @param {Object} media a parsed media M3U8 object
4351    * @return {Object} a new object that represents the original
4352    * main playlist with the updated media playlist merged in, or
4353    * null if the merge produced no change.
4354    */
4355
4356  const updateMain$1 = (main, newMedia, unchangedCheck = isPlaylistUnchanged) => {
4357    const result = merge$1(main, {});
4358    const oldMedia = result.playlists[newMedia.id];
4359
4360    if (!oldMedia) {
4361      return null;
4362    }
4363
4364    if (unchangedCheck(oldMedia, newMedia)) {
4365      return null;
4366    }
4367
4368    newMedia.segments = getAllSegments(newMedia);
4369    const mergedPlaylist = merge$1(oldMedia, newMedia); // always use the new media's preload segment
4370
4371    if (mergedPlaylist.preloadSegment && !newMedia.preloadSegment) {
4372      delete mergedPlaylist.preloadSegment;
4373    } // if the update could overlap existing segment information, merge the two segment lists
4374
4375
4376    if (oldMedia.segments) {
4377      if (newMedia.skip) {
4378        newMedia.segments = newMedia.segments || []; // add back in objects for skipped segments, so that we merge
4379        // old properties into the new segments
4380
4381        for (let i = 0; i < newMedia.skip.skippedSegments; i++) {
4382          newMedia.segments.unshift({
4383            skipped: true
4384          });
4385        }
4386      }
4387
4388      mergedPlaylist.segments = updateSegments(oldMedia.segments, newMedia.segments, newMedia.mediaSequence - oldMedia.mediaSequence);
4389    } // resolve any segment URIs to prevent us from having to do it later
4390
4391
4392    mergedPlaylist.segments.forEach(segment => {
4393      resolveSegmentUris(segment, mergedPlaylist.resolvedUri);
4394    }); // TODO Right now in the playlists array there are two references to each playlist, one
4395    // that is referenced by index, and one by URI. The index reference may no longer be
4396    // necessary.
4397
4398    for (let i = 0; i < result.playlists.length; i++) {
4399      if (result.playlists[i].id === newMedia.id) {
4400        result.playlists[i] = mergedPlaylist;
4401      }
4402    }
4403
4404    result.playlists[newMedia.id] = mergedPlaylist; // URI reference added for backwards compatibility
4405
4406    result.playlists[newMedia.uri] = mergedPlaylist; // update media group playlist references.
4407
4408    forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => {
4409      if (!properties.playlists) {
4410        return;
4411      }
4412
4413      for (let i = 0; i < properties.playlists.length; i++) {
4414        if (newMedia.id === properties.playlists[i].id) {
4415          properties.playlists[i] = mergedPlaylist;
4416        }
4417      }
4418    });
4419    return result;
4420  };
4421  /**
4422   * Calculates the time to wait before refreshing a live playlist
4423   *
4424   * @param {Object} media
4425   *        The current media
4426   * @param {boolean} update
4427   *        True if there were any updates from the last refresh, false otherwise
4428   * @return {number}
4429   *         The time in ms to wait before refreshing the live playlist
4430   */
4431
4432  const refreshDelay = (media, update) => {
4433    const segments = media.segments || [];
4434    const lastSegment = segments[segments.length - 1];
4435    const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1];
4436    const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration;
4437
4438    if (update && lastDuration) {
4439      return lastDuration * 1000;
4440    } // if the playlist is unchanged since the last reload or last segment duration
4441    // cannot be determined, try again after half the target duration
4442
4443
4444    return (media.partTargetDuration || media.targetDuration || 10) * 500;
4445  };
4446
4447  const playlistMetadataPayload = (playlists, type, isLive) => {
4448    if (!playlists) {
4449      return;
4450    }
4451
4452    const renditions = [];
4453    playlists.forEach(playlist => {
4454      // we need attributes to populate rendition data.
4455      if (!playlist.attributes) {
4456        return;
4457      }
4458
4459      const {
4460        BANDWIDTH,
4461        RESOLUTION,
4462        CODECS
4463      } = playlist.attributes;
4464      renditions.push({
4465        id: playlist.id,
4466        bandwidth: BANDWIDTH,
4467        resolution: RESOLUTION,
4468        codecs: CODECS
4469      });
4470    });
4471    return {
4472      type,
4473      isLive,
4474      renditions
4475    };
4476  };
4477  /**
4478   * Load a playlist from a remote location
4479   *
4480   * @class PlaylistLoader
4481   * @extends Stream
4482   * @param {string|Object} src url or object of manifest
4483   * @param {boolean} withCredentials the withCredentials xhr option
4484   * @class
4485   */
4486
4487
4488  class PlaylistLoader extends EventTarget$1 {
4489    constructor(src, vhs, options = {}) {
4490      super();
4491
4492      if (!src) {
4493        throw new Error('A non-empty playlist URL or object is required');
4494      }
4495
4496      this.logger_ = logger('PlaylistLoader');
4497      const {
4498        withCredentials = false
4499      } = options;
4500      this.src = src;
4501      this.vhs_ = vhs;
4502      this.withCredentials = withCredentials;
4503      this.addDateRangesToTextTrack_ = options.addDateRangesToTextTrack;
4504      const vhsOptions = vhs.options_;
4505      this.customTagParsers = vhsOptions && vhsOptions.customTagParsers || [];
4506      this.customTagMappers = vhsOptions && vhsOptions.customTagMappers || [];
4507      this.llhls = vhsOptions && vhsOptions.llhls;
4508      this.dateRangesStorage_ = new DateRangesStorage(); // initialize the loader state
4509
4510      this.state = 'HAVE_NOTHING'; // live playlist staleness timeout
4511
4512      this.handleMediaupdatetimeout_ = this.handleMediaupdatetimeout_.bind(this);
4513      this.on('mediaupdatetimeout', this.handleMediaupdatetimeout_);
4514      this.on('loadedplaylist', this.handleLoadedPlaylist_.bind(this));
4515    }
4516
4517    handleLoadedPlaylist_() {
4518      const mediaPlaylist = this.media();
4519
4520      if (!mediaPlaylist) {
4521        return;
4522      }
4523
4524      this.dateRangesStorage_.setOffset(mediaPlaylist.segments);
4525      this.dateRangesStorage_.setPendingDateRanges(mediaPlaylist.dateRanges);
4526      const availableDateRanges = this.dateRangesStorage_.getDateRangesToProcess();
4527
4528      if (!availableDateRanges.length || !this.addDateRangesToTextTrack_) {
4529        return;
4530      }
4531
4532      this.addDateRangesToTextTrack_(availableDateRanges);
4533    }
4534
4535    handleMediaupdatetimeout_() {
4536      if (this.state !== 'HAVE_METADATA') {
4537        // only refresh the media playlist if no other activity is going on
4538        return;
4539      }
4540
4541      const media = this.media();
4542      let uri = resolveUrl(this.main.uri, media.uri);
4543
4544      if (this.llhls) {
4545        uri = addLLHLSQueryDirectives(uri, media);
4546      }
4547
4548      this.state = 'HAVE_CURRENT_METADATA';
4549      this.request = this.vhs_.xhr({
4550        uri,
4551        withCredentials: this.withCredentials,
4552        requestType: 'hls-playlist'
4553      }, (error, req) => {
4554        // disposed
4555        if (!this.request) {
4556          return;
4557        }
4558
4559        if (error) {
4560          return this.playlistRequestError(this.request, this.media(), 'HAVE_METADATA');
4561        }
4562
4563        this.haveMetadata({
4564          playlistString: this.request.responseText,
4565          url: this.media().uri,
4566          id: this.media().id
4567        });
4568      });
4569    }
4570
4571    playlistRequestError(xhr, playlist, startingState) {
4572      const {
4573        uri,
4574        id
4575      } = playlist; // any in-flight request is now finished
4576
4577      this.request = null;
4578
4579      if (startingState) {
4580        this.state = startingState;
4581      }
4582
4583      this.error = {
4584        playlist: this.main.playlists[id],
4585        status: xhr.status,
4586        message: `HLS playlist request error at URL: ${uri}.`,
4587        responseText: xhr.responseText,
4588        code: xhr.status >= 500 ? 4 : 2,
4589        metadata: getStreamingNetworkErrorMetadata({
4590          requestType: xhr.requestType,
4591          request: xhr,
4592          error: xhr.error
4593        })
4594      };
4595      this.trigger('error');
4596    }
4597
4598    parseManifest_({
4599      url,
4600      manifestString
4601    }) {
4602      try {
4603        const parsed = parseManifest({
4604          onwarn: ({
4605            message
4606          }) => this.logger_(`m3u8-parser warn for ${url}: ${message}`),
4607          oninfo: ({
4608            message
4609          }) => this.logger_(`m3u8-parser info for ${url}: ${message}`),
4610          manifestString,
4611          customTagParsers: this.customTagParsers,
4612          customTagMappers: this.customTagMappers,
4613          llhls: this.llhls
4614        });
4615        /**
4616         * VHS does not support switching between variants with and without audio and video
4617         * so we want to filter out audio-only variants when variants with video and(or) audio are also detected.
4618         */
4619
4620        if (!parsed.playlists || !parsed.playlists.length) {
4621          return parsed;
4622        }
4623
4624        this.excludeAudioOnlyVariants(parsed.playlists);
4625        return parsed;
4626      } catch (error) {
4627        this.error = error;
4628        this.error.metadata = {
4629          errorType: videojs__default["default"].Error.StreamingHlsPlaylistParserError,
4630          error
4631        };
4632      }
4633    }
4634
4635    excludeAudioOnlyVariants(playlists) {
4636      // helper function
4637      const hasVideo = playlist => {
4638        const attributes = playlist.attributes || {};
4639        const {
4640          width,
4641          height
4642        } = attributes.RESOLUTION || {};
4643
4644        if (width && height) {
4645          return true;
4646        } // parse codecs string from playlist attributes
4647
4648
4649        const codecsList = getCodecs(playlist) || []; // unwrap list
4650
4651        const codecsInfo = unwrapCodecList(codecsList);
4652        return Boolean(codecsInfo.video);
4653      };
4654
4655      if (playlists.some(hasVideo)) {
4656        playlists.forEach(playlist => {
4657          if (!hasVideo(playlist)) {
4658            playlist.excludeUntil = Infinity;
4659          }
4660        });
4661      }
4662    }
4663    /**
4664     * Update the playlist loader's state in response to a new or updated playlist.
4665     *
4666     * @param {string} [playlistString]
4667     *        Playlist string (if playlistObject is not provided)
4668     * @param {Object} [playlistObject]
4669     *        Playlist object (if playlistString is not provided)
4670     * @param {string} url
4671     *        URL of playlist
4672     * @param {string} id
4673     *        ID to use for playlist
4674     */
4675
4676
4677    haveMetadata({
4678      playlistString,
4679      playlistObject,
4680      url,
4681      id
4682    }) {
4683      // any in-flight request is now finished
4684      this.request = null;
4685      this.state = 'HAVE_METADATA';
4686      const metadata = {
4687        playlistInfo: {
4688          type: 'media',
4689          uri: url
4690        }
4691      };
4692      this.trigger({
4693        type: 'playlistparsestart',
4694        metadata
4695      });
4696      const playlist = playlistObject || this.parseManifest_({
4697        url,
4698        manifestString: playlistString
4699      });
4700      playlist.lastRequest = Date.now();
4701      setupMediaPlaylist({
4702        playlist,
4703        uri: url,
4704        id
4705      }); // merge this playlist into the main manifest
4706
4707      const update = updateMain$1(this.main, playlist);
4708      this.targetDuration = playlist.partTargetDuration || playlist.targetDuration;
4709      this.pendingMedia_ = null;
4710
4711      if (update) {
4712        this.main = update;
4713        this.media_ = this.main.playlists[id];
4714      } else {
4715        this.trigger('playlistunchanged');
4716      }
4717
4718      this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update));
4719      metadata.parsedPlaylist = playlistMetadataPayload(this.main.playlists, metadata.playlistInfo.type, !this.media_.endList);
4720      this.trigger({
4721        type: 'playlistparsecomplete',
4722        metadata
4723      });
4724      this.trigger('loadedplaylist');
4725    }
4726    /**
4727      * Abort any outstanding work and clean up.
4728      */
4729
4730
4731    dispose() {
4732      this.trigger('dispose');
4733      this.stopRequest();
4734      window.clearTimeout(this.mediaUpdateTimeout);
4735      window.clearTimeout(this.finalRenditionTimeout);
4736      this.dateRangesStorage_ = new DateRangesStorage();
4737      this.off();
4738    }
4739
4740    stopRequest() {
4741      if (this.request) {
4742        const oldRequest = this.request;
4743        this.request = null;
4744        oldRequest.onreadystatechange = null;
4745        oldRequest.abort();
4746      }
4747    }
4748    /**
4749      * When called without any arguments, returns the currently
4750      * active media playlist. When called with a single argument,
4751      * triggers the playlist loader to asynchronously switch to the
4752      * specified media playlist. Calling this method while the
4753      * loader is in the HAVE_NOTHING causes an error to be emitted
4754      * but otherwise has no effect.
4755      *
4756      * @param {Object=} playlist the parsed media playlist
4757      * object to switch to
4758      * @param {boolean=} shouldDelay whether we should delay the request by half target duration
4759      *
4760      * @return {Playlist} the current loaded media
4761      */
4762
4763
4764    media(playlist, shouldDelay) {
4765      // getter
4766      if (!playlist) {
4767        return this.media_;
4768      } // setter
4769
4770
4771      if (this.state === 'HAVE_NOTHING') {
4772        throw new Error('Cannot switch media playlist from ' + this.state);
4773      } // find the playlist object if the target playlist has been
4774      // specified by URI
4775
4776
4777      if (typeof playlist === 'string') {
4778        if (!this.main.playlists[playlist]) {
4779          throw new Error('Unknown playlist URI: ' + playlist);
4780        }
4781
4782        playlist = this.main.playlists[playlist];
4783      }
4784
4785      window.clearTimeout(this.finalRenditionTimeout);
4786
4787      if (shouldDelay) {
4788        const delay = (playlist.partTargetDuration || playlist.targetDuration) / 2 * 1000 || 5 * 1000;
4789        this.finalRenditionTimeout = window.setTimeout(this.media.bind(this, playlist, false), delay);
4790        return;
4791      }
4792
4793      const startingState = this.state;
4794      const mediaChange = !this.media_ || playlist.id !== this.media_.id;
4795      const mainPlaylistRef = this.main.playlists[playlist.id]; // switch to fully loaded playlists immediately
4796
4797      if (mainPlaylistRef && mainPlaylistRef.endList || // handle the case of a playlist object (e.g., if using vhs-json with a resolved
4798      // media playlist or, for the case of demuxed audio, a resolved audio media group)
4799      playlist.endList && playlist.segments.length) {
4800        // abort outstanding playlist requests
4801        if (this.request) {
4802          this.request.onreadystatechange = null;
4803          this.request.abort();
4804          this.request = null;
4805        }
4806
4807        this.state = 'HAVE_METADATA';
4808        this.media_ = playlist; // trigger media change if the active media has been updated
4809
4810        if (mediaChange) {
4811          this.trigger('mediachanging');
4812
4813          if (startingState === 'HAVE_MAIN_MANIFEST') {
4814            // The initial playlist was a main manifest, and the first media selected was
4815            // also provided (in the form of a resolved playlist object) as part of the
4816            // source object (rather than just a URL). Therefore, since the media playlist
4817            // doesn't need to be requested, loadedmetadata won't trigger as part of the
4818            // normal flow, and needs an explicit trigger here.
4819            this.trigger('loadedmetadata');
4820          } else {
4821            this.trigger('mediachange');
4822          }
4823        }
4824
4825        return;
4826      } // We update/set the timeout here so that live playlists
4827      // that are not a media change will "start" the loader as expected.
4828      // We expect that this function will start the media update timeout
4829      // cycle again. This also prevents a playlist switch failure from
4830      // causing us to stall during live.
4831
4832
4833      this.updateMediaUpdateTimeout_(refreshDelay(playlist, true)); // switching to the active playlist is a no-op
4834
4835      if (!mediaChange) {
4836        return;
4837      }
4838
4839      this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request
4840
4841      if (this.request) {
4842        if (playlist.resolvedUri === this.request.url) {
4843          // requesting to switch to the same playlist multiple times
4844          // has no effect after the first
4845          return;
4846        }
4847
4848        this.request.onreadystatechange = null;
4849        this.request.abort();
4850        this.request = null;
4851      } // request the new playlist
4852
4853
4854      if (this.media_) {
4855        this.trigger('mediachanging');
4856      }
4857
4858      this.pendingMedia_ = playlist;
4859      const metadata = {
4860        playlistInfo: {
4861          type: 'media',
4862          uri: playlist.uri
4863        }
4864      };
4865      this.trigger({
4866        type: 'playlistrequeststart',
4867        metadata
4868      });
4869      this.request = this.vhs_.xhr({
4870        uri: playlist.resolvedUri,
4871        withCredentials: this.withCredentials,
4872        requestType: 'hls-playlist'
4873      }, (error, req) => {
4874        // disposed
4875        if (!this.request) {
4876          return;
4877        }
4878
4879        playlist.lastRequest = Date.now();
4880        playlist.resolvedUri = resolveManifestRedirect(playlist.resolvedUri, req);
4881
4882        if (error) {
4883          return this.playlistRequestError(this.request, playlist, startingState);
4884        }
4885
4886        this.trigger({
4887          type: 'playlistrequestcomplete',
4888          metadata
4889        });
4890        this.haveMetadata({
4891          playlistString: req.responseText,
4892          url: playlist.uri,
4893          id: playlist.id
4894        }); // fire loadedmetadata the first time a media playlist is loaded
4895
4896        if (startingState === 'HAVE_MAIN_MANIFEST') {
4897          this.trigger('loadedmetadata');
4898        } else {
4899          this.trigger('mediachange');
4900        }
4901      });
4902    }
4903    /**
4904     * pause loading of the playlist
4905     */
4906
4907
4908    pause() {
4909      if (this.mediaUpdateTimeout) {
4910        window.clearTimeout(this.mediaUpdateTimeout);
4911        this.mediaUpdateTimeout = null;
4912      }
4913
4914      this.stopRequest();
4915
4916      if (this.state === 'HAVE_NOTHING') {
4917        // If we pause the loader before any data has been retrieved, its as if we never
4918        // started, so reset to an unstarted state.
4919        this.started = false;
4920      } // Need to restore state now that no activity is happening
4921
4922
4923      if (this.state === 'SWITCHING_MEDIA') {
4924        // if the loader was in the process of switching media, it should either return to
4925        // HAVE_MAIN_MANIFEST or HAVE_METADATA depending on if the loader has loaded a media
4926        // playlist yet. This is determined by the existence of loader.media_
4927        if (this.media_) {
4928          this.state = 'HAVE_METADATA';
4929        } else {
4930          this.state = 'HAVE_MAIN_MANIFEST';
4931        }
4932      } else if (this.state === 'HAVE_CURRENT_METADATA') {
4933        this.state = 'HAVE_METADATA';
4934      }
4935    }
4936    /**
4937     * start loading of the playlist
4938     */
4939
4940
4941    load(shouldDelay) {
4942      if (this.mediaUpdateTimeout) {
4943        window.clearTimeout(this.mediaUpdateTimeout);
4944        this.mediaUpdateTimeout = null;
4945      }
4946
4947      const media = this.media();
4948
4949      if (shouldDelay) {
4950        const delay = media ? (media.partTargetDuration || media.targetDuration) / 2 * 1000 : 5 * 1000;
4951        this.mediaUpdateTimeout = window.setTimeout(() => {
4952          this.mediaUpdateTimeout = null;
4953          this.load();
4954        }, delay);
4955        return;
4956      }
4957
4958      if (!this.started) {
4959        this.start();
4960        return;
4961      }
4962
4963      if (media && !media.endList) {
4964        this.trigger('mediaupdatetimeout');
4965      } else {
4966        this.trigger('loadedplaylist');
4967      }
4968    }
4969
4970    updateMediaUpdateTimeout_(delay) {
4971      if (this.mediaUpdateTimeout) {
4972        window.clearTimeout(this.mediaUpdateTimeout);
4973        this.mediaUpdateTimeout = null;
4974      } // we only have use mediaupdatetimeout for live playlists.
4975
4976
4977      if (!this.media() || this.media().endList) {
4978        return;
4979      }
4980
4981      this.mediaUpdateTimeout = window.setTimeout(() => {
4982        this.mediaUpdateTimeout = null;
4983        this.trigger('mediaupdatetimeout');
4984        this.updateMediaUpdateTimeout_(delay);
4985      }, delay);
4986    }
4987    /**
4988     * start loading of the playlist
4989     */
4990
4991
4992    start() {
4993      this.started = true;
4994
4995      if (typeof this.src === 'object') {
4996        // in the case of an entirely constructed manifest object (meaning there's no actual
4997        // manifest on a server), default the uri to the page's href
4998        if (!this.src.uri) {
4999          this.src.uri = window.location.href;
5000        } // resolvedUri is added on internally after the initial request. Since there's no
5001        // request for pre-resolved manifests, add on resolvedUri here.
5002
5003
5004        this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first
5005        // request can be skipped (since the top level of the manifest, at a minimum, is
5006        // already available as a parsed manifest object). However, if the manifest object
5007        // represents a main playlist, some media playlists may need to be resolved before
5008        // the starting segment list is available. Therefore, go directly to setup of the
5009        // initial playlist, and let the normal flow continue from there.
5010        //
5011        // Note that the call to setup is asynchronous, as other sections of VHS may assume
5012        // that the first request is asynchronous.
5013
5014        setTimeout(() => {
5015          this.setupInitialPlaylist(this.src);
5016        }, 0);
5017        return;
5018      }
5019
5020      const metadata = {
5021        playlistInfo: {
5022          type: 'multivariant',
5023          uri: this.src
5024        }
5025      };
5026      this.trigger({
5027        type: 'playlistrequeststart',
5028        metadata
5029      }); // request the specified URL
5030
5031      this.request = this.vhs_.xhr({
5032        uri: this.src,
5033        withCredentials: this.withCredentials,
5034        requestType: 'hls-playlist'
5035      }, (error, req) => {
5036        // disposed
5037        if (!this.request) {
5038          return;
5039        } // clear the loader's request reference
5040
5041
5042        this.request = null;
5043
5044        if (error) {
5045          this.error = {
5046            status: req.status,
5047            message: `HLS playlist request error at URL: ${this.src}.`,
5048            responseText: req.responseText,
5049            // MEDIA_ERR_NETWORK
5050            code: 2,
5051            metadata: getStreamingNetworkErrorMetadata({
5052              requestType: req.requestType,
5053              request: req,
5054              error
5055            })
5056          };
5057
5058          if (this.state === 'HAVE_NOTHING') {
5059            this.started = false;
5060          }
5061
5062          return this.trigger('error');
5063        }
5064
5065        this.trigger({
5066          type: 'playlistrequestcomplete',
5067          metadata
5068        });
5069        this.src = resolveManifestRedirect(this.src, req);
5070        this.trigger({
5071          type: 'playlistparsestart',
5072          metadata
5073        });
5074        const manifest = this.parseManifest_({
5075          manifestString: req.responseText,
5076          url: this.src
5077        }); // we haven't loaded any variant playlists here so we default to false for isLive.
5078
5079        metadata.parsedPlaylist = playlistMetadataPayload(manifest.playlists, metadata.playlistInfo.type, false);
5080        this.trigger({
5081          type: 'playlistparsecomplete',
5082          metadata
5083        });
5084        this.setupInitialPlaylist(manifest);
5085      });
5086    }
5087
5088    srcUri() {
5089      return typeof this.src === 'string' ? this.src : this.src.uri;
5090    }
5091    /**
5092     * Given a manifest object that's either a main or media playlist, trigger the proper
5093     * events and set the state of the playlist loader.
5094     *
5095     * If the manifest object represents a main playlist, `loadedplaylist` will be
5096     * triggered to allow listeners to select a playlist. If none is selected, the loader
5097     * will default to the first one in the playlists array.
5098     *
5099     * If the manifest object represents a media playlist, `loadedplaylist` will be
5100     * triggered followed by `loadedmetadata`, as the only available playlist is loaded.
5101     *
5102     * In the case of a media playlist, a main playlist object wrapper with one playlist
5103     * will be created so that all logic can handle playlists in the same fashion (as an
5104     * assumed manifest object schema).
5105     *
5106     * @param {Object} manifest
5107     *        The parsed manifest object
5108     */
5109
5110
5111    setupInitialPlaylist(manifest) {
5112      this.state = 'HAVE_MAIN_MANIFEST';
5113
5114      if (manifest.playlists) {
5115        this.main = manifest;
5116        addPropertiesToMain(this.main, this.srcUri()); // If the initial main playlist has playlists wtih segments already resolved,
5117        // then resolve URIs in advance, as they are usually done after a playlist request,
5118        // which may not happen if the playlist is resolved.
5119
5120        manifest.playlists.forEach(playlist => {
5121          playlist.segments = getAllSegments(playlist);
5122          playlist.segments.forEach(segment => {
5123            resolveSegmentUris(segment, playlist.resolvedUri);
5124          });
5125        });
5126        this.trigger('loadedplaylist');
5127
5128        if (!this.request) {
5129          // no media playlist was specifically selected so start
5130          // from the first listed one
5131          this.media(this.main.playlists[0]);
5132        }
5133
5134        return;
5135      } // In order to support media playlists passed in as vhs-json, the case where the uri
5136      // is not provided as part of the manifest should be considered, and an appropriate
5137      // default used.
5138
5139
5140      const uri = this.srcUri() || window.location.href;
5141      this.main = mainForMedia(manifest, uri);
5142      this.haveMetadata({
5143        playlistObject: manifest,
5144        url: uri,
5145        id: this.main.playlists[0].id
5146      });
5147      this.trigger('loadedmetadata');
5148    }
5149    /**
5150     * Updates or deletes a preexisting pathway clone.
5151     * Ensures that all playlists related to the old pathway clone are
5152     * either updated or deleted.
5153     *
5154     * @param {Object} clone On update, the pathway clone object for the newly updated pathway clone.
5155     *        On delete, the old pathway clone object to be deleted.
5156     * @param {boolean} isUpdate True if the pathway is to be updated,
5157     *        false if it is meant to be deleted.
5158     */
5159
5160
5161    updateOrDeleteClone(clone, isUpdate) {
5162      const main = this.main;
5163      const pathway = clone.ID;
5164      let i = main.playlists.length; // Iterate backwards through the playlist so we can remove playlists if necessary.
5165
5166      while (i--) {
5167        const p = main.playlists[i];
5168
5169        if (p.attributes['PATHWAY-ID'] === pathway) {
5170          const oldPlaylistUri = p.resolvedUri;
5171          const oldPlaylistId = p.id; // update the indexed playlist and add new playlists by ID and URI
5172
5173          if (isUpdate) {
5174            const newPlaylistUri = this.createCloneURI_(p.resolvedUri, clone);
5175            const newPlaylistId = createPlaylistID(pathway, newPlaylistUri);
5176            const attributes = this.createCloneAttributes_(pathway, p.attributes);
5177            const updatedPlaylist = this.createClonePlaylist_(p, newPlaylistId, clone, attributes);
5178            main.playlists[i] = updatedPlaylist;
5179            main.playlists[newPlaylistId] = updatedPlaylist;
5180            main.playlists[newPlaylistUri] = updatedPlaylist;
5181          } else {
5182            // Remove the indexed playlist.
5183            main.playlists.splice(i, 1);
5184          } // Remove playlists by the old ID and URI.
5185
5186
5187          delete main.playlists[oldPlaylistId];
5188          delete main.playlists[oldPlaylistUri];
5189        }
5190      }
5191
5192      this.updateOrDeleteCloneMedia(clone, isUpdate);
5193    }
5194    /**
5195     * Updates or deletes media data based on the pathway clone object.
5196     * Due to the complexity of the media groups and playlists, in all cases
5197     * we remove all of the old media groups and playlists.
5198     * On updates, we then create new media groups and playlists based on the
5199     * new pathway clone object.
5200     *
5201     * @param {Object} clone The pathway clone object for the newly updated pathway clone.
5202     * @param {boolean} isUpdate True if the pathway is to be updated,
5203     *        false if it is meant to be deleted.
5204     */
5205
5206
5207    updateOrDeleteCloneMedia(clone, isUpdate) {
5208      const main = this.main;
5209      const id = clone.ID;
5210      ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => {
5211        if (!main.mediaGroups[mediaType] || !main.mediaGroups[mediaType][id]) {
5212          return;
5213        }
5214
5215        for (const groupKey in main.mediaGroups[mediaType]) {
5216          // Remove all media playlists for the media group for this pathway clone.
5217          if (groupKey === id) {
5218            for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
5219              const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey];
5220              oldMedia.playlists.forEach((p, i) => {
5221                const oldMediaPlaylist = main.playlists[p.id];
5222                const oldPlaylistId = oldMediaPlaylist.id;
5223                const oldPlaylistUri = oldMediaPlaylist.resolvedUri;
5224                delete main.playlists[oldPlaylistId];
5225                delete main.playlists[oldPlaylistUri];
5226              });
5227            } // Delete the old media group.
5228
5229
5230            delete main.mediaGroups[mediaType][groupKey];
5231          }
5232        }
5233      }); // Create the new media groups and playlists if there is an update.
5234
5235      if (isUpdate) {
5236        this.createClonedMediaGroups_(clone);
5237      }
5238    }
5239    /**
5240     * Given a pathway clone object, clones all necessary playlists.
5241     *
5242     * @param {Object} clone The pathway clone object.
5243     * @param {Object} basePlaylist The original playlist to clone from.
5244     */
5245
5246
5247    addClonePathway(clone, basePlaylist = {}) {
5248      const main = this.main;
5249      const index = main.playlists.length;
5250      const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone);
5251      const playlistId = createPlaylistID(clone.ID, uri);
5252      const attributes = this.createCloneAttributes_(clone.ID, basePlaylist.attributes);
5253      const playlist = this.createClonePlaylist_(basePlaylist, playlistId, clone, attributes);
5254      main.playlists[index] = playlist; // add playlist by ID and URI
5255
5256      main.playlists[playlistId] = playlist;
5257      main.playlists[uri] = playlist;
5258      this.createClonedMediaGroups_(clone);
5259    }
5260    /**
5261     * Given a pathway clone object we create clones of all media.
5262     * In this function, all necessary information and updated playlists
5263     * are added to the `mediaGroup` object.
5264     * Playlists are also added to the `playlists` array so the media groups
5265     * will be properly linked.
5266     *
5267     * @param {Object} clone The pathway clone object.
5268     */
5269
5270
5271    createClonedMediaGroups_(clone) {
5272      const id = clone.ID;
5273      const baseID = clone['BASE-ID'];
5274      const main = this.main;
5275      ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => {
5276        // If the media type doesn't exist, or there is already a clone, skip
5277        // to the next media type.
5278        if (!main.mediaGroups[mediaType] || main.mediaGroups[mediaType][id]) {
5279          return;
5280        }
5281
5282        for (const groupKey in main.mediaGroups[mediaType]) {
5283          if (groupKey === baseID) {
5284            // Create the group.
5285            main.mediaGroups[mediaType][id] = {};
5286          } else {
5287            // There is no need to iterate over label keys in this case.
5288            continue;
5289          }
5290
5291          for (const labelKey in main.mediaGroups[mediaType][groupKey]) {
5292            const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey];
5293            main.mediaGroups[mediaType][id][labelKey] = _extends({}, oldMedia);
5294            const newMedia = main.mediaGroups[mediaType][id][labelKey]; // update URIs on the media
5295
5296            const newUri = this.createCloneURI_(oldMedia.resolvedUri, clone);
5297            newMedia.resolvedUri = newUri;
5298            newMedia.uri = newUri; // Reset playlists in the new media group.
5299
5300            newMedia.playlists = []; // Create new playlists in the newly cloned media group.
5301
5302            oldMedia.playlists.forEach((p, i) => {
5303              const oldMediaPlaylist = main.playlists[p.id];
5304              const group = groupID(mediaType, id, labelKey);
5305              const newPlaylistID = createPlaylistID(id, group); // Check to see if it already exists
5306
5307              if (oldMediaPlaylist && !main.playlists[newPlaylistID]) {
5308                const newMediaPlaylist = this.createClonePlaylist_(oldMediaPlaylist, newPlaylistID, clone);
5309                const newPlaylistUri = newMediaPlaylist.resolvedUri;
5310                main.playlists[newPlaylistID] = newMediaPlaylist;
5311                main.playlists[newPlaylistUri] = newMediaPlaylist;
5312              }
5313
5314              newMedia.playlists[i] = this.createClonePlaylist_(p, newPlaylistID, clone);
5315            });
5316          }
5317        }
5318      });
5319    }
5320    /**
5321     * Using the original playlist to be cloned, and the pathway clone object
5322     * information, we create a new playlist.
5323     *
5324     * @param {Object} basePlaylist  The original playlist to be cloned from.
5325     * @param {string} id The desired id of the newly cloned playlist.
5326     * @param {Object} clone The pathway clone object.
5327     * @param {Object} attributes An optional object to populate the `attributes` property in the playlist.
5328     *
5329     * @return {Object} The combined cloned playlist.
5330     */
5331
5332
5333    createClonePlaylist_(basePlaylist, id, clone, attributes) {
5334      const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone);
5335      const newProps = {
5336        resolvedUri: uri,
5337        uri,
5338        id
5339      }; // Remove all segments from previous playlist in the clone.
5340
5341      if (basePlaylist.segments) {
5342        newProps.segments = [];
5343      }
5344
5345      if (attributes) {
5346        newProps.attributes = attributes;
5347      }
5348
5349      return merge$1(basePlaylist, newProps);
5350    }
5351    /**
5352     * Generates an updated URI for a cloned pathway based on the original
5353     * pathway's URI and the paramaters from the pathway clone object in the
5354     * content steering server response.
5355     *
5356     * @param {string} baseUri URI to be updated in the cloned pathway.
5357     * @param {Object} clone The pathway clone object.
5358     *
5359     * @return {string} The updated URI for the cloned pathway.
5360     */
5361
5362
5363    createCloneURI_(baseURI, clone) {
5364      const uri = new URL(baseURI);
5365      uri.hostname = clone['URI-REPLACEMENT'].HOST;
5366      const params = clone['URI-REPLACEMENT'].PARAMS; // Add params to the cloned URL.
5367
5368      for (const key of Object.keys(params)) {
5369        uri.searchParams.set(key, params[key]);
5370      }
5371
5372      return uri.href;
5373    }
5374    /**
5375     * Helper function to create the attributes needed for the new clone.
5376     * This mainly adds the necessary media attributes.
5377     *
5378     * @param {string} id The pathway clone object ID.
5379     * @param {Object} oldAttributes The old attributes to compare to.
5380     * @return {Object} The new attributes to add to the playlist.
5381     */
5382
5383
5384    createCloneAttributes_(id, oldAttributes) {
5385      const attributes = {
5386        ['PATHWAY-ID']: id
5387      };
5388      ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => {
5389        if (oldAttributes[mediaType]) {
5390          attributes[mediaType] = id;
5391        }
5392      });
5393      return attributes;
5394    }
5395    /**
5396     * Returns the key ID set from a playlist
5397     *
5398     * @param {playlist} playlist to fetch the key ID set from.
5399     * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist.
5400     */
5401
5402
5403    getKeyIdSet(playlist) {
5404      const keyIds = new Set();
5405
5406      if (!playlist || !playlist.contentProtection) {
5407        return keyIds;
5408      }
5409
5410      for (const keysystem in playlist.contentProtection) {
5411        if (playlist.contentProtection[keysystem] && playlist.contentProtection[keysystem].attributes && playlist.contentProtection[keysystem].attributes.keyId) {
5412          const keyId = playlist.contentProtection[keysystem].attributes.keyId;
5413          keyIds.add(keyId.toLowerCase());
5414        }
5415      }
5416
5417      return keyIds;
5418    }
5419
5420  }
5421
5422  /**
5423   * @file xhr.js
5424   */
5425
5426  const callbackWrapper = function (request, error, response, callback) {
5427    const reqResponse = request.responseType === 'arraybuffer' ? request.response : request.responseText;
5428
5429    if (!error && reqResponse) {
5430      request.responseTime = Date.now();
5431      request.roundTripTime = request.responseTime - request.requestTime;
5432      request.bytesReceived = reqResponse.byteLength || reqResponse.length;
5433
5434      if (!request.bandwidth) {
5435        request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000);
5436      }
5437    }
5438
5439    if (response.headers) {
5440      request.responseHeaders = response.headers;
5441    } // videojs.xhr now uses a specific code on the error
5442    // object to signal that a request has timed out instead
5443    // of setting a boolean on the request object
5444
5445
5446    if (error && error.code === 'ETIMEDOUT') {
5447      request.timedout = true;
5448    } // videojs.xhr no longer considers status codes outside of 200 and 0
5449    // (for file uris) to be errors, but the old XHR did, so emulate that
5450    // behavior. Status 206 may be used in response to byterange requests.
5451
5452
5453    if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) {
5454      error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText)));
5455    }
5456
5457    callback(error, request);
5458  };
5459  /**
5460   * Iterates over the request hooks Set and calls them in order
5461   *
5462   * @param {Set} hooks the hook Set to iterate over
5463   * @param {Object} options the request options to pass to the xhr wrapper
5464   * @return the callback hook function return value, the modified or new options Object.
5465   */
5466
5467
5468  const callAllRequestHooks = (requestSet, options) => {
5469    if (!requestSet || !requestSet.size) {
5470      return;
5471    }
5472
5473    let newOptions = options;
5474    requestSet.forEach(requestCallback => {
5475      newOptions = requestCallback(newOptions);
5476    });
5477    return newOptions;
5478  };
5479  /**
5480   * Iterates over the response hooks Set and calls them in order.
5481   *
5482   * @param {Set} hooks the hook Set to iterate over
5483   * @param {Object} request the xhr request object
5484   * @param {Object} error the xhr error object
5485   * @param {Object} response the xhr response object
5486   */
5487
5488
5489  const callAllResponseHooks = (responseSet, request, error, response) => {
5490    if (!responseSet || !responseSet.size) {
5491      return;
5492    }
5493
5494    responseSet.forEach(responseCallback => {
5495      responseCallback(request, error, response);
5496    });
5497  };
5498
5499  const xhrFactory = function () {
5500    const xhr = function XhrFunction(options, callback) {
5501      // Add a default timeout
5502      options = merge$1({
5503        timeout: 45e3
5504      }, options); // Allow an optional user-specified function to modify the option
5505      // object before we construct the xhr request
5506      // TODO: Remove beforeRequest in the next major release.
5507
5508      const beforeRequest = XhrFunction.beforeRequest || videojs__default["default"].Vhs.xhr.beforeRequest; // onRequest and onResponse hooks as a Set, at either the player or global level.
5509      // TODO: new Set added here for beforeRequest alias. Remove this when beforeRequest is removed.
5510
5511      const _requestCallbackSet = XhrFunction._requestCallbackSet || videojs__default["default"].Vhs.xhr._requestCallbackSet || new Set();
5512
5513      const _responseCallbackSet = XhrFunction._responseCallbackSet || videojs__default["default"].Vhs.xhr._responseCallbackSet;
5514
5515      if (beforeRequest && typeof beforeRequest === 'function') {
5516        videojs__default["default"].log.warn('beforeRequest is deprecated, use onRequest instead.');
5517
5518        _requestCallbackSet.add(beforeRequest);
5519      } // Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden
5520      // TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11
5521
5522
5523      const xhrMethod = videojs__default["default"].Vhs.xhr.original === true ? videojs__default["default"].xhr : videojs__default["default"].Vhs.xhr; // call all registered onRequest hooks, assign new options.
5524
5525      const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options); // Remove the beforeRequest function from the hooks set so stale beforeRequest functions are not called.
5526
5527      _requestCallbackSet.delete(beforeRequest); // xhrMethod will call XMLHttpRequest.open and XMLHttpRequest.send
5528
5529
5530      const request = xhrMethod(beforeRequestOptions || options, function (error, response) {
5531        // call all registered onResponse hooks
5532        callAllResponseHooks(_responseCallbackSet, request, error, response);
5533        return callbackWrapper(request, error, response, callback);
5534      });
5535      const originalAbort = request.abort;
5536
5537      request.abort = function () {
5538        request.aborted = true;
5539        return originalAbort.apply(request, arguments);
5540      };
5541
5542      request.uri = options.uri;
5543      request.requestType = options.requestType;
5544      request.requestTime = Date.now();
5545      return request;
5546    };
5547
5548    xhr.original = true;
5549    return xhr;
5550  };
5551  /**
5552   * Turns segment byterange into a string suitable for use in
5553   * HTTP Range requests
5554   *
5555   * @param {Object} byterange - an object with two values defining the start and end
5556   *                             of a byte-range
5557   */
5558
5559
5560  const byterangeStr = function (byterange) {
5561    // `byterangeEnd` is one less than `offset + length` because the HTTP range
5562    // header uses inclusive ranges
5563    let byterangeEnd;
5564    const byterangeStart = byterange.offset;
5565
5566    if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
5567      byterangeEnd = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
5568    } else {
5569      byterangeEnd = byterange.offset + byterange.length - 1;
5570    }
5571
5572    return 'bytes=' + byterangeStart + '-' + byterangeEnd;
5573  };
5574  /**
5575   * Defines headers for use in the xhr request for a particular segment.
5576   *
5577   * @param {Object} segment - a simplified copy of the segmentInfo object
5578   *                           from SegmentLoader
5579   */
5580
5581  const segmentXhrHeaders = function (segment) {
5582    const headers = {};
5583
5584    if (segment.byterange) {
5585      headers.Range = byterangeStr(segment.byterange);
5586    }
5587
5588    return headers;
5589  };
5590
5591  var MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
5592  var DASH_REGEX = /^application\/dash\+xml/i;
5593  /**
5594   * Returns a string that describes the type of source based on a video source object's
5595   * media type.
5596   *
5597   * @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type}
5598   *
5599   * @param {string} type
5600   *        Video source object media type
5601   * @return {('hls'|'dash'|'vhs-json'|null)}
5602   *         VHS source type string
5603   */
5604
5605  var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) {
5606    if (MPEGURL_REGEX.test(type)) {
5607      return 'hls';
5608    }
5609
5610    if (DASH_REGEX.test(type)) {
5611      return 'dash';
5612    } // Denotes the special case of a manifest object passed to http-streaming instead of a
5613    // source URL.
5614    //
5615    // See https://en.wikipedia.org/wiki/Media_type for details on specifying media types.
5616    //
5617    // In this case, vnd stands for vendor, video.js for the organization, VHS for this
5618    // project, and the +json suffix identifies the structure of the media type.
5619
5620
5621    if (type === 'application/vnd.videojs.vhs+json') {
5622      return 'vhs-json';
5623    }
5624
5625    return null;
5626  };
5627
5628  // const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2));
5629  // we used to do this with log2 but BigInt does not support builtin math
5630  // Math.ceil(log2(x));
5631
5632
5633  var countBits = function countBits(x) {
5634    return x.toString(2).length;
5635  }; // count the number of whole bytes it would take to represent a number
5636
5637  var countBytes = function countBytes(x) {
5638    return Math.ceil(countBits(x) / 8);
5639  };
5640  var isArrayBufferView = function isArrayBufferView(obj) {
5641    if (ArrayBuffer.isView === 'function') {
5642      return ArrayBuffer.isView(obj);
5643    }
5644
5645    return obj && obj.buffer instanceof ArrayBuffer;
5646  };
5647  var isTypedArray = function isTypedArray(obj) {
5648    return isArrayBufferView(obj);
5649  };
5650  var toUint8 = function toUint8(bytes) {
5651    if (bytes instanceof Uint8Array) {
5652      return bytes;
5653    }
5654
5655    if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) {
5656      // any non-number or NaN leads to empty uint8array
5657      // eslint-disable-next-line
5658      if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) {
5659        bytes = 0;
5660      } else {
5661        bytes = [bytes];
5662      }
5663    }
5664
5665    return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0);
5666  };
5667  var BigInt = window.BigInt || Number;
5668  var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
5669  (function () {
5670    var a = new Uint16Array([0xFFCC]);
5671    var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
5672
5673    if (b[0] === 0xFF) {
5674      return 'big';
5675    }
5676
5677    if (b[0] === 0xCC) {
5678      return 'little';
5679    }
5680
5681    return 'unknown';
5682  })();
5683  var bytesToNumber = function bytesToNumber(bytes, _temp) {
5684    var _ref = _temp === void 0 ? {} : _temp,
5685        _ref$signed = _ref.signed,
5686        signed = _ref$signed === void 0 ? false : _ref$signed,
5687        _ref$le = _ref.le,
5688        le = _ref$le === void 0 ? false : _ref$le;
5689
5690    bytes = toUint8(bytes);
5691    var fn = le ? 'reduce' : 'reduceRight';
5692    var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn];
5693    var number = obj.call(bytes, function (total, byte, i) {
5694      var exponent = le ? i : Math.abs(i + 1 - bytes.length);
5695      return total + BigInt(byte) * BYTE_TABLE[exponent];
5696    }, BigInt(0));
5697
5698    if (signed) {
5699      var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1);
5700      number = BigInt(number);
5701
5702      if (number > max) {
5703        number -= max;
5704        number -= max;
5705        number -= BigInt(2);
5706      }
5707    }
5708
5709    return Number(number);
5710  };
5711  var numberToBytes = function numberToBytes(number, _temp2) {
5712    var _ref2 = _temp2 === void 0 ? {} : _temp2,
5713        _ref2$le = _ref2.le,
5714        le = _ref2$le === void 0 ? false : _ref2$le; // eslint-disable-next-line
5715
5716
5717    if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) {
5718      number = 0;
5719    }
5720
5721    number = BigInt(number);
5722    var byteCount = countBytes(number);
5723    var bytes = new Uint8Array(new ArrayBuffer(byteCount));
5724
5725    for (var i = 0; i < byteCount; i++) {
5726      var byteIndex = le ? i : Math.abs(i + 1 - bytes.length);
5727      bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF));
5728
5729      if (number < 0) {
5730        bytes[byteIndex] = Math.abs(~bytes[byteIndex]);
5731        bytes[byteIndex] -= i === 0 ? 1 : 2;
5732      }
5733    }
5734
5735    return bytes;
5736  };
5737  var stringToBytes = function stringToBytes(string, stringIsBytes) {
5738    if (typeof string !== 'string' && string && typeof string.toString === 'function') {
5739      string = string.toString();
5740    }
5741
5742    if (typeof string !== 'string') {
5743      return new Uint8Array();
5744    } // If the string already is bytes, we don't have to do this
5745    // otherwise we do this so that we split multi length characters
5746    // into individual bytes
5747
5748
5749    if (!stringIsBytes) {
5750      string = unescape(encodeURIComponent(string));
5751    }
5752
5753    var view = new Uint8Array(string.length);
5754
5755    for (var i = 0; i < string.length; i++) {
5756      view[i] = string.charCodeAt(i);
5757    }
5758
5759    return view;
5760  };
5761  var concatTypedArrays = function concatTypedArrays() {
5762    for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) {
5763      buffers[_key] = arguments[_key];
5764    }
5765
5766    buffers = buffers.filter(function (b) {
5767      return b && (b.byteLength || b.length) && typeof b !== 'string';
5768    });
5769
5770    if (buffers.length <= 1) {
5771      // for 0 length we will return empty uint8
5772      // for 1 length we return the first uint8
5773      return toUint8(buffers[0]);
5774    }
5775
5776    var totalLen = buffers.reduce(function (total, buf, i) {
5777      return total + (buf.byteLength || buf.length);
5778    }, 0);
5779    var tempBuffer = new Uint8Array(totalLen);
5780    var offset = 0;
5781    buffers.forEach(function (buf) {
5782      buf = toUint8(buf);
5783      tempBuffer.set(buf, offset);
5784      offset += buf.byteLength;
5785    });
5786    return tempBuffer;
5787  };
5788  /**
5789   * Check if the bytes "b" are contained within bytes "a".
5790   *
5791   * @param {Uint8Array|Array} a
5792   *        Bytes to check in
5793   *
5794   * @param {Uint8Array|Array} b
5795   *        Bytes to check for
5796   *
5797   * @param {Object} options
5798   *        options
5799   *
5800   * @param {Array|Uint8Array} [offset=0]
5801   *        offset to use when looking at bytes in a
5802   *
5803   * @param {Array|Uint8Array} [mask=[]]
5804   *        mask to use on bytes before comparison.
5805   *
5806   * @return {boolean}
5807   *         If all bytes in b are inside of a, taking into account
5808   *         bit masks.
5809   */
5810
5811  var bytesMatch = function bytesMatch(a, b, _temp3) {
5812    var _ref3 = _temp3 === void 0 ? {} : _temp3,
5813        _ref3$offset = _ref3.offset,
5814        offset = _ref3$offset === void 0 ? 0 : _ref3$offset,
5815        _ref3$mask = _ref3.mask,
5816        mask = _ref3$mask === void 0 ? [] : _ref3$mask;
5817
5818    a = toUint8(a);
5819    b = toUint8(b); // ie 11 does not support uint8 every
5820
5821    var fn = b.every ? b.every : Array.prototype.every;
5822    return b.length && a.length - offset >= b.length && // ie 11 doesn't support every on uin8
5823    fn.call(b, function (bByte, i) {
5824      var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i];
5825      return bByte === aByte;
5826    });
5827  };
5828
5829  /**
5830   * @file bin-utils.js
5831   */
5832
5833  /**
5834   * convert a TimeRange to text
5835   *
5836   * @param {TimeRange} range the timerange to use for conversion
5837   * @param {number} i the iterator on the range to convert
5838   * @return {string} the range in string format
5839   */
5840
5841  const textRange = function (range, i) {
5842    return range.start(i) + '-' + range.end(i);
5843  };
5844  /**
5845   * format a number as hex string
5846   *
5847   * @param {number} e The number
5848   * @param {number} i the iterator
5849   * @return {string} the hex formatted number as a string
5850   */
5851
5852
5853  const formatHexString = function (e, i) {
5854    const value = e.toString(16);
5855    return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
5856  };
5857
5858  const formatAsciiString = function (e) {
5859    if (e >= 0x20 && e < 0x7e) {
5860      return String.fromCharCode(e);
5861    }
5862
5863    return '.';
5864  };
5865  /**
5866   * Creates an object for sending to a web worker modifying properties that are TypedArrays
5867   * into a new object with seperated properties for the buffer, byteOffset, and byteLength.
5868   *
5869   * @param {Object} message
5870   *        Object of properties and values to send to the web worker
5871   * @return {Object}
5872   *         Modified message with TypedArray values expanded
5873   * @function createTransferableMessage
5874   */
5875
5876
5877  const createTransferableMessage = function (message) {
5878    const transferable = {};
5879    Object.keys(message).forEach(key => {
5880      const value = message[key];
5881
5882      if (isArrayBufferView(value)) {
5883        transferable[key] = {
5884          bytes: value.buffer,
5885          byteOffset: value.byteOffset,
5886          byteLength: value.byteLength
5887        };
5888      } else {
5889        transferable[key] = value;
5890      }
5891    });
5892    return transferable;
5893  };
5894  /**
5895   * Returns a unique string identifier for a media initialization
5896   * segment.
5897   *
5898   * @param {Object} initSegment
5899   *        the init segment object.
5900   *
5901   * @return {string} the generated init segment id
5902   */
5903
5904  const initSegmentId = function (initSegment) {
5905    const byterange = initSegment.byterange || {
5906      length: Infinity,
5907      offset: 0
5908    };
5909    return [byterange.length, byterange.offset, initSegment.resolvedUri].join(',');
5910  };
5911  /**
5912   * Returns a unique string identifier for a media segment key.
5913   *
5914   * @param {Object} key the encryption key
5915   * @return {string} the unique id for the media segment key.
5916   */
5917
5918  const segmentKeyId = function (key) {
5919    return key.resolvedUri;
5920  };
5921  /**
5922   * utils to help dump binary data to the console
5923   *
5924   * @param {Array|TypedArray} data
5925   *        data to dump to a string
5926   *
5927   * @return {string} the data as a hex string.
5928   */
5929
5930  const hexDump = data => {
5931    const bytes = Array.prototype.slice.call(data);
5932    const step = 16;
5933    let result = '';
5934    let hex;
5935    let ascii;
5936
5937    for (let j = 0; j < bytes.length / step; j++) {
5938      hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
5939      ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
5940      result += hex + ' ' + ascii + '\n';
5941    }
5942
5943    return result;
5944  };
5945  const tagDump = ({
5946    bytes
5947  }) => hexDump(bytes);
5948  const textRanges = ranges => {
5949    let result = '';
5950    let i;
5951
5952    for (i = 0; i < ranges.length; i++) {
5953      result += textRange(ranges, i) + ' ';
5954    }
5955
5956    return result;
5957  };
5958
5959  var utils = /*#__PURE__*/Object.freeze({
5960    __proto__: null,
5961    createTransferableMessage: createTransferableMessage,
5962    initSegmentId: initSegmentId,
5963    segmentKeyId: segmentKeyId,
5964    hexDump: hexDump,
5965    tagDump: tagDump,
5966    textRanges: textRanges
5967  });
5968
5969  // TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux
5970  // 25% was arbitrarily chosen, and may need to be refined over time.
5971
5972  const SEGMENT_END_FUDGE_PERCENT = 0.25;
5973  /**
5974   * Converts a player time (any time that can be gotten/set from player.currentTime(),
5975   * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a
5976   * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)).
5977   *
5978   * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor
5979   * point" (a point where we have a mapping from program time to player time, with player
5980   * time being the post transmux start of the segment).
5981   *
5982   * For more details, see [this doc](../../docs/program-time-from-player-time.md).
5983   *
5984   * @param {number} playerTime the player time
5985   * @param {Object} segment the segment which contains the player time
5986   * @return {Date} program time
5987   */
5988
5989  const playerTimeToProgramTime = (playerTime, segment) => {
5990    if (!segment.dateTimeObject) {
5991      // Can't convert without an "anchor point" for the program time (i.e., a time that can
5992      // be used to map the start of a segment with a real world time).
5993      return null;
5994    }
5995
5996    const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds;
5997    const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended
5998
5999    const startOfSegment = transmuxedStart + transmuxerPrependedSeconds;
6000    const offsetFromSegmentStart = playerTime - startOfSegment;
6001    return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000);
6002  };
6003  const originalSegmentVideoDuration = videoTimingInfo => {
6004    return videoTimingInfo.transmuxedPresentationEnd - videoTimingInfo.transmuxedPresentationStart - videoTimingInfo.transmuxerPrependedSeconds;
6005  };
6006  /**
6007   * Finds a segment that contains the time requested given as an ISO-8601 string. The
6008   * returned segment might be an estimate or an accurate match.
6009   *
6010   * @param {string} programTime The ISO-8601 programTime to find a match for
6011   * @param {Object} playlist A playlist object to search within
6012   */
6013
6014  const findSegmentForProgramTime = (programTime, playlist) => {
6015    // Assumptions:
6016    //  - verifyProgramDateTimeTags has already been run
6017    //  - live streams have been started
6018    let dateTimeObject;
6019
6020    try {
6021      dateTimeObject = new Date(programTime);
6022    } catch (e) {
6023      return null;
6024    }
6025
6026    if (!playlist || !playlist.segments || playlist.segments.length === 0) {
6027      return null;
6028    }
6029
6030    let segment = playlist.segments[0];
6031
6032    if (dateTimeObject < new Date(segment.dateTimeObject)) {
6033      // Requested time is before stream start.
6034      return null;
6035    }
6036
6037    for (let i = 0; i < playlist.segments.length - 1; i++) {
6038      segment = playlist.segments[i];
6039      const nextSegmentStart = new Date(playlist.segments[i + 1].dateTimeObject);
6040
6041      if (dateTimeObject < nextSegmentStart) {
6042        break;
6043      }
6044    }
6045
6046    const lastSegment = playlist.segments[playlist.segments.length - 1];
6047    const lastSegmentStart = lastSegment.dateTimeObject;
6048    const lastSegmentDuration = lastSegment.videoTimingInfo ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT;
6049    const lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000);
6050
6051    if (dateTimeObject > lastSegmentEnd) {
6052      // Beyond the end of the stream, or our best guess of the end of the stream.
6053      return null;
6054    }
6055
6056    if (dateTimeObject > new Date(lastSegmentStart)) {
6057      segment = lastSegment;
6058    }
6059
6060    return {
6061      segment,
6062      estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)),
6063      // Although, given that all segments have accurate date time objects, the segment
6064      // selected should be accurate, unless the video has been transmuxed at some point
6065      // (determined by the presence of the videoTimingInfo object), the segment's "player
6066      // time" (the start time in the player) can't be considered accurate.
6067      type: segment.videoTimingInfo ? 'accurate' : 'estimate'
6068    };
6069  };
6070  /**
6071   * Finds a segment that contains the given player time(in seconds).
6072   *
6073   * @param {number} time The player time to find a match for
6074   * @param {Object} playlist A playlist object to search within
6075   */
6076
6077  const findSegmentForPlayerTime = (time, playlist) => {
6078    // Assumptions:
6079    // - there will always be a segment.duration
6080    // - we can start from zero
6081    // - segments are in time order
6082    if (!playlist || !playlist.segments || playlist.segments.length === 0) {
6083      return null;
6084    }
6085
6086    let segmentEnd = 0;
6087    let segment;
6088
6089    for (let i = 0; i < playlist.segments.length; i++) {
6090      segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and
6091      // should contain the most accurate values we have for the segment's player times.
6092      //
6093      // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall
6094      // back to an estimate based on the manifest derived (inaccurate) segment.duration, to
6095      // calculate an end value.
6096
6097      segmentEnd = segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration;
6098
6099      if (time <= segmentEnd) {
6100        break;
6101      }
6102    }
6103
6104    const lastSegment = playlist.segments[playlist.segments.length - 1];
6105
6106    if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) {
6107      // The time requested is beyond the stream end.
6108      return null;
6109    }
6110
6111    if (time > segmentEnd) {
6112      // The time is within or beyond the last segment.
6113      //
6114      // Check to see if the time is beyond a reasonable guess of the end of the stream.
6115      if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) {
6116        // Technically, because the duration value is only an estimate, the time may still
6117        // exist in the last segment, however, there isn't enough information to make even
6118        // a reasonable estimate.
6119        return null;
6120      }
6121
6122      segment = lastSegment;
6123    }
6124
6125    return {
6126      segment,
6127      estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration,
6128      // Because videoTimingInfo is only set after transmux, it is the only way to get
6129      // accurate timing values.
6130      type: segment.videoTimingInfo ? 'accurate' : 'estimate'
6131    };
6132  };
6133  /**
6134   * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds.
6135   * If the offset returned is positive, the programTime occurs after the
6136   * comparisonTimestamp.
6137   * If the offset is negative, the programTime occurs before the comparisonTimestamp.
6138   *
6139   * @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against
6140   * @param {string} programTime The programTime as an ISO-8601 string
6141   * @return {number} offset
6142   */
6143
6144  const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => {
6145    let segmentDateTime;
6146    let programDateTime;
6147
6148    try {
6149      segmentDateTime = new Date(comparisonTimeStamp);
6150      programDateTime = new Date(programTime);
6151    } catch (e) {// TODO handle error
6152    }
6153
6154    const segmentTimeEpoch = segmentDateTime.getTime();
6155    const programTimeEpoch = programDateTime.getTime();
6156    return (programTimeEpoch - segmentTimeEpoch) / 1000;
6157  };
6158  /**
6159   * Checks that all segments in this playlist have programDateTime tags.
6160   *
6161   * @param {Object} playlist A playlist object
6162   */
6163
6164  const verifyProgramDateTimeTags = playlist => {
6165    if (!playlist.segments || playlist.segments.length === 0) {
6166      return false;
6167    }
6168
6169    for (let i = 0; i < playlist.segments.length; i++) {
6170      const segment = playlist.segments[i];
6171
6172      if (!segment.dateTimeObject) {
6173        return false;
6174      }
6175    }
6176
6177    return true;
6178  };
6179  /**
6180   * Returns the programTime of the media given a playlist and a playerTime.
6181   * The playlist must have programDateTime tags for a programDateTime tag to be returned.
6182   * If the segments containing the time requested have not been buffered yet, an estimate
6183   * may be returned to the callback.
6184   *
6185   * @param {Object} args
6186   * @param {Object} args.playlist A playlist object to search within
6187   * @param {number} time A playerTime in seconds
6188   * @param {Function} callback(err, programTime)
6189   * @return {string} err.message A detailed error message
6190   * @return {Object} programTime
6191   * @return {number} programTime.mediaSeconds The streamTime in seconds
6192   * @return {string} programTime.programDateTime The programTime as an ISO-8601 String
6193   */
6194
6195  const getProgramTime = ({
6196    playlist,
6197    time = undefined,
6198    callback
6199  }) => {
6200    if (!callback) {
6201      throw new Error('getProgramTime: callback must be provided');
6202    }
6203
6204    if (!playlist || time === undefined) {
6205      return callback({
6206        message: 'getProgramTime: playlist and time must be provided'
6207      });
6208    }
6209
6210    const matchedSegment = findSegmentForPlayerTime(time, playlist);
6211
6212    if (!matchedSegment) {
6213      return callback({
6214        message: 'valid programTime was not found'
6215      });
6216    }
6217
6218    if (matchedSegment.type === 'estimate') {
6219      return callback({
6220        message: 'Accurate programTime could not be determined.' + ' Please seek to e.seekTime and try again',
6221        seekTime: matchedSegment.estimatedStart
6222      });
6223    }
6224
6225    const programTimeObject = {
6226      mediaSeconds: time
6227    };
6228    const programTime = playerTimeToProgramTime(time, matchedSegment.segment);
6229
6230    if (programTime) {
6231      programTimeObject.programDateTime = programTime.toISOString();
6232    }
6233
6234    return callback(null, programTimeObject);
6235  };
6236  /**
6237   * Seeks in the player to a time that matches the given programTime ISO-8601 string.
6238   *
6239   * @param {Object} args
6240   * @param {string} args.programTime A programTime to seek to as an ISO-8601 String
6241   * @param {Object} args.playlist A playlist to look within
6242   * @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2.
6243   * @param {Function} args.seekTo A method to perform a seek
6244   * @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true.
6245   * @param {Object} args.tech The tech to seek on
6246   * @param {Function} args.callback(err, newTime) A callback to return the new time to
6247   * @return {string} err.message A detailed error message
6248   * @return {number} newTime The exact time that was seeked to in seconds
6249   */
6250
6251  const seekToProgramTime = ({
6252    programTime,
6253    playlist,
6254    retryCount = 2,
6255    seekTo,
6256    pauseAfterSeek = true,
6257    tech,
6258    callback
6259  }) => {
6260    if (!callback) {
6261      throw new Error('seekToProgramTime: callback must be provided');
6262    }
6263
6264    if (typeof programTime === 'undefined' || !playlist || !seekTo) {
6265      return callback({
6266        message: 'seekToProgramTime: programTime, seekTo and playlist must be provided'
6267      });
6268    }
6269
6270    if (!playlist.endList && !tech.hasStarted_) {
6271      return callback({
6272        message: 'player must be playing a live stream to start buffering'
6273      });
6274    }
6275
6276    if (!verifyProgramDateTimeTags(playlist)) {
6277      return callback({
6278        message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri
6279      });
6280    }
6281
6282    const matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match
6283
6284    if (!matchedSegment) {
6285      return callback({
6286        message: `${programTime} was not found in the stream`
6287      });
6288    }
6289
6290    const segment = matchedSegment.segment;
6291    const mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime);
6292
6293    if (matchedSegment.type === 'estimate') {
6294      // we've run out of retries
6295      if (retryCount === 0) {
6296        return callback({
6297          message: `${programTime} is not buffered yet. Try again`
6298        });
6299      }
6300
6301      seekTo(matchedSegment.estimatedStart + mediaOffset);
6302      tech.one('seeked', () => {
6303        seekToProgramTime({
6304          programTime,
6305          playlist,
6306          retryCount: retryCount - 1,
6307          seekTo,
6308          pauseAfterSeek,
6309          tech,
6310          callback
6311        });
6312      });
6313      return;
6314    } // Since the segment.start value is determined from the buffered end or ending time
6315    // of the prior segment, the seekToTime doesn't need to account for any transmuxer
6316    // modifications.
6317
6318
6319    const seekToTime = segment.start + mediaOffset;
6320
6321    const seekedCallback = () => {
6322      return callback(null, tech.currentTime());
6323    }; // listen for seeked event
6324
6325
6326    tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state
6327
6328    if (pauseAfterSeek) {
6329      tech.pause();
6330    }
6331
6332    seekTo(seekToTime);
6333  };
6334
6335  /**
6336   * Loops through all supported media groups in master and calls the provided
6337   * callback for each group
6338   *
6339   * @param {Object} master
6340   *        The parsed master manifest object
6341   * @param {string[]} groups
6342   *        The media groups to call the callback for
6343   * @param {Function} callback
6344   *        Callback to call for each media group
6345   */
6346  var forEachMediaGroup = function forEachMediaGroup(master, groups, callback) {
6347    groups.forEach(function (mediaType) {
6348      for (var groupKey in master.mediaGroups[mediaType]) {
6349        for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
6350          var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
6351          callback(mediaProperties, mediaType, groupKey, labelKey);
6352        }
6353      }
6354    });
6355  };
6356
6357  /*! @name mpd-parser @version 1.
63573.1
vendor: 33,707 bytes, lines 6357-7353
6357 @license Apache-2.0 */
6358
6359  const isObject = obj => {
6360    return !!obj && typeof obj === 'object';
6361  };
6362
6363  const merge = (...objects) => {
6364    return objects.reduce((result, source) => {
6365      if (typeof source !== 'object') {
6366        return result;
6367      }
6368
6369      Object.keys(source).forEach(key => {
6370        if (Array.isArray(result[key]) && Array.isArray(source[key])) {
6371          result[key] = result[key].concat(source[key]);
6372        } else if (isObject(result[key]) && isObject(source[key])) {
6373          result[key] = merge(result[key], source[key]);
6374        } else {
6375          result[key] = source[key];
6376        }
6377      });
6378      return result;
6379    }, {});
6380  };
6381
6382  const values = o => Object.keys(o).map(k => o[k]);
6383
6384  const range = (start, end) => {
6385    const result = [];
6386
6387    for (let i = start; i < end; i++) {
6388      result.push(i);
6389    }
6390
6391    return result;
6392  };
6393
6394  const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
6395
6396  const from = list => {
6397    if (!list.length) {
6398      return [];
6399    }
6400
6401    const result = [];
6402
6403    for (let i = 0; i < list.length; i++) {
6404      result.push(list[i]);
6405    }
6406
6407    return result;
6408  };
6409
6410  const findIndexes = (l, key) => l.reduce((a, e, i) => {
6411    if (e[key]) {
6412      a.push(i);
6413    }
6414
6415    return a;
6416  }, []);
6417  /**
6418   * Returns a union of the included lists provided each element can be identified by a key.
6419   *
6420   * @param {Array} list - list of lists to get the union of
6421   * @param {Function} keyFunction - the function to use as a key for each element
6422   *
6423   * @return {Array} the union of the arrays
6424   */
6425
6426
6427  const union = (lists, keyFunction) => {
6428    return values(lists.reduce((acc, list) => {
6429      list.forEach(el => {
6430        acc[keyFunction(el)] = el;
6431      });
6432      return acc;
6433    }, {}));
6434  };
6435
6436  var errors = {
6437    INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
6438    INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING',
6439    DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
6440    DASH_INVALID_XML: 'DASH_INVALID_XML',
6441    NO_BASE_URL: 'NO_BASE_URL',
6442    MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
6443    SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
6444    UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
6445  };
6446  /**
6447   * @typedef {Object} SingleUri
6448   * @property {string} uri - relative location of segment
6449   * @property {string} resolvedUri - resolved location of segment
6450   * @property {Object} byterange - Object containing information on how to make byte range
6451   *   requests following byte-range-spec per RFC2616.
6452   * @property {String} byterange.length - length of range request
6453   * @property {String} byterange.offset - byte offset of range request
6454   *
6455   * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
6456   */
6457
6458  /**
6459   * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
6460   * that conforms to how m3u8-parser is structured
6461   *
6462   * @see https://github.com/videojs/m3u8-parser
6463   *
6464   * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes
6465   * @param {string} source - source url for segment
6466   * @param {string} range - optional range used for range calls,
6467   *   follows  RFC 2616, Clause 14.35.1
6468   * @return {SingleUri} full segment information transformed into a format similar
6469   *   to m3u8-parser
6470   */
6471
6472  const urlTypeToSegment = ({
6473    baseUrl = '',
6474    source = '',
6475    range = '',
6476    indexRange = ''
6477  }) => {
6478    const segment = {
6479      uri: source,
6480      resolvedUri: resolveUrl$1(baseUrl || '', source)
6481    };
6482
6483    if (range || indexRange) {
6484      const rangeStr = range ? range : indexRange;
6485      const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible
6486
6487      let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10);
6488      let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER
6489
6490      if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') {
6491        startRange = Number(startRange);
6492      }
6493
6494      if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') {
6495        endRange = Number(endRange);
6496      }
6497
6498      let length;
6499
6500      if (typeof endRange === 'bigint' || typeof startRange === 'bigint') {
6501        length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1);
6502      } else {
6503        length = endRange - startRange + 1;
6504      }
6505
6506      if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) {
6507        length = Number(length);
6508      } // byterange should be inclusive according to
6509      // RFC 2616, Clause 14.35.1
6510
6511
6512      segment.byterange = {
6513        length,
6514        offset: startRange
6515      };
6516    }
6517
6518    return segment;
6519  };
6520
6521  const byteRangeToString = byterange => {
6522    // `endRange` is one less than `offset + length` because the HTTP range
6523    // header uses inclusive ranges
6524    let endRange;
6525
6526    if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') {
6527      endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1);
6528    } else {
6529      endRange = byterange.offset + byterange.length - 1;
6530    }
6531
6532    return `${byterange.offset}-${endRange}`;
6533  };
6534  /**
6535   * parse the end number attribue that can be a string
6536   * number, or undefined.
6537   *
6538   * @param {string|number|undefined} endNumber
6539   *        The end number attribute.
6540   *
6541   * @return {number|null}
6542   *          The result of parsing the end number.
6543   */
6544
6545
6546  const parseEndNumber = endNumber => {
6547    if (endNumber && typeof endNumber !== 'number') {
6548      endNumber = parseInt(endNumber, 10);
6549    }
6550
6551    if (isNaN(endNumber)) {
6552      return null;
6553    }
6554
6555    return endNumber;
6556  };
6557  /**
6558   * Functions for calculating the range of available segments in static and dynamic
6559   * manifests.
6560   */
6561
6562
6563  const segmentRange = {
6564    /**
6565     * Returns the entire range of available segments for a static MPD
6566     *
6567     * @param {Object} attributes
6568     *        Inheritied MPD attributes
6569     * @return {{ start: number, end: number }}
6570     *         The start and end numbers for available segments
6571     */
6572    static(attributes) {
6573      const {
6574        duration,
6575        timescale = 1,
6576        sourceDuration,
6577        periodDuration
6578      } = attributes;
6579      const endNumber = parseEndNumber(attributes.endNumber);
6580      const segmentDuration = duration / timescale;
6581
6582      if (typeof endNumber === 'number') {
6583        return {
6584          start: 0,
6585          end: endNumber
6586        };
6587      }
6588
6589      if (typeof periodDuration === 'number') {
6590        return {
6591          start: 0,
6592          end: periodDuration / segmentDuration
6593        };
6594      }
6595
6596      return {
6597        start: 0,
6598        end: sourceDuration / segmentDuration
6599      };
6600    },
6601
6602    /**
6603     * Returns the current live window range of available segments for a dynamic MPD
6604     *
6605     * @param {Object} attributes
6606     *        Inheritied MPD attributes
6607     * @return {{ start: number, end: number }}
6608     *         The start and end numbers for available segments
6609     */
6610    dynamic(attributes) {
6611      const {
6612        NOW,
6613        clientOffset,
6614        availabilityStartTime,
6615        timescale = 1,
6616        duration,
6617        periodStart = 0,
6618        minimumUpdatePeriod = 0,
6619        timeShiftBufferDepth = Infinity
6620      } = attributes;
6621      const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated
6622      // after retrieving UTC server time.
6623
6624      const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock.
6625      // Convert the period start time to EPOCH.
6626
6627      const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update.
6628
6629      const periodEndWC = now + minimumUpdatePeriod;
6630      const periodDuration = periodEndWC - periodStartWC;
6631      const segmentCount = Math.ceil(periodDuration * timescale / duration);
6632      const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
6633      const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
6634      return {
6635        start: Math.max(0, availableStart),
6636        end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd)
6637      };
6638    }
6639
6640  };
6641  /**
6642   * Maps a range of numbers to objects with information needed to build the corresponding
6643   * segment list
6644   *
6645   * @name toSegmentsCallback
6646   * @function
6647   * @param {number} number
6648   *        Number of the segment
6649   * @param {number} index
6650   *        Index of the number in the range list
6651   * @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
6652   *         Object with segment timing and duration info
6653   */
6654
6655  /**
6656   * Returns a callback for Array.prototype.map for mapping a range of numbers to
6657   * information needed to build the segment list.
6658   *
6659   * @param {Object} attributes
6660   *        Inherited MPD attributes
6661   * @return {toSegmentsCallback}
6662   *         Callback map function
6663   */
6664
6665  const toSegments = attributes => number => {
6666    const {
6667      duration,
6668      timescale = 1,
6669      periodStart,
6670      startNumber = 1
6671    } = attributes;
6672    return {
6673      number: startNumber + number,
6674      duration: duration / timescale,
6675      timeline: periodStart,
6676      time: number * duration
6677    };
6678  };
6679  /**
6680   * Returns a list of objects containing segment timing and duration info used for
6681   * building the list of segments. This uses the @duration attribute specified
6682   * in the MPD manifest to derive the range of segments.
6683   *
6684   * @param {Object} attributes
6685   *        Inherited MPD attributes
6686   * @return {{number: number, duration: number, time: number, timeline: number}[]}
6687   *         List of Objects with segment timing and duration info
6688   */
6689
6690
6691  const parseByDuration = attributes => {
6692    const {
6693      type,
6694      duration,
6695      timescale = 1,
6696      periodDuration,
6697      sourceDuration
6698    } = attributes;
6699    const {
6700      start,
6701      end
6702    } = segmentRange[type](attributes);
6703    const segments = range(start, end).map(toSegments(attributes));
6704
6705    if (type === 'static') {
6706      const index = segments.length - 1; // section is either a period or the full source
6707
6708      const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration
6709
6710      segments[index].duration = sectionDuration - duration / timescale * index;
6711    }
6712
6713    return segments;
6714  };
6715  /**
6716   * Translates SegmentBase into a set of segments.
6717   * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes.  Each
6718   * node should be translated into segment.
6719   *
6720   * @param {Object} attributes
6721   *   Object containing all inherited attributes from parent elements with attribute
6722   *   names as keys
6723   * @return {Object.<Array>} list of segments
6724   */
6725
6726
6727  const segmentsFromBase = attributes => {
6728    const {
6729      baseUrl,
6730      initialization = {},
6731      sourceDuration,
6732      indexRange = '',
6733      periodStart,
6734      presentationTime,
6735      number = 0,
6736      duration
6737    } = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
6738
6739    if (!baseUrl) {
6740      throw new Error(errors.NO_BASE_URL);
6741    }
6742
6743    const initSegment = urlTypeToSegment({
6744      baseUrl,
6745      source: initialization.sourceURL,
6746      range: initialization.range
6747    });
6748    const segment = urlTypeToSegment({
6749      baseUrl,
6750      source: baseUrl,
6751      indexRange
6752    });
6753    segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source
6754    // (since SegmentBase is only for one total segment)
6755
6756    if (duration) {
6757      const segmentTimeInfo = parseByDuration(attributes);
6758
6759      if (segmentTimeInfo.length) {
6760        segment.duration = segmentTimeInfo[0].duration;
6761        segment.timeline = segmentTimeInfo[0].timeline;
6762      }
6763    } else if (sourceDuration) {
6764      segment.duration = sourceDuration;
6765      segment.timeline = periodStart;
6766    } // If presentation time is provided, these segments are being generated by SIDX
6767    // references, and should use the time provided. For the general case of SegmentBase,
6768    // there should only be one segment in the period, so its presentation time is the same
6769    // as its period start.
6770
6771
6772    segment.presentationTime = presentationTime || periodStart;
6773    segment.number = number;
6774    return [segment];
6775  };
6776  /**
6777   * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
6778   * according to the sidx information given.
6779   *
6780   * playlist.sidx has metadadata about the sidx where-as the sidx param
6781   * is the parsed sidx box itself.
6782   *
6783   * @param {Object} playlist the playlist to update the sidx information for
6784   * @param {Object} sidx the parsed sidx box
6785   * @return {Object} the playlist object with the updated sidx information
6786   */
6787
6788
6789  const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => {
6790    // Retain init segment information
6791    const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing
6792
6793    const sourceDuration = playlist.sidx.duration; // Retain source timeline
6794
6795    const timeline = playlist.timeline || 0;
6796    const sidxByteRange = playlist.sidx.byterange;
6797    const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx
6798
6799    const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes
6800
6801    const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
6802    const segments = [];
6803    const type = playlist.endList ? 'static' : 'dynamic';
6804    const periodStart = playlist.sidx.timeline;
6805    let presentationTime = periodStart;
6806    let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box
6807
6808    let startIndex; // eslint-disable-next-line
6809
6810    if (typeof sidx.firstOffset === 'bigint') {
6811      startIndex = window.BigInt(sidxEnd) + sidx.firstOffset;
6812    } else {
6813      startIndex = sidxEnd + sidx.firstOffset;
6814    }
6815
6816    for (let i = 0; i < mediaReferences.length; i++) {
6817      const reference = sidx.references[i]; // size of the referenced (sub)segment
6818
6819      const size = reference.referencedSize; // duration of the referenced (sub)segment, in  the  timescale
6820      // this will be converted to seconds when generating segments
6821
6822      const duration = reference.subsegmentDuration; // should be an inclusive range
6823
6824      let endIndex; // eslint-disable-next-line
6825
6826      if (typeof startIndex === 'bigint') {
6827        endIndex = startIndex + window.BigInt(size) - window.BigInt(1);
6828      } else {
6829        endIndex = startIndex + size - 1;
6830      }
6831
6832      const indexRange = `${startIndex}-${endIndex}`;
6833      const attributes = {
6834        baseUrl,
6835        timescale,
6836        timeline,
6837        periodStart,
6838        presentationTime,
6839        number,
6840        duration,
6841        sourceDuration,
6842        indexRange,
6843        type
6844      };
6845      const segment = segmentsFromBase(attributes)[0];
6846
6847      if (initSegment) {
6848        segment.map = initSegment;
6849      }
6850
6851      segments.push(segment);
6852
6853      if (typeof startIndex === 'bigint') {
6854        startIndex += window.BigInt(size);
6855      } else {
6856        startIndex += size;
6857      }
6858
6859      presentationTime += duration / timescale;
6860      number++;
6861    }
6862
6863    playlist.segments = segments;
6864    return playlist;
6865  };
6866
6867  const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen)
6868
6869  const TIME_FUDGE = 1 / 60;
6870  /**
6871   * Given a list of timelineStarts, combines, dedupes, and sorts them.
6872   *
6873   * @param {TimelineStart[]} timelineStarts - list of timeline starts
6874   *
6875   * @return {TimelineStart[]} the combined and deduped timeline starts
6876   */
6877
6878  const getUniqueTimelineStarts = timelineStarts => {
6879    return union(timelineStarts, ({
6880      timeline
6881    }) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1);
6882  };
6883  /**
6884   * Finds the playlist with the matching NAME attribute.
6885   *
6886   * @param {Array} playlists - playlists to search through
6887   * @param {string} name - the NAME attribute to search for
6888   *
6889   * @return {Object|null} the matching playlist object, or null
6890   */
6891
6892
6893  const findPlaylistWithName = (playlists, name) => {
6894    for (let i = 0; i < playlists.length; i++) {
6895      if (playlists[i].attributes.NAME === name) {
6896        return playlists[i];
6897      }
6898    }
6899
6900    return null;
6901  };
6902  /**
6903   * Gets a flattened array of media group playlists.
6904   *
6905   * @param {Object} manifest - the main manifest object
6906   *
6907   * @return {Array} the media group playlists
6908   */
6909
6910
6911  const getMediaGroupPlaylists = manifest => {
6912    let mediaGroupPlaylists = [];
6913    forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
6914      mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
6915    });
6916    return mediaGroupPlaylists;
6917  };
6918  /**
6919   * Updates the playlist's media sequence numbers.
6920   *
6921   * @param {Object} config - options object
6922   * @param {Object} config.playlist - the playlist to update
6923   * @param {number} config.mediaSequence - the mediaSequence number to start with
6924   */
6925
6926
6927  const updateMediaSequenceForPlaylist = ({
6928    playlist,
6929    mediaSequence
6930  }) => {
6931    playlist.mediaSequence = mediaSequence;
6932    playlist.segments.forEach((segment, index) => {
6933      segment.number = playlist.mediaSequence + index;
6934    });
6935  };
6936  /**
6937   * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists
6938   * and a complete list of timeline starts.
6939   *
6940   * If no matching playlist is found, only the discontinuity sequence number of the playlist
6941   * will be updated.
6942   *
6943   * Since early available timelines are not supported, at least one segment must be present.
6944   *
6945   * @param {Object} config - options object
6946   * @param {Object[]} oldPlaylists - the old playlists to use as a reference
6947   * @param {Object[]} newPlaylists - the new playlists to update
6948   * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point
6949   */
6950
6951
6952  const updateSequenceNumbers = ({
6953    oldPlaylists,
6954    newPlaylists,
6955    timelineStarts
6956  }) => {
6957    newPlaylists.forEach(playlist => {
6958      playlist.discontinuitySequence = timelineStarts.findIndex(function ({
6959        timeline
6960      }) {
6961        return timeline === playlist.timeline;
6962      }); // Playlists NAMEs come from DASH Representation IDs, which are mandatory
6963      // (see ISO_23009-1-2012 5.3.5.2).
6964      //
6965      // If the same Representation existed in a prior Period, it will retain the same NAME.
6966
6967      const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME);
6968
6969      if (!oldPlaylist) {
6970        // Since this is a new playlist, the media sequence values can start from 0 without
6971        // consequence.
6972        return;
6973      } // TODO better support for live SIDX
6974      //
6975      // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD).
6976      // This is evident by a playlist only having a single SIDX reference. In a multiperiod
6977      // playlist there would need to be multiple SIDX references. In addition, live SIDX is
6978      // not supported when the SIDX properties change on refreshes.
6979      //
6980      // In the future, if support needs to be added, the merging logic here can be called
6981      // after SIDX references are resolved. For now, exit early to prevent exceptions being
6982      // thrown due to undefined references.
6983
6984
6985      if (playlist.sidx) {
6986        return;
6987      } // Since we don't yet support early available timelines, we don't need to support
6988      // playlists with no segments.
6989
6990
6991      const firstNewSegment = playlist.segments[0];
6992      const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) {
6993        return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE;
6994      }); // No matching segment from the old playlist means the entire playlist was refreshed.
6995      // In this case the media sequence should account for this update, and the new segments
6996      // should be marked as discontinuous from the prior content, since the last prior
6997      // timeline was removed.
6998
6999      if (oldMatchingSegmentIndex === -1) {
7000        updateMediaSequenceForPlaylist({
7001          playlist,
7002          mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length
7003        });
7004        playlist.segments[0].discontinuity = true;
7005        playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content.
7006        //
7007        // If the new playlist's timeline is the same as the last seen segment's timeline,
7008        // then a discontinuity can be added to identify that there's potentially missing
7009        // content. If there's no missing content, the discontinuity should still be rather
7010        // harmless. It's possible that if segment durations are accurate enough, that the
7011        // existence of a gap can be determined using the presentation times and durations,
7012        // but if the segment timing info is off, it may introduce more problems than simply
7013        // adding the discontinuity.
7014        //
7015        // If the new playlist's timeline is different from the last seen segment's timeline,
7016        // then a discontinuity can be added to identify that this is the first seen segment
7017        // of a new timeline. However, the logic at the start of this function that
7018        // determined the disconinuity sequence by timeline index is now off by one (the
7019        // discontinuity of the newest timeline hasn't yet fallen off the manifest...since
7020        // we added it), so the disconinuity sequence must be decremented.
7021        //
7022        // A period may also have a duration of zero, so the case of no segments is handled
7023        // here even though we don't yet support early available periods.
7024
7025        if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) {
7026          playlist.discontinuitySequence--;
7027        }
7028
7029        return;
7030      } // If the first segment matched with a prior segment on a discontinuity (it's matching
7031      // on the first segment of a period), then the discontinuitySequence shouldn't be the
7032      // timeline's matching one, but instead should be the one prior, and the first segment
7033      // of the new manifest should be marked with a discontinuity.
7034      //
7035      // The reason for this special case is that discontinuity sequence shows how many
7036      // discontinuities have fallen off of the playlist, and discontinuities are marked on
7037      // the first segment of a new "timeline." Because of this, while DASH will retain that
7038      // Period while the "timeline" exists, HLS keeps track of it via the discontinuity
7039      // sequence, and that first segment is an indicator, but can be removed before that
7040      // timeline is gone.
7041
7042
7043      const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex];
7044
7045      if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) {
7046        firstNewSegment.discontinuity = true;
7047        playlist.discontinuityStarts.unshift(0);
7048        playlist.discontinuitySequence--;
7049      }
7050
7051      updateMediaSequenceForPlaylist({
7052        playlist,
7053        mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number
7054      });
7055    });
7056  };
7057  /**
7058   * Given an old parsed manifest object and a new parsed manifest object, updates the
7059   * sequence and timing values within the new manifest to ensure that it lines up with the
7060   * old.
7061   *
7062   * @param {Array} oldManifest - the old main manifest object
7063   * @param {Array} newManifest - the new main manifest object
7064   *
7065   * @return {Object} the updated new manifest object
7066   */
7067
7068
7069  const positionManifestOnTimeline = ({
7070    oldManifest,
7071    newManifest
7072  }) => {
7073    // Starting from v4.1.2 of the IOP, section 4.4.3.3 states:
7074    //
7075    // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates."
7076    //
7077    // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160
7078    //
7079    // Because of this change, and the difficulty of supporting periods with changing start
7080    // times, periods with changing start times are not supported. This makes the logic much
7081    // simpler, since periods with the same start time can be considerred the same period
7082    // across refreshes.
7083    //
7084    // To give an example as to the difficulty of handling periods where the start time may
7085    // change, if a single period manifest is refreshed with another manifest with a single
7086    // period, and both the start and end times are increased, then the only way to determine
7087    // if it's a new period or an old one that has changed is to look through the segments of
7088    // each playlist and determine the presentation time bounds to find a match. In addition,
7089    // if the period start changed to exceed the old period end, then there would be no
7090    // match, and it would not be possible to determine whether the refreshed period is a new
7091    // one or the old one.
7092    const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest));
7093    const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that
7094    // there's a "memory leak" in that it will never stop growing, in reality, only a couple
7095    // of properties are saved for each seen Period. Even long running live streams won't
7096    // generate too many Periods, unless the stream is watched for decades. In the future,
7097    // this can be optimized by mapping to discontinuity sequence numbers for each timeline,
7098    // but it may not become an issue, and the additional info can be useful for debugging.
7099
7100    newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]);
7101    updateSequenceNumbers({
7102      oldPlaylists,
7103      newPlaylists,
7104      timelineStarts: newManifest.timelineStarts
7105    });
7106    return newManifest;
7107  };
7108
7109  const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange);
7110
7111  const mergeDiscontiguousPlaylists = playlists => {
7112    // Break out playlists into groups based on their baseUrl
7113    const playlistsByBaseUrl = playlists.reduce(function (acc, cur) {
7114      if (!acc[cur.attributes.baseUrl]) {
7115        acc[cur.attributes.baseUrl] = [];
7116      }
7117
7118      acc[cur.attributes.baseUrl].push(cur);
7119      return acc;
7120    }, {});
7121    let allPlaylists = [];
7122    Object.values(playlistsByBaseUrl).forEach(playlistGroup => {
7123      const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => {
7124        // assuming playlist IDs are the same across periods
7125        // TODO: handle multiperiod where representation sets are not the same
7126        // across periods
7127        const name = playlist.attributes.id + (playlist.attributes.lang || '');
7128
7129        if (!acc[name]) {
7130          // First Period
7131          acc[name] = playlist;
7132          acc[name].attributes.timelineStarts = [];
7133        } else {
7134          // Subsequent Periods
7135          if (playlist.segments) {
7136            // first segment of subsequent periods signal a discontinuity
7137            if (playlist.segments[0]) {
7138              playlist.segments[0].discontinuity = true;
7139            }
7140
7141            acc[name].segments.push(...playlist.segments);
7142          } // bubble up contentProtection, this assumes all DRM content
7143          // has the same contentProtection
7144
7145
7146          if (playlist.attributes.contentProtection) {
7147            acc[name].attributes.contentProtection = playlist.attributes.contentProtection;
7148          }
7149        }
7150
7151        acc[name].attributes.timelineStarts.push({
7152          // Although they represent the same number, it's important to have both to make it
7153          // compatible with HLS potentially having a similar attribute.
7154          start: playlist.attributes.periodStart,
7155          timeline: playlist.attributes.periodStart
7156        });
7157        return acc;
7158      }, {}));
7159      allPlaylists = allPlaylists.concat(mergedPlaylists);
7160    });
7161    return allPlaylists.map(playlist => {
7162      playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity');
7163      return playlist;
7164    });
7165  };
7166
7167  const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => {
7168    const sidxKey = generateSidxKey(playlist.sidx);
7169    const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
7170
7171    if (sidxMatch) {
7172      addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri);
7173    }
7174
7175    return playlist;
7176  };
7177
7178  const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
7179    if (!Object.keys(sidxMapping).length) {
7180      return playlists;
7181    }
7182
7183    for (const i in playlists) {
7184      playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping);
7185    }
7186
7187    return playlists;
7188  };
7189
7190  const formatAudioPlaylist = ({
7191    attributes,
7192    segments,
7193    sidx,
7194    mediaSequence,
7195    discontinuitySequence,
7196    discontinuityStarts
7197  }, isAudioOnly) => {
7198    const playlist = {
7199      attributes: {
7200        NAME: attributes.id,
7201        BANDWIDTH: attributes.bandwidth,
7202        CODECS: attributes.codecs,
7203        ['PROGRAM-ID']: 1
7204      },
7205      uri: '',
7206      endList: attributes.type === 'static',
7207      timeline: attributes.periodStart,
7208      resolvedUri: attributes.baseUrl || '',
7209      targetDuration: attributes.duration,
7210      discontinuitySequence,
7211      discontinuityStarts,
7212      timelineStarts: attributes.timelineStarts,
7213      mediaSequence,
7214      segments
7215    };
7216
7217    if (attributes.contentProtection) {
7218      playlist.contentProtection = attributes.contentProtection;
7219    }
7220
7221    if (attributes.serviceLocation) {
7222      playlist.attributes.serviceLocation = attributes.serviceLocation;
7223    }
7224
7225    if (sidx) {
7226      playlist.sidx = sidx;
7227    }
7228
7229    if (isAudioOnly) {
7230      playlist.attributes.AUDIO = 'audio';
7231      playlist.attributes.SUBTITLES = 'subs';
7232    }
7233
7234    return playlist;
7235  };
7236
7237  const formatVttPlaylist = ({
7238    attributes,
7239    segments,
7240    mediaSequence,
7241    discontinuityStarts,
7242    discontinuitySequence
7243  }) => {
7244    if (typeof segments === 'undefined') {
7245      // vtt tracks may use single file in BaseURL
7246      segments = [{
7247        uri: attributes.baseUrl,
7248        timeline: attributes.periodStart,
7249        resolvedUri: attributes.baseUrl || '',
7250        duration: attributes.sourceDuration,
7251        number: 0
7252      }]; // targetDuration should be the same duration as the only segment
7253
7254      attributes.duration = attributes.sourceDuration;
7255    }
7256
7257    const m3u8Attributes = {
7258      NAME: attributes.id,
7259      BANDWIDTH: attributes.bandwidth,
7260      ['PROGRAM-ID']: 1
7261    };
7262
7263    if (attributes.codecs) {
7264      m3u8Attributes.CODECS = attributes.codecs;
7265    }
7266
7267    const vttPlaylist = {
7268      attributes: m3u8Attributes,
7269      uri: '',
7270      endList: attributes.type === 'static',
7271      timeline: attributes.periodStart,
7272      resolvedUri: attributes.baseUrl || '',
7273      targetDuration: attributes.duration,
7274      timelineStarts: attributes.timelineStarts,
7275      discontinuityStarts,
7276      discontinuitySequence,
7277      mediaSequence,
7278      segments
7279    };
7280
7281    if (attributes.serviceLocation) {
7282      vttPlaylist.attributes.serviceLocation = attributes.serviceLocation;
7283    }
7284
7285    return vttPlaylist;
7286  };
7287
7288  const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => {
7289    let mainPlaylist;
7290    const formattedPlaylists = playlists.reduce((a, playlist) => {
7291      const role = playlist.attributes.role && playlist.attributes.role.value || '';
7292      const language = playlist.attributes.lang || '';
7293      let label = playlist.attributes.label || 'main';
7294
7295      if (language && !playlist.attributes.label) {
7296        const roleLabel = role ? ` (${role})` : '';
7297        label = `${playlist.attributes.lang}${roleLabel}`;
7298      }
7299
7300      if (!a[label]) {
7301        a[label] = {
7302          language,
7303          autoselect: true,
7304          default: role === 'main',
7305          playlists: [],
7306          uri: ''
7307        };
7308      }
7309
7310      const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping);
7311      a[label].playlists.push(formatted);
7312
7313      if (typeof mainPlaylist === 'undefined' && role === 'main') {
7314        mainPlaylist = playlist;
7315        mainPlaylist.default = true;
7316      }
7317
7318      return a;
7319    }, {}); // if no playlists have role "main", mark the first as main
7320
7321    if (!mainPlaylist) {
7322      const firstLabel = Object.keys(formattedPlaylists)[0];
7323      formattedPlaylists[firstLabel].default = true;
7324    }
7325
7326    return formattedPlaylists;
7327  };
7328
7329  const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
7330    return playlists.reduce((a, playlist) => {
7331      const label = playlist.attributes.label || playlist.attributes.lang || 'text';
7332      const language = playlist.attributes.lang || 'und';
7333
7334      if (!a[label]) {
7335        a[label] = {
7336          language,
7337          default: false,
7338          autoselect: false,
7339          playlists: [],
7340          uri: ''
7341        };
7342      }
7343
7344      a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping));
7345      return a;
7346    }, {});
7347  };
7348
7349  const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => {
7350    if (!svc) {
7351      return svcObj;
7352    }
7353
vendor: 3,850 bytes, lines 7354-7473
7354    svc.forEach(service => {
7355      const {
7356        channel,
7357        language
7358      } = service;
7359      svcObj[language] = {
7360        autoselect: false,
7361        default: false,
7362        instreamId: channel,
7363        language
7364      };
7365
7366      if (service.hasOwnProperty('aspectRatio')) {
7367        svcObj[language].aspectRatio = service.aspectRatio;
7368      }
7369
7370      if (service.hasOwnProperty('easyReader')) {
7371        svcObj[language].easyReader = service.easyReader;
7372      }
7373
7374      if (service.hasOwnProperty('3D')) {
7375        svcObj[language]['3D'] = service['3D'];
7376      }
7377    });
7378    return svcObj;
7379  }, {});
7380
7381  const formatVideoPlaylist = ({
7382    attributes,
7383    segments,
7384    sidx,
7385    discontinuityStarts
7386  }) => {
7387    const playlist = {
7388      attributes: {
7389        NAME: attributes.id,
7390        AUDIO: 'audio',
7391        SUBTITLES: 'subs',
7392        RESOLUTION: {
7393          width: attributes.width,
7394          height: attributes.height
7395        },
7396        CODECS: attributes.codecs,
7397        BANDWIDTH: attributes.bandwidth,
7398        ['PROGRAM-ID']: 1
7399      },
7400      uri: '',
7401      endList: attributes.type === 'static',
7402      timeline: attributes.periodStart,
7403      resolvedUri: attributes.baseUrl || '',
7404      targetDuration: attributes.duration,
7405      discontinuityStarts,
7406      timelineStarts: attributes.timelineStarts,
7407      segments
7408    };
7409
7410    if (attributes.frameRate) {
7411      playlist.attributes['FRAME-RATE'] = attributes.frameRate;
7412    }
7413
7414    if (attributes.contentProtection) {
7415      playlist.contentProtection = attributes.contentProtection;
7416    }
7417
7418    if (attributes.serviceLocation) {
7419      playlist.attributes.serviceLocation = attributes.serviceLocation;
7420    }
7421
7422    if (sidx) {
7423      playlist.sidx = sidx;
7424    }
7425
7426    return playlist;
7427  };
7428
7429  const videoOnly = ({
7430    attributes
7431  }) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video';
7432
7433  const audioOnly = ({
7434    attributes
7435  }) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio';
7436
7437  const vttOnly = ({
7438    attributes
7439  }) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
7440  /**
7441   * Contains start and timeline properties denoting a timeline start. For DASH, these will
7442   * be the same number.
7443   *
7444   * @typedef {Object} TimelineStart
7445   * @property {number} start - the start time of the timeline
7446   * @property {number} timeline - the timeline number
7447   */
7448
7449  /**
7450   * Adds appropriate media and discontinuity sequence values to the segments and playlists.
7451   *
7452   * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a
7453   * DASH specific attribute used in constructing segment URI's from templates. However, from
7454   * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence`
7455   * value, which should start at the original media sequence value (or 0) and increment by 1
7456   * for each segment thereafter. Since DASH's `startNumber` values are independent per
7457   * period, it doesn't make sense to use it for `number`. Instead, assume everything starts
7458   * from a 0 mediaSequence value and increment from there.
7459   *
7460   * Note that VHS currently doesn't use the `number` property, but it can be helpful for
7461   * debugging and making sense of the manifest.
7462   *
7463   * For live playlists, to account for values increasing in manifests when periods are
7464   * removed on refreshes, merging logic should be used to update the numbers to their
7465   * appropriate values (to ensure they're sequential and increasing).
7466   *
7467   * @param {Object[]} playlists - the playlists to update
7468   * @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest
7469   */
7470
7471
7472  const addMediaSequenceValues = (playlists, timelineStarts) => {
7473    // increment all segments sequentially
vendor: 27,795 bytes, lines 7474-8362
7474    playlists.forEach(playlist => {
7475      playlist.mediaSequence = 0;
7476      playlist.discontinuitySequence = timelineStarts.findIndex(function ({
7477        timeline
7478      }) {
7479        return timeline === playlist.timeline;
7480      });
7481
7482      if (!playlist.segments) {
7483        return;
7484      }
7485
7486      playlist.segments.forEach((segment, index) => {
7487        segment.number = index;
7488      });
7489    });
7490  };
7491  /**
7492   * Given a media group object, flattens all playlists within the media group into a single
7493   * array.
7494   *
7495   * @param {Object} mediaGroupObject - the media group object
7496   *
7497   * @return {Object[]}
7498   *         The media group playlists
7499   */
7500
7501
7502  const flattenMediaGroupPlaylists = mediaGroupObject => {
7503    if (!mediaGroupObject) {
7504      return [];
7505    }
7506
7507    return Object.keys(mediaGroupObject).reduce((acc, label) => {
7508      const labelContents = mediaGroupObject[label];
7509      return acc.concat(labelContents.playlists);
7510    }, []);
7511  };
7512
7513  const toM3u8 = ({
7514    dashPlaylists,
7515    locations,
7516    contentSteering,
7517    sidxMapping = {},
7518    previousManifest,
7519    eventStream
7520  }) => {
7521    if (!dashPlaylists.length) {
7522      return {};
7523    } // grab all main manifest attributes
7524
7525
7526    const {
7527      sourceDuration: duration,
7528      type,
7529      suggestedPresentationDelay,
7530      minimumUpdatePeriod
7531    } = dashPlaylists[0].attributes;
7532    const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist);
7533    const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
7534    const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly));
7535    const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean);
7536    const manifest = {
7537      allowCache: true,
7538      discontinuityStarts: [],
7539      segments: [],
7540      endList: true,
7541      mediaGroups: {
7542        AUDIO: {},
7543        VIDEO: {},
7544        ['CLOSED-CAPTIONS']: {},
7545        SUBTITLES: {}
7546      },
7547      uri: '',
7548      duration,
7549      playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping)
7550    };
7551
7552    if (minimumUpdatePeriod >= 0) {
7553      manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000;
7554    }
7555
7556    if (locations) {
7557      manifest.locations = locations;
7558    }
7559
7560    if (contentSteering) {
7561      manifest.contentSteering = contentSteering;
7562    }
7563
7564    if (type === 'dynamic') {
7565      manifest.suggestedPresentationDelay = suggestedPresentationDelay;
7566    }
7567
7568    if (eventStream && eventStream.length > 0) {
7569      manifest.eventStream = eventStream;
7570    }
7571
7572    const isAudioOnly = manifest.playlists.length === 0;
7573    const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null;
7574    const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null;
7575    const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup));
7576    const playlistTimelineStarts = formattedPlaylists.map(({
7577      timelineStarts
7578    }) => timelineStarts);
7579    manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts);
7580    addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts);
7581
7582    if (organizedAudioGroup) {
7583      manifest.mediaGroups.AUDIO.audio = organizedAudioGroup;
7584    }
7585
7586    if (organizedVttGroup) {
7587      manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup;
7588    }
7589
7590    if (captions.length) {
7591      manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions);
7592    }
7593
7594    if (previousManifest) {
7595      return positionManifestOnTimeline({
7596        oldManifest: previousManifest,
7597        newManifest: manifest
7598      });
7599    }
7600
7601    return manifest;
7602  };
7603  /**
7604   * Calculates the R (repetition) value for a live stream (for the final segment
7605   * in a manifest where the r value is negative 1)
7606   *
7607   * @param {Object} attributes
7608   *        Object containing all inherited attributes from parent elements with attribute
7609   *        names as keys
7610   * @param {number} time
7611   *        current time (typically the total time up until the final segment)
7612   * @param {number} duration
7613   *        duration property for the given <S />
7614   *
7615   * @return {number}
7616   *        R value to reach the end of the given period
7617   */
7618
7619
7620  const getLiveRValue = (attributes, time, duration) => {
7621    const {
7622      NOW,
7623      clientOffset,
7624      availabilityStartTime,
7625      timescale = 1,
7626      periodStart = 0,
7627      minimumUpdatePeriod = 0
7628    } = attributes;
7629    const now = (NOW + clientOffset) / 1000;
7630    const periodStartWC = availabilityStartTime + periodStart;
7631    const periodEndWC = now + minimumUpdatePeriod;
7632    const periodDuration = periodEndWC - periodStartWC;
7633    return Math.ceil((periodDuration * timescale - time) / duration);
7634  };
7635  /**
7636   * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
7637   * timing and duration
7638   *
7639   * @param {Object} attributes
7640   *        Object containing all inherited attributes from parent elements with attribute
7641   *        names as keys
7642   * @param {Object[]} segmentTimeline
7643   *        List of objects representing the attributes of each S element contained within
7644   *
7645   * @return {{number: number, duration: number, time: number, timeline: number}[]}
7646   *         List of Objects with segment timing and duration info
7647   */
7648
7649
7650  const parseByTimeline = (attributes, segmentTimeline) => {
7651    const {
7652      type,
7653      minimumUpdatePeriod = 0,
7654      media = '',
7655      sourceDuration,
7656      timescale = 1,
7657      startNumber = 1,
7658      periodStart: timeline
7659    } = attributes;
7660    const segments = [];
7661    let time = -1;
7662
7663    for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
7664      const S = segmentTimeline[sIndex];
7665      const duration = S.d;
7666      const repeat = S.r || 0;
7667      const segmentTime = S.t || 0;
7668
7669      if (time < 0) {
7670        // first segment
7671        time = segmentTime;
7672      }
7673
7674      if (segmentTime && segmentTime > time) {
7675        // discontinuity
7676        // TODO: How to handle this type of discontinuity
7677        // timeline++ here would treat it like HLS discontuity and content would
7678        // get appended without gap
7679        // E.G.
7680        //  <S t="0" d="1" />
7681        //  <S d="1" />
7682        //  <S d="1" />
7683        //  <S t="5" d="1" />
7684        // would have $Time$ values of [0, 1, 2, 5]
7685        // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
7686        // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
7687        // does the value of sourceDuration consider this when calculating arbitrary
7688        // negative @r repeat value?
7689        // E.G. Same elements as above with this added at the end
7690        //  <S d="1" r="-1" />
7691        //  with a sourceDuration of 10
7692        // Would the 2 gaps be included in the time duration calculations resulting in
7693        // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
7694        // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
7695        time = segmentTime;
7696      }
7697
7698      let count;
7699
7700      if (repeat < 0) {
7701        const nextS = sIndex + 1;
7702
7703        if (nextS === segmentTimeline.length) {
7704          // last segment
7705          if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) {
7706            count = getLiveRValue(attributes, time, duration);
7707          } else {
7708            // TODO: This may be incorrect depending on conclusion of TODO above
7709            count = (sourceDuration * timescale - time) / duration;
7710          }
7711        } else {
7712          count = (segmentTimeline[nextS].t - time) / duration;
7713        }
7714      } else {
7715        count = repeat + 1;
7716      }
7717
7718      const end = startNumber + segments.length + count;
7719      let number = startNumber + segments.length;
7720
7721      while (number < end) {
7722        segments.push({
7723          number,
7724          duration: duration / timescale,
7725          time,
7726          timeline
7727        });
7728        time += duration;
7729        number++;
7730      }
7731    }
7732
7733    return segments;
7734  };
7735
7736  const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
7737  /**
7738   * Replaces template identifiers with corresponding values. To be used as the callback
7739   * for String.prototype.replace
7740   *
7741   * @name replaceCallback
7742   * @function
7743   * @param {string} match
7744   *        Entire match of identifier
7745   * @param {string} identifier
7746   *        Name of matched identifier
7747   * @param {string} format
7748   *        Format tag string. Its presence indicates that padding is expected
7749   * @param {string} width
7750   *        Desired length of the replaced value. Values less than this width shall be left
7751   *        zero padded
7752   * @return {string}
7753   *         Replacement for the matched identifier
7754   */
7755
7756  /**
7757   * Returns a function to be used as a callback for String.prototype.replace to replace
7758   * template identifiers
7759   *
7760   * @param {Obect} values
7761   *        Object containing values that shall be used to replace known identifiers
7762   * @param {number} values.RepresentationID
7763   *        Value of the Representation@id attribute
7764   * @param {number} values.Number
7765   *        Number of the corresponding segment
7766   * @param {number} values.Bandwidth
7767   *        Value of the Representation@bandwidth attribute.
7768   * @param {number} values.Time
7769   *        Timestamp value of the corresponding segment
7770   * @return {replaceCallback}
7771   *         Callback to be used with String.prototype.replace to replace identifiers
7772   */
7773
7774  const identifierReplacement = values => (match, identifier, format, width) => {
7775    if (match === '$$') {
7776      // escape sequence
7777      return '$';
7778    }
7779
7780    if (typeof values[identifier] === 'undefined') {
7781      return match;
7782    }
7783
7784    const value = '' + values[identifier];
7785
7786    if (identifier === 'RepresentationID') {
7787      // Format tag shall not be present with RepresentationID
7788      return value;
7789    }
7790
7791    if (!format) {
7792      width = 1;
7793    } else {
7794      width = parseInt(width, 10);
7795    }
7796
7797    if (value.length >= width) {
7798      return value;
7799    }
7800
7801    return `${new Array(width - value.length + 1).join('0')}${value}`;
7802  };
7803  /**
7804   * Constructs a segment url from a template string
7805   *
7806   * @param {string} url
7807   *        Template string to construct url from
7808   * @param {Obect} values
7809   *        Object containing values that shall be used to replace known identifiers
7810   * @param {number} values.RepresentationID
7811   *        Value of the Representation@id attribute
7812   * @param {number} values.Number
7813   *        Number of the corresponding segment
7814   * @param {number} values.Bandwidth
7815   *        Value of the Representation@bandwidth attribute.
7816   * @param {number} values.Time
7817   *        Timestamp value of the corresponding segment
7818   * @return {string}
7819   *         Segment url with identifiers replaced
7820   */
7821
7822
7823  const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values));
7824  /**
7825   * Generates a list of objects containing timing and duration information about each
7826   * segment needed to generate segment uris and the complete segment object
7827   *
7828   * @param {Object} attributes
7829   *        Object containing all inherited attributes from parent elements with attribute
7830   *        names as keys
7831   * @param {Object[]|undefined} segmentTimeline
7832   *        List of objects representing the attributes of each S element contained within
7833   *        the SegmentTimeline element
7834   * @return {{number: number, duration: number, time: number, timeline: number}[]}
7835   *         List of Objects with segment timing and duration info
7836   */
7837
7838
7839  const parseTemplateInfo = (attributes, segmentTimeline) => {
7840    if (!attributes.duration && !segmentTimeline) {
7841      // if neither @duration or SegmentTimeline are present, then there shall be exactly
7842      // one media segment
7843      return [{
7844        number: attributes.startNumber || 1,
7845        duration: attributes.sourceDuration,
7846        time: 0,
7847        timeline: attributes.periodStart
7848      }];
7849    }
7850
7851    if (attributes.duration) {
7852      return parseByDuration(attributes);
7853    }
7854
7855    return parseByTimeline(attributes, segmentTimeline);
7856  };
7857  /**
7858   * Generates a list of segments using information provided by the SegmentTemplate element
7859   *
7860   * @param {Object} attributes
7861   *        Object containing all inherited attributes from parent elements with attribute
7862   *        names as keys
7863   * @param {Object[]|undefined} segmentTimeline
7864   *        List of objects representing the attributes of each S element contained within
7865   *        the SegmentTimeline element
7866   * @return {Object[]}
7867   *         List of segment objects
7868   */
7869
7870
7871  const segmentsFromTemplate = (attributes, segmentTimeline) => {
7872    const templateValues = {
7873      RepresentationID: attributes.id,
7874      Bandwidth: attributes.bandwidth || 0
7875    };
7876    const {
7877      initialization = {
7878        sourceURL: '',
7879        range: ''
7880      }
7881    } = attributes;
7882    const mapSegment = urlTypeToSegment({
7883      baseUrl: attributes.baseUrl,
7884      source: constructTemplateUrl(initialization.sourceURL, templateValues),
7885      range: initialization.range
7886    });
7887    const segments = parseTemplateInfo(attributes, segmentTimeline);
7888    return segments.map(segment => {
7889      templateValues.Number = segment.number;
7890      templateValues.Time = segment.time;
7891      const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2
7892      // - if timescale isn't present on any level, default to 1.
7893
7894      const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
7895
7896      const presentationTimeOffset = attributes.presentationTimeOffset || 0;
7897      const presentationTime = // Even if the @t attribute is not specified for the segment, segment.time is
7898      // calculated in mpd-parser prior to this, so it's assumed to be available.
7899      attributes.periodStart + (segment.time - presentationTimeOffset) / timescale;
7900      const map = {
7901        uri,
7902        timeline: segment.timeline,
7903        duration: segment.duration,
7904        resolvedUri: resolveUrl$1(attributes.baseUrl || '', uri),
7905        map: mapSegment,
7906        number: segment.number,
7907        presentationTime
7908      };
7909      return map;
7910    });
7911  };
7912  /**
7913   * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14)
7914   * to an object that matches the output of a segment in videojs/mpd-parser
7915   *
7916   * @param {Object} attributes
7917   *   Object containing all inherited attributes from parent elements with attribute
7918   *   names as keys
7919   * @param {Object} segmentUrl
7920   *   <SegmentURL> node to translate into a segment object
7921   * @return {Object} translated segment object
7922   */
7923
7924
7925  const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
7926    const {
7927      baseUrl,
7928      initialization = {}
7929    } = attributes;
7930    const initSegment = urlTypeToSegment({
7931      baseUrl,
7932      source: initialization.sourceURL,
7933      range: initialization.range
7934    });
7935    const segment = urlTypeToSegment({
7936      baseUrl,
7937      source: segmentUrl.media,
7938      range: segmentUrl.mediaRange
7939    });
7940    segment.map = initSegment;
7941    return segment;
7942  };
7943  /**
7944   * Generates a list of segments using information provided by the SegmentList element
7945   * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes.  Each
7946   * node should be translated into segment.
7947   *
7948   * @param {Object} attributes
7949   *   Object containing all inherited attributes from parent elements with attribute
7950   *   names as keys
7951   * @param {Object[]|undefined} segmentTimeline
7952   *        List of objects representing the attributes of each S element contained within
7953   *        the SegmentTimeline element
7954   * @return {Object.<Array>} list of segments
7955   */
7956
7957
7958  const segmentsFromList = (attributes, segmentTimeline) => {
7959    const {
7960      duration,
7961      segmentUrls = [],
7962      periodStart
7963    } = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR
7964    // if both SegmentTimeline and @duration are defined, it is outside of spec.
7965
7966    if (!duration && !segmentTimeline || duration && segmentTimeline) {
7967      throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
7968    }
7969
7970    const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject));
7971    let segmentTimeInfo;
7972
7973    if (duration) {
7974      segmentTimeInfo = parseByDuration(attributes);
7975    }
7976
7977    if (segmentTimeline) {
7978      segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
7979    }
7980
7981    const segments = segmentTimeInfo.map((segmentTime, index) => {
7982      if (segmentUrlMap[index]) {
7983        const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2
7984        // - if timescale isn't present on any level, default to 1.
7985
7986        const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0
7987
7988        const presentationTimeOffset = attributes.presentationTimeOffset || 0;
7989        segment.timeline = segmentTime.timeline;
7990        segment.duration = segmentTime.duration;
7991        segment.number = segmentTime.number;
7992        segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale;
7993        return segment;
7994      } // Since we're mapping we should get rid of any blank segments (in case
7995      // the given SegmentTimeline is handling for more elements than we have
7996      // SegmentURLs for).
7997
7998    }).filter(segment => segment);
7999    return segments;
8000  };
8001
8002  const generateSegments = ({
8003    attributes,
8004    segmentInfo
8005  }) => {
8006    let segmentAttributes;
8007    let segmentsFn;
8008
8009    if (segmentInfo.template) {
8010      segmentsFn = segmentsFromTemplate;
8011      segmentAttributes = merge(attributes, segmentInfo.template);
8012    } else if (segmentInfo.base) {
8013      segmentsFn = segmentsFromBase;
8014      segmentAttributes = merge(attributes, segmentInfo.base);
8015    } else if (segmentInfo.list) {
8016      segmentsFn = segmentsFromList;
8017      segmentAttributes = merge(attributes, segmentInfo.list);
8018    }
8019
8020    const segmentsInfo = {
8021      attributes
8022    };
8023
8024    if (!segmentsFn) {
8025      return segmentsInfo;
8026    }
8027
8028    const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which
8029    // must be in seconds. Since we've generated the segment list, we no longer need
8030    // @duration to be in @timescale units, so we can convert it here.
8031
8032    if (segmentAttributes.duration) {
8033      const {
8034        duration,
8035        timescale = 1
8036      } = segmentAttributes;
8037      segmentAttributes.duration = duration / timescale;
8038    } else if (segments.length) {
8039      // if there is no @duration attribute, use the largest segment duration as
8040      // as target duration
8041      segmentAttributes.duration = segments.reduce((max, segment) => {
8042        return Math.max(max, Math.ceil(segment.duration));
8043      }, 0);
8044    } else {
8045      segmentAttributes.duration = 0;
8046    }
8047
8048    segmentsInfo.attributes = segmentAttributes;
8049    segmentsInfo.segments = segments; // This is a sidx box without actual segment information
8050
8051    if (segmentInfo.base && segmentAttributes.indexRange) {
8052      segmentsInfo.sidx = segments[0];
8053      segmentsInfo.segments = [];
8054    }
8055
8056    return segmentsInfo;
8057  };
8058
8059  const toPlaylists = representations => representations.map(generateSegments);
8060
8061  const findChildren = (element, name) => from(element.childNodes).filter(({
8062    tagName
8063  }) => tagName === name);
8064
8065  const getContent = element => element.textContent.trim();
8066  /**
8067   * Converts the provided string that may contain a division operation to a number.
8068   *
8069   * @param {string} value - the provided string value
8070   *
8071   * @return {number} the parsed string value
8072   */
8073
8074
8075  const parseDivisionValue = value => {
8076    return parseFloat(value.split('/').reduce((prev, current) => prev / current));
8077  };
8078
8079  const parseDuration = str => {
8080    const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
8081    const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
8082    const SECONDS_IN_DAY = 24 * 60 * 60;
8083    const SECONDS_IN_HOUR = 60 * 60;
8084    const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S
8085
8086    const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
8087    const match = durationRegex.exec(str);
8088
8089    if (!match) {
8090      return 0;
8091    }
8092
8093    const [year, month, day, hour, minute, second] = match.slice(1);
8094    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);
8095  };
8096
8097  const parseDate = str => {
8098    // Date format without timezone according to ISO 8601
8099    // YYY-MM-DDThh:mm:ss.ssssss
8100    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
8101    // expressed by ending with 'Z'
8102
8103    if (dateRegex.test(str)) {
8104      str += 'Z';
8105    }
8106
8107    return Date.parse(str);
8108  };
8109
8110  const parsers = {
8111    /**
8112     * Specifies the duration of the entire Media Presentation. Format is a duration string
8113     * as specified in ISO 8601
8114     *
8115     * @param {string} value
8116     *        value of attribute as a string
8117     * @return {number}
8118     *         The duration in seconds
8119     */
8120    mediaPresentationDuration(value) {
8121      return parseDuration(value);
8122    },
8123
8124    /**
8125     * Specifies the Segment availability start time for all Segments referred to in this
8126     * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
8127     * time. Format is a date string as specified in ISO 8601
8128     *
8129     * @param {string} value
8130     *        value of attribute as a string
8131     * @return {number}
8132     *         The date as seconds from unix epoch
8133     */
8134    availabilityStartTime(value) {
8135      return parseDate(value) / 1000;
8136    },
8137
8138    /**
8139     * Specifies the smallest period between potential changes to the MPD. Format is a
8140     * duration string as specified in ISO 8601
8141     *
8142     * @param {string} value
8143     *        value of attribute as a string
8144     * @return {number}
8145     *         The duration in seconds
8146     */
8147    minimumUpdatePeriod(value) {
8148      return parseDuration(value);
8149    },
8150
8151    /**
8152     * Specifies the suggested presentation delay. Format is a
8153     * duration string as specified in ISO 8601
8154     *
8155     * @param {string} value
8156     *        value of attribute as a string
8157     * @return {number}
8158     *         The duration in seconds
8159     */
8160    suggestedPresentationDelay(value) {
8161      return parseDuration(value);
8162    },
8163
8164    /**
8165     * specifices the type of mpd. Can be either "static" or "dynamic"
8166     *
8167     * @param {string} value
8168     *        value of attribute as a string
8169     *
8170     * @return {string}
8171     *         The type as a string
8172     */
8173    type(value) {
8174      return value;
8175    },
8176
8177    /**
8178     * Specifies the duration of the smallest time shifting buffer for any Representation
8179     * in the MPD. Format is a duration string as specified in ISO 8601
8180     *
8181     * @param {string} value
8182     *        value of attribute as a string
8183     * @return {number}
8184     *         The duration in seconds
8185     */
8186    timeShiftBufferDepth(value) {
8187      return parseDuration(value);
8188    },
8189
8190    /**
8191     * Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
8192     * Format is a duration string as specified in ISO 8601
8193     *
8194     * @param {string} value
8195     *        value of attribute as a string
8196     * @return {number}
8197     *         The duration in seconds
8198     */
8199    start(value) {
8200      return parseDuration(value);
8201    },
8202
8203    /**
8204     * Specifies the width of the visual presentation
8205     *
8206     * @param {string} value
8207     *        value of attribute as a string
8208     * @return {number}
8209     *         The parsed width
8210     */
8211    width(value) {
8212      return parseInt(value, 10);
8213    },
8214
8215    /**
8216     * Specifies the height of the visual presentation
8217     *
8218     * @param {string} value
8219     *        value of attribute as a string
8220     * @return {number}
8221     *         The parsed height
8222     */
8223    height(value) {
8224      return parseInt(value, 10);
8225    },
8226
8227    /**
8228     * Specifies the bitrate of the representation
8229     *
8230     * @param {string} value
8231     *        value of attribute as a string
8232     * @return {number}
8233     *         The parsed bandwidth
8234     */
8235    bandwidth(value) {
8236      return parseInt(value, 10);
8237    },
8238
8239    /**
8240     * Specifies the frame rate of the representation
8241     *
8242     * @param {string} value
8243     *        value of attribute as a string
8244     * @return {number}
8245     *         The parsed frame rate
8246     */
8247    frameRate(value) {
8248      return parseDivisionValue(value);
8249    },
8250
8251    /**
8252     * Specifies the number of the first Media Segment in this Representation in the Period
8253     *
8254     * @param {string} value
8255     *        value of attribute as a string
8256     * @return {number}
8257     *         The parsed number
8258     */
8259    startNumber(value) {
8260      return parseInt(value, 10);
8261    },
8262
8263    /**
8264     * Specifies the timescale in units per seconds
8265     *
8266     * @param {string} value
8267     *        value of attribute as a string
8268     * @return {number}
8269     *         The parsed timescale
8270     */
8271    timescale(value) {
8272      return parseInt(value, 10);
8273    },
8274
8275    /**
8276     * Specifies the presentationTimeOffset.
8277     *
8278     * @param {string} value
8279     *        value of the attribute as a string
8280     *
8281     * @return {number}
8282     *         The parsed presentationTimeOffset
8283     */
8284    presentationTimeOffset(value) {
8285      return parseInt(value, 10);
8286    },
8287
8288    /**
8289     * Specifies the constant approximate Segment duration
8290     * NOTE: The <Period> element also contains an @duration attribute. This duration
8291     *       specifies the duration of the Period. This attribute is currently not
8292     *       supported by the rest of the parser, however we still check for it to prevent
8293     *       errors.
8294     *
8295     * @param {string} value
8296     *        value of attribute as a string
8297     * @return {number}
8298     *         The parsed duration
8299     */
8300    duration(value) {
8301      const parsedValue = parseInt(value, 10);
8302
8303      if (isNaN(parsedValue)) {
8304        return parseDuration(value);
8305      }
8306
8307      return parsedValue;
8308    },
8309
8310    /**
8311     * Specifies the Segment duration, in units of the value of the @timescale.
8312     *
8313     * @param {string} value
8314     *        value of attribute as a string
8315     * @return {number}
8316     *         The parsed duration
8317     */
8318    d(value) {
8319      return parseInt(value, 10);
8320    },
8321
8322    /**
8323     * Specifies the MPD start time, in @timescale units, the first Segment in the series
8324     * starts relative to the beginning of the Period
8325     *
8326     * @param {string} value
8327     *        value of attribute as a string
8328     * @return {number}
8329     *         The parsed time
8330     */
8331    t(value) {
8332      return parseInt(value, 10);
8333    },
8334
8335    /**
8336     * Specifies the repeat count of the number of following contiguous Segments with the
8337     * same duration expressed by the value of @d
8338     *
8339     * @param {string} value
8340     *        value of attribute as a string
8341     * @return {number}
8342     *         The parsed number
8343     */
8344    r(value) {
8345      return parseInt(value, 10);
8346    },
8347
8348    /**
8349     * Specifies the presentationTime.
8350     *
8351     * @param {string} value
8352     *        value of the attribute as a string
8353     *
8354     * @return {number}
8355     *         The parsed presentationTime
8356     */
8357    presentationTime(value) {
8358      return parseInt(value, 10);
8359    },
8360
8361    /**
8362     * 
8362Default parser for all other attributes. Acts as a no-op and just returns the value
8363     * as a string
8364     *
8365     * @param {string} value
8366     *        value of attribute as a string
8367     * @return {string}
8368     *         Unparsed value
8369     */
8370    DEFAULT(value) {
8371      return value;
8372    }
8373
8374  };
8375  /**
8376   * Gets all the attributes and values of the provided node, parses attributes with known
8377   * types, and returns an object with attribute names mapped to values.
8378   *
8379   * @param {Node} el
8380   *        The node to parse attributes from
8381   * @return {Object}
8382   *         Object with all attributes of el parsed
8383   */
8384
8385  const parseAttributes = el => {
8386    if (!(el && el.attributes)) {
8387      return {};
8388    }
8389
8390    return from(el.attributes).reduce((a, e) => {
8391      const parseFn = parsers[e.name] || parsers.DEFAULT;
8392      a[e.n
vendor: 31,526 bytes, lines 8392-9250
8392ame] = parseFn(e.value);
8393      return a;
8394    }, {});
8395  };
8396
8397  const keySystemsMap = {
8398    'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey',
8399    'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha',
8400    'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready',
8401    'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime',
8402    // ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme
8403    'urn:mpeg:dash:mp4protection:2011': 'mp4protection'
8404  };
8405  /**
8406   * Builds a list of urls that is the product of the reference urls and BaseURL values
8407   *
8408   * @param {Object[]} references
8409   *        List of objects containing the reference URL as well as its attributes
8410   * @param {Node[]} baseUrlElements
8411   *        List of BaseURL nodes from the mpd
8412   * @return {Object[]}
8413   *         List of objects with resolved urls and attributes
8414   */
8415
8416  const buildBaseUrls = (references, baseUrlElements) => {
8417    if (!baseUrlElements.length) {
8418      return references;
8419    }
8420
8421    return flatten(references.map(function (reference) {
8422      return baseUrlElements.map(function (baseUrlElement) {
8423        const initialBaseUrl = getContent(baseUrlElement);
8424        const resolvedBaseUrl = resolveUrl$1(reference.baseUrl, initialBaseUrl);
8425        const finalBaseUrl = merge(parseAttributes(baseUrlElement), {
8426          baseUrl: resolvedBaseUrl
8427        }); // If the URL is resolved, we want to get the serviceLocation from the reference
8428        // assuming there is no serviceLocation on the initialBaseUrl
8429
8430        if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) {
8431          finalBaseUrl.serviceLocation = reference.serviceLocation;
8432        }
8433
8434        return finalBaseUrl;
8435      });
8436    }));
8437  };
8438  /**
8439   * Contains all Segment information for its containing AdaptationSet
8440   *
8441   * @typedef {Object} SegmentInformation
8442   * @property {Object|undefined} template
8443   *           Contains the attributes for the SegmentTemplate node
8444   * @property {Object[]|undefined} segmentTimeline
8445   *           Contains a list of atrributes for each S node within the SegmentTimeline node
8446   * @property {Object|undefined} list
8447   *           Contains the attributes for the SegmentList node
8448   * @property {Object|undefined} base
8449   *           Contains the attributes for the SegmentBase node
8450   */
8451
8452  /**
8453   * Returns all available Segment information contained within the AdaptationSet node
8454   *
8455   * @param {Node} adaptationSet
8456   *        The AdaptationSet node to get Segment information from
8457   * @return {SegmentInformation}
8458   *         The Segment information contained within the provided AdaptationSet
8459   */
8460
8461
8462  const getSegmentInformation = adaptationSet => {
8463    const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0];
8464    const segmentList = findChildren(adaptationSet, 'SegmentList')[0];
8465    const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge({
8466      tag: 'SegmentURL'
8467    }, parseAttributes(s)));
8468    const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0];
8469    const segmentTimelineParentNode = segmentList || segmentTemplate;
8470    const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0];
8471    const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate;
8472    const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both
8473    // @initialization and an <Initialization> node.  @initialization can be templated,
8474    // while the node can have a url and range specified.  If the <SegmentTemplate> has
8475    // both @initialization and an <Initialization> subelement we opt to override with
8476    // the node, as this interaction is not defined in the spec.
8477
8478    const template = segmentTemplate && parseAttributes(segmentTemplate);
8479
8480    if (template && segmentInitialization) {
8481      template.initialization = segmentInitialization && parseAttributes(segmentInitialization);
8482    } else if (template && template.initialization) {
8483      // If it is @initialization we convert it to an object since this is the format that
8484      // later functions will rely on for the initialization segment.  This is only valid
8485      // for <SegmentTemplate>
8486      template.initialization = {
8487        sourceURL: template.initialization
8488      };
8489    }
8490
8491    const segmentInfo = {
8492      template,
8493      segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)),
8494      list: segmentList && merge(parseAttributes(segmentList), {
8495        segmentUrls,
8496        initialization: parseAttributes(segmentInitialization)
8497      }),
8498      base: segmentBase && merge(parseAttributes(segmentBase), {
8499        initialization: parseAttributes(segmentInitialization)
8500      })
8501    };
8502    Object.keys(segmentInfo).forEach(key => {
8503      if (!segmentInfo[key]) {
8504        delete segmentInfo[key];
8505      }
8506    });
8507    return segmentInfo;
8508  };
8509  /**
8510   * Contains Segment information and attributes needed to construct a Playlist object
8511   * from a Representation
8512   *
8513   * @typedef {Object} RepresentationInformation
8514   * @property {SegmentInformation} segmentInfo
8515   *           Segment information for this Representation
8516   * @property {Object} attributes
8517   *           Inherited attributes for this Representation
8518   */
8519
8520  /**
8521   * Maps a Representation node to an object containing Segment information and attributes
8522   *
8523   * @name inheritBaseUrlsCallback
8524   * @function
8525   * @param {Node} representation
8526   *        Representation node from the mpd
8527   * @return {RepresentationInformation}
8528   *         Representation information needed to construct a Playlist object
8529   */
8530
8531  /**
8532   * Returns a callback for Array.prototype.map for mapping Representation nodes to
8533   * Segment information and attributes using inherited BaseURL nodes.
8534   *
8535   * @param {Object} adaptationSetAttributes
8536   *        Contains attributes inherited by the AdaptationSet
8537   * @param {Object[]} adaptationSetBaseUrls
8538   *        List of objects containing resolved base URLs and attributes
8539   *        inherited by the AdaptationSet
8540   * @param {SegmentInformation} adaptationSetSegmentInfo
8541   *        Contains Segment information for the AdaptationSet
8542   * @return {inheritBaseUrlsCallback}
8543   *         Callback map function
8544   */
8545
8546
8547  const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => {
8548    const repBaseUrlElements = findChildren(representation, 'BaseURL');
8549    const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements);
8550    const attributes = merge(adaptationSetAttributes, parseAttributes(representation));
8551    const representationSegmentInfo = getSegmentInformation(representation);
8552    return repBaseUrls.map(baseUrl => {
8553      return {
8554        segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo),
8555        attributes: merge(attributes, baseUrl)
8556      };
8557    });
8558  };
8559  /**
8560   * Tranforms a series of content protection nodes to
8561   * an object containing pssh data by key system
8562   *
8563   * @param {Node[]} contentProtectionNodes
8564   *        Content protection nodes
8565   * @return {Object}
8566   *        Object containing pssh data by key system
8567   */
8568
8569
8570  const generateKeySystemInformation = contentProtectionNodes => {
8571    return contentProtectionNodes.reduce((acc, node) => {
8572      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
8573      // as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system
8574      // UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do
8575      // .toLowerCase() on the input UUID string from the manifest (at least I could not think of one).
8576
8577      if (attributes.schemeIdUri) {
8578        attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase();
8579      }
8580
8581      const keySystem = keySystemsMap[attributes.schemeIdUri];
8582
8583      if (keySystem) {
8584        acc[keySystem] = {
8585          attributes
8586        };
8587        const psshNode = findChildren(node, 'cenc:pssh')[0];
8588
8589        if (psshNode) {
8590          const pssh = getContent(psshNode);
8591          acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh);
8592        }
8593      }
8594
8595      return acc;
8596    }, {});
8597  }; // defined in ANSI_SCTE 214-1 2016
8598
8599
8600  const parseCaptionServiceMetadata = service => {
8601    // 608 captions
8602    if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') {
8603      const values = typeof service.value !== 'string' ? [] : service.value.split(';');
8604      return values.map(value => {
8605        let channel;
8606        let language; // default language to value
8607
8608        language = value;
8609
8610        if (/^CC\d=/.test(value)) {
8611          [channel, language] = value.split('=');
8612        } else if (/^CC\d$/.test(value)) {
8613          channel = value;
8614        }
8615
8616        return {
8617          channel,
8618          language
8619        };
8620      });
8621    } else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') {
8622      const values = typeof service.value !== 'string' ? [] : service.value.split(';');
8623      return values.map(value => {
8624        const flags = {
8625          // service or channel number 1-63
8626          'channel': undefined,
8627          // language is a 3ALPHA per ISO 639.2/B
8628          // field is required
8629          'language': undefined,
8630          // BIT 1/0 or ?
8631          // default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown
8632          'aspectRatio': 1,
8633          // BIT 1/0
8634          // easy reader flag indicated the text is tailed to the needs of beginning readers
8635          // default 0, or off
8636          'easyReader': 0,
8637          // BIT 1/0
8638          // If 3d metadata is present (CEA-708.1) then 1
8639          // default 0
8640          '3D': 0
8641        };
8642
8643        if (/=/.test(value)) {
8644          const [channel, opts = ''] = value.split('=');
8645          flags.channel = channel;
8646          flags.language = value;
8647          opts.split(',').forEach(opt => {
8648            const [name, val] = opt.split(':');
8649
8650            if (name === 'lang') {
8651              flags.language = val; // er for easyReadery
8652            } else if (name === 'er') {
8653              flags.easyReader = Number(val); // war for wide aspect ratio
8654            } else if (name === 'war') {
8655              flags.aspectRatio = Number(val);
8656            } else if (name === '3D') {
8657              flags['3D'] = Number(val);
8658            }
8659          });
8660        } else {
8661          flags.language = value;
8662        }
8663
8664        if (flags.channel) {
8665          flags.channel = 'SERVICE' + flags.channel;
8666        }
8667
8668        return flags;
8669      });
8670    }
8671  };
8672  /**
8673   * A map callback that will parse all event stream data for a collection of periods
8674   * DASH ISO_IEC_23009 5.10.2.2
8675   * https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing
8676   *
8677   * @param {PeriodInformation} period object containing necessary period information
8678   * @return a collection of parsed eventstream event objects
8679   */
8680
8681
8682  const toEventStream = period => {
8683    // get and flatten all EventStreams tags and parse attributes and children
8684    return flatten(findChildren(period.node, 'EventStream').map(eventStream => {
8685      const eventStreamAttributes = parseAttributes(eventStream);
8686      const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects
8687
8688      return findChildren(eventStream, 'Event').map(event => {
8689        const eventAttributes = parseAttributes(event);
8690        const presentationTime = eventAttributes.presentationTime || 0;
8691        const timescale = eventStreamAttributes.timescale || 1;
8692        const duration = eventAttributes.duration || 0;
8693        const start = presentationTime / timescale + period.attributes.start;
8694        return {
8695          schemeIdUri,
8696          value: eventStreamAttributes.value,
8697          id: eventAttributes.id,
8698          start,
8699          end: start + duration / timescale,
8700          messageData: getContent(event) || eventAttributes.messageData,
8701          contentEncoding: eventStreamAttributes.contentEncoding,
8702          presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0
8703        };
8704      });
8705    }));
8706  };
8707  /**
8708   * Maps an AdaptationSet node to a list of Representation information objects
8709   *
8710   * @name toRepresentationsCallback
8711   * @function
8712   * @param {Node} adaptationSet
8713   *        AdaptationSet node from the mpd
8714   * @return {RepresentationInformation[]}
8715   *         List of objects containing Representaion information
8716   */
8717
8718  /**
8719   * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of
8720   * Representation information objects
8721   *
8722   * @param {Object} periodAttributes
8723   *        Contains attributes inherited by the Period
8724   * @param {Object[]} periodBaseUrls
8725   *        Contains list of objects with resolved base urls and attributes
8726   *        inherited by the Period
8727   * @param {string[]} periodSegmentInfo
8728   *        Contains Segment Information at the period level
8729   * @return {toRepresentationsCallback}
8730   *         Callback map function
8731   */
8732
8733
8734  const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => {
8735    const adaptationSetAttributes = parseAttributes(adaptationSet);
8736    const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL'));
8737    const role = findChildren(adaptationSet, 'Role')[0];
8738    const roleAttributes = {
8739      role: parseAttributes(role)
8740    };
8741    let attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes);
8742    const accessibility = findChildren(adaptationSet, 'Accessibility')[0];
8743    const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility));
8744
8745    if (captionServices) {
8746      attrs = merge(attrs, {
8747        captionServices
8748      });
8749    }
8750
8751    const label = findChildren(adaptationSet, 'Label')[0];
8752
8753    if (label && label.childNodes.length) {
8754      const labelVal = label.childNodes[0].nodeValue.trim();
8755      attrs = merge(attrs, {
8756        label: labelVal
8757      });
8758    }
8759
8760    const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection'));
8761
8762    if (Object.keys(contentProtection).length) {
8763      attrs = merge(attrs, {
8764        contentProtection
8765      });
8766    }
8767
8768    const segmentInfo = getSegmentInformation(adaptationSet);
8769    const representations = findChildren(adaptationSet, 'Representation');
8770    const adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo);
8771    return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo)));
8772  };
8773  /**
8774   * Contains all period information for mapping nodes onto adaptation sets.
8775   *
8776   * @typedef {Object} PeriodInformation
8777   * @property {Node} period.node
8778   *           Period node from the mpd
8779   * @property {Object} period.attributes
8780   *           Parsed period attributes from node plus any added
8781   */
8782
8783  /**
8784   * Maps a PeriodInformation object to a list of Representation information objects for all
8785   * AdaptationSet nodes contained within the Period.
8786   *
8787   * @name toAdaptationSetsCallback
8788   * @function
8789   * @param {PeriodInformation} period
8790   *        Period object containing necessary period information
8791   * @param {number} periodStart
8792   *        Start time of the Period within the mpd
8793   * @return {RepresentationInformation[]}
8794   *         List of objects containing Representaion information
8795   */
8796
8797  /**
8798   * Returns a callback for Array.prototype.map for mapping Period nodes to a list of
8799   * Representation information objects
8800   *
8801   * @param {Object} mpdAttributes
8802   *        Contains attributes inherited by the mpd
8803    * @param {Object[]} mpdBaseUrls
8804   *        Contains list of objects with resolved base urls and attributes
8805   *        inherited by the mpd
8806   * @return {toAdaptationSetsCallback}
8807   *         Callback map function
8808   */
8809
8810
8811  const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
8812    const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL'));
8813    const periodAttributes = merge(mpdAttributes, {
8814      periodStart: period.attributes.start
8815    });
8816
8817    if (typeof period.attributes.duration === 'number') {
8818      periodAttributes.periodDuration = period.attributes.duration;
8819    }
8820
8821    const adaptationSets = findChildren(period.node, 'AdaptationSet');
8822    const periodSegmentInfo = getSegmentInformation(period.node);
8823    return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo)));
8824  };
8825  /**
8826   * Tranforms an array of content steering nodes into an object
8827   * containing CDN content steering information from the MPD manifest.
8828   *
8829   * For more information on the DASH spec for Content Steering parsing, see:
8830   * https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
8831   *
8832   * @param {Node[]} contentSteeringNodes
8833   *        Content steering nodes
8834   * @param {Function} eventHandler
8835   *        The event handler passed into the parser options to handle warnings
8836   * @return {Object}
8837   *        Object containing content steering data
8838   */
8839
8840
8841  const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => {
8842    // If there are more than one ContentSteering tags, throw an error
8843    if (contentSteeringNodes.length > 1) {
8844      eventHandler({
8845        type: 'warn',
8846        message: 'The MPD manifest should contain no more than one ContentSteering tag'
8847      });
8848    } // Return a null value if there are no ContentSteering tags
8849
8850
8851    if (!contentSteeringNodes.length) {
8852      return null;
8853    }
8854
8855    const infoFromContentSteeringTag = merge({
8856      serverURL: getContent(contentSteeringNodes[0])
8857    }, parseAttributes(contentSteeringNodes[0])); // Converts `queryBeforeStart` to a boolean, as well as setting the default value
8858    // to `false` if it doesn't exist
8859
8860    infoFromContentSteeringTag.queryBeforeStart = infoFromContentSteeringTag.queryBeforeStart === 'true';
8861    return infoFromContentSteeringTag;
8862  };
8863  /**
8864   * Gets Period@start property for a given period.
8865   *
8866   * @param {Object} options
8867   *        Options object
8868   * @param {Object} options.attributes
8869   *        Period attributes
8870   * @param {Object} [options.priorPeriodAttributes]
8871   *        Prior period attributes (if prior period is available)
8872   * @param {string} options.mpdType
8873   *        The MPD@type these periods came from
8874   * @return {number|null}
8875   *         The period start, or null if it's an early available period or error
8876   */
8877
8878
8879  const getPeriodStart = ({
8880    attributes,
8881    priorPeriodAttributes,
8882    mpdType
8883  }) => {
8884    // Summary of period start time calculation from DASH spec section 5.3.2.1
8885    //
8886    // A period's start is the first period's start + time elapsed after playing all
8887    // prior periods to this one. Periods continue one after the other in time (without
8888    // gaps) until the end of the presentation.
8889    //
8890    // The value of Period@start should be:
8891    // 1. if Period@start is present: value of Period@start
8892    // 2. if previous period exists and it has @duration: previous Period@start +
8893    //    previous Period@duration
8894    // 3. if this is first period and MPD@type is 'static': 0
8895    // 4. in all other cases, consider the period an "early available period" (note: not
8896    //    currently supported)
8897    // (1)
8898    if (typeof attributes.start === 'number') {
8899      return attributes.start;
8900    } // (2)
8901
8902
8903    if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') {
8904      return priorPeriodAttributes.start + priorPeriodAttributes.duration;
8905    } // (3)
8906
8907
8908    if (!priorPeriodAttributes && mpdType === 'static') {
8909      return 0;
8910    } // (4)
8911    // There is currently no logic for calculating the Period@start value if there is
8912    // no Period@start or prior Period@start and Period@duration available. This is not made
8913    // explicit by the DASH interop guidelines or the DASH spec, however, since there's
8914    // nothing about any other resolution strategies, it's implied. Thus, this case should
8915    // be considered an early available period, or error, and null should suffice for both
8916    // of those cases.
8917
8918
8919    return null;
8920  };
8921  /**
8922   * Traverses the mpd xml tree to generate a list of Representation information objects
8923   * that have inherited attributes from parent nodes
8924   *
8925   * @param {Node} mpd
8926   *        The root node of the mpd
8927   * @param {Object} options
8928   *        Available options for inheritAttributes
8929   * @param {string} options.manifestUri
8930   *        The uri source of the mpd
8931   * @param {number} options.NOW
8932   *        Current time per DASH IOP.  Default is current time in ms since epoch
8933   * @param {number} options.clientOffset
8934   *        Client time difference from NOW (in milliseconds)
8935   * @return {RepresentationInformation[]}
8936   *         List of objects containing Representation information
8937   */
8938
8939
8940  const inheritAttributes = (mpd, options = {}) => {
8941    const {
8942      manifestUri = '',
8943      NOW = Date.now(),
8944      clientOffset = 0,
8945      // TODO: For now, we are expecting an eventHandler callback function
8946      // to be passed into the mpd parser as an option.
8947      // In the future, we should enable stream parsing by using the Stream class from vhs-utils.
8948      // This will support new features including a standardized event handler.
8949      // See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing.
8950      // https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9
8951      eventHandler = function () {}
8952    } = options;
8953    const periodNodes = findChildren(mpd, 'Period');
8954
8955    if (!periodNodes.length) {
8956      throw new Error(errors.INVALID_NUMBER_OF_PERIOD);
8957    }
8958
8959    const locations = findChildren(mpd, 'Location');
8960    const mpdAttributes = parseAttributes(mpd);
8961    const mpdBaseUrls = buildBaseUrls([{
8962      baseUrl: manifestUri
8963    }], findChildren(mpd, 'BaseURL'));
8964    const contentSteeringNodes = findChildren(mpd, 'ContentSteering'); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'.
8965
8966    mpdAttributes.type = mpdAttributes.type || 'static';
8967    mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0;
8968    mpdAttributes.NOW = NOW;
8969    mpdAttributes.clientOffset = clientOffset;
8970
8971    if (locations.length) {
8972      mpdAttributes.locations = locations.map(getContent);
8973    }
8974
8975    const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to
8976    // adding properties that require looking at prior periods is to parse attributes and add
8977    // missing ones before toAdaptationSets is called. If more such properties are added, it
8978    // may be better to refactor toAdaptationSets.
8979
8980    periodNodes.forEach((node, index) => {
8981      const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary
8982      // for this period.
8983
8984      const priorPeriod = periods[index - 1];
8985      attributes.start = getPeriodStart({
8986        attributes,
8987        priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null,
8988        mpdType: mpdAttributes.type
8989      });
8990      periods.push({
8991        node,
8992        attributes
8993      });
8994    });
8995    return {
8996      locations: mpdAttributes.locations,
8997      contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler),
8998      // TODO: There are occurences where this `representationInfo` array contains undesired
8999      // duplicates. This generally occurs when there are multiple BaseURL nodes that are
9000      // direct children of the MPD node. When we attempt to resolve URLs from a combination of the
9001      // parent BaseURL and a child BaseURL, and the value does not resolve,
9002      // we end up returning the child BaseURL multiple times.
9003      // We need to determine a way to remove these duplicates in a safe way.
9004      // See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527
9005      representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))),
9006      eventStream: flatten(periods.map(toEventStream))
9007    };
9008  };
9009
9010  const stringToMpdXml = manifestString => {
9011    if (manifestString === '') {
9012      throw new Error(errors.DASH_EMPTY_MANIFEST);
9013    }
9014
9015    const parser = new xmldom.DOMParser();
9016    let xml;
9017    let mpd;
9018
9019    try {
9020      xml = parser.parseFromString(manifestString, 'application/xml');
9021      mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null;
9022    } catch (e) {// ie 11 throws on invalid xml
9023    }
9024
9025    if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) {
9026      throw new Error(errors.DASH_INVALID_XML);
9027    }
9028
9029    return mpd;
9030  };
9031  /**
9032   * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
9033   *
9034   * @param {string} mpd
9035   *        XML string of the MPD manifest
9036   * @return {Object|null}
9037   *         Attributes of UTCTiming node specified in the manifest. Null if none found
9038   */
9039
9040
9041  const parseUTCTimingScheme = mpd => {
9042    const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
9043
9044    if (!UTCTimingNode) {
9045      return null;
9046    }
9047
9048    const attributes = parseAttributes(UTCTimingNode);
9049
9050    switch (attributes.schemeIdUri) {
9051      case 'urn:mpeg:dash:utc:http-head:2014':
9052      case 'urn:mpeg:dash:utc:http-head:2012':
9053        attributes.method = 'HEAD';
9054        break;
9055
9056      case 'urn:mpeg:dash:utc:http-xsdate:2014':
9057      case 'urn:mpeg:dash:utc:http-iso:2014':
9058      case 'urn:mpeg:dash:utc:http-xsdate:2012':
9059      case 'urn:mpeg:dash:utc:http-iso:2012':
9060        attributes.method = 'GET';
9061        break;
9062
9063      case 'urn:mpeg:dash:utc:direct:2014':
9064      case 'urn:mpeg:dash:utc:direct:2012':
9065        attributes.method = 'DIRECT';
9066        attributes.value = Date.parse(attributes.value);
9067        break;
9068
9069      case 'urn:mpeg:dash:utc:http-ntp:2014':
9070      case 'urn:mpeg:dash:utc:ntp:2014':
9071      case 'urn:mpeg:dash:utc:sntp:2014':
9072      default:
9073        throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
9074    }
9075
9076    return attributes;
9077  };
9078  /*
9079   * Given a DASH manifest string and options, parses the DASH manifest into an object in the
9080   * form outputed by m3u8-parser and accepted by videojs/http-streaming.
9081   *
9082   * For live DASH manifests, if `previousManifest` is provided in options, then the newly
9083   * parsed DASH manifest will have its media sequence and discontinuity sequence values
9084   * updated to reflect its position relative to the prior manifest.
9085   *
9086   * @param {string} manifestString - the DASH manifest as a string
9087   * @param {options} [options] - any options
9088   *
9089   * @return {Object} the manifest object
9090   */
9091
9092  const parse = (manifestString, options = {}) => {
9093    const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
9094    const playlists = toPlaylists(parsedManifestInfo.representationInfo);
9095    return toM3u8({
9096      dashPlaylists: playlists,
9097      locations: parsedManifestInfo.locations,
9098      contentSteering: parsedManifestInfo.contentSteeringInfo,
9099      sidxMapping: options.sidxMapping,
9100      previousManifest: options.previousManifest,
9101      eventStream: parsedManifestInfo.eventStream
9102    });
9103  };
9104  /**
9105   * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
9106   *
9107   * @param {string} manifestString
9108   *        XML string of the MPD manifest
9109   * @return {Object|null}
9110   *         Attributes of UTCTiming node specified in the manifest. Null if none found
9111   */
9112
9113
9114  const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString));
9115
9116  var MAX_UINT32 = Math.pow(2, 32);
9117
9118  var getUint64$1 = function (uint8) {
9119    var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength);
9120    var value;
9121
9122    if (dv.getBigUint64) {
9123      value = dv.getBigUint64(0);
9124
9125      if (value < Number.MAX_SAFE_INTEGER) {
9126        return Number(value);
9127      }
9128
9129      return value;
9130    }
9131
9132    return dv.getUint32(0) * MAX_UINT32 + dv.getUint32(4);
9133  };
9134
9135  var numbers = {
9136    getUint64: getUint64$1,
9137    MAX_UINT32: MAX_UINT32
9138  };
9139
9140  var getUint64 = numbers.getUint64;
9141
9142  var parseSidx = function (data) {
9143    var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
9144        result = {
9145      version: data[0],
9146      flags: new Uint8Array(data.subarray(1, 4)),
9147      references: [],
9148      referenceId: view.getUint32(4),
9149      timescale: view.getUint32(8)
9150    },
9151        i = 12;
9152
9153    if (result.version === 0) {
9154      result.earliestPresentationTime = view.getUint32(i);
9155      result.firstOffset = view.getUint32(i + 4);
9156      i += 8;
9157    } else {
9158      // read 64 bits
9159      result.earliestPresentationTime = getUint64(data.subarray(i));
9160      result.firstOffset = getUint64(data.subarray(i + 8));
9161      i += 16;
9162    }
9163
9164    i += 2; // reserved
9165
9166    var referenceCount = view.getUint16(i);
9167    i += 2; // start of references
9168
9169    for (; referenceCount > 0; i += 12, referenceCount--) {
9170      result.references.push({
9171        referenceType: (data[i] & 0x80) >>> 7,
9172        referencedSize: view.getUint32(i) & 0x7FFFFFFF,
9173        subsegmentDuration: view.getUint32(i + 4),
9174        startsWithSap: !!(data[i + 8] & 0x80),
9175        sapType: (data[i + 8] & 0x70) >>> 4,
9176        sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
9177      });
9178    }
9179
9180    return result;
9181  };
9182
9183  var parseSidx_1 = parseSidx;
9184
9185  var ID3 = toUint8([0x49, 0x44, 0x33]);
9186  var getId3Size = function getId3Size(bytes, offset) {
9187    if (offset === void 0) {
9188      offset = 0;
9189    }
9190
9191    bytes = toUint8(bytes);
9192    var flags = bytes[offset + 5];
9193    var returnSize = bytes[offset + 6] << 21 | bytes[offset + 7] << 14 | bytes[offset + 8] << 7 | bytes[offset + 9];
9194    var footerPresent = (flags & 16) >> 4;
9195
9196    if (footerPresent) {
9197      return returnSize + 20;
9198    }
9199
9200    return returnSize + 10;
9201  };
9202  var getId3Offset = function getId3Offset(bytes, offset) {
9203    if (offset === void 0) {
9204      offset = 0;
9205    }
9206
9207    bytes = toUint8(bytes);
9208
9209    if (bytes.length - offset < 10 || !bytesMatch(bytes, ID3, {
9210      offset: offset
9211    })) {
9212      return offset;
9213    }
9214
9215    offset += getId3Size(bytes, offset); // recursive check for id3 tags as some files
9216    // have multiple ID3 tag sections even though
9217    // they should not.
9218
9219    return getId3Offset(bytes, offset);
9220  };
9221
9222  var normalizePath$1 = function normalizePath(path) {
9223    if (typeof path === 'string') {
9224      return stringToBytes(path);
9225    }
9226
9227    if (typeof path === 'number') {
9228      return path;
9229    }
9230
9231    return path;
9232  };
9233
9234  var normalizePaths$1 = function normalizePaths(paths) {
9235    if (!Array.isArray(paths)) {
9236      return [normalizePath$1(paths)];
9237    }
9238
9239    return paths.map(function (p) {
9240      return normalizePath$1(p);
9241    });
9242  };
9243  /**
9244   * find any number of boxes by name given a path to it in an iso bmff
9245   * such as mp4.
9246   *
9247   * @param {TypedArray} bytes
9248   *        bytes for the iso bmff to search for boxes in
9249   *
9250   * @param {Uint8Array[]|string[]|string|Uint8Array}
vendor: 9,470 bytes, lines 9250-9565
9250 name
9251   *        An array of paths or a single path representing the name
9252   *        of boxes to search through in bytes. Paths may be
9253   *        uint8 (character codes) or strings.
9254   *
9255   * @param {boolean} [complete=false]
9256   *        Should we search only for complete boxes on the final path.
9257   *        This is very useful when you do not want to get back partial boxes
9258   *        in the case of streaming files.
9259   *
9260   * @return {Uint8Array[]}
9261   *         An array of the end paths that we found.
9262   */
9263
9264  var findBox = function findBox(bytes, paths, complete) {
9265    if (complete === void 0) {
9266      complete = false;
9267    }
9268
9269    paths = normalizePaths$1(paths);
9270    bytes = toUint8(bytes);
9271    var results = [];
9272
9273    if (!paths.length) {
9274      // short-circuit the search for empty paths
9275      return results;
9276    }
9277
9278    var i = 0;
9279
9280    while (i < bytes.length) {
9281      var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0;
9282      var type = bytes.subarray(i + 4, i + 8); // invalid box format.
9283
9284      if (size === 0) {
9285        break;
9286      }
9287
9288      var end = i + size;
9289
9290      if (end > bytes.length) {
9291        // this box is bigger than the number of bytes we have
9292        // and complete is set, we cannot find any more boxes.
9293        if (complete) {
9294          break;
9295        }
9296
9297        end = bytes.length;
9298      }
9299
9300      var data = bytes.subarray(i + 8, end);
9301
9302      if (bytesMatch(type, paths[0])) {
9303        if (paths.length === 1) {
9304          // this is the end of the path and we've found the box we were
9305          // looking for
9306          results.push(data);
9307        } else {
9308          // recursively search for the next box along the path
9309          results.push.apply(results, findBox(data, paths.slice(1), complete));
9310        }
9311      }
9312
9313      i = end;
9314    } // we've finished searching all of bytes
9315
9316
9317    return results;
9318  };
9319
9320  // https://matroska-org.github.io/libebml/specs.html
9321  // https://www.matroska.org/technical/elements.html
9322  // https://www.webmproject.org/docs/container/
9323
9324  var EBML_TAGS = {
9325    EBML: toUint8([0x1A, 0x45, 0xDF, 0xA3]),
9326    DocType: toUint8([0x42, 0x82]),
9327    Segment: toUint8([0x18, 0x53, 0x80, 0x67]),
9328    SegmentInfo: toUint8([0x15, 0x49, 0xA9, 0x66]),
9329    Tracks: toUint8([0x16, 0x54, 0xAE, 0x6B]),
9330    Track: toUint8([0xAE]),
9331    TrackNumber: toUint8([0xd7]),
9332    DefaultDuration: toUint8([0x23, 0xe3, 0x83]),
9333    TrackEntry: toUint8([0xAE]),
9334    TrackType: toUint8([0x83]),
9335    FlagDefault: toUint8([0x88]),
9336    CodecID: toUint8([0x86]),
9337    CodecPrivate: toUint8([0x63, 0xA2]),
9338    VideoTrack: toUint8([0xe0]),
9339    AudioTrack: toUint8([0xe1]),
9340    // Not used yet, but will be used for live webm/mkv
9341    // see https://www.matroska.org/technical/basics.html#block-structure
9342    // see https://www.matroska.org/technical/basics.html#simpleblock-structure
9343    Cluster: toUint8([0x1F, 0x43, 0xB6, 0x75]),
9344    Timestamp: toUint8([0xE7]),
9345    TimestampScale: toUint8([0x2A, 0xD7, 0xB1]),
9346    BlockGroup: toUint8([0xA0]),
9347    BlockDuration: toUint8([0x9B]),
9348    Block: toUint8([0xA1]),
9349    SimpleBlock: toUint8([0xA3])
9350  };
9351  /**
9352   * This is a simple table to determine the length
9353   * of things in ebml. The length is one based (starts at 1,
9354   * rather than zero) and for every zero bit before a one bit
9355   * we add one to length. We also need this table because in some
9356   * case we have to xor all the length bits from another value.
9357   */
9358
9359  var LENGTH_TABLE = [128, 64, 32, 16, 8, 4, 2, 1];
9360
9361  var getLength = function getLength(byte) {
9362    var len = 1;
9363
9364    for (var i = 0; i < LENGTH_TABLE.length; i++) {
9365      if (byte & LENGTH_TABLE[i]) {
9366        break;
9367      }
9368
9369      len++;
9370    }
9371
9372    return len;
9373  }; // length in ebml is stored in the first 4 to 8 bits
9374  // of the first byte. 4 for the id length and 8 for the
9375  // data size length. Length is measured by converting the number to binary
9376  // then 1 + the number of zeros before a 1 is encountered starting
9377  // from the left.
9378
9379
9380  var getvint = function getvint(bytes, offset, removeLength, signed) {
9381    if (removeLength === void 0) {
9382      removeLength = true;
9383    }
9384
9385    if (signed === void 0) {
9386      signed = false;
9387    }
9388
9389    var length = getLength(bytes[offset]);
9390    var valueBytes = bytes.subarray(offset, offset + length); // NOTE that we do **not** subarray here because we need to copy these bytes
9391    // as they will be modified below to remove the dataSizeLen bits and we do not
9392    // want to modify the original data. normally we could just call slice on
9393    // uint8array but ie 11 does not support that...
9394
9395    if (removeLength) {
9396      valueBytes = Array.prototype.slice.call(bytes, offset, offset + length);
9397      valueBytes[0] ^= LENGTH_TABLE[length - 1];
9398    }
9399
9400    return {
9401      length: length,
9402      value: bytesToNumber(valueBytes, {
9403        signed: signed
9404      }),
9405      bytes: valueBytes
9406    };
9407  };
9408
9409  var normalizePath = function normalizePath(path) {
9410    if (typeof path === 'string') {
9411      return path.match(/.{1,2}/g).map(function (p) {
9412        return normalizePath(p);
9413      });
9414    }
9415
9416    if (typeof path === 'number') {
9417      return numberToBytes(path);
9418    }
9419
9420    return path;
9421  };
9422
9423  var normalizePaths = function normalizePaths(paths) {
9424    if (!Array.isArray(paths)) {
9425      return [normalizePath(paths)];
9426    }
9427
9428    return paths.map(function (p) {
9429      return normalizePath(p);
9430    });
9431  };
9432
9433  var getInfinityDataSize = function getInfinityDataSize(id, bytes, offset) {
9434    if (offset >= bytes.length) {
9435      return bytes.length;
9436    }
9437
9438    var innerid = getvint(bytes, offset, false);
9439
9440    if (bytesMatch(id.bytes, innerid.bytes)) {
9441      return offset;
9442    }
9443
9444    var dataHeader = getvint(bytes, offset + innerid.length);
9445    return getInfinityDataSize(id, bytes, offset + dataHeader.length + dataHeader.value + innerid.length);
9446  };
9447  /**
9448   * Notes on the EBLM format.
9449   *
9450   * EBLM uses "vints" tags. Every vint tag contains
9451   * two parts
9452   *
9453   * 1. The length from the first byte. You get this by
9454   *    converting the byte to binary and counting the zeros
9455   *    before a 1. Then you add 1 to that. Examples
9456   *    00011111 = length 4 because there are 3 zeros before a 1.
9457   *    00100000 = length 3 because there are 2 zeros before a 1.
9458   *    00000011 = length 7 because there are 6 zeros before a 1.
9459   *
9460   * 2. The bits used for length are removed from the first byte
9461   *    Then all the bytes are merged into a value. NOTE: this
9462   *    is not the case for id ebml tags as there id includes
9463   *    length bits.
9464   *
9465   */
9466
9467
9468  var findEbml = function findEbml(bytes, paths) {
9469    paths = normalizePaths(paths);
9470    bytes = toUint8(bytes);
9471    var results = [];
9472
9473    if (!paths.length) {
9474      return results;
9475    }
9476
9477    var i = 0;
9478
9479    while (i < bytes.length) {
9480      var id = getvint(bytes, i, false);
9481      var dataHeader = getvint(bytes, i + id.length);
9482      var dataStart = i + id.length + dataHeader.length; // dataSize is unknown or this is a live stream
9483
9484      if (dataHeader.value === 0x7f) {
9485        dataHeader.value = getInfinityDataSize(id, bytes, dataStart);
9486
9487        if (dataHeader.value !== bytes.length) {
9488          dataHeader.value -= dataStart;
9489        }
9490      }
9491
9492      var dataEnd = dataStart + dataHeader.value > bytes.length ? bytes.length : dataStart + dataHeader.value;
9493      var data = bytes.subarray(dataStart, dataEnd);
9494
9495      if (bytesMatch(paths[0], id.bytes)) {
9496        if (paths.length === 1) {
9497          // this is the end of the paths and we've found the tag we were
9498          // looking for
9499          results.push(data);
9500        } else {
9501          // recursively search for the next tag inside of the data
9502          // of this one
9503          results = results.concat(findEbml(data, paths.slice(1)));
9504        }
9505      }
9506
9507      var totalLength = id.length + dataHeader.length + data.length; // move past this tag entirely, we are not looking for it
9508
9509      i += totalLength;
9510    }
9511
9512    return results;
9513  }; // see https://www.matroska.org/technical/basics.html#block-structure
9514
9515  var NAL_TYPE_ONE = toUint8([0x00, 0x00, 0x00, 0x01]);
9516  var NAL_TYPE_TWO = toUint8([0x00, 0x00, 0x01]);
9517  var EMULATION_PREVENTION = toUint8([0x00, 0x00, 0x03]);
9518  /**
9519   * Expunge any "Emulation Prevention" bytes from a "Raw Byte
9520   * Sequence Payload"
9521   *
9522   * @param data {Uint8Array} the bytes of a RBSP from a NAL
9523   * unit
9524   * @return {Uint8Array} the RBSP without any Emulation
9525   * Prevention Bytes
9526   */
9527
9528  var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(bytes) {
9529    var positions = [];
9530    var i = 1; // Find all `Emulation Prevention Bytes`
9531
9532    while (i < bytes.length - 2) {
9533      if (bytesMatch(bytes.subarray(i, i + 3), EMULATION_PREVENTION)) {
9534        positions.push(i + 2);
9535        i++;
9536      }
9537
9538      i++;
9539    } // If no Emulation Prevention Bytes were found just return the original
9540    // array
9541
9542
9543    if (positions.length === 0) {
9544      return bytes;
9545    } // Create a new array to hold the NAL unit data
9546
9547
9548    var newLength = bytes.length - positions.length;
9549    var newData = new Uint8Array(newLength);
9550    var sourceIndex = 0;
9551
9552    for (i = 0; i < newLength; sourceIndex++, i++) {
9553      if (sourceIndex === positions[0]) {
9554        // Skip this byte
9555        sourceIndex++; // Remove this position index
9556
9557        positions.shift();
9558      }
9559
9560      newData[i] = bytes[sourceIndex];
9561    }
9562
9563    return newData;
9564  };
9565  var findNal = function findNal(bytes, dataType, types, nalLimit) {
9566    if (nalLimit === void 0) {
9567      nalLimit = Infinity;
9568    }
vendor: 4,758 bytes, lines 9568-9723
9568
9569
9570    bytes = toUint8(bytes);
9571    types = [].concat(types);
9572    var i = 0;
9573    var nalStart;
9574    var nalsFound = 0; // keep searching until:
9575    // we reach the end of bytes
9576    // we reach the maximum number of nals they want to seach
9577    // NOTE: that we disregard nalLimit when we have found the start
9578    // of the nal we want so that we can find the end of the nal we want.
9579
9580    while (i < bytes.length && (nalsFound < nalLimit || nalStart)) {
9581      var nalOffset = void 0;
9582
9583      if (bytesMatch(bytes.subarray(i), NAL_TYPE_ONE)) {
9584        nalOffset = 4;
9585      } else if (bytesMatch(bytes.subarray(i), NAL_TYPE_TWO)) {
9586        nalOffset = 3;
9587      } // we are unsynced,
9588      // find the next nal unit
9589
9590
9591      if (!nalOffset) {
9592        i++;
9593        continue;
9594      }
9595
9596      nalsFound++;
9597
9598      if (nalStart) {
9599        return discardEmulationPreventionBytes(bytes.subarray(nalStart, i));
9600      }
9601
9602      var nalType = void 0;
9603
9604      if (dataType === 'h264') {
9605        nalType = bytes[i + nalOffset] & 0x1f;
9606      } else if (dataType === 'h265') {
9607        nalType = bytes[i + nalOffset] >> 1 & 0x3f;
9608      }
9609
9610      if (types.indexOf(nalType) !== -1) {
9611        nalStart = i + nalOffset;
9612      } // nal header is 1 length for h264, and 2 for h265
9613
9614
9615      i += nalOffset + (dataType === 'h264' ? 1 : 2);
9616    }
9617
9618    return bytes.subarray(0, 0);
9619  };
9620  var findH264Nal = function findH264Nal(bytes, type, nalLimit) {
9621    return findNal(bytes, 'h264', type, nalLimit);
9622  };
9623  var findH265Nal = function findH265Nal(bytes, type, nalLimit) {
9624    return findNal(bytes, 'h265', type, nalLimit);
9625  };
9626
9627  var CONSTANTS = {
9628    // "webm" string literal in hex
9629    'webm': toUint8([0x77, 0x65, 0x62, 0x6d]),
9630    // "matroska" string literal in hex
9631    'matroska': toUint8([0x6d, 0x61, 0x74, 0x72, 0x6f, 0x73, 0x6b, 0x61]),
9632    // "fLaC" string literal in hex
9633    'flac': toUint8([0x66, 0x4c, 0x61, 0x43]),
9634    // "OggS" string literal in hex
9635    'ogg': toUint8([0x4f, 0x67, 0x67, 0x53]),
9636    // ac-3 sync byte, also works for ec-3 as that is simply a codec
9637    // of ac-3
9638    'ac3': toUint8([0x0b, 0x77]),
9639    // "RIFF" string literal in hex used for wav and avi
9640    'riff': toUint8([0x52, 0x49, 0x46, 0x46]),
9641    // "AVI" string literal in hex
9642    'avi': toUint8([0x41, 0x56, 0x49]),
9643    // "WAVE" string literal in hex
9644    'wav': toUint8([0x57, 0x41, 0x56, 0x45]),
9645    // "ftyp3g" string literal in hex
9646    '3gp': toUint8([0x66, 0x74, 0x79, 0x70, 0x33, 0x67]),
9647    // "ftyp" string literal in hex
9648    'mp4': toUint8([0x66, 0x74, 0x79, 0x70]),
9649    // "styp" string literal in hex
9650    'fmp4': toUint8([0x73, 0x74, 0x79, 0x70]),
9651    // "ftypqt" string literal in hex
9652    'mov': toUint8([0x66, 0x74, 0x79, 0x70, 0x71, 0x74]),
9653    // moov string literal in hex
9654    'moov': toUint8([0x6D, 0x6F, 0x6F, 0x76]),
9655    // moof string literal in hex
9656    'moof': toUint8([0x6D, 0x6F, 0x6F, 0x66])
9657  };
9658  var _isLikely = {
9659    aac: function aac(bytes) {
9660      var offset = getId3Offset(bytes);
9661      return bytesMatch(bytes, [0xFF, 0x10], {
9662        offset: offset,
9663        mask: [0xFF, 0x16]
9664      });
9665    },
9666    mp3: function mp3(bytes) {
9667      var offset = getId3Offset(bytes);
9668      return bytesMatch(bytes, [0xFF, 0x02], {
9669        offset: offset,
9670        mask: [0xFF, 0x06]
9671      });
9672    },
9673    webm: function webm(bytes) {
9674      var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is webm
9675
9676      return bytesMatch(docType, CONSTANTS.webm);
9677    },
9678    mkv: function mkv(bytes) {
9679      var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is matroska
9680
9681      return bytesMatch(docType, CONSTANTS.matroska);
9682    },
9683    mp4: function mp4(bytes) {
9684      // if this file is another base media file format, it is not mp4
9685      if (_isLikely['3gp'](bytes) || _isLikely.mov(bytes)) {
9686        return false;
9687      } // if this file starts with a ftyp or styp box its mp4
9688
9689
9690      if (bytesMatch(bytes, CONSTANTS.mp4, {
9691        offset: 4
9692      }) || bytesMatch(bytes, CONSTANTS.fmp4, {
9693        offset: 4
9694      })) {
9695        return true;
9696      } // if this file starts with a moof/moov box its mp4
9697
9698
9699      if (bytesMatch(bytes, CONSTANTS.moof, {
9700        offset: 4
9701      }) || bytesMatch(bytes, CONSTANTS.moov, {
9702        offset: 4
9703      })) {
9704        return true;
9705      }
9706    },
9707    mov: function mov(bytes) {
9708      return bytesMatch(bytes, CONSTANTS.mov, {
9709        offset: 4
9710      });
9711    },
9712    '3gp': function gp(bytes) {
9713      return bytesMatch(bytes, CONSTANTS['3gp'], {
9714        offset: 4
9715      });
9716    },
9717    ac3: function ac3(bytes) {
9718      var offset = getId3Offset(bytes);
9719      return bytesMatch(bytes, CONSTANTS.ac3, {
9720        offset: offset
9721      });
9722    },
9723    ts: function ts(bytes) {
vendor: 39,877 bytes, lines 9724-10981
9724      if (bytes.length < 189 && bytes.length >= 1) {
9725        return bytes[0] === 0x47;
9726      }
9727
9728      var i = 0; // check the first 376 bytes for two matching sync bytes
9729
9730      while (i + 188 < bytes.length && i < 188) {
9731        if (bytes[i] === 0x47 && bytes[i + 188] === 0x47) {
9732          return true;
9733        }
9734
9735        i += 1;
9736      }
9737
9738      return false;
9739    },
9740    flac: function flac(bytes) {
9741      var offset = getId3Offset(bytes);
9742      return bytesMatch(bytes, CONSTANTS.flac, {
9743        offset: offset
9744      });
9745    },
9746    ogg: function ogg(bytes) {
9747      return bytesMatch(bytes, CONSTANTS.ogg);
9748    },
9749    avi: function avi(bytes) {
9750      return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.avi, {
9751        offset: 8
9752      });
9753    },
9754    wav: function wav(bytes) {
9755      return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.wav, {
9756        offset: 8
9757      });
9758    },
9759    'h264': function h264(bytes) {
9760      // find seq_parameter_set_rbsp
9761      return findH264Nal(bytes, 7, 3).length;
9762    },
9763    'h265': function h265(bytes) {
9764      // find video_parameter_set_rbsp or seq_parameter_set_rbsp
9765      return findH265Nal(bytes, [32, 33], 3).length;
9766    }
9767  }; // get all the isLikely functions
9768  // but make sure 'ts' is above h264 and h265
9769  // but below everything else as it is the least specific
9770
9771  var isLikelyTypes = Object.keys(_isLikely) // remove ts, h264, h265
9772  .filter(function (t) {
9773    return t !== 'ts' && t !== 'h264' && t !== 'h265';
9774  }) // add it back to the bottom
9775  .concat(['ts', 'h264', 'h265']); // make sure we are dealing with uint8 data.
9776
9777  isLikelyTypes.forEach(function (type) {
9778    var isLikelyFn = _isLikely[type];
9779
9780    _isLikely[type] = function (bytes) {
9781      return isLikelyFn(toUint8(bytes));
9782    };
9783  }); // export after wrapping
9784
9785  var isLikely = _isLikely; // A useful list of file signatures can be found here
9786  // https://en.wikipedia.org/wiki/List_of_file_signatures
9787
9788  var detectContainerForBytes = function detectContainerForBytes(bytes) {
9789    bytes = toUint8(bytes);
9790
9791    for (var i = 0; i < isLikelyTypes.length; i++) {
9792      var type = isLikelyTypes[i];
9793
9794      if (isLikely[type](bytes)) {
9795        return type;
9796      }
9797    }
9798
9799    return '';
9800  }; // fmp4 is not a container
9801
9802  var isLikelyFmp4MediaSegment = function isLikelyFmp4MediaSegment(bytes) {
9803    return findBox(bytes, ['moof']).length > 0;
9804  };
9805
9806  // which will only happen if the request is complete.
9807
9808  const callbackOnCompleted = (request, cb) => {
9809    if (request.readyState === 4) {
9810      return cb();
9811    }
9812
9813    return;
9814  };
9815
9816  const containerRequest = (uri, xhr, cb, requestType) => {
9817    let bytes = [];
9818    let id3Offset;
9819    let finished = false;
9820
9821    const endRequestAndCallback = function (err, req, type, _bytes) {
9822      req.abort();
9823      finished = true;
9824      return cb(err, req, type, _bytes);
9825    };
9826
9827    const progressListener = function (error, request) {
9828      if (finished) {
9829        return;
9830      }
9831
9832      if (error) {
9833        error.metadata = getStreamingNetworkErrorMetadata({
9834          requestType,
9835          request,
9836          error
9837        });
9838        return endRequestAndCallback(error, request, '', bytes);
9839      } // grap the new part of content that was just downloaded
9840
9841
9842      const newPart = request.responseText.substring(bytes && bytes.byteLength || 0, request.responseText.length); // add that onto bytes
9843
9844      bytes = concatTypedArrays(bytes, stringToBytes(newPart, true));
9845      id3Offset = id3Offset || getId3Offset(bytes); // we need at least 10 bytes to determine a type
9846      // or we need at least two bytes after an id3Offset
9847
9848      if (bytes.length < 10 || id3Offset && bytes.length < id3Offset + 2) {
9849        return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes));
9850      }
9851
9852      const type = detectContainerForBytes(bytes); // if this looks like a ts segment but we don't have enough data
9853      // to see the second sync byte, wait until we have enough data
9854      // before declaring it ts
9855
9856      if (type === 'ts' && bytes.length < 188) {
9857        return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes));
9858      } // this may be an unsynced ts segment
9859      // wait for 376 bytes before detecting no container
9860
9861
9862      if (!type && bytes.length < 376) {
9863        return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes));
9864      }
9865
9866      return endRequestAndCallback(null, request, type, bytes);
9867    };
9868
9869    const options = {
9870      uri,
9871
9872      beforeSend(request) {
9873        // this forces the browser to pass the bytes to us unprocessed
9874        request.overrideMimeType('text/plain; charset=x-user-defined');
9875        request.addEventListener('progress', function ({
9876          total,
9877          loaded
9878        }) {
9879          return callbackWrapper(request, null, {
9880            statusCode: request.status
9881          }, progressListener);
9882        });
9883      }
9884
9885    };
9886    const request = xhr(options, function (error, response) {
9887      return callbackWrapper(request, error, response, progressListener);
9888    });
9889    return request;
9890  };
9891
9892  const {
9893    EventTarget
9894  } = videojs__default["default"];
9895
9896  const dashPlaylistUnchanged = function (a, b) {
9897    if (!isPlaylistUnchanged(a, b)) {
9898      return false;
9899    } // for dash the above check will often return true in scenarios where
9900    // the playlist actually has changed because mediaSequence isn't a
9901    // dash thing, and we often set it to 1. So if the playlists have the same amount
9902    // of segments we return true.
9903    // So for dash we need to make sure that the underlying segments are different.
9904    // if sidx changed then the playlists are different.
9905
9906
9907    if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) {
9908      return false;
9909    } else if (!a.sidx && b.sidx || a.sidx && !b.sidx) {
9910      return false;
9911    } // one or the other does not have segments
9912    // there was a change.
9913
9914
9915    if (a.segments && !b.segments || !a.segments && b.segments) {
9916      return false;
9917    } // neither has segments nothing changed
9918
9919
9920    if (!a.segments && !b.segments) {
9921      return true;
9922    } // check segments themselves
9923
9924
9925    for (let i = 0; i < a.segments.length; i++) {
9926      const aSegment = a.segments[i];
9927      const bSegment = b.segments[i]; // if uris are different between segments there was a change
9928
9929      if (aSegment.uri !== bSegment.uri) {
9930        return false;
9931      } // neither segment has a byterange, there will be no byterange change.
9932
9933
9934      if (!aSegment.byterange && !bSegment.byterange) {
9935        continue;
9936      }
9937
9938      const aByterange = aSegment.byterange;
9939      const bByterange = bSegment.byterange; // if byterange only exists on one of the segments, there was a change.
9940
9941      if (aByterange && !bByterange || !aByterange && bByterange) {
9942        return false;
9943      } // if both segments have byterange with different offsets, there was a change.
9944
9945
9946      if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) {
9947        return false;
9948      }
9949    } // if everything was the same with segments, this is the same playlist.
9950
9951
9952    return true;
9953  };
9954  /**
9955   * Use the representation IDs from the mpd object to create groupIDs, the NAME is set to mandatory representation
9956   * ID in the parser. This allows for continuous playout across periods with the same representation IDs
9957   * (continuous periods as defined in DASH-IF 3.2.12). This is assumed in the mpd-parser as well. If we want to support
9958   * periods without continuous playback this function may need modification as well as the parser.
9959   */
9960
9961
9962  const dashGroupId = (type, group, label, playlist) => {
9963    // If the manifest somehow does not have an ID (non-dash compliant), use the label.
9964    const playlistId = playlist.attributes.NAME || label;
9965    return `placeholder-uri-${type}-${group}-${playlistId}`;
9966  };
9967  /**
9968   * Parses the main XML string and updates playlist URI references.
9969   *
9970   * @param {Object} config
9971   *        Object of arguments
9972   * @param {string} config.mainXml
9973   *        The mpd XML
9974   * @param {string} config.srcUrl
9975   *        The mpd URL
9976   * @param {Date} config.clientOffset
9977   *         A time difference between server and client
9978   * @param {Object} config.sidxMapping
9979   *        SIDX mappings for moof/mdat URIs and byte ranges
9980   * @return {Object}
9981   *         The parsed mpd manifest object
9982   */
9983
9984
9985  const parseMainXml = ({
9986    mainXml,
9987    srcUrl,
9988    clientOffset,
9989    sidxMapping,
9990    previousManifest
9991  }) => {
9992    const manifest = parse(mainXml, {
9993      manifestUri: srcUrl,
9994      clientOffset,
9995      sidxMapping,
9996      previousManifest
9997    });
9998    addPropertiesToMain(manifest, srcUrl, dashGroupId);
9999    return manifest;
10000  };
10001  /**
10002   * Removes any mediaGroup labels that no longer exist in the newMain
10003   *
10004   * @param {Object} update
10005   *         The previous mpd object being updated
10006   * @param {Object} newMain
10007   *         The new mpd object
10008   */
10009
10010  const removeOldMediaGroupLabels = (update, newMain) => {
10011    forEachMediaGroup$1(update, (properties, type, group, label) => {
10012      if (!newMain.mediaGroups[type][group] || !(label in newMain.mediaGroups[type][group])) {
10013        delete update.mediaGroups[type][group][label];
10014      }
10015    });
10016  };
10017  /**
10018   * Returns a new main manifest that is the result of merging an updated main manifest
10019   * into the original version.
10020   *
10021   * @param {Object} oldMain
10022   *        The old parsed mpd object
10023   * @param {Object} newMain
10024   *        The updated parsed mpd object
10025   * @return {Object}
10026   *         A new object representing the original main manifest with the updated media
10027   *         playlists merged in
10028   */
10029
10030
10031  const updateMain = (oldMain, newMain, sidxMapping) => {
10032    let noChanges = true;
10033    let update = merge$1(oldMain, {
10034      // These are top level properties that can be updated
10035      duration: newMain.duration,
10036      minimumUpdatePeriod: newMain.minimumUpdatePeriod,
10037      timelineStarts: newMain.timelineStarts
10038    }); // First update the playlists in playlist list
10039
10040    for (let i = 0; i < newMain.playlists.length; i++) {
10041      const playlist = newMain.playlists[i];
10042
10043      if (playlist.sidx) {
10044        const sidxKey = generateSidxKey(playlist.sidx); // add sidx segments to the playlist if we have all the sidx info already
10045
10046        if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) {
10047          addSidxSegmentsToPlaylist$1(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri);
10048        }
10049      }
10050
10051      const playlistUpdate = updateMain$1(update, playlist, dashPlaylistUnchanged);
10052
10053      if (playlistUpdate) {
10054        update = playlistUpdate;
10055        noChanges = false;
10056      }
10057    } // Then update media group playlists
10058
10059
10060    forEachMediaGroup$1(newMain, (properties, type, group, label) => {
10061      if (properties.playlists && properties.playlists.length) {
10062        const id = properties.playlists[0].id;
10063        const playlistUpdate = updateMain$1(update, properties.playlists[0], dashPlaylistUnchanged);
10064
10065        if (playlistUpdate) {
10066          update = playlistUpdate; // add new mediaGroup label if it doesn't exist and assign the new mediaGroup.
10067
10068          if (!(label in update.mediaGroups[type][group])) {
10069            update.mediaGroups[type][group][label] = properties;
10070          } // update the playlist reference within media groups
10071
10072
10073          update.mediaGroups[type][group][label].playlists[0] = update.playlists[id];
10074          noChanges = false;
10075        }
10076      }
10077    }); // remove mediaGroup labels and references that no longer exist in the newMain
10078
10079    removeOldMediaGroupLabels(update, newMain);
10080
10081    if (newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) {
10082      noChanges = false;
10083    }
10084
10085    if (noChanges) {
10086      return null;
10087    }
10088
10089    return update;
10090  }; // SIDX should be equivalent if the URI and byteranges of the SIDX match.
10091  // If the SIDXs have maps, the two maps should match,
10092  // both `a` and `b` missing SIDXs is considered matching.
10093  // If `a` or `b` but not both have a map, they aren't matching.
10094
10095  const equivalentSidx = (a, b) => {
10096    const neitherMap = Boolean(!a.map && !b.map);
10097    const equivalentMap = neitherMap || Boolean(a.map && b.map && a.map.byterange.offset === b.map.byterange.offset && a.map.byterange.length === b.map.byterange.length);
10098    return equivalentMap && a.uri === b.uri && a.byterange.offset === b.byterange.offset && a.byterange.length === b.byterange.length;
10099  }; // exported for testing
10100
10101
10102  const compareSidxEntry = (playlists, oldSidxMapping) => {
10103    const newSidxMapping = {};
10104
10105    for (const id in playlists) {
10106      const playlist = playlists[id];
10107      const currentSidxInfo = playlist.sidx;
10108
10109      if (currentSidxInfo) {
10110        const key = generateSidxKey(currentSidxInfo);
10111
10112        if (!oldSidxMapping[key]) {
10113          break;
10114        }
10115
10116        const savedSidxInfo = oldSidxMapping[key].sidxInfo;
10117
10118        if (equivalentSidx(savedSidxInfo, currentSidxInfo)) {
10119          newSidxMapping[key] = oldSidxMapping[key];
10120        }
10121      }
10122    }
10123
10124    return newSidxMapping;
10125  };
10126  /**
10127   *  A function that filters out changed items as they need to be requested separately.
10128   *
10129   *  The method is exported for testing
10130   *
10131   *  @param {Object} main the parsed mpd XML returned via mpd-parser
10132   *  @param {Object} oldSidxMapping the SIDX to compare against
10133   */
10134
10135  const filterChangedSidxMappings = (main, oldSidxMapping) => {
10136    const videoSidx = compareSidxEntry(main.playlists, oldSidxMapping);
10137    let mediaGroupSidx = videoSidx;
10138    forEachMediaGroup$1(main, (properties, mediaType, groupKey, labelKey) => {
10139      if (properties.playlists && properties.playlists.length) {
10140        const playlists = properties.playlists;
10141        mediaGroupSidx = merge$1(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping));
10142      }
10143    });
10144    return mediaGroupSidx;
10145  };
10146  class DashPlaylistLoader extends EventTarget {
10147    // DashPlaylistLoader must accept either a src url or a playlist because subsequent
10148    // playlist loader setups from media groups will expect to be able to pass a playlist
10149    // (since there aren't external URLs to media playlists with DASH)
10150    constructor(srcUrlOrPlaylist, vhs, options = {}, mainPlaylistLoader) {
10151      super();
10152      this.isPaused_ = true;
10153      this.mainPlaylistLoader_ = mainPlaylistLoader || this;
10154
10155      if (!mainPlaylistLoader) {
10156        this.isMain_ = true;
10157      }
10158
10159      const {
10160        withCredentials = false
10161      } = options;
10162      this.vhs_ = vhs;
10163      this.withCredentials = withCredentials;
10164      this.addMetadataToTextTrack = options.addMetadataToTextTrack;
10165
10166      if (!srcUrlOrPlaylist) {
10167        throw new Error('A non-empty playlist URL or object is required');
10168      } // event naming?
10169
10170
10171      this.on('minimumUpdatePeriod', () => {
10172        this.refreshXml_();
10173      }); // live playlist staleness timeout
10174
10175      this.on('mediaupdatetimeout', () => {
10176        this.refreshMedia_(this.media().id);
10177      });
10178      this.state = 'HAVE_NOTHING';
10179      this.loadedPlaylists_ = {};
10180      this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state
10181      // The mainPlaylistLoader will be created with a string
10182
10183      if (this.isMain_) {
10184        this.mainPlaylistLoader_.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes
10185        // once multi-period is refactored
10186
10187        this.mainPlaylistLoader_.sidxMapping_ = {};
10188      } else {
10189        this.childPlaylist_ = srcUrlOrPlaylist;
10190      }
10191    }
10192
10193    get isPaused() {
10194      return this.isPaused_;
10195    }
10196
10197    requestErrored_(err, request, startingState) {
10198      // disposed
10199      if (!this.request) {
10200        return true;
10201      } // pending request is cleared
10202
10203
10204      this.request = null;
10205
10206      if (err) {
10207        // use the provided error object or create one
10208        // based on the request/response
10209        this.error = typeof err === 'object' && !(err instanceof Error) ? err : {
10210          status: request.status,
10211          message: 'DASH request error at URL: ' + request.uri,
10212          response: request.response,
10213          // MEDIA_ERR_NETWORK
10214          code: 2,
10215          metadata: err.metadata
10216        };
10217
10218        if (startingState) {
10219          this.state = startingState;
10220        }
10221
10222        this.trigger('error');
10223        return true;
10224      }
10225    }
10226    /**
10227     * Verify that the container of the sidx segment can be parsed
10228     * and if it can, get and parse that segment.
10229     */
10230
10231
10232    addSidxSegments_(playlist, startingState, cb) {
10233      const sidxKey = playlist.sidx && generateSidxKey(playlist.sidx); // playlist lacks sidx or sidx segments were added to this playlist already.
10234
10235      if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) {
10236        // keep this function async
10237        window.clearTimeout(this.mediaRequest_);
10238        this.mediaRequest_ = window.setTimeout(() => cb(false), 0);
10239        return;
10240      } // resolve the segment URL relative to the playlist
10241
10242
10243      const uri = resolveManifestRedirect(playlist.sidx.resolvedUri);
10244
10245      const fin = (err, request) => {
10246        if (this.requestErrored_(err, request, startingState)) {
10247          return;
10248        }
10249
10250        const sidxMapping = this.mainPlaylistLoader_.sidxMapping_;
10251        const {
10252          requestType
10253        } = request;
10254        let sidx;
10255
10256        try {
10257          sidx = parseSidx_1(toUint8(request.response).subarray(8));
10258        } catch (e) {
10259          e.metadata = getStreamingNetworkErrorMetadata({
10260            requestType,
10261            request,
10262            parseFailure: true
10263          }); // sidx parsing failed.
10264
10265          this.requestErrored_(e, request, startingState);
10266          return;
10267        }
10268
10269        sidxMapping[sidxKey] = {
10270          sidxInfo: playlist.sidx,
10271          sidx
10272        };
10273        addSidxSegmentsToPlaylist$1(playlist, sidx, playlist.sidx.resolvedUri);
10274        return cb(true);
10275      };
10276
10277      const REQUEST_TYPE = 'dash-sidx';
10278      this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => {
10279        if (err) {
10280          return fin(err, request);
10281        }
10282
10283        if (!container || container !== 'mp4') {
10284          const sidxContainer = container || 'unknown';
10285          return fin({
10286            status: request.status,
10287            message: `Unsupported ${sidxContainer} container type for sidx segment at URL: ${uri}`,
10288            // response is just bytes in this case
10289            // but we really don't want to return that.
10290            response: '',
10291            playlist,
10292            internal: true,
10293            playlistExclusionDuration: Infinity,
10294            // MEDIA_ERR_NETWORK
10295            code: 2
10296          }, request);
10297        } // if we already downloaded the sidx bytes in the container request, use them
10298
10299
10300        const {
10301          offset,
10302          length
10303        } = playlist.sidx.byterange;
10304
10305        if (bytes.length >= length + offset) {
10306          return fin(err, {
10307            response: bytes.subarray(offset, offset + length),
10308            status: request.status,
10309            uri: request.uri
10310          });
10311        } // otherwise request sidx bytes
10312
10313
10314        this.request = this.vhs_.xhr({
10315          uri,
10316          responseType: 'arraybuffer',
10317          requestType: 'dash-sidx',
10318          headers: segmentXhrHeaders({
10319            byterange: playlist.sidx.byterange
10320          })
10321        }, fin);
10322      }, REQUEST_TYPE);
10323    }
10324
10325    dispose() {
10326      this.isPaused_ = true;
10327      this.trigger('dispose');
10328      this.stopRequest();
10329      this.loadedPlaylists_ = {};
10330      window.clearTimeout(this.minimumUpdatePeriodTimeout_);
10331      window.clearTimeout(this.mediaRequest_);
10332      window.clearTimeout(this.mediaUpdateTimeout);
10333      this.mediaUpdateTimeout = null;
10334      this.mediaRequest_ = null;
10335      this.minimumUpdatePeriodTimeout_ = null;
10336
10337      if (this.mainPlaylistLoader_.createMupOnMedia_) {
10338        this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_);
10339        this.mainPlaylistLoader_.createMupOnMedia_ = null;
10340      }
10341
10342      this.off();
10343    }
10344
10345    hasPendingRequest() {
10346      return this.request || this.mediaRequest_;
10347    }
10348
10349    stopRequest() {
10350      if (this.request) {
10351        const oldRequest = this.request;
10352        this.request = null;
10353        oldRequest.onreadystatechange = null;
10354        oldRequest.abort();
10355      }
10356    }
10357
10358    media(playlist) {
10359      // getter
10360      if (!playlist) {
10361        return this.media_;
10362      } // setter
10363
10364
10365      if (this.state === 'HAVE_NOTHING') {
10366        throw new Error('Cannot switch media playlist from ' + this.state);
10367      }
10368
10369      const startingState = this.state; // find the playlist object if the target playlist has been specified by URI
10370
10371      if (typeof playlist === 'string') {
10372        if (!this.mainPlaylistLoader_.main.playlists[playlist]) {
10373          throw new Error('Unknown playlist URI: ' + playlist);
10374        }
10375
10376        playlist = this.mainPlaylistLoader_.main.playlists[playlist];
10377      }
10378
10379      const mediaChange = !this.media_ || playlist.id !== this.media_.id; // switch to previously loaded playlists immediately
10380
10381      if (mediaChange && this.loadedPlaylists_[playlist.id] && this.loadedPlaylists_[playlist.id].endList) {
10382        this.state = 'HAVE_METADATA';
10383        this.media_ = playlist; // trigger media change if the active media has been updated
10384
10385        if (mediaChange) {
10386          this.trigger('mediachanging');
10387          this.trigger('mediachange');
10388        }
10389
10390        return;
10391      } // switching to the active playlist is a no-op
10392
10393
10394      if (!mediaChange) {
10395        return;
10396      } // switching from an already loaded playlist
10397
10398
10399      if (this.media_) {
10400        this.trigger('mediachanging');
10401      }
10402
10403      this.addSidxSegments_(playlist, startingState, sidxChanged => {
10404        // everything is ready just continue to haveMetadata
10405        this.haveMetadata({
10406          startingState,
10407          playlist
10408        });
10409      });
10410    }
10411
10412    haveMetadata({
10413      startingState,
10414      playlist
10415    }) {
10416      this.state = 'HAVE_METADATA';
10417      this.loadedPlaylists_[playlist.id] = playlist;
10418      window.clearTimeout(this.mediaRequest_);
10419      this.mediaRequest_ = null; // This will trigger loadedplaylist
10420
10421      this.refreshMedia_(playlist.id); // fire loadedmetadata the first time a media playlist is loaded
10422      // to resolve setup of media groups
10423
10424      if (startingState === 'HAVE_MAIN_MANIFEST') {
10425        this.trigger('loadedmetadata');
10426      } else {
10427        // trigger media change if the active media has been updated
10428        this.trigger('mediachange');
10429      }
10430    }
10431
10432    pause() {
10433      this.isPaused_ = true;
10434
10435      if (this.mainPlaylistLoader_.createMupOnMedia_) {
10436        this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_);
10437        this.mainPlaylistLoader_.createMupOnMedia_ = null;
10438      }
10439
10440      this.stopRequest();
10441      window.clearTimeout(this.mediaUpdateTimeout);
10442      this.mediaUpdateTimeout = null;
10443
10444      if (this.isMain_) {
10445        window.clearTimeout(this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_);
10446        this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_ = null;
10447      }
10448
10449      if (this.state === 'HAVE_NOTHING') {
10450        // If we pause the loader before any data has been retrieved, its as if we never
10451        // started, so reset to an unstarted state.
10452        this.started = false;
10453      }
10454    }
10455
10456    load(isFinalRendition) {
10457      this.isPaused_ = false;
10458      window.clearTimeout(this.mediaUpdateTimeout);
10459      this.mediaUpdateTimeout = null;
10460      const media = this.media();
10461
10462      if (isFinalRendition) {
10463        const delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000;
10464        this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay);
10465        return;
10466      } // because the playlists are internal to the manifest, load should either load the
10467      // main manifest, or do nothing but trigger an event
10468
10469
10470      if (!this.started) {
10471        this.start();
10472        return;
10473      }
10474
10475      if (media && !media.endList) {
10476        // Check to see if this is the main loader and the MUP was cleared (this happens
10477        // when the loader was paused). `media` should be set at this point since one is always
10478        // set during `start()`.
10479        if (this.isMain_ && !this.minimumUpdatePeriodTimeout_) {
10480          // Trigger minimumUpdatePeriod to refresh the main manifest
10481          this.trigger('minimumUpdatePeriod'); // Since there was no prior minimumUpdatePeriodTimeout it should be recreated
10482
10483          this.updateMinimumUpdatePeriodTimeout_();
10484        }
10485
10486        this.trigger('mediaupdatetimeout');
10487      } else {
10488        this.trigger('loadedplaylist');
10489      }
10490    }
10491
10492    start() {
10493      this.started = true; // We don't need to request the main manifest again
10494      // Call this asynchronously to match the xhr request behavior below
10495
10496      if (!this.isMain_) {
10497        window.clearTimeout(this.mediaRequest_);
10498        this.mediaRequest_ = window.setTimeout(() => this.haveMain_(), 0);
10499        return;
10500      }
10501
10502      this.requestMain_((req, mainChanged) => {
10503        this.haveMain_();
10504
10505        if (!this.hasPendingRequest() && !this.media_) {
10506          this.media(this.mainPlaylistLoader_.main.playlists[0]);
10507        }
10508      });
10509    }
10510
10511    requestMain_(cb) {
10512      const metadata = {
10513        manifestInfo: {
10514          uri: this.mainPlaylistLoader_.srcUrl
10515        }
10516      };
10517      this.trigger({
10518        type: 'manifestrequeststart',
10519        metadata
10520      });
10521      this.request = this.vhs_.xhr({
10522        uri: this.mainPlaylistLoader_.srcUrl,
10523        withCredentials: this.withCredentials,
10524        requestType: 'dash-manifest'
10525      }, (error, req) => {
10526        if (error) {
10527          const {
10528            requestType
10529          } = req;
10530          error.metadata = getStreamingNetworkErrorMetadata({
10531            requestType,
10532            request: req,
10533            error
10534          });
10535        }
10536
10537        if (this.requestErrored_(error, req)) {
10538          if (this.state === 'HAVE_NOTHING') {
10539            this.started = false;
10540          }
10541
10542          return;
10543        }
10544
10545        this.trigger({
10546          type: 'manifestrequestcomplete',
10547          metadata
10548        });
10549        const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_;
10550        this.mainPlaylistLoader_.mainXml_ = req.responseText;
10551
10552        if (req.responseHeaders && req.responseHeaders.date) {
10553          this.mainLoaded_ = Date.parse(req.responseHeaders.date);
10554        } else {
10555          this.mainLoaded_ = Date.now();
10556        }
10557
10558        this.mainPlaylistLoader_.srcUrl = resolveManifestRedirect(this.mainPlaylistLoader_.srcUrl, req);
10559
10560        if (mainChanged) {
10561          this.handleMain_();
10562          this.syncClientServerClock_(() => {
10563            return cb(req, mainChanged);
10564          });
10565          return;
10566        }
10567
10568        return cb(req, mainChanged);
10569      });
10570    }
10571    /**
10572     * Parses the main xml for UTCTiming node to sync the client clock to the server
10573     * clock. If the UTCTiming node requires a HEAD or GET request, that request is made.
10574     *
10575     * @param {Function} done
10576     *        Function to call when clock sync has completed
10577     */
10578
10579
10580    syncClientServerClock_(done) {
10581      const utcTiming = parseUTCTiming(this.mainPlaylistLoader_.mainXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the
10582      // server clock
10583
10584      if (utcTiming === null) {
10585        this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now();
10586        return done();
10587      }
10588
10589      if (utcTiming.method === 'DIRECT') {
10590        this.mainPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now();
10591        return done();
10592      }
10593
10594      this.request = this.vhs_.xhr({
10595        uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value),
10596        method: utcTiming.method,
10597        withCredentials: this.withCredentials,
10598        requestType: 'dash-clock-sync'
10599      }, (error, req) => {
10600        // disposed
10601        if (!this.request) {
10602          return;
10603        }
10604
10605        if (error) {
10606          const {
10607            requestType
10608          } = req;
10609          this.error.metadata = getStreamingNetworkErrorMetadata({
10610            requestType,
10611            request: req,
10612            error
10613          }); // sync request failed, fall back to using date header from mpd
10614          // TODO: log warning
10615
10616          this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now();
10617          return done();
10618        }
10619
10620        let serverTime;
10621
10622        if (utcTiming.method === 'HEAD') {
10623          if (!req.responseHeaders || !req.responseHeaders.date) {
10624            // expected date header not preset, fall back to using date header from mpd
10625            // TODO: log warning
10626            serverTime = this.mainLoaded_;
10627          } else {
10628            serverTime = Date.parse(req.responseHeaders.date);
10629          }
10630        } else {
10631          serverTime = Date.parse(req.responseText);
10632        }
10633
10634        this.mainPlaylistLoader_.clientOffset_ = serverTime - Date.now();
10635        done();
10636      });
10637    }
10638
10639    haveMain_() {
10640      this.state = 'HAVE_MAIN_MANIFEST';
10641
10642      if (this.isMain_) {
10643        // We have the main playlist at this point, so
10644        // trigger this to allow PlaylistController
10645        // to make an initial playlist selection
10646        this.trigger('loadedplaylist');
10647      } else if (!this.media_) {
10648        // no media playlist was specifically selected so select
10649        // the one the child playlist loader was created with
10650        this.media(this.childPlaylist_);
10651      }
10652    }
10653
10654    handleMain_() {
10655      // clear media request
10656      window.clearTimeout(this.mediaRequest_);
10657      this.mediaRequest_ = null;
10658      const oldMain = this.mainPlaylistLoader_.main;
10659      const metadata = {
10660        manifestInfo: {
10661          uri: this.mainPlaylistLoader_.srcUrl
10662        }
10663      };
10664      this.trigger({
10665        type: 'manifestparsestart',
10666        metadata
10667      });
10668      let newMain;
10669
10670      try {
10671        newMain = parseMainXml({
10672          mainXml: this.mainPlaylistLoader_.mainXml_,
10673          srcUrl: this.mainPlaylistLoader_.srcUrl,
10674          clientOffset: this.mainPlaylistLoader_.clientOffset_,
10675          sidxMapping: this.mainPlaylistLoader_.sidxMapping_,
10676          previousManifest: oldMain
10677        });
10678      } catch (error) {
10679        this.error = error;
10680        this.error.metadata = {
10681          errorType: videojs__default["default"].Error.StreamingDashManifestParserError,
10682          error
10683        };
10684        this.trigger('error');
10685      } // if we have an old main to compare the new main against
10686
10687
10688      if (oldMain) {
10689        newMain = updateMain(oldMain, newMain, this.mainPlaylistLoader_.sidxMapping_);
10690      } // only update main if we have a new main
10691
10692
10693      this.mainPlaylistLoader_.main = newMain ? newMain : oldMain;
10694      const location = this.mainPlaylistLoader_.main.locations && this.mainPlaylistLoader_.main.locations[0];
10695
10696      if (location && location !== this.mainPlaylistLoader_.srcUrl) {
10697        this.mainPlaylistLoader_.srcUrl = location;
10698      }
10699
10700      if (!oldMain || newMain && newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) {
10701        this.updateMinimumUpdatePeriodTimeout_();
10702      }
10703
10704      this.addEventStreamToMetadataTrack_(newMain);
10705
10706      if (newMain) {
10707        const {
10708          duration,
10709          endList
10710        } = newMain;
10711        const renditions = [];
10712        newMain.playlists.forEach(playlist => {
10713          renditions.push({
10714            id: playlist.id,
10715            bandwidth: playlist.attributes.BANDWIDTH,
10716            resolution: playlist.attributes.RESOLUTION,
10717            codecs: playlist.attributes.CODECS
10718          });
10719        });
10720        const parsedManifest = {
10721          duration,
10722          isLive: !endList,
10723          renditions
10724        };
10725        metadata.parsedManifest = parsedManifest;
10726        this.trigger({
10727          type: 'manifestparsecomplete',
10728          metadata
10729        });
10730      }
10731
10732      return Boolean(newMain);
10733    }
10734
10735    updateMinimumUpdatePeriodTimeout_() {
10736      const mpl = this.mainPlaylistLoader_; // cancel any pending creation of mup on media
10737      // a new one will be added if needed.
10738
10739      if (mpl.createMupOnMedia_) {
10740        mpl.off('loadedmetadata', mpl.createMupOnMedia_);
10741        mpl.createMupOnMedia_ = null;
10742      } // clear any pending timeouts
10743
10744
10745      if (mpl.minimumUpdatePeriodTimeout_) {
10746        window.clearTimeout(mpl.minimumUpdatePeriodTimeout_);
10747        mpl.minimumUpdatePeriodTimeout_ = null;
10748      }
10749
10750      let mup = mpl.main && mpl.main.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current
10751      // MPD has no future validity, so a new one will need to be acquired when new
10752      // media segments are to be made available. Thus, we use the target duration
10753      // in this case
10754
10755      if (mup === 0) {
10756        if (mpl.media()) {
10757          mup = mpl.media().targetDuration * 1000;
10758        } else {
10759          mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_;
10760          mpl.one('loadedmetadata', mpl.createMupOnMedia_);
10761        }
10762      } // if minimumUpdatePeriod is invalid or <= zero, which
10763      // can happen when a live video becomes VOD. skip timeout
10764      // creation.
10765
10766
10767      if (typeof mup !== 'number' || mup <= 0) {
10768        if (mup < 0) {
10769          this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`);
10770        }
10771
10772        return;
10773      }
10774
10775      this.createMUPTimeout_(mup);
10776    }
10777
10778    createMUPTimeout_(mup) {
10779      const mpl = this.mainPlaylistLoader_;
10780      mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => {
10781        mpl.minimumUpdatePeriodTimeout_ = null;
10782        mpl.trigger('minimumUpdatePeriod');
10783        mpl.createMUPTimeout_(mup);
10784      }, mup);
10785    }
10786    /**
10787     * Sends request to refresh the main xml and updates the parsed main manifest
10788     */
10789
10790
10791    refreshXml_() {
10792      this.requestMain_((req, mainChanged) => {
10793        if (!mainChanged) {
10794          return;
10795        }
10796
10797        if (this.media_) {
10798          this.media_ = this.mainPlaylistLoader_.main.playlists[this.media_.id];
10799        } // This will filter out updated sidx info from the mapping
10800
10801
10802        this.mainPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.sidxMapping_);
10803        this.addSidxSegments_(this.media(), this.state, sidxChanged => {
10804          // TODO: do we need to reload the current playlist?
10805          this.refreshMedia_(this.media().id);
10806        });
10807      });
10808    }
10809    /**
10810     * Refreshes the media playlist by re-parsing the main xml and updating playlist
10811     * references. If this is an alternate loader, the updated parsed manifest is retrieved
10812     * from the main loader.
10813     */
10814
10815
10816    refreshMedia_(mediaID) {
10817      if (!mediaID) {
10818        throw new Error('refreshMedia_ must take a media id');
10819      } // for main we have to reparse the main xml
10820      // to re-create segments based on current timing values
10821      // which may change media. We only skip updating the main manifest
10822      // if this is the first time this.media_ is being set.
10823      // as main was just parsed in that case.
10824
10825
10826      if (this.media_ && this.isMain_) {
10827        this.handleMain_();
10828      }
10829
10830      const playlists = this.mainPlaylistLoader_.main.playlists;
10831      const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID];
10832
10833      if (mediaChanged) {
10834        this.media_ = playlists[mediaID];
10835      } else {
10836        this.trigger('playlistunchanged');
10837      }
10838
10839      if (!this.mediaUpdateTimeout) {
10840        const createMediaUpdateTimeout = () => {
10841          if (this.media().endList) {
10842            return;
10843          }
10844
10845          this.mediaUpdateTimeout = window.setTimeout(() => {
10846            this.trigger('mediaupdatetimeout');
10847            createMediaUpdateTimeout();
10848          }, refreshDelay(this.media(), Boolean(mediaChanged)));
10849        };
10850
10851        createMediaUpdateTimeout();
10852      }
10853
10854      this.trigger('loadedplaylist');
10855    }
10856    /**
10857     * Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track.
10858     *
10859     * @param {manifest} newMain the newly parsed manifest
10860     */
10861
10862
10863    addEventStreamToMetadataTrack_(newMain) {
10864      // Only add new event stream metadata if we have a new manifest.
10865      if (newMain && this.mainPlaylistLoader_.main.eventStream) {
10866        // convert EventStream to ID3-like data.
10867        const metadataArray = this.mainPlaylistLoader_.main.eventStream.map(eventStreamNode => {
10868          return {
10869            cueTime: eventStreamNode.start,
10870            frames: [{
10871              data: eventStreamNode.messageData
10872            }]
10873          };
10874        });
10875        this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration);
10876      }
10877    }
10878    /**
10879     * Returns the key ID set from a playlist
10880     *
10881     * @param {playlist} playlist to fetch the key ID set from.
10882     * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist.
10883     */
10884
10885
10886    getKeyIdSet(playlist) {
10887      if (playlist.contentProtection) {
10888        const keyIds = new Set();
10889
10890        for (const keysystem in playlist.contentProtection) {
10891          const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID'];
10892
10893          if (defaultKID) {
10894            // DASH keyIds are separated by dashes.
10895            keyIds.add(defaultKID.replace(/-/g, '').toLowerCase());
10896          }
10897        }
10898
10899        return keyIds;
10900      }
10901    }
10902
10903  }
10904
10905  var Config = {
10906    GOAL_BUFFER_LENGTH: 30,
10907    MAX_GOAL_BUFFER_LENGTH: 60,
10908    BACK_BUFFER_LENGTH: 30,
10909    GOAL_BUFFER_LENGTH_RATE: 1,
10910    // 0.5 MB/s
10911    INITIAL_BANDWIDTH: 4194304,
10912    // A fudge factor to apply to advertised playlist bitrates to account for
10913    // temporary flucations in client bandwidth
10914    BANDWIDTH_VARIANCE: 1.2,
10915    // How much of the buffer must be filled before we consider upswitching
10916    BUFFER_LOW_WATER_LINE: 0,
10917    MAX_BUFFER_LOW_WATER_LINE: 30,
10918    // TODO: Remove this when experimentalBufferBasedABR is removed
10919    EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16,
10920    BUFFER_LOW_WATER_LINE_RATE: 1,
10921    // If the buffer is greater than the high water line, we won't switch down
10922    BUFFER_HIGH_WATER_LINE: 30
10923  };
10924
10925  const stringToArrayBuffer = string => {
10926    const view = new Uint8Array(new ArrayBuffer(string.length));
10927
10928    for (let i = 0; i < string.length; i++) {
10929      view[i] = string.charCodeAt(i);
10930    }
10931
10932    return view.buffer;
10933  };
10934
10935  /* global Blob, BlobBuilder, Worker */
10936  // unify worker interface
10937  const browserWorkerPolyFill = function (workerObj) {
10938    // node only supports on/off
10939    workerObj.on = workerObj.addEventListener;
10940    workerObj.off = workerObj.removeEventListener;
10941    return workerObj;
10942  };
10943
10944  const createObjectURL = function (str) {
10945    try {
10946      return URL.createObjectURL(new Blob([str], {
10947        type: 'application/javascript'
10948      }));
10949    } catch (e) {
10950      const blob = new BlobBuilder();
10951      blob.append(str);
10952      return URL.createObjectURL(blob.getBlob());
10953    }
10954  };
10955
10956  const factory = function (code) {
10957    return function () {
10958      const objectUrl = createObjectURL(code);
10959      const worker = browserWorkerPolyFill(new Worker(objectUrl));
10960      worker.objURL = objectUrl;
10961      const terminate = worker.terminate;
10962      worker.on = worker.addEventListener;
10963      worker.off = worker.removeEventListener;
10964
10965      worker.terminate = function () {
10966        URL.revokeObjectURL(objectUrl);
10967        return terminate.call(this);
10968      };
10969
10970      return worker;
10971    };
10972  };
10973  const transform = function (code) {
10974    return `var browserWorkerPolyFill = ${browserWorkerPolyFill.toString()};\n` + 'browserWorkerPolyFill(self);\n' + code;
10975  };
10976
10977  const getWorkerString = function (fn) {
10978    return fn.toString().replace(/^function.+?{/, '').slice(0, -1);
10979  };
10980
10981  /* rollup-plugin-worker-factory start for worker!/
10981home/runner/work/http-streaming
vendor: 124,433 bytes, lines 10981-14729
10981/http-streaming/src/transmuxer-worker.js */
10982  const workerCode$1 = transform(getWorkerString(function () {
10983
10984    var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
10985    /**
10986     * mux.js
10987     *
10988     * Copyright (c) Brightcove
10989     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
10990     *
10991     * A lightweight readable stream implemention that handles event dispatching.
10992     * Objects that inherit from streams should call init in their constructors.
10993     */
10994
10995    var Stream$8 = function () {
10996      this.init = function () {
10997        var listeners = {};
10998        /**
10999         * Add a listener for a specified event type.
11000         * @param type {string} the event name
11001         * @param listener {function} the callback to be invoked when an event of
11002         * the specified type occurs
11003         */
11004
11005        this.on = function (type, listener) {
11006          if (!listeners[type]) {
11007            listeners[type] = [];
11008          }
11009
11010          listeners[type] = listeners[type].concat(listener);
11011        };
11012        /**
11013         * Remove a listener for a specified event type.
11014         * @param type {string} the event name
11015         * @param listener {function} a function previously registered for this
11016         * type of event through `on`
11017         */
11018
11019
11020        this.off = function (type, listener) {
11021          var index;
11022
11023          if (!listeners[type]) {
11024            return false;
11025          }
11026
11027          index = listeners[type].indexOf(listener);
11028          listeners[type] = listeners[type].slice();
11029          listeners[type].splice(index, 1);
11030          return index > -1;
11031        };
11032        /**
11033         * Trigger an event of the specified type on this stream. Any additional
11034         * arguments to this function are passed as parameters to event listeners.
11035         * @param type {string} the event name
11036         */
11037
11038
11039        this.trigger = function (type) {
11040          var callbacks, i, length, args;
11041          callbacks = listeners[type];
11042
11043          if (!callbacks) {
11044            return;
11045          } // Slicing the arguments on every invocation of this method
11046          // can add a significant amount of overhead. Avoid the
11047          // intermediate object creation for the common case of a
11048          // single callback argument
11049
11050
11051          if (arguments.length === 2) {
11052            length = callbacks.length;
11053
11054            for (i = 0; i < length; ++i) {
11055              callbacks[i].call(this, arguments[1]);
11056            }
11057          } else {
11058            args = [];
11059            i = arguments.length;
11060
11061            for (i = 1; i < arguments.length; ++i) {
11062              args.push(arguments[i]);
11063            }
11064
11065            length = callbacks.length;
11066
11067            for (i = 0; i < length; ++i) {
11068              callbacks[i].apply(this, args);
11069            }
11070          }
11071        };
11072        /**
11073         * Destroys the stream and cleans up.
11074         */
11075
11076
11077        this.dispose = function () {
11078          listeners = {};
11079        };
11080      };
11081    };
11082    /**
11083     * Forwards all `data` events on this stream to the destination stream. The
11084     * destination stream should provide a method `push` to receive the data
11085     * events as they arrive.
11086     * @param destination {stream} the stream that will receive all `data` events
11087     * @param autoFlush {boolean} if false, we will not call `flush` on the destination
11088     *                            when the current stream emits a 'done' event
11089     * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
11090     */
11091
11092
11093    Stream$8.prototype.pipe = function (destination) {
11094      this.on('data', function (data) {
11095        destination.push(data);
11096      });
11097      this.on('done', function (flushSource) {
11098        destination.flush(flushSource);
11099      });
11100      this.on('partialdone', function (flushSource) {
11101        destination.partialFlush(flushSource);
11102      });
11103      this.on('endedtimeline', function (flushSource) {
11104        destination.endTimeline(flushSource);
11105      });
11106      this.on('reset', function (flushSource) {
11107        destination.reset(flushSource);
11108      });
11109      return destination;
11110    }; // Default stream functions that are expected to be overridden to perform
11111    // actual work. These are provided by the prototype as a sort of no-op
11112    // implementation so that we don't have to check for their existence in the
11113    // `pipe` function above.
11114
11115
11116    Stream$8.prototype.push = function (data) {
11117      this.trigger('data', data);
11118    };
11119
11120    Stream$8.prototype.flush = function (flushSource) {
11121      this.trigger('done', flushSource);
11122    };
11123
11124    Stream$8.prototype.partialFlush = function (flushSource) {
11125      this.trigger('partialdone', flushSource);
11126    };
11127
11128    Stream$8.prototype.endTimeline = function (flushSource) {
11129      this.trigger('endedtimeline', flushSource);
11130    };
11131
11132    Stream$8.prototype.reset = function (flushSource) {
11133      this.trigger('reset', flushSource);
11134    };
11135
11136    var stream = Stream$8;
11137    var MAX_UINT32$1 = Math.pow(2, 32);
11138
11139    var getUint64$5 = function (uint8) {
11140      var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength);
11141      var value;
11142
11143      if (dv.getBigUint64) {
11144        value = dv.getBigUint64(0);
11145
11146        if (value < Number.MAX_SAFE_INTEGER) {
11147          return Number(value);
11148        }
11149
11150        return value;
11151      }
11152
11153      return dv.getUint32(0) * MAX_UINT32$1 + dv.getUint32(4);
11154    };
11155
11156    var numbers = {
11157      getUint64: getUint64$5,
11158      MAX_UINT32: MAX_UINT32$1
11159    };
11160    /**
11161     * mux.js
11162     *
11163     * Copyright (c) Brightcove
11164     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
11165     *
11166     * Functions that generate fragmented MP4s suitable for use with Media
11167     * Source Extensions.
11168     */
11169
11170    var MAX_UINT32 = numbers.MAX_UINT32;
11171    var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun$1, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS; // pre-calculate constants
11172
11173    (function () {
11174      var i;
11175      types = {
11176        avc1: [],
11177        // codingname
11178        avcC: [],
11179        btrt: [],
11180        dinf: [],
11181        dref: [],
11182        esds: [],
11183        ftyp: [],
11184        hdlr: [],
11185        mdat: [],
11186        mdhd: [],
11187        mdia: [],
11188        mfhd: [],
11189        minf: [],
11190        moof: [],
11191        moov: [],
11192        mp4a: [],
11193        // codingname
11194        mvex: [],
11195        mvhd: [],
11196        pasp: [],
11197        sdtp: [],
11198        smhd: [],
11199        stbl: [],
11200        stco: [],
11201        stsc: [],
11202        stsd: [],
11203        stsz: [],
11204        stts: [],
11205        styp: [],
11206        tfdt: [],
11207        tfhd: [],
11208        traf: [],
11209        trak: [],
11210        trun: [],
11211        trex: [],
11212        tkhd: [],
11213        vmhd: []
11214      }; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we
11215      // don't throw an error
11216
11217      if (typeof Uint8Array === 'undefined') {
11218        return;
11219      }
11220
11221      for (i in types) {
11222        if (types.hasOwnProperty(i)) {
11223          types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)];
11224        }
11225      }
11226
11227      MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]);
11228      AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]);
11229      MINOR_VERSION = new Uint8Array([0, 0, 0, 1]);
11230      VIDEO_HDLR = new Uint8Array([0x00, // version 0
11231      0x00, 0x00, 0x00, // flags
11232      0x00, 0x00, 0x00, 0x00, // pre_defined
11233      0x76, 0x69, 0x64, 0x65, // handler_type: 'vide'
11234      0x00, 0x00, 0x00, 0x00, // reserved
11235      0x00, 0x00, 0x00, 0x00, // reserved
11236      0x00, 0x00, 0x00, 0x00, // reserved
11237      0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler'
11238      ]);
11239      AUDIO_HDLR = new Uint8Array([0x00, // version 0
11240      0x00, 0x00, 0x00, // flags
11241      0x00, 0x00, 0x00, 0x00, // pre_defined
11242      0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun'
11243      0x00, 0x00, 0x00, 0x00, // reserved
11244      0x00, 0x00, 0x00, 0x00, // reserved
11245      0x00, 0x00, 0x00, 0x00, // reserved
11246      0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler'
11247      ]);
11248      HDLR_TYPES = {
11249        video: VIDEO_HDLR,
11250        audio: AUDIO_HDLR
11251      };
11252      DREF = new Uint8Array([0x00, // version 0
11253      0x00, 0x00, 0x00, // flags
11254      0x00, 0x00, 0x00, 0x01, // entry_count
11255      0x00, 0x00, 0x00, 0x0c, // entry_size
11256      0x75, 0x72, 0x6c, 0x20, // 'url' type
11257      0x00, // version 0
11258      0x00, 0x00, 0x01 // entry_flags
11259      ]);
11260      SMHD = new Uint8Array([0x00, // version
11261      0x00, 0x00, 0x00, // flags
11262      0x00, 0x00, // balance, 0 means centered
11263      0x00, 0x00 // reserved
11264      ]);
11265      STCO = new Uint8Array([0x00, // version
11266      0x00, 0x00, 0x00, // flags
11267      0x00, 0x00, 0x00, 0x00 // entry_count
11268      ]);
11269      STSC = STCO;
11270      STSZ = new Uint8Array([0x00, // version
11271      0x00, 0x00, 0x00, // flags
11272      0x00, 0x00, 0x00, 0x00, // sample_size
11273      0x00, 0x00, 0x00, 0x00 // sample_count
11274      ]);
11275      STTS = STCO;
11276      VMHD = new Uint8Array([0x00, // version
11277      0x00, 0x00, 0x01, // flags
11278      0x00, 0x00, // graphicsmode
11279      0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor
11280      ]);
11281    })();
11282
11283    box = function (type) {
11284      var payload = [],
11285          size = 0,
11286          i,
11287          result,
11288          view;
11289
11290      for (i = 1; i < arguments.length; i++) {
11291        payload.push(arguments[i]);
11292      }
11293
11294      i = payload.length; // calculate the total size we need to allocate
11295
11296      while (i--) {
11297        size += payload[i].byteLength;
11298      }
11299
11300      result = new Uint8Array(size + 8);
11301      view = new DataView(result.buffer, result.byteOffset, result.byteLength);
11302      view.setUint32(0, result.byteLength);
11303      result.set(type, 4); // copy the payload into the result
11304
11305      for (i = 0, size = 8; i < payload.length; i++) {
11306        result.set(payload[i], size);
11307        size += payload[i].byteLength;
11308      }
11309
11310      return result;
11311    };
11312
11313    dinf = function () {
11314      return box(types.dinf, box(types.dref, DREF));
11315    };
11316
11317    esds = function (track) {
11318      return box(types.esds, new Uint8Array([0x00, // version
11319      0x00, 0x00, 0x00, // flags
11320      // ES_Descriptor
11321      0x03, // tag, ES_DescrTag
11322      0x19, // length
11323      0x00, 0x00, // ES_ID
11324      0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority
11325      // DecoderConfigDescriptor
11326      0x04, // tag, DecoderConfigDescrTag
11327      0x11, // length
11328      0x40, // object type
11329      0x15, // streamType
11330      0x00, 0x06, 0x00, // bufferSizeDB
11331      0x00, 0x00, 0xda, 0xc0, // maxBitrate
11332      0x00, 0x00, 0xda, 0xc0, // avgBitrate
11333      // DecoderSpecificInfo
11334      0x05, // tag, DecoderSpecificInfoTag
11335      0x02, // length
11336      // ISO/IEC 14496-3, AudioSpecificConfig
11337      // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35
11338      track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig
11339      ]));
11340    };
11341
11342    ftyp = function () {
11343      return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND);
11344    };
11345
11346    hdlr = function (type) {
11347      return box(types.hdlr, HDLR_TYPES[type]);
11348    };
11349
11350    mdat = function (data) {
11351      return box(types.mdat, data);
11352    };
11353
11354    mdhd = function (track) {
11355      var result = new Uint8Array([0x00, // version 0
11356      0x00, 0x00, 0x00, // flags
11357      0x00, 0x00, 0x00, 0x02, // creation_time
11358      0x00, 0x00, 0x00, 0x03, // modification_time
11359      0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second
11360      track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration
11361      0x55, 0xc4, // 'und' language (undetermined)
11362      0x00, 0x00]); // Use the sample rate from the track metadata, when it is
11363      // defined. The sample rate can be parsed out of an ADTS header, for
11364      // instance.
11365
11366      if (track.samplerate) {
11367        result[12] = track.samplerate >>> 24 & 0xFF;
11368        result[13] = track.samplerate >>> 16 & 0xFF;
11369        result[14] = track.samplerate >>> 8 & 0xFF;
11370        result[15] = track.samplerate & 0xFF;
11371      }
11372
11373      return box(types.mdhd, result);
11374    };
11375
11376    mdia = function (track) {
11377      return box(types.mdia, mdhd(track), hdlr(track.type), minf(track));
11378    };
11379
11380    mfhd = function (sequenceNumber) {
11381      return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags
11382      (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number
11383      ]));
11384    };
11385
11386    minf = function (track) {
11387      return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track));
11388    };
11389
11390    moof = function (sequenceNumber, tracks) {
11391      var trackFragments = [],
11392          i = tracks.length; // build traf boxes for each track fragment
11393
11394      while (i--) {
11395        trackFragments[i] = traf(tracks[i]);
11396      }
11397
11398      return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments));
11399    };
11400    /**
11401     * Returns a movie box.
11402     * @param tracks {array} the tracks associated with this movie
11403     * @see ISO/IEC 14496-12:2012(E), section 8.2.1
11404     */
11405
11406
11407    moov = function (tracks) {
11408      var i = tracks.length,
11409          boxes = [];
11410
11411      while (i--) {
11412        boxes[i] = trak(tracks[i]);
11413      }
11414
11415      return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks)));
11416    };
11417
11418    mvex = function (tracks) {
11419      var i = tracks.length,
11420          boxes = [];
11421
11422      while (i--) {
11423        boxes[i] = trex(tracks[i]);
11424      }
11425
11426      return box.apply(null, [types.mvex].concat(boxes));
11427    };
11428
11429    mvhd = function (duration) {
11430      var bytes = new Uint8Array([0x00, // version 0
11431      0x00, 0x00, 0x00, // flags
11432      0x00, 0x00, 0x00, 0x01, // creation_time
11433      0x00, 0x00, 0x00, 0x02, // modification_time
11434      0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second
11435      (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration
11436      0x00, 0x01, 0x00, 0x00, // 1.0 rate
11437      0x01, 0x00, // 1.0 volume
11438      0x00, 0x00, // reserved
11439      0x00, 0x00, 0x00, 0x00, // reserved
11440      0x00, 0x00, 0x00, 0x00, // reserved
11441      0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
11442      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined
11443      0xff, 0xff, 0xff, 0xff // next_track_ID
11444      ]);
11445      return box(types.mvhd, bytes);
11446    };
11447
11448    sdtp = function (track) {
11449      var samples = track.samples || [],
11450          bytes = new Uint8Array(4 + samples.length),
11451          flags,
11452          i; // leave the full box header (4 bytes) all zero
11453      // write the sample table
11454
11455      for (i = 0; i < samples.length; i++) {
11456        flags = samples[i].flags;
11457        bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy;
11458      }
11459
11460      return box(types.sdtp, bytes);
11461    };
11462
11463    stbl = function (track) {
11464      return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO));
11465    };
11466
11467    (function () {
11468      var videoSample, audioSample;
11469
11470      stsd = function (track) {
11471        return box(types.stsd, new Uint8Array([0x00, // version 0
11472        0x00, 0x00, 0x00, // flags
11473        0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track));
11474      };
11475
11476      videoSample = function (track) {
11477        var sps = track.sps || [],
11478            pps = track.pps || [],
11479            sequenceParameterSets = [],
11480            pictureParameterSets = [],
11481            i,
11482            avc1Box; // assemble the SPSs
11483
11484        for (i = 0; i < sps.length; i++) {
11485          sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8);
11486          sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength
11487
11488          sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS
11489        } // assemble the PPSs
11490
11491
11492        for (i = 0; i < pps.length; i++) {
11493          pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8);
11494          pictureParameterSets.push(pps[i].byteLength & 0xFF);
11495          pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i]));
11496        }
11497
11498        avc1Box = [types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
11499        0x00, 0x01, // data_reference_index
11500        0x00, 0x00, // pre_defined
11501        0x00, 0x00, // reserved
11502        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined
11503        (track.width & 0xff00) >> 8, track.width & 0xff, // width
11504        (track.height & 0xff00) >> 8, track.height & 0xff, // height
11505        0x00, 0x48, 0x00, 0x00, // horizresolution
11506        0x00, 0x48, 0x00, 0x00, // vertresolution
11507        0x00, 0x00, 0x00, 0x00, // reserved
11508        0x00, 0x01, // frame_count
11509        0x13, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6a, 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x69, 0x62, 0x2d, 0x68, 0x6c, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // compressorname
11510        0x00, 0x18, // depth = 24
11511        0x11, 0x11 // pre_defined = -1
11512        ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion
11513        track.profileIdc, // AVCProfileIndication
11514        track.profileCompatibility, // profile_compatibility
11515        track.levelIdc, // AVCLevelIndication
11516        0xff // lengthSizeMinusOne, hard-coded to 4 bytes
11517        ].concat([sps.length], // numOfSequenceParameterSets
11518        sequenceParameterSets, // "SPS"
11519        [pps.length], // numOfPictureParameterSets
11520        pictureParameterSets // "PPS"
11521        ))), box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB
11522        0x00, 0x2d, 0xc6, 0xc0, // maxBitrate
11523        0x00, 0x2d, 0xc6, 0xc0 // avgBitrate
11524        ]))];
11525
11526        if (track.sarRatio) {
11527          var hSpacing = track.sarRatio[0],
11528              vSpacing = track.sarRatio[1];
11529          avc1Box.push(box(types.pasp, new Uint8Array([(hSpacing & 0xFF000000) >> 24, (hSpacing & 0xFF0000) >> 16, (hSpacing & 0xFF00) >> 8, hSpacing & 0xFF, (vSpacing & 0xFF000000) >> 24, (vSpacing & 0xFF0000) >> 16, (vSpacing & 0xFF00) >> 8, vSpacing & 0xFF])));
11530        }
11531
11532        return box.apply(null, avc1Box);
11533      };
11534
11535      audioSample = function (track) {
11536        return box(types.mp4a, new Uint8Array([// SampleEntry, ISO/IEC 14496-12
11537        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
11538        0x00, 0x01, // data_reference_index
11539        // AudioSampleEntry, ISO/IEC 14496-12
11540        0x00, 0x00, 0x00, 0x00, // reserved
11541        0x00, 0x00, 0x00, 0x00, // reserved
11542        (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount
11543        (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize
11544        0x00, 0x00, // pre_defined
11545        0x00, 0x00, // reserved
11546        (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16
11547        // MP4AudioSampleEntry, ISO/IEC 14496-14
11548        ]), esds(track));
11549      };
11550    })();
11551
11552    tkhd = function (track) {
11553      var result = new Uint8Array([0x00, // version 0
11554      0x00, 0x00, 0x07, // flags
11555      0x00, 0x00, 0x00, 0x00, // creation_time
11556      0x00, 0x00, 0x00, 0x00, // modification_time
11557      (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
11558      0x00, 0x00, 0x00, 0x00, // reserved
11559      (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration
11560      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved
11561      0x00, 0x00, // layer
11562      0x00, 0x00, // alternate_group
11563      0x01, 0x00, // non-audio track volume
11564      0x00, 0x00, // reserved
11565      0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix
11566      (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width
11567      (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height
11568      ]);
11569      return box(types.tkhd, result);
11570    };
11571    /**
11572     * Generate a track fragment (traf) box. A traf box collects metadata
11573     * about tracks in a movie fragment (moof) box.
11574     */
11575
11576
11577    traf = function (track) {
11578      var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime;
11579      trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0
11580      0x00, 0x00, 0x3a, // flags
11581      (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
11582      0x00, 0x00, 0x00, 0x01, // sample_description_index
11583      0x00, 0x00, 0x00, 0x00, // default_sample_duration
11584      0x00, 0x00, 0x00, 0x00, // default_sample_size
11585      0x00, 0x00, 0x00, 0x00 // default_sample_flags
11586      ]));
11587      upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / MAX_UINT32);
11588      lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % MAX_UINT32);
11589      trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1
11590      0x00, 0x00, 0x00, // flags
11591      // baseMediaDecodeTime
11592      upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF])); // the data offset specifies the number of bytes from the start of
11593      // the containing moof to the first payload byte of the associated
11594      // mdat
11595
11596      dataOffset = 32 + // tfhd
11597      20 + // tfdt
11598      8 + // traf header
11599      16 + // mfhd
11600      8 + // moof header
11601      8; // mdat header
11602      // audio tracks require less metadata
11603
11604      if (track.type === 'audio') {
11605        trackFragmentRun = trun$1(track, dataOffset);
11606        return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun);
11607      } // video tracks should contain an independent and disposable samples
11608      // box (sdtp)
11609      // generate one and adjust offsets to match
11610
11611
11612      sampleDependencyTable = sdtp(track);
11613      trackFragmentRun = trun$1(track, sampleDependencyTable.length + dataOffset);
11614      return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable);
11615    };
11616    /**
11617     * Generate a track box.
11618     * @param track {object} a track definition
11619     * @return {Uint8Array} the track box
11620     */
11621
11622
11623    trak = function (track) {
11624      track.duration = track.duration || 0xffffffff;
11625      return box(types.trak, tkhd(track), mdia(track));
11626    };
11627
11628    trex = function (track) {
11629      var result = new Uint8Array([0x00, // version 0
11630      0x00, 0x00, 0x00, // flags
11631      (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID
11632      0x00, 0x00, 0x00, 0x01, // default_sample_description_index
11633      0x00, 0x00, 0x00, 0x00, // default_sample_duration
11634      0x00, 0x00, 0x00, 0x00, // default_sample_size
11635      0x00, 0x01, 0x00, 0x01 // default_sample_flags
11636      ]); // the last two bytes of default_sample_flags is the sample
11637      // degradation priority, a hint about the importance of this sample
11638      // relative to others. Lower the degradation priority for all sample
11639      // types other than video.
11640
11641      if (track.type !== 'video') {
11642        result[result.length - 1] = 0x00;
11643      }
11644
11645      return box(types.trex, result);
11646    };
11647
11648    (function () {
11649      var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a
11650      // duration is present for the first sample, it will be present for
11651      // all subsequent samples.
11652      // see ISO/IEC 14496-12:2012, Section 8.8.8.1
11653
11654      trunHeader = function (samples, offset) {
11655        var durationPresent = 0,
11656            sizePresent = 0,
11657            flagsPresent = 0,
11658            compositionTimeOffset = 0; // trun flag constants
11659
11660        if (samples.length) {
11661          if (samples[0].duration !== undefined) {
11662            durationPresent = 0x1;
11663          }
11664
11665          if (samples[0].size !== undefined) {
11666            sizePresent = 0x2;
11667          }
11668
11669          if (samples[0].flags !== undefined) {
11670            flagsPresent = 0x4;
11671          }
11672
11673          if (samples[0].compositionTimeOffset !== undefined) {
11674            compositionTimeOffset = 0x8;
11675          }
11676        }
11677
11678        return [0x00, // version 0
11679        0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags
11680        (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count
11681        (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset
11682        ];
11683      };
11684
11685      videoTrun = function (track, offset) {
11686        var bytesOffest, bytes, header, samples, sample, i;
11687        samples = track.samples || [];
11688        offset += 8 + 12 + 16 * samples.length;
11689        header = trunHeader(samples, offset);
11690        bytes = new Uint8Array(header.length + samples.length * 16);
11691        bytes.set(header);
11692        bytesOffest = header.length;
11693
11694        for (i = 0; i < samples.length; i++) {
11695          sample = samples[i];
11696          bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24;
11697          bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16;
11698          bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8;
11699          bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration
11700
11701          bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24;
11702          bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16;
11703          bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8;
11704          bytes[bytesOffest++] = sample.size & 0xFF; // sample_size
11705
11706          bytes[bytesOffest++] = sample.flags.isLeading << 2 | sample.flags.dependsOn;
11707          bytes[bytesOffest++] = sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample;
11708          bytes[bytesOffest++] = sample.flags.degradationPriority & 0xF0 << 8;
11709          bytes[bytesOffest++] = sample.flags.degradationPriority & 0x0F; // sample_flags
11710
11711          bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF000000) >>> 24;
11712          bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF0000) >>> 16;
11713          bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF00) >>> 8;
11714          bytes[bytesOffest++] = sample.compositionTimeOffset & 0xFF; // sample_composition_time_offset
11715        }
11716
11717        return box(types.trun, bytes);
11718      };
11719
11720      audioTrun = function (track, offset) {
11721        var bytes, bytesOffest, header, samples, sample, i;
11722        samples = track.samples || [];
11723        offset += 8 + 12 + 8 * samples.length;
11724        header = trunHeader(samples, offset);
11725        bytes = new Uint8Array(header.length + samples.length * 8);
11726        bytes.set(header);
11727        bytesOffest = header.length;
11728
11729        for (i = 0; i < samples.length; i++) {
11730          sample = samples[i];
11731          bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24;
11732          bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16;
11733          bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8;
11734          bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration
11735
11736          bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24;
11737          bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16;
11738          bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8;
11739          bytes[bytesOffest++] = sample.size & 0xFF; // sample_size
11740        }
11741
11742        return box(types.trun, bytes);
11743      };
11744
11745      trun$1 = function (track, offset) {
11746        if (track.type === 'audio') {
11747          return audioTrun(track, offset);
11748        }
11749
11750        return videoTrun(track, offset);
11751      };
11752    })();
11753
11754    var mp4Generator = {
11755      ftyp: ftyp,
11756      mdat: mdat,
11757      moof: moof,
11758      moov: moov,
11759      initSegment: function (tracks) {
11760        var fileType = ftyp(),
11761            movie = moov(tracks),
11762            result;
11763        result = new Uint8Array(fileType.byteLength + movie.byteLength);
11764        result.set(fileType);
11765        result.set(movie, fileType.byteLength);
11766        return result;
11767      }
11768    };
11769    /**
11770     * mux.js
11771     *
11772     * Copyright (c) Brightcove
11773     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
11774     */
11775    // composed of the nal units that make up that frame
11776    // Also keep track of cummulative data about the frame from the nal units such
11777    // as the frame duration, starting pts, etc.
11778
11779    var groupNalsIntoFrames = function (nalUnits) {
11780      var i,
11781          currentNal,
11782          currentFrame = [],
11783          frames = []; // TODO added for LHLS, make sure this is OK
11784
11785      frames.byteLength = 0;
11786      frames.nalCount = 0;
11787      frames.duration = 0;
11788      currentFrame.byteLength = 0;
11789
11790      for (i = 0; i < nalUnits.length; i++) {
11791        currentNal = nalUnits[i]; // Split on 'aud'-type nal units
11792
11793        if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') {
11794          // Since the very first nal unit is expected to be an AUD
11795          // only push to the frames array when currentFrame is not empty
11796          if (currentFrame.length) {
11797            currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK
11798
11799            frames.byteLength += currentFrame.byteLength;
11800            frames.nalCount += currentFrame.length;
11801            frames.duration += currentFrame.duration;
11802            frames.push(currentFrame);
11803          }
11804
11805          currentFrame = [currentNal];
11806          currentFrame.byteLength = currentNal.data.byteLength;
11807          currentFrame.pts = currentNal.pts;
11808          currentFrame.dts = currentNal.dts;
11809        } else {
11810          // Specifically flag key frames for ease of use later
11811          if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') {
11812            currentFrame.keyFrame = true;
11813          }
11814
11815          currentFrame.duration = currentNal.dts - currentFrame.dts;
11816          currentFrame.byteLength += currentNal.data.byteLength;
11817          currentFrame.push(currentNal);
11818        }
11819      } // For the last frame, use the duration of the previous frame if we
11820      // have nothing better to go on
11821
11822
11823      if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) {
11824        currentFrame.duration = frames[frames.length - 1].duration;
11825      } // Push the final frame
11826      // TODO added for LHLS, make sure this is OK
11827
11828
11829      frames.byteLength += currentFrame.byteLength;
11830      frames.nalCount += currentFrame.length;
11831      frames.duration += currentFrame.duration;
11832      frames.push(currentFrame);
11833      return frames;
11834    }; // Convert an array of frames into an array of Gop with each Gop being composed
11835    // of the frames that make up that Gop
11836    // Also keep track of cummulative data about the Gop from the frames such as the
11837    // Gop duration, starting pts, etc.
11838
11839
11840    var groupFramesIntoGops = function (frames) {
11841      var i,
11842          currentFrame,
11843          currentGop = [],
11844          gops = []; // We must pre-set some of the values on the Gop since we
11845      // keep running totals of these values
11846
11847      currentGop.byteLength = 0;
11848      currentGop.nalCount = 0;
11849      currentGop.duration = 0;
11850      currentGop.pts = frames[0].pts;
11851      currentGop.dts = frames[0].dts; // store some metadata about all the Gops
11852
11853      gops.byteLength = 0;
11854      gops.nalCount = 0;
11855      gops.duration = 0;
11856      gops.pts = frames[0].pts;
11857      gops.dts = frames[0].dts;
11858
11859      for (i = 0; i < frames.length; i++) {
11860        currentFrame = frames[i];
11861
11862        if (currentFrame.keyFrame) {
11863          // Since the very first frame is expected to be an keyframe
11864          // only push to the gops array when currentGop is not empty
11865          if (currentGop.length) {
11866            gops.push(currentGop);
11867            gops.byteLength += currentGop.byteLength;
11868            gops.nalCount += currentGop.nalCount;
11869            gops.duration += currentGop.duration;
11870          }
11871
11872          currentGop = [currentFrame];
11873          currentGop.nalCount = currentFrame.length;
11874          currentGop.byteLength = currentFrame.byteLength;
11875          currentGop.pts = currentFrame.pts;
11876          currentGop.dts = currentFrame.dts;
11877          currentGop.duration = currentFrame.duration;
11878        } else {
11879          currentGop.duration += currentFrame.duration;
11880          currentGop.nalCount += currentFrame.length;
11881          currentGop.byteLength += currentFrame.byteLength;
11882          currentGop.push(currentFrame);
11883        }
11884      }
11885
11886      if (gops.length && currentGop.duration <= 0) {
11887        currentGop.duration = gops[gops.length - 1].duration;
11888      }
11889
11890      gops.byteLength += currentGop.byteLength;
11891      gops.nalCount += currentGop.nalCount;
11892      gops.duration += currentGop.duration; // push the final Gop
11893
11894      gops.push(currentGop);
11895      return gops;
11896    };
11897    /*
11898     * Search for the first keyframe in the GOPs and throw away all frames
11899     * until that keyframe. Then extend the duration of the pulled keyframe
11900     * and pull the PTS and DTS of the keyframe so that it covers the time
11901     * range of the frames that were disposed.
11902     *
11903     * @param {Array} gops video GOPs
11904     * @returns {Array} modified video GOPs
11905     */
11906
11907
11908    var extendFirstKeyFrame = function (gops) {
11909      var currentGop;
11910
11911      if (!gops[0][0].keyFrame && gops.length > 1) {
11912        // Remove the first GOP
11913        currentGop = gops.shift();
11914        gops.byteLength -= currentGop.byteLength;
11915        gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the
11916        // first gop to cover the time period of the
11917        // frames we just removed
11918
11919        gops[0][0].dts = currentGop.dts;
11920        gops[0][0].pts = currentGop.pts;
11921        gops[0][0].duration += currentGop.duration;
11922      }
11923
11924      return gops;
11925    };
11926    /**
11927     * Default sample object
11928     * see ISO/IEC 14496-12:2012, section 8.6.4.3
11929     */
11930
11931
11932    var createDefaultSample = function () {
11933      return {
11934        size: 0,
11935        flags: {
11936          isLeading: 0,
11937          dependsOn: 1,
11938          isDependedOn: 0,
11939          hasRedundancy: 0,
11940          degradationPriority: 0,
11941          isNonSyncSample: 1
11942        }
11943      };
11944    };
11945    /*
11946     * Collates information from a video frame into an object for eventual
11947     * entry into an MP4 sample table.
11948     *
11949     * @param {Object} frame the video frame
11950     * @param {Number} dataOffset the byte offset to position the sample
11951     * @return {Object} object containing sample table info for a frame
11952     */
11953
11954
11955    var sampleForFrame = function (frame, dataOffset) {
11956      var sample = createDefaultSample();
11957      sample.dataOffset = dataOffset;
11958      sample.compositionTimeOffset = frame.pts - frame.dts;
11959      sample.duration = frame.duration;
11960      sample.size = 4 * frame.length; // Space for nal unit size
11961
11962      sample.size += frame.byteLength;
11963
11964      if (frame.keyFrame) {
11965        sample.flags.dependsOn = 2;
11966        sample.flags.isNonSyncSample = 0;
11967      }
11968
11969      return sample;
11970    }; // generate the track's sample table from an array of gops
11971
11972
11973    var generateSampleTable$1 = function (gops, baseDataOffset) {
11974      var h,
11975          i,
11976          sample,
11977          currentGop,
11978          currentFrame,
11979          dataOffset = baseDataOffset || 0,
11980          samples = [];
11981
11982      for (h = 0; h < gops.length; h++) {
11983        currentGop = gops[h];
11984
11985        for (i = 0; i < currentGop.length; i++) {
11986          currentFrame = currentGop[i];
11987          sample = sampleForFrame(currentFrame, dataOffset);
11988          dataOffset += sample.size;
11989          samples.push(sample);
11990        }
11991      }
11992
11993      return samples;
11994    }; // generate the track's raw mdat data from an array of gops
11995
11996
11997    var concatenateNalData = function (gops) {
11998      var h,
11999          i,
12000          j,
12001          currentGop,
12002          currentFrame,
12003          currentNal,
12004          dataOffset = 0,
12005          nalsByteLength = gops.byteLength,
12006          numberOfNals = gops.nalCount,
12007          totalByteLength = nalsByteLength + 4 * numberOfNals,
12008          data = new Uint8Array(totalByteLength),
12009          view = new DataView(data.buffer); // For each Gop..
12010
12011      for (h = 0; h < gops.length; h++) {
12012        currentGop = gops[h]; // For each Frame..
12013
12014        for (i = 0; i < currentGop.length; i++) {
12015          currentFrame = currentGop[i]; // For each NAL..
12016
12017          for (j = 0; j < currentFrame.length; j++) {
12018            currentNal = currentFrame[j];
12019            view.setUint32(dataOffset, currentNal.data.byteLength);
12020            dataOffset += 4;
12021            data.set(currentNal.data, dataOffset);
12022            dataOffset += currentNal.data.byteLength;
12023          }
12024        }
12025      }
12026
12027      return data;
12028    }; // generate the track's sample table from a frame
12029
12030
12031    var generateSampleTableForFrame = function (frame, baseDataOffset) {
12032      var sample,
12033          dataOffset = baseDataOffset || 0,
12034          samples = [];
12035      sample = sampleForFrame(frame, dataOffset);
12036      samples.push(sample);
12037      return samples;
12038    }; // generate the track's raw mdat data from a frame
12039
12040
12041    var concatenateNalDataForFrame = function (frame) {
12042      var i,
12043          currentNal,
12044          dataOffset = 0,
12045          nalsByteLength = frame.byteLength,
12046          numberOfNals = frame.length,
12047          totalByteLength = nalsByteLength + 4 * numberOfNals,
12048          data = new Uint8Array(totalByteLength),
12049          view = new DataView(data.buffer); // For each NAL..
12050
12051      for (i = 0; i < frame.length; i++) {
12052        currentNal = frame[i];
12053        view.setUint32(dataOffset, currentNal.data.byteLength);
12054        dataOffset += 4;
12055        data.set(currentNal.data, dataOffset);
12056        dataOffset += currentNal.data.byteLength;
12057      }
12058
12059      return data;
12060    };
12061
12062    var frameUtils$1 = {
12063      groupNalsIntoFrames: groupNalsIntoFrames,
12064      groupFramesIntoGops: groupFramesIntoGops,
12065      extendFirstKeyFrame: extendFirstKeyFrame,
12066      generateSampleTable: generateSampleTable$1,
12067      concatenateNalData: concatenateNalData,
12068      generateSampleTableForFrame: generateSampleTableForFrame,
12069      concatenateNalDataForFrame: concatenateNalDataForFrame
12070    };
12071    /**
12072     * mux.js
12073     *
12074     * Copyright (c) Brightcove
12075     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
12076     */
12077
12078    var highPrefix = [33, 16, 5, 32, 164, 27];
12079    var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252];
12080
12081    var zeroFill = function (count) {
12082      var a = [];
12083
12084      while (count--) {
12085        a.push(0);
12086      }
12087
12088      return a;
12089    };
12090
12091    var makeTable = function (metaTable) {
12092      return Object.keys(metaTable).reduce(function (obj, key) {
12093        obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) {
12094          return arr.concat(part);
12095        }, []));
12096        return obj;
12097      }, {});
12098    };
12099
12100    var silence;
12101
12102    var silence_1 = function () {
12103      if (!silence) {
12104        // Frames-of-silence to use for filling in missing AAC frames
12105        var coneOfSilence = {
12106          96000: [highPrefix, [227, 64], zeroFill(154), [56]],
12107          88200: [highPrefix, [231], zeroFill(170), [56]],
12108          64000: [highPrefix, [248, 192], zeroFill(240), [56]],
12109          48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]],
12110          44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]],
12111          32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]],
12112          24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]],
12113          16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]],
12114          12000: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 253, 128], zeroFill(259), [56]],
12115          11025: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 255, 192], zeroFill(268), [55, 175, 128], zeroFill(108), [112]],
12116          8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]]
12117        };
12118        silence = makeTable(coneOfSilence);
12119      }
12120
12121      return silence;
12122    };
12123    /**
12124     * mux.js
12125     *
12126     * Copyright (c) Brightcove
12127     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
12128     */
12129
12130
12131    var ONE_SECOND_IN_TS$4 = 90000,
12132        // 90kHz clock
12133    secondsToVideoTs,
12134        secondsToAudioTs,
12135        videoTsToSeconds,
12136        audioTsToSeconds,
12137        audioTsToVideoTs,
12138        videoTsToAudioTs,
12139        metadataTsToSeconds;
12140
12141    secondsToVideoTs = function (seconds) {
12142      return seconds * ONE_SECOND_IN_TS$4;
12143    };
12144
12145    secondsToAudioTs = function (seconds, sampleRate) {
12146      return seconds * sampleRate;
12147    };
12148
12149    videoTsToSeconds = function (timestamp) {
12150      return timestamp / ONE_SECOND_IN_TS$4;
12151    };
12152
12153    audioTsToSeconds = function (timestamp, sampleRate) {
12154      return timestamp / sampleRate;
12155    };
12156
12157    audioTsToVideoTs = function (timestamp, sampleRate) {
12158      return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate));
12159    };
12160
12161    videoTsToAudioTs = function (timestamp, sampleRate) {
12162      return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate);
12163    };
12164    /**
12165     * Adjust ID3 tag or caption timing information by the timeline pts values
12166     * (if keepOriginalTimestamps is false) and convert to seconds
12167     */
12168
12169
12170    metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) {
12171      return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts);
12172    };
12173
12174    var clock$2 = {
12175      ONE_SECOND_IN_TS: ONE_SECOND_IN_TS$4,
12176      secondsToVideoTs: secondsToVideoTs,
12177      secondsToAudioTs: secondsToAudioTs,
12178      videoTsToSeconds: videoTsToSeconds,
12179      audioTsToSeconds: audioTsToSeconds,
12180      audioTsToVideoTs: audioTsToVideoTs,
12181      videoTsToAudioTs: videoTsToAudioTs,
12182      metadataTsToSeconds: metadataTsToSeconds
12183    };
12184    /**
12185     * mux.js
12186     *
12187     * Copyright (c) Brightcove
12188     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
12189     */
12190
12191    var coneOfSilence = silence_1;
12192    var clock$1 = clock$2;
12193    /**
12194     * Sum the `byteLength` properties of the data in each AAC frame
12195     */
12196
12197    var sumFrameByteLengths = function (array) {
12198      var i,
12199          currentObj,
12200          sum = 0; // sum the byteLength's all each nal unit in the frame
12201
12202      for (i = 0; i < array.length; i++) {
12203        currentObj = array[i];
12204        sum += currentObj.data.byteLength;
12205      }
12206
12207      return sum;
12208    }; // Possibly pad (prefix) the audio track with silence if appending this track
12209    // would lead to the introduction of a gap in the audio buffer
12210
12211
12212    var prefixWithSilence = function (track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) {
12213      var baseMediaDecodeTimeTs,
12214          frameDuration = 0,
12215          audioGapDuration = 0,
12216          audioFillFrameCount = 0,
12217          audioFillDuration = 0,
12218          silentFrame,
12219          i,
12220          firstFrame;
12221
12222      if (!frames.length) {
12223        return;
12224      }
12225
12226      baseMediaDecodeTimeTs = clock$1.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills
12227
12228      frameDuration = Math.ceil(clock$1.ONE_SECOND_IN_TS / (track.samplerate / 1024));
12229
12230      if (audioAppendStartTs && videoBaseMediaDecodeTime) {
12231        // insert the shortest possible amount (audio gap or audio to video gap)
12232        audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap
12233
12234        audioFillFrameCount = Math.floor(audioGapDuration / frameDuration);
12235        audioFillDuration = audioFillFrameCount * frameDuration;
12236      } // don't attempt to fill gaps smaller than a single frame or larger
12237      // than a half second
12238
12239
12240      if (audioFillFrameCount < 1 || audioFillDuration > clock$1.ONE_SECOND_IN_TS / 2) {
12241        return;
12242      }
12243
12244      silentFrame = coneOfSilence()[track.samplerate];
12245
12246      if (!silentFrame) {
12247        // we don't have a silent frame pregenerated for the sample rate, so use a frame
12248        // from the content instead
12249        silentFrame = frames[0].data;
12250      }
12251
12252      for (i = 0; i < audioFillFrameCount; i++) {
12253        firstFrame = frames[0];
12254        frames.splice(0, 0, {
12255          data: silentFrame,
12256          dts: firstFrame.dts - frameDuration,
12257          pts: firstFrame.pts - frameDuration
12258        });
12259      }
12260
12261      track.baseMediaDecodeTime -= Math.floor(clock$1.videoTsToAudioTs(audioFillDuration, track.samplerate));
12262      return audioFillDuration;
12263    }; // If the audio segment extends before the earliest allowed dts
12264    // value, remove AAC frames until starts at or after the earliest
12265    // allowed DTS so that we don't end up with a negative baseMedia-
12266    // DecodeTime for the audio track
12267
12268
12269    var trimAdtsFramesByEarliestDts = function (adtsFrames, track, earliestAllowedDts) {
12270      if (track.minSegmentDts >= earliestAllowedDts) {
12271        return adtsFrames;
12272      } // We will need to recalculate the earliest segment Dts
12273
12274
12275      track.minSegmentDts = Infinity;
12276      return adtsFrames.filter(function (currentFrame) {
12277        // If this is an allowed frame, keep it and record it's Dts
12278        if (currentFrame.dts >= earliestAllowedDts) {
12279          track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts);
12280          track.minSegmentPts = track.minSegmentDts;
12281          return true;
12282        } // Otherwise, discard it
12283
12284
12285        return false;
12286      });
12287    }; // generate the track's raw mdat data from an array of frames
12288
12289
12290    var generateSampleTable = function (frames) {
12291      var i,
12292          currentFrame,
12293          samples = [];
12294
12295      for (i = 0; i < frames.length; i++) {
12296        currentFrame = frames[i];
12297        samples.push({
12298          size: currentFrame.data.byteLength,
12299          duration: 1024 // For AAC audio, all samples contain 1024 samples
12300
12301        });
12302      }
12303
12304      return samples;
12305    }; // generate the track's sample table from an array of frames
12306
12307
12308    var concatenateFrameData = function (frames) {
12309      var i,
12310          currentFrame,
12311          dataOffset = 0,
12312          data = new Uint8Array(sumFrameByteLengths(frames));
12313
12314      for (i = 0; i < frames.length; i++) {
12315        currentFrame = frames[i];
12316        data.set(currentFrame.data, dataOffset);
12317        dataOffset += currentFrame.data.byteLength;
12318      }
12319
12320      return data;
12321    };
12322
12323    var audioFrameUtils$1 = {
12324      prefixWithSilence: prefixWithSilence,
12325      trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts,
12326      generateSampleTable: generateSampleTable,
12327      concatenateFrameData: concatenateFrameData
12328    };
12329    /**
12330     * mux.js
12331     *
12332     * Copyright (c) Brightcove
12333     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
12334     */
12335
12336    var ONE_SECOND_IN_TS$3 = clock$2.ONE_SECOND_IN_TS;
12337    /**
12338     * Store information about the start and end of the track and the
12339     * duration for each frame/sample we process in order to calculate
12340     * the baseMediaDecodeTime
12341     */
12342
12343    var collectDtsInfo = function (track, data) {
12344      if (typeof data.pts === 'number') {
12345        if (track.timelineStartInfo.pts === undefined) {
12346          track.timelineStartInfo.pts = data.pts;
12347        }
12348
12349        if (track.minSegmentPts === undefined) {
12350          track.minSegmentPts = data.pts;
12351        } else {
12352          track.minSegmentPts = Math.min(track.minSegmentPts, data.pts);
12353        }
12354
12355        if (track.maxSegmentPts === undefined) {
12356          track.maxSegmentPts = data.pts;
12357        } else {
12358          track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts);
12359        }
12360      }
12361
12362      if (typeof data.dts === 'number') {
12363        if (track.timelineStartInfo.dts === undefined) {
12364          track.timelineStartInfo.dts = data.dts;
12365        }
12366
12367        if (track.minSegmentDts === undefined) {
12368          track.minSegmentDts = data.dts;
12369        } else {
12370          track.minSegmentDts = Math.min(track.minSegmentDts, data.dts);
12371        }
12372
12373        if (track.maxSegmentDts === undefined) {
12374          track.maxSegmentDts = data.dts;
12375        } else {
12376          track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts);
12377        }
12378      }
12379    };
12380    /**
12381     * Clear values used to calculate the baseMediaDecodeTime between
12382     * tracks
12383     */
12384
12385
12386    var clearDtsInfo = function (track) {
12387      delete track.minSegmentDts;
12388      delete track.maxSegmentDts;
12389      delete track.minSegmentPts;
12390      delete track.maxSegmentPts;
12391    };
12392    /**
12393     * Calculate the track's baseMediaDecodeTime based on the earliest
12394     * DTS the transmuxer has ever seen and the minimum DTS for the
12395     * current track
12396     * @param track {object} track metadata configuration
12397     * @param keepOriginalTimestamps {boolean} If true, keep the timestamps
12398     *        in the source; false to adjust the first segment to start at 0.
12399     */
12400
12401
12402    var calculateTrackBaseMediaDecodeTime = function (track, keepOriginalTimestamps) {
12403      var baseMediaDecodeTime,
12404          scale,
12405          minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero.
12406
12407      if (!keepOriginalTimestamps) {
12408        minSegmentDts -= track.timelineStartInfo.dts;
12409      } // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where
12410      // we want the start of the first segment to be placed
12411
12412
12413      baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first
12414
12415      baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative
12416
12417      baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime);
12418
12419      if (track.type === 'audio') {
12420        // Audio has a different clock equal to the sampling_rate so we need to
12421        // scale the PTS values into the clock rate of the track
12422        scale = track.samplerate / ONE_SECOND_IN_TS$3;
12423        baseMediaDecodeTime *= scale;
12424        baseMediaDecodeTime = Math.floor(baseMediaDecodeTime);
12425      }
12426
12427      return baseMediaDecodeTime;
12428    };
12429
12430    var trackDecodeInfo$1 = {
12431      clearDtsInfo: clearDtsInfo,
12432      calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime,
12433      collectDtsInfo: collectDtsInfo
12434    };
12435    /**
12436     * mux.js
12437     *
12438     * Copyright (c) Brightcove
12439     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
12440     *
12441     * Reads in-band caption information from a video elementary
12442     * stream. Captions must follow the CEA-708 standard for injection
12443     * into an MPEG-2 transport streams.
12444     * @see https://en.wikipedia.org/wiki/CEA-708
12445     * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
12446     */
12447    // payload type field to indicate how they are to be
12448    // interpreted. CEAS-708 caption content is always transmitted with
12449    // payload type 0x04.
12450
12451    var USER_DATA_REGISTERED_ITU_T_T35 = 4,
12452        RBSP_TRAILING_BITS = 128;
12453    /**
12454      * Parse a supplemental enhancement information (SEI) NAL unit.
12455      * Stops parsing once a message of type ITU T T35 has been found.
12456      *
12457      * @param bytes {Uint8Array} the bytes of a SEI NAL unit
12458      * @return {object} the parsed SEI payload
12459      * @see Rec. ITU-T H.264, 7.3.2.3.1
12460      */
12461
12462    var parseSei = function (bytes) {
12463      var i = 0,
12464          result = {
12465        payloadType: -1,
12466        payloadSize: 0
12467      },
12468          payloadType = 0,
12469          payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message
12470
12471      while (i < bytes.byteLength) {
12472        // stop once we have hit the end of the sei_rbsp
12473        if (bytes[i] === RBSP_TRAILING_BITS) {
12474          break;
12475        } // Parse payload type
12476
12477
12478        while (bytes[i] === 0xFF) {
12479          payloadType += 255;
12480          i++;
12481        }
12482
12483        payloadType += bytes[i++]; // Parse payload size
12484
12485        while (bytes[i] === 0xFF) {
12486          payloadSize += 255;
12487          i++;
12488        }
12489
12490        payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break
12491        // there can only ever be one caption message in a frame's sei
12492
12493        if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) {
12494          var userIdentifier = String.fromCharCode(bytes[i + 3], bytes[i + 4], bytes[i + 5], bytes[i + 6]);
12495
12496          if (userIdentifier === 'GA94') {
12497            result.payloadType = payloadType;
12498            result.payloadSize = payloadSize;
12499            result.payload = bytes.subarray(i, i + payloadSize);
12500            break;
12501          } else {
12502            result.payload = void 0;
12503          }
12504        } // skip the payload and parse the next message
12505
12506
12507        i += payloadSize;
12508        payloadType = 0;
12509        payloadSize = 0;
12510      }
12511
12512      return result;
12513    }; // see ANSI/SCTE 128-1 (2013), section 8.1
12514
12515
12516    var parseUserData = function (sei) {
12517      // itu_t_t35_contry_code must be 181 (United States) for
12518      // captions
12519      if (sei.payload[0] !== 181) {
12520        return null;
12521      } // itu_t_t35_provider_code should be 49 (ATSC) for captions
12522
12523
12524      if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) {
12525        return null;
12526      } // the user_identifier should be "GA94" to indicate ATSC1 data
12527
12528
12529      if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') {
12530        return null;
12531      } // finally, user_data_type_code should be 0x03 for caption data
12532
12533
12534      if (sei.payload[7] !== 0x03) {
12535        return null;
12536      } // return the user_data_type_structure and strip the trailing
12537      // marker bits
12538
12539
12540      return sei.payload.subarray(8, sei.payload.length - 1);
12541    }; // see CEA-708-D, section 4.4
12542
12543
12544    var parseCaptionPackets = function (pts, userData) {
12545      var results = [],
12546          i,
12547          count,
12548          offset,
12549          data; // if this is just filler, return immediately
12550
12551      if (!(userData[0] & 0x40)) {
12552        return results;
12553      } // parse out the cc_data_1 and cc_data_2 fields
12554
12555
12556      count = userData[0] & 0x1f;
12557
12558      for (i = 0; i < count; i++) {
12559        offset = i * 3;
12560        data = {
12561          type: userData[offset + 2] & 0x03,
12562          pts: pts
12563        }; // capture cc data when cc_valid is 1
12564
12565        if (userData[offset + 2] & 0x04) {
12566          data.ccData = userData[offset + 3] << 8 | userData[offset + 4];
12567          results.push(data);
12568        }
12569      }
12570
12571      return results;
12572    };
12573
12574    var discardEmulationPreventionBytes$1 = function (data) {
12575      var length = data.byteLength,
12576          emulationPreventionBytesPositions = [],
12577          i = 1,
12578          newLength,
12579          newData; // Find all `Emulation Prevention Bytes`
12580
12581      while (i < length - 2) {
12582        if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
12583          emulationPreventionBytesPositions.push(i + 2);
12584          i += 2;
12585        } else {
12586          i++;
12587        }
12588      } // If no Emulation Prevention Bytes were found just return the original
12589      // array
12590
12591
12592      if (emulationPreventionBytesPositions.length === 0) {
12593        return data;
12594      } // Create a new array to hold the NAL unit data
12595
12596
12597      newLength = length - emulationPreventionBytesPositions.length;
12598      newData = new Uint8Array(newLength);
12599      var sourceIndex = 0;
12600
12601      for (i = 0; i < newLength; sourceIndex++, i++) {
12602        if (sourceIndex === emulationPreventionBytesPositions[0]) {
12603          // Skip this byte
12604          sourceIndex++; // Remove this position index
12605
12606          emulationPreventionBytesPositions.shift();
12607        }
12608
12609        newData[i] = data[sourceIndex];
12610      }
12611
12612      return newData;
12613    }; // exports
12614
12615
12616    var captionPacketParser = {
12617      parseSei: parseSei,
12618      parseUserData: parseUserData,
12619      parseCaptionPackets: parseCaptionPackets,
12620      discardEmulationPreventionBytes: discardEmulationPreventionBytes$1,
12621      USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35
12622    };
12623    /**
12624     * mux.js
12625     *
12626     * Copyright (c) Brightcove
12627     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
12628     *
12629     * Reads in-band caption information from a video elementary
12630     * stream. Captions must follow the CEA-708 standard for injection
12631     * into an MPEG-2 transport streams.
12632     * @see https://en.wikipedia.org/wiki/CEA-708
12633     * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf
12634     */
12635    // Link To Transport
12636    // -----------------
12637
12638    var Stream$7 = stream;
12639    var cea708Parser = captionPacketParser;
12640
12641    var CaptionStream$2 = function (options) {
12642      options = options || {};
12643      CaptionStream$2.prototype.init.call(this); // parse708captions flag, default to true
12644
12645      this.parse708captions_ = typeof options.parse708captions === 'boolean' ? options.parse708captions : true;
12646      this.captionPackets_ = [];
12647      this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define
12648      new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define
12649      new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define
12650      new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define
12651      ];
12652
12653      if (this.parse708captions_) {
12654        this.cc708Stream_ = new Cea708Stream({
12655          captionServices: options.captionServices
12656        }); // eslint-disable-line no-use-before-define
12657      }
12658
12659      this.reset(); // forward data and done events from CCs to this CaptionStream
12660
12661      this.ccStreams_.forEach(function (cc) {
12662        cc.on('data', this.trigger.bind(this, 'data'));
12663        cc.on('partialdone', this.trigger.bind(this, 'partialdone'));
12664        cc.on('done', this.trigger.bind(this, 'done'));
12665      }, this);
12666
12667      if (this.parse708captions_) {
12668        this.cc708Stream_.on('data', this.trigger.bind(this, 'data'));
12669        this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone'));
12670        this.cc708Stream_.on('done', this.trigger.bind(this, 'done'));
12671      }
12672    };
12673
12674    CaptionStream$2.prototype = new Stream$7();
12675
12676    CaptionStream$2.prototype.push = function (event) {
12677      var sei, userData, newCaptionPackets; // only examine SEI NALs
12678
12679      if (event.nalUnitType !== 'sei_rbsp') {
12680        return;
12681      } // parse the sei
12682
12683
12684      sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip
12685
12686      if (!sei.payload) {
12687        return;
12688      } // ignore everything but user_data_registered_itu_t_t35
12689
12690
12691      if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) {
12692        return;
12693      } // parse out the user data payload
12694
12695
12696      userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData
12697
12698      if (!userData) {
12699        return;
12700      } // Sometimes, the same segment # will be downloaded twice. To stop the
12701      // caption data from being processed twice, we track the latest dts we've
12702      // received and ignore everything with a dts before that. However, since
12703      // data for a specific dts can be split across packets on either side of
12704      // a segment boundary, we need to make sure we *don't* ignore the packets
12705      // from the *next* segment that have dts === this.latestDts_. By constantly
12706      // tracking the number of packets received with dts === this.latestDts_, we
12707      // know how many should be ignored once we start receiving duplicates.
12708
12709
12710      if (event.dts < this.latestDts_) {
12711        // We've started getting older data, so set the flag.
12712        this.ignoreNextEqualDts_ = true;
12713        return;
12714      } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) {
12715        this.numSameDts_--;
12716
12717        if (!this.numSameDts_) {
12718          // We've received the last duplicate packet, time to start processing again
12719          this.ignoreNextEqualDts_ = false;
12720        }
12721
12722        return;
12723      } // parse out CC data packets and save them for later
12724
12725
12726      newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData);
12727      this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets);
12728
12729      if (this.latestDts_ !== event.dts) {
12730        this.numSameDts_ = 0;
12731      }
12732
12733      this.numSameDts_++;
12734      this.latestDts_ = event.dts;
12735    };
12736
12737    CaptionStream$2.prototype.flushCCStreams = function (flushType) {
12738      this.ccStreams_.forEach(function (cc) {
12739        return flushType === 'flush' ? cc.flush() : cc.partialFlush();
12740      }, this);
12741    };
12742
12743    CaptionStream$2.prototype.flushStream = function (flushType) {
12744      // make sure we actually parsed captions before proceeding
12745      if (!this.captionPackets_.length) {
12746        this.flushCCStreams(flushType);
12747        return;
12748      } // In Chrome, the Array#sort function is not stable so add a
12749      // presortIndex that we can use to ensure we get a stable-sort
12750
12751
12752      this.captionPackets_.forEach(function (elem, idx) {
12753        elem.presortIndex = idx;
12754      }); // sort caption byte-pairs based on their PTS values
12755
12756      this.captionPackets_.sort(function (a, b) {
12757        if (a.pts === b.pts) {
12758          return a.presortIndex - b.presortIndex;
12759        }
12760
12761        return a.pts - b.pts;
12762      });
12763      this.captionPackets_.forEach(function (packet) {
12764        if (packet.type < 2) {
12765          // Dispatch packet to the right Cea608Stream
12766          this.dispatchCea608Packet(packet);
12767        } else {
12768          // Dispatch packet to the Cea708Stream
12769          this.dispatchCea708Packet(packet);
12770        }
12771      }, this);
12772      this.captionPackets_.length = 0;
12773      this.flushCCStreams(flushType);
12774    };
12775
12776    CaptionStream$2.prototype.flush = function () {
12777      return this.flushStream('flush');
12778    }; // Only called if handling partial data
12779
12780
12781    CaptionStream$2.prototype.partialFlush = function () {
12782      return this.flushStream('partialFlush');
12783    };
12784
12785    CaptionStream$2.prototype.reset = function () {
12786      this.latestDts_ = null;
12787      this.ignoreNextEqualDts_ = false;
12788      this.numSameDts_ = 0;
12789      this.activeCea608Channel_ = [null, null];
12790      this.ccStreams_.forEach(function (ccStream) {
12791        ccStream.reset();
12792      });
12793    }; // From the CEA-608 spec:
12794
12795    /*
12796     * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed
12797     * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is
12798     * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair
12799     * and subsequent data should then be processed according to the FCC rules. It may be necessary for the
12800     * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD)
12801     * to switch to captioning or Text.
12802    */
12803    // With that in mind, we ignore any data between an XDS control code and a
12804    // subsequent closed-captioning control code.
12805
12806
12807    CaptionStream$2.prototype.dispatchCea608Packet = function (packet) {
12808      // NOTE: packet.type is the CEA608 field
12809      if (this.setsTextOrXDSActive(packet)) {
12810        this.activeCea608Channel_[packet.type] = null;
12811      } else if (this.setsChannel1Active(packet)) {
12812        this.activeCea608Channel_[packet.type] = 0;
12813      } else if (this.setsChannel2Active(packet)) {
12814        this.activeCea608Channel_[packet.type] = 1;
12815      }
12816
12817      if (this.activeCea608Channel_[packet.type] === null) {
12818        // If we haven't received anything to set the active channel, or the
12819        // packets are Text/XDS data, discard the data; we don't want jumbled
12820        // captions
12821        return;
12822      }
12823
12824      this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet);
12825    };
12826
12827    CaptionStream$2.prototype.setsChannel1Active = function (packet) {
12828      return (packet.ccData & 0x7800) === 0x1000;
12829    };
12830
12831    CaptionStream$2.prototype.setsChannel2Active = function (packet) {
12832      return (packet.ccData & 0x7800) === 0x1800;
12833    };
12834
12835    CaptionStream$2.prototype.setsTextOrXDSActive = function (packet) {
12836      return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a;
12837    };
12838
12839    CaptionStream$2.prototype.dispatchCea708Packet = function (packet) {
12840      if (this.parse708captions_) {
12841        this.cc708Stream_.push(packet);
12842      }
12843    }; // ----------------------
12844    // Session to Application
12845    // ----------------------
12846    // This hash maps special and extended character codes to their
12847    // proper Unicode equivalent. The first one-byte key is just a
12848    // non-standard character code. The two-byte keys that follow are
12849    // the extended CEA708 character codes, along with the preceding
12850    // 0x10 extended character byte to distinguish these codes from
12851    // non-extended character codes. Every CEA708 character code that
12852    // is not in this object maps directly to a standard unicode
12853    // character code.
12854    // The transparent space and non-breaking transparent space are
12855    // technically not fully supported since there is no code to
12856    // make them transparent, so they have normal non-transparent
12857    // stand-ins.
12858    // The special closed caption (CC) character isn't a standard
12859    // unicode character, so a fairly similar unicode character was
12860    // chosen in it's place.
12861
12862
12863    var CHARACTER_TRANSLATION_708 = {
12864      0x7f: 0x266a,
12865      // ♪
12866      0x1020: 0x20,
12867      // Transparent Space
12868      0x1021: 0xa0,
12869      // Nob-breaking Transparent Space
12870      0x1025: 0x2026,
12871      // …
12872      0x102a: 0x0160,
12873      // Å 
12874      0x102c: 0x0152,
12875      // Œ
12876      0x1030: 0x2588,
12877      // █
12878      0x1031: 0x2018,
12879      // ‘
12880      0x1032: 0x2019,
12881      // ’
12882      0x1033: 0x201c,
12883      // “
12884      0x1034: 0x201d,
12885      // ”
12886      0x1035: 0x2022,
12887      // •
12888      0x1039: 0x2122,
12889      // ™
12890      0x103a: 0x0161,
12891      // Å¡
12892      0x103c: 0x0153,
12893      // œ
12894      0x103d: 0x2120,
12895      // ℠
12896      0x103f: 0x0178,
12897      // Ÿ
12898      0x1076: 0x215b,
12899      // ⅛
12900      0x1077: 0x215c,
12901      // ⅜
12902      0x1078: 0x215d,
12903      // ⅝
12904      0x1079: 0x215e,
12905      // ⅞
12906      0x107a: 0x23d0,
12907      // ⏐
12908      0x107b: 0x23a4,
12909      // ⎤
12910      0x107c: 0x23a3,
12911      // ⎣
12912      0x107d: 0x23af,
12913      // ⎯
12914      0x107e: 0x23a6,
12915      // ⎦
12916      0x107f: 0x23a1,
12917      // ⎡
12918      0x10a0: 0x3138 // ㄸ (CC char)
12919
12920    };
12921
12922    var get708CharFromCode = function (code) {
12923      var newCode = CHARACTER_TRANSLATION_708[code] || code;
12924
12925      if (code & 0x1000 && code === newCode) {
12926        // Invalid extended code
12927        return '';
12928      }
12929
12930      return String.fromCharCode(newCode);
12931    };
12932
12933    var within708TextBlock = function (b) {
12934      return 0x20 <= b && b <= 0x7f || 0xa0 <= b && b <= 0xff;
12935    };
12936
12937    var Cea708Window = function (windowNum) {
12938      this.windowNum = windowNum;
12939      this.reset();
12940    };
12941
12942    Cea708Window.prototype.reset = function () {
12943      this.clearText();
12944      this.pendingNewLine = false;
12945      this.winAttr = {};
12946      this.penAttr = {};
12947      this.penLoc = {};
12948      this.penColor = {}; // These default values are arbitrary,
12949      // defineWindow will usually override them
12950
12951      this.visible = 0;
12952      this.rowLock = 0;
12953      this.columnLock = 0;
12954      this.priority = 0;
12955      this.relativePositioning = 0;
12956      this.anchorVertical = 0;
12957      this.anchorHorizontal = 0;
12958      this.anchorPoint = 0;
12959      this.rowCount = 1;
12960      this.virtualRowCount = this.rowCount + 1;
12961      this.columnCount = 41;
12962      this.windowStyle = 0;
12963      this.penStyle = 0;
12964    };
12965
12966    Cea708Window.prototype.getText = function () {
12967      return this.rows.join('\n');
12968    };
12969
12970    Cea708Window.prototype.clearText = function () {
12971      this.rows = [''];
12972      this.rowIdx = 0;
12973    };
12974
12975    Cea708Window.prototype.newLine = function (pts) {
12976      if (this.rows.length >= this.virtualRowCount && typeof this.beforeRowOverflow === 'function') {
12977        this.beforeRowOverflow(pts);
12978      }
12979
12980      if (this.rows.length > 0) {
12981        this.rows.push('');
12982        this.rowIdx++;
12983      } // Show all virtual rows since there's no visible scrolling
12984
12985
12986      while (this.rows.length > this.virtualRowCount) {
12987        this.rows.shift();
12988        this.rowIdx--;
12989      }
12990    };
12991
12992    Cea708Window.prototype.isEmpty = function () {
12993      if (this.rows.length === 0) {
12994        return true;
12995      } else if (this.rows.length === 1) {
12996        return this.rows[0] === '';
12997      }
12998
12999      return false;
13000    };
13001
13002    Cea708Window.prototype.addText = function (text) {
13003      this.rows[this.rowIdx] += text;
13004    };
13005
13006    Cea708Window.prototype.backspace = function () {
13007      if (!this.isEmpty()) {
13008        var row = this.rows[this.rowIdx];
13009        this.rows[this.rowIdx] = row.substr(0, row.length - 1);
13010      }
13011    };
13012
13013    var Cea708Service = function (serviceNum, encoding, stream) {
13014      this.serviceNum = serviceNum;
13015      this.text = '';
13016      this.currentWindow = new Cea708Window(-1);
13017      this.windows = [];
13018      this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided
13019
13020      if (typeof encoding === 'string') {
13021        this.createTextDecoder(encoding);
13022      }
13023    };
13024    /**
13025     * Initialize service windows
13026     * Must be run before service use
13027     *
13028     * @param  {Integer}  pts               PTS value
13029     * @param  {Function} beforeRowOverflow Function to execute before row overflow of a window
13030     */
13031
13032
13033    Cea708Service.prototype.init = function (pts, beforeRowOverflow) {
13034      this.startPts = pts;
13035
13036      for (var win = 0; win < 8; win++) {
13037        this.windows[win] = new Cea708Window(win);
13038
13039        if (typeof beforeRowOverflow === 'function') {
13040          this.windows[win].beforeRowOverflow = beforeRowOverflow;
13041        }
13042      }
13043    };
13044    /**
13045     * Set current window of service to be affected by commands
13046     *
13047     * @param  {Integer} windowNum Window number
13048     */
13049
13050
13051    Cea708Service.prototype.setCurrentWindow = function (windowNum) {
13052      this.currentWindow = this.windows[windowNum];
13053    };
13054    /**
13055     * Try to create a TextDecoder if it is natively supported
13056     */
13057
13058
13059    Cea708Service.prototype.createTextDecoder = function (encoding) {
13060      if (typeof TextDecoder === 'undefined') {
13061        this.stream.trigger('log', {
13062          level: 'warn',
13063          message: 'The `encoding` option is unsupported without TextDecoder support'
13064        });
13065      } else {
13066        try {
13067          this.textDecoder_ = new TextDecoder(encoding);
13068        } catch (error) {
13069          this.stream.trigger('log', {
13070            level: 'warn',
13071            message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error
13072          });
13073        }
13074      }
13075    };
13076
13077    var Cea708Stream = function (options) {
13078      options = options || {};
13079      Cea708Stream.prototype.init.call(this);
13080      var self = this;
13081      var captionServices = options.captionServices || {};
13082      var captionServiceEncodings = {};
13083      var serviceProps; // Get service encodings from captionServices option block
13084
13085      Object.keys(captionServices).forEach(serviceName => {
13086        serviceProps = captionServices[serviceName];
13087
13088        if (/^SERVICE/.test(serviceName)) {
13089          captionServiceEncodings[serviceName] = serviceProps.encoding;
13090        }
13091      });
13092      this.serviceEncodings = captionServiceEncodings;
13093      this.current708Packet = null;
13094      this.services = {};
13095
13096      this.push = function (packet) {
13097        if (packet.type === 3) {
13098          // 708 packet start
13099          self.new708Packet();
13100          self.add708Bytes(packet);
13101        } else {
13102          if (self.current708Packet === null) {
13103            // This should only happen at the start of a file if there's no packet start.
13104            self.new708Packet();
13105          }
13106
13107          self.add708Bytes(packet);
13108        }
13109      };
13110    };
13111
13112    Cea708Stream.prototype = new Stream$7();
13113    /**
13114     * Push current 708 packet, create new 708 packet.
13115     */
13116
13117    Cea708Stream.prototype.new708Packet = function () {
13118      if (this.current708Packet !== null) {
13119        this.push708Packet();
13120      }
13121
13122      this.current708Packet = {
13123        data: [],
13124        ptsVals: []
13125      };
13126    };
13127    /**
13128     * Add pts and both bytes from packet into current 708 packet.
13129     */
13130
13131
13132    Cea708Stream.prototype.add708Bytes = function (packet) {
13133      var data = packet.ccData;
13134      var byte0 = data >>> 8;
13135      var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec
13136      // that service blocks will always line up with byte pairs.
13137
13138      this.current708Packet.ptsVals.push(packet.pts);
13139      this.current708Packet.data.push(byte0);
13140      this.current708Packet.data.push(byte1);
13141    };
13142    /**
13143     * Parse completed 708 packet into service blocks and push each service block.
13144     */
13145
13146
13147    Cea708Stream.prototype.push708Packet = function () {
13148      var packet708 = this.current708Packet;
13149      var packetData = packet708.data;
13150      var serviceNum = null;
13151      var blockSize = null;
13152      var i = 0;
13153      var b = packetData[i++];
13154      packet708.seq = b >> 6;
13155      packet708.sizeCode = b & 0x3f; // 0b00111111;
13156
13157      for (; i < packetData.length; i++) {
13158        b = packetData[i++];
13159        serviceNum = b >> 5;
13160        blockSize = b & 0x1f; // 0b00011111
13161
13162        if (serviceNum === 7 && blockSize > 0) {
13163          // Extended service num
13164          b = packetData[i++];
13165          serviceNum = b;
13166        }
13167
13168        this.pushServiceBlock(serviceNum, i, blockSize);
13169
13170        if (blockSize > 0) {
13171          i += blockSize - 1;
13172        }
13173      }
13174    };
13175    /**
13176     * Parse service block, execute commands, read text.
13177     *
13178     * Note: While many of these commands serve important purposes,
13179     * many others just parse out the parameters or attributes, but
13180     * nothing is done with them because this is not a full and complete
13181     * implementation of the entire 708 spec.
13182     *
13183     * @param  {Integer} serviceNum Service number
13184     * @param  {Integer} start      Start index of the 708 packet data
13185     * @param  {Integer} size       Block size
13186     */
13187
13188
13189    Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) {
13190      var b;
13191      var i = start;
13192      var packetData = this.current708Packet.data;
13193      var service = this.services[serviceNum];
13194
13195      if (!service) {
13196        service = this.initService(serviceNum, i);
13197      }
13198
13199      for (; i < start + size && i < packetData.length; i++) {
13200        b = packetData[i];
13201
13202        if (within708TextBlock(b)) {
13203          i = this.handleText(i, service);
13204        } else if (b === 0x18) {
13205          i = this.multiByteCharacter(i, service);
13206        } else if (b === 0x10) {
13207          i = this.extendedCommands(i, service);
13208        } else if (0x80 <= b && b <= 0x87) {
13209          i = this.setCurrentWindow(i, service);
13210        } else if (0x98 <= b && b <= 0x9f) {
13211          i = this.defineWindow(i, service);
13212        } else if (b === 0x88) {
13213          i = this.clearWindows(i, service);
13214        } else if (b === 0x8c) {
13215          i = this.deleteWindows(i, service);
13216        } else if (b === 0x89) {
13217          i = this.displayWindows(i, service);
13218        } else if (b === 0x8a) {
13219          i = this.hideWindows(i, service);
13220        } else if (b === 0x8b) {
13221          i = this.toggleWindows(i, service);
13222        } else if (b === 0x97) {
13223          i = this.setWindowAttributes(i, service);
13224        } else if (b === 0x90) {
13225          i = this.setPenAttributes(i, service);
13226        } else if (b === 0x91) {
13227          i = this.setPenColor(i, service);
13228        } else if (b === 0x92) {
13229          i = this.setPenLocation(i, service);
13230        } else if (b === 0x8f) {
13231          service = this.reset(i, service);
13232        } else if (b === 0x08) {
13233          // BS: Backspace
13234          service.currentWindow.backspace();
13235        } else if (b === 0x0c) {
13236          // FF: Form feed
13237          service.currentWindow.clearText();
13238        } else if (b === 0x0d) {
13239          // CR: Carriage return
13240          service.currentWindow.pendingNewLine = true;
13241        } else if (b === 0x0e) {
13242          // HCR: Horizontal carriage return
13243          service.currentWindow.clearText();
13244        } else if (b === 0x8d) {
13245          // DLY: Delay, nothing to do
13246          i++;
13247        } else ;
13248      }
13249    };
13250    /**
13251     * Execute an extended command
13252     *
13253     * @param  {Integer} i        Current index in the 708 packet
13254     * @param  {Service} service  The service object to be affected
13255     * @return {Integer}          New index after parsing
13256     */
13257
13258
13259    Cea708Stream.prototype.extendedCommands = function (i, service) {
13260      var packetData = this.current708Packet.data;
13261      var b = packetData[++i];
13262
13263      if (within708TextBlock(b)) {
13264        i = this.handleText(i, service, {
13265          isExtended: true
13266        });
13267      }
13268
13269      return i;
13270    };
13271    /**
13272     * Get PTS value of a given byte index
13273     *
13274     * @param  {Integer} byteIndex  Index of the byte
13275     * @return {Integer}            PTS
13276     */
13277
13278
13279    Cea708Stream.prototype.getPts = function (byteIndex) {
13280      // There's 1 pts value per 2 bytes
13281      return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)];
13282    };
13283    /**
13284     * Initializes a service
13285     *
13286     * @param  {Integer} serviceNum Service number
13287     * @return {Service}            Initialized service object
13288     */
13289
13290
13291    Cea708Stream.prototype.initService = function (serviceNum, i) {
13292      var serviceName = 'SERVICE' + serviceNum;
13293      var self = this;
13294      var serviceName;
13295      var encoding;
13296
13297      if (serviceName in this.serviceEncodings) {
13298        encoding = this.serviceEncodings[serviceName];
13299      }
13300
13301      this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self);
13302      this.services[serviceNum].init(this.getPts(i), function (pts) {
13303        self.flushDisplayed(pts, self.services[serviceNum]);
13304      });
13305      return this.services[serviceNum];
13306    };
13307    /**
13308     * Execute text writing to current window
13309     *
13310     * @param  {Integer} i        Current index in the 708 packet
13311     * @param  {Service} service  The service object to be affected
13312     * @return {Integer}          New index after parsing
13313     */
13314
13315
13316    Cea708Stream.prototype.handleText = function (i, service, options) {
13317      var isExtended = options && options.isExtended;
13318      var isMultiByte = options && options.isMultiByte;
13319      var packetData = this.current708Packet.data;
13320      var extended = isExtended ? 0x1000 : 0x0000;
13321      var currentByte = packetData[i];
13322      var nextByte = packetData[i + 1];
13323      var win = service.currentWindow;
13324      var char;
13325      var charCodeArray; // Converts an array of bytes to a unicode hex string.
13326
13327      function toHexString(byteArray) {
13328        return byteArray.map(byte => {
13329          return ('0' + (byte & 0xFF).toString(16)).slice(-2);
13330        }).join('');
13331      }
13332
13333      if (isMultiByte) {
13334        charCodeArray = [currentByte, nextByte];
13335        i++;
13336      } else {
13337        charCodeArray = [currentByte];
13338      } // Use the TextDecoder if one was created for this service
13339
13340
13341      if (service.textDecoder_ && !isExtended) {
13342        char = service.textDecoder_.decode(new Uint8Array(charCodeArray));
13343      } else {
13344        // We assume any multi-byte char without a decoder is unicode.
13345        if (isMultiByte) {
13346          const unicode = toHexString(charCodeArray); // Takes a unicode hex string and creates a single character.
13347
13348          char = String.fromCharCode(parseInt(unicode, 16));
13349        } else {
13350          char = get708CharFromCode(extended | currentByte);
13351        }
13352      }
13353
13354      if (win.pendingNewLine && !win.isEmpty()) {
13355        win.newLine(this.getPts(i));
13356      }
13357
13358      win.pendingNewLine = false;
13359      win.addText(char);
13360      return i;
13361    };
13362    /**
13363     * Handle decoding of multibyte character
13364     *
13365     * @param  {Integer} i        Current index in the 708 packet
13366     * @param  {Service} service  The service object to be affected
13367     * @return {Integer}          New index after parsing
13368     */
13369
13370
13371    Cea708Stream.prototype.multiByteCharacter = function (i, service) {
13372      var packetData = this.current708Packet.data;
13373      var firstByte = packetData[i + 1];
13374      var secondByte = packetData[i + 2];
13375
13376      if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) {
13377        i = this.handleText(++i, service, {
13378          isMultiByte: true
13379        });
13380      }
13381
13382      return i;
13383    };
13384    /**
13385     * Parse and execute the CW# command.
13386     *
13387     * Set the current window.
13388     *
13389     * @param  {Integer} i        Current index in the 708 packet
13390     * @param  {Service} service  The service object to be affected
13391     * @return {Integer}          New index after parsing
13392     */
13393
13394
13395    Cea708Stream.prototype.setCurrentWindow = function (i, service) {
13396      var packetData = this.current708Packet.data;
13397      var b = packetData[i];
13398      var windowNum = b & 0x07;
13399      service.setCurrentWindow(windowNum);
13400      return i;
13401    };
13402    /**
13403     * Parse and execute the DF# command.
13404     *
13405     * Define a window and set it as the current window.
13406     *
13407     * @param  {Integer} i        Current index in the 708 packet
13408     * @param  {Service} service  The service object to be affected
13409     * @return {Integer}          New index after parsing
13410     */
13411
13412
13413    Cea708Stream.prototype.defineWindow = function (i, service) {
13414      var packetData = this.current708Packet.data;
13415      var b = packetData[i];
13416      var windowNum = b & 0x07;
13417      service.setCurrentWindow(windowNum);
13418      var win = service.currentWindow;
13419      b = packetData[++i];
13420      win.visible = (b & 0x20) >> 5; // v
13421
13422      win.rowLock = (b & 0x10) >> 4; // rl
13423
13424      win.columnLock = (b & 0x08) >> 3; // cl
13425
13426      win.priority = b & 0x07; // p
13427
13428      b = packetData[++i];
13429      win.relativePositioning = (b & 0x80) >> 7; // rp
13430
13431      win.anchorVertical = b & 0x7f; // av
13432
13433      b = packetData[++i];
13434      win.anchorHorizontal = b; // ah
13435
13436      b = packetData[++i];
13437      win.anchorPoint = (b & 0xf0) >> 4; // ap
13438
13439      win.rowCount = b & 0x0f; // rc
13440
13441      b = packetData[++i];
13442      win.columnCount = b & 0x3f; // cc
13443
13444      b = packetData[++i];
13445      win.windowStyle = (b & 0x38) >> 3; // ws
13446
13447      win.penStyle = b & 0x07; // ps
13448      // The spec says there are (rowCount+1) "virtual rows"
13449
13450      win.virtualRowCount = win.rowCount + 1;
13451      return i;
13452    };
13453    /**
13454     * Parse and execute the SWA command.
13455     *
13456     * Set attributes of the current window.
13457     *
13458     * @param  {Integer} i        Current index in the 708 packet
13459     * @param  {Service} service  The service object to be affected
13460     * @return {Integer}          New index after parsing
13461     */
13462
13463
13464    Cea708Stream.prototype.setWindowAttributes = function (i, service) {
13465      var packetData = this.current708Packet.data;
13466      var b = packetData[i];
13467      var winAttr = service.currentWindow.winAttr;
13468      b = packetData[++i];
13469      winAttr.fillOpacity = (b & 0xc0) >> 6; // fo
13470
13471      winAttr.fillRed = (b & 0x30) >> 4; // fr
13472
13473      winAttr.fillGreen = (b & 0x0c) >> 2; // fg
13474
13475      winAttr.fillBlue = b & 0x03; // fb
13476
13477      b = packetData[++i];
13478      winAttr.borderType = (b & 0xc0) >> 6; // bt
13479
13480      winAttr.borderRed = (b & 0x30) >> 4; // br
13481
13482      winAttr.borderGreen = (b & 0x0c) >> 2; // bg
13483
13484      winAttr.borderBlue = b & 0x03; // bb
13485
13486      b = packetData[++i];
13487      winAttr.borderType += (b & 0x80) >> 5; // bt
13488
13489      winAttr.wordWrap = (b & 0x40) >> 6; // ww
13490
13491      winAttr.printDirection = (b & 0x30) >> 4; // pd
13492
13493      winAttr.scrollDirection = (b & 0x0c) >> 2; // sd
13494
13495      winAttr.justify = b & 0x03; // j
13496
13497      b = packetData[++i];
13498      winAttr.effectSpeed = (b & 0xf0) >> 4; // es
13499
13500      winAttr.effectDirection = (b & 0x0c) >> 2; // ed
13501
13502      winAttr.displayEffect = b & 0x03; // de
13503
13504      return i;
13505    };
13506    /**
13507     * Gather text from all displayed windows and push a caption to output.
13508     *
13509     * @param  {Integer} i        Current index in the 708 packet
13510     * @param  {Service} service  The service object to be affected
13511     */
13512
13513
13514    Cea708Stream.prototype.flushDisplayed = function (pts, service) {
13515      var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily
13516      // display text in the correct order, but sample files so far have not shown any issue.
13517
13518      for (var winId = 0; winId < 8; winId++) {
13519        if (service.windows[winId].visible && !service.windows[winId].isEmpty()) {
13520          displayedText.push(service.windows[winId].getText());
13521        }
13522      }
13523
13524      service.endPts = pts;
13525      service.text = displayedText.join('\n\n');
13526      this.pushCaption(service);
13527      service.startPts = pts;
13528    };
13529    /**
13530     * Push a caption to output if the caption contains text.
13531     *
13532     * @param  {Service} service  The service object to be affected
13533     */
13534
13535
13536    Cea708Stream.prototype.pushCaption = function (service) {
13537      if (service.text !== '') {
13538        this.trigger('data', {
13539          startPts: service.startPts,
13540          endPts: service.endPts,
13541          text: service.text,
13542          stream: 'cc708_' + service.serviceNum
13543        });
13544        service.text = '';
13545        service.startPts = service.endPts;
13546      }
13547    };
13548    /**
13549     * Parse and execute the DSW command.
13550     *
13551     * Set visible property of windows based on the parsed bitmask.
13552     *
13553     * @param  {Integer} i        Current index in the 708 packet
13554     * @param  {Service} service  The service object to be affected
13555     * @return {Integer}          New index after parsing
13556     */
13557
13558
13559    Cea708Stream.prototype.displayWindows = function (i, service) {
13560      var packetData = this.current708Packet.data;
13561      var b = packetData[++i];
13562      var pts = this.getPts(i);
13563      this.flushDisplayed(pts, service);
13564
13565      for (var winId = 0; winId < 8; winId++) {
13566        if (b & 0x01 << winId) {
13567          service.windows[winId].visible = 1;
13568        }
13569      }
13570
13571      return i;
13572    };
13573    /**
13574     * Parse and execute the HDW command.
13575     *
13576     * Set visible property of windows based on the parsed bitmask.
13577     *
13578     * @param  {Integer} i        Current index in the 708 packet
13579     * @param  {Service} service  The service object to be affected
13580     * @return {Integer}          New index after parsing
13581     */
13582
13583
13584    Cea708Stream.prototype.hideWindows = function (i, service) {
13585      var packetData = this.current708Packet.data;
13586      var b = packetData[++i];
13587      var pts = this.getPts(i);
13588      this.flushDisplayed(pts, service);
13589
13590      for (var winId = 0; winId < 8; winId++) {
13591        if (b & 0x01 << winId) {
13592          service.windows[winId].visible = 0;
13593        }
13594      }
13595
13596      return i;
13597    };
13598    /**
13599     * Parse and execute the TGW command.
13600     *
13601     * Set visible property of windows based on the parsed bitmask.
13602     *
13603     * @param  {Integer} i        Current index in the 708 packet
13604     * @param  {Service} service  The service object to be affected
13605     * @return {Integer}          New index after parsing
13606     */
13607
13608
13609    Cea708Stream.prototype.toggleWindows = function (i, service) {
13610      var packetData = this.current708Packet.data;
13611      var b = packetData[++i];
13612      var pts = this.getPts(i);
13613      this.flushDisplayed(pts, service);
13614
13615      for (var winId = 0; winId < 8; winId++) {
13616        if (b & 0x01 << winId) {
13617          service.windows[winId].visible ^= 1;
13618        }
13619      }
13620
13621      return i;
13622    };
13623    /**
13624     * Parse and execute the CLW command.
13625     *
13626     * Clear text of windows based on the parsed bitmask.
13627     *
13628     * @param  {Integer} i        Current index in the 708 packet
13629     * @param  {Service} service  The service object to be affected
13630     * @return {Integer}          New index after parsing
13631     */
13632
13633
13634    Cea708Stream.prototype.clearWindows = function (i, service) {
13635      var packetData = this.current708Packet.data;
13636      var b = packetData[++i];
13637      var pts = this.getPts(i);
13638      this.flushDisplayed(pts, service);
13639
13640      for (var winId = 0; winId < 8; winId++) {
13641        if (b & 0x01 << winId) {
13642          service.windows[winId].clearText();
13643        }
13644      }
13645
13646      return i;
13647    };
13648    /**
13649     * Parse and execute the DLW command.
13650     *
13651     * Re-initialize windows based on the parsed bitmask.
13652     *
13653     * @param  {Integer} i        Current index in the 708 packet
13654     * @param  {Service} service  The service object to be affected
13655     * @return {Integer}          New index after parsing
13656     */
13657
13658
13659    Cea708Stream.prototype.deleteWindows = function (i, service) {
13660      var packetData = this.current708Packet.data;
13661      var b = packetData[++i];
13662      var pts = this.getPts(i);
13663      this.flushDisplayed(pts, service);
13664
13665      for (var winId = 0; winId < 8; winId++) {
13666        if (b & 0x01 << winId) {
13667          service.windows[winId].reset();
13668        }
13669      }
13670
13671      return i;
13672    };
13673    /**
13674     * Parse and execute the SPA command.
13675     *
13676     * Set pen attributes of the current window.
13677     *
13678     * @param  {Integer} i        Current index in the 708 packet
13679     * @param  {Service} service  The service object to be affected
13680     * @return {Integer}          New index after parsing
13681     */
13682
13683
13684    Cea708Stream.prototype.setPenAttributes = function (i, service) {
13685      var packetData = this.current708Packet.data;
13686      var b = packetData[i];
13687      var penAttr = service.currentWindow.penAttr;
13688      b = packetData[++i];
13689      penAttr.textTag = (b & 0xf0) >> 4; // tt
13690
13691      penAttr.offset = (b & 0x0c) >> 2; // o
13692
13693      penAttr.penSize = b & 0x03; // s
13694
13695      b = packetData[++i];
13696      penAttr.italics = (b & 0x80) >> 7; // i
13697
13698      penAttr.underline = (b & 0x40) >> 6; // u
13699
13700      penAttr.edgeType = (b & 0x38) >> 3; // et
13701
13702      penAttr.fontStyle = b & 0x07; // fs
13703
13704      return i;
13705    };
13706    /**
13707     * Parse and execute the SPC command.
13708     *
13709     * Set pen color of the current window.
13710     *
13711     * @param  {Integer} i        Current index in the 708 packet
13712     * @param  {Service} service  The service object to be affected
13713     * @return {Integer}          New index after parsing
13714     */
13715
13716
13717    Cea708Stream.prototype.setPenColor = function (i, service) {
13718      var packetData = this.current708Packet.data;
13719      var b = packetData[i];
13720      var penColor = service.currentWindow.penColor;
13721      b = packetData[++i];
13722      penColor.fgOpacity = (b & 0xc0) >> 6; // fo
13723
13724      penColor.fgRed = (b & 0x30) >> 4; // fr
13725
13726      penColor.fgGreen = (b & 0x0c) >> 2; // fg
13727
13728      penColor.fgBlue = b & 0x03; // fb
13729
13730      b = packetData[++i];
13731      penColor.bgOpacity = (b & 0xc0) >> 6; // bo
13732
13733      penColor.bgRed = (b & 0x30) >> 4; // br
13734
13735      penColor.bgGreen = (b & 0x0c) >> 2; // bg
13736
13737      penColor.bgBlue = b & 0x03; // bb
13738
13739      b = packetData[++i];
13740      penColor.edgeRed = (b & 0x30) >> 4; // er
13741
13742      penColor.edgeGreen = (b & 0x0c) >> 2; // eg
13743
13744      penColor.edgeBlue = b & 0x03; // eb
13745
13746      return i;
13747    };
13748    /**
13749     * Parse and execute the SPL command.
13750     *
13751     * Set pen location of the current window.
13752     *
13753     * @param  {Integer} i        Current index in the 708 packet
13754     * @param  {Service} service  The service object to be affected
13755     * @return {Integer}          New index after parsing
13756     */
13757
13758
13759    Cea708Stream.prototype.setPenLocation = function (i, service) {
13760      var packetData = this.current708Packet.data;
13761      var b = packetData[i];
13762      var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak
13763
13764      service.currentWindow.pendingNewLine = true;
13765      b = packetData[++i];
13766      penLoc.row = b & 0x0f; // r
13767
13768      b = packetData[++i];
13769      penLoc.column = b & 0x3f; // c
13770
13771      return i;
13772    };
13773    /**
13774     * Execute the RST command.
13775     *
13776     * Reset service to a clean slate. Re-initialize.
13777     *
13778     * @param  {Integer} i        Current index in the 708 packet
13779     * @param  {Service} service  The service object to be affected
13780     * @return {Service}          Re-initialized service
13781     */
13782
13783
13784    Cea708Stream.prototype.reset = function (i, service) {
13785      var pts = this.getPts(i);
13786      this.flushDisplayed(pts, service);
13787      return this.initService(service.serviceNum, i);
13788    }; // This hash maps non-ASCII, special, and extended character codes to their
13789    // proper Unicode equivalent. The first keys that are only a single byte
13790    // are the non-standard ASCII characters, which simply map the CEA608 byte
13791    // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608
13792    // character codes, but have their MSB bitmasked with 0x03 so that a lookup
13793    // can be performed regardless of the field and data channel on which the
13794    // character code was received.
13795
13796
13797    var CHARACTER_TRANSLATION = {
13798      0x2a: 0xe1,
13799      // á
13800      0x5c: 0xe9,
13801      // é
13802      0x5e: 0xed,
13803      // í
13804      0x5f: 0xf3,
13805      // ó
13806      0x60: 0xfa,
13807      // ú
13808      0x7b: 0xe7,
13809      // ç
13810      0x7c: 0xf7,
13811      // ÷
13812      0x7d: 0xd1,
13813      // Ñ
13814      0x7e: 0xf1,
13815      // ñ
13816      0x7f: 0x2588,
13817      // █
13818      0x0130: 0xae,
13819      // ®
13820      0x0131: 0xb0,
13821      // °
13822      0x0132: 0xbd,
13823      // ½
13824      0x0133: 0xbf,
13825      // ¿
13826      0x0134: 0x2122,
13827      // ™
13828      0x0135: 0xa2,
13829      // ¢
13830      0x0136: 0xa3,
13831      // £
13832      0x0137: 0x266a,
13833      // ♪
13834      0x0138: 0xe0,
13835      // à
13836      0x0139: 0xa0,
13837      //
13838      0x013a: 0xe8,
13839      // è
13840      0x013b: 0xe2,
13841      // â
13842      0x013c: 0xea,
13843      // ê
13844      0x013d: 0xee,
13845      // î
13846      0x013e: 0xf4,
13847      // ô
13848      0x013f: 0xfb,
13849      // û
13850      0x0220: 0xc1,
13851      // Á
13852      0x0221: 0xc9,
13853      // É
13854      0x0222: 0xd3,
13855      // Ó
13856      0x0223: 0xda,
13857      // Ú
13858      0x0224: 0xdc,
13859      // Ü
13860      0x0225: 0xfc,
13861      // ü
13862      0x0226: 0x2018,
13863      // ‘
13864      0x0227: 0xa1,
13865      // ¡
13866      0x0228: 0x2a,
13867      // *
13868      0x0229: 0x27,
13869      // '
13870      0x022a: 0x2014,
13871      // —
13872      0x022b: 0xa9,
13873      // ©
13874      0x022c: 0x2120,
13875      // ℠
13876      0x022d: 0x2022,
13877      // •
13878      0x022e: 0x201c,
13879      // “
13880      0x022f: 0x201d,
13881      // ”
13882      0x0230: 0xc0,
13883      // À
13884      0x0231: 0xc2,
13885      // Â
13886      0x0232: 0xc7,
13887      // Ç
13888      0x0233: 0xc8,
13889      // È
13890      0x0234: 0xca,
13891      // Ê
13892      0x0235: 0xcb,
13893      // Ë
13894      0x0236: 0xeb,
13895      // ë
13896      0x0237: 0xce,
13897      // Î
13898      0x0238: 0xcf,
13899      // Ï
13900      0x0239: 0xef,
13901      // ï
13902      0x023a: 0xd4,
13903      // Ô
13904      0x023b: 0xd9,
13905      // Ù
13906      0x023c: 0xf9,
13907      // ù
13908      0x023d: 0xdb,
13909      // Û
13910      0x023e: 0xab,
13911      // «
13912      0x023f: 0xbb,
13913      // »
13914      0x0320: 0xc3,
13915      // Ã
13916      0x0321: 0xe3,
13917      // ã
13918      0x0322: 0xcd,
13919      // Í
13920      0x0323: 0xcc,
13921      // Ì
13922      0x0324: 0xec,
13923      // ì
13924      0x0325: 0xd2,
13925      // Ò
13926      0x0326: 0xf2,
13927      // ò
13928      0x0327: 0xd5,
13929      // Õ
13930      0x0328: 0xf5,
13931      // õ
13932      0x0329: 0x7b,
13933      // {
13934      0x032a: 0x7d,
13935      // }
13936      0x032b: 0x5c,
13937      // \
13938      0x032c: 0x5e,
13939      // ^
13940      0x032d: 0x5f,
13941      // _
13942      0x032e: 0x7c,
13943      // |
13944      0x032f: 0x7e,
13945      // ~
13946      0x0330: 0xc4,
13947      // Ä
13948      0x0331: 0xe4,
13949      // ä
13950      0x0332: 0xd6,
13951      // Ö
13952      0x0333: 0xf6,
13953      // ö
13954      0x0334: 0xdf,
13955      // ß
13956      0x0335: 0xa5,
13957      // ¥
13958      0x0336: 0xa4,
13959      // ¤
13960      0x0337: 0x2502,
13961      // │
13962      0x0338: 0xc5,
13963      // Å
13964      0x0339: 0xe5,
13965      // å
13966      0x033a: 0xd8,
13967      // Ø
13968      0x033b: 0xf8,
13969      // ø
13970      0x033c: 0x250c,
13971      // ┌
13972      0x033d: 0x2510,
13973      // ┐
13974      0x033e: 0x2514,
13975      // └
13976      0x033f: 0x2518 // ┘
13977
13978    };
13979
13980    var getCharFromCode = function (code) {
13981      if (code === null) {
13982        return '';
13983      }
13984
13985      code = CHARACTER_TRANSLATION[code] || code;
13986      return String.fromCharCode(code);
13987    }; // the index of the last row in a CEA-608 display buffer
13988
13989
13990    var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of
13991    // getting it through bit logic.
13992
13993    var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; // CEA-608 captions are rendered onto a 34x15 matrix of character
13994    // cells. The "bottom" row is the last element in the outer array.
13995    // We keep track of positioning information as we go by storing the
13996    // number of indentations and the tab offset in this buffer.
13997
13998    var createDisplayBuffer = function () {
13999      var result = [],
14000          i = BOTTOM_ROW + 1;
14001
14002      while (i--) {
14003        result.push({
14004          text: '',
14005          indent: 0,
14006          offset: 0
14007        });
14008      }
14009
14010      return result;
14011    };
14012
14013    var Cea608Stream = function (field, dataChannel) {
14014      Cea608Stream.prototype.init.call(this);
14015      this.field_ = field || 0;
14016      this.dataChannel_ = dataChannel || 0;
14017      this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1);
14018      this.setConstants();
14019      this.reset();
14020
14021      this.push = function (packet) {
14022        var data, swap, char0, char1, text; // remove the parity bits
14023
14024        data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice
14025
14026        if (data === this.lastControlCode_) {
14027          this.lastControlCode_ = null;
14028          return;
14029        } // Store control codes
14030
14031
14032        if ((data & 0xf000) === 0x1000) {
14033          this.lastControlCode_ = data;
14034        } else if (data !== this.PADDING_) {
14035          this.lastControlCode_ = null;
14036        }
14037
14038        char0 = data >>> 8;
14039        char1 = data & 0xff;
14040
14041        if (data === this.PADDING_) {
14042          return;
14043        } else if (data === this.RESUME_CAPTION_LOADING_) {
14044          this.mode_ = 'popOn';
14045        } else if (data === this.END_OF_CAPTION_) {
14046          // If an EOC is received while in paint-on mode, the displayed caption
14047          // text should be swapped to non-displayed memory as if it was a pop-on
14048          // caption. Because of that, we should explicitly switch back to pop-on
14049          // mode
14050          this.mode_ = 'popOn';
14051          this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now
14052
14053          this.flushDisplayed(packet.pts); // flip memory
14054
14055          swap = this.displayed_;
14056          this.displayed_ = this.nonDisplayed_;
14057          this.nonDisplayed_ = swap; // start measuring the time to display the caption
14058
14059          this.startPts_ = packet.pts;
14060        } else if (data === this.ROLL_UP_2_ROWS_) {
14061          this.rollUpRows_ = 2;
14062          this.setRollUp(packet.pts);
14063        } else if (data === this.ROLL_UP_3_ROWS_) {
14064          this.rollUpRows_ = 3;
14065          this.setRollUp(packet.pts);
14066        } else if (data === this.ROLL_UP_4_ROWS_) {
14067          this.rollUpRows_ = 4;
14068          this.setRollUp(packet.pts);
14069        } else if (data === this.CARRIAGE_RETURN_) {
14070          this.clearFormatting(packet.pts);
14071          this.flushDisplayed(packet.pts);
14072          this.shiftRowsUp_();
14073          this.startPts_ = packet.pts;
14074        } else if (data === this.BACKSPACE_) {
14075          if (this.mode_ === 'popOn') {
14076            this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1);
14077          } else {
14078            this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1);
14079          }
14080        } else if (data === this.ERASE_DISPLAYED_MEMORY_) {
14081          this.flushDisplayed(packet.pts);
14082          this.displayed_ = createDisplayBuffer();
14083        } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) {
14084          this.nonDisplayed_ = createDisplayBuffer();
14085        } else if (data === this.RESUME_DIRECT_CAPTIONING_) {
14086          if (this.mode_ !== 'paintOn') {
14087            // NOTE: This should be removed when proper caption positioning is
14088            // implemented
14089            this.flushDisplayed(packet.pts);
14090            this.displayed_ = createDisplayBuffer();
14091          }
14092
14093          this.mode_ = 'paintOn';
14094          this.startPts_ = packet.pts; // Append special characters to caption text
14095        } else if (this.isSpecialCharacter(char0, char1)) {
14096          // Bitmask char0 so that we can apply character transformations
14097          // regardless of field and data channel.
14098          // Then byte-shift to the left and OR with char1 so we can pass the
14099          // entire character code to `getCharFromCode`.
14100          char0 = (char0 & 0x03) << 8;
14101          text = getCharFromCode(char0 | char1);
14102          this[this.mode_](packet.pts, text);
14103          this.column_++; // Append extended characters to caption text
14104        } else if (this.isExtCharacter(char0, char1)) {
14105          // Extended characters always follow their "non-extended" equivalents.
14106          // IE if a "è" is desired, you'll always receive "eè"; non-compliant
14107          // decoders are supposed to drop the "è", while compliant decoders
14108          // backspace the "e" and insert "è".
14109          // Delete the previous character
14110          if (this.mode_ === 'popOn') {
14111            this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1);
14112          } else {
14113            this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1);
14114          } // Bitmask char0 so that we can apply character transformations
14115          // regardless of field and data channel.
14116          // Then byte-shift to the left and OR with char1 so we can pass the
14117          // entire character code to `getCharFromCode`.
14118
14119
14120          char0 = (char0 & 0x03) << 8;
14121          text = getCharFromCode(char0 | char1);
14122          this[this.mode_](packet.pts, text);
14123          this.column_++; // Process mid-row codes
14124        } else if (this.isMidRowCode(char0, char1)) {
14125          // Attributes are not additive, so clear all formatting
14126          this.clearFormatting(packet.pts); // According to the standard, mid-row codes
14127          // should be replaced with spaces, so add one now
14128
14129          this[this.mode_](packet.pts, ' ');
14130          this.column_++;
14131
14132          if ((char1 & 0xe) === 0xe) {
14133            this.addFormatting(packet.pts, ['i']);
14134          }
14135
14136          if ((char1 & 0x1) === 0x1) {
14137            this.addFormatting(packet.pts, ['u']);
14138          } // Detect offset control codes and adjust cursor
14139
14140        } else if (this.isOffsetControlCode(char0, char1)) {
14141          // Cursor position is set by indent PAC (see below) in 4-column
14142          // increments, with an additional offset code of 1-3 to reach any
14143          // of the 32 columns specified by CEA-608. So all we need to do
14144          // here is increment the column cursor by the given offset.
14145          const offset = char1 & 0x03; // For an offest value 1-3, set the offset for that caption
14146          // in the non-displayed array.
14147
14148          this.nonDisplayed_[this.row_].offset = offset;
14149          this.column_ += offset; // Detect PACs (Preamble Address Codes)
14150        } else if (this.isPAC(char0, char1)) {
14151          // There's no logic for PAC -> row mapping, so we have to just
14152          // find the row code in an array and use its index :(
14153          var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode
14154
14155          if (this.mode_ === 'rollUp') {
14156            // This implies that the base row is incorrectly set.
14157            // As per the recommendation in CEA-608(Base Row Implementation), defer to the number
14158            // of roll-up rows set.
14159            if (row - this.rollUpRows_ + 1 < 0) {
14160              row = this.rollUpRows_ - 1;
14161            }
14162
14163            this.setRollUp(packet.pts, row);
14164          } // Ensure the row is between 0 and 14, otherwise use the most
14165          // recent or default row.
14166
14167
14168          if (row !== this.row_ && row >= 0 && row <= 14) {
14169            // formatting is only persistent for current row
14170            this.clearFormatting(packet.pts);
14171            this.row_ = row;
14172          } // All PACs can apply underline, so detect and apply
14173          // (All odd-numbered second bytes set underline)
14174
14175
14176          if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) {
14177            this.addFormatting(packet.pts, ['u']);
14178          }
14179
14180          if ((data & 0x10) === 0x10) {
14181            // We've got an indent level code. Each successive even number
14182            // increments the column cursor by 4, so we can get the desired
14183            // column position by bit-shifting to the right (to get n/2)
14184            // and multiplying by 4.
14185            const indentations = (data & 0xe) >> 1;
14186            this.column_ = indentations * 4; // add to the number of indentations for positioning
14187
14188            this.nonDisplayed_[this.row_].indent += indentations;
14189          }
14190
14191          if (this.isColorPAC(char1)) {
14192            // it's a color code, though we only support white, which
14193            // can be either normal or italicized. white italics can be
14194            // either 0x4e or 0x6e depending on the row, so we just
14195            // bitwise-and with 0xe to see if italics should be turned on
14196            if ((char1 & 0xe) === 0xe) {
14197              this.addFormatting(packet.pts, ['i']);
14198            }
14199          } // We have a normal character in char0, and possibly one in char1
14200
14201        } else if (this.isNormalChar(char0)) {
14202          if (char1 === 0x00) {
14203            char1 = null;
14204          }
14205
14206          text = getCharFromCode(char0);
14207          text += getCharFromCode(char1);
14208          this[this.mode_](packet.pts, text);
14209          this.column_ += text.length;
14210        } // finish data processing
14211
14212      };
14213    };
14214
14215    Cea608Stream.prototype = new Stream$7(); // Trigger a cue point that captures the current state of the
14216    // display buffer
14217
14218    Cea608Stream.prototype.flushDisplayed = function (pts) {
14219      const logWarning = index => {
14220        this.trigger('log', {
14221          level: 'warn',
14222          message: 'Skipping a malformed 608 caption at index ' + index + '.'
14223        });
14224      };
14225
14226      const content = [];
14227      this.displayed_.forEach((row, i) => {
14228        if (row && row.text && row.text.length) {
14229          try {
14230            // remove spaces from the start and end of the string
14231            row.text = row.text.trim();
14232          } catch (e) {
14233            // Ordinarily, this shouldn't happen. However, caption
14234            // parsing errors should not throw exceptions and
14235            // break playback.
14236            logWarning(i);
14237          } // See the below link for more details on the following fields:
14238          // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608
14239
14240
14241          if (row.text.length) {
14242            content.push({
14243              // The text to be displayed in the caption from this specific row, with whitespace removed.
14244              text: row.text,
14245              // Value between 1 and 15 representing the PAC row used to calculate line height.
14246              line: i + 1,
14247              // A number representing the indent position by percentage (CEA-608 PAC indent code).
14248              // The value will be a number between 10 and 80. Offset is used to add an aditional
14249              // value to the position if necessary.
14250              position: 10 + Math.min(70, row.indent * 10) + row.offset * 2.5
14251            });
14252          }
14253        } else if (row === undefined || row === null) {
14254          logWarning(i);
14255        }
14256      });
14257
14258      if (content.length) {
14259        this.trigger('data', {
14260          startPts: this.startPts_,
14261          endPts: pts,
14262          content,
14263          stream: this.name_
14264        });
14265      }
14266    };
14267    /**
14268     * Zero out the data, used for startup and on seek
14269     */
14270
14271
14272    Cea608Stream.prototype.reset = function () {
14273      this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will
14274      // actually display captions. If a caption is shifted to a row
14275      // with a lower index than this, it is cleared from the display
14276      // buffer
14277
14278      this.topRow_ = 0;
14279      this.startPts_ = 0;
14280      this.displayed_ = createDisplayBuffer();
14281      this.nonDisplayed_ = createDisplayBuffer();
14282      this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing
14283
14284      this.column_ = 0;
14285      this.row_ = BOTTOM_ROW;
14286      this.rollUpRows_ = 2; // This variable holds currently-applied formatting
14287
14288      this.formatting_ = [];
14289    };
14290    /**
14291     * Sets up control code and related constants for this instance
14292     */
14293
14294
14295    Cea608Stream.prototype.setConstants = function () {
14296      // The following attributes have these uses:
14297      // ext_ :    char0 for mid-row codes, and the base for extended
14298      //           chars (ext_+0, ext_+1, and ext_+2 are char0s for
14299      //           extended codes)
14300      // control_: char0 for control codes, except byte-shifted to the
14301      //           left so that we can do this.control_ | CONTROL_CODE
14302      // offset_:  char0 for tab offset codes
14303      //
14304      // It's also worth noting that control codes, and _only_ control codes,
14305      // differ between field 1 and field2. Field 2 control codes are always
14306      // their field 1 value plus 1. That's why there's the "| field" on the
14307      // control value.
14308      if (this.dataChannel_ === 0) {
14309        this.BASE_ = 0x10;
14310        this.EXT_ = 0x11;
14311        this.CONTROL_ = (0x14 | this.field_) << 8;
14312        this.OFFSET_ = 0x17;
14313      } else if (this.dataChannel_ === 1) {
14314        this.BASE_ = 0x18;
14315        this.EXT_ = 0x19;
14316        this.CONTROL_ = (0x1c | this.field_) << 8;
14317        this.OFFSET_ = 0x1f;
14318      } // Constants for the LSByte command codes recognized by Cea608Stream. This
14319      // list is not exhaustive. For a more comprehensive listing and semantics see
14320      // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf
14321      // Padding
14322
14323
14324      this.PADDING_ = 0x0000; // Pop-on Mode
14325
14326      this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20;
14327      this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode
14328
14329      this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25;
14330      this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26;
14331      this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27;
14332      this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode
14333
14334      this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure
14335
14336      this.BACKSPACE_ = this.CONTROL_ | 0x21;
14337      this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c;
14338      this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e;
14339    };
14340    /**
14341     * Detects if the 2-byte packet data is a special character
14342     *
14343     * Special characters have a second byte in the range 0x30 to 0x3f,
14344     * with the first byte being 0x11 (for data channel 1) or 0x19 (for
14345     * data channel 2).
14346     *
14347     * @param  {Integer} char0 The first byte
14348     * @param  {Integer} char1 The second byte
14349     * @return {Boolean}       Whether the 2 bytes are an special character
14350     */
14351
14352
14353    Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) {
14354      return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f;
14355    };
14356    /**
14357     * Detects if the 2-byte packet data is an extended character
14358     *
14359     * Extended characters have a second byte in the range 0x20 to 0x3f,
14360     * with the first byte being 0x12 or 0x13 (for data channel 1) or
14361     * 0x1a or 0x1b (for data channel 2).
14362     *
14363     * @param  {Integer} char0 The first byte
14364     * @param  {Integer} char1 The second byte
14365     * @return {Boolean}       Whether the 2 bytes are an extended character
14366     */
14367
14368
14369    Cea608Stream.prototype.isExtCharacter = function (char0, char1) {
14370      return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f;
14371    };
14372    /**
14373     * Detects if the 2-byte packet is a mid-row code
14374     *
14375     * Mid-row codes have a second byte in the range 0x20 to 0x2f, with
14376     * the first byte being 0x11 (for data channel 1) or 0x19 (for data
14377     * channel 2).
14378     *
14379     * @param  {Integer} char0 The first byte
14380     * @param  {Integer} char1 The second byte
14381     * @return {Boolean}       Whether the 2 bytes are a mid-row code
14382     */
14383
14384
14385    Cea608Stream.prototype.isMidRowCode = function (char0, char1) {
14386      return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f;
14387    };
14388    /**
14389     * Detects if the 2-byte packet is an offset control code
14390     *
14391     * Offset control codes have a second byte in the range 0x21 to 0x23,
14392     * with the first byte being 0x17 (for data channel 1) or 0x1f (for
14393     * data channel 2).
14394     *
14395     * @param  {Integer} char0 The first byte
14396     * @param  {Integer} char1 The second byte
14397     * @return {Boolean}       Whether the 2 bytes are an offset control code
14398     */
14399
14400
14401    Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) {
14402      return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23;
14403    };
14404    /**
14405     * Detects if the 2-byte packet is a Preamble Address Code
14406     *
14407     * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1)
14408     * or 0x18 to 0x1f (for data channel 2), with the second byte in the
14409     * range 0x40 to 0x7f.
14410     *
14411     * @param  {Integer} char0 The first byte
14412     * @param  {Integer} char1 The second byte
14413     * @return {Boolean}       Whether the 2 bytes are a PAC
14414     */
14415
14416
14417    Cea608Stream.prototype.isPAC = function (char0, char1) {
14418      return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f;
14419    };
14420    /**
14421     * Detects if a packet's second byte is in the range of a PAC color code
14422     *
14423     * PAC color codes have the second byte be in the range 0x40 to 0x4f, or
14424     * 0x60 to 0x6f.
14425     *
14426     * @param  {Integer} char1 The second byte
14427     * @return {Boolean}       Whether the byte is a color PAC
14428     */
14429
14430
14431    Cea608Stream.prototype.isColorPAC = function (char1) {
14432      return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f;
14433    };
14434    /**
14435     * Detects if a single byte is in the range of a normal character
14436     *
14437     * Normal text bytes are in the range 0x20 to 0x7f.
14438     *
14439     * @param  {Integer} char  The byte
14440     * @return {Boolean}       Whether the byte is a normal character
14441     */
14442
14443
14444    Cea608Stream.prototype.isNormalChar = function (char) {
14445      return char >= 0x20 && char <= 0x7f;
14446    };
14447    /**
14448     * Configures roll-up
14449     *
14450     * @param  {Integer} pts         Current PTS
14451     * @param  {Integer} newBaseRow  Used by PACs to slide the current window to
14452     *                               a new position
14453     */
14454
14455
14456    Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) {
14457      // Reset the base row to the bottom row when switching modes
14458      if (this.mode_ !== 'rollUp') {
14459        this.row_ = BOTTOM_ROW;
14460        this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up
14461
14462        this.flushDisplayed(pts);
14463        this.nonDisplayed_ = createDisplayBuffer();
14464        this.displayed_ = createDisplayBuffer();
14465      }
14466
14467      if (newBaseRow !== undefined && newBaseRow !== this.row_) {
14468        // move currently displayed captions (up or down) to the new base row
14469        for (var i = 0; i < this.rollUpRows_; i++) {
14470          this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i];
14471          this.displayed_[this.row_ - i] = {
14472            text: '',
14473            indent: 0,
14474            offset: 0
14475          };
14476        }
14477      }
14478
14479      if (newBaseRow === undefined) {
14480        newBaseRow = this.row_;
14481      }
14482
14483      this.topRow_ = newBaseRow - this.rollUpRows_ + 1;
14484    }; // Adds the opening HTML tag for the passed character to the caption text,
14485    // and keeps track of it for later closing
14486
14487
14488    Cea608Stream.prototype.addFormatting = function (pts, format) {
14489      this.formatting_ = this.formatting_.concat(format);
14490      var text = format.reduce(function (text, format) {
14491        return text + '<' + format + '>';
14492      }, '');
14493      this[this.mode_](pts, text);
14494    }; // Adds HTML closing tags for current formatting to caption text and
14495    // clears remembered formatting
14496
14497
14498    Cea608Stream.prototype.clearFormatting = function (pts) {
14499      if (!this.formatting_.length) {
14500        return;
14501      }
14502
14503      var text = this.formatting_.reverse().reduce(function (text, format) {
14504        return text + '</' + format + '>';
14505      }, '');
14506      this.formatting_ = [];
14507      this[this.mode_](pts, text);
14508    }; // Mode Implementations
14509
14510
14511    Cea608Stream.prototype.popOn = function (pts, text) {
14512      var baseRow = this.nonDisplayed_[this.row_].text; // buffer characters
14513
14514      baseRow += text;
14515      this.nonDisplayed_[this.row_].text = baseRow;
14516    };
14517
14518    Cea608Stream.prototype.rollUp = function (pts, text) {
14519      var baseRow = this.displayed_[this.row_].text;
14520      baseRow += text;
14521      this.displayed_[this.row_].text = baseRow;
14522    };
14523
14524    Cea608Stream.prototype.shiftRowsUp_ = function () {
14525      var i; // clear out inactive rows
14526
14527      for (i = 0; i < this.topRow_; i++) {
14528        this.displayed_[i] = {
14529          text: '',
14530          indent: 0,
14531          offset: 0
14532        };
14533      }
14534
14535      for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) {
14536        this.displayed_[i] = {
14537          text: '',
14538          indent: 0,
14539          offset: 0
14540        };
14541      } // shift displayed rows up
14542
14543
14544      for (i = this.topRow_; i < this.row_; i++) {
14545        this.displayed_[i] = this.displayed_[i + 1];
14546      } // clear out the bottom row
14547
14548
14549      this.displayed_[this.row_] = {
14550        text: '',
14551        indent: 0,
14552        offset: 0
14553      };
14554    };
14555
14556    Cea608Stream.prototype.paintOn = function (pts, text) {
14557      var baseRow = this.displayed_[this.row_].text;
14558      baseRow += text;
14559      this.displayed_[this.row_].text = baseRow;
14560    }; // exports
14561
14562
14563    var captionStream = {
14564      CaptionStream: CaptionStream$2,
14565      Cea608Stream: Cea608Stream,
14566      Cea708Stream: Cea708Stream
14567    };
14568    /**
14569     * mux.js
14570     *
14571     * Copyright (c) Brightcove
14572     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
14573     */
14574
14575    var streamTypes = {
14576      H264_STREAM_TYPE: 0x1B,
14577      ADTS_STREAM_TYPE: 0x0F,
14578      METADATA_STREAM_TYPE: 0x15
14579    };
14580    /**
14581     * mux.js
14582     *
14583     * Copyright (c) Brightcove
14584     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
14585     *
14586     * Accepts program elementary stream (PES) data events and corrects
14587     * decode and presentation time stamps to account for a rollover
14588     * of the 33 bit value.
14589     */
14590
14591    var Stream$6 = stream;
14592    var MAX_TS = 8589934592;
14593    var RO_THRESH = 4294967296;
14594    var TYPE_SHARED = 'shared';
14595
14596    var handleRollover$1 = function (value, reference) {
14597      var direction = 1;
14598
14599      if (value > reference) {
14600        // If the current timestamp value is greater than our reference timestamp and we detect a
14601        // timestamp rollover, this means the roll over is happening in the opposite direction.
14602        // Example scenario: Enter a long stream/video just after a rollover occurred. The reference
14603        // point will be set to a small number, e.g. 1. The user then seeks backwards over the
14604        // rollover point. In loading this segment, the timestamp values will be very large,
14605        // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust
14606        // the time stamp to be `value - 2^33`.
14607        direction = -1;
14608      } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will
14609      // cause an incorrect adjustment.
14610
14611
14612      while (Math.abs(reference - value) > RO_THRESH) {
14613        value += direction * MAX_TS;
14614      }
14615
14616      return value;
14617    };
14618
14619    var TimestampRolloverStream$1 = function (type) {
14620      var lastDTS, referenceDTS;
14621      TimestampRolloverStream$1.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed
14622      // video and audio. We could use `undefined` here, but having a string
14623      // makes debugging a little clearer.
14624
14625      this.type_ = type || TYPE_SHARED;
14626
14627      this.push = function (data) {
14628        /**
14629         * Rollover stream expects data from elementary stream.
14630         * Elementary stream can push forward 2 types of data
14631         * - Parsed Video/Audio/Timed-metadata PES (packetized elementary stream) packets
14632         * - Tracks metadata from PMT (Program Map Table)
14633         * Rollover stream expects pts/dts info to be available, since it stores lastDTS
14634         * We should ignore non-PES packets since they may override lastDTS to undefined.
14635         * lastDTS is important to signal the next segments
14636         * about rollover from the previous segments.
14637         */
14638        if (data.type === 'metadata') {
14639          this.trigger('data', data);
14640          return;
14641        } // Any "shared" rollover streams will accept _all_ data. Otherwise,
14642        // streams will only accept data that matches their type.
14643
14644
14645        if (this.type_ !== TYPE_SHARED && data.type !== this.type_) {
14646          return;
14647        }
14648
14649        if (referenceDTS === undefined) {
14650          referenceDTS = data.dts;
14651        }
14652
14653        data.dts = handleRollover$1(data.dts, referenceDTS);
14654        data.pts = handleRollover$1(data.pts, referenceDTS);
14655        lastDTS = data.dts;
14656        this.trigger('data', data);
14657      };
14658
14659      this.flush = function () {
14660        referenceDTS = lastDTS;
14661        this.trigger('done');
14662      };
14663
14664      this.endTimeline = function () {
14665        this.flush();
14666        this.trigger('endedtimeline');
14667      };
14668
14669      this.discontinuity = function () {
14670        referenceDTS = void 0;
14671        lastDTS = void 0;
14672      };
14673
14674      this.reset = function () {
14675        this.discontinuity();
14676        this.trigger('reset');
14677      };
14678    };
14679
14680    TimestampRolloverStream$1.prototype = new Stream$6();
14681    var timestampRolloverStream = {
14682      TimestampRolloverStream: TimestampRolloverStream$1,
14683      handleRollover: handleRollover$1
14684    }; // Once IE11 support is dropped, this function should be removed.
14685
14686    var typedArrayIndexOf$1 = (typedArray, element, fromIndex) => {
14687      if (!typedArray) {
14688        return -1;
14689      }
14690
14691      var currentIndex = fromIndex;
14692
14693      for (; currentIndex < typedArray.length; currentIndex++) {
14694        if (typedArray[currentIndex] === element) {
14695          return currentIndex;
14696        }
14697      }
14698
14699      return -1;
14700    };
14701
14702    var typedArray = {
14703      typedArrayIndexOf: typedArrayIndexOf$1
14704    };
14705    /**
14706     * mux.js
14707     *
14708     * Copyright (c) Brightcove
14709     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
14710     *
14711     * Tools for parsing ID3 frame data
14712     * @see http://id3.org/id3v2.3.0
14713     */
14714
14715    var typedArrayIndexOf = typedArray.typedArrayIndexOf,
14716        // Frames that allow different types of text encoding contain a text
14717    // encoding description byte [ID3v2.4.0 section 4.]
14718    textEncodingDescriptionByte = {
14719      Iso88591: 0x00,
14720      // ISO-8859-1, terminated with \0.
14721      Utf16: 0x01,
14722      // UTF-16 encoded Unicode BOM, terminated with \0\0
14723      Utf16be: 0x02,
14724      // UTF-16BE encoded Unicode, without BOM, terminated with \0\0
14725      Utf8: 0x03 // UTF-8 encoded Unicode, terminated with \0
14726
14727    },
14728        // return a percent-encoded representation of the specified byte range
14729    // @see http://en.wikipedia.org/wiki/Percent-encoding
vendor: 5,570 bytes, lines 14729-14879
14729
14730    percentEncode$1 = function (bytes, start, end) {
14731      var i,
14732          result = '';
14733
14734      for (i = start; i < end; i++) {
14735        result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
14736      }
14737
14738      return result;
14739    },
14740        // return the string representation of the specified byte range,
14741    // interpreted as UTf-8.
14742    parseUtf8 = function (bytes, start, end) {
14743      return decodeURIComponent(percentEncode$1(bytes, start, end));
14744    },
14745        // return the string representation of the specified byte range,
14746    // interpreted as ISO-8859-1.
14747    parseIso88591$1 = function (bytes, start, end) {
14748      return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line
14749    },
14750        parseSyncSafeInteger$1 = function (data) {
14751      return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
14752    },
14753        frameParsers = {
14754      'APIC': function (frame) {
14755        var i = 1,
14756            mimeTypeEndIndex,
14757            descriptionEndIndex,
14758            LINK_MIME_TYPE = '-->';
14759
14760        if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
14761          // ignore frames with unrecognized character encodings
14762          return;
14763        } // parsing fields [ID3v2.4.0 section 4.14.]
14764
14765
14766        mimeTypeEndIndex = typedArrayIndexOf(frame.data, 0, i);
14767
14768        if (mimeTypeEndIndex < 0) {
14769          // malformed frame
14770          return;
14771        } // parsing Mime type field (terminated with \0)
14772
14773
14774        frame.mimeType = parseIso88591$1(frame.data, i, mimeTypeEndIndex);
14775        i = mimeTypeEndIndex + 1; // parsing 1-byte Picture Type field
14776
14777        frame.pictureType = frame.data[i];
14778        i++;
14779        descriptionEndIndex = typedArrayIndexOf(frame.data, 0, i);
14780
14781        if (descriptionEndIndex < 0) {
14782          // malformed frame
14783          return;
14784        } // parsing Description field (terminated with \0)
14785
14786
14787        frame.description = parseUtf8(frame.data, i, descriptionEndIndex);
14788        i = descriptionEndIndex + 1;
14789
14790        if (frame.mimeType === LINK_MIME_TYPE) {
14791          // parsing Picture Data field as URL (always represented as ISO-8859-1 [ID3v2.4.0 section 4.])
14792          frame.url = parseIso88591$1(frame.data, i, frame.data.length);
14793        } else {
14794          // parsing Picture Data field as binary data
14795          frame.pictureData = frame.data.subarray(i, frame.data.length);
14796        }
14797      },
14798      'T*': function (frame) {
14799        if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
14800          // ignore frames with unrecognized character encodings
14801          return;
14802        } // parse text field, do not include null terminator in the frame value
14803        // frames that allow different types of encoding contain terminated text [ID3v2.4.0 section 4.]
14804
14805
14806        frame.value = parseUtf8(frame.data, 1, frame.data.length).replace(/\0*$/, ''); // text information frames supports multiple strings, stored as a terminator separated list [ID3v2.4.0 section 4.2.]
14807
14808        frame.values = frame.value.split('\0');
14809      },
14810      'TXXX': function (frame) {
14811        var descriptionEndIndex;
14812
14813        if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
14814          // ignore frames with unrecognized character encodings
14815          return;
14816        }
14817
14818        descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1);
14819
14820        if (descriptionEndIndex === -1) {
14821          return;
14822        } // parse the text fields
14823
14824
14825        frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // do not include the null terminator in the tag value
14826        // frames that allow different types of encoding contain terminated text
14827        // [ID3v2.4.0 section 4.]
14828
14829        frame.value = parseUtf8(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0*$/, '');
14830        frame.data = frame.value;
14831      },
14832      'W*': function (frame) {
14833        // parse URL field; URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.]
14834        // if the value is followed by a string termination all the following information should be ignored [ID3v2.4.0 section 4.3]
14835        frame.url = parseIso88591$1(frame.data, 0, frame.data.length).replace(/\0.*$/, '');
14836      },
14837      'WXXX': function (frame) {
14838        var descriptionEndIndex;
14839
14840        if (frame.data[0] !== textEncodingDescriptionByte.Utf8) {
14841          // ignore frames with unrecognized character encodings
14842          return;
14843        }
14844
14845        descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1);
14846
14847        if (descriptionEndIndex === -1) {
14848          return;
14849        } // parse the description and URL fields
14850
14851
14852        frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.]
14853        // if the value is followed by a string termination all the following information
14854        // should be ignored [ID3v2.4.0 section 4.3]
14855
14856        frame.url = parseIso88591$1(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0.*$/, '');
14857      },
14858      'PRIV': function (frame) {
14859        var i;
14860
14861        for (i = 0; i < frame.data.length; i++) {
14862          if (frame.data[i] === 0) {
14863            // parse the description and URL fields
14864            frame.owner = parseIso88591$1(frame.data, 0, i);
14865            break;
14866          }
14867        }
14868
14869        frame.privateData = frame.data.subarray(i + 1);
14870        frame.data = frame.privateData;
14871      }
14872    };
14873
14874    var parseId3Frames$1 = function (data) {
14875      var frameSize,
14876          frameHeader,
14877          frameStart = 10,
14878          tagSize = 0,
14879          frames = []; // If we don't have enough data for a header, 10 bytes,
vendor: 141,022 bytes, lines 14879-18979
14879
14880      // or 'ID3' in the first 3 bytes this is not a valid ID3 tag.
14881
14882      if (data.length < 10 || data[0] !== 'I'.charCodeAt(0) || data[1] !== 'D'.charCodeAt(0) || data[2] !== '3'.charCodeAt(0)) {
14883        return;
14884      } // the frame size is transmitted as a 28-bit integer in the
14885      // last four bytes of the ID3 header.
14886      // The most significant bit of each byte is dropped and the
14887      // results concatenated to recover the actual value.
14888
14889
14890      tagSize = parseSyncSafeInteger$1(data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more
14891      // convenient for our comparisons to include it
14892
14893      tagSize += 10; // check bit 6 of byte 5 for the extended header flag.
14894
14895      var hasExtendedHeader = data[5] & 0x40;
14896
14897      if (hasExtendedHeader) {
14898        // advance the frame start past the extended header
14899        frameStart += 4; // header size field
14900
14901        frameStart += parseSyncSafeInteger$1(data.subarray(10, 14));
14902        tagSize -= parseSyncSafeInteger$1(data.subarray(16, 20)); // clip any padding off the end
14903      } // parse one or more ID3 frames
14904      // http://id3.org/id3v2.3.0#ID3v2_frame_overview
14905
14906
14907      do {
14908        // determine the number of bytes in this frame
14909        frameSize = parseSyncSafeInteger$1(data.subarray(frameStart + 4, frameStart + 8));
14910
14911        if (frameSize < 1) {
14912          break;
14913        }
14914
14915        frameHeader = String.fromCharCode(data[frameStart], data[frameStart + 1], data[frameStart + 2], data[frameStart + 3]);
14916        var frame = {
14917          id: frameHeader,
14918          data: data.subarray(frameStart + 10, frameStart + frameSize + 10)
14919        };
14920        frame.key = frame.id; // parse frame values
14921
14922        if (frameParsers[frame.id]) {
14923          // use frame specific parser
14924          frameParsers[frame.id](frame);
14925        } else if (frame.id[0] === 'T') {
14926          // use text frame generic parser
14927          frameParsers['T*'](frame);
14928        } else if (frame.id[0] === 'W') {
14929          // use URL link frame generic parser
14930          frameParsers['W*'](frame);
14931        }
14932
14933        frames.push(frame);
14934        frameStart += 10; // advance past the frame header
14935
14936        frameStart += frameSize; // advance past the frame body
14937      } while (frameStart < tagSize);
14938
14939      return frames;
14940    };
14941
14942    var parseId3 = {
14943      parseId3Frames: parseId3Frames$1,
14944      parseSyncSafeInteger: parseSyncSafeInteger$1,
14945      frameParsers: frameParsers
14946    };
14947    /**
14948     * mux.js
14949     *
14950     * Copyright (c) Brightcove
14951     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
14952     *
14953     * Accepts program elementary stream (PES) data events and parses out
14954     * ID3 metadata from them, if present.
14955     * @see http://id3.org/id3v2.3.0
14956     */
14957
14958    var Stream$5 = stream,
14959        StreamTypes$3 = streamTypes,
14960        id3 = parseId3,
14961        MetadataStream;
14962
14963    MetadataStream = function (options) {
14964      var settings = {
14965        // the bytes of the program-level descriptor field in MP2T
14966        // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and
14967        // program element descriptors"
14968        descriptor: options && options.descriptor
14969      },
14970          // the total size in bytes of the ID3 tag being parsed
14971      tagSize = 0,
14972          // tag data that is not complete enough to be parsed
14973      buffer = [],
14974          // the total number of bytes currently in the buffer
14975      bufferSize = 0,
14976          i;
14977      MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type
14978      // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
14979
14980      this.dispatchType = StreamTypes$3.METADATA_STREAM_TYPE.toString(16);
14981
14982      if (settings.descriptor) {
14983        for (i = 0; i < settings.descriptor.length; i++) {
14984          this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2);
14985        }
14986      }
14987
14988      this.push = function (chunk) {
14989        var tag, frameStart, frameSize, frame, i, frameHeader;
14990
14991        if (chunk.type !== 'timed-metadata') {
14992          return;
14993        } // if data_alignment_indicator is set in the PES header,
14994        // we must have the start of a new ID3 tag. Assume anything
14995        // remaining in the buffer was malformed and throw it out
14996
14997
14998        if (chunk.dataAlignmentIndicator) {
14999          bufferSize = 0;
15000          buffer.length = 0;
15001        } // ignore events that don't look like ID3 data
15002
15003
15004        if (buffer.length === 0 && (chunk.data.length < 10 || chunk.data[0] !== 'I'.charCodeAt(0) || chunk.data[1] !== 'D'.charCodeAt(0) || chunk.data[2] !== '3'.charCodeAt(0))) {
15005          this.trigger('log', {
15006            level: 'warn',
15007            message: 'Skipping unrecognized metadata packet'
15008          });
15009          return;
15010        } // add this chunk to the data we've collected so far
15011
15012
15013        buffer.push(chunk);
15014        bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header
15015
15016        if (buffer.length === 1) {
15017          // the frame size is transmitted as a 28-bit integer in the
15018          // last four bytes of the ID3 header.
15019          // The most significant bit of each byte is dropped and the
15020          // results concatenated to recover the actual value.
15021          tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more
15022          // convenient for our comparisons to include it
15023
15024          tagSize += 10;
15025        } // if the entire frame has not arrived, wait for more data
15026
15027
15028        if (bufferSize < tagSize) {
15029          return;
15030        } // collect the entire frame so it can be parsed
15031
15032
15033        tag = {
15034          data: new Uint8Array(tagSize),
15035          frames: [],
15036          pts: buffer[0].pts,
15037          dts: buffer[0].dts
15038        };
15039
15040        for (i = 0; i < tagSize;) {
15041          tag.data.set(buffer[0].data.subarray(0, tagSize - i), i);
15042          i += buffer[0].data.byteLength;
15043          bufferSize -= buffer[0].data.byteLength;
15044          buffer.shift();
15045        } // find the start of the first frame and the end of the tag
15046
15047
15048        frameStart = 10;
15049
15050        if (tag.data[5] & 0x40) {
15051          // advance the frame start past the extended header
15052          frameStart += 4; // header size field
15053
15054          frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end
15055
15056          tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20));
15057        } // parse one or more ID3 frames
15058        // http://id3.org/id3v2.3.0#ID3v2_frame_overview
15059
15060
15061        do {
15062          // determine the number of bytes in this frame
15063          frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8));
15064
15065          if (frameSize < 1) {
15066            this.trigger('log', {
15067              level: 'warn',
15068              message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.'
15069            }); // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames
15070            // to be sent along.
15071
15072            break;
15073          }
15074
15075          frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]);
15076          frame = {
15077            id: frameHeader,
15078            data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10)
15079          };
15080          frame.key = frame.id; // parse frame values
15081
15082          if (id3.frameParsers[frame.id]) {
15083            // use frame specific parser
15084            id3.frameParsers[frame.id](frame);
15085          } else if (frame.id[0] === 'T') {
15086            // use text frame generic parser
15087            id3.frameParsers['T*'](frame);
15088          } else if (frame.id[0] === 'W') {
15089            // use URL link frame generic parser
15090            id3.frameParsers['W*'](frame);
15091          } // handle the special PRIV frame used to indicate the start
15092          // time for raw AAC data
15093
15094
15095          if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') {
15096            var d = frame.data,
15097                size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
15098            size *= 4;
15099            size += d[7] & 0x03;
15100            frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based
15101            // on the value of this frame
15102            // we couldn't have known the appropriate pts and dts before
15103            // parsing this ID3 tag so set those values now
15104
15105            if (tag.pts === undefined && tag.dts === undefined) {
15106              tag.pts = frame.timeStamp;
15107              tag.dts = frame.timeStamp;
15108            }
15109
15110            this.trigger('timestamp', frame);
15111          }
15112
15113          tag.frames.push(frame);
15114          frameStart += 10; // advance past the frame header
15115
15116          frameStart += frameSize; // advance past the frame body
15117        } while (frameStart < tagSize);
15118
15119        this.trigger('data', tag);
15120      };
15121    };
15122
15123    MetadataStream.prototype = new Stream$5();
15124    var metadataStream = MetadataStream;
15125    /**
15126     * mux.js
15127     *
15128     * Copyright (c) Brightcove
15129     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
15130     *
15131     * A stream-based mp2t to mp4 converter. This utility can be used to
15132     * deliver mp4s to a SourceBuffer on platforms that support native
15133     * Media Source Extensions.
15134     */
15135
15136    var Stream$4 = stream,
15137        CaptionStream$1 = captionStream,
15138        StreamTypes$2 = streamTypes,
15139        TimestampRolloverStream = timestampRolloverStream.TimestampRolloverStream; // object types
15140
15141    var TransportPacketStream, TransportParseStream, ElementaryStream; // constants
15142
15143    var MP2T_PACKET_LENGTH$1 = 188,
15144        // bytes
15145    SYNC_BYTE$1 = 0x47;
15146    /**
15147     * Splits an incoming stream of binary data into MPEG-2 Transport
15148     * Stream packets.
15149     */
15150
15151    TransportPacketStream = function () {
15152      var buffer = new Uint8Array(MP2T_PACKET_LENGTH$1),
15153          bytesInBuffer = 0;
15154      TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream.
15155
15156      /**
15157       * Split a stream of data into M2TS packets
15158      **/
15159
15160      this.push = function (bytes) {
15161        var startIndex = 0,
15162            endIndex = MP2T_PACKET_LENGTH$1,
15163            everything; // If there are bytes remaining from the last segment, prepend them to the
15164        // bytes that were pushed in
15165
15166        if (bytesInBuffer) {
15167          everything = new Uint8Array(bytes.byteLength + bytesInBuffer);
15168          everything.set(buffer.subarray(0, bytesInBuffer));
15169          everything.set(bytes, bytesInBuffer);
15170          bytesInBuffer = 0;
15171        } else {
15172          everything = bytes;
15173        } // While we have enough data for a packet
15174
15175
15176        while (endIndex < everything.byteLength) {
15177          // Look for a pair of start and end sync bytes in the data..
15178          if (everything[startIndex] === SYNC_BYTE$1 && everything[endIndex] === SYNC_BYTE$1) {
15179            // We found a packet so emit it and jump one whole packet forward in
15180            // the stream
15181            this.trigger('data', everything.subarray(startIndex, endIndex));
15182            startIndex += MP2T_PACKET_LENGTH$1;
15183            endIndex += MP2T_PACKET_LENGTH$1;
15184            continue;
15185          } // If we get here, we have somehow become de-synchronized and we need to step
15186          // forward one byte at a time until we find a pair of sync bytes that denote
15187          // a packet
15188
15189
15190          startIndex++;
15191          endIndex++;
15192        } // If there was some data left over at the end of the segment that couldn't
15193        // possibly be a whole packet, keep it because it might be the start of a packet
15194        // that continues in the next segment
15195
15196
15197        if (startIndex < everything.byteLength) {
15198          buffer.set(everything.subarray(startIndex), 0);
15199          bytesInBuffer = everything.byteLength - startIndex;
15200        }
15201      };
15202      /**
15203       * Passes identified M2TS packets to the TransportParseStream to be parsed
15204      **/
15205
15206
15207      this.flush = function () {
15208        // If the buffer contains a whole packet when we are being flushed, emit it
15209        // and empty the buffer. Otherwise hold onto the data because it may be
15210        // important for decoding the next segment
15211        if (bytesInBuffer === MP2T_PACKET_LENGTH$1 && buffer[0] === SYNC_BYTE$1) {
15212          this.trigger('data', buffer);
15213          bytesInBuffer = 0;
15214        }
15215
15216        this.trigger('done');
15217      };
15218
15219      this.endTimeline = function () {
15220        this.flush();
15221        this.trigger('endedtimeline');
15222      };
15223
15224      this.reset = function () {
15225        bytesInBuffer = 0;
15226        this.trigger('reset');
15227      };
15228    };
15229
15230    TransportPacketStream.prototype = new Stream$4();
15231    /**
15232     * Accepts an MP2T TransportPacketStream and emits data events with parsed
15233     * forms of the individual transport stream packets.
15234     */
15235
15236    TransportParseStream = function () {
15237      var parsePsi, parsePat, parsePmt, self;
15238      TransportParseStream.prototype.init.call(this);
15239      self = this;
15240      this.packetsWaitingForPmt = [];
15241      this.programMapTable = undefined;
15242
15243      parsePsi = function (payload, psi) {
15244        var offset = 0; // PSI packets may be split into multiple sections and those
15245        // sections may be split into multiple packets. If a PSI
15246        // section starts in this packet, the payload_unit_start_indicator
15247        // will be true and the first byte of the payload will indicate
15248        // the offset from the current position to the start of the
15249        // section.
15250
15251        if (psi.payloadUnitStartIndicator) {
15252          offset += payload[offset] + 1;
15253        }
15254
15255        if (psi.type === 'pat') {
15256          parsePat(payload.subarray(offset), psi);
15257        } else {
15258          parsePmt(payload.subarray(offset), psi);
15259        }
15260      };
15261
15262      parsePat = function (payload, pat) {
15263        pat.section_number = payload[7]; // eslint-disable-line camelcase
15264
15265        pat.last_section_number = payload[8]; // eslint-disable-line camelcase
15266        // skip the PSI header and parse the first PMT entry
15267
15268        self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11];
15269        pat.pmtPid = self.pmtPid;
15270      };
15271      /**
15272       * Parse out the relevant fields of a Program Map Table (PMT).
15273       * @param payload {Uint8Array} the PMT-specific portion of an MP2T
15274       * packet. The first byte in this array should be the table_id
15275       * field.
15276       * @param pmt {object} the object that should be decorated with
15277       * fields parsed from the PMT.
15278       */
15279
15280
15281      parsePmt = function (payload, pmt) {
15282        var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually
15283        // take effect. We don't believe this should ever be the case
15284        // for HLS but we'll ignore "forward" PMT declarations if we see
15285        // them. Future PMT declarations have the current_next_indicator
15286        // set to zero.
15287
15288        if (!(payload[5] & 0x01)) {
15289          return;
15290        } // overwrite any existing program map table
15291
15292
15293        self.programMapTable = {
15294          video: null,
15295          audio: null,
15296          'timed-metadata': {}
15297        }; // the mapping table ends at the end of the current section
15298
15299        sectionLength = (payload[1] & 0x0f) << 8 | payload[2];
15300        tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how
15301        // long the program info descriptors are
15302
15303        programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table
15304
15305        offset = 12 + programInfoLength;
15306
15307        while (offset < tableEnd) {
15308          var streamType = payload[offset];
15309          var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types
15310          // TODO: should this be done for metadata too? for now maintain behavior of
15311          //       multiple metadata streams
15312
15313          if (streamType === StreamTypes$2.H264_STREAM_TYPE && self.programMapTable.video === null) {
15314            self.programMapTable.video = pid;
15315          } else if (streamType === StreamTypes$2.ADTS_STREAM_TYPE && self.programMapTable.audio === null) {
15316            self.programMapTable.audio = pid;
15317          } else if (streamType === StreamTypes$2.METADATA_STREAM_TYPE) {
15318            // map pid to stream type for metadata streams
15319            self.programMapTable['timed-metadata'][pid] = streamType;
15320          } // move to the next table entry
15321          // skip past the elementary stream descriptors, if present
15322
15323
15324          offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5;
15325        } // record the map on the packet as well
15326
15327
15328        pmt.programMapTable = self.programMapTable;
15329      };
15330      /**
15331       * Deliver a new MP2T packet to the next stream in the pipeline.
15332       */
15333
15334
15335      this.push = function (packet) {
15336        var result = {},
15337            offset = 4;
15338        result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1]
15339
15340        result.pid = packet[1] & 0x1f;
15341        result.pid <<= 8;
15342        result.pid |= packet[2]; // if an adaption field is present, its length is specified by the
15343        // fifth byte of the TS packet header. The adaptation field is
15344        // used to add stuffing to PES packets that don't fill a complete
15345        // TS packet, and to specify some forms of timing and control data
15346        // that we do not currently use.
15347
15348        if ((packet[3] & 0x30) >>> 4 > 0x01) {
15349          offset += packet[offset] + 1;
15350        } // parse the rest of the packet based on the type
15351
15352
15353        if (result.pid === 0) {
15354          result.type = 'pat';
15355          parsePsi(packet.subarray(offset), result);
15356          this.trigger('data', result);
15357        } else if (result.pid === this.pmtPid) {
15358          result.type = 'pmt';
15359          parsePsi(packet.subarray(offset), result);
15360          this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now
15361
15362          while (this.packetsWaitingForPmt.length) {
15363            this.processPes_.apply(this, this.packetsWaitingForPmt.shift());
15364          }
15365        } else if (this.programMapTable === undefined) {
15366          // When we have not seen a PMT yet, defer further processing of
15367          // PES packets until one has been parsed
15368          this.packetsWaitingForPmt.push([packet, offset, result]);
15369        } else {
15370          this.processPes_(packet, offset, result);
15371        }
15372      };
15373
15374      this.processPes_ = function (packet, offset, result) {
15375        // set the appropriate stream type
15376        if (result.pid === this.programMapTable.video) {
15377          result.streamType = StreamTypes$2.H264_STREAM_TYPE;
15378        } else if (result.pid === this.programMapTable.audio) {
15379          result.streamType = StreamTypes$2.ADTS_STREAM_TYPE;
15380        } else {
15381          // if not video or audio, it is timed-metadata or unknown
15382          // if unknown, streamType will be undefined
15383          result.streamType = this.programMapTable['timed-metadata'][result.pid];
15384        }
15385
15386        result.type = 'pes';
15387        result.data = packet.subarray(offset);
15388        this.trigger('data', result);
15389      };
15390    };
15391
15392    TransportParseStream.prototype = new Stream$4();
15393    TransportParseStream.STREAM_TYPES = {
15394      h264: 0x1b,
15395      adts: 0x0f
15396    };
15397    /**
15398     * Reconsistutes program elementary stream (PES) packets from parsed
15399     * transport stream packets. That is, if you pipe an
15400     * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output
15401     * events will be events which capture the bytes for individual PES
15402     * packets plus relevant metadata that has been extracted from the
15403     * container.
15404     */
15405
15406    ElementaryStream = function () {
15407      var self = this,
15408          segmentHadPmt = false,
15409          // PES packet fragments
15410      video = {
15411        data: [],
15412        size: 0
15413      },
15414          audio = {
15415        data: [],
15416        size: 0
15417      },
15418          timedMetadata = {
15419        data: [],
15420        size: 0
15421      },
15422          programMapTable,
15423          parsePes = function (payload, pes) {
15424        var ptsDtsFlags;
15425        const startPrefix = payload[0] << 16 | payload[1] << 8 | payload[2]; // default to an empty array
15426
15427        pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets
15428        // that are frame data that is continuing from the previous fragment. This
15429        // is to check that the pes data is the start of a new pes payload
15430
15431        if (startPrefix !== 1) {
15432          return;
15433        } // get the packet length, this will be 0 for video
15434
15435
15436        pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe
15437
15438        pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value
15439        // and a DTS value. Determine what combination of values is
15440        // available to work with.
15441
15442        ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number.  Javascript
15443        // performs all bitwise operations on 32-bit integers but javascript
15444        // supports a much greater range (52-bits) of integer using standard
15445        // mathematical operations.
15446        // We construct a 31-bit value using bitwise operators over the 31
15447        // most significant bits and then multiply by 4 (equal to a left-shift
15448        // of 2) before we add the final 2 least significant bits of the
15449        // timestamp (equal to an OR.)
15450
15451        if (ptsDtsFlags & 0xC0) {
15452          // the PTS and DTS are not written out directly. For information
15453          // on how they are encoded, see
15454          // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
15455          pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3;
15456          pes.pts *= 4; // Left shift by 2
15457
15458          pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs
15459
15460          pes.dts = pes.pts;
15461
15462          if (ptsDtsFlags & 0x40) {
15463            pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3;
15464            pes.dts *= 4; // Left shift by 2
15465
15466            pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs
15467          }
15468        } // the data section starts immediately after the PES header.
15469        // pes_header_data_length specifies the number of header bytes
15470        // that follow the last byte of the field.
15471
15472
15473        pes.data = payload.subarray(9 + payload[8]);
15474      },
15475
15476      /**
15477        * Pass completely parsed PES packets to the next stream in the pipeline
15478       **/
15479      flushStream = function (stream, type, forceFlush) {
15480        var packetData = new Uint8Array(stream.size),
15481            event = {
15482          type: type
15483        },
15484            i = 0,
15485            offset = 0,
15486            packetFlushable = false,
15487            fragment; // do nothing if there is not enough buffered data for a complete
15488        // PES header
15489
15490        if (!stream.data.length || stream.size < 9) {
15491          return;
15492        }
15493
15494        event.trackId = stream.data[0].pid; // reassemble the packet
15495
15496        for (i = 0; i < stream.data.length; i++) {
15497          fragment = stream.data[i];
15498          packetData.set(fragment.data, offset);
15499          offset += fragment.data.byteLength;
15500        } // parse assembled packet's PES header
15501
15502
15503        parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length
15504        // check that there is enough stream data to fill the packet
15505
15506        packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right
15507
15508        if (forceFlush || packetFlushable) {
15509          stream.size = 0;
15510          stream.data.length = 0;
15511        } // only emit packets that are complete. this is to avoid assembling
15512        // incomplete PES packets due to poor segmentation
15513
15514
15515        if (packetFlushable) {
15516          self.trigger('data', event);
15517        }
15518      };
15519
15520      ElementaryStream.prototype.init.call(this);
15521      /**
15522       * Identifies M2TS packet types and parses PES packets using metadata
15523       * parsed from the PMT
15524       **/
15525
15526      this.push = function (data) {
15527        ({
15528          pat: function () {// we have to wait for the PMT to arrive as well before we
15529            // have any meaningful metadata
15530          },
15531          pes: function () {
15532            var stream, streamType;
15533
15534            switch (data.streamType) {
15535              case StreamTypes$2.H264_STREAM_TYPE:
15536                stream = video;
15537                streamType = 'video';
15538                break;
15539
15540              case StreamTypes$2.ADTS_STREAM_TYPE:
15541                stream = audio;
15542                streamType = 'audio';
15543                break;
15544
15545              case StreamTypes$2.METADATA_STREAM_TYPE:
15546                stream = timedMetadata;
15547                streamType = 'timed-metadata';
15548                break;
15549
15550              default:
15551                // ignore unknown stream types
15552                return;
15553            } // if a new packet is starting, we can flush the completed
15554            // packet
15555
15556
15557            if (data.payloadUnitStartIndicator) {
15558              flushStream(stream, streamType, true);
15559            } // buffer this fragment until we are sure we've received the
15560            // complete payload
15561
15562
15563            stream.data.push(data);
15564            stream.size += data.data.byteLength;
15565          },
15566          pmt: function () {
15567            var event = {
15568              type: 'metadata',
15569              tracks: []
15570            };
15571            programMapTable = data.programMapTable; // translate audio and video streams to tracks
15572
15573            if (programMapTable.video !== null) {
15574              event.tracks.push({
15575                timelineStartInfo: {
15576                  baseMediaDecodeTime: 0
15577                },
15578                id: +programMapTable.video,
15579                codec: 'avc',
15580                type: 'video'
15581              });
15582            }
15583
15584            if (programMapTable.audio !== null) {
15585              event.tracks.push({
15586                timelineStartInfo: {
15587                  baseMediaDecodeTime: 0
15588                },
15589                id: +programMapTable.audio,
15590                codec: 'adts',
15591                type: 'audio'
15592              });
15593            }
15594
15595            segmentHadPmt = true;
15596            self.trigger('data', event);
15597          }
15598        })[data.type]();
15599      };
15600
15601      this.reset = function () {
15602        video.size = 0;
15603        video.data.length = 0;
15604        audio.size = 0;
15605        audio.data.length = 0;
15606        this.trigger('reset');
15607      };
15608      /**
15609       * Flush any remaining input. Video PES packets may be of variable
15610       * length. Normally, the start of a new video packet can trigger the
15611       * finalization of the previous packet. That is not possible if no
15612       * more video is forthcoming, however. In that case, some other
15613       * mechanism (like the end of the file) has to be employed. When it is
15614       * clear that no additional data is forthcoming, calling this method
15615       * will flush the buffered packets.
15616       */
15617
15618
15619      this.flushStreams_ = function () {
15620        // !!THIS ORDER IS IMPORTANT!!
15621        // video first then audio
15622        flushStream(video, 'video');
15623        flushStream(audio, 'audio');
15624        flushStream(timedMetadata, 'timed-metadata');
15625      };
15626
15627      this.flush = function () {
15628        // if on flush we haven't had a pmt emitted
15629        // and we have a pmt to emit. emit the pmt
15630        // so that we trigger a trackinfo downstream.
15631        if (!segmentHadPmt && programMapTable) {
15632          var pmt = {
15633            type: 'metadata',
15634            tracks: []
15635          }; // translate audio and video streams to tracks
15636
15637          if (programMapTable.video !== null) {
15638            pmt.tracks.push({
15639              timelineStartInfo: {
15640                baseMediaDecodeTime: 0
15641              },
15642              id: +programMapTable.video,
15643              codec: 'avc',
15644              type: 'video'
15645            });
15646          }
15647
15648          if (programMapTable.audio !== null) {
15649            pmt.tracks.push({
15650              timelineStartInfo: {
15651                baseMediaDecodeTime: 0
15652              },
15653              id: +programMapTable.audio,
15654              codec: 'adts',
15655              type: 'audio'
15656            });
15657          }
15658
15659          self.trigger('data', pmt);
15660        }
15661
15662        segmentHadPmt = false;
15663        this.flushStreams_();
15664        this.trigger('done');
15665      };
15666    };
15667
15668    ElementaryStream.prototype = new Stream$4();
15669    var m2ts$1 = {
15670      PAT_PID: 0x0000,
15671      MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH$1,
15672      TransportPacketStream: TransportPacketStream,
15673      TransportParseStream: TransportParseStream,
15674      ElementaryStream: ElementaryStream,
15675      TimestampRolloverStream: TimestampRolloverStream,
15676      CaptionStream: CaptionStream$1.CaptionStream,
15677      Cea608Stream: CaptionStream$1.Cea608Stream,
15678      Cea708Stream: CaptionStream$1.Cea708Stream,
15679      MetadataStream: metadataStream
15680    };
15681
15682    for (var type in StreamTypes$2) {
15683      if (StreamTypes$2.hasOwnProperty(type)) {
15684        m2ts$1[type] = StreamTypes$2[type];
15685      }
15686    }
15687
15688    var m2ts_1 = m2ts$1;
15689    /**
15690     * mux.js
15691     *
15692     * Copyright (c) Brightcove
15693     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
15694     */
15695
15696    var Stream$3 = stream;
15697    var ONE_SECOND_IN_TS$2 = clock$2.ONE_SECOND_IN_TS;
15698    var AdtsStream$1;
15699    var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
15700    /*
15701     * Accepts a ElementaryStream and emits data events with parsed
15702     * AAC Audio Frames of the individual packets. Input audio in ADTS
15703     * format is unpacked and re-emitted as AAC frames.
15704     *
15705     * @see http://wiki.multimedia.cx/index.php?title=ADTS
15706     * @see http://wiki.multimedia.cx/?title=Understanding_AAC
15707     */
15708
15709    AdtsStream$1 = function (handlePartialSegments) {
15710      var buffer,
15711          frameNum = 0;
15712      AdtsStream$1.prototype.init.call(this);
15713
15714      this.skipWarn_ = function (start, end) {
15715        this.trigger('log', {
15716          level: 'warn',
15717          message: `adts skiping bytes ${start} to ${end} in frame ${frameNum} outside syncword`
15718        });
15719      };
15720
15721      this.push = function (packet) {
15722        var i = 0,
15723            frameLength,
15724            protectionSkipBytes,
15725            oldBuffer,
15726            sampleCount,
15727            adtsFrameDuration;
15728
15729        if (!handlePartialSegments) {
15730          frameNum = 0;
15731        }
15732
15733        if (packet.type !== 'audio') {
15734          // ignore non-audio data
15735          return;
15736        } // Prepend any data in the buffer to the input data so that we can parse
15737        // aac frames the cross a PES packet boundary
15738
15739
15740        if (buffer && buffer.length) {
15741          oldBuffer = buffer;
15742          buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength);
15743          buffer.set(oldBuffer);
15744          buffer.set(packet.data, oldBuffer.byteLength);
15745        } else {
15746          buffer = packet.data;
15747        } // unpack any ADTS frames which have been fully received
15748        // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS
15749
15750
15751        var skip; // We use i + 7 here because we want to be able to parse the entire header.
15752        // If we don't have enough bytes to do that, then we definitely won't have a full frame.
15753
15754        while (i + 7 < buffer.length) {
15755          // Look for the start of an ADTS header..
15756          if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) {
15757            if (typeof skip !== 'number') {
15758              skip = i;
15759            } // If a valid header was not found,  jump one forward and attempt to
15760            // find a valid ADTS header starting at the next byte
15761
15762
15763            i++;
15764            continue;
15765          }
15766
15767          if (typeof skip === 'number') {
15768            this.skipWarn_(skip, i);
15769            skip = null;
15770          } // The protection skip bit tells us if we have 2 bytes of CRC data at the
15771          // end of the ADTS header
15772
15773
15774          protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the
15775          // end of the sync sequence
15776          // NOTE: frame length includes the size of the header
15777
15778          frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5;
15779          sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024;
15780          adtsFrameDuration = sampleCount * ONE_SECOND_IN_TS$2 / ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2]; // If we don't have enough data to actually finish this ADTS frame,
15781          // then we have to wait for more data
15782
15783          if (buffer.byteLength - i < frameLength) {
15784            break;
15785          } // Otherwise, deliver the complete AAC frame
15786
15787
15788          this.trigger('data', {
15789            pts: packet.pts + frameNum * adtsFrameDuration,
15790            dts: packet.dts + frameNum * adtsFrameDuration,
15791            sampleCount: sampleCount,
15792            audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1,
15793            channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6,
15794            samplerate: ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2],
15795            samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2,
15796            // assume ISO/IEC 14496-12 AudioSampleEntry default of 16
15797            samplesize: 16,
15798            // data is the frame without it's header
15799            data: buffer.subarray(i + 7 + protectionSkipBytes, i + frameLength)
15800          });
15801          frameNum++;
15802          i += frameLength;
15803        }
15804
15805        if (typeof skip === 'number') {
15806          this.skipWarn_(skip, i);
15807          skip = null;
15808        } // remove processed bytes from the buffer.
15809
15810
15811        buffer = buffer.subarray(i);
15812      };
15813
15814      this.flush = function () {
15815        frameNum = 0;
15816        this.trigger('done');
15817      };
15818
15819      this.reset = function () {
15820        buffer = void 0;
15821        this.trigger('reset');
15822      };
15823
15824      this.endTimeline = function () {
15825        buffer = void 0;
15826        this.trigger('endedtimeline');
15827      };
15828    };
15829
15830    AdtsStream$1.prototype = new Stream$3();
15831    var adts = AdtsStream$1;
15832    /**
15833     * mux.js
15834     *
15835     * Copyright (c) Brightcove
15836     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
15837     */
15838
15839    var ExpGolomb$1;
15840    /**
15841     * Parser for exponential Golomb codes, a variable-bitwidth number encoding
15842     * scheme used by h264.
15843     */
15844
15845    ExpGolomb$1 = function (workingData) {
15846      var // the number of bytes left to examine in workingData
15847      workingBytesAvailable = workingData.byteLength,
15848          // the current word being examined
15849      workingWord = 0,
15850          // :uint
15851      // the number of bits left to examine in the current word
15852      workingBitsAvailable = 0; // :uint;
15853      // ():uint
15854
15855      this.length = function () {
15856        return 8 * workingBytesAvailable;
15857      }; // ():uint
15858
15859
15860      this.bitsAvailable = function () {
15861        return 8 * workingBytesAvailable + workingBitsAvailable;
15862      }; // ():void
15863
15864
15865      this.loadWord = function () {
15866        var position = workingData.byteLength - workingBytesAvailable,
15867            workingBytes = new Uint8Array(4),
15868            availableBytes = Math.min(4, workingBytesAvailable);
15869
15870        if (availableBytes === 0) {
15871          throw new Error('no bytes available');
15872        }
15873
15874        workingBytes.set(workingData.subarray(position, position + availableBytes));
15875        workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed
15876
15877        workingBitsAvailable = availableBytes * 8;
15878        workingBytesAvailable -= availableBytes;
15879      }; // (count:int):void
15880
15881
15882      this.skipBits = function (count) {
15883        var skipBytes; // :int
15884
15885        if (workingBitsAvailable > count) {
15886          workingWord <<= count;
15887          workingBitsAvailable -= count;
15888        } else {
15889          count -= workingBitsAvailable;
15890          skipBytes = Math.floor(count / 8);
15891          count -= skipBytes * 8;
15892          workingBytesAvailable -= skipBytes;
15893          this.loadWord();
15894          workingWord <<= count;
15895          workingBitsAvailable -= count;
15896        }
15897      }; // (size:int):uint
15898
15899
15900      this.readBits = function (size) {
15901        var bits = Math.min(workingBitsAvailable, size),
15902            // :uint
15903        valu = workingWord >>> 32 - bits; // :uint
15904        // if size > 31, handle error
15905
15906        workingBitsAvailable -= bits;
15907
15908        if (workingBitsAvailable > 0) {
15909          workingWord <<= bits;
15910        } else if (workingBytesAvailable > 0) {
15911          this.loadWord();
15912        }
15913
15914        bits = size - bits;
15915
15916        if (bits > 0) {
15917          return valu << bits | this.readBits(bits);
15918        }
15919
15920        return valu;
15921      }; // ():uint
15922
15923
15924      this.skipLeadingZeros = function () {
15925        var leadingZeroCount; // :uint
15926
15927        for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) {
15928          if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) {
15929            // the first bit of working word is 1
15930            workingWord <<= leadingZeroCount;
15931            workingBitsAvailable -= leadingZeroCount;
15932            return leadingZeroCount;
15933          }
15934        } // we exhausted workingWord and still have not found a 1
15935
15936
15937        this.loadWord();
15938        return leadingZeroCount + this.skipLeadingZeros();
15939      }; // ():void
15940
15941
15942      this.skipUnsignedExpGolomb = function () {
15943        this.skipBits(1 + this.skipLeadingZeros());
15944      }; // ():void
15945
15946
15947      this.skipExpGolomb = function () {
15948        this.skipBits(1 + this.skipLeadingZeros());
15949      }; // ():uint
15950
15951
15952      this.readUnsignedExpGolomb = function () {
15953        var clz = this.skipLeadingZeros(); // :uint
15954
15955        return this.readBits(clz + 1) - 1;
15956      }; // ():int
15957
15958
15959      this.readExpGolomb = function () {
15960        var valu = this.readUnsignedExpGolomb(); // :int
15961
15962        if (0x01 & valu) {
15963          // the number is odd if the low order bit is set
15964          return 1 + valu >>> 1; // add 1 to make it even, and divide by 2
15965        }
15966
15967        return -1 * (valu >>> 1); // divide by two then make it negative
15968      }; // Some convenience functions
15969      // :Boolean
15970
15971
15972      this.readBoolean = function () {
15973        return this.readBits(1) === 1;
15974      }; // ():int
15975
15976
15977      this.readUnsignedByte = function () {
15978        return this.readBits(8);
15979      };
15980
15981      this.loadWord();
15982    };
15983
15984    var expGolomb = ExpGolomb$1;
15985    /**
15986     * mux.js
15987     *
15988     * Copyright (c) Brightcove
15989     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
15990     */
15991
15992    var Stream$2 = stream;
15993    var ExpGolomb = expGolomb;
15994    var H264Stream$1, NalByteStream;
15995    var PROFILES_WITH_OPTIONAL_SPS_DATA;
15996    /**
15997     * Accepts a NAL unit byte stream and unpacks the embedded NAL units.
15998     */
15999
16000    NalByteStream = function () {
16001      var syncPoint = 0,
16002          i,
16003          buffer;
16004      NalByteStream.prototype.init.call(this);
16005      /*
16006       * Scans a byte stream and triggers a data event with the NAL units found.
16007       * @param {Object} data Event received from H264Stream
16008       * @param {Uint8Array} data.data The h264 byte stream to be scanned
16009       *
16010       * @see H264Stream.push
16011       */
16012
16013      this.push = function (data) {
16014        var swapBuffer;
16015
16016        if (!buffer) {
16017          buffer = data.data;
16018        } else {
16019          swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength);
16020          swapBuffer.set(buffer);
16021          swapBuffer.set(data.data, buffer.byteLength);
16022          buffer = swapBuffer;
16023        }
16024
16025        var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B
16026        // scan for NAL unit boundaries
16027        // a match looks like this:
16028        // 0 0 1 .. NAL .. 0 0 1
16029        // ^ sync point        ^ i
16030        // or this:
16031        // 0 0 1 .. NAL .. 0 0 0
16032        // ^ sync point        ^ i
16033        // advance the sync point to a NAL start, if necessary
16034
16035        for (; syncPoint < len - 3; syncPoint++) {
16036          if (buffer[syncPoint + 2] === 1) {
16037            // the sync point is properly aligned
16038            i = syncPoint + 5;
16039            break;
16040          }
16041        }
16042
16043        while (i < len) {
16044          // look at the current byte to determine if we've hit the end of
16045          // a NAL unit boundary
16046          switch (buffer[i]) {
16047            case 0:
16048              // skip past non-sync sequences
16049              if (buffer[i - 1] !== 0) {
16050                i += 2;
16051                break;
16052              } else if (buffer[i - 2] !== 0) {
16053                i++;
16054                break;
16055              } // deliver the NAL unit if it isn't empty
16056
16057
16058              if (syncPoint + 3 !== i - 2) {
16059                this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
16060              } // drop trailing zeroes
16061
16062
16063              do {
16064                i++;
16065              } while (buffer[i] !== 1 && i < len);
16066
16067              syncPoint = i - 2;
16068              i += 3;
16069              break;
16070
16071            case 1:
16072              // skip past non-sync sequences
16073              if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) {
16074                i += 3;
16075                break;
16076              } // deliver the NAL unit
16077
16078
16079              this.trigger('data', buffer.subarray(syncPoint + 3, i - 2));
16080              syncPoint = i - 2;
16081              i += 3;
16082              break;
16083
16084            default:
16085              // the current byte isn't a one or zero, so it cannot be part
16086              // of a sync sequence
16087              i += 3;
16088              break;
16089          }
16090        } // filter out the NAL units that were delivered
16091
16092
16093        buffer = buffer.subarray(syncPoint);
16094        i -= syncPoint;
16095        syncPoint = 0;
16096      };
16097
16098      this.reset = function () {
16099        buffer = null;
16100        syncPoint = 0;
16101        this.trigger('reset');
16102      };
16103
16104      this.flush = function () {
16105        // deliver the last buffered NAL unit
16106        if (buffer && buffer.byteLength > 3) {
16107          this.trigger('data', buffer.subarray(syncPoint + 3));
16108        } // reset the stream state
16109
16110
16111        buffer = null;
16112        syncPoint = 0;
16113        this.trigger('done');
16114      };
16115
16116      this.endTimeline = function () {
16117        this.flush();
16118        this.trigger('endedtimeline');
16119      };
16120    };
16121
16122    NalByteStream.prototype = new Stream$2(); // values of profile_idc that indicate additional fields are included in the SPS
16123    // see Recommendation ITU-T H.264 (4/2013),
16124    // 7.3.2.1.1 Sequence parameter set data syntax
16125
16126    PROFILES_WITH_OPTIONAL_SPS_DATA = {
16127      100: true,
16128      110: true,
16129      122: true,
16130      244: true,
16131      44: true,
16132      83: true,
16133      86: true,
16134      118: true,
16135      128: true,
16136      // TODO: the three profiles below don't
16137      // appear to have sps data in the specificiation anymore?
16138      138: true,
16139      139: true,
16140      134: true
16141    };
16142    /**
16143     * Accepts input from a ElementaryStream and produces H.264 NAL unit data
16144     * events.
16145     */
16146
16147    H264Stream$1 = function () {
16148      var nalByteStream = new NalByteStream(),
16149          self,
16150          trackId,
16151          currentPts,
16152          currentDts,
16153          discardEmulationPreventionBytes,
16154          readSequenceParameterSet,
16155          skipScalingList;
16156      H264Stream$1.prototype.init.call(this);
16157      self = this;
16158      /*
16159       * Pushes a packet from a stream onto the NalByteStream
16160       *
16161       * @param {Object} packet - A packet received from a stream
16162       * @param {Uint8Array} packet.data - The raw bytes of the packet
16163       * @param {Number} packet.dts - Decode timestamp of the packet
16164       * @param {Number} packet.pts - Presentation timestamp of the packet
16165       * @param {Number} packet.trackId - The id of the h264 track this packet came from
16166       * @param {('video'|'audio')} packet.type - The type of packet
16167       *
16168       */
16169
16170      this.push = function (packet) {
16171        if (packet.type !== 'video') {
16172          return;
16173        }
16174
16175        trackId = packet.trackId;
16176        currentPts = packet.pts;
16177        currentDts = packet.dts;
16178        nalByteStream.push(packet);
16179      };
16180      /*
16181       * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps
16182       * for the NALUs to the next stream component.
16183       * Also, preprocess caption and sequence parameter NALUs.
16184       *
16185       * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push`
16186       * @see NalByteStream.push
16187       */
16188
16189
16190      nalByteStream.on('data', function (data) {
16191        var event = {
16192          trackId: trackId,
16193          pts: currentPts,
16194          dts: currentDts,
16195          data: data,
16196          nalUnitTypeCode: data[0] & 0x1f
16197        };
16198
16199        switch (event.nalUnitTypeCode) {
16200          case 0x05:
16201            event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr';
16202            break;
16203
16204          case 0x06:
16205            event.nalUnitType = 'sei_rbsp';
16206            event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
16207            break;
16208
16209          case 0x07:
16210            event.nalUnitType = 'seq_parameter_set_rbsp';
16211            event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1));
16212            event.config = readSequenceParameterSet(event.escapedRBSP);
16213            break;
16214
16215          case 0x08:
16216            event.nalUnitType = 'pic_parameter_set_rbsp';
16217            break;
16218
16219          case 0x09:
16220            event.nalUnitType = 'access_unit_delimiter_rbsp';
16221            break;
16222        } // This triggers data on the H264Stream
16223
16224
16225        self.trigger('data', event);
16226      });
16227      nalByteStream.on('done', function () {
16228        self.trigger('done');
16229      });
16230      nalByteStream.on('partialdone', function () {
16231        self.trigger('partialdone');
16232      });
16233      nalByteStream.on('reset', function () {
16234        self.trigger('reset');
16235      });
16236      nalByteStream.on('endedtimeline', function () {
16237        self.trigger('endedtimeline');
16238      });
16239
16240      this.flush = function () {
16241        nalByteStream.flush();
16242      };
16243
16244      this.partialFlush = function () {
16245        nalByteStream.partialFlush();
16246      };
16247
16248      this.reset = function () {
16249        nalByteStream.reset();
16250      };
16251
16252      this.endTimeline = function () {
16253        nalByteStream.endTimeline();
16254      };
16255      /**
16256       * Advance the ExpGolomb decoder past a scaling list. The scaling
16257       * list is optionally transmitted as part of a sequence parameter
16258       * set and is not relevant to transmuxing.
16259       * @param count {number} the number of entries in this scaling list
16260       * @param expGolombDecoder {object} an ExpGolomb pointed to the
16261       * start of a scaling list
16262       * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
16263       */
16264
16265
16266      skipScalingList = function (count, expGolombDecoder) {
16267        var lastScale = 8,
16268            nextScale = 8,
16269            j,
16270            deltaScale;
16271
16272        for (j = 0; j < count; j++) {
16273          if (nextScale !== 0) {
16274            deltaScale = expGolombDecoder.readExpGolomb();
16275            nextScale = (lastScale + deltaScale + 256) % 256;
16276          }
16277
16278          lastScale = nextScale === 0 ? lastScale : nextScale;
16279        }
16280      };
16281      /**
16282       * Expunge any "Emulation Prevention" bytes from a "Raw Byte
16283       * Sequence Payload"
16284       * @param data {Uint8Array} the bytes of a RBSP from a NAL
16285       * unit
16286       * @return {Uint8Array} the RBSP without any Emulation
16287       * Prevention Bytes
16288       */
16289
16290
16291      discardEmulationPreventionBytes = function (data) {
16292        var length = data.byteLength,
16293            emulationPreventionBytesPositions = [],
16294            i = 1,
16295            newLength,
16296            newData; // Find all `Emulation Prevention Bytes`
16297
16298        while (i < length - 2) {
16299          if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
16300            emulationPreventionBytesPositions.push(i + 2);
16301            i += 2;
16302          } else {
16303            i++;
16304          }
16305        } // If no Emulation Prevention Bytes were found just return the original
16306        // array
16307
16308
16309        if (emulationPreventionBytesPositions.length === 0) {
16310          return data;
16311        } // Create a new array to hold the NAL unit data
16312
16313
16314        newLength = length - emulationPreventionBytesPositions.length;
16315        newData = new Uint8Array(newLength);
16316        var sourceIndex = 0;
16317
16318        for (i = 0; i < newLength; sourceIndex++, i++) {
16319          if (sourceIndex === emulationPreventionBytesPositions[0]) {
16320            // Skip this byte
16321            sourceIndex++; // Remove this position index
16322
16323            emulationPreventionBytesPositions.shift();
16324          }
16325
16326          newData[i] = data[sourceIndex];
16327        }
16328
16329        return newData;
16330      };
16331      /**
16332       * Read a sequence parameter set and return some interesting video
16333       * properties. A sequence parameter set is the H264 metadata that
16334       * describes the properties of upcoming video frames.
16335       * @param data {Uint8Array} the bytes of a sequence parameter set
16336       * @return {object} an object with configuration parsed from the
16337       * sequence parameter set, including the dimensions of the
16338       * associated video frames.
16339       */
16340
16341
16342      readSequenceParameterSet = function (data) {
16343        var frameCropLeftOffset = 0,
16344            frameCropRightOffset = 0,
16345            frameCropTopOffset = 0,
16346            frameCropBottomOffset = 0,
16347            expGolombDecoder,
16348            profileIdc,
16349            levelIdc,
16350            profileCompatibility,
16351            chromaFormatIdc,
16352            picOrderCntType,
16353            numRefFramesInPicOrderCntCycle,
16354            picWidthInMbsMinus1,
16355            picHeightInMapUnitsMinus1,
16356            frameMbsOnlyFlag,
16357            scalingListCount,
16358            sarRatio = [1, 1],
16359            aspectRatioIdc,
16360            i;
16361        expGolombDecoder = new ExpGolomb(data);
16362        profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc
16363
16364        profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag
16365
16366        levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8)
16367
16368        expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id
16369        // some profiles have more optional data we don't need
16370
16371        if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) {
16372          chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb();
16373
16374          if (chromaFormatIdc === 3) {
16375            expGolombDecoder.skipBits(1); // separate_colour_plane_flag
16376          }
16377
16378          expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8
16379
16380          expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8
16381
16382          expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag
16383
16384          if (expGolombDecoder.readBoolean()) {
16385            // seq_scaling_matrix_present_flag
16386            scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
16387
16388            for (i = 0; i < scalingListCount; i++) {
16389              if (expGolombDecoder.readBoolean()) {
16390                // seq_scaling_list_present_flag[ i ]
16391                if (i < 6) {
16392                  skipScalingList(16, expGolombDecoder);
16393                } else {
16394                  skipScalingList(64, expGolombDecoder);
16395                }
16396              }
16397            }
16398          }
16399        }
16400
16401        expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4
16402
16403        picOrderCntType = expGolombDecoder.readUnsignedExpGolomb();
16404
16405        if (picOrderCntType === 0) {
16406          expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4
16407        } else if (picOrderCntType === 1) {
16408          expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag
16409
16410          expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic
16411
16412          expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field
16413
16414          numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb();
16415
16416          for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
16417            expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ]
16418          }
16419        }
16420
16421        expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames
16422
16423        expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag
16424
16425        picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
16426        picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb();
16427        frameMbsOnlyFlag = expGolombDecoder.readBits(1);
16428
16429        if (frameMbsOnlyFlag === 0) {
16430          expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag
16431        }
16432
16433        expGolombDecoder.skipBits(1); // direct_8x8_inference_flag
16434
16435        if (expGolombDecoder.readBoolean()) {
16436          // frame_cropping_flag
16437          frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb();
16438          frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb();
16439          frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb();
16440          frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb();
16441        }
16442
16443        if (expGolombDecoder.readBoolean()) {
16444          // vui_parameters_present_flag
16445          if (expGolombDecoder.readBoolean()) {
16446            // aspect_ratio_info_present_flag
16447            aspectRatioIdc = expGolombDecoder.readUnsignedByte();
16448
16449            switch (aspectRatioIdc) {
16450              case 1:
16451                sarRatio = [1, 1];
16452                break;
16453
16454              case 2:
16455                sarRatio = [12, 11];
16456                break;
16457
16458              case 3:
16459                sarRatio = [10, 11];
16460                break;
16461
16462              case 4:
16463                sarRatio = [16, 11];
16464                break;
16465
16466              case 5:
16467                sarRatio = [40, 33];
16468                break;
16469
16470              case 6:
16471                sarRatio = [24, 11];
16472                break;
16473
16474              case 7:
16475                sarRatio = [20, 11];
16476                break;
16477
16478              case 8:
16479                sarRatio = [32, 11];
16480                break;
16481
16482              case 9:
16483                sarRatio = [80, 33];
16484                break;
16485
16486              case 10:
16487                sarRatio = [18, 11];
16488                break;
16489
16490              case 11:
16491                sarRatio = [15, 11];
16492                break;
16493
16494              case 12:
16495                sarRatio = [64, 33];
16496                break;
16497
16498              case 13:
16499                sarRatio = [160, 99];
16500                break;
16501
16502              case 14:
16503                sarRatio = [4, 3];
16504                break;
16505
16506              case 15:
16507                sarRatio = [3, 2];
16508                break;
16509
16510              case 16:
16511                sarRatio = [2, 1];
16512                break;
16513
16514              case 255:
16515                {
16516                  sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()];
16517                  break;
16518                }
16519            }
16520
16521            if (sarRatio) {
16522              sarRatio[0] / sarRatio[1];
16523            }
16524          }
16525        }
16526
16527        return {
16528          profileIdc: profileIdc,
16529          levelIdc: levelIdc,
16530          profileCompatibility: profileCompatibility,
16531          width: (picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2,
16532          height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2,
16533          // sar is sample aspect ratio
16534          sarRatio: sarRatio
16535        };
16536      };
16537    };
16538
16539    H264Stream$1.prototype = new Stream$2();
16540    var h264 = {
16541      H264Stream: H264Stream$1,
16542      NalByteStream: NalByteStream
16543    };
16544    /**
16545     * mux.js
16546     *
16547     * Copyright (c) Brightcove
16548     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
16549     *
16550     * Utilities to detect basic properties and metadata about Aac data.
16551     */
16552
16553    var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
16554
16555    var parseId3TagSize = function (header, byteIndex) {
16556      var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9],
16557          flags = header[byteIndex + 5],
16558          footerPresent = (flags & 16) >> 4; // if we get a negative returnSize clamp it to 0
16559
16560      returnSize = returnSize >= 0 ? returnSize : 0;
16561
16562      if (footerPresent) {
16563        return returnSize + 20;
16564      }
16565
16566      return returnSize + 10;
16567    };
16568
16569    var getId3Offset = function (data, offset) {
16570      if (data.length - offset < 10 || data[offset] !== 'I'.charCodeAt(0) || data[offset + 1] !== 'D'.charCodeAt(0) || data[offset + 2] !== '3'.charCodeAt(0)) {
16571        return offset;
16572      }
16573
16574      offset += parseId3TagSize(data, offset);
16575      return getId3Offset(data, offset);
16576    }; // TODO: use vhs-utils
16577
16578
16579    var isLikelyAacData$1 = function (data) {
16580      var offset = getId3Offset(data, 0);
16581      return data.length >= offset + 2 && (data[offset] & 0xFF) === 0xFF && (data[offset + 1] & 0xF0) === 0xF0 && // verify that the 2 layer bits are 0, aka this
16582      // is not mp3 data but aac data.
16583      (data[offset + 1] & 0x16) === 0x10;
16584    };
16585
16586    var parseSyncSafeInteger = function (data) {
16587      return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3];
16588    }; // return a percent-encoded representation of the specified byte range
16589    // @see http://en.wikipedia.org/wiki/Percent-encoding
16590
16591
16592    var percentEncode = function (bytes, start, end) {
16593      var i,
16594          result = '';
16595
16596      for (i = start; i < end; i++) {
16597        result += '%' + ('00' + bytes[i].toString(16)).slice(-2);
16598      }
16599
16600      return result;
16601    }; // return the string representation of the specified byte range,
16602    // interpreted as ISO-8859-1.
16603
16604
16605    var parseIso88591 = function (bytes, start, end) {
16606      return unescape(percentEncode(bytes, start, end)); // jshint ignore:line
16607    };
16608
16609    var parseAdtsSize = function (header, byteIndex) {
16610      var lowThree = (header[byteIndex + 5] & 0xE0) >> 5,
16611          middle = header[byteIndex + 4] << 3,
16612          highTwo = header[byteIndex + 3] & 0x3 << 11;
16613      return highTwo | middle | lowThree;
16614    };
16615
16616    var parseType$5 = function (header, byteIndex) {
16617      if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) {
16618        return 'timed-metadata';
16619      } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) {
16620        return 'audio';
16621      }
16622
16623      return null;
16624    };
16625
16626    var parseSampleRate = function (packet) {
16627      var i = 0;
16628
16629      while (i + 5 < packet.length) {
16630        if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) {
16631          // If a valid header was not found,  jump one forward and attempt to
16632          // find a valid ADTS header starting at the next byte
16633          i++;
16634          continue;
16635        }
16636
16637        return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2];
16638      }
16639
16640      return null;
16641    };
16642
16643    var parseAacTimestamp = function (packet) {
16644      var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag
16645
16646      frameStart = 10;
16647
16648      if (packet[5] & 0x40) {
16649        // advance the frame start past the extended header
16650        frameStart += 4; // header size field
16651
16652        frameStart += parseSyncSafeInteger(packet.subarray(10, 14));
16653      } // parse one or more ID3 frames
16654      // http://id3.org/id3v2.3.0#ID3v2_frame_overview
16655
16656
16657      do {
16658        // determine the number of bytes in this frame
16659        frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8));
16660
16661        if (frameSize < 1) {
16662          return null;
16663        }
16664
16665        frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]);
16666
16667        if (frameHeader === 'PRIV') {
16668          frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10);
16669
16670          for (var i = 0; i < frame.byteLength; i++) {
16671            if (frame[i] === 0) {
16672              var owner = parseIso88591(frame, 0, i);
16673
16674              if (owner === 'com.apple.streaming.transportStreamTimestamp') {
16675                var d = frame.subarray(i + 1);
16676                var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2;
16677                size *= 4;
16678                size += d[7] & 0x03;
16679                return size;
16680              }
16681
16682              break;
16683            }
16684          }
16685        }
16686
16687        frameStart += 10; // advance past the frame header
16688
16689        frameStart += frameSize; // advance past the frame body
16690      } while (frameStart < packet.byteLength);
16691
16692      return null;
16693    };
16694
16695    var utils = {
16696      isLikelyAacData: isLikelyAacData$1,
16697      parseId3TagSize: parseId3TagSize,
16698      parseAdtsSize: parseAdtsSize,
16699      parseType: parseType$5,
16700      parseSampleRate: parseSampleRate,
16701      parseAacTimestamp: parseAacTimestamp
16702    };
16703    /**
16704     * mux.js
16705     *
16706     * Copyright (c) Brightcove
16707     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
16708     *
16709     * A stream-based aac to mp4 converter. This utility can be used to
16710     * deliver mp4s to a SourceBuffer on platforms that support native
16711     * Media Source Extensions.
16712     */
16713
16714    var Stream$1 = stream;
16715    var aacUtils = utils; // Constants
16716
16717    var AacStream$1;
16718    /**
16719     * Splits an incoming stream of binary data into ADTS and ID3 Frames.
16720     */
16721
16722    AacStream$1 = function () {
16723      var everything = new Uint8Array(),
16724          timeStamp = 0;
16725      AacStream$1.prototype.init.call(this);
16726
16727      this.setTimestamp = function (timestamp) {
16728        timeStamp = timestamp;
16729      };
16730
16731      this.push = function (bytes) {
16732        var frameSize = 0,
16733            byteIndex = 0,
16734            bytesLeft,
16735            chunk,
16736            packet,
16737            tempLength; // If there are bytes remaining from the last segment, prepend them to the
16738        // bytes that were pushed in
16739
16740        if (everything.length) {
16741          tempLength = everything.length;
16742          everything = new Uint8Array(bytes.byteLength + tempLength);
16743          everything.set(everything.subarray(0, tempLength));
16744          everything.set(bytes, tempLength);
16745        } else {
16746          everything = bytes;
16747        }
16748
16749        while (everything.length - byteIndex >= 3) {
16750          if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) {
16751            // Exit early because we don't have enough to parse
16752            // the ID3 tag header
16753            if (everything.length - byteIndex < 10) {
16754              break;
16755            } // check framesize
16756
16757
16758            frameSize = aacUtils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer
16759            // to emit a full packet
16760            // Add to byteIndex to support multiple ID3 tags in sequence
16761
16762            if (byteIndex + frameSize > everything.length) {
16763              break;
16764            }
16765
16766            chunk = {
16767              type: 'timed-metadata',
16768              data: everything.subarray(byteIndex, byteIndex + frameSize)
16769            };
16770            this.trigger('data', chunk);
16771            byteIndex += frameSize;
16772            continue;
16773          } else if ((everything[byteIndex] & 0xff) === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) {
16774            // Exit early because we don't have enough to parse
16775            // the ADTS frame header
16776            if (everything.length - byteIndex < 7) {
16777              break;
16778            }
16779
16780            frameSize = aacUtils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer
16781            // to emit a full packet
16782
16783            if (byteIndex + frameSize > everything.length) {
16784              break;
16785            }
16786
16787            packet = {
16788              type: 'audio',
16789              data: everything.subarray(byteIndex, byteIndex + frameSize),
16790              pts: timeStamp,
16791              dts: timeStamp
16792            };
16793            this.trigger('data', packet);
16794            byteIndex += frameSize;
16795            continue;
16796          }
16797
16798          byteIndex++;
16799        }
16800
16801        bytesLeft = everything.length - byteIndex;
16802
16803        if (bytesLeft > 0) {
16804          everything = everything.subarray(byteIndex);
16805        } else {
16806          everything = new Uint8Array();
16807        }
16808      };
16809
16810      this.reset = function () {
16811        everything = new Uint8Array();
16812        this.trigger('reset');
16813      };
16814
16815      this.endTimeline = function () {
16816        everything = new Uint8Array();
16817        this.trigger('endedtimeline');
16818      };
16819    };
16820
16821    AacStream$1.prototype = new Stream$1();
16822    var aac = AacStream$1;
16823    var AUDIO_PROPERTIES$1 = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize'];
16824    var audioProperties = AUDIO_PROPERTIES$1;
16825    var VIDEO_PROPERTIES$1 = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility', 'sarRatio'];
16826    var videoProperties = VIDEO_PROPERTIES$1;
16827    /**
16828     * mux.js
16829     *
16830     * Copyright (c) Brightcove
16831     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
16832     *
16833     * A stream-based mp2t to mp4 converter. This utility can be used to
16834     * deliver mp4s to a SourceBuffer on platforms that support native
16835     * Media Source Extensions.
16836     */
16837
16838    var Stream = stream;
16839    var mp4 = mp4Generator;
16840    var frameUtils = frameUtils$1;
16841    var audioFrameUtils = audioFrameUtils$1;
16842    var trackDecodeInfo = trackDecodeInfo$1;
16843    var m2ts = m2ts_1;
16844    var clock = clock$2;
16845    var AdtsStream = adts;
16846    var H264Stream = h264.H264Stream;
16847    var AacStream = aac;
16848    var isLikelyAacData = utils.isLikelyAacData;
16849    var ONE_SECOND_IN_TS$1 = clock$2.ONE_SECOND_IN_TS;
16850    var AUDIO_PROPERTIES = audioProperties;
16851    var VIDEO_PROPERTIES = videoProperties; // object types
16852
16853    var VideoSegmentStream, AudioSegmentStream, Transmuxer, CoalesceStream;
16854
16855    var retriggerForStream = function (key, event) {
16856      event.stream = key;
16857      this.trigger('log', event);
16858    };
16859
16860    var addPipelineLogRetriggers = function (transmuxer, pipeline) {
16861      var keys = Object.keys(pipeline);
16862
16863      for (var i = 0; i < keys.length; i++) {
16864        var key = keys[i]; // skip non-stream keys and headOfPipeline
16865        // which is just a duplicate
16866
16867        if (key === 'headOfPipeline' || !pipeline[key].on) {
16868          continue;
16869        }
16870
16871        pipeline[key].on('log', retriggerForStream.bind(transmuxer, key));
16872      }
16873    };
16874    /**
16875     * Compare two arrays (even typed) for same-ness
16876     */
16877
16878
16879    var arrayEquals = function (a, b) {
16880      var i;
16881
16882      if (a.length !== b.length) {
16883        return false;
16884      } // compare the value of each element in the array
16885
16886
16887      for (i = 0; i < a.length; i++) {
16888        if (a[i] !== b[i]) {
16889          return false;
16890        }
16891      }
16892
16893      return true;
16894    };
16895
16896    var generateSegmentTimingInfo = function (baseMediaDecodeTime, startDts, startPts, endDts, endPts, prependedContentDuration) {
16897      var ptsOffsetFromDts = startPts - startDts,
16898          decodeDuration = endDts - startDts,
16899          presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment,
16900      // however, the player time values will reflect a start from the baseMediaDecodeTime.
16901      // In order to provide relevant values for the player times, base timing info on the
16902      // baseMediaDecodeTime and the DTS and PTS durations of the segment.
16903
16904      return {
16905        start: {
16906          dts: baseMediaDecodeTime,
16907          pts: baseMediaDecodeTime + ptsOffsetFromDts
16908        },
16909        end: {
16910          dts: baseMediaDecodeTime + decodeDuration,
16911          pts: baseMediaDecodeTime + presentationDuration
16912        },
16913        prependedContentDuration: prependedContentDuration,
16914        baseMediaDecodeTime: baseMediaDecodeTime
16915      };
16916    };
16917    /**
16918     * Constructs a single-track, ISO BMFF media segment from AAC data
16919     * events. The output of this stream can be fed to a SourceBuffer
16920     * configured with a suitable initialization segment.
16921     * @param track {object} track metadata configuration
16922     * @param options {object} transmuxer options object
16923     * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
16924     *        in the source; false to adjust the first segment to start at 0.
16925     */
16926
16927
16928    AudioSegmentStream = function (track, options) {
16929      var adtsFrames = [],
16930          sequenceNumber,
16931          earliestAllowedDts = 0,
16932          audioAppendStartTs = 0,
16933          videoBaseMediaDecodeTime = Infinity;
16934      options = options || {};
16935      sequenceNumber = options.firstSequenceNumber || 0;
16936      AudioSegmentStream.prototype.init.call(this);
16937
16938      this.push = function (data) {
16939        trackDecodeInfo.collectDtsInfo(track, data);
16940
16941        if (track) {
16942          AUDIO_PROPERTIES.forEach(function (prop) {
16943            track[prop] = data[prop];
16944          });
16945        } // buffer audio data until end() is called
16946
16947
16948        adtsFrames.push(data);
16949      };
16950
16951      this.setEarliestDts = function (earliestDts) {
16952        earliestAllowedDts = earliestDts;
16953      };
16954
16955      this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) {
16956        videoBaseMediaDecodeTime = baseMediaDecodeTime;
16957      };
16958
16959      this.setAudioAppendStart = function (timestamp) {
16960        audioAppendStartTs = timestamp;
16961      };
16962
16963      this.flush = function () {
16964        var frames, moof, mdat, boxes, frameDuration, segmentDuration, videoClockCyclesOfSilencePrefixed; // return early if no audio data has been observed
16965
16966        if (adtsFrames.length === 0) {
16967          this.trigger('done', 'AudioSegmentStream');
16968          return;
16969        }
16970
16971        frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts);
16972        track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); // amount of audio filled but the value is in video clock rather than audio clock
16973
16974        videoClockCyclesOfSilencePrefixed = audioFrameUtils.prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime); // we have to build the index from byte locations to
16975        // samples (that is, adts frames) in the audio data
16976
16977        track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat
16978
16979        mdat = mp4.mdat(audioFrameUtils.concatenateFrameData(frames));
16980        adtsFrames = [];
16981        moof = mp4.moof(sequenceNumber, [track]);
16982        boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time
16983
16984        sequenceNumber++;
16985        boxes.set(moof);
16986        boxes.set(mdat, moof.byteLength);
16987        trackDecodeInfo.clearDtsInfo(track);
16988        frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 * 1024 / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with
16989        // tests) on adding the timingInfo event. However, it seems unlikely that there's a
16990        // valid use-case where an init segment/data should be triggered without associated
16991        // frames. Leaving for now, but should be looked into.
16992
16993        if (frames.length) {
16994          segmentDuration = frames.length * frameDuration;
16995          this.trigger('segmentTimingInfo', generateSegmentTimingInfo( // The audio track's baseMediaDecodeTime is in audio clock cycles, but the
16996          // frame info is in video clock cycles. Convert to match expectation of
16997          // listeners (that all timestamps will be based on video clock cycles).
16998          clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate), // frame times are already in video clock, as is segment duration
16999          frames[0].dts, frames[0].pts, frames[0].dts + segmentDuration, frames[0].pts + segmentDuration, videoClockCyclesOfSilencePrefixed || 0));
17000          this.trigger('timingInfo', {
17001            start: frames[0].pts,
17002            end: frames[0].pts + segmentDuration
17003          });
17004        }
17005
17006        this.trigger('data', {
17007          track: track,
17008          boxes: boxes
17009        });
17010        this.trigger('done', 'AudioSegmentStream');
17011      };
17012
17013      this.reset = function () {
17014        trackDecodeInfo.clearDtsInfo(track);
17015        adtsFrames = [];
17016        this.trigger('reset');
17017      };
17018    };
17019
17020    AudioSegmentStream.prototype = new Stream();
17021    /**
17022     * Constructs a single-track, ISO BMFF media segment from H264 data
17023     * events. The output of this stream can be fed to a SourceBuffer
17024     * configured with a suitable initialization segment.
17025     * @param track {object} track metadata configuration
17026     * @param options {object} transmuxer options object
17027     * @param options.alignGopsAtEnd {boolean} If true, start from the end of the
17028     *        gopsToAlignWith list when attempting to align gop pts
17029     * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
17030     *        in the source; false to adjust the first segment to start at 0.
17031     */
17032
17033    VideoSegmentStream = function (track, options) {
17034      var sequenceNumber,
17035          nalUnits = [],
17036          gopsToAlignWith = [],
17037          config,
17038          pps;
17039      options = options || {};
17040      sequenceNumber = options.firstSequenceNumber || 0;
17041      VideoSegmentStream.prototype.init.call(this);
17042      delete track.minPTS;
17043      this.gopCache_ = [];
17044      /**
17045        * Constructs a ISO BMFF segment given H264 nalUnits
17046        * @param {Object} nalUnit A data event representing a nalUnit
17047        * @param {String} nalUnit.nalUnitType
17048        * @param {Object} nalUnit.config Properties for a mp4 track
17049        * @param {Uint8Array} nalUnit.data The nalUnit bytes
17050        * @see lib/codecs/h264.js
17051       **/
17052
17053      this.push = function (nalUnit) {
17054        trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config
17055
17056        if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) {
17057          config = nalUnit.config;
17058          track.sps = [nalUnit.data];
17059          VIDEO_PROPERTIES.forEach(function (prop) {
17060            track[prop] = config[prop];
17061          }, this);
17062        }
17063
17064        if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) {
17065          pps = nalUnit.data;
17066          track.pps = [nalUnit.data];
17067        } // buffer video until flush() is called
17068
17069
17070        nalUnits.push(nalUnit);
17071      };
17072      /**
17073        * Pass constructed ISO BMFF track and boxes on to the
17074        * next stream in the pipeline
17075       **/
17076
17077
17078      this.flush = function () {
17079        var frames,
17080            gopForFusion,
17081            gops,
17082            moof,
17083            mdat,
17084            boxes,
17085            prependedContentDuration = 0,
17086            firstGop,
17087            lastGop; // Throw away nalUnits at the start of the byte stream until
17088        // we find the first AUD
17089
17090        while (nalUnits.length) {
17091          if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') {
17092            break;
17093          }
17094
17095          nalUnits.shift();
17096        } // Return early if no video data has been observed
17097
17098
17099        if (nalUnits.length === 0) {
17100          this.resetStream_();
17101          this.trigger('done', 'VideoSegmentStream');
17102          return;
17103        } // Organize the raw nal-units into arrays that represent
17104        // higher-level constructs such as frames and gops
17105        // (group-of-pictures)
17106
17107
17108        frames = frameUtils.groupNalsIntoFrames(nalUnits);
17109        gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have
17110        // a problem since MSE (on Chrome) requires a leading keyframe.
17111        //
17112        // We have two approaches to repairing this situation:
17113        // 1) GOP-FUSION:
17114        //    This is where we keep track of the GOPS (group-of-pictures)
17115        //    from previous fragments and attempt to find one that we can
17116        //    prepend to the current fragment in order to create a valid
17117        //    fragment.
17118        // 2) KEYFRAME-PULLING:
17119        //    Here we search for the first keyframe in the fragment and
17120        //    throw away all the frames between the start of the fragment
17121        //    and that keyframe. We then extend the duration and pull the
17122        //    PTS of the keyframe forward so that it covers the time range
17123        //    of the frames that were disposed of.
17124        //
17125        // #1 is far prefereable over #2 which can cause "stuttering" but
17126        // requires more things to be just right.
17127
17128        if (!gops[0][0].keyFrame) {
17129          // Search for a gop for fusion from our gopCache
17130          gopForFusion = this.getGopForFusion_(nalUnits[0], track);
17131
17132          if (gopForFusion) {
17133            // in order to provide more accurate timing information about the segment, save
17134            // the number of seconds prepended to the original segment due to GOP fusion
17135            prependedContentDuration = gopForFusion.duration;
17136            gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the
17137            // new gop at the beginning
17138
17139            gops.byteLength += gopForFusion.byteLength;
17140            gops.nalCount += gopForFusion.nalCount;
17141            gops.pts = gopForFusion.pts;
17142            gops.dts = gopForFusion.dts;
17143            gops.duration += gopForFusion.duration;
17144          } else {
17145            // If we didn't find a candidate gop fall back to keyframe-pulling
17146            gops = frameUtils.extendFirstKeyFrame(gops);
17147          }
17148        } // Trim gops to align with gopsToAlignWith
17149
17150
17151        if (gopsToAlignWith.length) {
17152          var alignedGops;
17153
17154          if (options.alignGopsAtEnd) {
17155            alignedGops = this.alignGopsAtEnd_(gops);
17156          } else {
17157            alignedGops = this.alignGopsAtStart_(gops);
17158          }
17159
17160          if (!alignedGops) {
17161            // save all the nals in the last GOP into the gop cache
17162            this.gopCache_.unshift({
17163              gop: gops.pop(),
17164              pps: track.pps,
17165              sps: track.sps
17166            }); // Keep a maximum of 6 GOPs in the cache
17167
17168            this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits
17169
17170            nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith
17171
17172            this.resetStream_();
17173            this.trigger('done', 'VideoSegmentStream');
17174            return;
17175          } // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct
17176          // when recalculated before sending off to CoalesceStream
17177
17178
17179          trackDecodeInfo.clearDtsInfo(track);
17180          gops = alignedGops;
17181        }
17182
17183        trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to
17184        // samples (that is, frames) in the video data
17185
17186        track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat
17187
17188        mdat = mp4.mdat(frameUtils.concatenateNalData(gops));
17189        track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps);
17190        this.trigger('processedGopsInfo', gops.map(function (gop) {
17191          return {
17192            pts: gop.pts,
17193            dts: gop.dts,
17194            byteLength: gop.byteLength
17195          };
17196        }));
17197        firstGop = gops[0];
17198        lastGop = gops[gops.length - 1];
17199        this.trigger('segmentTimingInfo', generateSegmentTimingInfo(track.baseMediaDecodeTime, firstGop.dts, firstGop.pts, lastGop.dts + lastGop.duration, lastGop.pts + lastGop.duration, prependedContentDuration));
17200        this.trigger('timingInfo', {
17201          start: gops[0].pts,
17202          end: gops[gops.length - 1].pts + gops[gops.length - 1].duration
17203        }); // save all the nals in the last GOP into the gop cache
17204
17205        this.gopCache_.unshift({
17206          gop: gops.pop(),
17207          pps: track.pps,
17208          sps: track.sps
17209        }); // Keep a maximum of 6 GOPs in the cache
17210
17211        this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits
17212
17213        nalUnits = [];
17214        this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime);
17215        this.trigger('timelineStartInfo', track.timelineStartInfo);
17216        moof = mp4.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of
17217        // throwing away hundreds of media segment fragments
17218
17219        boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time
17220
17221        sequenceNumber++;
17222        boxes.set(moof);
17223        boxes.set(mdat, moof.byteLength);
17224        this.trigger('data', {
17225          track: track,
17226          boxes: boxes
17227        });
17228        this.resetStream_(); // Continue with the flush process now
17229
17230        this.trigger('done', 'VideoSegmentStream');
17231      };
17232
17233      this.reset = function () {
17234        this.resetStream_();
17235        nalUnits = [];
17236        this.gopCache_.length = 0;
17237        gopsToAlignWith.length = 0;
17238        this.trigger('reset');
17239      };
17240
17241      this.resetStream_ = function () {
17242        trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments
17243        // for instance, when we are rendition switching
17244
17245        config = undefined;
17246        pps = undefined;
17247      }; // Search for a candidate Gop for gop-fusion from the gop cache and
17248      // return it or return null if no good candidate was found
17249
17250
17251      this.getGopForFusion_ = function (nalUnit) {
17252        var halfSecond = 45000,
17253            // Half-a-second in a 90khz clock
17254        allowableOverlap = 10000,
17255            // About 3 frames @ 30fps
17256        nearestDistance = Infinity,
17257            dtsDistance,
17258            nearestGopObj,
17259            currentGop,
17260            currentGopObj,
17261            i; // Search for the GOP nearest to the beginning of this nal unit
17262
17263        for (i = 0; i < this.gopCache_.length; i++) {
17264          currentGopObj = this.gopCache_[i];
17265          currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS
17266
17267          if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) {
17268            continue;
17269          } // Reject Gops that would require a negative baseMediaDecodeTime
17270
17271
17272          if (currentGop.dts < track.timelineStartInfo.dts) {
17273            continue;
17274          } // The distance between the end of the gop and the start of the nalUnit
17275
17276
17277          dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within
17278          // a half-second of the nal unit
17279
17280          if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) {
17281            // Always use the closest GOP we found if there is more than
17282            // one candidate
17283            if (!nearestGopObj || nearestDistance > dtsDistance) {
17284              nearestGopObj = currentGopObj;
17285              nearestDistance = dtsDistance;
17286            }
17287          }
17288        }
17289
17290        if (nearestGopObj) {
17291          return nearestGopObj.gop;
17292        }
17293
17294        return null;
17295      }; // trim gop list to the first gop found that has a matching pts with a gop in the list
17296      // of gopsToAlignWith starting from the START of the list
17297
17298
17299      this.alignGopsAtStart_ = function (gops) {
17300        var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops;
17301        byteLength = gops.byteLength;
17302        nalCount = gops.nalCount;
17303        duration = gops.duration;
17304        alignIndex = gopIndex = 0;
17305
17306        while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) {
17307          align = gopsToAlignWith[alignIndex];
17308          gop = gops[gopIndex];
17309
17310          if (align.pts === gop.pts) {
17311            break;
17312          }
17313
17314          if (gop.pts > align.pts) {
17315            // this current gop starts after the current gop we want to align on, so increment
17316            // align index
17317            alignIndex++;
17318            continue;
17319          } // current gop starts before the current gop we want to align on. so increment gop
17320          // index
17321
17322
17323          gopIndex++;
17324          byteLength -= gop.byteLength;
17325          nalCount -= gop.nalCount;
17326          duration -= gop.duration;
17327        }
17328
17329        if (gopIndex === 0) {
17330          // no gops to trim
17331          return gops;
17332        }
17333
17334        if (gopIndex === gops.length) {
17335          // all gops trimmed, skip appending all gops
17336          return null;
17337        }
17338
17339        alignedGops = gops.slice(gopIndex);
17340        alignedGops.byteLength = byteLength;
17341        alignedGops.duration = duration;
17342        alignedGops.nalCount = nalCount;
17343        alignedGops.pts = alignedGops[0].pts;
17344        alignedGops.dts = alignedGops[0].dts;
17345        return alignedGops;
17346      }; // trim gop list to the first gop found that has a matching pts with a gop in the list
17347      // of gopsToAlignWith starting from the END of the list
17348
17349
17350      this.alignGopsAtEnd_ = function (gops) {
17351        var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound;
17352        alignIndex = gopsToAlignWith.length - 1;
17353        gopIndex = gops.length - 1;
17354        alignEndIndex = null;
17355        matchFound = false;
17356
17357        while (alignIndex >= 0 && gopIndex >= 0) {
17358          align = gopsToAlignWith[alignIndex];
17359          gop = gops[gopIndex];
17360
17361          if (align.pts === gop.pts) {
17362            matchFound = true;
17363            break;
17364          }
17365
17366          if (align.pts > gop.pts) {
17367            alignIndex--;
17368            continue;
17369          }
17370
17371          if (alignIndex === gopsToAlignWith.length - 1) {
17372            // gop.pts is greater than the last alignment candidate. If no match is found
17373            // by the end of this loop, we still want to append gops that come after this
17374            // point
17375            alignEndIndex = gopIndex;
17376          }
17377
17378          gopIndex--;
17379        }
17380
17381        if (!matchFound && alignEndIndex === null) {
17382          return null;
17383        }
17384
17385        var trimIndex;
17386
17387        if (matchFound) {
17388          trimIndex = gopIndex;
17389        } else {
17390          trimIndex = alignEndIndex;
17391        }
17392
17393        if (trimIndex === 0) {
17394          return gops;
17395        }
17396
17397        var alignedGops = gops.slice(trimIndex);
17398        var metadata = alignedGops.reduce(function (total, gop) {
17399          total.byteLength += gop.byteLength;
17400          total.duration += gop.duration;
17401          total.nalCount += gop.nalCount;
17402          return total;
17403        }, {
17404          byteLength: 0,
17405          duration: 0,
17406          nalCount: 0
17407        });
17408        alignedGops.byteLength = metadata.byteLength;
17409        alignedGops.duration = metadata.duration;
17410        alignedGops.nalCount = metadata.nalCount;
17411        alignedGops.pts = alignedGops[0].pts;
17412        alignedGops.dts = alignedGops[0].dts;
17413        return alignedGops;
17414      };
17415
17416      this.alignGopsWith = function (newGopsToAlignWith) {
17417        gopsToAlignWith = newGopsToAlignWith;
17418      };
17419    };
17420
17421    VideoSegmentStream.prototype = new Stream();
17422    /**
17423     * A Stream that can combine multiple streams (ie. audio & video)
17424     * into a single output segment for MSE. Also supports audio-only
17425     * and video-only streams.
17426     * @param options {object} transmuxer options object
17427     * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps
17428     *        in the source; false to adjust the first segment to start at media timeline start.
17429     */
17430
17431    CoalesceStream = function (options, metadataStream) {
17432      // Number of Tracks per output segment
17433      // If greater than 1, we combine multiple
17434      // tracks into a single segment
17435      this.numberOfTracks = 0;
17436      this.metadataStream = metadataStream;
17437      options = options || {};
17438
17439      if (typeof options.remux !== 'undefined') {
17440        this.remuxTracks = !!options.remux;
17441      } else {
17442        this.remuxTracks = true;
17443      }
17444
17445      if (typeof options.keepOriginalTimestamps === 'boolean') {
17446        this.keepOriginalTimestamps = options.keepOriginalTimestamps;
17447      } else {
17448        this.keepOriginalTimestamps = false;
17449      }
17450
17451      this.pendingTracks = [];
17452      this.videoTrack = null;
17453      this.pendingBoxes = [];
17454      this.pendingCaptions = [];
17455      this.pendingMetadata = [];
17456      this.pendingBytes = 0;
17457      this.emittedTracks = 0;
17458      CoalesceStream.prototype.init.call(this); // Take output from multiple
17459
17460      this.push = function (output) {
17461        // buffer incoming captions until the associated video segment
17462        // finishes
17463        if (output.content || output.text) {
17464          return this.pendingCaptions.push(output);
17465        } // buffer incoming id3 tags until the final flush
17466
17467
17468        if (output.frames) {
17469          return this.pendingMetadata.push(output);
17470        } // Add this track to the list of pending tracks and store
17471        // important information required for the construction of
17472        // the final segment
17473
17474
17475        this.pendingTracks.push(output.track);
17476        this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome?
17477        // We unshift audio and push video because
17478        // as of Chrome 75 when switching from
17479        // one init segment to another if the video
17480        // mdat does not appear after the audio mdat
17481        // only audio will play for the duration of our transmux.
17482
17483        if (output.track.type === 'video') {
17484          this.videoTrack = output.track;
17485          this.pendingBoxes.push(output.boxes);
17486        }
17487
17488        if (output.track.type === 'audio') {
17489          this.audioTrack = output.track;
17490          this.pendingBoxes.unshift(output.boxes);
17491        }
17492      };
17493    };
17494
17495    CoalesceStream.prototype = new Stream();
17496
17497    CoalesceStream.prototype.flush = function (flushSource) {
17498      var offset = 0,
17499          event = {
17500        captions: [],
17501        captionStreams: {},
17502        metadata: [],
17503        info: {}
17504      },
17505          caption,
17506          id3,
17507          initSegment,
17508          timelineStartPts = 0,
17509          i;
17510
17511      if (this.pendingTracks.length < this.numberOfTracks) {
17512        if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') {
17513          // Return because we haven't received a flush from a data-generating
17514          // portion of the segment (meaning that we have only recieved meta-data
17515          // or captions.)
17516          return;
17517        } else if (this.remuxTracks) {
17518          // Return until we have enough tracks from the pipeline to remux (if we
17519          // are remuxing audio and video into a single MP4)
17520          return;
17521        } else if (this.pendingTracks.length === 0) {
17522          // In the case where we receive a flush without any data having been
17523          // received we consider it an emitted track for the purposes of coalescing
17524          // `done` events.
17525          // We do this for the case where there is an audio and video track in the
17526          // segment but no audio data. (seen in several playlists with alternate
17527          // audio tracks and no audio present in the main TS segments.)
17528          this.emittedTracks++;
17529
17530          if (this.emittedTracks >= this.numberOfTracks) {
17531            this.trigger('done');
17532            this.emittedTracks = 0;
17533          }
17534
17535          return;
17536        }
17537      }
17538
17539      if (this.videoTrack) {
17540        timelineStartPts = this.videoTrack.timelineStartInfo.pts;
17541        VIDEO_PROPERTIES.forEach(function (prop) {
17542          event.info[prop] = this.videoTrack[prop];
17543        }, this);
17544      } else if (this.audioTrack) {
17545        timelineStartPts = this.audioTrack.timelineStartInfo.pts;
17546        AUDIO_PROPERTIES.forEach(function (prop) {
17547          event.info[prop] = this.audioTrack[prop];
17548        }, this);
17549      }
17550
17551      if (this.videoTrack || this.audioTrack) {
17552        if (this.pendingTracks.length === 1) {
17553          event.type = this.pendingTracks[0].type;
17554        } else {
17555          event.type = 'combined';
17556        }
17557
17558        this.emittedTracks += this.pendingTracks.length;
17559        initSegment = mp4.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment
17560
17561        event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov
17562        // and track definitions
17563
17564        event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats
17565
17566        event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together
17567
17568        for (i = 0; i < this.pendingBoxes.length; i++) {
17569          event.data.set(this.pendingBoxes[i], offset);
17570          offset += this.pendingBoxes[i].byteLength;
17571        } // Translate caption PTS times into second offsets to match the
17572        // video timeline for the segment, and add track info
17573
17574
17575        for (i = 0; i < this.pendingCaptions.length; i++) {
17576          caption = this.pendingCaptions[i];
17577          caption.startTime = clock.metadataTsToSeconds(caption.startPts, timelineStartPts, this.keepOriginalTimestamps);
17578          caption.endTime = clock.metadataTsToSeconds(caption.endPts, timelineStartPts, this.keepOriginalTimestamps);
17579          event.captionStreams[caption.stream] = true;
17580          event.captions.push(caption);
17581        } // Translate ID3 frame PTS times into second offsets to match the
17582        // video timeline for the segment
17583
17584
17585        for (i = 0; i < this.pendingMetadata.length; i++) {
17586          id3 = this.pendingMetadata[i];
17587          id3.cueTime = clock.metadataTsToSeconds(id3.pts, timelineStartPts, this.keepOriginalTimestamps);
17588          event.metadata.push(id3);
17589        } // We add this to every single emitted segment even though we only need
17590        // it for the first
17591
17592
17593        event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state
17594
17595        this.pendingTracks.length = 0;
17596        this.videoTrack = null;
17597        this.pendingBoxes.length = 0;
17598        this.pendingCaptions.length = 0;
17599        this.pendingBytes = 0;
17600        this.pendingMetadata.length = 0; // Emit the built segment
17601        // We include captions and ID3 tags for backwards compatibility,
17602        // ideally we should send only video and audio in the data event
17603
17604        this.trigger('data', event); // Emit each caption to the outside world
17605        // Ideally, this would happen immediately on parsing captions,
17606        // but we need to ensure that video data is sent back first
17607        // so that caption timing can be adjusted to match video timing
17608
17609        for (i = 0; i < event.captions.length; i++) {
17610          caption = event.captions[i];
17611          this.trigger('caption', caption);
17612        } // Emit each id3 tag to the outside world
17613        // Ideally, this would happen immediately on parsing the tag,
17614        // but we need to ensure that video data is sent back first
17615        // so that ID3 frame timing can be adjusted to match video timing
17616
17617
17618        for (i = 0; i < event.metadata.length; i++) {
17619          id3 = event.metadata[i];
17620          this.trigger('id3Frame', id3);
17621        }
17622      } // Only emit `done` if all tracks have been flushed and emitted
17623
17624
17625      if (this.emittedTracks >= this.numberOfTracks) {
17626        this.trigger('done');
17627        this.emittedTracks = 0;
17628      }
17629    };
17630
17631    CoalesceStream.prototype.setRemux = function (val) {
17632      this.remuxTracks = val;
17633    };
17634    /**
17635     * A Stream that expects MP2T binary data as input and produces
17636     * corresponding media segments, suitable for use with Media Source
17637     * Extension (MSE) implementations that support the ISO BMFF byte
17638     * stream format, like Chrome.
17639     */
17640
17641
17642    Transmuxer = function (options) {
17643      var self = this,
17644          hasFlushed = true,
17645          videoTrack,
17646          audioTrack;
17647      Transmuxer.prototype.init.call(this);
17648      options = options || {};
17649      this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0;
17650      this.transmuxPipeline_ = {};
17651
17652      this.setupAacPipeline = function () {
17653        var pipeline = {};
17654        this.transmuxPipeline_ = pipeline;
17655        pipeline.type = 'aac';
17656        pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline
17657
17658        pipeline.aacStream = new AacStream();
17659        pipeline.audioTimestampRolloverStream = new m2ts.TimestampRolloverStream('audio');
17660        pipeline.timedMetadataTimestampRolloverStream = new m2ts.TimestampRolloverStream('timed-metadata');
17661        pipeline.adtsStream = new AdtsStream();
17662        pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream);
17663        pipeline.headOfPipeline = pipeline.aacStream;
17664        pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream);
17665        pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream);
17666        pipeline.metadataStream.on('timestamp', function (frame) {
17667          pipeline.aacStream.setTimestamp(frame.timeStamp);
17668        });
17669        pipeline.aacStream.on('data', function (data) {
17670          if (data.type !== 'timed-metadata' && data.type !== 'audio' || pipeline.audioSegmentStream) {
17671            return;
17672          }
17673
17674          audioTrack = audioTrack || {
17675            timelineStartInfo: {
17676              baseMediaDecodeTime: self.baseMediaDecodeTime
17677            },
17678            codec: 'adts',
17679            type: 'audio'
17680          }; // hook up the audio segment stream to the first track with aac data
17681
17682          pipeline.coalesceStream.numberOfTracks++;
17683          pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options);
17684          pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream'));
17685          pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline
17686
17687          pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); // emit pmt info
17688
17689          self.trigger('trackinfo', {
17690            hasAudio: !!audioTrack,
17691            hasVideo: !!videoTrack
17692          });
17693        }); // Re-emit any data coming from the coalesce stream to the outside world
17694
17695        pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline
17696
17697        pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
17698        addPipelineLogRetriggers(this, pipeline);
17699      };
17700
17701      this.setupTsPipeline = function () {
17702        var pipeline = {};
17703        this.transmuxPipeline_ = pipeline;
17704        pipeline.type = 'ts';
17705        pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline
17706
17707        pipeline.packetStream = new m2ts.TransportPacketStream();
17708        pipeline.parseStream = new m2ts.TransportParseStream();
17709        pipeline.elementaryStream = new m2ts.ElementaryStream();
17710        pipeline.timestampRolloverStream = new m2ts.TimestampRolloverStream();
17711        pipeline.adtsStream = new AdtsStream();
17712        pipeline.h264Stream = new H264Stream();
17713        pipeline.captionStream = new m2ts.CaptionStream(options);
17714        pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream);
17715        pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams
17716
17717        pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream).pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!!
17718        // demux the streams
17719
17720        pipeline.timestampRolloverStream.pipe(pipeline.h264Stream);
17721        pipeline.timestampRolloverStream.pipe(pipeline.adtsStream);
17722        pipeline.timestampRolloverStream.pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream
17723
17724        pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream);
17725        pipeline.elementaryStream.on('data', function (data) {
17726          var i;
17727
17728          if (data.type === 'metadata') {
17729            i = data.tracks.length; // scan the tracks listed in the metadata
17730
17731            while (i--) {
17732              if (!videoTrack && data.tracks[i].type === 'video') {
17733                videoTrack = data.tracks[i];
17734                videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
17735              } else if (!audioTrack && data.tracks[i].type === 'audio') {
17736                audioTrack = data.tracks[i];
17737                audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime;
17738              }
17739            } // hook up the video segment stream to the first track with h264 data
17740
17741
17742            if (videoTrack && !pipeline.videoSegmentStream) {
17743              pipeline.coalesceStream.numberOfTracks++;
17744              pipeline.videoSegmentStream = new VideoSegmentStream(videoTrack, options);
17745              pipeline.videoSegmentStream.on('log', self.getLogTrigger_('videoSegmentStream'));
17746              pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) {
17747                // When video emits timelineStartInfo data after a flush, we forward that
17748                // info to the AudioSegmentStream, if it exists, because video timeline
17749                // data takes precedence.  Do not do this if keepOriginalTimestamps is set,
17750                // because this is a particularly subtle form of timestamp alteration.
17751                if (audioTrack && !options.keepOriginalTimestamps) {
17752                  audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the
17753                  // very earliest DTS we have seen in video because Chrome will
17754                  // interpret any video track with a baseMediaDecodeTime that is
17755                  // non-zero as a gap.
17756
17757                  pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts - self.baseMediaDecodeTime);
17758                }
17759              });
17760              pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo'));
17761              pipeline.videoSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'videoSegmentTimingInfo'));
17762              pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) {
17763                if (audioTrack) {
17764                  pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime);
17765                }
17766              });
17767              pipeline.videoSegmentStream.on('timingInfo', self.trigger.bind(self, 'videoTimingInfo')); // Set up the final part of the video pipeline
17768
17769              pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream);
17770            }
17771
17772            if (audioTrack && !pipeline.audioSegmentStream) {
17773              // hook up the audio segment stream to the first track with aac data
17774              pipeline.coalesceStream.numberOfTracks++;
17775              pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options);
17776              pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream'));
17777              pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo'));
17778              pipeline.audioSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'audioSegmentTimingInfo')); // Set up the final part of the audio pipeline
17779
17780              pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream);
17781            } // emit pmt info
17782
17783
17784            self.trigger('trackinfo', {
17785              hasAudio: !!audioTrack,
17786              hasVideo: !!videoTrack
17787            });
17788          }
17789        }); // Re-emit any data coming from the coalesce stream to the outside world
17790
17791        pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data'));
17792        pipeline.coalesceStream.on('id3Frame', function (id3Frame) {
17793          id3Frame.dispatchType = pipeline.metadataStream.dispatchType;
17794          self.trigger('id3Frame', id3Frame);
17795        });
17796        pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline
17797
17798        pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done'));
17799        addPipelineLogRetriggers(this, pipeline);
17800      }; // hook up the segment streams once track metadata is delivered
17801
17802
17803      this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) {
17804        var pipeline = this.transmuxPipeline_;
17805
17806        if (!options.keepOriginalTimestamps) {
17807          this.baseMediaDecodeTime = baseMediaDecodeTime;
17808        }
17809
17810        if (audioTrack) {
17811          audioTrack.timelineStartInfo.dts = undefined;
17812          audioTrack.timelineStartInfo.pts = undefined;
17813          trackDecodeInfo.clearDtsInfo(audioTrack);
17814
17815          if (pipeline.audioTimestampRolloverStream) {
17816            pipeline.audioTimestampRolloverStream.discontinuity();
17817          }
17818        }
17819
17820        if (videoTrack) {
17821          if (pipeline.videoSegmentStream) {
17822            pipeline.videoSegmentStream.gopCache_ = [];
17823          }
17824
17825          videoTrack.timelineStartInfo.dts = undefined;
17826          videoTrack.timelineStartInfo.pts = undefined;
17827          trackDecodeInfo.clearDtsInfo(videoTrack);
17828          pipeline.captionStream.reset();
17829        }
17830
17831        if (pipeline.timestampRolloverStream) {
17832          pipeline.timestampRolloverStream.discontinuity();
17833        }
17834      };
17835
17836      this.setAudioAppendStart = function (timestamp) {
17837        if (audioTrack) {
17838          this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp);
17839        }
17840      };
17841
17842      this.setRemux = function (val) {
17843        var pipeline = this.transmuxPipeline_;
17844        options.remux = val;
17845
17846        if (pipeline && pipeline.coalesceStream) {
17847          pipeline.coalesceStream.setRemux(val);
17848        }
17849      };
17850
17851      this.alignGopsWith = function (gopsToAlignWith) {
17852        if (videoTrack && this.transmuxPipeline_.videoSegmentStream) {
17853          this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith);
17854        }
17855      };
17856
17857      this.getLogTrigger_ = function (key) {
17858        var self = this;
17859        return function (event) {
17860          event.stream = key;
17861          self.trigger('log', event);
17862        };
17863      }; // feed incoming data to the front of the parsing pipeline
17864
17865
17866      this.push = function (data) {
17867        if (hasFlushed) {
17868          var isAac = isLikelyAacData(data);
17869
17870          if (isAac && this.transmuxPipeline_.type !== 'aac') {
17871            this.setupAacPipeline();
17872          } else if (!isAac && this.transmuxPipeline_.type !== 'ts') {
17873            this.setupTsPipeline();
17874          }
17875
17876          hasFlushed = false;
17877        }
17878
17879        this.transmuxPipeline_.headOfPipeline.push(data);
17880      }; // flush any buffered data
17881
17882
17883      this.flush = function () {
17884        hasFlushed = true; // Start at the top of the pipeline and flush all pending work
17885
17886        this.transmuxPipeline_.headOfPipeline.flush();
17887      };
17888
17889      this.endTimeline = function () {
17890        this.transmuxPipeline_.headOfPipeline.endTimeline();
17891      };
17892
17893      this.reset = function () {
17894        if (this.transmuxPipeline_.headOfPipeline) {
17895          this.transmuxPipeline_.headOfPipeline.reset();
17896        }
17897      }; // Caption data has to be reset when seeking outside buffered range
17898
17899
17900      this.resetCaptions = function () {
17901        if (this.transmuxPipeline_.captionStream) {
17902          this.transmuxPipeline_.captionStream.reset();
17903        }
17904      };
17905    };
17906
17907    Transmuxer.prototype = new Stream();
17908    var transmuxer = {
17909      Transmuxer: Transmuxer,
17910      VideoSegmentStream: VideoSegmentStream,
17911      AudioSegmentStream: AudioSegmentStream,
17912      AUDIO_PROPERTIES: AUDIO_PROPERTIES,
17913      VIDEO_PROPERTIES: VIDEO_PROPERTIES,
17914      // exported for testing
17915      generateSegmentTimingInfo: generateSegmentTimingInfo
17916    };
17917    /**
17918     * mux.js
17919     *
17920     * Copyright (c) Brightcove
17921     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
17922     */
17923
17924    var toUnsigned$3 = function (value) {
17925      return value >>> 0;
17926    };
17927
17928    var toHexString$1 = function (value) {
17929      return ('00' + value.toString(16)).slice(-2);
17930    };
17931
17932    var bin = {
17933      toUnsigned: toUnsigned$3,
17934      toHexString: toHexString$1
17935    };
17936
17937    var parseType$4 = function (buffer) {
17938      var result = '';
17939      result += String.fromCharCode(buffer[0]);
17940      result += String.fromCharCode(buffer[1]);
17941      result += String.fromCharCode(buffer[2]);
17942      result += String.fromCharCode(buffer[3]);
17943      return result;
17944    };
17945
17946    var parseType_1 = parseType$4;
17947    var toUnsigned$2 = bin.toUnsigned;
17948    var parseType$3 = parseType_1;
17949
17950    var findBox$5 = function (data, path) {
17951      var results = [],
17952          i,
17953          size,
17954          type,
17955          end,
17956          subresults;
17957
17958      if (!path.length) {
17959        // short-circuit the search for empty paths
17960        return null;
17961      }
17962
17963      for (i = 0; i < data.byteLength;) {
17964        size = toUnsigned$2(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]);
17965        type = parseType$3(data.subarray(i + 4, i + 8));
17966        end = size > 1 ? i + size : data.byteLength;
17967
17968        if (type === path[0]) {
17969          if (path.length === 1) {
17970            // this is the end of the path and we've found the box we were
17971            // looking for
17972            results.push(data.subarray(i + 8, end));
17973          } else {
17974            // recursively search for the next box along the path
17975            subresults = findBox$5(data.subarray(i + 8, end), path.slice(1));
17976
17977            if (subresults.length) {
17978              results = results.concat(subresults);
17979            }
17980          }
17981        }
17982
17983        i = end;
17984      } // we've finished searching all of data
17985
17986
17987      return results;
17988    };
17989
17990    var findBox_1 = findBox$5;
17991    var toUnsigned$1 = bin.toUnsigned;
17992    var getUint64$4 = numbers.getUint64;
17993
17994    var tfdt = function (data) {
17995      var result = {
17996        version: data[0],
17997        flags: new Uint8Array(data.subarray(1, 4))
17998      };
17999
18000      if (result.version === 1) {
18001        result.baseMediaDecodeTime = getUint64$4(data.subarray(4));
18002      } else {
18003        result.baseMediaDecodeTime = toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]);
18004      }
18005
18006      return result;
18007    };
18008
18009    var parseTfdt$3 = tfdt;
18010
18011    var tfhd = function (data) {
18012      var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18013          result = {
18014        version: data[0],
18015        flags: new Uint8Array(data.subarray(1, 4)),
18016        trackId: view.getUint32(4)
18017      },
18018          baseDataOffsetPresent = result.flags[2] & 0x01,
18019          sampleDescriptionIndexPresent = result.flags[2] & 0x02,
18020          defaultSampleDurationPresent = result.flags[2] & 0x08,
18021          defaultSampleSizePresent = result.flags[2] & 0x10,
18022          defaultSampleFlagsPresent = result.flags[2] & 0x20,
18023          durationIsEmpty = result.flags[0] & 0x010000,
18024          defaultBaseIsMoof = result.flags[0] & 0x020000,
18025          i;
18026      i = 8;
18027
18028      if (baseDataOffsetPresent) {
18029        i += 4; // truncate top 4 bytes
18030        // FIXME: should we read the full 64 bits?
18031
18032        result.baseDataOffset = view.getUint32(12);
18033        i += 4;
18034      }
18035
18036      if (sampleDescriptionIndexPresent) {
18037        result.sampleDescriptionIndex = view.getUint32(i);
18038        i += 4;
18039      }
18040
18041      if (defaultSampleDurationPresent) {
18042        result.defaultSampleDuration = view.getUint32(i);
18043        i += 4;
18044      }
18045
18046      if (defaultSampleSizePresent) {
18047        result.defaultSampleSize = view.getUint32(i);
18048        i += 4;
18049      }
18050
18051      if (defaultSampleFlagsPresent) {
18052        result.defaultSampleFlags = view.getUint32(i);
18053      }
18054
18055      if (durationIsEmpty) {
18056        result.durationIsEmpty = true;
18057      }
18058
18059      if (!baseDataOffsetPresent && defaultBaseIsMoof) {
18060        result.baseDataOffsetIsMoof = true;
18061      }
18062
18063      return result;
18064    };
18065
18066    var parseTfhd$2 = tfhd;
18067    var getUint64$3 = numbers.getUint64;
18068
18069    var parseSidx = function (data) {
18070      var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18071          result = {
18072        version: data[0],
18073        flags: new Uint8Array(data.subarray(1, 4)),
18074        references: [],
18075        referenceId: view.getUint32(4),
18076        timescale: view.getUint32(8)
18077      },
18078          i = 12;
18079
18080      if (result.version === 0) {
18081        result.earliestPresentationTime = view.getUint32(i);
18082        result.firstOffset = view.getUint32(i + 4);
18083        i += 8;
18084      } else {
18085        // read 64 bits
18086        result.earliestPresentationTime = getUint64$3(data.subarray(i));
18087        result.firstOffset = getUint64$3(data.subarray(i + 8));
18088        i += 16;
18089      }
18090
18091      i += 2; // reserved
18092
18093      var referenceCount = view.getUint16(i);
18094      i += 2; // start of references
18095
18096      for (; referenceCount > 0; i += 12, referenceCount--) {
18097        result.references.push({
18098          referenceType: (data[i] & 0x80) >>> 7,
18099          referencedSize: view.getUint32(i) & 0x7FFFFFFF,
18100          subsegmentDuration: view.getUint32(i + 4),
18101          startsWithSap: !!(data[i + 8] & 0x80),
18102          sapType: (data[i + 8] & 0x70) >>> 4,
18103          sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF
18104        });
18105      }
18106
18107      return result;
18108    };
18109
18110    var parseSidx_1 = parseSidx;
18111
18112    var parseSampleFlags$1 = function (flags) {
18113      return {
18114        isLeading: (flags[0] & 0x0c) >>> 2,
18115        dependsOn: flags[0] & 0x03,
18116        isDependedOn: (flags[1] & 0xc0) >>> 6,
18117        hasRedundancy: (flags[1] & 0x30) >>> 4,
18118        paddingValue: (flags[1] & 0x0e) >>> 1,
18119        isNonSyncSample: flags[1] & 0x01,
18120        degradationPriority: flags[2] << 8 | flags[3]
18121      };
18122    };
18123
18124    var parseSampleFlags_1 = parseSampleFlags$1;
18125    var parseSampleFlags = parseSampleFlags_1;
18126
18127    var trun = function (data) {
18128      var result = {
18129        version: data[0],
18130        flags: new Uint8Array(data.subarray(1, 4)),
18131        samples: []
18132      },
18133          view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18134          // Flag interpretation
18135      dataOffsetPresent = result.flags[2] & 0x01,
18136          // compare with 2nd byte of 0x1
18137      firstSampleFlagsPresent = result.flags[2] & 0x04,
18138          // compare with 2nd byte of 0x4
18139      sampleDurationPresent = result.flags[1] & 0x01,
18140          // compare with 2nd byte of 0x100
18141      sampleSizePresent = result.flags[1] & 0x02,
18142          // compare with 2nd byte of 0x200
18143      sampleFlagsPresent = result.flags[1] & 0x04,
18144          // compare with 2nd byte of 0x400
18145      sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08,
18146          // compare with 2nd byte of 0x800
18147      sampleCount = view.getUint32(4),
18148          offset = 8,
18149          sample;
18150
18151      if (dataOffsetPresent) {
18152        // 32 bit signed integer
18153        result.dataOffset = view.getInt32(offset);
18154        offset += 4;
18155      } // Overrides the flags for the first sample only. The order of
18156      // optional values will be: duration, size, compositionTimeOffset
18157
18158
18159      if (firstSampleFlagsPresent && sampleCount) {
18160        sample = {
18161          flags: parseSampleFlags(data.subarray(offset, offset + 4))
18162        };
18163        offset += 4;
18164
18165        if (sampleDurationPresent) {
18166          sample.duration = view.getUint32(offset);
18167          offset += 4;
18168        }
18169
18170        if (sampleSizePresent) {
18171          sample.size = view.getUint32(offset);
18172          offset += 4;
18173        }
18174
18175        if (sampleCompositionTimeOffsetPresent) {
18176          if (result.version === 1) {
18177            sample.compositionTimeOffset = view.getInt32(offset);
18178          } else {
18179            sample.compositionTimeOffset = view.getUint32(offset);
18180          }
18181
18182          offset += 4;
18183        }
18184
18185        result.samples.push(sample);
18186        sampleCount--;
18187      }
18188
18189      while (sampleCount--) {
18190        sample = {};
18191
18192        if (sampleDurationPresent) {
18193          sample.duration = view.getUint32(offset);
18194          offset += 4;
18195        }
18196
18197        if (sampleSizePresent) {
18198          sample.size = view.getUint32(offset);
18199          offset += 4;
18200        }
18201
18202        if (sampleFlagsPresent) {
18203          sample.flags = parseSampleFlags(data.subarray(offset, offset + 4));
18204          offset += 4;
18205        }
18206
18207        if (sampleCompositionTimeOffsetPresent) {
18208          if (result.version === 1) {
18209            sample.compositionTimeOffset = view.getInt32(offset);
18210          } else {
18211            sample.compositionTimeOffset = view.getUint32(offset);
18212          }
18213
18214          offset += 4;
18215        }
18216
18217        result.samples.push(sample);
18218      }
18219
18220      return result;
18221    };
18222
18223    var parseTrun$2 = trun;
18224    /**
18225     * mux.js
18226     *
18227     * Copyright (c) Brightcove
18228     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
18229     *
18230     * Parse the internal MP4 structure into an equivalent javascript
18231     * object.
18232     */
18233
18234    var numberHelpers = numbers;
18235    var getUint64$2 = numberHelpers.getUint64;
18236
18237    var inspectMp4,
18238        textifyMp4,
18239        parseMp4Date = function (seconds) {
18240      return new Date(seconds * 1000 - 2082844800000);
18241    },
18242        parseType$2 = parseType_1,
18243        findBox$4 = findBox_1,
18244        nalParse = function (avcStream) {
18245      var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
18246          result = [],
18247          i,
18248          length;
18249
18250      for (i = 0; i + 4 < avcStream.length; i += length) {
18251        length = avcView.getUint32(i);
18252        i += 4; // bail if this doesn't appear to be an H264 stream
18253
18254        if (length <= 0) {
18255          result.push('<span style=\'color:red;\'>MALFORMED DATA</span>');
18256          continue;
18257        }
18258
18259        switch (avcStream[i] & 0x1F) {
18260          case 0x01:
18261            result.push('slice_layer_without_partitioning_rbsp');
18262            break;
18263
18264          case 0x05:
18265            result.push('slice_layer_without_partitioning_rbsp_idr');
18266            break;
18267
18268          case 0x06:
18269            result.push('sei_rbsp');
18270            break;
18271
18272          case 0x07:
18273            result.push('seq_parameter_set_rbsp');
18274            break;
18275
18276          case 0x08:
18277            result.push('pic_parameter_set_rbsp');
18278            break;
18279
18280          case 0x09:
18281            result.push('access_unit_delimiter_rbsp');
18282            break;
18283
18284          default:
18285            result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F);
18286            break;
18287        }
18288      }
18289
18290      return result;
18291    },
18292        // registry of handlers for individual mp4 box types
18293    parse = {
18294      // codingname, not a first-class box type. stsd entries share the
18295      // same format as real boxes so the parsing infrastructure can be
18296      // shared
18297      avc1: function (data) {
18298        var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
18299        return {
18300          dataReferenceIndex: view.getUint16(6),
18301          width: view.getUint16(24),
18302          height: view.getUint16(26),
18303          horizresolution: view.getUint16(28) + view.getUint16(30) / 16,
18304          vertresolution: view.getUint16(32) + view.getUint16(34) / 16,
18305          frameCount: view.getUint16(40),
18306          depth: view.getUint16(74),
18307          config: inspectMp4(data.subarray(78, data.byteLength))
18308        };
18309      },
18310      avcC: function (data) {
18311        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18312            result = {
18313          configurationVersion: data[0],
18314          avcProfileIndication: data[1],
18315          profileCompatibility: data[2],
18316          avcLevelIndication: data[3],
18317          lengthSizeMinusOne: data[4] & 0x03,
18318          sps: [],
18319          pps: []
18320        },
18321            numOfSequenceParameterSets = data[5] & 0x1f,
18322            numOfPictureParameterSets,
18323            nalSize,
18324            offset,
18325            i; // iterate past any SPSs
18326
18327        offset = 6;
18328
18329        for (i = 0; i < numOfSequenceParameterSets; i++) {
18330          nalSize = view.getUint16(offset);
18331          offset += 2;
18332          result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
18333          offset += nalSize;
18334        } // iterate past any PPSs
18335
18336
18337        numOfPictureParameterSets = data[offset];
18338        offset++;
18339
18340        for (i = 0; i < numOfPictureParameterSets; i++) {
18341          nalSize = view.getUint16(offset);
18342          offset += 2;
18343          result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize)));
18344          offset += nalSize;
18345        }
18346
18347        return result;
18348      },
18349      btrt: function (data) {
18350        var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
18351        return {
18352          bufferSizeDB: view.getUint32(0),
18353          maxBitrate: view.getUint32(4),
18354          avgBitrate: view.getUint32(8)
18355        };
18356      },
18357      edts: function edts(data) {
18358        return {
18359          boxes: inspectMp4(data)
18360        };
18361      },
18362      elst: function elst(data) {
18363        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18364            result = {
18365          version: view.getUint8(0),
18366          flags: new Uint8Array(data.subarray(1, 4)),
18367          edits: []
18368        },
18369            entryCount = view.getUint32(4),
18370            i;
18371
18372        for (i = 8; entryCount; entryCount--) {
18373          if (result.version === 0) {
18374            result.edits.push({
18375              segmentDuration: view.getUint32(i),
18376              mediaTime: view.getInt32(i + 4),
18377              mediaRate: view.getUint16(i + 8) + view.getUint16(i + 10) / (256 * 256)
18378            });
18379            i += 12;
18380          } else {
18381            result.edits.push({
18382              segmentDuration: getUint64$2(data.subarray(i)),
18383              mediaTime: getUint64$2(data.subarray(i + 8)),
18384              mediaRate: view.getUint16(i + 16) + view.getUint16(i + 18) / (256 * 256)
18385            });
18386            i += 20;
18387          }
18388        }
18389
18390        return result;
18391      },
18392      esds: function (data) {
18393        return {
18394          version: data[0],
18395          flags: new Uint8Array(data.subarray(1, 4)),
18396          esId: data[6] << 8 | data[7],
18397          streamPriority: data[8] & 0x1f,
18398          decoderConfig: {
18399            objectProfileIndication: data[11],
18400            streamType: data[12] >>> 2 & 0x3f,
18401            bufferSize: data[13] << 16 | data[14] << 8 | data[15],
18402            maxBitrate: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19],
18403            avgBitrate: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23],
18404            decoderConfigDescriptor: {
18405              tag: data[24],
18406              length: data[25],
18407              audioObjectType: data[26] >>> 3 & 0x1f,
18408              samplingFrequencyIndex: (data[26] & 0x07) << 1 | data[27] >>> 7 & 0x01,
18409              channelConfiguration: data[27] >>> 3 & 0x0f
18410            }
18411          }
18412        };
18413      },
18414      ftyp: function (data) {
18415        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18416            result = {
18417          majorBrand: parseType$2(data.subarray(0, 4)),
18418          minorVersion: view.getUint32(4),
18419          compatibleBrands: []
18420        },
18421            i = 8;
18422
18423        while (i < data.byteLength) {
18424          result.compatibleBrands.push(parseType$2(data.subarray(i, i + 4)));
18425          i += 4;
18426        }
18427
18428        return result;
18429      },
18430      dinf: function (data) {
18431        return {
18432          boxes: inspectMp4(data)
18433        };
18434      },
18435      dref: function (data) {
18436        return {
18437          version: data[0],
18438          flags: new Uint8Array(data.subarray(1, 4)),
18439          dataReferences: inspectMp4(data.subarray(8))
18440        };
18441      },
18442      hdlr: function (data) {
18443        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18444            result = {
18445          version: view.getUint8(0),
18446          flags: new Uint8Array(data.subarray(1, 4)),
18447          handlerType: parseType$2(data.subarray(8, 12)),
18448          name: ''
18449        },
18450            i = 8; // parse out the name field
18451
18452        for (i = 24; i < data.byteLength; i++) {
18453          if (data[i] === 0x00) {
18454            // the name field is null-terminated
18455            i++;
18456            break;
18457          }
18458
18459          result.name += String.fromCharCode(data[i]);
18460        } // decode UTF-8 to javascript's internal representation
18461        // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
18462
18463
18464        result.name = decodeURIComponent(escape(result.name));
18465        return result;
18466      },
18467      mdat: function (data) {
18468        return {
18469          byteLength: data.byteLength,
18470          nals: nalParse(data)
18471        };
18472      },
18473      mdhd: function (data) {
18474        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18475            i = 4,
18476            language,
18477            result = {
18478          version: view.getUint8(0),
18479          flags: new Uint8Array(data.subarray(1, 4)),
18480          language: ''
18481        };
18482
18483        if (result.version === 1) {
18484          i += 4;
18485          result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
18486
18487          i += 8;
18488          result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
18489
18490          i += 4;
18491          result.timescale = view.getUint32(i);
18492          i += 8;
18493          result.duration = view.getUint32(i); // truncating top 4 bytes
18494        } else {
18495          result.creationTime = parseMp4Date(view.getUint32(i));
18496          i += 4;
18497          result.modificationTime = parseMp4Date(view.getUint32(i));
18498          i += 4;
18499          result.timescale = view.getUint32(i);
18500          i += 4;
18501          result.duration = view.getUint32(i);
18502        }
18503
18504        i += 4; // language is stored as an ISO-639-2/T code in an array of three 5-bit fields
18505        // each field is the packed difference between its ASCII value and 0x60
18506
18507        language = view.getUint16(i);
18508        result.language += String.fromCharCode((language >> 10) + 0x60);
18509        result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60);
18510        result.language += String.fromCharCode((language & 0x1f) + 0x60);
18511        return result;
18512      },
18513      mdia: function (data) {
18514        return {
18515          boxes: inspectMp4(data)
18516        };
18517      },
18518      mfhd: function (data) {
18519        return {
18520          version: data[0],
18521          flags: new Uint8Array(data.subarray(1, 4)),
18522          sequenceNumber: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]
18523        };
18524      },
18525      minf: function (data) {
18526        return {
18527          boxes: inspectMp4(data)
18528        };
18529      },
18530      // codingname, not a first-class box type. stsd entries share the
18531      // same format as real boxes so the parsing infrastructure can be
18532      // shared
18533      mp4a: function (data) {
18534        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18535            result = {
18536          // 6 bytes reserved
18537          dataReferenceIndex: view.getUint16(6),
18538          // 4 + 4 bytes reserved
18539          channelcount: view.getUint16(16),
18540          samplesize: view.getUint16(18),
18541          // 2 bytes pre_defined
18542          // 2 bytes reserved
18543          samplerate: view.getUint16(24) + view.getUint16(26) / 65536
18544        }; // if there are more bytes to process, assume this is an ISO/IEC
18545        // 14496-14 MP4AudioSampleEntry and parse the ESDBox
18546
18547        if (data.byteLength > 28) {
18548          result.streamDescriptor = inspectMp4(data.subarray(28))[0];
18549        }
18550
18551        return result;
18552      },
18553      moof: function (data) {
18554        return {
18555          boxes: inspectMp4(data)
18556        };
18557      },
18558      moov: function (data) {
18559        return {
18560          boxes: inspectMp4(data)
18561        };
18562      },
18563      mvex: function (data) {
18564        return {
18565          boxes: inspectMp4(data)
18566        };
18567      },
18568      mvhd: function (data) {
18569        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18570            i = 4,
18571            result = {
18572          version: view.getUint8(0),
18573          flags: new Uint8Array(data.subarray(1, 4))
18574        };
18575
18576        if (result.version === 1) {
18577          i += 4;
18578          result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
18579
18580          i += 8;
18581          result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
18582
18583          i += 4;
18584          result.timescale = view.getUint32(i);
18585          i += 8;
18586          result.duration = view.getUint32(i); // truncating top 4 bytes
18587        } else {
18588          result.creationTime = parseMp4Date(view.getUint32(i));
18589          i += 4;
18590          result.modificationTime = parseMp4Date(view.getUint32(i));
18591          i += 4;
18592          result.timescale = view.getUint32(i);
18593          i += 4;
18594          result.duration = view.getUint32(i);
18595        }
18596
18597        i += 4; // convert fixed-point, base 16 back to a number
18598
18599        result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16;
18600        i += 4;
18601        result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
18602        i += 2;
18603        i += 2;
18604        i += 2 * 4;
18605        result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
18606        i += 9 * 4;
18607        i += 6 * 4;
18608        result.nextTrackId = view.getUint32(i);
18609        return result;
18610      },
18611      pdin: function (data) {
18612        var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
18613        return {
18614          version: view.getUint8(0),
18615          flags: new Uint8Array(data.subarray(1, 4)),
18616          rate: view.getUint32(4),
18617          initialDelay: view.getUint32(8)
18618        };
18619      },
18620      sdtp: function (data) {
18621        var result = {
18622          version: data[0],
18623          flags: new Uint8Array(data.subarray(1, 4)),
18624          samples: []
18625        },
18626            i;
18627
18628        for (i = 4; i < data.byteLength; i++) {
18629          result.samples.push({
18630            dependsOn: (data[i] & 0x30) >> 4,
18631            isDependedOn: (data[i] & 0x0c) >> 2,
18632            hasRedundancy: data[i] & 0x03
18633          });
18634        }
18635
18636        return result;
18637      },
18638      sidx: parseSidx_1,
18639      smhd: function (data) {
18640        return {
18641          version: data[0],
18642          flags: new Uint8Array(data.subarray(1, 4)),
18643          balance: data[4] + data[5] / 256
18644        };
18645      },
18646      stbl: function (data) {
18647        return {
18648          boxes: inspectMp4(data)
18649        };
18650      },
18651      ctts: function (data) {
18652        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18653            result = {
18654          version: view.getUint8(0),
18655          flags: new Uint8Array(data.subarray(1, 4)),
18656          compositionOffsets: []
18657        },
18658            entryCount = view.getUint32(4),
18659            i;
18660
18661        for (i = 8; entryCount; i += 8, entryCount--) {
18662          result.compositionOffsets.push({
18663            sampleCount: view.getUint32(i),
18664            sampleOffset: view[result.version === 0 ? 'getUint32' : 'getInt32'](i + 4)
18665          });
18666        }
18667
18668        return result;
18669      },
18670      stss: function (data) {
18671        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18672            result = {
18673          version: view.getUint8(0),
18674          flags: new Uint8Array(data.subarray(1, 4)),
18675          syncSamples: []
18676        },
18677            entryCount = view.getUint32(4),
18678            i;
18679
18680        for (i = 8; entryCount; i += 4, entryCount--) {
18681          result.syncSamples.push(view.getUint32(i));
18682        }
18683
18684        return result;
18685      },
18686      stco: function (data) {
18687        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18688            result = {
18689          version: data[0],
18690          flags: new Uint8Array(data.subarray(1, 4)),
18691          chunkOffsets: []
18692        },
18693            entryCount = view.getUint32(4),
18694            i;
18695
18696        for (i = 8; entryCount; i += 4, entryCount--) {
18697          result.chunkOffsets.push(view.getUint32(i));
18698        }
18699
18700        return result;
18701      },
18702      stsc: function (data) {
18703        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18704            entryCount = view.getUint32(4),
18705            result = {
18706          version: data[0],
18707          flags: new Uint8Array(data.subarray(1, 4)),
18708          sampleToChunks: []
18709        },
18710            i;
18711
18712        for (i = 8; entryCount; i += 12, entryCount--) {
18713          result.sampleToChunks.push({
18714            firstChunk: view.getUint32(i),
18715            samplesPerChunk: view.getUint32(i + 4),
18716            sampleDescriptionIndex: view.getUint32(i + 8)
18717          });
18718        }
18719
18720        return result;
18721      },
18722      stsd: function (data) {
18723        return {
18724          version: data[0],
18725          flags: new Uint8Array(data.subarray(1, 4)),
18726          sampleDescriptions: inspectMp4(data.subarray(8))
18727        };
18728      },
18729      stsz: function (data) {
18730        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18731            result = {
18732          version: data[0],
18733          flags: new Uint8Array(data.subarray(1, 4)),
18734          sampleSize: view.getUint32(4),
18735          entries: []
18736        },
18737            i;
18738
18739        for (i = 12; i < data.byteLength; i += 4) {
18740          result.entries.push(view.getUint32(i));
18741        }
18742
18743        return result;
18744      },
18745      stts: function (data) {
18746        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18747            result = {
18748          version: data[0],
18749          flags: new Uint8Array(data.subarray(1, 4)),
18750          timeToSamples: []
18751        },
18752            entryCount = view.getUint32(4),
18753            i;
18754
18755        for (i = 8; entryCount; i += 8, entryCount--) {
18756          result.timeToSamples.push({
18757            sampleCount: view.getUint32(i),
18758            sampleDelta: view.getUint32(i + 4)
18759          });
18760        }
18761
18762        return result;
18763      },
18764      styp: function (data) {
18765        return parse.ftyp(data);
18766      },
18767      tfdt: parseTfdt$3,
18768      tfhd: parseTfhd$2,
18769      tkhd: function (data) {
18770        var view = new DataView(data.buffer, data.byteOffset, data.byteLength),
18771            i = 4,
18772            result = {
18773          version: view.getUint8(0),
18774          flags: new Uint8Array(data.subarray(1, 4))
18775        };
18776
18777        if (result.version === 1) {
18778          i += 4;
18779          result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
18780
18781          i += 8;
18782          result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes
18783
18784          i += 4;
18785          result.trackId = view.getUint32(i);
18786          i += 4;
18787          i += 8;
18788          result.duration = view.getUint32(i); // truncating top 4 bytes
18789        } else {
18790          result.creationTime = parseMp4Date(view.getUint32(i));
18791          i += 4;
18792          result.modificationTime = parseMp4Date(view.getUint32(i));
18793          i += 4;
18794          result.trackId = view.getUint32(i);
18795          i += 4;
18796          i += 4;
18797          result.duration = view.getUint32(i);
18798        }
18799
18800        i += 4;
18801        i += 2 * 4;
18802        result.layer = view.getUint16(i);
18803        i += 2;
18804        result.alternateGroup = view.getUint16(i);
18805        i += 2; // convert fixed-point, base 16 back to a number
18806
18807        result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8;
18808        i += 2;
18809        i += 2;
18810        result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4));
18811        i += 9 * 4;
18812        result.width = view.getUint16(i) + view.getUint16(i + 2) / 65536;
18813        i += 4;
18814        result.height = view.getUint16(i) + view.getUint16(i + 2) / 65536;
18815        return result;
18816      },
18817      traf: function (data) {
18818        return {
18819          boxes: inspectMp4(data)
18820        };
18821      },
18822      trak: function (data) {
18823        return {
18824          boxes: inspectMp4(data)
18825        };
18826      },
18827      trex: function (data) {
18828        var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
18829        return {
18830          version: data[0],
18831          flags: new Uint8Array(data.subarray(1, 4)),
18832          trackId: view.getUint32(4),
18833          defaultSampleDescriptionIndex: view.getUint32(8),
18834          defaultSampleDuration: view.getUint32(12),
18835          defaultSampleSize: view.getUint32(16),
18836          sampleDependsOn: data[20] & 0x03,
18837          sampleIsDependedOn: (data[21] & 0xc0) >> 6,
18838          sampleHasRedundancy: (data[21] & 0x30) >> 4,
18839          samplePaddingValue: (data[21] & 0x0e) >> 1,
18840          sampleIsDifferenceSample: !!(data[21] & 0x01),
18841          sampleDegradationPriority: view.getUint16(22)
18842        };
18843      },
18844      trun: parseTrun$2,
18845      'url ': function (data) {
18846        return {
18847          version: data[0],
18848          flags: new Uint8Array(data.subarray(1, 4))
18849        };
18850      },
18851      vmhd: function (data) {
18852        var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
18853        return {
18854          version: data[0],
18855          flags: new Uint8Array(data.subarray(1, 4)),
18856          graphicsmode: view.getUint16(4),
18857          opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)])
18858        };
18859      }
18860    };
18861    /**
18862     * Return a javascript array of box objects parsed from an ISO base
18863     * media file.
18864     * @param data {Uint8Array} the binary data of the media to be inspected
18865     * @return {array} a javascript array of potentially nested box objects
18866     */
18867
18868
18869    inspectMp4 = function (data) {
18870      var i = 0,
18871          result = [],
18872          view,
18873          size,
18874          type,
18875          end,
18876          box; // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API
18877
18878      var ab = new ArrayBuffer(data.length);
18879      var v = new Uint8Array(ab);
18880
18881      for (var z = 0; z < data.length; ++z) {
18882        v[z] = data[z];
18883      }
18884
18885      view = new DataView(ab);
18886
18887      while (i < data.byteLength) {
18888        // parse box data
18889        size = view.getUint32(i);
18890        type = parseType$2(data.subarray(i + 4, i + 8));
18891        end = size > 1 ? i + size : data.byteLength; // parse type-specific data
18892
18893        box = (parse[type] || function (data) {
18894          return {
18895            data: data
18896          };
18897        })(data.subarray(i + 8, end));
18898
18899        box.size = size;
18900        box.type = type; // store this box and move to the next
18901
18902        result.push(box);
18903        i = end;
18904      }
18905
18906      return result;
18907    };
18908    /**
18909     * Returns a textual representation of the javascript represtentation
18910     * of an MP4 file. You can use it as an alternative to
18911     * JSON.stringify() to compare inspected MP4s.
18912     * @param inspectedMp4 {array} the parsed array of boxes in an MP4
18913     * file
18914     * @param depth {number} (optional) the number of ancestor boxes of
18915     * the elements of inspectedMp4. Assumed to be zero if unspecified.
18916     * @return {string} a text representation of the parsed MP4
18917     */
18918
18919
18920    textifyMp4 = function (inspectedMp4, depth) {
18921      var indent;
18922      depth = depth || 0;
18923      indent = new Array(depth * 2 + 1).join(' '); // iterate over all the boxes
18924
18925      return inspectedMp4.map(function (box, index) {
18926        // list the box type first at the current indentation level
18927        return indent + box.type + '\n' + // the type is already included and handle child boxes separately
18928        Object.keys(box).filter(function (key) {
18929          return key !== 'type' && key !== 'boxes'; // output all the box properties
18930        }).map(function (key) {
18931          var prefix = indent + '  ' + key + ': ',
18932              value = box[key]; // print out raw bytes as hexademical
18933
18934          if (value instanceof Uint8Array || value instanceof Uint32Array) {
18935            var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)).map(function (byte) {
18936              return ' ' + ('00' + byte.toString(16)).slice(-2);
18937            }).join('').match(/.{1,24}/g);
18938
18939            if (!bytes) {
18940              return prefix + '<>';
18941            }
18942
18943            if (bytes.length === 1) {
18944              return prefix + '<' + bytes.join('').slice(1) + '>';
18945            }
18946
18947            return prefix + '<\n' + bytes.map(function (line) {
18948              return indent + '  ' + line;
18949            }).join('\n') + '\n' + indent + '  >';
18950          } // stringify generic objects
18951
18952
18953          return prefix + JSON.stringify(value, null, 2).split('\n').map(function (line, index) {
18954            if (index === 0) {
18955              return line;
18956            }
18957
18958            return indent + '  ' + line;
18959          }).join('\n');
18960        }).join('\n') + ( // recursively textify the child boxes
18961        box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : '');
18962      }).join('\n');
18963    };
18964
18965    var mp4Inspector = {
18966      inspect: inspectMp4,
18967      textify: textifyMp4,
18968      parseType: parseType$2,
18969      findBox: findBox$4,
18970      parseTraf: parse.traf,
18971      parseTfdt: parse.tfdt,
18972      parseHdlr: parse.hdlr,
18973      parseTfhd: parse.tfhd,
18974      parseTrun: parse.trun,
18975      parseSidx: parse.sidx
18976    };
18977    /**
18978     * Returns the first string in the data array ending with a null char '\0'
18979     * @param {UInt8} data
vendor: 2,985 bytes, lines 18979-19070
18979
18980     * @returns the string with the null char
18981     */
18982
18983    var uint8ToCString$1 = function (data) {
18984      var index = 0;
18985      var curChar = String.fromCharCode(data[index]);
18986      var retString = '';
18987
18988      while (curChar !== '\0') {
18989        retString += curChar;
18990        index++;
18991        curChar = String.fromCharCode(data[index]);
18992      } // Add nullChar
18993
18994
18995      retString += curChar;
18996      return retString;
18997    };
18998
18999    var string = {
19000      uint8ToCString: uint8ToCString$1
19001    };
19002    var uint8ToCString = string.uint8ToCString;
19003    var getUint64$1 = numbers.getUint64;
19004    /**
19005     * Based on: ISO/IEC 23009 Section: 5.10.3.3
19006     * References:
19007     * https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format
19008     * https://aomediacodec.github.io/id3-emsg/
19009     *
19010     * Takes emsg box data as a uint8 array and returns a emsg box object
19011     * @param {UInt8Array} boxData data from emsg box
19012     * @returns A parsed emsg box object
19013     */
19014
19015    var parseEmsgBox = function (boxData) {
19016      // version + flags
19017      var offset = 4;
19018      var version = boxData[0];
19019      var scheme_id_uri, value, timescale, presentation_time, presentation_time_delta, event_duration, id, message_data;
19020
19021      if (version === 0) {
19022        scheme_id_uri = uint8ToCString(boxData.subarray(offset));
19023        offset += scheme_id_uri.length;
19024        value = uint8ToCString(boxData.subarray(offset));
19025        offset += value.length;
19026        var dv = new DataView(boxData.buffer);
19027        timescale = dv.getUint32(offset);
19028        offset += 4;
19029        presentation_time_delta = dv.getUint32(offset);
19030        offset += 4;
19031        event_duration = dv.getUint32(offset);
19032        offset += 4;
19033        id = dv.getUint32(offset);
19034        offset += 4;
19035      } else if (version === 1) {
19036        var dv = new DataView(boxData.buffer);
19037        timescale = dv.getUint32(offset);
19038        offset += 4;
19039        presentation_time = getUint64$1(boxData.subarray(offset));
19040        offset += 8;
19041        event_duration = dv.getUint32(offset);
19042        offset += 4;
19043        id = dv.getUint32(offset);
19044        offset += 4;
19045        scheme_id_uri = uint8ToCString(boxData.subarray(offset));
19046        offset += scheme_id_uri.length;
19047        value = uint8ToCString(boxData.subarray(offset));
19048        offset += value.length;
19049      }
19050
19051      message_data = new Uint8Array(boxData.subarray(offset, boxData.byteLength));
19052      var emsgBox = {
19053        scheme_id_uri,
19054        value,
19055        // if timescale is undefined or 0 set to 1
19056        timescale: timescale ? timescale : 1,
19057        presentation_time,
19058        presentation_time_delta,
19059        event_duration,
19060        id,
19061        message_data
19062      };
19063      return isValidEmsgBox(version, emsgBox) ? emsgBox : undefined;
19064    };
19065    /**
19066     * Scales a presentation time or time delta with an offset with a provided timescale
19067     * @param {number} presentationTime
19068     * @param {number} timescale
19069     * @param {number} timeDelta
19070     * @param {number} offset
vendor: 13,887 bytes, lines 19070-19457
19070
19071     * @returns the scaled time as a number
19072     */
19073
19074
19075    var scaleTime = function (presentationTime, timescale, timeDelta, offset) {
19076      return presentationTime || presentationTime === 0 ? presentationTime / timescale : offset + timeDelta / timescale;
19077    };
19078    /**
19079     * Checks the emsg box data for validity based on the version
19080     * @param {number} version of the emsg box to validate
19081     * @param {Object} emsg the emsg data to validate
19082     * @returns if the box is valid as a boolean
19083     */
19084
19085
19086    var isValidEmsgBox = function (version, emsg) {
19087      var hasScheme = emsg.scheme_id_uri !== '\0';
19088      var isValidV0Box = version === 0 && isDefined(emsg.presentation_time_delta) && hasScheme;
19089      var isValidV1Box = version === 1 && isDefined(emsg.presentation_time) && hasScheme; // Only valid versions of emsg are 0 and 1
19090
19091      return !(version > 1) && isValidV0Box || isValidV1Box;
19092    }; // Utility function to check if an object is defined
19093
19094
19095    var isDefined = function (data) {
19096      return data !== undefined || data !== null;
19097    };
19098
19099    var emsg$1 = {
19100      parseEmsgBox: parseEmsgBox,
19101      scaleTime: scaleTime
19102    };
19103    var win;
19104
19105    if (typeof window !== "undefined") {
19106      win = window;
19107    } else if (typeof commonjsGlobal !== "undefined") {
19108      win = commonjsGlobal;
19109    } else if (typeof self !== "undefined") {
19110      win = self;
19111    } else {
19112      win = {};
19113    }
19114
19115    var window_1 = win;
19116    /**
19117     * mux.js
19118     *
19119     * Copyright (c) Brightcove
19120     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
19121     *
19122     * Utilities to detect basic properties and metadata about MP4s.
19123     */
19124
19125    var toUnsigned = bin.toUnsigned;
19126    var toHexString = bin.toHexString;
19127    var findBox$3 = findBox_1;
19128    var parseType$1 = parseType_1;
19129    var emsg = emsg$1;
19130    var parseTfhd$1 = parseTfhd$2;
19131    var parseTrun$1 = parseTrun$2;
19132    var parseTfdt$2 = parseTfdt$3;
19133    var getUint64 = numbers.getUint64;
19134    var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks, getTimescaleFromMediaHeader$1, getEmsgID3;
19135    var window$2 = window_1;
19136    var parseId3Frames = parseId3.parseId3Frames;
19137    /**
19138     * Parses an MP4 initialization segment and extracts the timescale
19139     * values for any declared tracks. Timescale values indicate the
19140     * number of clock ticks per second to assume for time-based values
19141     * elsewhere in the MP4.
19142     *
19143     * To determine the start time of an MP4, you need two pieces of
19144     * information: the timescale unit and the earliest base media decode
19145     * time. Multiple timescales can be specified within an MP4 but the
19146     * base media decode time is always expressed in the timescale from
19147     * the media header box for the track:
19148     * ```
19149     * moov > trak > mdia > mdhd.timescale
19150     * ```
19151     * @param init {Uint8Array} the bytes of the init segment
19152     * @return {object} a hash of track ids to timescale values or null if
19153     * the init segment is malformed.
19154     */
19155
19156    timescale = function (init) {
19157      var result = {},
19158          traks = findBox$3(init, ['moov', 'trak']); // mdhd timescale
19159
19160      return traks.reduce(function (result, trak) {
19161        var tkhd, version, index, id, mdhd;
19162        tkhd = findBox$3(trak, ['tkhd'])[0];
19163
19164        if (!tkhd) {
19165          return null;
19166        }
19167
19168        version = tkhd[0];
19169        index = version === 0 ? 12 : 20;
19170        id = toUnsigned(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]);
19171        mdhd = findBox$3(trak, ['mdia', 'mdhd'])[0];
19172
19173        if (!mdhd) {
19174          return null;
19175        }
19176
19177        version = mdhd[0];
19178        index = version === 0 ? 12 : 20;
19179        result[id] = toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]);
19180        return result;
19181      }, result);
19182    };
19183    /**
19184     * Determine the base media decode start time, in seconds, for an MP4
19185     * fragment. If multiple fragments are specified, the earliest time is
19186     * returned.
19187     *
19188     * The base media decode time can be parsed from track fragment
19189     * metadata:
19190     * ```
19191     * moof > traf > tfdt.baseMediaDecodeTime
19192     * ```
19193     * It requires the timescale value from the mdhd to interpret.
19194     *
19195     * @param timescale {object} a hash of track ids to timescale values.
19196     * @return {number} the earliest base media decode start time for the
19197     * fragment, in seconds
19198     */
19199
19200
19201    startTime = function (timescale, fragment) {
19202      var trafs; // we need info from two childrend of each track fragment box
19203
19204      trafs = findBox$3(fragment, ['moof', 'traf']); // determine the start times for each track
19205
19206      var lowestTime = trafs.reduce(function (acc, traf) {
19207        var tfhd = findBox$3(traf, ['tfhd'])[0]; // get the track id from the tfhd
19208
19209        var id = toUnsigned(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); // assume a 90kHz clock if no timescale was specified
19210
19211        var scale = timescale[id] || 90e3; // get the base media decode time from the tfdt
19212
19213        var tfdt = findBox$3(traf, ['tfdt'])[0];
19214        var dv = new DataView(tfdt.buffer, tfdt.byteOffset, tfdt.byteLength);
19215        var baseTime; // version 1 is 64 bit
19216
19217        if (tfdt[0] === 1) {
19218          baseTime = getUint64(tfdt.subarray(4, 12));
19219        } else {
19220          baseTime = dv.getUint32(4);
19221        } // convert base time to seconds if it is a valid number.
19222
19223
19224        let seconds;
19225
19226        if (typeof baseTime === 'bigint') {
19227          seconds = baseTime / window$2.BigInt(scale);
19228        } else if (typeof baseTime === 'number' && !isNaN(baseTime)) {
19229          seconds = baseTime / scale;
19230        }
19231
19232        if (seconds < Number.MAX_SAFE_INTEGER) {
19233          seconds = Number(seconds);
19234        }
19235
19236        if (seconds < acc) {
19237          acc = seconds;
19238        }
19239
19240        return acc;
19241      }, Infinity);
19242      return typeof lowestTime === 'bigint' || isFinite(lowestTime) ? lowestTime : 0;
19243    };
19244    /**
19245     * Determine the composition start, in seconds, for an MP4
19246     * fragment.
19247     *
19248     * The composition start time of a fragment can be calculated using the base
19249     * media decode time, composition time offset, and timescale, as follows:
19250     *
19251     * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale
19252     *
19253     * All of the aforementioned information is contained within a media fragment's
19254     * `traf` box, except for timescale info, which comes from the initialization
19255     * segment, so a track id (also contained within a `traf`) is also necessary to
19256     * associate it with a timescale
19257     *
19258     *
19259     * @param timescales {object} - a hash of track ids to timescale values.
19260     * @param fragment {Unit8Array} - the bytes of a media segment
19261     * @return {number} the composition start time for the fragment, in seconds
19262     **/
19263
19264
19265    compositionStartTime = function (timescales, fragment) {
19266      var trafBoxes = findBox$3(fragment, ['moof', 'traf']);
19267      var baseMediaDecodeTime = 0;
19268      var compositionTimeOffset = 0;
19269      var trackId;
19270
19271      if (trafBoxes && trafBoxes.length) {
19272        // The spec states that track run samples contained within a `traf` box are contiguous, but
19273        // it does not explicitly state whether the `traf` boxes themselves are contiguous.
19274        // We will assume that they are, so we only need the first to calculate start time.
19275        var tfhd = findBox$3(trafBoxes[0], ['tfhd'])[0];
19276        var trun = findBox$3(trafBoxes[0], ['trun'])[0];
19277        var tfdt = findBox$3(trafBoxes[0], ['tfdt'])[0];
19278
19279        if (tfhd) {
19280          var parsedTfhd = parseTfhd$1(tfhd);
19281          trackId = parsedTfhd.trackId;
19282        }
19283
19284        if (tfdt) {
19285          var parsedTfdt = parseTfdt$2(tfdt);
19286          baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime;
19287        }
19288
19289        if (trun) {
19290          var parsedTrun = parseTrun$1(trun);
19291
19292          if (parsedTrun.samples && parsedTrun.samples.length) {
19293            compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0;
19294          }
19295        }
19296      } // Get timescale for this specific track. Assume a 90kHz clock if no timescale was
19297      // specified.
19298
19299
19300      var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds
19301
19302      if (typeof baseMediaDecodeTime === 'bigint') {
19303        compositionTimeOffset = window$2.BigInt(compositionTimeOffset);
19304        timescale = window$2.BigInt(timescale);
19305      }
19306
19307      var result = (baseMediaDecodeTime + compositionTimeOffset) / timescale;
19308
19309      if (typeof result === 'bigint' && result < Number.MAX_SAFE_INTEGER) {
19310        result = Number(result);
19311      }
19312
19313      return result;
19314    };
19315    /**
19316      * Find the trackIds of the video tracks in this source.
19317      * Found by parsing the Handler Reference and Track Header Boxes:
19318      *   moov > trak > mdia > hdlr
19319      *   moov > trak > tkhd
19320      *
19321      * @param {Uint8Array} init - The bytes of the init segment for this source
19322      * @return {Number[]} A list of trackIds
19323      *
19324      * @see ISO-BMFF-12/2015, Section 8.4.3
19325     **/
19326
19327
19328    getVideoTrackIds = function (init) {
19329      var traks = findBox$3(init, ['moov', 'trak']);
19330      var videoTrackIds = [];
19331      traks.forEach(function (trak) {
19332        var hdlrs = findBox$3(trak, ['mdia', 'hdlr']);
19333        var tkhds = findBox$3(trak, ['tkhd']);
19334        hdlrs.forEach(function (hdlr, index) {
19335          var handlerType = parseType$1(hdlr.subarray(8, 12));
19336          var tkhd = tkhds[index];
19337          var view;
19338          var version;
19339          var trackId;
19340
19341          if (handlerType === 'vide') {
19342            view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
19343            version = view.getUint8(0);
19344            trackId = version === 0 ? view.getUint32(12) : view.getUint32(20);
19345            videoTrackIds.push(trackId);
19346          }
19347        });
19348      });
19349      return videoTrackIds;
19350    };
19351
19352    getTimescaleFromMediaHeader$1 = function (mdhd) {
19353      // mdhd is a FullBox, meaning it will have its own version as the first byte
19354      var version = mdhd[0];
19355      var index = version === 0 ? 12 : 20;
19356      return toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]);
19357    };
19358    /**
19359     * Get all the video, audio, and hint tracks from a non fragmented
19360     * mp4 segment
19361     */
19362
19363
19364    getTracks = function (init) {
19365      var traks = findBox$3(init, ['moov', 'trak']);
19366      var tracks = [];
19367      traks.forEach(function (trak) {
19368        var track = {};
19369        var tkhd = findBox$3(trak, ['tkhd'])[0];
19370        var view, tkhdVersion; // id
19371
19372        if (tkhd) {
19373          view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength);
19374          tkhdVersion = view.getUint8(0);
19375          track.id = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20);
19376        }
19377
19378        var hdlr = findBox$3(trak, ['mdia', 'hdlr'])[0]; // type
19379
19380        if (hdlr) {
19381          var type = parseType$1(hdlr.subarray(8, 12));
19382
19383          if (type === 'vide') {
19384            track.type = 'video';
19385          } else if (type === 'soun') {
19386            track.type = 'audio';
19387          } else {
19388            track.type = type;
19389          }
19390        } // codec
19391
19392
19393        var stsd = findBox$3(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0];
19394
19395        if (stsd) {
19396          var sampleDescriptions = stsd.subarray(8); // gives the codec type string
19397
19398          track.codec = parseType$1(sampleDescriptions.subarray(4, 8));
19399          var codecBox = findBox$3(sampleDescriptions, [track.codec])[0];
19400          var codecConfig, codecConfigType;
19401
19402          if (codecBox) {
19403            // https://tools.ietf.org/html/rfc6381#section-3.3
19404            if (/^[asm]vc[1-9]$/i.test(track.codec)) {
19405              // we don't need anything but the "config" parameter of the
19406              // avc1 codecBox
19407              codecConfig = codecBox.subarray(78);
19408              codecConfigType = parseType$1(codecConfig.subarray(4, 8));
19409
19410              if (codecConfigType === 'avcC' && codecConfig.length > 11) {
19411                track.codec += '.'; // left padded with zeroes for single digit hex
19412                // profile idc
19413
19414                track.codec += toHexString(codecConfig[9]); // the byte containing the constraint_set flags
19415
19416                track.codec += toHexString(codecConfig[10]); // level idc
19417
19418                track.codec += toHexString(codecConfig[11]);
19419              } else {
19420                // TODO: show a warning that we couldn't parse the codec
19421                // and are using the default
19422                track.codec = 'avc1.4d400d';
19423              }
19424            } else if (/^mp4[a,v]$/i.test(track.codec)) {
19425              // we do not need anything but the streamDescriptor of the mp4a codecBox
19426              codecConfig = codecBox.subarray(28);
19427              codecConfigType = parseType$1(codecConfig.subarray(4, 8));
19428
19429              if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) {
19430                track.codec += '.' + toHexString(codecConfig[19]); // this value is only a single digit
19431
19432                track.codec += '.' + toHexString(codecConfig[20] >>> 2 & 0x3f).replace(/^0/, '');
19433              } else {
19434                // TODO: show a warning that we couldn't parse the codec
19435                // and are using the default
19436                track.codec = 'mp4a.40.2';
19437              }
19438            } else {
19439              // flac, opus, etc
19440              track.codec = track.codec.toLowerCase();
19441            }
19442          }
19443        }
19444
19445        var mdhd = findBox$3(trak, ['mdia', 'mdhd'])[0];
19446
19447        if (mdhd) {
19448          track.timescale = getTimescaleFromMediaHeader$1(mdhd);
19449        }
19450
19451        tracks.push(track);
19452      });
19453      return tracks;
19454    };
19455    /**
19456     * Returns an array of emsg ID3 data from the provided segmentData.
19457     * An offset can also be provided as the Latest Arrival Time to calculate
19457
19458     * the Event Start Time of v0 EMSG boxes.
19459     * See: https://dashif-documents.azurewebsites.net/Events/master/event.html#Inband-event-timing
19460     *
19461     * @param {Uint8Array} segmentData the segment byte array.
19462     * @param {number} offset the segment start time or Latest Arrival Time,
vendor: 61,727 bytes, lines 19462-21366
19462
19463     * @return {Object[]} an array of ID3 parsed from EMSG boxes
19464     */
19465
19466
19467    getEmsgID3 = function (segmentData, offset = 0) {
19468      var emsgBoxes = findBox$3(segmentData, ['emsg']);
19469      return emsgBoxes.map(data => {
19470        var parsedBox = emsg.parseEmsgBox(new Uint8Array(data));
19471        var parsedId3Frames = parseId3Frames(parsedBox.message_data);
19472        return {
19473          cueTime: emsg.scaleTime(parsedBox.presentation_time, parsedBox.timescale, parsedBox.presentation_time_delta, offset),
19474          duration: emsg.scaleTime(parsedBox.event_duration, parsedBox.timescale),
19475          frames: parsedId3Frames
19476        };
19477      });
19478    };
19479
19480    var probe$2 = {
19481      // export mp4 inspector's findBox and parseType for backwards compatibility
19482      findBox: findBox$3,
19483      parseType: parseType$1,
19484      timescale: timescale,
19485      startTime: startTime,
19486      compositionStartTime: compositionStartTime,
19487      videoTrackIds: getVideoTrackIds,
19488      tracks: getTracks,
19489      getTimescaleFromMediaHeader: getTimescaleFromMediaHeader$1,
19490      getEmsgID3: getEmsgID3
19491    };
19492    const {
19493      parseTrun
19494    } = mp4Inspector;
19495    const {
19496      findBox: findBox$2
19497    } = probe$2;
19498    var window$1 = window_1;
19499    /**
19500     * Utility function for parsing data from mdat boxes.
19501     * @param {Array<Uint8Array>} segment the segment data to create mdat/traf pairs from.
19502     * @returns mdat and traf boxes paired up for easier parsing.
19503     */
19504
19505    var getMdatTrafPairs$2 = function (segment) {
19506      var trafs = findBox$2(segment, ['moof', 'traf']);
19507      var mdats = findBox$2(segment, ['mdat']);
19508      var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs
19509
19510      mdats.forEach(function (mdat, index) {
19511        var matchingTraf = trafs[index];
19512        mdatTrafPairs.push({
19513          mdat: mdat,
19514          traf: matchingTraf
19515        });
19516      });
19517      return mdatTrafPairs;
19518    };
19519    /**
19520      * Parses sample information out of Track Run Boxes and calculates
19521      * the absolute presentation and decode timestamps of each sample.
19522      *
19523      * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed
19524      * @param {Number|BigInt} baseMediaDecodeTime - base media decode time from tfdt
19525          @see ISO-BMFF-12/2015, Section 8.8.12
19526      * @param {Object} tfhd - The parsed Track Fragment Header
19527      *   @see inspect.parseTfhd
19528      * @return {Object[]} the parsed samples
19529      *
19530      * @see ISO-BMFF-12/2015, Section 8.8.8
19531     **/
19532
19533
19534    var parseSamples$2 = function (truns, baseMediaDecodeTime, tfhd) {
19535      var currentDts = baseMediaDecodeTime;
19536      var defaultSampleDuration = tfhd.defaultSampleDuration || 0;
19537      var defaultSampleSize = tfhd.defaultSampleSize || 0;
19538      var trackId = tfhd.trackId;
19539      var allSamples = [];
19540      truns.forEach(function (trun) {
19541        // Note: We currently do not parse the sample table as well
19542        // as the trun. It's possible some sources will require this.
19543        // moov > trak > mdia > minf > stbl
19544        var trackRun = parseTrun(trun);
19545        var samples = trackRun.samples;
19546        samples.forEach(function (sample) {
19547          if (sample.duration === undefined) {
19548            sample.duration = defaultSampleDuration;
19549          }
19550
19551          if (sample.size === undefined) {
19552            sample.size = defaultSampleSize;
19553          }
19554
19555          sample.trackId = trackId;
19556          sample.dts = currentDts;
19557
19558          if (sample.compositionTimeOffset === undefined) {
19559            sample.compositionTimeOffset = 0;
19560          }
19561
19562          if (typeof currentDts === 'bigint') {
19563            sample.pts = currentDts + window$1.BigInt(sample.compositionTimeOffset);
19564            currentDts += window$1.BigInt(sample.duration);
19565          } else {
19566            sample.pts = currentDts + sample.compositionTimeOffset;
19567            currentDts += sample.duration;
19568          }
19569        });
19570        allSamples = allSamples.concat(samples);
19571      });
19572      return allSamples;
19573    };
19574
19575    var samples = {
19576      getMdatTrafPairs: getMdatTrafPairs$2,
19577      parseSamples: parseSamples$2
19578    };
19579    /**
19580     * mux.js
19581     *
19582     * Copyright (c) Brightcove
19583     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
19584     *
19585     * Reads in-band CEA-708 captions out of FMP4 segments.
19586     * @see https://en.wikipedia.org/wiki/CEA-708
19587     */
19588
19589    var discardEmulationPreventionBytes = captionPacketParser.discardEmulationPreventionBytes;
19590    var CaptionStream = captionStream.CaptionStream;
19591    var findBox$1 = findBox_1;
19592    var parseTfdt$1 = parseTfdt$3;
19593    var parseTfhd = parseTfhd$2;
19594    var {
19595      getMdatTrafPairs: getMdatTrafPairs$1,
19596      parseSamples: parseSamples$1
19597    } = samples;
19598    /**
19599      * Maps an offset in the mdat to a sample based on the the size of the samples.
19600      * Assumes that `parseSamples` has been called first.
19601      *
19602      * @param {Number} offset - The offset into the mdat
19603      * @param {Object[]} samples - An array of samples, parsed using `parseSamples`
19604      * @return {?Object} The matching sample, or null if no match was found.
19605      *
19606      * @see ISO-BMFF-12/2015, Section 8.8.8
19607     **/
19608
19609    var mapToSample = function (offset, samples) {
19610      var approximateOffset = offset;
19611
19612      for (var i = 0; i < samples.length; i++) {
19613        var sample = samples[i];
19614
19615        if (approximateOffset < sample.size) {
19616          return sample;
19617        }
19618
19619        approximateOffset -= sample.size;
19620      }
19621
19622      return null;
19623    };
19624    /**
19625      * Finds SEI nal units contained in a Media Data Box.
19626      * Assumes that `parseSamples` has been called first.
19627      *
19628      * @param {Uint8Array} avcStream - The bytes of the mdat
19629      * @param {Object[]} samples - The samples parsed out by `parseSamples`
19630      * @param {Number} trackId - The trackId of this video track
19631      * @return {Object[]} seiNals - the parsed SEI NALUs found.
19632      *   The contents of the seiNal should match what is expected by
19633      *   CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts)
19634      *
19635      * @see ISO-BMFF-12/2015, Section 8.1.1
19636      * @see Rec. ITU-T H.264, 7.3.2.3.1
19637     **/
19638
19639
19640    var findSeiNals = function (avcStream, samples, trackId) {
19641      var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength),
19642          result = {
19643        logs: [],
19644        seiNals: []
19645      },
19646          seiNal,
19647          i,
19648          length,
19649          lastMatchedSample;
19650
19651      for (i = 0; i + 4 < avcStream.length; i += length) {
19652        length = avcView.getUint32(i);
19653        i += 4; // Bail if this doesn't appear to be an H264 stream
19654
19655        if (length <= 0) {
19656          continue;
19657        }
19658
19659        switch (avcStream[i] & 0x1F) {
19660          case 0x06:
19661            var data = avcStream.subarray(i + 1, i + 1 + length);
19662            var matchingSample = mapToSample(i, samples);
19663            seiNal = {
19664              nalUnitType: 'sei_rbsp',
19665              size: length,
19666              data: data,
19667              escapedRBSP: discardEmulationPreventionBytes(data),
19668              trackId: trackId
19669            };
19670
19671            if (matchingSample) {
19672              seiNal.pts = matchingSample.pts;
19673              seiNal.dts = matchingSample.dts;
19674              lastMatchedSample = matchingSample;
19675            } else if (lastMatchedSample) {
19676              // If a matching sample cannot be found, use the last
19677              // sample's values as they should be as close as possible
19678              seiNal.pts = lastMatchedSample.pts;
19679              seiNal.dts = lastMatchedSample.dts;
19680            } else {
19681              result.logs.push({
19682                level: 'warn',
19683                message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.'
19684              });
19685              break;
19686            }
19687
19688            result.seiNals.push(seiNal);
19689            break;
19690        }
19691      }
19692
19693      return result;
19694    };
19695    /**
19696      * Parses out caption nals from an FMP4 segment's video tracks.
19697      *
19698      * @param {Uint8Array} segment - The bytes of a single segment
19699      * @param {Number} videoTrackId - The trackId of a video track in the segment
19700      * @return {Object.<Number, Object[]>} A mapping of video trackId to
19701      *   a list of seiNals found in that track
19702     **/
19703
19704
19705    var parseCaptionNals = function (segment, videoTrackId) {
19706      var captionNals = {};
19707      var mdatTrafPairs = getMdatTrafPairs$1(segment);
19708      mdatTrafPairs.forEach(function (pair) {
19709        var mdat = pair.mdat;
19710        var traf = pair.traf;
19711        var tfhd = findBox$1(traf, ['tfhd']); // Exactly 1 tfhd per traf
19712
19713        var headerInfo = parseTfhd(tfhd[0]);
19714        var trackId = headerInfo.trackId;
19715        var tfdt = findBox$1(traf, ['tfdt']); // Either 0 or 1 tfdt per traf
19716
19717        var baseMediaDecodeTime = tfdt.length > 0 ? parseTfdt$1(tfdt[0]).baseMediaDecodeTime : 0;
19718        var truns = findBox$1(traf, ['trun']);
19719        var samples;
19720        var result; // Only parse video data for the chosen video track
19721
19722        if (videoTrackId === trackId && truns.length > 0) {
19723          samples = parseSamples$1(truns, baseMediaDecodeTime, headerInfo);
19724          result = findSeiNals(mdat, samples, trackId);
19725
19726          if (!captionNals[trackId]) {
19727            captionNals[trackId] = {
19728              seiNals: [],
19729              logs: []
19730            };
19731          }
19732
19733          captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals);
19734          captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs);
19735        }
19736      });
19737      return captionNals;
19738    };
19739    /**
19740      * Parses out inband captions from an MP4 container and returns
19741      * caption objects that can be used by WebVTT and the TextTrack API.
19742      * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue
19743      * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack
19744      * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first
19745      *
19746      * @param {Uint8Array} segment - The fmp4 segment containing embedded captions
19747      * @param {Number} trackId - The id of the video track to parse
19748      * @param {Number} timescale - The timescale for the video track from the init segment
19749      *
19750      * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks
19751      * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds
19752      * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds
19753      * @return {Object[]} parsedCaptions[].content - A list of individual caption segments
19754      * @return {String} parsedCaptions[].content.text - The visible content of the caption segment
19755      * @return {Number} parsedCaptions[].content.line - The line height from 1-15 for positioning of the caption segment
19756      * @return {Number} parsedCaptions[].content.position - The column indent percentage for cue positioning from 10-80
19757     **/
19758
19759
19760    var parseEmbeddedCaptions = function (segment, trackId, timescale) {
19761      var captionNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
19762
19763      if (trackId === null) {
19764        return null;
19765      }
19766
19767      captionNals = parseCaptionNals(segment, trackId);
19768      var trackNals = captionNals[trackId] || {};
19769      return {
19770        seiNals: trackNals.seiNals,
19771        logs: trackNals.logs,
19772        timescale: timescale
19773      };
19774    };
19775    /**
19776      * Converts SEI NALUs into captions that can be used by video.js
19777     **/
19778
19779
19780    var CaptionParser = function () {
19781      var isInitialized = false;
19782      var captionStream; // Stores segments seen before trackId and timescale are set
19783
19784      var segmentCache; // Stores video track ID of the track being parsed
19785
19786      var trackId; // Stores the timescale of the track being parsed
19787
19788      var timescale; // Stores captions parsed so far
19789
19790      var parsedCaptions; // Stores whether we are receiving partial data or not
19791
19792      var parsingPartial;
19793      /**
19794        * A method to indicate whether a CaptionParser has been initalized
19795        * @returns {Boolean}
19796       **/
19797
19798      this.isInitialized = function () {
19799        return isInitialized;
19800      };
19801      /**
19802        * Initializes the underlying CaptionStream, SEI NAL parsing
19803        * and management, and caption collection
19804       **/
19805
19806
19807      this.init = function (options) {
19808        captionStream = new CaptionStream();
19809        isInitialized = true;
19810        parsingPartial = options ? options.isPartial : false; // Collect dispatched captions
19811
19812        captionStream.on('data', function (event) {
19813          // Convert to seconds in the source's timescale
19814          event.startTime = event.startPts / timescale;
19815          event.endTime = event.endPts / timescale;
19816          parsedCaptions.captions.push(event);
19817          parsedCaptions.captionStreams[event.stream] = true;
19818        });
19819        captionStream.on('log', function (log) {
19820          parsedCaptions.logs.push(log);
19821        });
19822      };
19823      /**
19824        * Determines if a new video track will be selected
19825        * or if the timescale changed
19826        * @return {Boolean}
19827       **/
19828
19829
19830      this.isNewInit = function (videoTrackIds, timescales) {
19831        if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) {
19832          return false;
19833        }
19834
19835        return trackId !== videoTrackIds[0] || timescale !== timescales[trackId];
19836      };
19837      /**
19838        * Parses out SEI captions and interacts with underlying
19839        * CaptionStream to return dispatched captions
19840        *
19841        * @param {Uint8Array} segment - The fmp4 segment containing embedded captions
19842        * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment
19843        * @param {Object.<Number, Number>} timescales - The timescales found in the init segment
19844        * @see parseEmbeddedCaptions
19845        * @see m2ts/caption-stream.js
19846       **/
19847
19848
19849      this.parse = function (segment, videoTrackIds, timescales) {
19850        var parsedData;
19851
19852        if (!this.isInitialized()) {
19853          return null; // This is not likely to be a video segment
19854        } else if (!videoTrackIds || !timescales) {
19855          return null;
19856        } else if (this.isNewInit(videoTrackIds, timescales)) {
19857          // Use the first video track only as there is no
19858          // mechanism to switch to other video tracks
19859          trackId = videoTrackIds[0];
19860          timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment
19861          // data until we have one.
19862          // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there
19863        } else if (trackId === null || !timescale) {
19864          segmentCache.push(segment);
19865          return null;
19866        } // Now that a timescale and trackId is set, parse cached segments
19867
19868
19869        while (segmentCache.length > 0) {
19870          var cachedSegment = segmentCache.shift();
19871          this.parse(cachedSegment, videoTrackIds, timescales);
19872        }
19873
19874        parsedData = parseEmbeddedCaptions(segment, trackId, timescale);
19875
19876        if (parsedData && parsedData.logs) {
19877          parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs);
19878        }
19879
19880        if (parsedData === null || !parsedData.seiNals) {
19881          if (parsedCaptions.logs.length) {
19882            return {
19883              logs: parsedCaptions.logs,
19884              captions: [],
19885              captionStreams: []
19886            };
19887          }
19888
19889          return null;
19890        }
19891
19892        this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched
19893
19894        this.flushStream();
19895        return parsedCaptions;
19896      };
19897      /**
19898        * Pushes SEI NALUs onto CaptionStream
19899        * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals`
19900        * Assumes that `parseCaptionNals` has been called first
19901        * @see m2ts/caption-stream.js
19902        **/
19903
19904
19905      this.pushNals = function (nals) {
19906        if (!this.isInitialized() || !nals || nals.length === 0) {
19907          return null;
19908        }
19909
19910        nals.forEach(function (nal) {
19911          captionStream.push(nal);
19912        });
19913      };
19914      /**
19915        * Flushes underlying CaptionStream to dispatch processed, displayable captions
19916        * @see m2ts/caption-stream.js
19917       **/
19918
19919
19920      this.flushStream = function () {
19921        if (!this.isInitialized()) {
19922          return null;
19923        }
19924
19925        if (!parsingPartial) {
19926          captionStream.flush();
19927        } else {
19928          captionStream.partialFlush();
19929        }
19930      };
19931      /**
19932        * Reset caption buckets for new data
19933       **/
19934
19935
19936      this.clearParsedCaptions = function () {
19937        parsedCaptions.captions = [];
19938        parsedCaptions.captionStreams = {};
19939        parsedCaptions.logs = [];
19940      };
19941      /**
19942        * Resets underlying CaptionStream
19943        * @see m2ts/caption-stream.js
19944       **/
19945
19946
19947      this.resetCaptionStream = function () {
19948        if (!this.isInitialized()) {
19949          return null;
19950        }
19951
19952        captionStream.reset();
19953      };
19954      /**
19955        * Convenience method to clear all captions flushed from the
19956        * CaptionStream and still being parsed
19957        * @see m2ts/caption-stream.js
19958       **/
19959
19960
19961      this.clearAllCaptions = function () {
19962        this.clearParsedCaptions();
19963        this.resetCaptionStream();
19964      };
19965      /**
19966        * Reset caption parser
19967       **/
19968
19969
19970      this.reset = function () {
19971        segmentCache = [];
19972        trackId = null;
19973        timescale = null;
19974
19975        if (!parsedCaptions) {
19976          parsedCaptions = {
19977            captions: [],
19978            // CC1, CC2, CC3, CC4
19979            captionStreams: {},
19980            logs: []
19981          };
19982        } else {
19983          this.clearParsedCaptions();
19984        }
19985
19986        this.resetCaptionStream();
19987      };
19988
19989      this.reset();
19990    };
19991
19992    var captionParser = CaptionParser;
19993    const {
19994      parseTfdt
19995    } = mp4Inspector;
19996    const findBox = findBox_1;
19997    const {
19998      getTimescaleFromMediaHeader
19999    } = probe$2;
20000    const {
20001      parseSamples,
20002      getMdatTrafPairs
20003    } = samples;
20004    /**
20005     * Module for parsing WebVTT text and styles from FMP4 segments.
20006     * Based on the ISO/IEC 14496-30.
20007     */
20008
20009    const WebVttParser = function () {
20010      // default timescale to 90k
20011      let timescale = 90e3;
20012      /**
20013       * Parses the timescale from the init segment.
20014       * @param {Array<Uint8Array>} segment The initialization segment to parse the timescale from.
20015       */
20016
20017      this.init = function (segment) {
20018        // We just need the timescale from the init segment.
20019        const mdhd = findBox(segment, ['moov', 'trak', 'mdia', 'mdhd'])[0];
20020
20021        if (mdhd) {
20022          timescale = getTimescaleFromMediaHeader(mdhd);
20023        }
20024      };
20025      /**
20026       * Parses a WebVTT FMP4 segment.
20027       * @param {Array<Uint8Array>} segment The content segment to parse the WebVTT cues from.
20028       * @returns The WebVTT cue text, styling, and timing info as an array of cue objects.
20029       */
20030
20031
20032      this.parseSegment = function (segment) {
20033        const vttCues = [];
20034        const mdatTrafPairs = getMdatTrafPairs(segment);
20035        let baseMediaDecodeTime = 0;
20036        mdatTrafPairs.forEach(function (pair) {
20037          const mdatBox = pair.mdat;
20038          const trafBox = pair.traf; // zero or one.
20039
20040          const tfdtBox = findBox(trafBox, ['tfdt'])[0]; // zero or one.
20041
20042          const tfhdBox = findBox(trafBox, ['tfhd'])[0]; // zero or more.
20043
20044          const trunBoxes = findBox(trafBox, ['trun']);
20045
20046          if (tfdtBox) {
20047            const tfdt = parseTfdt(tfdtBox);
20048            baseMediaDecodeTime = tfdt.baseMediaDecodeTime;
20049          }
20050
20051          if (trunBoxes.length && tfhdBox) {
20052            const samples = parseSamples(trunBoxes, baseMediaDecodeTime, tfhdBox);
20053            let mdatOffset = 0;
20054            samples.forEach(function (sample) {
20055              // decode utf8 payload
20056              const UTF_8 = 'utf-8';
20057              const textDecoder = new TextDecoder(UTF_8); // extract sample data from the mdat box.
20058              // WebVTT Sample format:
20059              // Exactly one VTTEmptyCueBox box
20060              // OR one or more VTTCueBox boxes.
20061
20062              const sampleData = mdatBox.slice(mdatOffset, mdatOffset + sample.size); // single vtte box.
20063
20064              const vtteBox = findBox(sampleData, ['vtte'])[0]; // empty box
20065
20066              if (vtteBox) {
20067                mdatOffset += sample.size;
20068                return;
20069              } // TODO: Support 'vtta' boxes.
20070              // VTTAdditionalTextBoxes can be interleaved between VTTCueBoxes.
20071
20072
20073              const vttcBoxes = findBox(sampleData, ['vttc']);
20074              vttcBoxes.forEach(function (vttcBox) {
20075                // mandatory payload box.
20076                const paylBox = findBox(vttcBox, ['payl'])[0]; // optional settings box
20077
20078                const sttgBox = findBox(vttcBox, ['sttg'])[0];
20079                const start = sample.pts / timescale;
20080                const end = (sample.pts + sample.duration) / timescale;
20081                let cueText, settings; // contains cue text.
20082
20083                if (paylBox) {
20084                  try {
20085                    cueText = textDecoder.decode(paylBox);
20086                  } catch (e) {
20087                    console.error(e);
20088                  }
20089                } // settings box contains styling.
20090
20091
20092                if (sttgBox) {
20093                  try {
20094                    settings = textDecoder.decode(sttgBox);
20095                  } catch (e) {
20096                    console.error(e);
20097                  }
20098                }
20099
20100                if (sample.duration && cueText) {
20101                  vttCues.push({
20102                    cueText,
20103                    start,
20104                    end,
20105                    settings
20106                  });
20107                }
20108              });
20109              mdatOffset += sample.size;
20110            });
20111          }
20112        });
20113        return vttCues;
20114      };
20115    };
20116
20117    var webvttParser = WebVttParser;
20118    /**
20119     * mux.js
20120     *
20121     * Copyright (c) Brightcove
20122     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
20123     *
20124     * Utilities to detect basic properties and metadata about TS Segments.
20125     */
20126
20127    var StreamTypes$1 = streamTypes;
20128
20129    var parsePid = function (packet) {
20130      var pid = packet[1] & 0x1f;
20131      pid <<= 8;
20132      pid |= packet[2];
20133      return pid;
20134    };
20135
20136    var parsePayloadUnitStartIndicator = function (packet) {
20137      return !!(packet[1] & 0x40);
20138    };
20139
20140    var parseAdaptionField = function (packet) {
20141      var offset = 0; // if an adaption field is present, its length is specified by the
20142      // fifth byte of the TS packet header. The adaptation field is
20143      // used to add stuffing to PES packets that don't fill a complete
20144      // TS packet, and to specify some forms of timing and control data
20145      // that we do not currently use.
20146
20147      if ((packet[3] & 0x30) >>> 4 > 0x01) {
20148        offset += packet[4] + 1;
20149      }
20150
20151      return offset;
20152    };
20153
20154    var parseType = function (packet, pmtPid) {
20155      var pid = parsePid(packet);
20156
20157      if (pid === 0) {
20158        return 'pat';
20159      } else if (pid === pmtPid) {
20160        return 'pmt';
20161      } else if (pmtPid) {
20162        return 'pes';
20163      }
20164
20165      return null;
20166    };
20167
20168    var parsePat = function (packet) {
20169      var pusi = parsePayloadUnitStartIndicator(packet);
20170      var offset = 4 + parseAdaptionField(packet);
20171
20172      if (pusi) {
20173        offset += packet[offset] + 1;
20174      }
20175
20176      return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11];
20177    };
20178
20179    var parsePmt = function (packet) {
20180      var programMapTable = {};
20181      var pusi = parsePayloadUnitStartIndicator(packet);
20182      var payloadOffset = 4 + parseAdaptionField(packet);
20183
20184      if (pusi) {
20185        payloadOffset += packet[payloadOffset] + 1;
20186      } // PMTs can be sent ahead of the time when they should actually
20187      // take effect. We don't believe this should ever be the case
20188      // for HLS but we'll ignore "forward" PMT declarations if we see
20189      // them. Future PMT declarations have the current_next_indicator
20190      // set to zero.
20191
20192
20193      if (!(packet[payloadOffset + 5] & 0x01)) {
20194        return;
20195      }
20196
20197      var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section
20198
20199      sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2];
20200      tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how
20201      // long the program info descriptors are
20202
20203      programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table
20204
20205      var offset = 12 + programInfoLength;
20206
20207      while (offset < tableEnd) {
20208        var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type
20209
20210        programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; // move to the next table entry
20211        // skip past the elementary stream descriptors, if present
20212
20213        offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5;
20214      }
20215
20216      return programMapTable;
20217    };
20218
20219    var parsePesType = function (packet, programMapTable) {
20220      var pid = parsePid(packet);
20221      var type = programMapTable[pid];
20222
20223      switch (type) {
20224        case StreamTypes$1.H264_STREAM_TYPE:
20225          return 'video';
20226
20227        case StreamTypes$1.ADTS_STREAM_TYPE:
20228          return 'audio';
20229
20230        case StreamTypes$1.METADATA_STREAM_TYPE:
20231          return 'timed-metadata';
20232
20233        default:
20234          return null;
20235      }
20236    };
20237
20238    var parsePesTime = function (packet) {
20239      var pusi = parsePayloadUnitStartIndicator(packet);
20240
20241      if (!pusi) {
20242        return null;
20243      }
20244
20245      var offset = 4 + parseAdaptionField(packet);
20246
20247      if (offset >= packet.byteLength) {
20248        // From the H 222.0 MPEG-TS spec
20249        // "For transport stream packets carrying PES packets, stuffing is needed when there
20250        //  is insufficient PES packet data to completely fill the transport stream packet
20251        //  payload bytes. Stuffing is accomplished by defining an adaptation field longer than
20252        //  the sum of the lengths of the data elements in it, so that the payload bytes
20253        //  remaining after the adaptation field exactly accommodates the available PES packet
20254        //  data."
20255        //
20256        // If the offset is >= the length of the packet, then the packet contains no data
20257        // and instead is just adaption field stuffing bytes
20258        return null;
20259      }
20260
20261      var pes = null;
20262      var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value
20263      // and a DTS value. Determine what combination of values is
20264      // available to work with.
20265
20266      ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number.  Javascript
20267      // performs all bitwise operations on 32-bit integers but javascript
20268      // supports a much greater range (52-bits) of integer using standard
20269      // mathematical operations.
20270      // We construct a 31-bit value using bitwise operators over the 31
20271      // most significant bits and then multiply by 4 (equal to a left-shift
20272      // of 2) before we add the final 2 least significant bits of the
20273      // timestamp (equal to an OR.)
20274
20275      if (ptsDtsFlags & 0xC0) {
20276        pes = {}; // the PTS and DTS are not written out directly. For information
20277        // on how they are encoded, see
20278        // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
20279
20280        pes.pts = (packet[offset + 9] & 0x0E) << 27 | (packet[offset + 10] & 0xFF) << 20 | (packet[offset + 11] & 0xFE) << 12 | (packet[offset + 12] & 0xFF) << 5 | (packet[offset + 13] & 0xFE) >>> 3;
20281        pes.pts *= 4; // Left shift by 2
20282
20283        pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs
20284
20285        pes.dts = pes.pts;
20286
20287        if (ptsDtsFlags & 0x40) {
20288          pes.dts = (packet[offset + 14] & 0x0E) << 27 | (packet[offset + 15] & 0xFF) << 20 | (packet[offset + 16] & 0xFE) << 12 | (packet[offset + 17] & 0xFF) << 5 | (packet[offset + 18] & 0xFE) >>> 3;
20289          pes.dts *= 4; // Left shift by 2
20290
20291          pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs
20292        }
20293      }
20294
20295      return pes;
20296    };
20297
20298    var parseNalUnitType = function (type) {
20299      switch (type) {
20300        case 0x05:
20301          return 'slice_layer_without_partitioning_rbsp_idr';
20302
20303        case 0x06:
20304          return 'sei_rbsp';
20305
20306        case 0x07:
20307          return 'seq_parameter_set_rbsp';
20308
20309        case 0x08:
20310          return 'pic_parameter_set_rbsp';
20311
20312        case 0x09:
20313          return 'access_unit_delimiter_rbsp';
20314
20315        default:
20316          return null;
20317      }
20318    };
20319
20320    var videoPacketContainsKeyFrame = function (packet) {
20321      var offset = 4 + parseAdaptionField(packet);
20322      var frameBuffer = packet.subarray(offset);
20323      var frameI = 0;
20324      var frameSyncPoint = 0;
20325      var foundKeyFrame = false;
20326      var nalType; // advance the sync point to a NAL start, if necessary
20327
20328      for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) {
20329        if (frameBuffer[frameSyncPoint + 2] === 1) {
20330          // the sync point is properly aligned
20331          frameI = frameSyncPoint + 5;
20332          break;
20333        }
20334      }
20335
20336      while (frameI < frameBuffer.byteLength) {
20337        // look at the current byte to determine if we've hit the end of
20338        // a NAL unit boundary
20339        switch (frameBuffer[frameI]) {
20340          case 0:
20341            // skip past non-sync sequences
20342            if (frameBuffer[frameI - 1] !== 0) {
20343              frameI += 2;
20344              break;
20345            } else if (frameBuffer[frameI - 2] !== 0) {
20346              frameI++;
20347              break;
20348            }
20349
20350            if (frameSyncPoint + 3 !== frameI - 2) {
20351              nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
20352
20353              if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
20354                foundKeyFrame = true;
20355              }
20356            } // drop trailing zeroes
20357
20358
20359            do {
20360              frameI++;
20361            } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length);
20362
20363            frameSyncPoint = frameI - 2;
20364            frameI += 3;
20365            break;
20366
20367          case 1:
20368            // skip past non-sync sequences
20369            if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) {
20370              frameI += 3;
20371              break;
20372            }
20373
20374            nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
20375
20376            if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
20377              foundKeyFrame = true;
20378            }
20379
20380            frameSyncPoint = frameI - 2;
20381            frameI += 3;
20382            break;
20383
20384          default:
20385            // the current byte isn't a one or zero, so it cannot be part
20386            // of a sync sequence
20387            frameI += 3;
20388            break;
20389        }
20390      }
20391
20392      frameBuffer = frameBuffer.subarray(frameSyncPoint);
20393      frameI -= frameSyncPoint;
20394      frameSyncPoint = 0; // parse the final nal
20395
20396      if (frameBuffer && frameBuffer.byteLength > 3) {
20397        nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f);
20398
20399        if (nalType === 'slice_layer_without_partitioning_rbsp_idr') {
20400          foundKeyFrame = true;
20401        }
20402      }
20403
20404      return foundKeyFrame;
20405    };
20406
20407    var probe$1 = {
20408      parseType: parseType,
20409      parsePat: parsePat,
20410      parsePmt: parsePmt,
20411      parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator,
20412      parsePesType: parsePesType,
20413      parsePesTime: parsePesTime,
20414      videoPacketContainsKeyFrame: videoPacketContainsKeyFrame
20415    };
20416    /**
20417     * mux.js
20418     *
20419     * Copyright (c) Brightcove
20420     * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
20421     *
20422     * Parse mpeg2 transport stream packets to extract basic timing information
20423     */
20424
20425    var StreamTypes = streamTypes;
20426    var handleRollover = timestampRolloverStream.handleRollover;
20427    var probe = {};
20428    probe.ts = probe$1;
20429    probe.aac = utils;
20430    var ONE_SECOND_IN_TS = clock$2.ONE_SECOND_IN_TS;
20431    var MP2T_PACKET_LENGTH = 188,
20432        // bytes
20433    SYNC_BYTE = 0x47;
20434    /**
20435     * walks through segment data looking for pat and pmt packets to parse out
20436     * program map table information
20437     */
20438
20439    var parsePsi_ = function (bytes, pmt) {
20440      var startIndex = 0,
20441          endIndex = MP2T_PACKET_LENGTH,
20442          packet,
20443          type;
20444
20445      while (endIndex < bytes.byteLength) {
20446        // Look for a pair of start and end sync bytes in the data..
20447        if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) {
20448          // We found a packet
20449          packet = bytes.subarray(startIndex, endIndex);
20450          type = probe.ts.parseType(packet, pmt.pid);
20451
20452          switch (type) {
20453            case 'pat':
20454              pmt.pid = probe.ts.parsePat(packet);
20455              break;
20456
20457            case 'pmt':
20458              var table = probe.ts.parsePmt(packet);
20459              pmt.table = pmt.table || {};
20460              Object.keys(table).forEach(function (key) {
20461                pmt.table[key] = table[key];
20462              });
20463              break;
20464          }
20465
20466          startIndex += MP2T_PACKET_LENGTH;
20467          endIndex += MP2T_PACKET_LENGTH;
20468          continue;
20469        } // If we get here, we have somehow become de-synchronized and we need to step
20470        // forward one byte at a time until we find a pair of sync bytes that denote
20471        // a packet
20472
20473
20474        startIndex++;
20475        endIndex++;
20476      }
20477    };
20478    /**
20479     * walks through the segment data from the start and end to get timing information
20480     * for the first and last audio pes packets
20481     */
20482
20483
20484    var parseAudioPes_ = function (bytes, pmt, result) {
20485      var startIndex = 0,
20486          endIndex = MP2T_PACKET_LENGTH,
20487          packet,
20488          type,
20489          pesType,
20490          pusi,
20491          parsed;
20492      var endLoop = false; // Start walking from start of segment to get first audio packet
20493
20494      while (endIndex <= bytes.byteLength) {
20495        // Look for a pair of start and end sync bytes in the data..
20496        if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) {
20497          // We found a packet
20498          packet = bytes.subarray(startIndex, endIndex);
20499          type = probe.ts.parseType(packet, pmt.pid);
20500
20501          switch (type) {
20502            case 'pes':
20503              pesType = probe.ts.parsePesType(packet, pmt.table);
20504              pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
20505
20506              if (pesType === 'audio' && pusi) {
20507                parsed = probe.ts.parsePesTime(packet);
20508
20509                if (parsed) {
20510                  parsed.type = 'audio';
20511                  result.audio.push(parsed);
20512                  endLoop = true;
20513                }
20514              }
20515
20516              break;
20517          }
20518
20519          if (endLoop) {
20520            break;
20521          }
20522
20523          startIndex += MP2T_PACKET_LENGTH;
20524          endIndex += MP2T_PACKET_LENGTH;
20525          continue;
20526        } // If we get here, we have somehow become de-synchronized and we need to step
20527        // forward one byte at a time until we find a pair of sync bytes that denote
20528        // a packet
20529
20530
20531        startIndex++;
20532        endIndex++;
20533      } // Start walking from end of segment to get last audio packet
20534
20535
20536      endIndex = bytes.byteLength;
20537      startIndex = endIndex - MP2T_PACKET_LENGTH;
20538      endLoop = false;
20539
20540      while (startIndex >= 0) {
20541        // Look for a pair of start and end sync bytes in the data..
20542        if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) {
20543          // We found a packet
20544          packet = bytes.subarray(startIndex, endIndex);
20545          type = probe.ts.parseType(packet, pmt.pid);
20546
20547          switch (type) {
20548            case 'pes':
20549              pesType = probe.ts.parsePesType(packet, pmt.table);
20550              pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
20551
20552              if (pesType === 'audio' && pusi) {
20553                parsed = probe.ts.parsePesTime(packet);
20554
20555                if (parsed) {
20556                  parsed.type = 'audio';
20557                  result.audio.push(parsed);
20558                  endLoop = true;
20559                }
20560              }
20561
20562              break;
20563          }
20564
20565          if (endLoop) {
20566            break;
20567          }
20568
20569          startIndex -= MP2T_PACKET_LENGTH;
20570          endIndex -= MP2T_PACKET_LENGTH;
20571          continue;
20572        } // If we get here, we have somehow become de-synchronized and we need to step
20573        // forward one byte at a time until we find a pair of sync bytes that denote
20574        // a packet
20575
20576
20577        startIndex--;
20578        endIndex--;
20579      }
20580    };
20581    /**
20582     * walks through the segment data from the start and end to get timing information
20583     * for the first and last video pes packets as well as timing information for the first
20584     * key frame.
20585     */
20586
20587
20588    var parseVideoPes_ = function (bytes, pmt, result) {
20589      var startIndex = 0,
20590          endIndex = MP2T_PACKET_LENGTH,
20591          packet,
20592          type,
20593          pesType,
20594          pusi,
20595          parsed,
20596          frame,
20597          i,
20598          pes;
20599      var endLoop = false;
20600      var currentFrame = {
20601        data: [],
20602        size: 0
20603      }; // Start walking from start of segment to get first video packet
20604
20605      while (endIndex < bytes.byteLength) {
20606        // Look for a pair of start and end sync bytes in the data..
20607        if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) {
20608          // We found a packet
20609          packet = bytes.subarray(startIndex, endIndex);
20610          type = probe.ts.parseType(packet, pmt.pid);
20611
20612          switch (type) {
20613            case 'pes':
20614              pesType = probe.ts.parsePesType(packet, pmt.table);
20615              pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
20616
20617              if (pesType === 'video') {
20618                if (pusi && !endLoop) {
20619                  parsed = probe.ts.parsePesTime(packet);
20620
20621                  if (parsed) {
20622                    parsed.type = 'video';
20623                    result.video.push(parsed);
20624                    endLoop = true;
20625                  }
20626                }
20627
20628                if (!result.firstKeyFrame) {
20629                  if (pusi) {
20630                    if (currentFrame.size !== 0) {
20631                      frame = new Uint8Array(currentFrame.size);
20632                      i = 0;
20633
20634                      while (currentFrame.data.length) {
20635                        pes = currentFrame.data.shift();
20636                        frame.set(pes, i);
20637                        i += pes.byteLength;
20638                      }
20639
20640                      if (probe.ts.videoPacketContainsKeyFrame(frame)) {
20641                        var firstKeyFrame = probe.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting
20642                        // the keyframe seems to work fine with HLS playback
20643                        // and definitely preferable to a crash with TypeError...
20644
20645                        if (firstKeyFrame) {
20646                          result.firstKeyFrame = firstKeyFrame;
20647                          result.firstKeyFrame.type = 'video';
20648                        } else {
20649                          // eslint-disable-next-line
20650                          console.warn('Failed to extract PTS/DTS from PES at first keyframe. ' + 'This could be an unusual TS segment, or else mux.js did not ' + 'parse your TS segment correctly. If you know your TS ' + 'segments do contain PTS/DTS on keyframes please file a bug ' + 'report! You can try ffprobe to double check for yourself.');
20651                        }
20652                      }
20653
20654                      currentFrame.size = 0;
20655                    }
20656                  }
20657
20658                  currentFrame.data.push(packet);
20659                  currentFrame.size += packet.byteLength;
20660                }
20661              }
20662
20663              break;
20664          }
20665
20666          if (endLoop && result.firstKeyFrame) {
20667            break;
20668          }
20669
20670          startIndex += MP2T_PACKET_LENGTH;
20671          endIndex += MP2T_PACKET_LENGTH;
20672          continue;
20673        } // If we get here, we have somehow become de-synchronized and we need to step
20674        // forward one byte at a time until we find a pair of sync bytes that denote
20675        // a packet
20676
20677
20678        startIndex++;
20679        endIndex++;
20680      } // Start walking from end of segment to get last video packet
20681
20682
20683      endIndex = bytes.byteLength;
20684      startIndex = endIndex - MP2T_PACKET_LENGTH;
20685      endLoop = false;
20686
20687      while (startIndex >= 0) {
20688        // Look for a pair of start and end sync bytes in the data..
20689        if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) {
20690          // We found a packet
20691          packet = bytes.subarray(startIndex, endIndex);
20692          type = probe.ts.parseType(packet, pmt.pid);
20693
20694          switch (type) {
20695            case 'pes':
20696              pesType = probe.ts.parsePesType(packet, pmt.table);
20697              pusi = probe.ts.parsePayloadUnitStartIndicator(packet);
20698
20699              if (pesType === 'video' && pusi) {
20700                parsed = probe.ts.parsePesTime(packet);
20701
20702                if (parsed) {
20703                  parsed.type = 'video';
20704                  result.video.push(parsed);
20705                  endLoop = true;
20706                }
20707              }
20708
20709              break;
20710          }
20711
20712          if (endLoop) {
20713            break;
20714          }
20715
20716          startIndex -= MP2T_PACKET_LENGTH;
20717          endIndex -= MP2T_PACKET_LENGTH;
20718          continue;
20719        } // If we get here, we have somehow become de-synchronized and we need to step
20720        // forward one byte at a time until we find a pair of sync bytes that denote
20721        // a packet
20722
20723
20724        startIndex--;
20725        endIndex--;
20726      }
20727    };
20728    /**
20729     * Adjusts the timestamp information for the segment to account for
20730     * rollover and convert to seconds based on pes packet timescale (90khz clock)
20731     */
20732
20733
20734    var adjustTimestamp_ = function (segmentInfo, baseTimestamp) {
20735      if (segmentInfo.audio && segmentInfo.audio.length) {
20736        var audioBaseTimestamp = baseTimestamp;
20737
20738        if (typeof audioBaseTimestamp === 'undefined' || isNaN(audioBaseTimestamp)) {
20739          audioBaseTimestamp = segmentInfo.audio[0].dts;
20740        }
20741
20742        segmentInfo.audio.forEach(function (info) {
20743          info.dts = handleRollover(info.dts, audioBaseTimestamp);
20744          info.pts = handleRollover(info.pts, audioBaseTimestamp); // time in seconds
20745
20746          info.dtsTime = info.dts / ONE_SECOND_IN_TS;
20747          info.ptsTime = info.pts / ONE_SECOND_IN_TS;
20748        });
20749      }
20750
20751      if (segmentInfo.video && segmentInfo.video.length) {
20752        var videoBaseTimestamp = baseTimestamp;
20753
20754        if (typeof videoBaseTimestamp === 'undefined' || isNaN(videoBaseTimestamp)) {
20755          videoBaseTimestamp = segmentInfo.video[0].dts;
20756        }
20757
20758        segmentInfo.video.forEach(function (info) {
20759          info.dts = handleRollover(info.dts, videoBaseTimestamp);
20760          info.pts = handleRollover(info.pts, videoBaseTimestamp); // time in seconds
20761
20762          info.dtsTime = info.dts / ONE_SECOND_IN_TS;
20763          info.ptsTime = info.pts / ONE_SECOND_IN_TS;
20764        });
20765
20766        if (segmentInfo.firstKeyFrame) {
20767          var frame = segmentInfo.firstKeyFrame;
20768          frame.dts = handleRollover(frame.dts, videoBaseTimestamp);
20769          frame.pts = handleRollover(frame.pts, videoBaseTimestamp); // time in seconds
20770
20771          frame.dtsTime = frame.dts / ONE_SECOND_IN_TS;
20772          frame.ptsTime = frame.pts / ONE_SECOND_IN_TS;
20773        }
20774      }
20775    };
20776    /**
20777     * inspects the aac data stream for start and end time information
20778     */
20779
20780
20781    var inspectAac_ = function (bytes) {
20782      var endLoop = false,
20783          audioCount = 0,
20784          sampleRate = null,
20785          timestamp = null,
20786          frameSize = 0,
20787          byteIndex = 0,
20788          packet;
20789
20790      while (bytes.length - byteIndex >= 3) {
20791        var type = probe.aac.parseType(bytes, byteIndex);
20792
20793        switch (type) {
20794          case 'timed-metadata':
20795            // Exit early because we don't have enough to parse
20796            // the ID3 tag header
20797            if (bytes.length - byteIndex < 10) {
20798              endLoop = true;
20799              break;
20800            }
20801
20802            frameSize = probe.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer
20803            // to emit a full packet
20804
20805            if (frameSize > bytes.length) {
20806              endLoop = true;
20807              break;
20808            }
20809
20810            if (timestamp === null) {
20811              packet = bytes.subarray(byteIndex, byteIndex + frameSize);
20812              timestamp = probe.aac.parseAacTimestamp(packet);
20813            }
20814
20815            byteIndex += frameSize;
20816            break;
20817
20818          case 'audio':
20819            // Exit early because we don't have enough to parse
20820            // the ADTS frame header
20821            if (bytes.length - byteIndex < 7) {
20822              endLoop = true;
20823              break;
20824            }
20825
20826            frameSize = probe.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer
20827            // to emit a full packet
20828
20829            if (frameSize > bytes.length) {
20830              endLoop = true;
20831              break;
20832            }
20833
20834            if (sampleRate === null) {
20835              packet = bytes.subarray(byteIndex, byteIndex + frameSize);
20836              sampleRate = probe.aac.parseSampleRate(packet);
20837            }
20838
20839            audioCount++;
20840            byteIndex += frameSize;
20841            break;
20842
20843          default:
20844            byteIndex++;
20845            break;
20846        }
20847
20848        if (endLoop) {
20849          return null;
20850        }
20851      }
20852
20853      if (sampleRate === null || timestamp === null) {
20854        return null;
20855      }
20856
20857      var audioTimescale = ONE_SECOND_IN_TS / sampleRate;
20858      var result = {
20859        audio: [{
20860          type: 'audio',
20861          dts: timestamp,
20862          pts: timestamp
20863        }, {
20864          type: 'audio',
20865          dts: timestamp + audioCount * 1024 * audioTimescale,
20866          pts: timestamp + audioCount * 1024 * audioTimescale
20867        }]
20868      };
20869      return result;
20870    };
20871    /**
20872     * inspects the transport stream segment data for start and end time information
20873     * of the audio and video tracks (when present) as well as the first key frame's
20874     * start time.
20875     */
20876
20877
20878    var inspectTs_ = function (bytes) {
20879      var pmt = {
20880        pid: null,
20881        table: null
20882      };
20883      var result = {};
20884      parsePsi_(bytes, pmt);
20885
20886      for (var pid in pmt.table) {
20887        if (pmt.table.hasOwnProperty(pid)) {
20888          var type = pmt.table[pid];
20889
20890          switch (type) {
20891            case StreamTypes.H264_STREAM_TYPE:
20892              result.video = [];
20893              parseVideoPes_(bytes, pmt, result);
20894
20895              if (result.video.length === 0) {
20896                delete result.video;
20897              }
20898
20899              break;
20900
20901            case StreamTypes.ADTS_STREAM_TYPE:
20902              result.audio = [];
20903              parseAudioPes_(bytes, pmt, result);
20904
20905              if (result.audio.length === 0) {
20906                delete result.audio;
20907              }
20908
20909              break;
20910          }
20911        }
20912      }
20913
20914      return result;
20915    };
20916    /**
20917     * Inspects segment byte data and returns an object with start and end timing information
20918     *
20919     * @param {Uint8Array} bytes The segment byte data
20920     * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame
20921     *  timestamps for rollover. This value must be in 90khz clock.
20922     * @return {Object} Object containing start and end frame timing info of segment.
20923     */
20924
20925
20926    var inspect = function (bytes, baseTimestamp) {
20927      var isAacData = probe.aac.isLikelyAacData(bytes);
20928      var result;
20929
20930      if (isAacData) {
20931        result = inspectAac_(bytes);
20932      } else {
20933        result = inspectTs_(bytes);
20934      }
20935
20936      if (!result || !result.audio && !result.video) {
20937        return null;
20938      }
20939
20940      adjustTimestamp_(result, baseTimestamp);
20941      return result;
20942    };
20943
20944    var tsInspector = {
20945      inspect: inspect,
20946      parseAudioPes_: parseAudioPes_
20947    };
20948    /* global self */
20949
20950    /**
20951     * Re-emits transmuxer events by converting them into messages to the
20952     * world outside the worker.
20953     *
20954     * @param {Object} transmuxer the transmuxer to wire events on
20955     * @private
20956     */
20957
20958    const wireTransmuxerEvents = function (self, transmuxer) {
20959      transmuxer.on('data', function (segment) {
20960        // transfer ownership of the underlying ArrayBuffer
20961        // instead of doing a copy to save memory
20962        // ArrayBuffers are transferable but generic TypedArrays are not
20963        // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects)
20964        const initArray = segment.initSegment;
20965        segment.initSegment = {
20966          data: initArray.buffer,
20967          byteOffset: initArray.byteOffset,
20968          byteLength: initArray.byteLength
20969        };
20970        const typedArray = segment.data;
20971        segment.data = typedArray.buffer;
20972        self.postMessage({
20973          action: 'data',
20974          segment,
20975          byteOffset: typedArray.byteOffset,
20976          byteLength: typedArray.byteLength
20977        }, [segment.data]);
20978      });
20979      transmuxer.on('done', function (data) {
20980        self.postMessage({
20981          action: 'done'
20982        });
20983      });
20984      transmuxer.on('gopInfo', function (gopInfo) {
20985        self.postMessage({
20986          action: 'gopInfo',
20987          gopInfo
20988        });
20989      });
20990      transmuxer.on('videoSegmentTimingInfo', function (timingInfo) {
20991        const videoSegmentTimingInfo = {
20992          start: {
20993            decode: clock$2.videoTsToSeconds(timingInfo.start.dts),
20994            presentation: clock$2.videoTsToSeconds(timingInfo.start.pts)
20995          },
20996          end: {
20997            decode: clock$2.videoTsToSeconds(timingInfo.end.dts),
20998            presentation: clock$2.videoTsToSeconds(timingInfo.end.pts)
20999          },
21000          baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime)
21001        };
21002
21003        if (timingInfo.prependedContentDuration) {
21004          videoSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration);
21005        }
21006
21007        self.postMessage({
21008          action: 'videoSegmentTimingInfo',
21009          videoSegmentTimingInfo
21010        });
21011      });
21012      transmuxer.on('audioSegmentTimingInfo', function (timingInfo) {
21013        // Note that all times for [audio/video]SegmentTimingInfo events are in video clock
21014        const audioSegmentTimingInfo = {
21015          start: {
21016            decode: clock$2.videoTsToSeconds(timingInfo.start.dts),
21017            presentation: clock$2.videoTsToSeconds(timingInfo.start.pts)
21018          },
21019          end: {
21020            decode: clock$2.videoTsToSeconds(timingInfo.end.dts),
21021            presentation: clock$2.videoTsToSeconds(timingInfo.end.pts)
21022          },
21023          baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime)
21024        };
21025
21026        if (timingInfo.prependedContentDuration) {
21027          audioSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration);
21028        }
21029
21030        self.postMessage({
21031          action: 'audioSegmentTimingInfo',
21032          audioSegmentTimingInfo
21033        });
21034      });
21035      transmuxer.on('id3Frame', function (id3Frame) {
21036        self.postMessage({
21037          action: 'id3Frame',
21038          id3Frame
21039        });
21040      });
21041      transmuxer.on('caption', function (caption) {
21042        self.postMessage({
21043          action: 'caption',
21044          caption
21045        });
21046      });
21047      transmuxer.on('trackinfo', function (trackInfo) {
21048        self.postMessage({
21049          action: 'trackinfo',
21050          trackInfo
21051        });
21052      });
21053      transmuxer.on('audioTimingInfo', function (audioTimingInfo) {
21054        // convert to video TS since we prioritize video time over audio
21055        self.postMessage({
21056          action: 'audioTimingInfo',
21057          audioTimingInfo: {
21058            start: clock$2.videoTsToSeconds(audioTimingInfo.start),
21059            end: clock$2.videoTsToSeconds(audioTimingInfo.end)
21060          }
21061        });
21062      });
21063      transmuxer.on('videoTimingInfo', function (videoTimingInfo) {
21064        self.postMessage({
21065          action: 'videoTimingInfo',
21066          videoTimingInfo: {
21067            start: clock$2.videoTsToSeconds(videoTimingInfo.start),
21068            end: clock$2.videoTsToSeconds(videoTimingInfo.end)
21069          }
21070        });
21071      });
21072      transmuxer.on('log', function (log) {
21073        self.postMessage({
21074          action: 'log',
21075          log
21076        });
21077      });
21078    };
21079    /**
21080     * All incoming messages route through this hash. If no function exists
21081     * to handle an incoming message, then we ignore the message.
21082     *
21083     * @class MessageHandlers
21084     * @param {Object} options the options to initialize with
21085     */
21086
21087
21088    class MessageHandlers {
21089      constructor(self, options) {
21090        this.options = options || {};
21091        this.self = self;
21092        this.init();
21093      }
21094      /**
21095       * initialize our web worker and wire all the events.
21096       */
21097
21098
21099      init() {
21100        if (this.transmuxer) {
21101          this.transmuxer.dispose();
21102        }
21103
21104        this.transmuxer = new transmuxer.Transmuxer(this.options);
21105        wireTransmuxerEvents(this.self, this.transmuxer);
21106      }
21107
21108      pushMp4Captions(data) {
21109        if (!this.captionParser) {
21110          this.captionParser = new captionParser();
21111          this.captionParser.init();
21112        }
21113
21114        const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
21115        const parsed = this.captionParser.parse(segment, data.trackIds, data.timescales);
21116        this.self.postMessage({
21117          action: 'mp4Captions',
21118          captions: parsed && parsed.captions || [],
21119          logs: parsed && parsed.logs || [],
21120          data: segment.buffer
21121        }, [segment.buffer]);
21122      }
21123      /**
21124       * Initializes the WebVttParser and passes the init segment.
21125       *
21126       * @param {Uint8Array} data mp4 boxed WebVTT init segment data
21127       */
21128
21129
21130      initMp4WebVttParser(data) {
21131        if (!this.webVttParser) {
21132          this.webVttParser = new webvttParser();
21133        }
21134
21135        const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); // Set the timescale for the parser.
21136        // This can be called repeatedly in order to set and re-set the timescale.
21137
21138        this.webVttParser.init(segment);
21139      }
21140      /**
21141       * Parse an mp4 encapsulated WebVTT segment and return an array of cues.
21142       *
21143       * @param {Uint8Array} data a text/webvtt segment
21144       * @return {Object[]} an array of parsed cue objects
21145       */
21146
21147
21148      getMp4WebVttText(data) {
21149        if (!this.webVttParser) {
21150          // timescale might not be set yet if the parser is created before an init segment is passed.
21151          // default timescale is 90k.
21152          this.webVttParser = new webvttParser();
21153        }
21154
21155        const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
21156        const parsed = this.webVttParser.parseSegment(segment);
21157        this.self.postMessage({
21158          action: 'getMp4WebVttText',
21159          mp4VttCues: parsed || [],
21160          data: segment.buffer
21161        }, [segment.buffer]);
21162      }
21163
21164      probeMp4StartTime({
21165        timescales,
21166        data
21167      }) {
21168        const startTime = probe$2.startTime(timescales, data);
21169        this.self.postMessage({
21170          action: 'probeMp4StartTime',
21171          startTime,
21172          data
21173        }, [data.buffer]);
21174      }
21175
21176      probeMp4Tracks({
21177        data
21178      }) {
21179        const tracks = probe$2.tracks(data);
21180        this.self.postMessage({
21181          action: 'probeMp4Tracks',
21182          tracks,
21183          data
21184        }, [data.buffer]);
21185      }
21186      /**
21187       * Probes an mp4 segment for EMSG boxes containing ID3 data.
21188       * https://aomediacodec.github.io/id3-emsg/
21189       *
21190       * @param {Uint8Array} data segment data
21191       * @param {number} offset segment start time
21192       * @return {Object[]} an array of ID3 frames
21193       */
21194
21195
21196      probeEmsgID3({
21197        data,
21198        offset
21199      }) {
21200        const id3Frames = probe$2.getEmsgID3(data, offset);
21201        this.self.postMessage({
21202          action: 'probeEmsgID3',
21203          id3Frames,
21204          emsgData: data
21205        }, [data.buffer]);
21206      }
21207      /**
21208       * Probe an mpeg2-ts segment to determine the start time of the segment in it's
21209       * internal "media time," as well as whether it contains video and/or audio.
21210       *
21211       * @private
21212       * @param {Uint8Array} bytes - segment bytes
21213       * @param {number} baseStartTime
21214       *        Relative reference timestamp used when adjusting frame timestamps for rollover.
21215       *        This value should be in seconds, as it's converted to a 90khz clock within the
21216       *        function body.
21217       * @return {Object} The start time of the current segment in "media time" as well as
21218       *                  whether it contains video and/or audio
21219       */
21220
21221
21222      probeTs({
21223        data,
21224        baseStartTime
21225      }) {
21226        const tsStartTime = typeof baseStartTime === 'number' && !isNaN(baseStartTime) ? baseStartTime * clock$2.ONE_SECOND_IN_TS : void 0;
21227        const timeInfo = tsInspector.inspect(data, tsStartTime);
21228        let result = null;
21229
21230        if (timeInfo) {
21231          result = {
21232            // each type's time info comes back as an array of 2 times, start and end
21233            hasVideo: timeInfo.video && timeInfo.video.length === 2 || false,
21234            hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false
21235          };
21236
21237          if (result.hasVideo) {
21238            result.videoStart = timeInfo.video[0].ptsTime;
21239          }
21240
21241          if (result.hasAudio) {
21242            result.audioStart = timeInfo.audio[0].ptsTime;
21243          }
21244        }
21245
21246        this.self.postMessage({
21247          action: 'probeTs',
21248          result,
21249          data
21250        }, [data.buffer]);
21251      }
21252
21253      clearAllMp4Captions() {
21254        if (this.captionParser) {
21255          this.captionParser.clearAllCaptions();
21256        }
21257      }
21258
21259      clearParsedMp4Captions() {
21260        if (this.captionParser) {
21261          this.captionParser.clearParsedCaptions();
21262        }
21263      }
21264      /**
21265       * Adds data (a ts segment) to the start of the transmuxer pipeline for
21266       * processing.
21267       *
21268       * @param {ArrayBuffer} data data to push into the muxer
21269       */
21270
21271
21272      push(data) {
21273        // Cast array buffer to correct type for transmuxer
21274        const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
21275        this.transmuxer.push(segment);
21276      }
21277      /**
21278       * Recreate the transmuxer so that the next segment added via `push`
21279       * start with a fresh transmuxer.
21280       */
21281
21282
21283      reset() {
21284        this.transmuxer.reset();
21285      }
21286      /**
21287       * Set the value that will be used as the `baseMediaDecodeTime` time for the
21288       * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime`
21289       * set relative to the first based on the PTS values.
21290       *
21291       * @param {Object} data used to set the timestamp offset in the muxer
21292       */
21293
21294
21295      setTimestampOffset(data) {
21296        const timestampOffset = data.timestampOffset || 0;
21297        this.transmuxer.setBaseMediaDecodeTime(Math.round(clock$2.secondsToVideoTs(timestampOffset)));
21298      }
21299
21300      setAudioAppendStart(data) {
21301        this.transmuxer.setAudioAppendStart(Math.ceil(clock$2.secondsToVideoTs(data.appendStart)));
21302      }
21303
21304      setRemux(data) {
21305        this.transmuxer.setRemux(data.remux);
21306      }
21307      /**
21308       * Forces the pipeline to finish processing the last segment and emit it's
21309       * results.
21310       *
21311       * @param {Object} data event data, not really used
21312       */
21313
21314
21315      flush(data) {
21316        this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed
21317
21318        self.postMessage({
21319          action: 'done',
21320          type: 'transmuxed'
21321        });
21322      }
21323
21324      endTimeline() {
21325        this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their
21326        // timelines
21327
21328        self.postMessage({
21329          action: 'endedtimeline',
21330          type: 'transmuxed'
21331        });
21332      }
21333
21334      alignGopsWith(data) {
21335        this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice());
21336      }
21337
21338    }
21339    /**
21340     * Our web worker interface so that things can talk to mux.js
21341     * that will be running in a web worker. the scope is passed to this by
21342     * webworkify.
21343     *
21344     * @param {Object} self the scope for the web worker
21345     */
21346
21347
21348    self.onmessage = function (event) {
21349      if (event.data.action === 'init' && event.data.options) {
21350        this.messageHandlers = new MessageHandlers(self, event.data.options);
21351        return;
21352      }
21353
21354      if (!this.messageHandlers) {
21355        this.messageHandlers = new MessageHandlers(self);
21356      }
21357
21358      if (event.data && event.data.action && event.data.action !== 'init') {
21359        if (this.messageHandlers[event.data.action]) {
21360          this.messageHandlers[event.data.action](event.data);
21361        }
21362      }
21363    };
21364  }));
21365  var TransmuxWorker = factory(workerCode$1);
21366  /* rollup-plugin-worker-factory end for worker!/
21366home/runner/work/http-streaming
vendor: 98,517 bytes, lines 21366-24328
21366/http-streaming/src/transmuxer-worker.js */
21367
21368  const handleData_ = (event, transmuxedData, callback) => {
21369    const {
21370      type,
21371      initSegment,
21372      captions,
21373      captionStreams,
21374      metadata,
21375      videoFrameDtsTime,
21376      videoFramePtsTime
21377    } = event.data.segment;
21378    transmuxedData.buffer.push({
21379      captions,
21380      captionStreams,
21381      metadata
21382    });
21383    const boxes = event.data.segment.boxes || {
21384      data: event.data.segment.data
21385    };
21386    const result = {
21387      type,
21388      // cast ArrayBuffer to TypedArray
21389      data: new Uint8Array(boxes.data, boxes.data.byteOffset, boxes.data.byteLength),
21390      initSegment: new Uint8Array(initSegment.data, initSegment.byteOffset, initSegment.byteLength)
21391    };
21392
21393    if (typeof videoFrameDtsTime !== 'undefined') {
21394      result.videoFrameDtsTime = videoFrameDtsTime;
21395    }
21396
21397    if (typeof videoFramePtsTime !== 'undefined') {
21398      result.videoFramePtsTime = videoFramePtsTime;
21399    }
21400
21401    callback(result);
21402  };
21403  const handleDone_ = ({
21404    transmuxedData,
21405    callback
21406  }) => {
21407    // Previously we only returned data on data events,
21408    // not on done events. Clear out the buffer to keep that consistent.
21409    transmuxedData.buffer = []; // all buffers should have been flushed from the muxer, so start processing anything we
21410    // have received
21411
21412    callback(transmuxedData);
21413  };
21414  const handleGopInfo_ = (event, transmuxedData) => {
21415    transmuxedData.gopInfo = event.data.gopInfo;
21416  };
21417  const processTransmux = options => {
21418    const {
21419      transmuxer,
21420      bytes,
21421      audioAppendStart,
21422      gopsToAlignWith,
21423      remux,
21424      onData,
21425      onTrackInfo,
21426      onAudioTimingInfo,
21427      onVideoTimingInfo,
21428      onVideoSegmentTimingInfo,
21429      onAudioSegmentTimingInfo,
21430      onId3,
21431      onCaptions,
21432      onDone,
21433      onEndedTimeline,
21434      onTransmuxerLog,
21435      isEndOfTimeline,
21436      segment,
21437      triggerSegmentEventFn
21438    } = options;
21439    const transmuxedData = {
21440      buffer: []
21441    };
21442    let waitForEndedTimelineEvent = isEndOfTimeline;
21443
21444    const handleMessage = event => {
21445      if (transmuxer.currentTransmux !== options) {
21446        // disposed
21447        return;
21448      }
21449
21450      if (event.data.action === 'data') {
21451        handleData_(event, transmuxedData, onData);
21452      }
21453
21454      if (event.data.action === 'trackinfo') {
21455        onTrackInfo(event.data.trackInfo);
21456      }
21457
21458      if (event.data.action === 'gopInfo') {
21459        handleGopInfo_(event, transmuxedData);
21460      }
21461
21462      if (event.data.action === 'audioTimingInfo') {
21463        onAudioTimingInfo(event.data.audioTimingInfo);
21464      }
21465
21466      if (event.data.action === 'videoTimingInfo') {
21467        onVideoTimingInfo(event.data.videoTimingInfo);
21468      }
21469
21470      if (event.data.action === 'videoSegmentTimingInfo') {
21471        onVideoSegmentTimingInfo(event.data.videoSegmentTimingInfo);
21472      }
21473
21474      if (event.data.action === 'audioSegmentTimingInfo') {
21475        onAudioSegmentTimingInfo(event.data.audioSegmentTimingInfo);
21476      }
21477
21478      if (event.data.action === 'id3Frame') {
21479        onId3([event.data.id3Frame], event.data.id3Frame.dispatchType);
21480      }
21481
21482      if (event.data.action === 'caption') {
21483        onCaptions(event.data.caption);
21484      }
21485
21486      if (event.data.action === 'endedtimeline') {
21487        waitForEndedTimelineEvent = false;
21488        onEndedTimeline();
21489      }
21490
21491      if (event.data.action === 'log') {
21492        onTransmuxerLog(event.data.log);
21493      } // wait for the transmuxed event since we may have audio and video
21494
21495
21496      if (event.data.type !== 'transmuxed') {
21497        return;
21498      } // If the "endedtimeline" event has not yet fired, and this segment represents the end
21499      // of a timeline, that means there may still be data events before the segment
21500      // processing can be considerred complete. In that case, the final event should be
21501      // an "endedtimeline" event with the type "transmuxed."
21502
21503
21504      if (waitForEndedTimelineEvent) {
21505        return;
21506      }
21507
21508      transmuxer.onmessage = null;
21509      handleDone_({
21510        transmuxedData,
21511        callback: onDone
21512      });
21513      /* eslint-disable no-use-before-define */
21514
21515      dequeue(transmuxer);
21516      /* eslint-enable */
21517    };
21518
21519    const handleError = () => {
21520      const error = {
21521        message: 'Received an error message from the transmuxer worker',
21522        metadata: {
21523          errorType: videojs__default["default"].Error.StreamingFailedToTransmuxSegment,
21524          segmentInfo: segmentInfoPayload({
21525            segment
21526          })
21527        }
21528      };
21529      onDone(null, error);
21530    };
21531
21532    transmuxer.onmessage = handleMessage;
21533    transmuxer.onerror = handleError;
21534
21535    if (audioAppendStart) {
21536      transmuxer.postMessage({
21537        action: 'setAudioAppendStart',
21538        appendStart: audioAppendStart
21539      });
21540    } // allow empty arrays to be passed to clear out GOPs
21541
21542
21543    if (Array.isArray(gopsToAlignWith)) {
21544      transmuxer.postMessage({
21545        action: 'alignGopsWith',
21546        gopsToAlignWith
21547      });
21548    }
21549
21550    if (typeof remux !== 'undefined') {
21551      transmuxer.postMessage({
21552        action: 'setRemux',
21553        remux
21554      });
21555    }
21556
21557    if (bytes.byteLength) {
21558      const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer;
21559      const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset;
21560      triggerSegmentEventFn({
21561        type: 'segmenttransmuxingstart',
21562        segment
21563      });
21564      transmuxer.postMessage({
21565        action: 'push',
21566        // Send the typed-array of data as an ArrayBuffer so that
21567        // it can be sent as a "Transferable" and avoid the costly
21568        // memory copy
21569        data: buffer,
21570        // To recreate the original typed-array, we need information
21571        // about what portion of the ArrayBuffer it was a view into
21572        byteOffset,
21573        byteLength: bytes.byteLength
21574      }, [buffer]);
21575    }
21576
21577    if (isEndOfTimeline) {
21578      transmuxer.postMessage({
21579        action: 'endTimeline'
21580      });
21581    } // even if we didn't push any bytes, we have to make sure we flush in case we reached
21582    // the end of the segment
21583
21584
21585    transmuxer.postMessage({
21586      action: 'flush'
21587    });
21588  };
21589  const dequeue = transmuxer => {
21590    transmuxer.currentTransmux = null;
21591
21592    if (transmuxer.transmuxQueue.length) {
21593      transmuxer.currentTransmux = transmuxer.transmuxQueue.shift();
21594
21595      if (typeof transmuxer.currentTransmux === 'function') {
21596        transmuxer.currentTransmux();
21597      } else {
21598        processTransmux(transmuxer.currentTransmux);
21599      }
21600    }
21601  };
21602  const processAction = (transmuxer, action) => {
21603    transmuxer.postMessage({
21604      action
21605    });
21606    dequeue(transmuxer);
21607  };
21608  const enqueueAction = (action, transmuxer) => {
21609    if (!transmuxer.currentTransmux) {
21610      transmuxer.currentTransmux = action;
21611      processAction(transmuxer, action);
21612      return;
21613    }
21614
21615    transmuxer.transmuxQueue.push(processAction.bind(null, transmuxer, action));
21616  };
21617  const reset = transmuxer => {
21618    enqueueAction('reset', transmuxer);
21619  };
21620  const endTimeline = transmuxer => {
21621    enqueueAction('endTimeline', transmuxer);
21622  };
21623  const transmux = options => {
21624    if (!options.transmuxer.currentTransmux) {
21625      options.transmuxer.currentTransmux = options;
21626      processTransmux(options);
21627      return;
21628    }
21629
21630    options.transmuxer.transmuxQueue.push(options);
21631  };
21632  const createTransmuxer = options => {
21633    const transmuxer = new TransmuxWorker();
21634    transmuxer.currentTransmux = null;
21635    transmuxer.transmuxQueue = [];
21636    const term = transmuxer.terminate;
21637
21638    transmuxer.terminate = () => {
21639      transmuxer.currentTransmux = null;
21640      transmuxer.transmuxQueue.length = 0;
21641      return term.call(transmuxer);
21642    };
21643
21644    transmuxer.postMessage({
21645      action: 'init',
21646      options
21647    });
21648    return transmuxer;
21649  };
21650  var segmentTransmuxer = {
21651    reset,
21652    endTimeline,
21653    transmux,
21654    createTransmuxer
21655  };
21656
21657  const workerCallback = function (options) {
21658    const transmuxer = options.transmuxer;
21659    const endAction = options.endAction || options.action;
21660    const callback = options.callback;
21661
21662    const message = _extends({}, options, {
21663      endAction: null,
21664      transmuxer: null,
21665      callback: null
21666    });
21667
21668    const listenForEndEvent = event => {
21669      if (event.data.action !== endAction) {
21670        return;
21671      }
21672
21673      transmuxer.removeEventListener('message', listenForEndEvent); // transfer ownership of bytes back to us.
21674
21675      if (event.data.data) {
21676        event.data.data = new Uint8Array(event.data.data, options.byteOffset || 0, options.byteLength || event.data.data.byteLength);
21677
21678        if (options.data) {
21679          options.data = event.data.data;
21680        }
21681      }
21682
21683      callback(event.data);
21684    };
21685
21686    transmuxer.addEventListener('message', listenForEndEvent);
21687
21688    if (options.data) {
21689      const isArrayBuffer = options.data instanceof ArrayBuffer;
21690      message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset;
21691      message.byteLength = options.data.byteLength;
21692      const transfers = [isArrayBuffer ? options.data : options.data.buffer];
21693      transmuxer.postMessage(message, transfers);
21694    } else {
21695      transmuxer.postMessage(message);
21696    }
21697  };
21698
21699  const REQUEST_ERRORS = {
21700    FAILURE: 2,
21701    TIMEOUT: -101,
21702    ABORTED: -102
21703  };
21704  const WEB_VTT_CODEC = 'wvtt';
21705  /**
21706   * Abort all requests
21707   *
21708   * @param {Object} activeXhrs - an object that tracks all XHR requests
21709   */
21710
21711  const abortAll = activeXhrs => {
21712    activeXhrs.forEach(xhr => {
21713      xhr.abort();
21714    });
21715  };
21716  /**
21717   * Gather important bandwidth stats once a request has completed
21718   *
21719   * @param {Object} request - the XHR request from which to gather stats
21720   */
21721
21722
21723  const getRequestStats = request => {
21724    return {
21725      bandwidth: request.bandwidth,
21726      bytesReceived: request.bytesReceived || 0,
21727      roundTripTime: request.roundTripTime || 0
21728    };
21729  };
21730  /**
21731   * If possible gather bandwidth stats as a request is in
21732   * progress
21733   *
21734   * @param {Event} progressEvent - an event object from an XHR's progress event
21735   */
21736
21737
21738  const getProgressStats = progressEvent => {
21739    const request = progressEvent.target;
21740    const roundTripTime = Date.now() - request.requestTime;
21741    const stats = {
21742      bandwidth: Infinity,
21743      bytesReceived: 0,
21744      roundTripTime: roundTripTime || 0
21745    };
21746    stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok
21747    // because we should only use bandwidth stats on progress to determine when
21748    // abort a request early due to insufficient bandwidth
21749
21750    stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000);
21751    return stats;
21752  };
21753  /**
21754   * Handle all error conditions in one place and return an object
21755   * with all the information
21756   *
21757   * @param {Error|null} error - if non-null signals an error occured with the XHR
21758   * @param {Object} request -  the XHR request that possibly generated the error
21759   */
21760
21761
21762  const handleErrors = (error, request) => {
21763    const {
21764      requestType
21765    } = request;
21766    const metadata = getStreamingNetworkErrorMetadata({
21767      requestType,
21768      request,
21769      error
21770    });
21771
21772    if (request.timedout) {
21773      return {
21774        status: request.status,
21775        message: 'HLS request timed-out at URL: ' + request.uri,
21776        code: REQUEST_ERRORS.TIMEOUT,
21777        xhr: request,
21778        metadata
21779      };
21780    }
21781
21782    if (request.aborted) {
21783      return {
21784        status: request.status,
21785        message: 'HLS request aborted at URL: ' + request.uri,
21786        code: REQUEST_ERRORS.ABORTED,
21787        xhr: request,
21788        metadata
21789      };
21790    }
21791
21792    if (error) {
21793      return {
21794        status: request.status,
21795        message: 'HLS request errored at URL: ' + request.uri,
21796        code: REQUEST_ERRORS.FAILURE,
21797        xhr: request,
21798        metadata
21799      };
21800    }
21801
21802    if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) {
21803      return {
21804        status: request.status,
21805        message: 'Empty HLS response at URL: ' + request.uri,
21806        code: REQUEST_ERRORS.FAILURE,
21807        xhr: request,
21808        metadata
21809      };
21810    }
21811
21812    return null;
21813  };
21814  /**
21815   * Handle responses for key data and convert the key data to the correct format
21816   * for the decryption step later
21817   *
21818   * @param {Object} segment - a simplified copy of the segmentInfo object
21819   *                           from SegmentLoader
21820   * @param {Array} objects - objects to add the key bytes to.
21821   * @param {Function} finishProcessingFn - a callback to execute to continue processing
21822   *                                        this request
21823   */
21824
21825
21826  const handleKeyResponse = (segment, objects, finishProcessingFn, triggerSegmentEventFn) => (error, request) => {
21827    const response = request.response;
21828    const errorObj = handleErrors(error, request);
21829
21830    if (errorObj) {
21831      return finishProcessingFn(errorObj, segment);
21832    }
21833
21834    if (response.byteLength !== 16) {
21835      return finishProcessingFn({
21836        status: request.status,
21837        message: 'Invalid HLS key at URL: ' + request.uri,
21838        code: REQUEST_ERRORS.FAILURE,
21839        xhr: request
21840      }, segment);
21841    }
21842
21843    const view = new DataView(response);
21844    const bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);
21845
21846    for (let i = 0; i < objects.length; i++) {
21847      objects[i].bytes = bytes;
21848    }
21849
21850    const keyInfo = {
21851      uri: request.uri
21852    };
21853    triggerSegmentEventFn({
21854      type: 'segmentkeyloadcomplete',
21855      segment,
21856      keyInfo
21857    });
21858    return finishProcessingFn(null, segment);
21859  };
21860  /**
21861   * Processes an mp4 init segment depending on the codec through the transmuxer.
21862   *
21863   * @param {Object} segment init segment to process
21864   * @param {string} codec the codec of the text segments
21865   */
21866
21867
21868  const initMp4Text = (segment, codec) => {
21869    if (codec === WEB_VTT_CODEC) {
21870      segment.transmuxer.postMessage({
21871        action: 'initMp4WebVttParser',
21872        data: segment.map.bytes
21873      });
21874    }
21875  };
21876  /**
21877   * Parses an mp4 text segment with the transmuxer and calls the doneFn from
21878   * the segment loader.
21879   *
21880   * @param {Object} segment the text segment to parse
21881   * @param {string} codec the codec of the text segment
21882   * @param {Function} doneFn the doneFn passed from the segment loader
21883   */
21884
21885
21886  const parseMp4TextSegment = (segment, codec, doneFn) => {
21887    if (codec === WEB_VTT_CODEC) {
21888      workerCallback({
21889        action: 'getMp4WebVttText',
21890        data: segment.bytes,
21891        transmuxer: segment.transmuxer,
21892        callback: ({
21893          data,
21894          mp4VttCues
21895        }) => {
21896          segment.bytes = data;
21897          doneFn(null, segment, {
21898            mp4VttCues
21899          });
21900        }
21901      });
21902    }
21903  };
21904
21905  const parseInitSegment = (segment, callback) => {
21906    const type = detectContainerForBytes(segment.map.bytes); // TODO: We should also handle ts init segments here, but we
21907    // only know how to parse mp4 init segments at the moment
21908
21909    if (type !== 'mp4') {
21910      const uri = segment.map.resolvedUri || segment.map.uri;
21911      const mediaType = type || 'unknown';
21912      return callback({
21913        internal: true,
21914        message: `Found unsupported ${mediaType} container for initialization segment at URL: ${uri}`,
21915        code: REQUEST_ERRORS.FAILURE,
21916        metadata: {
21917          mediaType
21918        }
21919      });
21920    }
21921
21922    workerCallback({
21923      action: 'probeMp4Tracks',
21924      data: segment.map.bytes,
21925      transmuxer: segment.transmuxer,
21926      callback: ({
21927        tracks,
21928        data
21929      }) => {
21930        // transfer bytes back to us
21931        segment.map.bytes = data;
21932        tracks.forEach(function (track) {
21933          segment.map.tracks = segment.map.tracks || {}; // only support one track of each type for now
21934
21935          if (segment.map.tracks[track.type]) {
21936            return;
21937          }
21938
21939          segment.map.tracks[track.type] = track;
21940
21941          if (typeof track.id === 'number' && track.timescale) {
21942            segment.map.timescales = segment.map.timescales || {};
21943            segment.map.timescales[track.id] = track.timescale;
21944          }
21945
21946          if (track.type === 'text') {
21947            initMp4Text(segment, track.codec);
21948          }
21949        });
21950        return callback(null);
21951      }
21952    });
21953  };
21954  /**
21955   * Handle init-segment responses
21956   *
21957   * @param {Object} segment - a simplified copy of the segmentInfo object
21958   *                           from SegmentLoader
21959   * @param {Function} finishProcessingFn - a callback to execute to continue processing
21960   *                                        this request
21961   */
21962
21963
21964  const handleInitSegmentResponse = ({
21965    segment,
21966    finishProcessingFn,
21967    triggerSegmentEventFn
21968  }) => (error, request) => {
21969    const errorObj = handleErrors(error, request);
21970
21971    if (errorObj) {
21972      return finishProcessingFn(errorObj, segment);
21973    }
21974
21975    const bytes = new Uint8Array(request.response);
21976    triggerSegmentEventFn({
21977      type: 'segmentloaded',
21978      segment
21979    }); // init segment is encypted, we will have to wait
21980    // until the key request is done to decrypt.
21981
21982    if (segment.map.key) {
21983      segment.map.encryptedBytes = bytes;
21984      return finishProcessingFn(null, segment);
21985    }
21986
21987    segment.map.bytes = bytes;
21988    parseInitSegment(segment, function (parseError) {
21989      if (parseError) {
21990        parseError.xhr = request;
21991        parseError.status = request.status;
21992        return finishProcessingFn(parseError, segment);
21993      }
21994
21995      finishProcessingFn(null, segment);
21996    });
21997  };
21998  /**
21999   * Response handler for segment-requests being sure to set the correct
22000   * property depending on whether the segment is encryped or not
22001   * Also records and keeps track of stats that are used for ABR purposes
22002   *
22003   * @param {Object} segment - a simplified copy of the segmentInfo object
22004   *                           from SegmentLoader
22005   * @param {Function} finishProcessingFn - a callback to execute to continue processing
22006   *                                        this request
22007   */
22008
22009
22010  const handleSegmentResponse = ({
22011    segment,
22012    finishProcessingFn,
22013    responseType,
22014    triggerSegmentEventFn
22015  }) => (error, request) => {
22016    const errorObj = handleErrors(error, request);
22017
22018    if (errorObj) {
22019      return finishProcessingFn(errorObj, segment);
22020    }
22021
22022    triggerSegmentEventFn({
22023      type: 'segmentloaded',
22024      segment
22025    });
22026    const newBytes = // although responseText "should" exist, this guard serves to prevent an error being
22027    // thrown for two primary cases:
22028    // 1. the mime type override stops working, or is not implemented for a specific
22029    //    browser
22030    // 2. when using mock XHR libraries like sinon that do not allow the override behavior
22031    responseType === 'arraybuffer' || !request.responseText ? request.response : stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0));
22032    segment.stats = getRequestStats(request);
22033
22034    if (segment.key) {
22035      segment.encryptedBytes = new Uint8Array(newBytes);
22036    } else {
22037      segment.bytes = new Uint8Array(newBytes);
22038    }
22039
22040    return finishProcessingFn(null, segment);
22041  };
22042
22043  const transmuxAndNotify = ({
22044    segment,
22045    bytes,
22046    trackInfoFn,
22047    timingInfoFn,
22048    videoSegmentTimingInfoFn,
22049    audioSegmentTimingInfoFn,
22050    id3Fn,
22051    captionsFn,
22052    isEndOfTimeline,
22053    endedTimelineFn,
22054    dataFn,
22055    doneFn,
22056    onTransmuxerLog,
22057    triggerSegmentEventFn
22058  }) => {
22059    const fmp4Tracks = segment.map && segment.map.tracks || {};
22060    const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); // Keep references to each function so we can null them out after we're done with them.
22061    // One reason for this is that in the case of full segments, we want to trust start
22062    // times from the probe, rather than the transmuxer.
22063
22064    let audioStartFn = timingInfoFn.bind(null, segment, 'audio', 'start');
22065    const audioEndFn = timingInfoFn.bind(null, segment, 'audio', 'end');
22066    let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start');
22067    const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end');
22068
22069    const finish = () => transmux({
22070      bytes,
22071      transmuxer: segment.transmuxer,
22072      audioAppendStart: segment.audioAppendStart,
22073      gopsToAlignWith: segment.gopsToAlignWith,
22074      remux: isMuxed,
22075      onData: result => {
22076        result.type = result.type === 'combined' ? 'video' : result.type;
22077        dataFn(segment, result);
22078      },
22079      onTrackInfo: trackInfo => {
22080        if (trackInfoFn) {
22081          if (isMuxed) {
22082            trackInfo.isMuxed = true;
22083          }
22084
22085          trackInfoFn(segment, trackInfo);
22086        }
22087      },
22088      onAudioTimingInfo: audioTimingInfo => {
22089        // we only want the first start value we encounter
22090        if (audioStartFn && typeof audioTimingInfo.start !== 'undefined') {
22091          audioStartFn(audioTimingInfo.start);
22092          audioStartFn = null;
22093        } // we want to continually update the end time
22094
22095
22096        if (audioEndFn && typeof audioTimingInfo.end !== 'undefined') {
22097          audioEndFn(audioTimingInfo.end);
22098        }
22099      },
22100      onVideoTimingInfo: videoTimingInfo => {
22101        // we only want the first start value we encounter
22102        if (videoStartFn && typeof videoTimingInfo.start !== 'undefined') {
22103          videoStartFn(videoTimingInfo.start);
22104          videoStartFn = null;
22105        } // we want to continually update the end time
22106
22107
22108        if (videoEndFn && typeof videoTimingInfo.end !== 'undefined') {
22109          videoEndFn(videoTimingInfo.end);
22110        }
22111      },
22112      onVideoSegmentTimingInfo: videoSegmentTimingInfo => {
22113        const timingInfo = {
22114          pts: {
22115            start: videoSegmentTimingInfo.start.presentation,
22116            end: videoSegmentTimingInfo.end.presentation
22117          },
22118          dts: {
22119            start: videoSegmentTimingInfo.start.decode,
22120            end: videoSegmentTimingInfo.end.decode
22121          }
22122        };
22123        triggerSegmentEventFn({
22124          type: 'segmenttransmuxingtiminginfoavailable',
22125          segment,
22126          timingInfo
22127        });
22128        videoSegmentTimingInfoFn(videoSegmentTimingInfo);
22129      },
22130      onAudioSegmentTimingInfo: audioSegmentTimingInfo => {
22131        const timingInfo = {
22132          pts: {
22133            start: audioSegmentTimingInfo.start.pts,
22134            end: audioSegmentTimingInfo.end.pts
22135          },
22136          dts: {
22137            start: audioSegmentTimingInfo.start.dts,
22138            end: audioSegmentTimingInfo.end.dts
22139          }
22140        };
22141        triggerSegmentEventFn({
22142          type: 'segmenttransmuxingtiminginfoavailable',
22143          segment,
22144          timingInfo
22145        });
22146        audioSegmentTimingInfoFn(audioSegmentTimingInfo);
22147      },
22148      onId3: (id3Frames, dispatchType) => {
22149        id3Fn(segment, id3Frames, dispatchType);
22150      },
22151      onCaptions: captions => {
22152        captionsFn(segment, [captions]);
22153      },
22154      isEndOfTimeline,
22155      onEndedTimeline: () => {
22156        endedTimelineFn();
22157      },
22158      onTransmuxerLog,
22159      onDone: (result, error) => {
22160        if (!doneFn) {
22161          return;
22162        }
22163
22164        result.type = result.type === 'combined' ? 'video' : result.type;
22165        triggerSegmentEventFn({
22166          type: 'segmenttransmuxingcomplete',
22167          segment
22168        });
22169        doneFn(error, segment, result);
22170      },
22171      segment,
22172      triggerSegmentEventFn
22173    }); // In the transmuxer, we don't yet have the ability to extract a "proper" start time.
22174    // Meaning cached frame data may corrupt our notion of where this segment
22175    // really starts. To get around this, probe for the info needed.
22176
22177
22178    workerCallback({
22179      action: 'probeTs',
22180      transmuxer: segment.transmuxer,
22181      data: bytes,
22182      baseStartTime: segment.baseStartTime,
22183      callback: data => {
22184        segment.bytes = bytes = data.data;
22185        const probeResult = data.result;
22186
22187        if (probeResult) {
22188          trackInfoFn(segment, {
22189            hasAudio: probeResult.hasAudio,
22190            hasVideo: probeResult.hasVideo,
22191            isMuxed
22192          });
22193          trackInfoFn = null;
22194        }
22195
22196        finish();
22197      }
22198    });
22199  };
22200
22201  const handleSegmentBytes = ({
22202    segment,
22203    bytes,
22204    trackInfoFn,
22205    timingInfoFn,
22206    videoSegmentTimingInfoFn,
22207    audioSegmentTimingInfoFn,
22208    id3Fn,
22209    captionsFn,
22210    isEndOfTimeline,
22211    endedTimelineFn,
22212    dataFn,
22213    doneFn,
22214    onTransmuxerLog,
22215    triggerSegmentEventFn
22216  }) => {
22217    let bytesAsUint8Array = new Uint8Array(bytes); // TODO:
22218    // We should have a handler that fetches the number of bytes required
22219    // to check if something is fmp4. This will allow us to save bandwidth
22220    // because we can only exclude a playlist and abort requests
22221    // by codec after trackinfo triggers.
22222
22223    if (isLikelyFmp4MediaSegment(bytesAsUint8Array)) {
22224      segment.isFmp4 = true;
22225      const {
22226        tracks
22227      } = segment.map;
22228      const isMp4TextSegment = tracks.text && (!tracks.audio || !tracks.video);
22229
22230      if (isMp4TextSegment) {
22231        dataFn(segment, {
22232          data: bytesAsUint8Array,
22233          type: 'text'
22234        });
22235        parseMp4TextSegment(segment, tracks.text.codec, doneFn);
22236        return;
22237      }
22238
22239      const trackInfo = {
22240        isFmp4: true,
22241        hasVideo: !!tracks.video,
22242        hasAudio: !!tracks.audio
22243      }; // if we have a audio track, with a codec that is not set to
22244      // encrypted audio
22245
22246      if (tracks.audio && tracks.audio.codec && tracks.audio.codec !== 'enca') {
22247        trackInfo.audioCodec = tracks.audio.codec;
22248      } // if we have a video track, with a codec that is not set to
22249      // encrypted video
22250
22251
22252      if (tracks.video && tracks.video.codec && tracks.video.codec !== 'encv') {
22253        trackInfo.videoCodec = tracks.video.codec;
22254      }
22255
22256      if (tracks.video && tracks.audio) {
22257        trackInfo.isMuxed = true;
22258      } // since we don't support appending fmp4 data on progress, we know we have the full
22259      // segment here
22260
22261
22262      trackInfoFn(segment, trackInfo); // The probe doesn't provide the segment end time, so only callback with the start
22263      // time. The end time can be roughly calculated by the receiver using the duration.
22264      //
22265      // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as
22266      // that is the true start of the segment (where the playback engine should begin
22267      // decoding).
22268
22269      const finishLoading = (captions, id3Frames) => {
22270        // if the track still has audio at this point it is only possible
22271        // for it to be audio only. See `tracks.video && tracks.audio` if statement
22272        // above.
22273        // we make sure to use segment.bytes here as that
22274        dataFn(segment, {
22275          data: bytesAsUint8Array,
22276          type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video'
22277        });
22278
22279        if (id3Frames && id3Frames.length) {
22280          id3Fn(segment, id3Frames);
22281        }
22282
22283        if (captions && captions.length) {
22284          captionsFn(segment, captions);
22285        }
22286
22287        doneFn(null, segment, {});
22288      };
22289
22290      workerCallback({
22291        action: 'probeMp4StartTime',
22292        timescales: segment.map.timescales,
22293        data: bytesAsUint8Array,
22294        transmuxer: segment.transmuxer,
22295        callback: ({
22296          data,
22297          startTime
22298        }) => {
22299          // transfer bytes back to us
22300          bytes = data.buffer;
22301          segment.bytes = bytesAsUint8Array = data;
22302
22303          if (trackInfo.hasAudio && !trackInfo.isMuxed) {
22304            timingInfoFn(segment, 'audio', 'start', startTime);
22305          }
22306
22307          if (trackInfo.hasVideo) {
22308            timingInfoFn(segment, 'video', 'start', startTime);
22309          }
22310
22311          workerCallback({
22312            action: 'probeEmsgID3',
22313            data: bytesAsUint8Array,
22314            transmuxer: segment.transmuxer,
22315            offset: startTime,
22316            callback: ({
22317              emsgData,
22318              id3Frames
22319            }) => {
22320              // transfer bytes back to us
22321              bytes = emsgData.buffer;
22322              segment.bytes = bytesAsUint8Array = emsgData; // Run through the CaptionParser in case there are captions.
22323              // Initialize CaptionParser if it hasn't been yet
22324
22325              if (!tracks.video || !emsgData.byteLength || !segment.transmuxer) {
22326                finishLoading(undefined, id3Frames);
22327                return;
22328              }
22329
22330              workerCallback({
22331                action: 'pushMp4Captions',
22332                endAction: 'mp4Captions',
22333                transmuxer: segment.transmuxer,
22334                data: bytesAsUint8Array,
22335                timescales: segment.map.timescales,
22336                trackIds: [tracks.video.id],
22337                callback: message => {
22338                  // transfer bytes back to us
22339                  bytes = message.data.buffer;
22340                  segment.bytes = bytesAsUint8Array = message.data;
22341                  message.logs.forEach(function (log) {
22342                    onTransmuxerLog(merge$1(log, {
22343                      stream: 'mp4CaptionParser'
22344                    }));
22345                  });
22346                  finishLoading(message.captions, id3Frames);
22347                }
22348              });
22349            }
22350          });
22351        }
22352      });
22353      return;
22354    } // VTT or other segments that don't need processing
22355
22356
22357    if (!segment.transmuxer) {
22358      doneFn(null, segment, {});
22359      return;
22360    }
22361
22362    if (typeof segment.container === 'undefined') {
22363      segment.container = detectContainerForBytes(bytesAsUint8Array);
22364    }
22365
22366    if (segment.container !== 'ts' && segment.container !== 'aac') {
22367      trackInfoFn(segment, {
22368        hasAudio: false,
22369        hasVideo: false
22370      });
22371      doneFn(null, segment, {});
22372      return;
22373    } // ts or aac
22374
22375
22376    transmuxAndNotify({
22377      segment,
22378      bytes,
22379      trackInfoFn,
22380      timingInfoFn,
22381      videoSegmentTimingInfoFn,
22382      audioSegmentTimingInfoFn,
22383      id3Fn,
22384      captionsFn,
22385      isEndOfTimeline,
22386      endedTimelineFn,
22387      dataFn,
22388      doneFn,
22389      onTransmuxerLog,
22390      triggerSegmentEventFn
22391    });
22392  };
22393
22394  const decrypt = function ({
22395    id,
22396    key,
22397    encryptedBytes,
22398    decryptionWorker,
22399    segment,
22400    doneFn
22401  }, callback) {
22402    const decryptionHandler = event => {
22403      if (event.data.source === id) {
22404        decryptionWorker.removeEventListener('message', decryptionHandler);
22405        const decrypted = event.data.decrypted;
22406        callback(new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength));
22407      }
22408    };
22409
22410    decryptionWorker.onerror = () => {
22411      const message = 'An error occurred in the decryption worker';
22412      const segmentInfo = segmentInfoPayload({
22413        segment
22414      });
22415      const decryptError = {
22416        message,
22417        metadata: {
22418          error: new Error(message),
22419          errorType: videojs__default["default"].Error.StreamingFailedToDecryptSegment,
22420          segmentInfo,
22421          keyInfo: {
22422            uri: segment.key.resolvedUri || segment.map.key.resolvedUri
22423          }
22424        }
22425      };
22426      doneFn(decryptError, segment);
22427    };
22428
22429    decryptionWorker.addEventListener('message', decryptionHandler);
22430    let keyBytes;
22431
22432    if (key.bytes.slice) {
22433      keyBytes = key.bytes.slice();
22434    } else {
22435      keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes));
22436    } // incrementally decrypt the bytes
22437
22438
22439    decryptionWorker.postMessage(createTransferableMessage({
22440      source: id,
22441      encrypted: encryptedBytes,
22442      key: keyBytes,
22443      iv: key.iv
22444    }), [encryptedBytes.buffer, keyBytes.buffer]);
22445  };
22446  /**
22447   * Decrypt the segment via the decryption web worker
22448   *
22449   * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption
22450   *                                       routines
22451   * @param {Object} segment - a simplified copy of the segmentInfo object
22452   *                           from SegmentLoader
22453   * @param {Function} trackInfoFn - a callback that receives track info
22454   * @param {Function} timingInfoFn - a callback that receives timing info
22455   * @param {Function} videoSegmentTimingInfoFn
22456   *                   a callback that receives video timing info based on media times and
22457   *                   any adjustments made by the transmuxer
22458   * @param {Function} audioSegmentTimingInfoFn
22459   *                   a callback that receives audio timing info based on media times and
22460   *                   any adjustments made by the transmuxer
22461   * @param {boolean}  isEndOfTimeline
22462   *                   true if this segment represents the last segment in a timeline
22463   * @param {Function} endedTimelineFn
22464   *                   a callback made when a timeline is ended, will only be called if
22465   *                   isEndOfTimeline is true
22466   * @param {Function} dataFn - a callback that is executed when segment bytes are available
22467   *                            and ready to use
22468   * @param {Function} doneFn - a callback that is executed after decryption has completed
22469   */
22470
22471
22472  const decryptSegment = ({
22473    decryptionWorker,
22474    segment,
22475    trackInfoFn,
22476    timingInfoFn,
22477    videoSegmentTimingInfoFn,
22478    audioSegmentTimingInfoFn,
22479    id3Fn,
22480    captionsFn,
22481    isEndOfTimeline,
22482    endedTimelineFn,
22483    dataFn,
22484    doneFn,
22485    onTransmuxerLog,
22486    triggerSegmentEventFn
22487  }) => {
22488    triggerSegmentEventFn({
22489      type: 'segmentdecryptionstart'
22490    });
22491    decrypt({
22492      id: segment.requestId,
22493      key: segment.key,
22494      encryptedBytes: segment.encryptedBytes,
22495      decryptionWorker,
22496      segment,
22497      doneFn
22498    }, decryptedBytes => {
22499      segment.bytes = decryptedBytes;
22500      triggerSegmentEventFn({
22501        type: 'segmentdecryptioncomplete',
22502        segment
22503      });
22504      handleSegmentBytes({
22505        segment,
22506        bytes: segment.bytes,
22507        trackInfoFn,
22508        timingInfoFn,
22509        videoSegmentTimingInfoFn,
22510        audioSegmentTimingInfoFn,
22511        id3Fn,
22512        captionsFn,
22513        isEndOfTimeline,
22514        endedTimelineFn,
22515        dataFn,
22516        doneFn,
22517        onTransmuxerLog,
22518        triggerSegmentEventFn
22519      });
22520    });
22521  };
22522  /**
22523   * This function waits for all XHRs to finish (with either success or failure)
22524   * before continueing processing via it's callback. The function gathers errors
22525   * from each request into a single errors array so that the error status for
22526   * each request can be examined later.
22527   *
22528   * @param {Object} activeXhrs - an object that tracks all XHR requests
22529   * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption
22530   *                                       routines
22531   * @param {Function} trackInfoFn - a callback that receives track info
22532   * @param {Function} timingInfoFn - a callback that receives timing info
22533   * @param {Function} videoSegmentTimingInfoFn
22534   *                   a callback that receives video timing info based on media times and
22535   *                   any adjustments made by the transmuxer
22536   * @param {Function} audioSegmentTimingInfoFn
22537   *                   a callback that receives audio timing info based on media times and
22538   *                   any adjustments made by the transmuxer
22539   * @param {Function} id3Fn - a callback that receives ID3 metadata
22540   * @param {Function} captionsFn - a callback that receives captions
22541   * @param {boolean}  isEndOfTimeline
22542   *                   true if this segment represents the last segment in a timeline
22543   * @param {Function} endedTimelineFn
22544   *                   a callback made when a timeline is ended, will only be called if
22545   *                   isEndOfTimeline is true
22546   * @param {Function} dataFn - a callback that is executed when segment bytes are available
22547   *                            and ready to use
22548   * @param {Function} doneFn - a callback that is executed after all resources have been
22549   *                            downloaded and any decryption completed
22550   */
22551
22552
22553  const waitForCompletion = ({
22554    activeXhrs,
22555    decryptionWorker,
22556    trackInfoFn,
22557    timingInfoFn,
22558    videoSegmentTimingInfoFn,
22559    audioSegmentTimingInfoFn,
22560    id3Fn,
22561    captionsFn,
22562    isEndOfTimeline,
22563    endedTimelineFn,
22564    dataFn,
22565    doneFn,
22566    onTransmuxerLog,
22567    triggerSegmentEventFn
22568  }) => {
22569    let count = 0;
22570    let didError = false;
22571    return (error, segment) => {
22572      if (didError) {
22573        return;
22574      }
22575
22576      if (error) {
22577        didError = true; // If there are errors, we have to abort any outstanding requests
22578
22579        abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we
22580        // handle the aborted events from those requests, there are some cases where we may
22581        // never get an aborted event. For instance, if the network connection is lost and
22582        // there were two requests, the first may have triggered an error immediately, while
22583        // the second request remains unsent. In that case, the aborted algorithm will not
22584        // trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method
22585        //
22586        // We also can't rely on the ready state of the XHR, since the request that
22587        // triggered the connection error may also show as a ready state of 0 (unsent).
22588        // Therefore, we have to finish this group of requests immediately after the first
22589        // seen error.
22590
22591        return doneFn(error, segment);
22592      }
22593
22594      count += 1;
22595
22596      if (count === activeXhrs.length) {
22597        const segmentFinish = function () {
22598          if (segment.encryptedBytes) {
22599            return decryptSegment({
22600              decryptionWorker,
22601              segment,
22602              trackInfoFn,
22603              timingInfoFn,
22604              videoSegmentTimingInfoFn,
22605              audioSegmentTimingInfoFn,
22606              id3Fn,
22607              captionsFn,
22608              isEndOfTimeline,
22609              endedTimelineFn,
22610              dataFn,
22611              doneFn,
22612              onTransmuxerLog,
22613              triggerSegmentEventFn
22614            });
22615          } // Otherwise, everything is ready just continue
22616
22617
22618          handleSegmentBytes({
22619            segment,
22620            bytes: segment.bytes,
22621            trackInfoFn,
22622            timingInfoFn,
22623            videoSegmentTimingInfoFn,
22624            audioSegmentTimingInfoFn,
22625            id3Fn,
22626            captionsFn,
22627            isEndOfTimeline,
22628            endedTimelineFn,
22629            dataFn,
22630            doneFn,
22631            onTransmuxerLog,
22632            triggerSegmentEventFn
22633          });
22634        }; // Keep track of when *all* of the requests have completed
22635
22636
22637        segment.endOfAllRequests = Date.now();
22638
22639        if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) {
22640          triggerSegmentEventFn({
22641            type: 'segmentdecryptionstart',
22642            segment
22643          });
22644          return decrypt({
22645            decryptionWorker,
22646            // add -init to the "id" to differentiate between segment
22647            // and init segment decryption, just in case they happen
22648            // at the same time at some point in the future.
22649            id: segment.requestId + '-init',
22650            encryptedBytes: segment.map.encryptedBytes,
22651            key: segment.map.key,
22652            segment,
22653            doneFn
22654          }, decryptedBytes => {
22655            segment.map.bytes = decryptedBytes;
22656            triggerSegmentEventFn({
22657              type: 'segmentdecryptioncomplete',
22658              segment
22659            });
22660            parseInitSegment(segment, parseError => {
22661              if (parseError) {
22662                abortAll(activeXhrs);
22663                return doneFn(parseError, segment);
22664              }
22665
22666              segmentFinish();
22667            });
22668          });
22669        }
22670
22671        segmentFinish();
22672      }
22673    };
22674  };
22675  /**
22676   * Calls the abort callback if any request within the batch was aborted. Will only call
22677   * the callback once per batch of requests, even if multiple were aborted.
22678   *
22679   * @param {Object} loadendState - state to check to see if the abort function was called
22680   * @param {Function} abortFn - callback to call for abort
22681   */
22682
22683
22684  const handleLoadEnd = ({
22685    loadendState,
22686    abortFn
22687  }) => event => {
22688    const request = event.target;
22689
22690    if (request.aborted && abortFn && !loadendState.calledAbortFn) {
22691      abortFn();
22692      loadendState.calledAbortFn = true;
22693    }
22694  };
22695  /**
22696   * Simple progress event callback handler that gathers some stats before
22697   * executing a provided callback with the `segment` object
22698   *
22699   * @param {Object} segment - a simplified copy of the segmentInfo object
22700   *                           from SegmentLoader
22701   * @param {Function} progressFn - a callback that is executed each time a progress event
22702   *                                is received
22703   * @param {Function} trackInfoFn - a callback that receives track info
22704   * @param {Function} timingInfoFn - a callback that receives timing info
22705   * @param {Function} videoSegmentTimingInfoFn
22706   *                   a callback that receives video timing info based on media times and
22707   *                   any adjustments made by the transmuxer
22708   * @param {Function} audioSegmentTimingInfoFn
22709   *                   a callback that receives audio timing info based on media times and
22710   *                   any adjustments made by the transmuxer
22711   * @param {boolean}  isEndOfTimeline
22712   *                   true if this segment represents the last segment in a timeline
22713   * @param {Function} endedTimelineFn
22714   *                   a callback made when a timeline is ended, will only be called if
22715   *                   isEndOfTimeline is true
22716   * @param {Function} dataFn - a callback that is executed when segment bytes are available
22717   *                            and ready to use
22718   * @param {Event} event - the progress event object from XMLHttpRequest
22719   */
22720
22721
22722  const handleProgress = ({
22723    segment,
22724    progressFn,
22725    trackInfoFn,
22726    timingInfoFn,
22727    videoSegmentTimingInfoFn,
22728    audioSegmentTimingInfoFn,
22729    id3Fn,
22730    captionsFn,
22731    isEndOfTimeline,
22732    endedTimelineFn,
22733    dataFn
22734  }) => event => {
22735    const request = event.target;
22736
22737    if (request.aborted) {
22738      return;
22739    }
22740
22741    segment.stats = merge$1(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data
22742
22743    if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) {
22744      segment.stats.firstBytesReceivedAt = Date.now();
22745    }
22746
22747    return progressFn(event, segment);
22748  };
22749  /**
22750   * Load all resources and does any processing necessary for a media-segment
22751   *
22752   * Features:
22753   *   decrypts the media-segment if it has a key uri and an iv
22754   *   aborts *all* requests if *any* one request fails
22755   *
22756   * The segment object, at minimum, has the following format:
22757   * {
22758   *   resolvedUri: String,
22759   *   [transmuxer]: Object,
22760   *   [byterange]: {
22761   *     offset: Number,
22762   *     length: Number
22763   *   },
22764   *   [key]: {
22765   *     resolvedUri: String
22766   *     [byterange]: {
22767   *       offset: Number,
22768   *       length: Number
22769   *     },
22770   *     iv: {
22771   *       bytes: Uint32Array
22772   *     }
22773   *   },
22774   *   [map]: {
22775   *     resolvedUri: String,
22776   *     [byterange]: {
22777   *       offset: Number,
22778   *       length: Number
22779   *     },
22780   *     [bytes]: Uint8Array
22781   *   }
22782   * }
22783   * ...where [name] denotes optional properties
22784   *
22785   * @param {Function} xhr - an instance of the xhr wrapper in xhr.js
22786   * @param {Object} xhrOptions - the base options to provide to all xhr requests
22787   * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128
22788   *                                       decryption routines
22789   * @param {Object} segment - a simplified copy of the segmentInfo object
22790   *                           from SegmentLoader
22791   * @param {Function} abortFn - a callback called (only once) if any piece of a request was
22792   *                             aborted
22793   * @param {Function} progressFn - a callback that receives progress events from the main
22794   *                                segment's xhr request
22795   * @param {Function} trackInfoFn - a callback that receives track info
22796   * @param {Function} timingInfoFn - a callback that receives timing info
22797   * @param {Function} videoSegmentTimingInfoFn
22798   *                   a callback that receives video timing info based on media times and
22799   *                   any adjustments made by the transmuxer
22800   * @param {Function} audioSegmentTimingInfoFn
22801   *                   a callback that receives audio timing info based on media times and
22802   *                   any adjustments made by the transmuxer
22803   * @param {Function} id3Fn - a callback that receives ID3 metadata
22804   * @param {Function} captionsFn - a callback that receives captions
22805   * @param {boolean}  isEndOfTimeline
22806   *                   true if this segment represents the last segment in a timeline
22807   * @param {Function} endedTimelineFn
22808   *                   a callback made when a timeline is ended, will only be called if
22809   *                   isEndOfTimeline is true
22810   * @param {Function} dataFn - a callback that receives data from the main segment's xhr
22811   *                            request, transmuxed if needed
22812   * @param {Function} doneFn - a callback that is executed only once all requests have
22813   *                            succeeded or failed
22814   * @return {Function} a function that, when invoked, immediately aborts all
22815   *                     outstanding requests
22816   */
22817
22818
22819  const mediaSegmentRequest = ({
22820    xhr,
22821    xhrOptions,
22822    decryptionWorker,
22823    segment,
22824    abortFn,
22825    progressFn,
22826    trackInfoFn,
22827    timingInfoFn,
22828    videoSegmentTimingInfoFn,
22829    audioSegmentTimingInfoFn,
22830    id3Fn,
22831    captionsFn,
22832    isEndOfTimeline,
22833    endedTimelineFn,
22834    dataFn,
22835    doneFn,
22836    onTransmuxerLog,
22837    triggerSegmentEventFn
22838  }) => {
22839    const activeXhrs = [];
22840    const finishProcessingFn = waitForCompletion({
22841      activeXhrs,
22842      decryptionWorker,
22843      trackInfoFn,
22844      timingInfoFn,
22845      videoSegmentTimingInfoFn,
22846      audioSegmentTimingInfoFn,
22847      id3Fn,
22848      captionsFn,
22849      isEndOfTimeline,
22850      endedTimelineFn,
22851      dataFn,
22852      doneFn,
22853      onTransmuxerLog,
22854      triggerSegmentEventFn
22855    }); // optionally, request the decryption key
22856
22857    if (segment.key && !segment.key.bytes) {
22858      const objects = [segment.key];
22859
22860      if (segment.map && !segment.map.bytes && segment.map.key && segment.map.key.resolvedUri === segment.key.resolvedUri) {
22861        objects.push(segment.map.key);
22862      }
22863
22864      const keyRequestOptions = merge$1(xhrOptions, {
22865        uri: segment.key.resolvedUri,
22866        responseType: 'arraybuffer',
22867        requestType: 'segment-key'
22868      });
22869      const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn, triggerSegmentEventFn);
22870      const keyInfo = {
22871        uri: segment.key.resolvedUri
22872      };
22873      triggerSegmentEventFn({
22874        type: 'segmentkeyloadstart',
22875        segment,
22876        keyInfo
22877      });
22878      const keyXhr = xhr(keyRequestOptions, keyRequestCallback);
22879      activeXhrs.push(keyXhr);
22880    } // optionally, request the associated media init segment
22881
22882
22883    if (segment.map && !segment.map.bytes) {
22884      const differentMapKey = segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri);
22885
22886      if (differentMapKey) {
22887        const mapKeyRequestOptions = merge$1(xhrOptions, {
22888          uri: segment.map.key.resolvedUri,
22889          responseType: 'arraybuffer',
22890          requestType: 'segment-key'
22891        });
22892        const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn, triggerSegmentEventFn);
22893        const keyInfo = {
22894          uri: segment.map.key.resolvedUri
22895        };
22896        triggerSegmentEventFn({
22897          type: 'segmentkeyloadstart',
22898          segment,
22899          keyInfo
22900        });
22901        const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback);
22902        activeXhrs.push(mapKeyXhr);
22903      }
22904
22905      const initSegmentOptions = merge$1(xhrOptions, {
22906        uri: segment.map.resolvedUri,
22907        responseType: 'arraybuffer',
22908        headers: segmentXhrHeaders(segment.map),
22909        requestType: 'segment-media-initialization'
22910      });
22911      const initSegmentRequestCallback = handleInitSegmentResponse({
22912        segment,
22913        finishProcessingFn,
22914        triggerSegmentEventFn
22915      });
22916      triggerSegmentEventFn({
22917        type: 'segmentloadstart',
22918        segment
22919      });
22920      const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
22921      activeXhrs.push(initSegmentXhr);
22922    }
22923
22924    const segmentRequestOptions = merge$1(xhrOptions, {
22925      uri: segment.part && segment.part.resolvedUri || segment.resolvedUri,
22926      responseType: 'arraybuffer',
22927      headers: segmentXhrHeaders(segment),
22928      requestType: 'segment'
22929    });
22930    const segmentRequestCallback = handleSegmentResponse({
22931      segment,
22932      finishProcessingFn,
22933      responseType: segmentRequestOptions.responseType,
22934      triggerSegmentEventFn
22935    });
22936    triggerSegmentEventFn({
22937      type: 'segmentloadstart',
22938      segment
22939    });
22940    const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback);
22941    segmentXhr.addEventListener('progress', handleProgress({
22942      segment,
22943      progressFn,
22944      trackInfoFn,
22945      timingInfoFn,
22946      videoSegmentTimingInfoFn,
22947      audioSegmentTimingInfoFn,
22948      id3Fn,
22949      captionsFn,
22950      isEndOfTimeline,
22951      endedTimelineFn,
22952      dataFn
22953    }));
22954    activeXhrs.push(segmentXhr); // since all parts of the request must be considered, but should not make callbacks
22955    // multiple times, provide a shared state object
22956
22957    const loadendState = {};
22958    activeXhrs.forEach(activeXhr => {
22959      activeXhr.addEventListener('loadend', handleLoadEnd({
22960        loadendState,
22961        abortFn
22962      }));
22963    });
22964    return () => abortAll(activeXhrs);
22965  };
22966
22967  const logFn = logger('PlaylistSelector');
22968
22969  const representationToString = function (representation) {
22970    if (!representation || !representation.playlist) {
22971      return;
22972    }
22973
22974    const playlist = representation.playlist;
22975    return JSON.stringify({
22976      id: playlist.id,
22977      bandwidth: representation.bandwidth,
22978      width: representation.width,
22979      height: representation.height,
22980      codecs: playlist.attributes && playlist.attributes.CODECS || ''
22981    });
22982  }; // Utilities
22983
22984  /**
22985   * Returns the CSS value for the specified property on an element
22986   * using `getComputedStyle`. Firefox has a long-standing issue where
22987   * getComputedStyle() may return null when running in an iframe with
22988   * `display: none`.
22989   *
22990   * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
22991   * @param {HTMLElement} el the htmlelement to work on
22992   * @param {string} the proprety to get the style for
22993   */
22994
22995
22996  const safeGetComputedStyle = function (el, property) {
22997    if (!el) {
22998      return '';
22999    }
23000
23001    const result = window.getComputedStyle(el);
23002
23003    if (!result) {
23004      return '';
23005    }
23006
23007    return result[property];
23008  };
23009  /**
23010   * Resuable stable sort function
23011   *
23012   * @param {Playlists} array
23013   * @param {Function} sortFn Different comparators
23014   * @function stableSort
23015   */
23016
23017
23018  const stableSort = function (array, sortFn) {
23019    const newArray = array.slice();
23020    array.sort(function (left, right) {
23021      const cmp = sortFn(left, right);
23022
23023      if (cmp === 0) {
23024        return newArray.indexOf(left) - newArray.indexOf(right);
23025      }
23026
23027      return cmp;
23028    });
23029  };
23030  /**
23031   * A comparator function to sort two playlist object by bandwidth.
23032   *
23033   * @param {Object} left a media playlist object
23034   * @param {Object} right a media playlist object
23035   * @return {number} Greater than zero if the bandwidth attribute of
23036   * left is greater than the corresponding attribute of right. Less
23037   * than zero if the bandwidth of right is greater than left and
23038   * exactly zero if the two are equal.
23039   */
23040
23041
23042  const comparePlaylistBandwidth = function (left, right) {
23043    let leftBandwidth;
23044    let rightBandwidth;
23045
23046    if (left.attributes.BANDWIDTH) {
23047      leftBandwidth = left.attributes.BANDWIDTH;
23048    }
23049
23050    leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
23051
23052    if (right.attributes.BANDWIDTH) {
23053      rightBandwidth = right.attributes.BANDWIDTH;
23054    }
23055
23056    rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
23057    return leftBandwidth - rightBandwidth;
23058  };
23059  /**
23060   * A comparator function to sort two playlist object by resolution (width).
23061   *
23062   * @param {Object} left a media playlist object
23063   * @param {Object} right a media playlist object
23064   * @return {number} Greater than zero if the resolution.width attribute of
23065   * left is greater than the corresponding attribute of right. Less
23066   * than zero if the resolution.width of right is greater than left and
23067   * exactly zero if the two are equal.
23068   */
23069
23070  const comparePlaylistResolution = function (left, right) {
23071    let leftWidth;
23072    let rightWidth;
23073
23074    if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
23075      leftWidth = left.attributes.RESOLUTION.width;
23076    }
23077
23078    leftWidth = leftWidth || window.Number.MAX_VALUE;
23079
23080    if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
23081      rightWidth = right.attributes.RESOLUTION.width;
23082    }
23083
23084    rightWidth = rightWidth || window.Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
23085    // have the same media dimensions/ resolution
23086
23087    if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
23088      return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
23089    }
23090
23091    return leftWidth - rightWidth;
23092  };
23093  /**
23094   * Chooses the appropriate media playlist based on bandwidth and player size
23095   *
23096   * @param {Object} settings
23097   *        Object of information required to use this selector
23098   * @param {Object} settings.main
23099   *        Object representation of the main manifest
23100   * @param {number} settings.bandwidth
23101   *        Current calculated bandwidth of the player
23102   * @param {number} settings.playerWidth
23103   *        Current width of the player element (should account for the device pixel ratio)
23104   * @param {number} settings.playerHeight
23105   *        Current height of the player element (should account for the device pixel ratio)
23106   * @param {number} settings.playerObjectFit
23107   *        Current value of the video element's object-fit CSS property. Allows taking into
23108   *        account that the video might be scaled up to cover the media element when selecting
23109   *        media playlists based on player size.
23110   * @param {boolean} settings.limitRenditionByPlayerDimensions
23111   *        True if the player width and height should be used during the selection, false otherwise
23112   * @param {Object} settings.playlistController
23113   *        the current playlistController object
23114   * @return {Playlist} the highest bitrate playlist less than the
23115   * currently detected bandwidth, accounting for some amount of
23116   * bandwidth variance
23117   */
23118
23119  let simpleSelector = function (settings) {
23120    const {
23121      main,
23122      bandwidth: playerBandwidth,
23123      playerWidth,
23124      playerHeight,
23125      playerObjectFit,
23126      limitRenditionByPlayerDimensions,
23127      playlistController
23128    } = settings; // If we end up getting called before `main` is available, exit early
23129
23130    if (!main) {
23131      return;
23132    }
23133
23134    const options = {
23135      bandwidth: playerBandwidth,
23136      width: playerWidth,
23137      height: playerHeight,
23138      limitRenditionByPlayerDimensions
23139    };
23140    let playlists = main.playlists; // if playlist is audio only, select between currently active audio group playlists.
23141
23142    if (Playlist.isAudioOnly(main)) {
23143      playlists = playlistController.getAudioTrackPlaylists_(); // add audioOnly to options so that we log audioOnly: true
23144      // at the buttom of this function for debugging.
23145
23146      options.audioOnly = true;
23147    } // convert the playlists to an intermediary representation to make comparisons easier
23148
23149
23150    let sortedPlaylistReps = playlists.map(playlist => {
23151      let bandwidth;
23152      const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width;
23153      const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height;
23154      bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH;
23155      bandwidth = bandwidth || window.Number.MAX_VALUE;
23156      return {
23157        bandwidth,
23158        width,
23159        height,
23160        playlist
23161      };
23162    });
23163    stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); // filter out any playlists that have been excluded due to
23164    // incompatible configurations
23165
23166    sortedPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isIncompatible(rep.playlist)); // filter out any playlists that have been disabled manually through the representations
23167    // api or excluded temporarily due to playback errors.
23168
23169    let enabledPlaylistReps = sortedPlaylistReps.filter(rep => Playlist.isEnabled(rep.playlist));
23170
23171    if (!enabledPlaylistReps.length) {
23172      // if there are no enabled playlists, then they have all been excluded or disabled
23173      // by the user through the representations api. In this case, ignore exclusion and
23174      // fallback to what the user wants by using playlists the user has not disabled.
23175      enabledPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isDisabled(rep.playlist));
23176    } // filter out any variant that has greater effective bitrate
23177    // than the current estimated bandwidth
23178
23179
23180    const bandwidthPlaylistReps = enabledPlaylistReps.filter(rep => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth);
23181    let highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth
23182    // and then taking the very first element
23183
23184    const bandwidthBestRep = bandwidthPlaylistReps.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; // if we're not going to limit renditions by player size, make an early decision.
23185
23186    if (limitRenditionByPlayerDimensions === false) {
23187      const chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0];
23188
23189      if (chosenRep && chosenRep.playlist) {
23190        let type = 'sortedPlaylistReps';
23191
23192        if (bandwidthBestRep) {
23193          type = 'bandwidthBestRep';
23194        }
23195
23196        if (enabledPlaylistReps[0]) {
23197          type = 'enabledPlaylistReps';
23198        }
23199
23200        logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options);
23201        return chosenRep.playlist;
23202      }
23203
23204      logFn('could not choose a playlist with options', options);
23205      return null;
23206    } // filter out playlists without resolution information
23207
23208
23209    const haveResolution = bandwidthPlaylistReps.filter(rep => rep.width && rep.height); // sort variants by resolution
23210
23211    stableSort(haveResolution, (left, right) => left.width - right.width); // if we have the exact resolution as the player use it
23212
23213    const resolutionBestRepList = haveResolution.filter(rep => rep.width === playerWidth && rep.height === playerHeight);
23214    highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution
23215
23216    const resolutionBestRep = resolutionBestRepList.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
23217    let resolutionPlusOneList;
23218    let resolutionPlusOneSmallest;
23219    let resolutionPlusOneRep; // find the smallest variant that is larger than the player
23220    // if there is no match of exact resolution
23221
23222    if (!resolutionBestRep) {
23223      resolutionPlusOneList = haveResolution.filter(rep => {
23224        if (playerObjectFit === 'cover') {
23225          // video will be scaled up to cover the player. We need to
23226          // make sure rendition is at least as wide and as high as the
23227          // player.
23228          return rep.width > playerWidth && rep.height > playerHeight;
23229        } // video will be scaled down to fit inside the player soon as
23230        // its resolution exceeds player size in at least one dimension.
23231
23232
23233        return rep.width > playerWidth || rep.height > playerHeight;
23234      }); // find all the variants have the same smallest resolution
23235
23236      resolutionPlusOneSmallest = resolutionPlusOneList.filter(rep => rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height); // ensure that we also pick the highest bandwidth variant that
23237      // is just-larger-than the video player
23238
23239      highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1];
23240      resolutionPlusOneRep = resolutionPlusOneSmallest.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0];
23241    }
23242
23243    let leastPixelDiffRep; // If this selector proves to be better than others,
23244    // resolutionPlusOneRep and resolutionBestRep and all
23245    // the code involving them should be removed.
23246
23247    if (playlistController.leastPixelDiffSelector) {
23248      // find the variant that is closest to the player's pixel size
23249      const leastPixelDiffList = haveResolution.map(rep => {
23250        rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight);
23251        return rep;
23252      }); // get the highest bandwidth, closest resolution playlist
23253
23254      stableSort(leastPixelDiffList, (left, right) => {
23255        // sort by highest bandwidth if pixelDiff is the same
23256        if (left.pixelDiff === right.pixelDiff) {
23257          return right.bandwidth - left.bandwidth;
23258        }
23259
23260        return left.pixelDiff - right.pixelDiff;
23261      });
23262      leastPixelDiffRep = leastPixelDiffList[0];
23263    } // fallback chain of variants
23264
23265
23266    const chosenRep = leastPixelDiffRep || resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0];
23267
23268    if (chosenRep && chosenRep.playlist) {
23269      let type = 'sortedPlaylistReps';
23270
23271      if (leastPixelDiffRep) {
23272        type = 'leastPixelDiffRep';
23273      } else if (resolutionPlusOneRep) {
23274        type = 'resolutionPlusOneRep';
23275      } else if (resolutionBestRep) {
23276        type = 'resolutionBestRep';
23277      } else if (bandwidthBestRep) {
23278        type = 'bandwidthBestRep';
23279      } else if (enabledPlaylistReps[0]) {
23280        type = 'enabledPlaylistReps';
23281      }
23282
23283      logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options);
23284      return chosenRep.playlist;
23285    }
23286
23287    logFn('could not choose a playlist with options', options);
23288    return null;
23289  };
23290
23291  /**
23292   * Chooses the appropriate media playlist based on the most recent
23293   * bandwidth estimate and the player size.
23294   *
23295   * Expects to be called within the context of an instance of VhsHandler
23296   *
23297   * @return {Playlist} the highest bitrate playlist less than the
23298   * currently detected bandwidth, accounting for some amount of
23299   * bandwidth variance
23300   */
23301
23302  const lastBandwidthSelector = function () {
23303    let pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
23304
23305    if (!isNaN(this.customPixelRatio)) {
23306      pixelRatio = this.customPixelRatio;
23307    }
23308
23309    return simpleSelector({
23310      main: this.playlists.main,
23311      bandwidth: this.systemBandwidth,
23312      playerWidth: parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio,
23313      playerHeight: parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio,
23314      playerObjectFit: this.usePlayerObjectFit ? safeGetComputedStyle(this.tech_.el(), 'objectFit') : '',
23315      limitRenditionByPlayerDimensions: this.limitRenditionByPlayerDimensions,
23316      playlistController: this.playlistController_
23317    });
23318  };
23319  /**
23320   * Chooses the appropriate media playlist based on an
23321   * exponential-weighted moving average of the bandwidth after
23322   * filtering for player size.
23323   *
23324   * Expects to be called within the context of an instance of VhsHandler
23325   *
23326   * @param {number} decay - a number between 0 and 1. Higher values of
23327   * this parameter will cause previous bandwidth estimates to lose
23328   * significance more quickly.
23329   * @return {Function} a function which can be invoked to create a new
23330   * playlist selector function.
23331   * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
23332   */
23333
23334  const movingAverageBandwidthSelector = function (decay) {
23335    let average = -1;
23336    let lastSystemBandwidth = -1;
23337
23338    if (decay < 0 || decay > 1) {
23339      throw new Error('Moving average bandwidth decay must be between 0 and 1.');
23340    }
23341
23342    return function () {
23343      let pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
23344
23345      if (!isNaN(this.customPixelRatio)) {
23346        pixelRatio = this.customPixelRatio;
23347      }
23348
23349      if (average < 0) {
23350        average = this.systemBandwidth;
23351        lastSystemBandwidth = this.systemBandwidth;
23352      } // stop the average value from decaying for every 250ms
23353      // when the systemBandwidth is constant
23354      // and
23355      // stop average from setting to a very low value when the
23356      // systemBandwidth becomes 0 in case of chunk cancellation
23357
23358
23359      if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) {
23360        average = decay * this.systemBandwidth + (1 - decay) * average;
23361        lastSystemBandwidth = this.systemBandwidth;
23362      }
23363
23364      return simpleSelector({
23365        main: this.playlists.main,
23366        bandwidth: average,
23367        playerWidth: parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio,
23368        playerHeight: parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio,
23369        playerObjectFit: this.usePlayerObjectFit ? safeGetComputedStyle(this.tech_.el(), 'objectFit') : '',
23370        limitRenditionByPlayerDimensions: this.limitRenditionByPlayerDimensions,
23371        playlistController: this.playlistController_
23372      });
23373    };
23374  };
23375  /**
23376   * Chooses the appropriate media playlist based on the potential to rebuffer
23377   *
23378   * @param {Object} settings
23379   *        Object of information required to use this selector
23380   * @param {Object} settings.main
23381   *        Object representation of the main manifest
23382   * @param {number} settings.currentTime
23383   *        The current time of the player
23384   * @param {number} settings.bandwidth
23385   *        Current measured bandwidth
23386   * @param {number} settings.duration
23387   *        Duration of the media
23388   * @param {number} settings.segmentDuration
23389   *        Segment duration to be used in round trip time calculations
23390   * @param {number} settings.timeUntilRebuffer
23391   *        Time left in seconds until the player has to rebuffer
23392   * @param {number} settings.currentTimeline
23393   *        The current timeline segments are being loaded from
23394   * @param {SyncController} settings.syncController
23395   *        SyncController for determining if we have a sync point for a given playlist
23396   * @return {Object|null}
23397   *         {Object} return.playlist
23398   *         The highest bandwidth playlist with the least amount of rebuffering
23399   *         {Number} return.rebufferingImpact
23400   *         The amount of time in seconds switching to this playlist will rebuffer. A
23401   *         negative value means that switching will cause zero rebuffering.
23402   */
23403
23404  const minRebufferMaxBandwidthSelector = function (settings) {
23405    const {
23406      main,
23407      currentTime,
23408      bandwidth,
23409      duration,
23410      segmentDuration,
23411      timeUntilRebuffer,
23412      currentTimeline,
23413      syncController
23414    } = settings; // filter out any playlists that have been excluded due to
23415    // incompatible configurations
23416
23417    const compatiblePlaylists = main.playlists.filter(playlist => !Playlist.isIncompatible(playlist)); // filter out any playlists that have been disabled manually through the representations
23418    // api or excluded temporarily due to playback errors.
23419
23420    let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled);
23421
23422    if (!enabledPlaylists.length) {
23423      // if there are no enabled playlists, then they have all been excluded or disabled
23424      // by the user through the representations api. In this case, ignore exclusion and
23425      // fallback to what the user wants by using playlists the user has not disabled.
23426      enabledPlaylists = compatiblePlaylists.filter(playlist => !Playlist.isDisabled(playlist));
23427    }
23428
23429    const bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH'));
23430    const rebufferingEstimates = bandwidthPlaylists.map(playlist => {
23431      const syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a
23432      // sync request first. This will double the request time
23433
23434      const numRequests = syncPoint ? 1 : 2;
23435      const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist);
23436      const rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer;
23437      return {
23438        playlist,
23439        rebufferingImpact
23440      };
23441    });
23442    const noRebufferingPlaylists = rebufferingEstimates.filter(estimate => estimate.rebufferingImpact <= 0); // Sort by bandwidth DESC
23443
23444    stableSort(noRebufferingPlaylists, (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist));
23445
23446    if (noRebufferingPlaylists.length) {
23447      return noRebufferingPlaylists[0];
23448    }
23449
23450    stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact);
23451    return rebufferingEstimates[0] || null;
23452  };
23453  /**
23454   * Chooses the appropriate media playlist, which in this case is the lowest bitrate
23455   * one with video.  If no renditions with video exist, return the lowest audio rendition.
23456   *
23457   * Expects to be called within the context of an instance of VhsHandler
23458   *
23459   * @return {Object|null}
23460   *         {Object} return.playlist
23461   *         The lowest bitrate playlist that contains a video codec.  If no such rendition
23462   *         exists pick the lowest audio rendition.
23463   */
23464
23465  const lowestBitrateCompatibleVariantSelector = function () {
23466    // filter out any playlists that have been excluded due to
23467    // incompatible configurations or playback errors
23468    const playlists = this.playlists.main.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate
23469
23470    stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); // Parse and assume that playlists with no video codec have no video
23471    // (this is not necessarily true, although it is generally true).
23472    //
23473    // If an entire manifest has no valid videos everything will get filtered
23474    // out.
23475
23476    const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.main, playlist).video);
23477    return playlistsWithVideo[0] || null;
23478  };
23479
23480  /**
23481   * Combine all segments into a single Uint8Array
23482   *
23483   * @param {Object} segmentObj
23484   * @return {Uint8Array} concatenated bytes
23485   * @private
23486   */
23487  const concatSegments = segmentObj => {
23488    let offset = 0;
23489    let tempBuffer;
23490
23491    if (segmentObj.bytes) {
23492      tempBuffer = new Uint8Array(segmentObj.bytes); // combine the individual segments into one large typed-array
23493
23494      segmentObj.segments.forEach(segment => {
23495        tempBuffer.set(segment, offset);
23496        offset += segment.byteLength;
23497      });
23498    }
23499
23500    return tempBuffer;
23501  };
23502  /**
23503   * Example:
23504   * https://host.com/path1/path2/path3/segment.ts?arg1=val1
23505   * -->
23506   * path3/segment.ts
23507   *
23508   * @param resolvedUri
23509   * @return {string}
23510   */
23511
23512  function compactSegmentUrlDescription(resolvedUri) {
23513    try {
23514      return new URL(resolvedUri).pathname.split('/').slice(-2).join('/');
23515    } catch (e) {
23516      return '';
23517    }
23518  }
23519
23520  /**
23521   * @file text-tracks.js
23522   */
23523  /**
23524   * Create captions text tracks on video.js if they do not exist
23525   *
23526   * @param {Object} inbandTextTracks a reference to current inbandTextTracks
23527   * @param {Object} tech the video.js tech
23528   * @param {Object} captionStream the caption stream to create
23529   * @private
23530   */
23531
23532  const createCaptionsTrackIfNotExists = function (inbandTextTracks, tech, captionStream) {
23533    if (!inbandTextTracks[captionStream]) {
23534      tech.trigger({
23535        type: 'usage',
23536        name: 'vhs-608'
23537      });
23538      let instreamId = captionStream; // we need to translate SERVICEn for 708 to how mux.js currently labels them
23539
23540      if (/^cc708_/.test(captionStream)) {
23541        instreamId = 'SERVICE' + captionStream.split('_')[1];
23542      }
23543
23544      const track = tech.textTracks().getTrackById(instreamId);
23545
23546      if (track) {
23547        // Resuse an existing track with a CC# id because this was
23548        // very likely created by videojs-contrib-hls from information
23549        // in the m3u8 for us to use
23550        inbandTextTracks[captionStream] = track;
23551      } else {
23552        // This section gets called when we have caption services that aren't specified in the manifest.
23553        // Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS.
23554        const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {};
23555        let label = captionStream;
23556        let language = captionStream;
23557        let def = false;
23558        const captionService = captionServices[instreamId];
23559
23560        if (captionService) {
23561          label = captionService.label;
23562          language = captionService.language;
23563          def = captionService.default;
23564        } // Otherwise, create a track with the default `CC#` label and
23565        // without a language
23566
23567
23568        inbandTextTracks[captionStream] = tech.addRemoteTextTrack({
23569          kind: 'captions',
23570          id: instreamId,
23571          // TODO: investigate why this doesn't seem to turn the caption on by default
23572          default: def,
23573          label,
23574          language
23575        }, false).track;
23576      }
23577    }
23578  };
23579  /**
23580   * Add caption text track data to a source handler given an array of captions
23581   *
23582   * @param {Object}
23583   *   @param {Object} inbandTextTracks the inband text tracks
23584   *   @param {number} timestampOffset the timestamp offset of the source buffer
23585   *   @param {Array} captionArray an array of caption data
23586   * @private
23587   */
23588
23589  const addCaptionData = function ({
23590    inbandTextTracks,
23591    captionArray,
23592    timestampOffset
23593  }) {
23594    if (!captionArray) {
23595      return;
23596    }
23597
23598    const Cue = window.WebKitDataCue || window.VTTCue;
23599    captionArray.forEach(caption => {
23600      const track = caption.stream; // in CEA 608 captions, video.js/mux.js sends a content array
23601      // with positioning data
23602
23603      if (caption.content) {
23604        caption.content.forEach(value => {
23605          const cue = new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, value.text);
23606          cue.line = value.line;
23607          cue.align = 'left';
23608          cue.position = value.position;
23609          cue.positionAlign = 'line-left';
23610          inbandTextTracks[track].addCue(cue);
23611        });
23612      } else {
23613        // otherwise, a text value with combined captions is sent
23614        inbandTextTracks[track].addCue(new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, caption.text));
23615      }
23616    });
23617  };
23618  /**
23619   * Define properties on a cue for backwards compatability,
23620   * but warn the user that the way that they are using it
23621   * is depricated and will be removed at a later date.
23622   *
23623   * @param {Cue} cue the cue to add the properties on
23624   * @private
23625   */
23626
23627  const deprecateOldCue = function (cue) {
23628    Object.defineProperties(cue.frame, {
23629      id: {
23630        get() {
23631          videojs__default["default"].log.warn('cue.frame.id is deprecated. Use cue.value.key instead.');
23632          return cue.value.key;
23633        }
23634
23635      },
23636      value: {
23637        get() {
23638          videojs__default["default"].log.warn('cue.frame.value is deprecated. Use cue.value.data instead.');
23639          return cue.value.data;
23640        }
23641
23642      },
23643      privateData: {
23644        get() {
23645          videojs__default["default"].log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.');
23646          return cue.value.data;
23647        }
23648
23649      }
23650    });
23651  };
23652  /**
23653   * Add metadata text track data to a source handler given an array of metadata
23654   *
23655   * @param {Object}
23656   *   @param {Object} inbandTextTracks the inband text tracks
23657   *   @param {Array} metadataArray an array of meta data
23658   *   @param {number} timestampOffset the timestamp offset of the source buffer
23659   *   @param {number} videoDuration the duration of the video
23660   * @private
23661   */
23662
23663
23664  const addMetadata = ({
23665    inbandTextTracks,
23666    metadataArray,
23667    timestampOffset,
23668    videoDuration
23669  }) => {
23670    if (!metadataArray) {
23671      return;
23672    }
23673
23674    const Cue = window.WebKitDataCue || window.VTTCue;
23675    const metadataTrack = inbandTextTracks.metadataTrack_;
23676
23677    if (!metadataTrack) {
23678      return;
23679    }
23680
23681    metadataArray.forEach(metadata => {
23682      const time = metadata.cueTime + timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN,
23683      // ignore this bit of metadata.
23684      // This likely occurs when you have an non-timed ID3 tag like TIT2,
23685      // which is the "Title/Songname/Content description" frame
23686
23687      if (typeof time !== 'number' || window.isNaN(time) || time < 0 || !(time < Infinity)) {
23688        return;
23689      } // If we have no frames, we can't create a cue.
23690
23691
23692      if (!metadata.frames || !metadata.frames.length) {
23693        return;
23694      }
23695
23696      metadata.frames.forEach(frame => {
23697        const cue = new Cue(time, time, frame.value || frame.url || frame.data || '');
23698        cue.frame = frame;
23699        cue.value = frame;
23700        deprecateOldCue(cue);
23701        metadataTrack.addCue(cue);
23702      });
23703    });
23704
23705    if (!metadataTrack.cues || !metadataTrack.cues.length) {
23706      return;
23707    } // Updating the metadeta cues so that
23708    // the endTime of each cue is the startTime of the next cue
23709    // the endTime of last cue is the duration of the video
23710
23711
23712    const cues = metadataTrack.cues;
23713    const cuesArray = []; // Create a copy of the TextTrackCueList...
23714    // ...disregarding cues with a falsey value
23715
23716    for (let i = 0; i < cues.length; i++) {
23717      if (cues[i]) {
23718        cuesArray.push(cues[i]);
23719      }
23720    } // Group cues by their startTime value
23721
23722
23723    const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => {
23724      const timeSlot = obj[cue.startTime] || [];
23725      timeSlot.push(cue);
23726      obj[cue.startTime] = timeSlot;
23727      return obj;
23728    }, {}); // Sort startTimes by ascending order
23729
23730    const sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort((a, b) => Number(a) - Number(b)); // Map each cue group's endTime to the next group's startTime
23731
23732    sortedStartTimes.forEach((startTime, idx) => {
23733      const cueGroup = cuesGroupedByStartTime[startTime];
23734      const finiteDuration = isFinite(videoDuration) ? videoDuration : startTime;
23735      const nextTime = Number(sortedStartTimes[idx + 1]) || finiteDuration; // Map each cue's endTime the next group's startTime
23736
23737      cueGroup.forEach(cue => {
23738        cue.endTime = nextTime;
23739      });
23740    });
23741  }; // object for mapping daterange attributes
23742
23743  const dateRangeAttr = {
23744    id: 'ID',
23745    class: 'CLASS',
23746    startDate: 'START-DATE',
23747    duration: 'DURATION',
23748    endDate: 'END-DATE',
23749    endOnNext: 'END-ON-NEXT',
23750    plannedDuration: 'PLANNED-DURATION',
23751    scte35Out: 'SCTE35-OUT',
23752    scte35In: 'SCTE35-IN'
23753  };
23754  const dateRangeKeysToOmit = new Set(['id', 'class', 'startDate', 'duration', 'endDate', 'endOnNext', 'startTime', 'endTime', 'processDateRange']);
23755  /**
23756   * Add DateRange metadata text track to a source handler given an array of metadata
23757   *
23758   * @param {Object}
23759   *   @param {Object} inbandTextTracks the inband text tracks
23760   *   @param {Array} dateRanges parsed media playlist
23761   * @private
23762   */
23763
23764  const addDateRangeMetadata = ({
23765    inbandTextTracks,
23766    dateRanges
23767  }) => {
23768    const metadataTrack = inbandTextTracks.metadataTrack_;
23769
23770    if (!metadataTrack) {
23771      return;
23772    }
23773
23774    const Cue = window.WebKitDataCue || window.VTTCue;
23775    dateRanges.forEach(dateRange => {
23776      // we generate multiple cues for each date range with different attributes
23777      for (const key of Object.keys(dateRange)) {
23778        if (dateRangeKeysToOmit.has(key)) {
23779          continue;
23780        }
23781
23782        const cue = new Cue(dateRange.startTime, dateRange.endTime, '');
23783        cue.id = dateRange.id;
23784        cue.type = 'com.apple.quicktime.HLS';
23785        cue.value = {
23786          key: dateRangeAttr[key],
23787          data: dateRange[key]
23788        };
23789
23790        if (key === 'scte35Out' || key === 'scte35In') {
23791          cue.value.data = new Uint8Array(cue.value.data.match(/[\da-f]{2}/gi)).buffer;
23792        }
23793
23794        metadataTrack.addCue(cue);
23795      }
23796
23797      dateRange.processDateRange();
23798    });
23799  };
23800  /**
23801   * Create metadata text track on video.js if it does not exist
23802   *
23803   * @param {Object} inbandTextTracks a reference to current inbandTextTracks
23804   * @param {string} dispatchType the inband metadata track dispatch type
23805   * @param {Object} tech the video.js tech
23806   * @private
23807   */
23808
23809  const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => {
23810    if (inbandTextTracks.metadataTrack_) {
23811      return;
23812    }
23813
23814    inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack({
23815      kind: 'metadata',
23816      label: 'Timed Metadata'
23817    }, false).track;
23818
23819    if (!videojs__default["default"].browser.IS_ANY_SAFARI) {
23820      inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType;
23821    }
23822  };
23823  /**
23824   * Remove cues from a track on video.js.
23825   *
23826   * @param {Double} start start of where we should remove the cue
23827   * @param {Double} end end of where the we should remove the cue
23828   * @param {Object} track the text track to remove the cues from
23829   * @private
23830   */
23831
23832  const removeCuesFromTrack = function (start, end, track) {
23833    let i;
23834    let cue;
23835
23836    if (!track) {
23837      return;
23838    }
23839
23840    if (!track.cues) {
23841      return;
23842    }
23843
23844    i = track.cues.length;
23845
23846    while (i--) {
23847      cue = track.cues[i]; // Remove any cue within the provided start and end time
23848
23849      if (cue.startTime >= start && cue.endTime <= end) {
23850        track.removeCue(cue);
23851      }
23852    }
23853  };
23854  /**
23855   * Remove duplicate cues from a track on video.js (a cue is considered a
23856   * duplicate if it has the same time interval and text as another)
23857   *
23858   * @param {Object} track the text track to remove the duplicate cues from
23859   * @private
23860   */
23861
23862  const removeDuplicateCuesFromTrack = function (track) {
23863    const cues = track.cues;
23864
23865    if (!cues) {
23866      return;
23867    }
23868
23869    const uniqueCues = {};
23870
23871    for (let i = cues.length - 1; i >= 0; i--) {
23872      const cue = cues[i];
23873      const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`;
23874
23875      if (uniqueCues[cueKey]) {
23876        track.removeCue(cue);
23877      } else {
23878        uniqueCues[cueKey] = cue;
23879      }
23880    }
23881  };
23882
23883  /**
23884   * mux.js
23885   *
23886   * Copyright (c) Brightcove
23887   * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE
23888   */
23889  var ONE_SECOND_IN_TS = 90000,
23890      // 90kHz clock
23891  secondsToVideoTs,
23892      secondsToAudioTs,
23893      videoTsToSeconds,
23894      audioTsToSeconds,
23895      audioTsToVideoTs,
23896      videoTsToAudioTs,
23897      metadataTsToSeconds;
23898
23899  secondsToVideoTs = function (seconds) {
23900    return seconds * ONE_SECOND_IN_TS;
23901  };
23902
23903  secondsToAudioTs = function (seconds, sampleRate) {
23904    return seconds * sampleRate;
23905  };
23906
23907  videoTsToSeconds = function (timestamp) {
23908    return timestamp / ONE_SECOND_IN_TS;
23909  };
23910
23911  audioTsToSeconds = function (timestamp, sampleRate) {
23912    return timestamp / sampleRate;
23913  };
23914
23915  audioTsToVideoTs = function (timestamp, sampleRate) {
23916    return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate));
23917  };
23918
23919  videoTsToAudioTs = function (timestamp, sampleRate) {
23920    return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate);
23921  };
23922  /**
23923   * Adjust ID3 tag or caption timing information by the timeline pts values
23924   * (if keepOriginalTimestamps is false) and convert to seconds
23925   */
23926
23927
23928  metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) {
23929    return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts);
23930  };
23931
23932  var clock = {
23933    ONE_SECOND_IN_TS: ONE_SECOND_IN_TS,
23934    secondsToVideoTs: secondsToVideoTs,
23935    secondsToAudioTs: secondsToAudioTs,
23936    videoTsToSeconds: videoTsToSeconds,
23937    audioTsToSeconds: audioTsToSeconds,
23938    audioTsToVideoTs: audioTsToVideoTs,
23939    videoTsToAudioTs: videoTsToAudioTs,
23940    metadataTsToSeconds: metadataTsToSeconds
23941  };
23942
23943  /**
23944   * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in
23945   * front of current time.
23946   *
23947   * @param {Array} buffer
23948   *        The current buffer of gop information
23949   * @param {number} currentTime
23950   *        The current time
23951   * @param {Double} mapping
23952   *        Offset to map display time to stream presentation time
23953   * @return {Array}
23954   *         List of gops considered safe to append over
23955   */
23956
23957  const gopsSafeToAlignWith = (buffer, currentTime, mapping) => {
23958    if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) {
23959      return [];
23960    } // pts value for current time + 3 seconds to give a bit more wiggle room
23961
23962
23963    const currentTimePts = Math.ceil((currentTime - mapping + 3) * clock.ONE_SECOND_IN_TS);
23964    let i;
23965
23966    for (i = 0; i < buffer.length; i++) {
23967      if (buffer[i].pts > currentTimePts) {
23968        break;
23969      }
23970    }
23971
23972    return buffer.slice(i);
23973  };
23974  /**
23975   * Appends gop information (timing and byteLength) received by the transmuxer for the
23976   * gops appended in the last call to appendBuffer
23977   *
23978   * @param {Array} buffer
23979   *        The current buffer of gop information
23980   * @param {Array} gops
23981   *        List of new gop information
23982   * @param {boolean} replace
23983   *        If true, replace the buffer with the new gop information. If false, append the
23984   *        new gop information to the buffer in the right location of time.
23985   * @return {Array}
23986   *         Updated list of gop information
23987   */
23988
23989  const updateGopBuffer = (buffer, gops, replace) => {
23990    if (!gops.length) {
23991      return buffer;
23992    }
23993
23994    if (replace) {
23995      // If we are in safe append mode, then completely overwrite the gop buffer
23996      // with the most recent appeneded data. This will make sure that when appending
23997      // future segments, we only try to align with gops that are both ahead of current
23998      // time and in the last segment appended.
23999      return gops.slice();
24000    }
24001
24002    const start = gops[0].pts;
24003    let i = 0;
24004
24005    for (i; i < buffer.length; i++) {
24006      if (buffer[i].pts >= start) {
24007        break;
24008      }
24009    }
24010
24011    return buffer.slice(0, i).concat(gops);
24012  };
24013  /**
24014   * Removes gop information in buffer that overlaps with provided start and end
24015   *
24016   * @param {Array} buffer
24017   *        The current buffer of gop information
24018   * @param {Double} start
24019   *        position to start the remove at
24020   * @param {Double} end
24021   *        position to end the remove at
24022   * @param {Double} mapping
24023   *        Offset to map display time to stream presentation time
24024   */
24025
24026  const removeGopBuffer = (buffer, start, end, mapping) => {
24027    const startPts = Math.ceil((start - mapping) * clock.ONE_SECOND_IN_TS);
24028    const endPts = Math.ceil((end - mapping) * clock.ONE_SECOND_IN_TS);
24029    const updatedBuffer = buffer.slice();
24030    let i = buffer.length;
24031
24032    while (i--) {
24033      if (buffer[i].pts <= endPts) {
24034        break;
24035      }
24036    }
24037
24038    if (i === -1) {
24039      // no removal because end of remove range is before start of buffer
24040      return updatedBuffer;
24041    }
24042
24043    let j = i + 1;
24044
24045    while (j--) {
24046      if (buffer[j].pts <= startPts) {
24047        break;
24048      }
24049    } // clamp remove range start to 0 index
24050
24051
24052    j = Math.max(j, 0);
24053    updatedBuffer.splice(j, i - j + 1);
24054    return updatedBuffer;
24055  };
24056
24057  const shallowEqual = function (a, b) {
24058    // if both are undefined
24059    // or one or the other is undefined
24060    // they are not equal
24061    if (!a && !b || !a && b || a && !b) {
24062      return false;
24063    } // they are the same object and thus, equal
24064
24065
24066    if (a === b) {
24067      return true;
24068    } // sort keys so we can make sure they have
24069    // all the same keys later.
24070
24071
24072    const akeys = Object.keys(a).sort();
24073    const bkeys = Object.keys(b).sort(); // different number of keys, not equal
24074
24075    if (akeys.length !== bkeys.length) {
24076      return false;
24077    }
24078
24079    for (let i = 0; i < akeys.length; i++) {
24080      const key = akeys[i]; // different sorted keys, not equal
24081
24082      if (key !== bkeys[i]) {
24083        return false;
24084      } // different values, not equal
24085
24086
24087      if (a[key] !== b[key]) {
24088        return false;
24089      }
24090    }
24091
24092    return true;
24093  };
24094
24095  /**
24096   * The segment loader has no recourse except to fetch a segment in the
24097   * current playlist and use the internal timestamps in that segment to
24098   * generate a syncPoint. This function returns a good candidate index
24099   * for that process.
24100   *
24101   * @param {Array} segments - the segments array from a playlist.
24102   * @return {number} An index of a segment from the playlist to load
24103   */
24104
24105  const getSyncSegmentCandidate = function (currentTimeline, segments, targetTime) {
24106    segments = segments || [];
24107    const timelineSegments = [];
24108    let time = 0;
24109
24110    for (let i = 0; i < segments.length; i++) {
24111      const segment = segments[i];
24112
24113      if (currentTimeline === segment.timeline) {
24114        timelineSegments.push(i);
24115        time += segment.duration;
24116
24117        if (time > targetTime) {
24118          return i;
24119        }
24120      }
24121    }
24122
24123    if (timelineSegments.length === 0) {
24124      return 0;
24125    } // default to the last timeline segment
24126
24127
24128    return timelineSegments[timelineSegments.length - 1];
24129  }; // In the event of a quota exceeded error, keep at least one second of back buffer. This
24130  // number was arbitrarily chosen and may be updated in the future, but seemed reasonable
24131  // as a start to prevent any potential issues with removing content too close to the
24132  // playhead.
24133
24134  const MIN_BACK_BUFFER = 1; // in ms
24135
24136  const CHECK_BUFFER_DELAY = 500;
24137
24138  const finite = num => typeof num === 'number' && isFinite(num); // With most content hovering around 30fps, if a segment has a duration less than a half
24139  // frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will
24140  // not accurately reflect the rest of the content.
24141
24142
24143  const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60;
24144  const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => {
24145    // Although these checks should most likely cover non 'main' types, for now it narrows
24146    // the scope of our checks.
24147    if (loaderType !== 'main' || !startingMedia || !trackInfo) {
24148      return null;
24149    }
24150
24151    if (!trackInfo.hasAudio && !trackInfo.hasVideo) {
24152      return 'Neither audio nor video found in segment.';
24153    }
24154
24155    if (startingMedia.hasVideo && !trackInfo.hasVideo) {
24156      return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.';
24157    }
24158
24159    if (!startingMedia.hasVideo && trackInfo.hasVideo) {
24160      return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.';
24161    }
24162
24163    return null;
24164  };
24165  /**
24166   * Calculates a time value that is safe to remove from the back buffer without interrupting
24167   * playback.
24168   *
24169   * @param {TimeRange} seekable
24170   *        The current seekable range
24171   * @param {number} currentTime
24172   *        The current time of the player
24173   * @param {number} targetDuration
24174   *        The target duration of the current playlist
24175   * @return {number}
24176   *         Time that is safe to remove from the back buffer without interrupting playback
24177   */
24178
24179  const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => {
24180    // 30 seconds before the playhead provides a safe default for trimming.
24181    //
24182    // Choosing a reasonable default is particularly important for high bitrate content and
24183    // VOD videos/live streams with large windows, as the buffer may end up overfilled and
24184    // throw an APPEND_BUFFER_ERR.
24185    let trimTime = currentTime - Config.BACK_BUFFER_LENGTH;
24186
24187    if (seekable.length) {
24188      // Some live playlists may have a shorter window of content than the full allowed back
24189      // buffer. For these playlists, don't save content that's no longer within the window.
24190      trimTime = Math.max(trimTime, seekable.start(0));
24191    } // Don't remove within target duration of the current time to avoid the possibility of
24192    // removing the GOP currently being played, as removing it can cause playback stalls.
24193
24194
24195    const maxTrimTime = currentTime - targetDuration;
24196    return Math.min(maxTrimTime, trimTime);
24197  };
24198  const segmentInfoString = segmentInfo => {
24199    const {
24200      startOfSegment,
24201      duration,
24202      segment,
24203      part,
24204      playlist: {
24205        mediaSequence: seq,
24206        id,
24207        segments = []
24208      },
24209      mediaIndex: index,
24210      partIndex,
24211      timeline
24212    } = segmentInfo;
24213    const segmentLen = segments.length - 1;
24214    let selection = 'mediaIndex/partIndex increment';
24215
24216    if (segmentInfo.getMediaInfoForTime) {
24217      selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`;
24218    } else if (segmentInfo.isSyncRequest) {
24219      selection = 'getSyncSegmentCandidate (isSyncRequest)';
24220    }
24221
24222    if (segmentInfo.independent) {
24223      selection += ` with independent ${segmentInfo.independent}`;
24224    }
24225
24226    const hasPartIndex = typeof partIndex === 'number';
24227    const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment';
24228    const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({
24229      preloadSegment: segment
24230    }) - 1 : 0;
24231    return `${name} [${seq + index}/${seq + segmentLen}]` + (hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') + ` segment start/end [${segment.start} => ${segment.end}]` + (hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') + ` startOfSegment [${startOfSegment}]` + ` duration [${duration}]` + ` timeline [${timeline}]` + ` selected by [${selection}]` + ` playlist [${id}]`;
24232  };
24233
24234  const timingInfoPropertyForMedia = mediaType => `${mediaType}TimingInfo`;
24235  /**
24236   * Returns the timestamp offset to use for the segment.
24237   *
24238   * @param {number} segmentTimeline
24239   *        The timeline of the segment
24240   * @param {number} currentTimeline
24241   *        The timeline currently being followed by the loader
24242   * @param {number} startOfSegment
24243   *        The estimated segment start
24244   * @param {TimeRange[]} buffered
24245   *        The loader's buffer
24246   * @param {boolean} overrideCheck
24247   *        If true, no checks are made to see if the timestamp offset value should be set,
24248   *        but sets it directly to a value.
24249   *
24250   * @return {number|null}
24251   *         Either a number representing a new timestamp offset, or null if the segment is
24252   *         part of the same timeline
24253   */
24254
24255
24256  const timestampOffsetForSegment = ({
24257    segmentTimeline,
24258    currentTimeline,
24259    startOfSegment,
24260    buffered,
24261    overrideCheck
24262  }) => {
24263    // Check to see if we are crossing a discontinuity to see if we need to set the
24264    // timestamp offset on the transmuxer and source buffer.
24265    //
24266    // Previously, we changed the timestampOffset if the start of this segment was less than
24267    // the currently set timestampOffset, but this isn't desirable as it can produce bad
24268    // behavior, especially around long running live streams.
24269    if (!overrideCheck && segmentTimeline === currentTimeline) {
24270      return null;
24271    } // When changing renditions, it's possible to request a segment on an older timeline. For
24272    // instance, given two renditions with the following:
24273    //
24274    // #EXTINF:10
24275    // segment1
24276    // #EXT-X-DISCONTINUITY
24277    // #EXTINF:10
24278    // segment2
24279    // #EXTINF:10
24280    // segment3
24281    //
24282    // And the current player state:
24283    //
24284    // current time: 8
24285    // buffer: 0 => 20
24286    //
24287    // The next segment on the current rendition would be segment3, filling the buffer from
24288    // 20s onwards. However, if a rendition switch happens after segment2 was requested,
24289    // then the next segment to be requested will be segment1 from the new rendition in
24290    // order to fill time 8 and onwards. Using the buffered end would result in repeated
24291    // content (since it would position segment1 of the new rendition starting at 20s). This
24292    // case can be identified when the new segment's timeline is a prior value. Instead of
24293    // using the buffered end, the startOfSegment can be used, which, hopefully, will be
24294    // more accurate to the actual start time of the segment.
24295
24296
24297    if (segmentTimeline < currentTimeline) {
24298      return startOfSegment;
24299    } // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that
24300    // value uses the end of the last segment if it is available. While this value
24301    // should often be correct, it's better to rely on the buffered end, as the new
24302    // content post discontinuity should line up with the buffered end as if it were
24303    // time 0 for the new content.
24304
24305
24306    return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment;
24307  };
24308  /**
24309   * Returns whether or not the loader should wait for a timeline change from the timeline
24310   * change controller before processing the segment.
24311   *
24312   * Primary timing in VHS goes by video. This is different from most media players, as
24313   * audio is more often used as the primary timing source. For the foreseeable future, VHS
24314   * will continue to use video as the primary timing source, due to the current logic and
24315   * expectations built around it.
24316
24317   * Since the timing follows video, in order to maintain sync, the video loader is
24318   * responsible for setting both audio and video source buffer timestamp offsets.
24319   *
24320   * Setting different values for audio and video source buffers could lead to
24321   * desyncing. The following examples demonstrate some of the situations where this
24322   * distinction is important. Note that all of these cases involve demuxed content. When
24323   * content is muxed, the audio and video are packaged together, therefore syncing
24324   * separate media playlists is not an issue.
24325   *
24326   * CASE 1: Audio prepares to load a new timeline before video:
24327   *
24328   * Timeline:       0                 1
vendor: 2,320 bytes, lines 24329-24369
24329   * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
24330   * Audio Loader:                     ^
24331   * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
24332   * Video Loader              ^
24333   *
24334   * In the above example, the audio loader is preparing to load the 6th segment, the first
24335   * after a discontinuity, while the video loader is still loading the 5th segment, before
24336   * the discontinuity.
24337   *
24338   * If the audio loader goes ahead and loads and appends the 6th segment before the video
24339   * loader crosses the discontinuity, then when appended, the 6th audio segment will use
24340   * the timestamp offset from timeline 0. This will likely lead to desyncing. In addition,
24341   * the audio loader must provide the audioAppendStart value to trim the content in the
24342   * transmuxer, and that value relies on the audio timestamp offset. Since the audio
24343   * timestamp offset is set by the video (main) loader, the audio loader shouldn't load the
24344   * segment until that value is provided.
24345   *
24346   * CASE 2: Video prepares to load a new timeline before audio:
24347   *
24348   * Timeline:       0                 1
24349   * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
24350   * Audio Loader:             ^
24351   * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
24352   * Video Loader                      ^
24353   *
24354   * In the above example, the video loader is preparing to load the 6th segment, the first
24355   * after a discontinuity, while the audio loader is still loading the 5th segment, before
24356   * the discontinuity.
24357   *
24358   * If the video loader goes ahead and loads and appends the 6th segment, then once the
24359   * segment is loaded and processed, both the video and audio timestamp offsets will be
24360   * set, since video is used as the primary timing source. This is to ensure content lines
24361   * up appropriately, as any modifications to the video timing are reflected by audio when
24362   * the video loader sets the audio and video timestamp offsets to the same value. However,
24363   * setting the timestamp offset for audio before audio has had a chance to change
24364   * timelines will likely lead to desyncing, as the audio loader will append segment 5 with
24365   * a timestamp intended to apply to segments from timeline 1 rather than timeline 0.
24366   *
24367   * CASE 3: When seeking, audio prepares to load a new timeline before video
24368   *
24369   * Timeline:       0                 1
vendor: 201,965 bytes, lines 24370-30130
24370   * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
24371   * Audio Loader:           ^
24372   * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9
24373   * Video Loader            ^
24374   *
24375   * In the above example, both audio and video loaders are loading segments from timeline
24376   * 0, but imagine that the seek originated from timeline 1.
24377   *
24378   * When seeking to a new timeline, the timestamp offset will be set based on the expected
24379   * segment start of the loaded video segment. In order to maintain sync, the audio loader
24380   * must wait for the video loader to load its segment and update both the audio and video
24381   * timestamp offsets before it may load and append its own segment. This is the case
24382   * whether the seek results in a mismatched segment request (e.g., the audio loader
24383   * chooses to load segment 3 and the video loader chooses to load segment 4) or the
24384   * loaders choose to load the same segment index from each playlist, as the segments may
24385   * not be aligned perfectly, even for matching segment indexes.
24386   *
24387   * @param {Object} timelinechangeController
24388   * @param {number} currentTimeline
24389   *        The timeline currently being followed by the loader
24390   * @param {number} segmentTimeline
24391   *        The timeline of the segment being loaded
24392   * @param {('main'|'audio')} loaderType
24393   *        The loader type
24394   * @param {boolean} audioDisabled
24395   *        Whether the audio is disabled for the loader. This should only be true when the
24396   *        loader may have muxed audio in its segment, but should not append it, e.g., for
24397   *        the main loader when an alternate audio playlist is active.
24398   *
24399   * @return {boolean}
24400   *         Whether the loader should wait for a timeline change from the timeline change
24401   *         controller before processing the segment
24402   */
24403
24404  const shouldWaitForTimelineChange = ({
24405    timelineChangeController,
24406    currentTimeline,
24407    segmentTimeline,
24408    loaderType,
24409    audioDisabled
24410  }) => {
24411    if (currentTimeline === segmentTimeline) {
24412      return false;
24413    }
24414
24415    if (loaderType === 'audio') {
24416      const lastMainTimelineChange = timelineChangeController.lastTimelineChange({
24417        type: 'main'
24418      }); // Audio loader should wait if:
24419      //
24420      // * main hasn't had a timeline change yet (thus has not loaded its first segment)
24421      // * main hasn't yet changed to the timeline audio is looking to load
24422
24423      return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline;
24424    } // The main loader only needs to wait for timeline changes if there's demuxed audio.
24425    // Otherwise, there's nothing to wait for, since audio would be muxed into the main
24426    // loader's segments (or the content is audio/video only and handled by the main
24427    // loader).
24428
24429
24430    if (loaderType === 'main' && audioDisabled) {
24431      const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({
24432        type: 'audio'
24433      }); // Main loader should wait for the audio loader if audio is not pending a timeline
24434      // change to the current timeline.
24435      //
24436      // Since the main loader is responsible for setting the timestamp offset for both
24437      // audio and video, the main loader must wait for audio to be about to change to its
24438      // timeline before setting the offset, otherwise, if audio is behind in loading,
24439      // segments from the previous timeline would be adjusted by the new timestamp offset.
24440      //
24441      // This requirement means that video will not cross a timeline until the audio is
24442      // about to cross to it, so that way audio and video will always cross the timeline
24443      // together.
24444      //
24445      // In addition to normal timeline changes, these rules also apply to the start of a
24446      // stream (going from a non-existent timeline, -1, to timeline 0). It's important
24447      // that these rules apply to the first timeline change because if they did not, it's
24448      // possible that the main loader will cross two timelines before the audio loader has
24449      // crossed one. Logic may be implemented to handle the startup as a special case, but
24450      // it's easier to simply treat all timeline changes the same.
24451
24452      if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) {
24453        return false;
24454      }
24455
24456      return true;
24457    }
24458
24459    return false;
24460  };
24461  const shouldFixBadTimelineChanges = timelineChangeController => {
24462    if (!timelineChangeController) {
24463      return false;
24464    }
24465
24466    const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({
24467      type: 'audio'
24468    });
24469    const pendingMainTimelineChange = timelineChangeController.pendingTimelineChange({
24470      type: 'main'
24471    });
24472    const hasPendingTimelineChanges = pendingAudioTimelineChange && pendingMainTimelineChange;
24473    const differentPendingChanges = hasPendingTimelineChanges && pendingAudioTimelineChange.to !== pendingMainTimelineChange.to;
24474    const isNotInitialPendingTimelineChange = hasPendingTimelineChanges && pendingAudioTimelineChange.from !== -1 && pendingMainTimelineChange.from !== -1;
24475
24476    if (isNotInitialPendingTimelineChange && differentPendingChanges) {
24477      return true;
24478    }
24479
24480    return false;
24481  };
24482  /**
24483   * Check if the pending audio timeline change is behind the
24484   * pending main timeline change.
24485   *
24486   * @param {SegmentLoader} segmentLoader
24487   * @return {boolean}
24488   */
24489
24490  const isAudioTimelineBehind = segmentLoader => {
24491    const pendingAudioTimelineChange = segmentLoader.timelineChangeController_.pendingTimelineChange({
24492      type: 'audio'
24493    });
24494    const pendingMainTimelineChange = segmentLoader.timelineChangeController_.pendingTimelineChange({
24495      type: 'main'
24496    });
24497    const hasPendingTimelineChanges = pendingAudioTimelineChange && pendingMainTimelineChange;
24498    return hasPendingTimelineChanges && pendingAudioTimelineChange.to < pendingMainTimelineChange.to;
24499  };
24500  /**
24501   * A method to check if the player is waiting for a timeline change, and fixes
24502   * certain scenarios where the timelines need to be updated.
24503   *
24504   * @param {SegmentLoader} segmentLoader
24505   */
24506
24507
24508  const checkAndFixTimelines = segmentLoader => {
24509    const segmentInfo = segmentLoader.pendingSegment_;
24510
24511    if (!segmentInfo) {
24512      return;
24513    }
24514
24515    const waitingForTimelineChange = shouldWaitForTimelineChange({
24516      timelineChangeController: segmentLoader.timelineChangeController_,
24517      currentTimeline: segmentLoader.currentTimeline_,
24518      segmentTimeline: segmentInfo.timeline,
24519      loaderType: segmentLoader.loaderType_,
24520      audioDisabled: segmentLoader.audioDisabled_
24521    });
24522
24523    if (waitingForTimelineChange && shouldFixBadTimelineChanges(segmentLoader.timelineChangeController_)) {
24524      if (isAudioTimelineBehind(segmentLoader)) {
24525        segmentLoader.timelineChangeController_.trigger('audioTimelineBehind');
24526        return;
24527      }
24528
24529      segmentLoader.timelineChangeController_.trigger('fixBadTimelineChange');
24530    }
24531  };
24532
24533  const mediaDuration = timingInfos => {
24534    let maxDuration = 0;
24535    ['video', 'audio'].forEach(function (type) {
24536      const typeTimingInfo = timingInfos[`${type}TimingInfo`];
24537
24538      if (!typeTimingInfo) {
24539        return;
24540      }
24541
24542      const {
24543        start,
24544        end
24545      } = typeTimingInfo;
24546      let duration;
24547
24548      if (typeof start === 'bigint' || typeof end === 'bigint') {
24549        duration = window.BigInt(end) - window.BigInt(start);
24550      } else if (typeof start === 'number' && typeof end === 'number') {
24551        duration = end - start;
24552      }
24553
24554      if (typeof duration !== 'undefined' && duration > maxDuration) {
24555        maxDuration = duration;
24556      }
24557    }); // convert back to a number if it is lower than MAX_SAFE_INTEGER
24558    // as we only need BigInt when we are above that.
24559
24560    if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) {
24561      maxDuration = Number(maxDuration);
24562    }
24563
24564    return maxDuration;
24565  };
24566  const segmentTooLong = ({
24567    segmentDuration,
24568    maxDuration
24569  }) => {
24570    // 0 duration segments are most likely due to metadata only segments or a lack of
24571    // information.
24572    if (!segmentDuration) {
24573      return false;
24574    } // For HLS:
24575    //
24576    // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1
24577    // The EXTINF duration of each Media Segment in the Playlist
24578    // file, when rounded to the nearest integer, MUST be less than or equal
24579    // to the target duration; longer segments can trigger playback stalls
24580    // or other errors.
24581    //
24582    // For DASH, the mpd-parser uses the largest reported segment duration as the target
24583    // duration. Although that reported duration is occasionally approximate (i.e., not
24584    // exact), a strict check may report that a segment is too long more often in DASH.
24585
24586
24587    return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR;
24588  };
24589  const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => {
24590    // Right now we aren't following DASH's timing model exactly, so only perform
24591    // this check for HLS content.
24592    if (sourceType !== 'hls') {
24593      return null;
24594    }
24595
24596    const segmentDuration = mediaDuration({
24597      audioTimingInfo: segmentInfo.audioTimingInfo,
24598      videoTimingInfo: segmentInfo.videoTimingInfo
24599    }); // Don't report if we lack information.
24600    //
24601    // If the segment has a duration of 0 it is either a lack of information or a
24602    // metadata only segment and shouldn't be reported here.
24603
24604    if (!segmentDuration) {
24605      return null;
24606    }
24607
24608    const targetDuration = segmentInfo.playlist.targetDuration;
24609    const isSegmentWayTooLong = segmentTooLong({
24610      segmentDuration,
24611      maxDuration: targetDuration * 2
24612    });
24613    const isSegmentSlightlyTooLong = segmentTooLong({
24614      segmentDuration,
24615      maxDuration: targetDuration
24616    });
24617    const segmentTooLongMessage = `Segment with index ${segmentInfo.mediaIndex} ` + `from playlist ${segmentInfo.playlist.id} ` + `has a duration of ${segmentDuration} ` + `when the reported duration is ${segmentInfo.duration} ` + `and the target duration is ${targetDuration}. ` + 'For HLS content, a duration in excess of the target duration may result in ' + 'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' + 'more details: ' + 'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1';
24618
24619    if (isSegmentWayTooLong || isSegmentSlightlyTooLong) {
24620      return {
24621        severity: isSegmentWayTooLong ? 'warn' : 'info',
24622        message: segmentTooLongMessage
24623      };
24624    }
24625
24626    return null;
24627  };
24628  /**
24629   *
24630   * @param {Object} options type of segment loader and segment either segmentInfo or simple segment
24631   * @return a segmentInfo payload for events or errors.
24632   */
24633
24634  const segmentInfoPayload = ({
24635    type,
24636    segment
24637  }) => {
24638    if (!segment) {
24639      return;
24640    }
24641
24642    const isEncrypted = Boolean(segment.key || segment.map && segment.map.ke);
24643    const isMediaInitialization = Boolean(segment.map && !segment.map.bytes);
24644    const start = segment.startOfSegment === undefined ? segment.start : segment.startOfSegment;
24645    return {
24646      type: type || segment.type,
24647      uri: segment.resolvedUri || segment.uri,
24648      start,
24649      duration: segment.duration,
24650      isEncrypted,
24651      isMediaInitialization
24652    };
24653  };
24654  /**
24655   * An object that manages segment loading and appending.
24656   *
24657   * @class SegmentLoader
24658   * @param {Object} options required and optional options
24659   * @extends videojs.EventTarget
24660   */
24661
24662  class SegmentLoader extends videojs__default["default"].EventTarget {
24663    constructor(settings, options = {}) {
24664      super(); // check pre-conditions
24665
24666      if (!settings) {
24667        throw new TypeError('Initialization settings are required');
24668      }
24669
24670      if (typeof settings.currentTime !== 'function') {
24671        throw new TypeError('No currentTime getter specified');
24672      }
24673
24674      if (!settings.mediaSource) {
24675        throw new TypeError('No MediaSource specified');
24676      } // public properties
24677
24678
24679      this.bandwidth = settings.bandwidth;
24680      this.throughput = {
24681        rate: 0,
24682        count: 0
24683      };
24684      this.roundTrip = NaN;
24685      this.resetStats_();
24686      this.mediaIndex = null;
24687      this.partIndex = null; // private settings
24688
24689      this.hasPlayed_ = settings.hasPlayed;
24690      this.currentTime_ = settings.currentTime;
24691      this.seekable_ = settings.seekable;
24692      this.seeking_ = settings.seeking;
24693      this.duration_ = settings.duration;
24694      this.mediaSource_ = settings.mediaSource;
24695      this.vhs_ = settings.vhs;
24696      this.loaderType_ = settings.loaderType;
24697      this.currentMediaInfo_ = void 0;
24698      this.startingMediaInfo_ = void 0;
24699      this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
24700      this.goalBufferLength_ = settings.goalBufferLength;
24701      this.sourceType_ = settings.sourceType;
24702      this.sourceUpdater_ = settings.sourceUpdater;
24703      this.inbandTextTracks_ = settings.inbandTextTracks;
24704      this.state_ = 'INIT';
24705      this.timelineChangeController_ = settings.timelineChangeController;
24706      this.shouldSaveSegmentTimingInfo_ = true;
24707      this.parse708captions_ = settings.parse708captions;
24708      this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset;
24709      this.captionServices_ = settings.captionServices;
24710      this.exactManifestTimings = settings.exactManifestTimings;
24711      this.addMetadataToTextTrack = settings.addMetadataToTextTrack; // private instance variables
24712
24713      this.checkBufferTimeout_ = null;
24714      this.error_ = void 0;
24715      this.currentTimeline_ = -1;
24716      this.shouldForceTimestampOffsetAfterResync_ = false;
24717      this.pendingSegment_ = null;
24718      this.xhrOptions_ = null;
24719      this.pendingSegments_ = [];
24720      this.audioDisabled_ = false;
24721      this.isPendingTimestampOffset_ = false; // TODO possibly move gopBuffer and timeMapping info to a separate controller
24722
24723      this.gopBuffer_ = [];
24724      this.timeMapping_ = 0;
24725      this.safeAppend_ = false;
24726      this.appendInitSegment_ = {
24727        audio: true,
24728        video: true
24729      };
24730      this.playlistOfLastInitSegment_ = {
24731        audio: null,
24732        video: null
24733      };
24734      this.callQueue_ = []; // If the segment loader prepares to load a segment, but does not have enough
24735      // information yet to start the loading process (e.g., if the audio loader wants to
24736      // load a segment from the next timeline but the main loader hasn't yet crossed that
24737      // timeline), then the load call will be added to the queue until it is ready to be
24738      // processed.
24739
24740      this.loadQueue_ = [];
24741      this.metadataQueue_ = {
24742        id3: [],
24743        caption: []
24744      };
24745      this.waitingOnRemove_ = false;
24746      this.quotaExceededErrorRetryTimeout_ = null; // Fragmented mp4 playback
24747
24748      this.activeInitSegmentId_ = null;
24749      this.initSegments_ = {}; // HLSe playback
24750
24751      this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys;
24752      this.keyCache_ = {};
24753      this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings
24754      // between a time in the display time and a segment index within
24755      // a playlist
24756
24757      this.syncController_ = settings.syncController;
24758      this.syncPoint_ = {
24759        segmentIndex: 0,
24760        time: 0
24761      };
24762      this.transmuxer_ = this.createTransmuxer_();
24763
24764      this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate');
24765
24766      this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_);
24767      this.mediaSource_.addEventListener('sourceopen', () => {
24768        if (!this.isEndOfStream_()) {
24769          this.ended_ = false;
24770        }
24771      }); // ...for determining the fetch location
24772
24773      this.fetchAtBuffer_ = false;
24774      this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`);
24775      Object.defineProperty(this, 'state', {
24776        get() {
24777          return this.state_;
24778        },
24779
24780        set(newState) {
24781          if (newState !== this.state_) {
24782            this.logger_(`${this.state_} -> ${newState}`);
24783            this.state_ = newState;
24784            this.trigger('statechange');
24785          }
24786        }
24787
24788      });
24789      this.sourceUpdater_.on('ready', () => {
24790        if (this.hasEnoughInfoToAppend_()) {
24791          this.processCallQueue_();
24792        } else {
24793          checkAndFixTimelines(this);
24794        }
24795      });
24796      this.sourceUpdater_.on('codecschange', metadata => {
24797        this.trigger(_extends({
24798          type: 'codecschange'
24799        }, metadata));
24800      }); // Only the main loader needs to listen for pending timeline changes, as the main
24801      // loader should wait for audio to be ready to change its timeline so that both main
24802      // and audio timelines change together. For more details, see the
24803      // shouldWaitForTimelineChange function.
24804
24805      if (this.loaderType_ === 'main') {
24806        this.timelineChangeController_.on('pendingtimelinechange', () => {
24807          if (this.hasEnoughInfoToAppend_()) {
24808            this.processCallQueue_();
24809          } else {
24810            checkAndFixTimelines(this);
24811          }
24812        });
24813      } // The main loader only listens on pending timeline changes, but the audio loader,
24814      // since its loads follow main, needs to listen on timeline changes. For more details,
24815      // see the shouldWaitForTimelineChange function.
24816
24817
24818      if (this.loaderType_ === 'audio') {
24819        this.timelineChangeController_.on('timelinechange', metadata => {
24820          this.trigger(_extends({
24821            type: 'timelinechange'
24822          }, metadata));
24823
24824          if (this.hasEnoughInfoToLoad_()) {
24825            this.processLoadQueue_();
24826          } else {
24827            checkAndFixTimelines(this);
24828          }
24829
24830          if (this.hasEnoughInfoToAppend_()) {
24831            this.processCallQueue_();
24832          } else {
24833            checkAndFixTimelines(this);
24834          }
24835        });
24836      }
24837    }
24838    /**
24839     * TODO: Current sync controller consists of many hls-specific strategies
24840     * media sequence sync is also hls-specific, and we would like to be protocol-agnostic on this level
24841     * this should be a part of the sync-controller and sync controller should expect different strategy list based on the protocol.
24842     *
24843     * @return {MediaSequenceSync|null}
24844     * @private
24845     */
24846
24847
24848    get mediaSequenceSync_() {
24849      return this.syncController_.getMediaSequenceSync(this.loaderType_);
24850    }
24851
24852    createTransmuxer_() {
24853      return segmentTransmuxer.createTransmuxer({
24854        remux: false,
24855        alignGopsAtEnd: this.safeAppend_,
24856        keepOriginalTimestamps: true,
24857        parse708captions: this.parse708captions_,
24858        captionServices: this.captionServices_
24859      });
24860    }
24861    /**
24862     * reset all of our media stats
24863     *
24864     * @private
24865     */
24866
24867
24868    resetStats_() {
24869      this.mediaBytesTransferred = 0;
24870      this.mediaRequests = 0;
24871      this.mediaRequestsAborted = 0;
24872      this.mediaRequestsTimedout = 0;
24873      this.mediaRequestsErrored = 0;
24874      this.mediaTransferDuration = 0;
24875      this.mediaSecondsLoaded = 0;
24876      this.mediaAppends = 0;
24877    }
24878    /**
24879     * dispose of the SegmentLoader and reset to the default state
24880     */
24881
24882
24883    dispose() {
24884      this.trigger('dispose');
24885      this.state = 'DISPOSED';
24886      this.pause();
24887      this.abort_();
24888
24889      if (this.transmuxer_) {
24890        this.transmuxer_.terminate();
24891      }
24892
24893      this.resetStats_();
24894
24895      if (this.checkBufferTimeout_) {
24896        window.clearTimeout(this.checkBufferTimeout_);
24897      }
24898
24899      if (this.syncController_ && this.triggerSyncInfoUpdate_) {
24900        this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_);
24901      }
24902
24903      this.off();
24904    }
24905
24906    setAudio(enable) {
24907      this.audioDisabled_ = !enable;
24908
24909      if (enable) {
24910        this.appendInitSegment_.audio = true;
24911      } else {
24912        // remove current track audio if it gets disabled
24913        this.sourceUpdater_.removeAudio(0, this.duration_());
24914      }
24915    }
24916    /**
24917     * abort anything that is currently doing on with the SegmentLoader
24918     * and reset to a default state
24919     */
24920
24921
24922    abort() {
24923      if (this.state !== 'WAITING') {
24924        if (this.pendingSegment_) {
24925          this.pendingSegment_ = null;
24926        }
24927
24928        this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_);
24929        return;
24930      }
24931
24932      this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY
24933      // since we are no longer "waiting" on any requests. XHR callback is not always run
24934      // when the request is aborted. This will prevent the loader from being stuck in the
24935      // WAITING state indefinitely.
24936
24937      this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the
24938      // next segment
24939
24940      if (!this.paused()) {
24941        this.monitorBuffer_();
24942      }
24943    }
24944    /**
24945     * abort all pending xhr requests and null any pending segements
24946     *
24947     * @private
24948     */
24949
24950
24951    abort_() {
24952      if (this.pendingSegment_ && this.pendingSegment_.abortRequests) {
24953        this.pendingSegment_.abortRequests();
24954      } // clear out the segment being processed
24955
24956
24957      this.pendingSegment_ = null;
24958      this.callQueue_ = [];
24959      this.loadQueue_ = [];
24960      this.metadataQueue_.id3 = [];
24961      this.metadataQueue_.caption = [];
24962      this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_);
24963      this.waitingOnRemove_ = false;
24964      window.clearTimeout(this.quotaExceededErrorRetryTimeout_);
24965      this.quotaExceededErrorRetryTimeout_ = null;
24966    }
24967
24968    checkForAbort_(requestId) {
24969      // If the state is APPENDING, then aborts will not modify the state, meaning the first
24970      // callback that happens should reset the state to READY so that loading can continue.
24971      if (this.state === 'APPENDING' && !this.pendingSegment_) {
24972        this.state = 'READY';
24973        return true;
24974      }
24975
24976      if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) {
24977        return true;
24978      }
24979
24980      return false;
24981    }
24982    /**
24983     * set an error on the segment loader and null out any pending segements
24984     *
24985     * @param {Error} error the error to set on the SegmentLoader
24986     * @return {Error} the error that was set or that is currently set
24987     */
24988
24989
24990    error(error) {
24991      if (typeof error !== 'undefined') {
24992        this.logger_('error occurred:', error);
24993        this.error_ = error;
24994      }
24995
24996      this.pendingSegment_ = null;
24997      return this.error_;
24998    }
24999
25000    endOfStream() {
25001      this.ended_ = true;
25002
25003      if (this.transmuxer_) {
25004        // need to clear out any cached data to prepare for the new segment
25005        segmentTransmuxer.reset(this.transmuxer_);
25006      }
25007
25008      this.gopBuffer_.length = 0;
25009      this.pause();
25010      this.trigger('ended');
25011    }
25012    /**
25013     * Indicates which time ranges are buffered
25014     *
25015     * @return {TimeRange}
25016     *         TimeRange object representing the current buffered ranges
25017     */
25018
25019
25020    buffered_() {
25021      const trackInfo = this.getMediaInfo_();
25022
25023      if (!this.sourceUpdater_ || !trackInfo) {
25024        return createTimeRanges();
25025      }
25026
25027      if (this.loaderType_ === 'main') {
25028        const {
25029          hasAudio,
25030          hasVideo,
25031          isMuxed
25032        } = trackInfo;
25033
25034        if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) {
25035          return this.sourceUpdater_.buffered();
25036        }
25037
25038        if (hasVideo) {
25039          return this.sourceUpdater_.videoBuffered();
25040        }
25041      } // One case that can be ignored for now is audio only with alt audio,
25042      // as we don't yet have proper support for that.
25043
25044
25045      return this.sourceUpdater_.audioBuffered();
25046    }
25047    /**
25048     * Gets and sets init segment for the provided map
25049     *
25050     * @param {Object} map
25051     *        The map object representing the init segment to get or set
25052     * @param {boolean=} set
25053     *        If true, the init segment for the provided map should be saved
25054     * @return {Object}
25055     *         map object for desired init segment
25056     */
25057
25058
25059    initSegmentForMap(map, set = false) {
25060      if (!map) {
25061        return null;
25062      }
25063
25064      const id = initSegmentId(map);
25065      let storedMap = this.initSegments_[id];
25066
25067      if (set && !storedMap && map.bytes) {
25068        this.initSegments_[id] = storedMap = {
25069          resolvedUri: map.resolvedUri,
25070          byterange: map.byterange,
25071          bytes: map.bytes,
25072          tracks: map.tracks,
25073          timescales: map.timescales
25074        };
25075      }
25076
25077      return storedMap || map;
25078    }
25079    /**
25080     * Gets and sets key for the provided key
25081     *
25082     * @param {Object} key
25083     *        The key object representing the key to get or set
25084     * @param {boolean=} set
25085     *        If true, the key for the provided key should be saved
25086     * @return {Object}
25087     *         Key object for desired key
25088     */
25089
25090
25091    segmentKey(key, set = false) {
25092      if (!key) {
25093        return null;
25094      }
25095
25096      const id = segmentKeyId(key);
25097      let storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per
25098      // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3
25099
25100      if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) {
25101        this.keyCache_[id] = storedKey = {
25102          resolvedUri: key.resolvedUri,
25103          bytes: key.bytes
25104        };
25105      }
25106
25107      const result = {
25108        resolvedUri: (storedKey || key).resolvedUri
25109      };
25110
25111      if (storedKey) {
25112        result.bytes = storedKey.bytes;
25113      }
25114
25115      return result;
25116    }
25117    /**
25118     * Returns true if all configuration required for loading is present, otherwise false.
25119     *
25120     * @return {boolean} True if the all configuration is ready for loading
25121     * @private
25122     */
25123
25124
25125    couldBeginLoading_() {
25126      return this.playlist_ && !this.paused();
25127    }
25128    /**
25129     * load a playlist and start to fill the buffer
25130     */
25131
25132
25133    load() {
25134      // un-pause
25135      this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be
25136      // specified
25137
25138      if (!this.playlist_) {
25139        return;
25140      } // if all the configuration is ready, initialize and begin loading
25141
25142
25143      if (this.state === 'INIT' && this.couldBeginLoading_()) {
25144        return this.init_();
25145      } // if we're in the middle of processing a segment already, don't
25146      // kick off an additional segment request
25147
25148
25149      if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') {
25150        return;
25151      }
25152
25153      this.state = 'READY';
25154    }
25155    /**
25156     * Once all the starting parameters have been specified, begin
25157     * operation. This method should only be invoked from the INIT
25158     * state.
25159     *
25160     * @private
25161     */
25162
25163
25164    init_() {
25165      this.state = 'READY'; // if this is the audio segment loader, and it hasn't been inited before, then any old
25166      // audio data from the muxed content should be removed
25167
25168      this.resetEverything();
25169      return this.monitorBuffer_();
25170    }
25171    /**
25172     * set a playlist on the segment loader
25173     *
25174     * @param {PlaylistLoader} media the playlist to set on the segment loader
25175     */
25176
25177
25178    playlist(newPlaylist, options = {}) {
25179      if (!newPlaylist) {
25180        return;
25181      }
25182
25183      if (this.playlist_ && this.playlist_.endList && newPlaylist.endList && this.playlist_.uri === newPlaylist.uri) {
25184        // skip update if both prev and new are vod and have the same URI
25185        return;
25186      }
25187
25188      const oldPlaylist = this.playlist_;
25189      const segmentInfo = this.pendingSegment_;
25190      this.playlist_ = newPlaylist;
25191      this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist
25192      // is always our zero-time so force a sync update each time the playlist
25193      // is refreshed from the server
25194      //
25195      // Use the INIT state to determine if playback has started, as the playlist sync info
25196      // should be fixed once requests begin (as sync points are generated based on sync
25197      // info), but not before then.
25198
25199      if (this.state === 'INIT') {
25200        newPlaylist.syncInfo = {
25201          mediaSequence: newPlaylist.mediaSequence,
25202          time: 0
25203        }; // Setting the date time mapping means mapping the program date time (if available)
25204        // to time 0 on the player's timeline. The playlist's syncInfo serves a similar
25205        // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can
25206        // be updated as the playlist is refreshed before the loader starts loading, the
25207        // program date time mapping needs to be updated as well.
25208        //
25209        // This mapping is only done for the main loader because a program date time should
25210        // map equivalently between playlists.
25211
25212        if (this.loaderType_ === 'main') {
25213          this.syncController_.setDateTimeMappingForStart(newPlaylist);
25214        }
25215      }
25216
25217      let oldId = null;
25218
25219      if (oldPlaylist) {
25220        if (oldPlaylist.id) {
25221          oldId = oldPlaylist.id;
25222        } else if (oldPlaylist.uri) {
25223          oldId = oldPlaylist.uri;
25224        }
25225      }
25226
25227      this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`);
25228
25229      if (this.mediaSequenceSync_) {
25230        this.mediaSequenceSync_.update(newPlaylist, this.currentTime_());
25231        this.logger_(`Playlist update:
25232currentTime: ${this.currentTime_()}
25233bufferedEnd: ${lastBufferedEnd(this.buffered_())}
25234`, this.mediaSequenceSync_.diagnostics);
25235      } // in VOD, this is always a rendition switch (or we updated our syncInfo above)
25236      // in LIVE, we always want to update with new playlists (including refreshes)
25237
25238
25239      this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start
25240      // buffering now
25241
25242      if (this.state === 'INIT' && this.couldBeginLoading_()) {
25243        return this.init_();
25244      }
25245
25246      if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
25247        if (this.mediaIndex !== null) {
25248          // we must reset/resync the segment loader when we switch renditions and
25249          // the segment loader is already synced to the previous rendition
25250          // We only want to reset the loader here for LLHLS playback, as resetLoader sets fetchAtBuffer_
25251          // to false, resulting in fetching segments at currentTime and causing repeated
25252          // same-segment requests on playlist change. This erroneously drives up the playback watcher
25253          // stalled segment count, as re-requesting segments at the currentTime or browser cached segments
25254          // will not change the buffer.
25255          // Reference for LLHLS fixes: https://github.com/videojs/http-streaming/pull/1201
25256          const isLLHLS = !newPlaylist.endList && typeof newPlaylist.partTargetDuration === 'number';
25257
25258          if (isLLHLS) {
25259            this.resetLoader();
25260          } else {
25261            this.resyncLoader();
25262          }
25263        }
25264
25265        this.currentMediaInfo_ = void 0;
25266        this.trigger('playlistupdate'); // the rest of this function depends on `oldPlaylist` being defined
25267
25268        return;
25269      } // we reloaded the same playlist so we are in a live scenario
25270      // and we will likely need to adjust the mediaIndex
25271
25272
25273      const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;
25274      this.logger_(`live window shift [${mediaSequenceDiff}]`); // update the mediaIndex on the SegmentLoader
25275      // this is important because we can abort a request and this value must be
25276      // equal to the last appended mediaIndex
25277
25278      if (this.mediaIndex !== null) {
25279        this.mediaIndex -= mediaSequenceDiff; // this can happen if we are going to load the first segment, but get a playlist
25280        // update during that. mediaIndex would go from 0 to -1 if mediaSequence in the
25281        // new playlist was incremented by 1.
25282
25283        if (this.mediaIndex < 0) {
25284          this.mediaIndex = null;
25285          this.partIndex = null;
25286        } else {
25287          const segment = this.playlist_.segments[this.mediaIndex]; // partIndex should remain the same for the same segment
25288          // unless parts fell off of the playlist for this segment.
25289          // In that case we need to reset partIndex and resync
25290
25291          if (this.partIndex && (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex])) {
25292            const mediaIndex = this.mediaIndex;
25293            this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`);
25294            this.resetLoader(); // We want to throw away the partIndex and the data associated with it,
25295            // as the part was dropped from our current playlists segment.
25296            // The mediaIndex will still be valid so keep that around.
25297
25298            this.mediaIndex = mediaIndex;
25299          }
25300        }
25301      } // update the mediaIndex on the SegmentInfo object
25302      // this is important because we will update this.mediaIndex with this value
25303      // in `handleAppendsDone_` after the segment has been successfully appended
25304
25305
25306      if (segmentInfo) {
25307        segmentInfo.mediaIndex -= mediaSequenceDiff;
25308
25309        if (segmentInfo.mediaIndex < 0) {
25310          segmentInfo.mediaIndex = null;
25311          segmentInfo.partIndex = null;
25312        } else {
25313          // we need to update the referenced segment so that timing information is
25314          // saved for the new playlist's segment, however, if the segment fell off the
25315          // playlist, we can leave the old reference and just lose the timing info
25316          if (segmentInfo.mediaIndex >= 0) {
25317            segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
25318          }
25319
25320          if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) {
25321            segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex];
25322          }
25323        }
25324      }
25325
25326      this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
25327    }
25328    /**
25329     * Prevent the loader from fetching additional segments. If there
25330     * is a segment request outstanding, it will finish processing
25331     * before the loader halts. A segment loader can be unpaused by
25332     * calling load().
25333     */
25334
25335
25336    pause() {
25337      if (this.checkBufferTimeout_) {
25338        window.clearTimeout(this.checkBufferTimeout_);
25339        this.checkBufferTimeout_ = null;
25340      }
25341    }
25342    /**
25343     * Returns whether the segment loader is fetching additional
25344     * segments when given the opportunity. This property can be
25345     * modified through calls to pause() and load().
25346     */
25347
25348
25349    paused() {
25350      return this.checkBufferTimeout_ === null;
25351    }
25352    /**
25353     * Delete all the buffered data and reset the SegmentLoader
25354     *
25355     * @param {Function} [done] an optional callback to be executed when the remove
25356     * operation is complete
25357     */
25358
25359
25360    resetEverything(done) {
25361      this.ended_ = false;
25362      this.activeInitSegmentId_ = null;
25363      this.appendInitSegment_ = {
25364        audio: true,
25365        video: true
25366      };
25367      this.resetLoader(); // remove from 0, the earliest point, to Infinity, to signify removal of everything.
25368      // VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader,
25369      // we then clamp the value to duration if necessary.
25370
25371      this.remove(0, Infinity, done); // clears fmp4 captions
25372
25373      if (this.transmuxer_) {
25374        this.transmuxer_.postMessage({
25375          action: 'clearAllMp4Captions'
25376        }); // reset the cache in the transmuxer
25377
25378        this.transmuxer_.postMessage({
25379          action: 'reset'
25380        });
25381      }
25382    }
25383    /**
25384     * Force the SegmentLoader to resync and start loading around the currentTime instead
25385     * of starting at the end of the buffer
25386     *
25387     * Useful for fast quality changes
25388     */
25389
25390
25391    resetLoader() {
25392      this.fetchAtBuffer_ = false;
25393
25394      if (this.mediaSequenceSync_) {
25395        this.mediaSequenceSync_.resetAppendedStatus();
25396      }
25397
25398      this.resyncLoader();
25399    }
25400    /**
25401     * Force the SegmentLoader to restart synchronization and make a conservative guess
25402     * before returning to the simple walk-forward method
25403     */
25404
25405
25406    resyncLoader() {
25407      if (this.transmuxer_) {
25408        // need to clear out any cached data to prepare for the new segment
25409        segmentTransmuxer.reset(this.transmuxer_);
25410      }
25411
25412      this.mediaIndex = null;
25413      this.partIndex = null;
25414      this.syncPoint_ = null;
25415      this.isPendingTimestampOffset_ = false; // this is mainly to sync timing-info when switching between renditions with and without timestamp-rollover,
25416      // so we don't want it for DASH or fragmented mp4 segments.
25417
25418      const isFmp4 = this.currentMediaInfo_ && this.currentMediaInfo_.isFmp4;
25419      const isHlsTs = this.sourceType_ === 'hls' && !isFmp4;
25420
25421      if (isHlsTs) {
25422        this.shouldForceTimestampOffsetAfterResync_ = true;
25423      }
25424
25425      this.callQueue_ = [];
25426      this.loadQueue_ = [];
25427      this.metadataQueue_.id3 = [];
25428      this.metadataQueue_.caption = [];
25429      this.abort();
25430
25431      if (this.transmuxer_) {
25432        this.transmuxer_.postMessage({
25433          action: 'clearParsedMp4Captions'
25434        });
25435      }
25436    }
25437    /**
25438     * Remove any data in the source buffer between start and end times
25439     *
25440     * @param {number} start - the start time of the region to remove from the buffer
25441     * @param {number} end - the end time of the region to remove from the buffer
25442     * @param {Function} [done] - an optional callback to be executed when the remove
25443     * @param {boolean} force - force all remove operations to happen
25444     * operation is complete
25445     */
25446
25447
25448    remove(start, end, done = () => {}, force = false) {
25449      // clamp end to duration if we need to remove everything.
25450      // This is due to a browser bug that causes issues if we remove to Infinity.
25451      // videojs/videojs-contrib-hls#1225
25452      if (end === Infinity) {
25453        end = this.duration_();
25454      } // skip removes that would throw an error
25455      // commonly happens during a rendition switch at the start of a video
25456      // from start 0 to end 0
25457
25458
25459      if (end <= start) {
25460        this.logger_('skipping remove because end ${end} is <= start ${start}');
25461        return;
25462      }
25463
25464      if (!this.sourceUpdater_ || !this.getMediaInfo_()) {
25465        this.logger_('skipping remove because no source updater or starting media info'); // nothing to remove if we haven't processed any media
25466
25467        return;
25468      } // set it to one to complete this function's removes
25469
25470
25471      let removesRemaining = 1;
25472
25473      const removeFinished = () => {
25474        removesRemaining--;
25475
25476        if (removesRemaining === 0) {
25477          done();
25478        }
25479      };
25480
25481      if (force || !this.audioDisabled_) {
25482        removesRemaining++;
25483        this.sourceUpdater_.removeAudio(start, end, removeFinished);
25484      } // While it would be better to only remove video if the main loader has video, this
25485      // should be safe with audio only as removeVideo will call back even if there's no
25486      // video buffer.
25487      //
25488      // In theory we can check to see if there's video before calling the remove, but in
25489      // the event that we're switching between renditions and from video to audio only
25490      // (when we add support for that), we may need to clear the video contents despite
25491      // what the new media will contain.
25492
25493
25494      if (force || this.loaderType_ === 'main') {
25495        this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_);
25496        removesRemaining++;
25497        this.sourceUpdater_.removeVideo(start, end, removeFinished);
25498      } // remove any captions and ID3 tags
25499
25500
25501      for (const track in this.inbandTextTracks_) {
25502        removeCuesFromTrack(start, end, this.inbandTextTracks_[track]);
25503      }
25504
25505      removeCuesFromTrack(start, end, this.segmentMetadataTrack_); // finished this function's removes
25506
25507      removeFinished();
25508    }
25509    /**
25510     * (re-)schedule monitorBufferTick_ to run as soon as possible
25511     *
25512     * @private
25513     */
25514
25515
25516    monitorBuffer_() {
25517      if (this.checkBufferTimeout_) {
25518        window.clearTimeout(this.checkBufferTimeout_);
25519      }
25520
25521      this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1);
25522    }
25523    /**
25524     * As long as the SegmentLoader is in the READY state, periodically
25525     * invoke fillBuffer_().
25526     *
25527     * @private
25528     */
25529
25530
25531    monitorBufferTick_() {
25532      if (this.state === 'READY') {
25533        this.fillBuffer_();
25534      }
25535
25536      if (this.checkBufferTimeout_) {
25537        window.clearTimeout(this.checkBufferTimeout_);
25538      }
25539
25540      this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY);
25541    }
25542    /**
25543     * fill the buffer with segements unless the sourceBuffers are
25544     * currently updating
25545     *
25546     * Note: this function should only ever be called by monitorBuffer_
25547     * and never directly
25548     *
25549     * @private
25550     */
25551
25552
25553    fillBuffer_() {
25554      // TODO since the source buffer maintains a queue, and we shouldn't call this function
25555      // except when we're ready for the next segment, this check can most likely be removed
25556      if (this.sourceUpdater_.updating()) {
25557        return;
25558      } // see if we need to begin loading immediately
25559
25560
25561      const segmentInfo = this.chooseNextRequest_();
25562
25563      if (!segmentInfo) {
25564        return;
25565      }
25566
25567      const metadata = {
25568        segmentInfo: segmentInfoPayload({
25569          type: this.loaderType_,
25570          segment: segmentInfo
25571        })
25572      };
25573      this.trigger({
25574        type: 'segmentselected',
25575        metadata
25576      });
25577
25578      if (typeof segmentInfo.timestampOffset === 'number') {
25579        this.isPendingTimestampOffset_ = false;
25580        this.timelineChangeController_.pendingTimelineChange({
25581          type: this.loaderType_,
25582          from: this.currentTimeline_,
25583          to: segmentInfo.timeline
25584        });
25585      }
25586
25587      this.loadSegment_(segmentInfo);
25588    }
25589    /**
25590     * Determines if we should call endOfStream on the media source based
25591     * on the state of the buffer or if appened segment was the final
25592     * segment in the playlist.
25593     *
25594     * @param {number} [mediaIndex] the media index of segment we last appended
25595     * @param {Object} [playlist] a media playlist object
25596     * @return {boolean} do we need to call endOfStream on the MediaSource
25597     */
25598
25599
25600    isEndOfStream_(mediaIndex = this.mediaIndex, playlist = this.playlist_, partIndex = this.partIndex) {
25601      if (!playlist || !this.mediaSource_) {
25602        return false;
25603      }
25604
25605      const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex]; // mediaIndex is zero based but length is 1 based
25606
25607      const appendedLastSegment = mediaIndex + 1 === playlist.segments.length; // true if there are no parts, or this is the last part.
25608
25609      const appendedLastPart = !segment || !segment.parts || partIndex + 1 === segment.parts.length; // if we've buffered to the end of the video, we need to call endOfStream
25610      // so that MediaSources can trigger the `ended` event when it runs out of
25611      // buffered data instead of waiting for me
25612
25613      return playlist.endList && this.mediaSource_.readyState === 'open' && appendedLastSegment && appendedLastPart;
25614    }
25615    /**
25616     * Determines what request should be made given current segment loader state.
25617     *
25618     * @return {Object} a request object that describes the segment/part to load
25619     */
25620
25621
25622    chooseNextRequest_() {
25623      const buffered = this.buffered_();
25624      const bufferedEnd = lastBufferedEnd(buffered) || 0;
25625      const bufferedTime = timeAheadOf(buffered, this.currentTime_());
25626      const preloaded = !this.hasPlayed_() && bufferedTime >= 1;
25627      const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_();
25628      const segments = this.playlist_.segments; // return no segment if:
25629      // 1. we don't have segments
25630      // 2. The video has not yet played and we already downloaded a segment
25631      // 3. we already have enough buffered time
25632
25633      if (!segments.length || preloaded || haveEnoughBuffer) {
25634        return null;
25635      }
25636
25637      this.syncPoint_ = this.syncPoint_ || this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_(), this.loaderType_);
25638      const next = {
25639        partIndex: null,
25640        mediaIndex: null,
25641        startOfSegment: null,
25642        playlist: this.playlist_,
25643        isSyncRequest: Boolean(!this.syncPoint_)
25644      };
25645
25646      if (next.isSyncRequest) {
25647        next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd);
25648        this.logger_(`choose next request. Can not find sync point. Fallback to media Index: ${next.mediaIndex}`);
25649      } else if (this.mediaIndex !== null) {
25650        const segment = segments[this.mediaIndex];
25651        const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1;
25652        next.startOfSegment = segment.end ? segment.end : bufferedEnd;
25653
25654        if (segment.parts && segment.parts[partIndex + 1]) {
25655          next.mediaIndex = this.mediaIndex;
25656          next.partIndex = partIndex + 1;
25657        } else {
25658          next.mediaIndex = this.mediaIndex + 1;
25659        }
25660      } else {
25661        let segmentIndex;
25662        let partIndex;
25663        let startTime;
25664        const targetTime = this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_();
25665
25666        if (this.mediaSequenceSync_) {
25667          this.logger_(`chooseNextRequest_ request after Quality Switch:
25668For TargetTime: ${targetTime}.
25669CurrentTime: ${this.currentTime_()}
25670BufferedEnd: ${bufferedEnd}
25671Fetch At Buffer: ${this.fetchAtBuffer_}
25672`, this.mediaSequenceSync_.diagnostics);
25673        }
25674
25675        if (this.mediaSequenceSync_ && this.mediaSequenceSync_.isReliable) {
25676          const syncInfo = this.getSyncInfoFromMediaSequenceSync_(targetTime);
25677
25678          if (!syncInfo) {
25679            const message = 'No sync info found while using media sequence sync';
25680            this.error({
25681              message,
25682              metadata: {
25683                errorType: videojs__default["default"].Error.StreamingFailedToSelectNextSegment,
25684                error: new Error(message)
25685              }
25686            });
25687            this.logger_('chooseNextRequest_ - no sync info found using media sequence sync'); // no match
25688
25689            return null;
25690          }
25691
25692          this.logger_(`chooseNextRequest_ mediaSequence syncInfo (${syncInfo.start} --> ${syncInfo.end})`);
25693          segmentIndex = syncInfo.segmentIndex;
25694          partIndex = syncInfo.partIndex;
25695          startTime = syncInfo.start;
25696        } else {
25697          this.logger_('chooseNextRequest_ - fallback to a regular segment selection algorithm, based on a syncPoint.'); // fallback
25698
25699          const mediaInfoForTime = Playlist.getMediaInfoForTime({
25700            exactManifestTimings: this.exactManifestTimings,
25701            playlist: this.playlist_,
25702            currentTime: targetTime,
25703            startingPartIndex: this.syncPoint_.partIndex,
25704            startingSegmentIndex: this.syncPoint_.segmentIndex,
25705            startTime: this.syncPoint_.time
25706          });
25707          segmentIndex = mediaInfoForTime.segmentIndex;
25708          partIndex = mediaInfoForTime.partIndex;
25709          startTime = mediaInfoForTime.startTime;
25710        }
25711
25712        next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${targetTime}` : `currentTime ${targetTime}`;
25713        next.mediaIndex = segmentIndex;
25714        next.startOfSegment = startTime;
25715        next.partIndex = partIndex;
25716        this.logger_(`choose next request. Playlist switched and we have a sync point. Media Index: ${next.mediaIndex} `);
25717      }
25718
25719      const nextSegment = segments[next.mediaIndex];
25720      let nextPart = nextSegment && typeof next.partIndex === 'number' && nextSegment.parts && nextSegment.parts[next.partIndex]; // if the next segment index is invalid or
25721      // the next partIndex is invalid do not choose a next segment.
25722
25723      if (!nextSegment || typeof next.partIndex === 'number' && !nextPart) {
25724        return null;
25725      } // if the next segment has parts, and we don't have a partIndex.
25726      // Set partIndex to 0
25727
25728
25729      if (typeof next.partIndex !== 'number' && nextSegment.parts) {
25730        next.partIndex = 0;
25731        nextPart = nextSegment.parts[0];
25732      } // independentSegments applies to every segment in a playlist. If independentSegments appears in a main playlist,
25733      // it applies to each segment in each media playlist.
25734      // https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.5.1
25735
25736
25737      const hasIndependentSegments = this.vhs_.playlists && this.vhs_.playlists.main && this.vhs_.playlists.main.independentSegments || this.playlist_.independentSegments; // if we have no buffered data then we need to make sure
25738      // that the next part we append is "independent" if possible.
25739      // So we check if the previous part is independent, and request
25740      // it if it is.
25741
25742      if (!bufferedTime && nextPart && !hasIndependentSegments && !nextPart.independent) {
25743        if (next.partIndex === 0) {
25744          const lastSegment = segments[next.mediaIndex - 1];
25745          const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1];
25746
25747          if (lastSegmentLastPart && lastSegmentLastPart.independent) {
25748            next.mediaIndex -= 1;
25749            next.partIndex = lastSegment.parts.length - 1;
25750            next.independent = 'previous segment';
25751          }
25752        } else if (nextSegment.parts[next.partIndex - 1].independent) {
25753          next.partIndex -= 1;
25754          next.independent = 'previous part';
25755        }
25756      }
25757
25758      const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended'; // do not choose a next segment if all of the following:
25759      // 1. this is the last segment in the playlist
25760      // 2. end of stream has been called on the media source already
25761      // 3. the player is not seeking
25762
25763      if (next.mediaIndex >= segments.length - 1 && ended && !this.seeking_()) {
25764        return null;
25765      }
25766
25767      if (this.shouldForceTimestampOffsetAfterResync_) {
25768        this.shouldForceTimestampOffsetAfterResync_ = false;
25769        next.forceTimestampOffset = true;
25770        this.logger_('choose next request. Force timestamp offset after loader resync');
25771      }
25772
25773      return this.generateSegmentInfo_(next);
25774    }
25775
25776    getSyncInfoFromMediaSequenceSync_(targetTime) {
25777      if (!this.mediaSequenceSync_) {
25778        return null;
25779      } // we should pull the target time to the least available time if we drop out of sync for any reason
25780
25781
25782      const finalTargetTime = Math.max(targetTime, this.mediaSequenceSync_.start);
25783
25784      if (targetTime !== finalTargetTime) {
25785        this.logger_(`getSyncInfoFromMediaSequenceSync_. Pulled target time from ${targetTime} to ${finalTargetTime}`);
25786      }
25787
25788      const mediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(finalTargetTime);
25789
25790      if (!mediaSequenceSyncInfo) {
25791        // no match at all
25792        return null;
25793      }
25794
25795      if (!mediaSequenceSyncInfo.isAppended) {
25796        // has a perfect match
25797        return mediaSequenceSyncInfo;
25798      } // has match, but segment was already appended.
25799      // attempt to auto-advance to the nearest next segment:
25800
25801
25802      const nextMediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(mediaSequenceSyncInfo.end);
25803
25804      if (!nextMediaSequenceSyncInfo) {
25805        // no match at all
25806        return null;
25807      }
25808
25809      if (nextMediaSequenceSyncInfo.isAppended) {
25810        this.logger_('getSyncInfoFromMediaSequenceSync_: We encounter unexpected scenario where next media sequence sync info is also appended!');
25811      } // got match with the nearest next segment
25812
25813
25814      return nextMediaSequenceSyncInfo;
25815    }
25816
25817    generateSegmentInfo_(options) {
25818      const {
25819        independent,
25820        playlist,
25821        mediaIndex,
25822        startOfSegment,
25823        isSyncRequest,
25824        partIndex,
25825        forceTimestampOffset,
25826        getMediaInfoForTime
25827      } = options;
25828      const segment = playlist.segments[mediaIndex];
25829      const part = typeof partIndex === 'number' && segment.parts[partIndex];
25830      const segmentInfo = {
25831        requestId: 'segment-loader-' + Math.random(),
25832        // resolve the segment URL relative to the playlist
25833        uri: part && part.resolvedUri || segment.resolvedUri,
25834        // the segment's mediaIndex at the time it was requested
25835        mediaIndex,
25836        partIndex: part ? partIndex : null,
25837        // whether or not to update the SegmentLoader's state with this
25838        // segment's mediaIndex
25839        isSyncRequest,
25840        startOfSegment,
25841        // the segment's playlist
25842        playlist,
25843        // unencrypted bytes of the segment
25844        bytes: null,
25845        // when a key is defined for this segment, the encrypted bytes
25846        encryptedBytes: null,
25847        // The target timestampOffset for this segment when we append it
25848        // to the source buffer
25849        timestampOffset: null,
25850        // The timeline that the segment is in
25851        timeline: segment.timeline,
25852        // The expected duration of the segment in seconds
25853        duration: part && part.duration || segment.duration,
25854        // retain the segment in case the playlist updates while doing an async process
25855        segment,
25856        part,
25857        byteLength: 0,
25858        transmuxer: this.transmuxer_,
25859        // type of getMediaInfoForTime that was used to get this segment
25860        getMediaInfoForTime,
25861        independent
25862      };
25863      const overrideCheck = typeof forceTimestampOffset !== 'undefined' ? forceTimestampOffset : this.isPendingTimestampOffset_;
25864      segmentInfo.timestampOffset = this.timestampOffsetForSegment_({
25865        segmentTimeline: segment.timeline,
25866        currentTimeline: this.currentTimeline_,
25867        startOfSegment,
25868        buffered: this.buffered_(),
25869        overrideCheck
25870      });
25871      const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered());
25872
25873      if (typeof audioBufferedEnd === 'number') {
25874        // since the transmuxer is using the actual timing values, but the buffer is
25875        // adjusted by the timestamp offset, we must adjust the value here
25876        segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset();
25877      }
25878
25879      if (this.sourceUpdater_.videoBuffered().length) {
25880        segmentInfo.gopsToAlignWith = gopsSafeToAlignWith(this.gopBuffer_, // since the transmuxer is using the actual timing values, but the time is
25881        // adjusted by the timestmap offset, we must adjust the value here
25882        this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(), this.timeMapping_);
25883      }
25884
25885      return segmentInfo;
25886    } // get the timestampoffset for a segment,
25887    // added so that vtt segment loader can override and prevent
25888    // adding timestamp offsets.
25889
25890
25891    timestampOffsetForSegment_(options) {
25892      return timestampOffsetForSegment(options);
25893    }
25894    /**
25895     * Determines if the network has enough bandwidth to complete the current segment
25896     * request in a timely manner. If not, the request will be aborted early and bandwidth
25897     * updated to trigger a playlist switch.
25898     *
25899     * @param {Object} stats
25900     *        Object containing stats about the request timing and size
25901     * @private
25902     */
25903
25904
25905    earlyAbortWhenNeeded_(stats) {
25906      if (this.vhs_.tech_.paused() || // Don't abort if the current playlist is on the lowestEnabledRendition
25907      // TODO: Replace using timeout with a boolean indicating whether this playlist is
25908      //       the lowestEnabledRendition.
25909      !this.xhrOptions_.timeout || // Don't abort if we have no bandwidth information to estimate segment sizes
25910      !this.playlist_.attributes.BANDWIDTH) {
25911        return;
25912      } // Wait at least 1 second since the first byte of data has been received before
25913      // using the calculated bandwidth from the progress event to allow the bitrate
25914      // to stabilize
25915
25916
25917      if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
25918        return;
25919      }
25920
25921      const currentTime = this.currentTime_();
25922      const measuredBandwidth = stats.bandwidth;
25923      const segmentDuration = this.pendingSegment_.duration;
25924      const requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort
25925      // if we are only left with less than 1 second when the request completes.
25926      // A negative timeUntilRebuffering indicates we are already rebuffering
25927
25928      const timeUntilRebuffer$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.vhs_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download
25929      // is larger than the estimated time until the player runs out of forward buffer
25930
25931      if (requestTimeRemaining <= timeUntilRebuffer$1) {
25932        return;
25933      }
25934
25935      const switchCandidate = minRebufferMaxBandwidthSelector({
25936        main: this.vhs_.playlists.main,
25937        currentTime,
25938        bandwidth: measuredBandwidth,
25939        duration: this.duration_(),
25940        segmentDuration,
25941        timeUntilRebuffer: timeUntilRebuffer$1,
25942        currentTimeline: this.currentTimeline_,
25943        syncController: this.syncController_
25944      });
25945
25946      if (!switchCandidate) {
25947        return;
25948      }
25949
25950      const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$1;
25951      const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;
25952      let minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the
25953      // potential round trip time of the new request so that we are not too aggressive
25954      // with switching to a playlist that might save us a fraction of a second.
25955
25956      if (timeUntilRebuffer$1 <= TIME_FUDGE_FACTOR) {
25957        minimumTimeSaving = 1;
25958      }
25959
25960      if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) {
25961        return;
25962      } // set the bandwidth to that of the desired playlist being sure to scale by
25963      // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
25964      // don't trigger a bandwidthupdate as the bandwidth is artifial
25965
25966
25967      this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1;
25968      this.trigger('earlyabort');
25969    }
25970
25971    handleAbort_(segmentInfo) {
25972      this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`);
25973      this.mediaRequestsAborted += 1;
25974    }
25975    /**
25976     * XHR `progress` event handler
25977     *
25978     * @param {Event}
25979     *        The XHR `progress` event
25980     * @param {Object} simpleSegment
25981     *        A simplified segment object copy
25982     * @private
25983     */
25984
25985
25986    handleProgress_(event, simpleSegment) {
25987      this.earlyAbortWhenNeeded_(simpleSegment.stats);
25988
25989      if (this.checkForAbort_(simpleSegment.requestId)) {
25990        return;
25991      }
25992
25993      this.trigger('progress');
25994    }
25995
25996    handleTrackInfo_(simpleSegment, trackInfo) {
25997      const {
25998        hasAudio,
25999        hasVideo
26000      } = trackInfo;
26001      const metadata = {
26002        segmentInfo: segmentInfoPayload({
26003          type: this.loaderType_,
26004          segment: simpleSegment
26005        }),
26006        trackInfo: {
26007          hasAudio,
26008          hasVideo
26009        }
26010      };
26011      this.trigger({
26012        type: 'segmenttransmuxingtrackinfoavailable',
26013        metadata
26014      });
26015      this.earlyAbortWhenNeeded_(simpleSegment.stats);
26016
26017      if (this.checkForAbort_(simpleSegment.requestId)) {
26018        return;
26019      }
26020
26021      if (this.checkForIllegalMediaSwitch(trackInfo)) {
26022        return;
26023      }
26024
26025      trackInfo = trackInfo || {}; // When we have track info, determine what media types this loader is dealing with.
26026      // Guard against cases where we're not getting track info at all until we are
26027      // certain that all streams will provide it.
26028
26029      if (!shallowEqual(this.currentMediaInfo_, trackInfo)) {
26030        this.appendInitSegment_ = {
26031          audio: true,
26032          video: true
26033        };
26034        this.startingMediaInfo_ = trackInfo;
26035        this.currentMediaInfo_ = trackInfo;
26036        this.logger_('trackinfo update', trackInfo);
26037        this.trigger('trackinfo');
26038      } // trackinfo may cause an abort if the trackinfo
26039      // causes a codec change to an unsupported codec.
26040
26041
26042      if (this.checkForAbort_(simpleSegment.requestId)) {
26043        return;
26044      } // set trackinfo on the pending segment so that
26045      // it can append.
26046
26047
26048      this.pendingSegment_.trackInfo = trackInfo; // check if any calls were waiting on the track info
26049
26050      if (this.hasEnoughInfoToAppend_()) {
26051        this.processCallQueue_();
26052      } else {
26053        checkAndFixTimelines(this);
26054      }
26055    }
26056
26057    handleTimingInfo_(simpleSegment, mediaType, timeType, time) {
26058      this.earlyAbortWhenNeeded_(simpleSegment.stats);
26059
26060      if (this.checkForAbort_(simpleSegment.requestId)) {
26061        return;
26062      }
26063
26064      const segmentInfo = this.pendingSegment_;
26065      const timingInfoProperty = timingInfoPropertyForMedia(mediaType);
26066      segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {};
26067      segmentInfo[timingInfoProperty][timeType] = time;
26068      this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`); // check if any calls were waiting on the timing info
26069
26070      if (this.hasEnoughInfoToAppend_()) {
26071        this.processCallQueue_();
26072      } else {
26073        checkAndFixTimelines(this);
26074      }
26075    }
26076
26077    handleCaptions_(simpleSegment, captionData) {
26078      this.earlyAbortWhenNeeded_(simpleSegment.stats);
26079
26080      if (this.checkForAbort_(simpleSegment.requestId)) {
26081        return;
26082      } // This could only happen with fmp4 segments, but
26083      // should still not happen in general
26084
26085
26086      if (captionData.length === 0) {
26087        this.logger_('SegmentLoader received no captions from a caption event');
26088        return;
26089      }
26090
26091      const segmentInfo = this.pendingSegment_; // Wait until we have some video data so that caption timing
26092      // can be adjusted by the timestamp offset
26093
26094      if (!segmentInfo.hasAppendedData_) {
26095        this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData));
26096        return;
26097      }
26098
26099      const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset();
26100      const captionTracks = {}; // get total start/end and captions for each track/stream
26101
26102      captionData.forEach(caption => {
26103        // caption.stream is actually a track name...
26104        // set to the existing values in tracks or default values
26105        captionTracks[caption.stream] = captionTracks[caption.stream] || {
26106          // Infinity, as any other value will be less than this
26107          startTime: Infinity,
26108          captions: [],
26109          // 0 as an other value will be more than this
26110          endTime: 0
26111        };
26112        const captionTrack = captionTracks[caption.stream];
26113        captionTrack.startTime = Math.min(captionTrack.startTime, caption.startTime + timestampOffset);
26114        captionTrack.endTime = Math.max(captionTrack.endTime, caption.endTime + timestampOffset);
26115        captionTrack.captions.push(caption);
26116      });
26117      Object.keys(captionTracks).forEach(trackName => {
26118        const {
26119          startTime,
26120          endTime,
26121          captions
26122        } = captionTracks[trackName];
26123        const inbandTextTracks = this.inbandTextTracks_;
26124        this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`);
26125        createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName); // clear out any cues that start and end at the same time period for the same track.
26126        // We do this because a rendition change that also changes the timescale for captions
26127        // will result in captions being re-parsed for certain segments. If we add them again
26128        // without clearing we will have two of the same captions visible.
26129
26130        removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]);
26131        addCaptionData({
26132          captionArray: captions,
26133          inbandTextTracks,
26134          timestampOffset
26135        });
26136      }); // Reset stored captions since we added parsed
26137      // captions to a text track at this point
26138
26139      if (this.transmuxer_) {
26140        this.transmuxer_.postMessage({
26141          action: 'clearParsedMp4Captions'
26142        });
26143      }
26144    }
26145
26146    handleId3_(simpleSegment, id3Frames, dispatchType) {
26147      this.earlyAbortWhenNeeded_(simpleSegment.stats);
26148
26149      if (this.checkForAbort_(simpleSegment.requestId)) {
26150        return;
26151      }
26152
26153      const segmentInfo = this.pendingSegment_; // we need to have appended data in order for the timestamp offset to be set
26154
26155      if (!segmentInfo.hasAppendedData_) {
26156        this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType));
26157        return;
26158      }
26159
26160      this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_());
26161    }
26162
26163    processMetadataQueue_() {
26164      this.metadataQueue_.id3.forEach(fn => fn());
26165      this.metadataQueue_.caption.forEach(fn => fn());
26166      this.metadataQueue_.id3 = [];
26167      this.metadataQueue_.caption = [];
26168    }
26169
26170    processCallQueue_() {
26171      const callQueue = this.callQueue_; // Clear out the queue before the queued functions are run, since some of the
26172      // functions may check the length of the load queue and default to pushing themselves
26173      // back onto the queue.
26174
26175      this.callQueue_ = [];
26176      callQueue.forEach(fun => fun());
26177    }
26178
26179    processLoadQueue_() {
26180      const loadQueue = this.loadQueue_; // Clear out the queue before the queued functions are run, since some of the
26181      // functions may check the length of the load queue and default to pushing themselves
26182      // back onto the queue.
26183
26184      this.loadQueue_ = [];
26185      loadQueue.forEach(fun => fun());
26186    }
26187    /**
26188     * Determines whether the loader has enough info to load the next segment.
26189     *
26190     * @return {boolean}
26191     *         Whether or not the loader has enough info to load the next segment
26192     */
26193
26194
26195    hasEnoughInfoToLoad_() {
26196      // Since primary timing goes by video, only the audio loader potentially needs to wait
26197      // to load.
26198      if (this.loaderType_ !== 'audio') {
26199        return true;
26200      }
26201
26202      const segmentInfo = this.pendingSegment_; // A fill buffer must have already run to establish a pending segment before there's
26203      // enough info to load.
26204
26205      if (!segmentInfo) {
26206        return false;
26207      } // The first segment can and should be loaded immediately so that source buffers are
26208      // created together (before appending). Source buffer creation uses the presence of
26209      // audio and video data to determine whether to create audio/video source buffers, and
26210      // uses processed (transmuxed or parsed) media to determine the types required.
26211
26212
26213      if (!this.getCurrentMediaInfo_()) {
26214        return true;
26215      }
26216
26217      if ( // Technically, instead of waiting to load a segment on timeline changes, a segment
26218      // can be requested and downloaded and only wait before it is transmuxed or parsed.
26219      // But in practice, there are a few reasons why it is better to wait until a loader
26220      // is ready to append that segment before requesting and downloading:
26221      //
26222      // 1. Because audio and main loaders cross discontinuities together, if this loader
26223      //    is waiting for the other to catch up, then instead of requesting another
26224      //    segment and using up more bandwidth, by not yet loading, more bandwidth is
26225      //    allotted to the loader currently behind.
26226      // 2. media-segment-request doesn't have to have logic to consider whether a segment
26227      // is ready to be processed or not, isolating the queueing behavior to the loader.
26228      // 3. The audio loader bases some of its segment properties on timing information
26229      //    provided by the main loader, meaning that, if the logic for waiting on
26230      //    processing was in media-segment-request, then it would also need to know how
26231      //    to re-generate the segment information after the main loader caught up.
26232      shouldWaitForTimelineChange({
26233        timelineChangeController: this.timelineChangeController_,
26234        currentTimeline: this.currentTimeline_,
26235        segmentTimeline: segmentInfo.timeline,
26236        loaderType: this.loaderType_,
26237        audioDisabled: this.audioDisabled_
26238      })) {
26239        return false;
26240      }
26241
26242      return true;
26243    }
26244
26245    getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) {
26246      return segmentInfo && segmentInfo.trackInfo || this.currentMediaInfo_;
26247    }
26248
26249    getMediaInfo_(segmentInfo = this.pendingSegment_) {
26250      return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_;
26251    }
26252
26253    getPendingSegmentPlaylist() {
26254      return this.pendingSegment_ ? this.pendingSegment_.playlist : null;
26255    }
26256
26257    hasEnoughInfoToAppend_() {
26258      if (!this.sourceUpdater_.ready()) {
26259        return false;
26260      } // If content needs to be removed or the loader is waiting on an append reattempt,
26261      // then no additional content should be appended until the prior append is resolved.
26262
26263
26264      if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) {
26265        return false;
26266      }
26267
26268      const segmentInfo = this.pendingSegment_;
26269      const trackInfo = this.getCurrentMediaInfo_(); // no segment to append any data for or
26270      // we do not have information on this specific
26271      // segment yet
26272
26273      if (!segmentInfo || !trackInfo) {
26274        return false;
26275      }
26276
26277      const {
26278        hasAudio,
26279        hasVideo,
26280        isMuxed
26281      } = trackInfo;
26282
26283      if (hasVideo && !segmentInfo.videoTimingInfo) {
26284        return false;
26285      } // muxed content only relies on video timing information for now.
26286
26287
26288      if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) {
26289        return false;
26290      } // we need to allow an append here even if we're moving to different timelines.
26291
26292
26293      if (shouldWaitForTimelineChange({
26294        timelineChangeController: this.timelineChangeController_,
26295        currentTimeline: this.currentTimeline_,
26296        segmentTimeline: segmentInfo.timeline,
26297        loaderType: this.loaderType_,
26298        audioDisabled: this.audioDisabled_
26299      })) {
26300        return false;
26301      }
26302
26303      return true;
26304    }
26305
26306    handleData_(simpleSegment, result) {
26307      this.earlyAbortWhenNeeded_(simpleSegment.stats);
26308
26309      if (this.checkForAbort_(simpleSegment.requestId)) {
26310        return;
26311      } // If there's anything in the call queue, then this data came later and should be
26312      // executed after the calls currently queued.
26313
26314
26315      if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) {
26316        checkAndFixTimelines(this);
26317        this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result));
26318        return;
26319      }
26320
26321      const segmentInfo = this.pendingSegment_; // update the time mapping so we can translate from display time to media time
26322
26323      this.setTimeMapping_(segmentInfo.timeline); // for tracking overall stats
26324
26325      this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment); // Note that the state isn't changed from loading to appending. This is because abort
26326      // logic may change behavior depending on the state, and changing state too early may
26327      // inflate our estimates of bandwidth. In the future this should be re-examined to
26328      // note more granular states.
26329      // don't process and append data if the mediaSource is closed
26330
26331      if (this.mediaSource_.readyState === 'closed') {
26332        return;
26333      } // if this request included an initialization segment, save that data
26334      // to the initSegment cache
26335
26336
26337      if (simpleSegment.map) {
26338        simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true); // move over init segment properties to media request
26339
26340        segmentInfo.segment.map = simpleSegment.map;
26341      } // if this request included a segment key, save that data in the cache
26342
26343
26344      if (simpleSegment.key) {
26345        this.segmentKey(simpleSegment.key, true);
26346      }
26347
26348      segmentInfo.isFmp4 = simpleSegment.isFmp4;
26349      segmentInfo.timingInfo = segmentInfo.timingInfo || {};
26350
26351      if (segmentInfo.isFmp4) {
26352        this.trigger('fmp4');
26353        segmentInfo.timingInfo.start = segmentInfo[timingInfoPropertyForMedia(result.type)].start;
26354      } else {
26355        const trackInfo = this.getCurrentMediaInfo_();
26356        const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo;
26357        let firstVideoFrameTimeForData;
26358
26359        if (useVideoTimingInfo) {
26360          firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start;
26361        } // Segment loader knows more about segment timing than the transmuxer (in certain
26362        // aspects), so make any changes required for a more accurate start time.
26363        // Don't set the end time yet, as the segment may not be finished processing.
26364
26365
26366        segmentInfo.timingInfo.start = this.trueSegmentStart_({
26367          currentStart: segmentInfo.timingInfo.start,
26368          playlist: segmentInfo.playlist,
26369          mediaIndex: segmentInfo.mediaIndex,
26370          currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(),
26371          useVideoTimingInfo,
26372          firstVideoFrameTimeForData,
26373          videoTimingInfo: segmentInfo.videoTimingInfo,
26374          audioTimingInfo: segmentInfo.audioTimingInfo
26375        });
26376      } // Init segments for audio and video only need to be appended in certain cases. Now
26377      // that data is about to be appended, we can check the final cases to determine
26378      // whether we should append an init segment.
26379
26380
26381      this.updateAppendInitSegmentStatus(segmentInfo, result.type); // Timestamp offset should be updated once we get new data and have its timing info,
26382      // as we use the start of the segment to offset the best guess (playlist provided)
26383      // timestamp offset.
26384
26385      this.updateSourceBufferTimestampOffset_(segmentInfo); // if this is a sync request we need to determine whether it should
26386      // be appended or not.
26387
26388      if (segmentInfo.isSyncRequest) {
26389        // first save/update our timing info for this segment.
26390        // this is what allows us to choose an accurate segment
26391        // and the main reason we make a sync request.
26392        this.updateTimingInfoEnd_(segmentInfo);
26393        this.syncController_.saveSegmentTimingInfo({
26394          segmentInfo,
26395          shouldSaveTimelineMapping: this.loaderType_ === 'main'
26396        });
26397        const next = this.chooseNextRequest_(); // If the sync request isn't the segment that would be requested next
26398        // after taking into account its timing info, do not append it.
26399
26400        if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) {
26401          this.logger_('sync segment was incorrect, not appending');
26402          return;
26403        } // otherwise append it like any other segment as our guess was correct.
26404
26405
26406        this.logger_('sync segment was correct, appending');
26407      } // Save some state so that in the future anything waiting on first append (and/or
26408      // timestamp offset(s)) can process immediately. While the extra state isn't optimal,
26409      // we need some notion of whether the timestamp offset or other relevant information
26410      // has had a chance to be set.
26411
26412
26413      segmentInfo.hasAppendedData_ = true; // Now that the timestamp offset should be set, we can append any waiting ID3 tags.
26414
26415      this.processMetadataQueue_();
26416      this.appendData_(segmentInfo, result);
26417    }
26418
26419    updateAppendInitSegmentStatus(segmentInfo, type) {
26420      // alt audio doesn't manage timestamp offset
26421      if (this.loaderType_ === 'main' && typeof segmentInfo.timestampOffset === 'number' && // in the case that we're handling partial data, we don't want to append an init
26422      // segment for each chunk
26423      !segmentInfo.changedTimestampOffset) {
26424        // if the timestamp offset changed, the timeline may have changed, so we have to re-
26425        // append init segments
26426        this.appendInitSegment_ = {
26427          audio: true,
26428          video: true
26429        };
26430      }
26431
26432      if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) {
26433        // make sure we append init segment on playlist changes, in case the media config
26434        // changed
26435        this.appendInitSegment_[type] = true;
26436      }
26437    }
26438
26439    getInitSegmentAndUpdateState_({
26440      type,
26441      initSegment,
26442      map,
26443      playlist
26444    }) {
26445      // "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section
26446      // (Section 3) required to parse the applicable Media Segments.  It applies to every
26447      // Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag
26448      // or until the end of the playlist."
26449      // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5
26450      if (map) {
26451        const id = initSegmentId(map);
26452
26453        if (this.activeInitSegmentId_ === id) {
26454          // don't need to re-append the init segment if the ID matches
26455          return null;
26456        } // a map-specified init segment takes priority over any transmuxed (or otherwise
26457        // obtained) init segment
26458        //
26459        // this also caches the init segment for later use
26460
26461
26462        initSegment = this.initSegmentForMap(map, true).bytes;
26463        this.activeInitSegmentId_ = id;
26464      } // We used to always prepend init segments for video, however, that shouldn't be
26465      // necessary. Instead, we should only append on changes, similar to what we've always
26466      // done for audio. This is more important (though may not be that important) for
26467      // frame-by-frame appending for LHLS, simply because of the increased quantity of
26468      // appends.
26469
26470
26471      if (initSegment && this.appendInitSegment_[type]) {
26472        // Make sure we track the playlist that we last used for the init segment, so that
26473        // we can re-append the init segment in the event that we get data from a new
26474        // playlist. Discontinuities and track changes are handled in other sections.
26475        this.playlistOfLastInitSegment_[type] = playlist; // Disable future init segment appends for this type. Until a change is necessary.
26476
26477        this.appendInitSegment_[type] = false; // we need to clear out the fmp4 active init segment id, since
26478        // we are appending the muxer init segment
26479
26480        this.activeInitSegmentId_ = null;
26481        return initSegment;
26482      }
26483
26484      return null;
26485    }
26486
26487    handleQuotaExceededError_({
26488      segmentInfo,
26489      type,
26490      bytes
26491    }, error) {
26492      const audioBuffered = this.sourceUpdater_.audioBuffered();
26493      const videoBuffered = this.sourceUpdater_.videoBuffered(); // For now we're ignoring any notion of gaps in the buffer, but they, in theory,
26494      // should be cleared out during the buffer removals. However, log in case it helps
26495      // debug.
26496
26497      if (audioBuffered.length > 1) {
26498        this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' + timeRangesToArray(audioBuffered).join(', '));
26499      }
26500
26501      if (videoBuffered.length > 1) {
26502        this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' + timeRangesToArray(videoBuffered).join(', '));
26503      }
26504
26505      const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0;
26506      const audioBufferEnd = audioBuffered.length ? audioBuffered.end(audioBuffered.length - 1) : 0;
26507      const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0;
26508      const videoBufferEnd = videoBuffered.length ? videoBuffered.end(videoBuffered.length - 1) : 0;
26509
26510      if (audioBufferEnd - audioBufferStart <= MIN_BACK_BUFFER && videoBufferEnd - videoBufferStart <= MIN_BACK_BUFFER) {
26511        // Can't remove enough buffer to make room for new segment (or the browser doesn't
26512        // allow for appends of segments this size). In the future, it may be possible to
26513        // split up the segment and append in pieces, but for now, error out this playlist
26514        // in an attempt to switch to a more manageable rendition.
26515        this.logger_('On QUOTA_EXCEEDED_ERR, single segment too large to append to ' + 'buffer, triggering an error. ' + `Appended byte length: ${bytes.byteLength}, ` + `audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` + `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `);
26516        this.error({
26517          message: 'Quota exceeded error with append of a single segment of content',
26518          excludeUntil: Infinity
26519        });
26520        this.trigger('error');
26521        return;
26522      } // To try to resolve the quota exceeded error, clear back buffer and retry. This means
26523      // that the segment-loader should block on future events until this one is handled, so
26524      // that it doesn't keep moving onto further segments. Adding the call to the call
26525      // queue will prevent further appends until waitingOnRemove_ and
26526      // quotaExceededErrorRetryTimeout_ are cleared.
26527      //
26528      // Note that this will only block the current loader. In the case of demuxed content,
26529      // the other load may keep filling as fast as possible. In practice, this should be
26530      // OK, as it is a rare case when either audio has a high enough bitrate to fill up a
26531      // source buffer, or video fills without enough room for audio to append (and without
26532      // the availability of clearing out seconds of back buffer to make room for audio).
26533      // But it might still be good to handle this case in the future as a TODO.
26534
26535
26536      this.waitingOnRemove_ = true;
26537      this.callQueue_.push(this.appendToSourceBuffer_.bind(this, {
26538        segmentInfo,
26539        type,
26540        bytes
26541      }));
26542      const currentTime = this.currentTime_(); // Try to remove as much audio and video as possible to make room for new content
26543      // before retrying.
26544
26545      const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER;
26546      this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`);
26547      this.remove(0, timeToRemoveUntil, () => {
26548        this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`);
26549        this.waitingOnRemove_ = false; // wait the length of time alotted in the back buffer to prevent wasted
26550        // attempts (since we can't clear less than the minimum)
26551
26552        this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => {
26553          this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue');
26554          this.quotaExceededErrorRetryTimeout_ = null;
26555          this.processCallQueue_();
26556        }, MIN_BACK_BUFFER * 1000);
26557      }, true);
26558    }
26559
26560    handleAppendError_({
26561      segmentInfo,
26562      type,
26563      bytes
26564    }, error) {
26565      // if there's no error, nothing to do
26566      if (!error) {
26567        return;
26568      }
26569
26570      if (error.code === QUOTA_EXCEEDED_ERR) {
26571        this.handleQuotaExceededError_({
26572          segmentInfo,
26573          type,
26574          bytes
26575        }); // A quota exceeded error should be recoverable with a future re-append, so no need
26576        // to trigger an append error.
26577
26578        return;
26579      }
26580
26581      this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error); // If an append errors, we often can't recover.
26582      // (see https://w3c.github.io/media-source/#sourcebuffer-append-error).
26583      //
26584      // Trigger a special error so that it can be handled separately from normal,
26585      // recoverable errors.
26586
26587      this.error({
26588        message: `${type} append of ${bytes.length}b failed for segment ` + `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`,
26589        metadata: {
26590          errorType: videojs__default["default"].Error.StreamingFailedToAppendSegment
26591        }
26592      });
26593      this.trigger('appenderror');
26594    }
26595
26596    appendToSourceBuffer_({
26597      segmentInfo,
26598      type,
26599      initSegment,
26600      data,
26601      bytes
26602    }) {
26603      // If this is a re-append, bytes were already created and don't need to be recreated
26604      if (!bytes) {
26605        const segments = [data];
26606        let byteLength = data.byteLength;
26607
26608        if (initSegment) {
26609          // if the media initialization segment is changing, append it before the content
26610          // segment
26611          segments.unshift(initSegment);
26612          byteLength += initSegment.byteLength;
26613        } // Technically we should be OK appending the init segment separately, however, we
26614        // haven't yet tested that, and prepending is how we have always done things.
26615
26616
26617        bytes = concatSegments({
26618          bytes: byteLength,
26619          segments
26620        });
26621      }
26622
26623      const metadata = {
26624        segmentInfo: segmentInfoPayload({
26625          type: this.loaderType_,
26626          segment: segmentInfo
26627        })
26628      };
26629      this.trigger({
26630        type: 'segmentappendstart',
26631        metadata
26632      });
26633      this.sourceUpdater_.appendBuffer({
26634        segmentInfo,
26635        type,
26636        bytes
26637      }, this.handleAppendError_.bind(this, {
26638        segmentInfo,
26639        type,
26640        bytes
26641      }));
26642    }
26643
26644    handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) {
26645      if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) {
26646        return;
26647      }
26648
26649      const segment = this.pendingSegment_.segment;
26650      const timingInfoProperty = `${type}TimingInfo`;
26651
26652      if (!segment[timingInfoProperty]) {
26653        segment[timingInfoProperty] = {};
26654      }
26655
26656      segment[timingInfoProperty].transmuxerPrependedSeconds = segmentTimingInfo.prependedContentDuration || 0;
26657      segment[timingInfoProperty].transmuxedPresentationStart = segmentTimingInfo.start.presentation;
26658      segment[timingInfoProperty].transmuxedDecodeStart = segmentTimingInfo.start.decode;
26659      segment[timingInfoProperty].transmuxedPresentationEnd = segmentTimingInfo.end.presentation;
26660      segment[timingInfoProperty].transmuxedDecodeEnd = segmentTimingInfo.end.decode; // mainly used as a reference for debugging
26661
26662      segment[timingInfoProperty].baseMediaDecodeTime = segmentTimingInfo.baseMediaDecodeTime;
26663    }
26664
26665    appendData_(segmentInfo, result) {
26666      const {
26667        type,
26668        data
26669      } = result;
26670
26671      if (!data || !data.byteLength) {
26672        return;
26673      }
26674
26675      if (type === 'audio' && this.audioDisabled_) {
26676        return;
26677      }
26678
26679      const initSegment = this.getInitSegmentAndUpdateState_({
26680        type,
26681        initSegment: result.initSegment,
26682        playlist: segmentInfo.playlist,
26683        map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null
26684      });
26685      this.appendToSourceBuffer_({
26686        segmentInfo,
26687        type,
26688        initSegment,
26689        data
26690      });
26691    }
26692    /**
26693     * load a specific segment from a request into the buffer
26694     *
26695     * @private
26696     */
26697
26698
26699    loadSegment_(segmentInfo) {
26700      this.state = 'WAITING';
26701      this.pendingSegment_ = segmentInfo;
26702      this.trimBackBuffer_(segmentInfo);
26703
26704      if (typeof segmentInfo.timestampOffset === 'number') {
26705        if (this.transmuxer_) {
26706          this.transmuxer_.postMessage({
26707            action: 'clearAllMp4Captions'
26708          });
26709        }
26710      }
26711
26712      if (!this.hasEnoughInfoToLoad_()) {
26713        checkAndFixTimelines(this);
26714        this.loadQueue_.push(() => {
26715          // regenerate the audioAppendStart, timestampOffset, etc as they
26716          // may have changed since this function was added to the queue.
26717          const options = _extends({}, segmentInfo, {
26718            forceTimestampOffset: true
26719          });
26720
26721          _extends(segmentInfo, this.generateSegmentInfo_(options));
26722
26723          this.isPendingTimestampOffset_ = false;
26724          this.updateTransmuxerAndRequestSegment_(segmentInfo);
26725        });
26726        return;
26727      }
26728
26729      this.updateTransmuxerAndRequestSegment_(segmentInfo);
26730    }
26731
26732    updateTransmuxerAndRequestSegment_(segmentInfo) {
26733      // We'll update the source buffer's timestamp offset once we have transmuxed data, but
26734      // the transmuxer still needs to be updated before then.
26735      //
26736      // Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp
26737      // offset must be passed to the transmuxer for stream correcting adjustments.
26738      if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) {
26739        this.gopBuffer_.length = 0; // gopsToAlignWith was set before the GOP buffer was cleared
26740
26741        segmentInfo.gopsToAlignWith = [];
26742        this.timeMapping_ = 0; // reset values in the transmuxer since a discontinuity should start fresh
26743
26744        this.transmuxer_.postMessage({
26745          action: 'reset'
26746        });
26747        this.transmuxer_.postMessage({
26748          action: 'setTimestampOffset',
26749          timestampOffset: segmentInfo.timestampOffset
26750        });
26751      }
26752
26753      const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo);
26754      const isEndOfStream = this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex);
26755      const isWalkingForward = this.mediaIndex !== null;
26756      const isDiscontinuity = segmentInfo.timeline !== this.currentTimeline_ && // currentTimeline starts at -1, so we shouldn't end the timeline switching to 0,
26757      // the first timeline
26758      segmentInfo.timeline > 0;
26759      const isEndOfTimeline = isEndOfStream || isWalkingForward && isDiscontinuity;
26760      this.logger_(`Requesting
26761${compactSegmentUrlDescription(segmentInfo.uri)}
26762${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes),
26763      // then this init segment has never been seen before and should be appended.
26764      //
26765      // At this point the content type (audio/video or both) is not yet known, but it should be safe to set
26766      // both to true and leave the decision of whether to append the init segment to append time.
26767
26768      if (simpleSegment.map && !simpleSegment.map.bytes) {
26769        this.logger_('going to request init segment.');
26770        this.appendInitSegment_ = {
26771          video: true,
26772          audio: true
26773        };
26774      }
26775
26776      segmentInfo.abortRequests = mediaSegmentRequest({
26777        xhr: this.vhs_.xhr,
26778        xhrOptions: this.xhrOptions_,
26779        decryptionWorker: this.decrypter_,
26780        segment: simpleSegment,
26781        abortFn: this.handleAbort_.bind(this, segmentInfo),
26782        progressFn: this.handleProgress_.bind(this),
26783        trackInfoFn: this.handleTrackInfo_.bind(this),
26784        timingInfoFn: this.handleTimingInfo_.bind(this),
26785        videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'video', segmentInfo.requestId),
26786        audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'audio', segmentInfo.requestId),
26787        captionsFn: this.handleCaptions_.bind(this),
26788        isEndOfTimeline,
26789        endedTimelineFn: () => {
26790          this.logger_('received endedtimeline callback');
26791        },
26792        id3Fn: this.handleId3_.bind(this),
26793        dataFn: this.handleData_.bind(this),
26794        doneFn: this.segmentRequestFinished_.bind(this),
26795        onTransmuxerLog: ({
26796          message,
26797          level,
26798          stream
26799        }) => {
26800          this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`);
26801        },
26802        triggerSegmentEventFn: ({
26803          type,
26804          segment,
26805          keyInfo,
26806          trackInfo,
26807          timingInfo
26808        }) => {
26809          const segInfo = segmentInfoPayload({
26810            segment
26811          });
26812          const metadata = {
26813            segmentInfo: segInfo
26814          }; // add other properties if necessary.
26815
26816          if (keyInfo) {
26817            metadata.keyInfo = keyInfo;
26818          }
26819
26820          if (trackInfo) {
26821            metadata.trackInfo = trackInfo;
26822          }
26823
26824          if (timingInfo) {
26825            metadata.timingInfo = timingInfo;
26826          }
26827
26828          this.trigger({
26829            type,
26830            metadata
26831          });
26832        }
26833      });
26834    }
26835    /**
26836     * trim the back buffer so that we don't have too much data
26837     * in the source buffer
26838     *
26839     * @private
26840     *
26841     * @param {Object} segmentInfo - the current segment
26842     */
26843
26844
26845    trimBackBuffer_(segmentInfo) {
26846      const removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); // Chrome has a hard limit of 150MB of
26847      // buffer and a very conservative "garbage collector"
26848      // We manually clear out the old buffer to ensure
26849      // we don't trigger the QuotaExceeded error
26850      // on the source buffer during subsequent appends
26851
26852      if (removeToTime > 0) {
26853        this.remove(0, removeToTime);
26854      }
26855    }
26856    /**
26857     * created a simplified copy of the segment object with just the
26858     * information necessary to perform the XHR and decryption
26859     *
26860     * @private
26861     *
26862     * @param {Object} segmentInfo - the current segment
26863     * @return {Object} a simplified segment object copy
26864     */
26865
26866
26867    createSimplifiedSegmentObj_(segmentInfo) {
26868      const segment = segmentInfo.segment;
26869      const part = segmentInfo.part;
26870      const isEncrypted = segmentInfo.segment.key || segmentInfo.segment.map && segmentInfo.segment.map.key;
26871      const isMediaInitialization = segmentInfo.segment.map && !segmentInfo.segment.map.bytes;
26872      const simpleSegment = {
26873        resolvedUri: part ? part.resolvedUri : segment.resolvedUri,
26874        byterange: part ? part.byterange : segment.byterange,
26875        requestId: segmentInfo.requestId,
26876        transmuxer: segmentInfo.transmuxer,
26877        audioAppendStart: segmentInfo.audioAppendStart,
26878        gopsToAlignWith: segmentInfo.gopsToAlignWith,
26879        part: segmentInfo.part,
26880        type: this.loaderType_,
26881        start: segmentInfo.startOfSegment,
26882        duration: segmentInfo.duration,
26883        isEncrypted,
26884        isMediaInitialization
26885      };
26886      const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1];
26887
26888      if (previousSegment && previousSegment.timeline === segment.timeline) {
26889        // The baseStartTime of a segment is used to handle rollover when probing the TS
26890        // segment to retrieve timing information. Since the probe only looks at the media's
26891        // times (e.g., PTS and DTS values of the segment), and doesn't consider the
26892        // player's time (e.g., player.currentTime()), baseStartTime should reflect the
26893        // media time as well. transmuxedDecodeEnd represents the end time of a segment, in
26894        // seconds of media time, so should be used here. The previous segment is used since
26895        // the end of the previous segment should represent the beginning of the current
26896        // segment, so long as they are on the same timeline.
26897        if (previousSegment.videoTimingInfo) {
26898          simpleSegment.baseStartTime = previousSegment.videoTimingInfo.transmuxedDecodeEnd;
26899        } else if (previousSegment.audioTimingInfo) {
26900          simpleSegment.baseStartTime = previousSegment.audioTimingInfo.transmuxedDecodeEnd;
26901        }
26902      }
26903
26904      if (segment.key) {
26905        // if the media sequence is greater than 2^32, the IV will be incorrect
26906        // assuming 10s segments, that would be about 1300 years
26907        const iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]);
26908        simpleSegment.key = this.segmentKey(segment.key);
26909        simpleSegment.key.iv = iv;
26910      }
26911
26912      if (segment.map) {
26913        simpleSegment.map = this.initSegmentForMap(segment.map);
26914      }
26915
26916      return simpleSegment;
26917    }
26918
26919    saveTransferStats_(stats) {
26920      // every request counts as a media request even if it has been aborted
26921      // or canceled due to a timeout
26922      this.mediaRequests += 1;
26923
26924      if (stats) {
26925        this.mediaBytesTransferred += stats.bytesReceived;
26926        this.mediaTransferDuration += stats.roundTripTime;
26927      }
26928    }
26929
26930    saveBandwidthRelatedStats_(duration, stats) {
26931      // byteLength will be used for throughput, and should be based on bytes receieved,
26932      // which we only know at the end of the request and should reflect total bytes
26933      // downloaded rather than just bytes processed from components of the segment
26934      this.pendingSegment_.byteLength = stats.bytesReceived;
26935
26936      if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) {
26937        this.logger_(`Ignoring segment's bandwidth because its duration of ${duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`);
26938        return;
26939      }
26940
26941      const metadata = {
26942        bandwidthInfo: {
26943          from: this.bandwidth,
26944          to: stats.bandwidth
26945        }
26946      }; // player event with payload
26947
26948      this.trigger({
26949        type: 'bandwidthupdated',
26950        metadata
26951      });
26952      this.bandwidth = stats.bandwidth;
26953      this.roundTrip = stats.roundTripTime;
26954    }
26955
26956    handleTimeout_() {
26957      // although the VTT segment loader bandwidth isn't really used, it's good to
26958      // maintain functinality between segment loaders
26959      this.mediaRequestsTimedout += 1;
26960      this.bandwidth = 1;
26961      this.roundTrip = NaN;
26962      this.trigger('bandwidthupdate');
26963      this.trigger('timeout');
26964    }
26965    /**
26966     * Handle the callback from the segmentRequest function and set the
26967     * associated SegmentLoader state and errors if necessary
26968     *
26969     * @private
26970     */
26971
26972
26973    segmentRequestFinished_(error, simpleSegment, result) {
26974      // TODO handle special cases, e.g., muxed audio/video but only audio in the segment
26975      // check the call queue directly since this function doesn't need to deal with any
26976      // data, and can continue even if the source buffers are not set up and we didn't get
26977      // any data from the segment
26978      if (this.callQueue_.length) {
26979        this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result));
26980        return;
26981      }
26982
26983      this.saveTransferStats_(simpleSegment.stats); // The request was aborted and the SegmentLoader has already been reset
26984
26985      if (!this.pendingSegment_) {
26986        return;
26987      } // the request was aborted and the SegmentLoader has already started
26988      // another request. this can happen when the timeout for an aborted
26989      // request triggers due to a limitation in the XHR library
26990      // do not count this as any sort of request or we risk double-counting
26991
26992
26993      if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
26994        return;
26995      } // an error occurred from the active pendingSegment_ so reset everything
26996
26997
26998      if (error) {
26999        this.pendingSegment_ = null;
27000        this.state = 'READY'; // aborts are not a true error condition and nothing corrective needs to be done
27001
27002        if (error.code === REQUEST_ERRORS.ABORTED) {
27003          return;
27004        }
27005
27006        this.pause(); // the error is really just that at least one of the requests timed-out
27007        // set the bandwidth to a very low value and trigger an ABR switch to
27008        // take emergency action
27009
27010        if (error.code === REQUEST_ERRORS.TIMEOUT) {
27011          this.handleTimeout_();
27012          return;
27013        } // if control-flow has arrived here, then the error is real
27014        // emit an error event to exclude the current playlist
27015
27016
27017        this.mediaRequestsErrored += 1;
27018        this.error(error);
27019        this.trigger('error');
27020        return;
27021      }
27022
27023      const segmentInfo = this.pendingSegment_; // the response was a success so set any bandwidth stats the request
27024      // generated for ABR purposes
27025
27026      this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats);
27027      segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
27028
27029      if (result.gopInfo) {
27030        this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_);
27031      } // Although we may have already started appending on progress, we shouldn't switch the
27032      // state away from loading until we are officially done loading the segment data.
27033
27034
27035      this.state = 'APPENDING'; // used for testing
27036
27037      this.trigger('appending');
27038      this.waitForAppendsToComplete_(segmentInfo);
27039    }
27040
27041    setTimeMapping_(timeline) {
27042      const timelineMapping = this.syncController_.mappingForTimeline(timeline);
27043
27044      if (timelineMapping !== null) {
27045        this.timeMapping_ = timelineMapping;
27046      }
27047    }
27048
27049    updateMediaSecondsLoaded_(segment) {
27050      if (typeof segment.start === 'number' && typeof segment.end === 'number') {
27051        this.mediaSecondsLoaded += segment.end - segment.start;
27052      } else {
27053        this.mediaSecondsLoaded += segment.duration;
27054      }
27055    }
27056
27057    shouldUpdateTransmuxerTimestampOffset_(timestampOffset) {
27058      if (timestampOffset === null) {
27059        return false;
27060      } // note that we're potentially using the same timestamp offset for both video and
27061      // audio
27062
27063
27064      if (this.loaderType_ === 'main' && timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) {
27065        return true;
27066      }
27067
27068      if (!this.audioDisabled_ && timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) {
27069        return true;
27070      }
27071
27072      return false;
27073    }
27074
27075    trueSegmentStart_({
27076      currentStart,
27077      playlist,
27078      mediaIndex,
27079      firstVideoFrameTimeForData,
27080      currentVideoTimestampOffset,
27081      useVideoTimingInfo,
27082      videoTimingInfo,
27083      audioTimingInfo
27084    }) {
27085      if (typeof currentStart !== 'undefined') {
27086        // if start was set once, keep using it
27087        return currentStart;
27088      }
27089
27090      if (!useVideoTimingInfo) {
27091        return audioTimingInfo.start;
27092      }
27093
27094      const previousSegment = playlist.segments[mediaIndex - 1]; // The start of a segment should be the start of the first full frame contained
27095      // within that segment. Since the transmuxer maintains a cache of incomplete data
27096      // from and/or the last frame seen, the start time may reflect a frame that starts
27097      // in the previous segment. Check for that case and ensure the start time is
27098      // accurate for the segment.
27099
27100      if (mediaIndex === 0 || !previousSegment || typeof previousSegment.start === 'undefined' || previousSegment.end !== firstVideoFrameTimeForData + currentVideoTimestampOffset) {
27101        return firstVideoFrameTimeForData;
27102      }
27103
27104      return videoTimingInfo.start;
27105    }
27106
27107    waitForAppendsToComplete_(segmentInfo) {
27108      const trackInfo = this.getCurrentMediaInfo_(segmentInfo);
27109
27110      if (!trackInfo) {
27111        this.error({
27112          message: 'No starting media returned, likely due to an unsupported media format.',
27113          playlistExclusionDuration: Infinity
27114        });
27115        this.trigger('error');
27116        return;
27117      } // Although transmuxing is done, appends may not yet be finished. Throw a marker
27118      // on each queue this loader is responsible for to ensure that the appends are
27119      // complete.
27120
27121
27122      const {
27123        hasAudio,
27124        hasVideo,
27125        isMuxed
27126      } = trackInfo;
27127      const waitForVideo = this.loaderType_ === 'main' && hasVideo;
27128      const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed;
27129      segmentInfo.waitingOnAppends = 0; // segments with no data
27130
27131      if (!segmentInfo.hasAppendedData_) {
27132        if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') {
27133          // When there's no audio or video data in the segment, there's no audio or video
27134          // timing information.
27135          //
27136          // If there's no audio or video timing information, then the timestamp offset
27137          // can't be adjusted to the appropriate value for the transmuxer and source
27138          // buffers.
27139          //
27140          // Therefore, the next segment should be used to set the timestamp offset.
27141          this.isPendingTimestampOffset_ = true;
27142        } // override settings for metadata only segments
27143
27144
27145        segmentInfo.timingInfo = {
27146          start: 0
27147        };
27148        segmentInfo.waitingOnAppends++;
27149
27150        if (!this.isPendingTimestampOffset_) {
27151          // update the timestampoffset
27152          this.updateSourceBufferTimestampOffset_(segmentInfo); // make sure the metadata queue is processed even though we have
27153          // no video/audio data.
27154
27155          this.processMetadataQueue_();
27156        } // append is "done" instantly with no data.
27157
27158
27159        this.checkAppendsDone_(segmentInfo);
27160        return;
27161      } // Since source updater could call back synchronously, do the increments first.
27162
27163
27164      if (waitForVideo) {
27165        segmentInfo.waitingOnAppends++;
27166      }
27167
27168      if (waitForAudio) {
27169        segmentInfo.waitingOnAppends++;
27170      }
27171
27172      if (waitForVideo) {
27173        this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo));
27174      }
27175
27176      if (waitForAudio) {
27177        this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo));
27178      }
27179    }
27180
27181    checkAppendsDone_(segmentInfo) {
27182      if (this.checkForAbort_(segmentInfo.requestId)) {
27183        return;
27184      }
27185
27186      segmentInfo.waitingOnAppends--;
27187
27188      if (segmentInfo.waitingOnAppends === 0) {
27189        this.handleAppendsDone_();
27190      }
27191    }
27192
27193    checkForIllegalMediaSwitch(trackInfo) {
27194      const illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.getCurrentMediaInfo_(), trackInfo);
27195
27196      if (illegalMediaSwitchError) {
27197        this.error({
27198          message: illegalMediaSwitchError,
27199          playlistExclusionDuration: Infinity
27200        });
27201        this.trigger('error');
27202        return true;
27203      }
27204
27205      return false;
27206    }
27207
27208    updateSourceBufferTimestampOffset_(segmentInfo) {
27209      if (segmentInfo.timestampOffset === null || // we don't yet have the start for whatever media type (video or audio) has
27210      // priority, timing-wise, so we must wait
27211      typeof segmentInfo.timingInfo.start !== 'number' || // already updated the timestamp offset for this segment
27212      segmentInfo.changedTimestampOffset || // the alt audio loader should not be responsible for setting the timestamp offset
27213      this.loaderType_ !== 'main') {
27214        return;
27215      }
27216
27217      let didChange = false; // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that
27218      // the timing info here comes from video. In the event that the audio is longer than
27219      // the video, this will trim the start of the audio.
27220      // This also trims any offset from 0 at the beginning of the media
27221
27222      segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({
27223        videoTimingInfo: segmentInfo.segment.videoTimingInfo,
27224        audioTimingInfo: segmentInfo.segment.audioTimingInfo,
27225        timingInfo: segmentInfo.timingInfo
27226      }); // In the event that there are part segment downloads, each will try to update the
27227      // timestamp offset. Retaining this bit of state prevents us from updating in the
27228      // future (within the same segment), however, there may be a better way to handle it.
27229
27230      segmentInfo.changedTimestampOffset = true;
27231
27232      if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) {
27233        this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset);
27234        didChange = true;
27235      }
27236
27237      if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) {
27238        this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset);
27239        didChange = true;
27240      }
27241
27242      if (didChange) {
27243        this.trigger('timestampoffset');
27244      }
27245    }
27246
27247    getSegmentStartTimeForTimestampOffsetCalculation_({
27248      videoTimingInfo,
27249      audioTimingInfo,
27250      timingInfo
27251    }) {
27252      if (!this.useDtsForTimestampOffset_) {
27253        return timingInfo.start;
27254      }
27255
27256      if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') {
27257        return videoTimingInfo.transmuxedDecodeStart;
27258      } // handle audio only
27259
27260
27261      if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') {
27262        return audioTimingInfo.transmuxedDecodeStart;
27263      } // handle content not transmuxed (e.g., MP4)
27264
27265
27266      return timingInfo.start;
27267    }
27268
27269    updateTimingInfoEnd_(segmentInfo) {
27270      segmentInfo.timingInfo = segmentInfo.timingInfo || {};
27271      const trackInfo = this.getMediaInfo_();
27272      const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo;
27273      const prioritizedTimingInfo = useVideoTimingInfo && segmentInfo.videoTimingInfo ? segmentInfo.videoTimingInfo : segmentInfo.audioTimingInfo;
27274
27275      if (!prioritizedTimingInfo) {
27276        return;
27277      }
27278
27279      segmentInfo.timingInfo.end = typeof prioritizedTimingInfo.end === 'number' ? // End time may not exist in a case where we aren't parsing the full segment (one
27280      // current example is the case of fmp4), so use the rough duration to calculate an
27281      // end time.
27282      prioritizedTimingInfo.end : prioritizedTimingInfo.start + segmentInfo.duration;
27283    }
27284    /**
27285     * callback to run when appendBuffer is finished. detects if we are
27286     * in a good state to do things with the data we got, or if we need
27287     * to wait for more
27288     *
27289     * @private
27290     */
27291
27292
27293    handleAppendsDone_() {
27294      // appendsdone can cause an abort
27295      if (this.pendingSegment_) {
27296        const metadata = {
27297          segmentInfo: segmentInfoPayload({
27298            type: this.loaderType_,
27299            segment: this.pendingSegment_
27300          })
27301        };
27302        this.trigger({
27303          type: 'appendsdone',
27304          metadata
27305        });
27306      }
27307
27308      if (!this.pendingSegment_) {
27309        this.state = 'READY'; // TODO should this move into this.checkForAbort to speed up requests post abort in
27310        // all appending cases?
27311
27312        if (!this.paused()) {
27313          this.monitorBuffer_();
27314        }
27315
27316        return;
27317      }
27318
27319      const segmentInfo = this.pendingSegment_;
27320
27321      if (segmentInfo.part && segmentInfo.part.syncInfo) {
27322        // low-latency flow
27323        segmentInfo.part.syncInfo.markAppended();
27324      } else if (segmentInfo.segment.syncInfo) {
27325        // normal flow
27326        segmentInfo.segment.syncInfo.markAppended();
27327      } // Now that the end of the segment has been reached, we can set the end time. It's
27328      // best to wait until all appends are done so we're sure that the primary media is
27329      // finished (and we have its end time).
27330
27331
27332      this.updateTimingInfoEnd_(segmentInfo);
27333
27334      if (this.shouldSaveSegmentTimingInfo_) {
27335        // Timeline mappings should only be saved for the main loader. This is for multiple
27336        // reasons:
27337        //
27338        // 1) Only one mapping is saved per timeline, meaning that if both the audio loader
27339        //    and the main loader try to save the timeline mapping, whichever comes later
27340        //    will overwrite the first. In theory this is OK, as the mappings should be the
27341        //    same, however, it breaks for (2)
27342        // 2) In the event of a live stream, the initial live point will make for a somewhat
27343        //    arbitrary mapping. If audio and video streams are not perfectly in-sync, then
27344        //    the mapping will be off for one of the streams, dependent on which one was
27345        //    first saved (see (1)).
27346        // 3) Primary timing goes by video in VHS, so the mapping should be video.
27347        //
27348        // Since the audio loader will wait for the main loader to load the first segment,
27349        // the main loader will save the first timeline mapping, and ensure that there won't
27350        // be a case where audio loads two segments without saving a mapping (thus leading
27351        // to missing segment timing info).
27352        this.syncController_.saveSegmentTimingInfo({
27353          segmentInfo,
27354          shouldSaveTimelineMapping: this.loaderType_ === 'main'
27355        });
27356      }
27357
27358      const segmentDurationMessage = getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_);
27359
27360      if (segmentDurationMessage) {
27361        if (segmentDurationMessage.severity === 'warn') {
27362          videojs__default["default"].log.warn(segmentDurationMessage.message);
27363        } else {
27364          this.logger_(segmentDurationMessage.message);
27365        }
27366      }
27367
27368      this.recordThroughput_(segmentInfo);
27369      this.pendingSegment_ = null;
27370      this.state = 'READY';
27371
27372      if (segmentInfo.isSyncRequest) {
27373        this.trigger('syncinfoupdate'); // if the sync request was not appended
27374        // then it was not the correct segment.
27375        // throw it away and use the data it gave us
27376        // to get the correct one.
27377
27378        if (!segmentInfo.hasAppendedData_) {
27379          this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`);
27380          return;
27381        }
27382      }
27383
27384      this.logger_(`Appended ${segmentInfoString(segmentInfo)}`);
27385      this.addSegmentMetadataCue_(segmentInfo);
27386      this.fetchAtBuffer_ = true;
27387
27388      if (this.currentTimeline_ !== segmentInfo.timeline) {
27389        this.timelineChangeController_.lastTimelineChange({
27390          type: this.loaderType_,
27391          from: this.currentTimeline_,
27392          to: segmentInfo.timeline
27393        }); // If audio is not disabled, the main segment loader is responsible for updating
27394        // the audio timeline as well. If the content is video only, this won't have any
27395        // impact.
27396
27397        if (this.loaderType_ === 'main' && !this.audioDisabled_) {
27398          this.timelineChangeController_.lastTimelineChange({
27399            type: 'audio',
27400            from: this.currentTimeline_,
27401            to: segmentInfo.timeline
27402          });
27403        }
27404      }
27405
27406      this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before
27407      // the following conditional otherwise it may consider this a bad "guess"
27408      // and attempt to resync when the post-update seekable window and live
27409      // point would mean that this was the perfect segment to fetch
27410
27411      this.trigger('syncinfoupdate');
27412      const segment = segmentInfo.segment;
27413      const part = segmentInfo.part;
27414      const badSegmentGuess = segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3;
27415      const badPartGuess = part && part.end && this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3; // If we previously appended a segment/part that ends more than 3 part/targetDurations before
27416      // the currentTime_ that means that our conservative guess was too conservative.
27417      // In that case, reset the loader state so that we try to use any information gained
27418      // from the previous request to create a new, more accurate, sync-point.
27419
27420      if (badSegmentGuess || badPartGuess) {
27421        this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`);
27422        this.resetEverything();
27423        return;
27424      }
27425
27426      const isWalkingForward = this.mediaIndex !== null; // Don't do a rendition switch unless we have enough time to get a sync segment
27427      // and conservatively guess
27428
27429      if (isWalkingForward) {
27430        this.trigger('bandwidthupdate');
27431      }
27432
27433      this.trigger('progress');
27434      this.mediaIndex = segmentInfo.mediaIndex;
27435      this.partIndex = segmentInfo.partIndex; // any time an update finishes and the last segment is in the
27436      // buffer, end the stream. this ensures the "ended" event will
27437      // fire if playback reaches that point.
27438
27439      if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) {
27440        this.endOfStream();
27441      } // used for testing
27442
27443
27444      this.trigger('appended');
27445
27446      if (segmentInfo.hasAppendedData_) {
27447        this.mediaAppends++;
27448      }
27449
27450      if (!this.paused()) {
27451        this.monitorBuffer_();
27452      }
27453    }
27454    /**
27455     * Records the current throughput of the decrypt, transmux, and append
27456     * portion of the semgment pipeline. `throughput.rate` is a the cumulative
27457     * moving average of the throughput. `throughput.count` is the number of
27458     * data points in the average.
27459     *
27460     * @private
27461     * @param {Object} segmentInfo the object returned by loadSegment
27462     */
27463
27464
27465    recordThroughput_(segmentInfo) {
27466      if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) {
27467        this.logger_(`Ignoring segment's throughput because its duration of ${segmentInfo.duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`);
27468        return;
27469      }
27470
27471      const rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide
27472      // by zero in the case where the throughput is ridiculously high
27473
27474      const segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second
27475
27476      const segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); // This is just a cumulative moving average calculation:
27477      //   newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
27478
27479      this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count;
27480    }
27481    /**
27482     * Adds a cue to the segment-metadata track with some metadata information about the
27483     * segment
27484     *
27485     * @private
27486     * @param {Object} segmentInfo
27487     *        the object returned by loadSegment
27488     * @method addSegmentMetadataCue_
27489     */
27490
27491
27492    addSegmentMetadataCue_(segmentInfo) {
27493      if (!this.segmentMetadataTrack_) {
27494        return;
27495      }
27496
27497      const segment = segmentInfo.segment;
27498      const start = segment.start;
27499      const end = segment.end; // Do not try adding the cue if the start and end times are invalid.
27500
27501      if (!finite(start) || !finite(end)) {
27502        return;
27503      }
27504
27505      removeCuesFromTrack(start, end, this.segmentMetadataTrack_);
27506      const Cue = window.WebKitDataCue || window.VTTCue;
27507      const value = {
27508        custom: segment.custom,
27509        dateTimeObject: segment.dateTimeObject,
27510        dateTimeString: segment.dateTimeString,
27511        programDateTime: segment.programDateTime,
27512        bandwidth: segmentInfo.playlist.attributes.BANDWIDTH,
27513        resolution: segmentInfo.playlist.attributes.RESOLUTION,
27514        codecs: segmentInfo.playlist.attributes.CODECS,
27515        byteLength: segmentInfo.byteLength,
27516        uri: segmentInfo.uri,
27517        timeline: segmentInfo.timeline,
27518        playlist: segmentInfo.playlist.id,
27519        start,
27520        end
27521      };
27522      const data = JSON.stringify(value);
27523      const cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between
27524      // the differences of WebKitDataCue in safari and VTTCue in other browsers
27525
27526      cue.value = value;
27527      this.segmentMetadataTrack_.addCue(cue);
27528    }
27529
27530  }
27531
27532  function noop() {}
27533
27534  const toTitleCase = function (string) {
27535    if (typeof string !== 'string') {
27536      return string;
27537    }
27538
27539    return string.replace(/./, w => w.toUpperCase());
27540  };
27541
27542  /**
27543   * @file source-updater.js
27544   */
27545  const bufferTypes = ['video', 'audio'];
27546
27547  const updating = (type, sourceUpdater) => {
27548    const sourceBuffer = sourceUpdater[`${type}Buffer`];
27549    return sourceBuffer && sourceBuffer.updating || sourceUpdater.queuePending[type];
27550  };
27551
27552  const nextQueueIndexOfType = (type, queue) => {
27553    for (let i = 0; i < queue.length; i++) {
27554      const queueEntry = queue[i];
27555
27556      if (queueEntry.type === 'mediaSource') {
27557        // If the next entry is a media source entry (uses multiple source buffers), block
27558        // processing to allow it to go through first.
27559        return null;
27560      }
27561
27562      if (queueEntry.type === type) {
27563        return i;
27564      }
27565    }
27566
27567    return null;
27568  };
27569
27570  const shiftQueue = (type, sourceUpdater) => {
27571    if (sourceUpdater.queue.length === 0) {
27572      return;
27573    }
27574
27575    let queueIndex = 0;
27576    let queueEntry = sourceUpdater.queue[queueIndex];
27577
27578    if (queueEntry.type === 'mediaSource') {
27579      if (!sourceUpdater.updating() && sourceUpdater.mediaSource.readyState !== 'closed') {
27580        sourceUpdater.queue.shift();
27581        queueEntry.action(sourceUpdater);
27582
27583        if (queueEntry.doneFn) {
27584          queueEntry.doneFn();
27585        } // Only specific source buffer actions must wait for async updateend events. Media
27586        // Source actions process synchronously. Therefore, both audio and video source
27587        // buffers are now clear to process the next queue entries.
27588
27589
27590        shiftQueue('audio', sourceUpdater);
27591        shiftQueue('video', sourceUpdater);
27592      } // Media Source actions require both source buffers, so if the media source action
27593      // couldn't process yet (because one or both source buffers are busy), block other
27594      // queue actions until both are available and the media source action can process.
27595
27596
27597      return;
27598    }
27599
27600    if (type === 'mediaSource') {
27601      // If the queue was shifted by a media source action (this happens when pushing a
27602      // media source action onto the queue), then it wasn't from an updateend event from an
27603      // audio or video source buffer, so there's no change from previous state, and no
27604      // processing should be done.
27605      return;
27606    } // Media source queue entries don't need to consider whether the source updater is
27607    // started (i.e., source buffers are created) as they don't need the source buffers, but
27608    // source buffer queue entries do.
27609
27610
27611    if (!sourceUpdater.ready() || sourceUpdater.mediaSource.readyState === 'closed' || updating(type, sourceUpdater)) {
27612      return;
27613    }
27614
27615    if (queueEntry.type !== type) {
27616      queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue);
27617
27618      if (queueIndex === null) {
27619        // Either there's no queue entry that uses this source buffer type in the queue, or
27620        // there's a media source queue entry before the next entry of this type, in which
27621        // case wait for that action to process first.
27622        return;
27623      }
27624
27625      queueEntry = sourceUpdater.queue[queueIndex];
27626    }
27627
27628    sourceUpdater.queue.splice(queueIndex, 1); // Keep a record that this source buffer type is in use.
27629    //
27630    // The queue pending operation must be set before the action is performed in the event
27631    // that the action results in a synchronous event that is acted upon. For instance, if
27632    // an exception is thrown that can be handled, it's possible that new actions will be
27633    // appended to an empty queue and immediately executed, but would not have the correct
27634    // pending information if this property was set after the action was performed.
27635
27636    sourceUpdater.queuePending[type] = queueEntry;
27637    queueEntry.action(type, sourceUpdater);
27638
27639    if (!queueEntry.doneFn) {
27640      // synchronous operation, process next entry
27641      sourceUpdater.queuePending[type] = null;
27642      shiftQueue(type, sourceUpdater);
27643      return;
27644    }
27645  };
27646
27647  const cleanupBuffer = (type, sourceUpdater) => {
27648    const buffer = sourceUpdater[`${type}Buffer`];
27649    const titleType = toTitleCase(type);
27650
27651    if (!buffer) {
27652      return;
27653    }
27654
27655    buffer.removeEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]);
27656    buffer.removeEventListener('error', sourceUpdater[`on${titleType}Error_`]);
27657    sourceUpdater.codecs[type] = null;
27658    sourceUpdater[`${type}Buffer`] = null;
27659  };
27660
27661  const inSourceBuffers = (mediaSource, sourceBuffer) => mediaSource && sourceBuffer && Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1;
27662
27663  const actions = {
27664    appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => {
27665      const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
27666      // or the media source does not contain this source buffer.
27667
27668      if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
27669        return;
27670      }
27671
27672      sourceUpdater.logger_(`Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer`);
27673
27674      try {
27675        sourceBuffer.appendBuffer(bytes);
27676      } catch (e) {
27677        sourceUpdater.logger_(`Error with code ${e.code} ` + (e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') + `when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer`);
27678        sourceUpdater.queuePending[type] = null;
27679        onError(e);
27680      }
27681    },
27682    remove: (start, end) => (type, sourceUpdater) => {
27683      const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
27684      // or the media source does not contain this source buffer.
27685
27686      if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
27687        return;
27688      }
27689
27690      sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`);
27691
27692      try {
27693        sourceBuffer.remove(start, end);
27694      } catch (e) {
27695        sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`);
27696      }
27697    },
27698    timestampOffset: offset => (type, sourceUpdater) => {
27699      const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
27700      // or the media source does not contain this source buffer.
27701
27702      if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
27703        return;
27704      }
27705
27706      sourceUpdater.logger_(`Setting ${type}timestampOffset to ${offset}`);
27707      sourceBuffer.timestampOffset = offset;
27708    },
27709    callback: callback => (type, sourceUpdater) => {
27710      callback();
27711    },
27712    endOfStream: error => sourceUpdater => {
27713      if (sourceUpdater.mediaSource.readyState !== 'open') {
27714        return;
27715      }
27716
27717      sourceUpdater.logger_(`Calling mediaSource endOfStream(${error || ''})`);
27718
27719      try {
27720        sourceUpdater.mediaSource.endOfStream(error);
27721      } catch (e) {
27722        videojs__default["default"].log.warn('Failed to call media source endOfStream', e);
27723      }
27724    },
27725    duration: duration => sourceUpdater => {
27726      sourceUpdater.logger_(`Setting mediaSource duration to ${duration}`);
27727
27728      try {
27729        sourceUpdater.mediaSource.duration = duration;
27730      } catch (e) {
27731        videojs__default["default"].log.warn('Failed to set media source duration', e);
27732      }
27733    },
27734    abort: () => (type, sourceUpdater) => {
27735      if (sourceUpdater.mediaSource.readyState !== 'open') {
27736        return;
27737      }
27738
27739      const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null
27740      // or the media source does not contain this source buffer.
27741
27742      if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
27743        return;
27744      }
27745
27746      sourceUpdater.logger_(`calling abort on ${type}Buffer`);
27747
27748      try {
27749        sourceBuffer.abort();
27750      } catch (e) {
27751        videojs__default["default"].log.warn(`Failed to abort on ${type}Buffer`, e);
27752      }
27753    },
27754    addSourceBuffer: (type, codec) => sourceUpdater => {
27755      const titleType = toTitleCase(type);
27756      const mime = getMimeForCodec(codec);
27757      sourceUpdater.logger_(`Adding ${type}Buffer with codec ${codec} to mediaSource`);
27758      const sourceBuffer = sourceUpdater.mediaSource.addSourceBuffer(mime);
27759      sourceBuffer.addEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]);
27760      sourceBuffer.addEventListener('error', sourceUpdater[`on${titleType}Error_`]);
27761      sourceUpdater.codecs[type] = codec;
27762      sourceUpdater[`${type}Buffer`] = sourceBuffer;
27763    },
27764    removeSourceBuffer: type => sourceUpdater => {
27765      const sourceBuffer = sourceUpdater[`${type}Buffer`];
27766      cleanupBuffer(type, sourceUpdater); // can't do anything if the media source / source buffer is null
27767      // or the media source does not contain this source buffer.
27768
27769      if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
27770        return;
27771      }
27772
27773      sourceUpdater.logger_(`Removing ${type}Buffer with codec ${sourceUpdater.codecs[type]} from mediaSource`);
27774
27775      try {
27776        sourceUpdater.mediaSource.removeSourceBuffer(sourceBuffer);
27777      } catch (e) {
27778        videojs__default["default"].log.warn(`Failed to removeSourceBuffer ${type}Buffer`, e);
27779      }
27780    },
27781    changeType: codec => (type, sourceUpdater) => {
27782      const sourceBuffer = sourceUpdater[`${type}Buffer`];
27783      const mime = getMimeForCodec(codec); // can't do anything if the media source / source buffer is null
27784      // or the media source does not contain this source buffer.
27785
27786      if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) {
27787        return;
27788      } // do not update codec if we don't need to.
27789      // Only update if we change the codec base.
27790      // For example, going from avc1.640028 to avc1.64001f does not require a changeType call.
27791
27792
27793      const newCodecBase = codec.substring(0, codec.indexOf('.'));
27794      const oldCodec = sourceUpdater.codecs[type];
27795      const oldCodecBase = oldCodec.substring(0, oldCodec.indexOf('.'));
27796
27797      if (oldCodecBase === newCodecBase) {
27798        return;
27799      }
27800
27801      const metadata = {
27802        codecsChangeInfo: {
27803          from: oldCodec,
27804          to: codec
27805        }
27806      };
27807      sourceUpdater.trigger({
27808        type: 'codecschange',
27809        metadata
27810      });
27811      sourceUpdater.logger_(`changing ${type}Buffer codec from ${oldCodec} to ${codec}`); // check if change to the provided type is supported
27812
27813      try {
27814        sourceBuffer.changeType(mime);
27815        sourceUpdater.codecs[type] = codec;
27816      } catch (e) {
27817        metadata.errorType = videojs__default["default"].Error.StreamingCodecsChangeError;
27818        metadata.error = e;
27819        e.metadata = metadata;
27820        sourceUpdater.error_ = e;
27821        sourceUpdater.trigger('error');
27822        videojs__default["default"].log.warn(`Failed to changeType on ${type}Buffer`, e);
27823      }
27824    }
27825  };
27826
27827  const pushQueue = ({
27828    type,
27829    sourceUpdater,
27830    action,
27831    doneFn,
27832    name
27833  }) => {
27834    sourceUpdater.queue.push({
27835      type,
27836      action,
27837      doneFn,
27838      name
27839    });
27840    shiftQueue(type, sourceUpdater);
27841  };
27842
27843  const onUpdateend = (type, sourceUpdater) => e => {
27844    // Although there should, in theory, be a pending action for any updateend receieved,
27845    // there are some actions that may trigger updateend events without set definitions in
27846    // the w3c spec. For instance, setting the duration on the media source may trigger
27847    // updateend events on source buffers. This does not appear to be in the spec. As such,
27848    // if we encounter an updateend without a corresponding pending action from our queue
27849    // for that source buffer type, process the next action.
27850    const bufferedRangesForType = sourceUpdater[`${type}Buffered`]();
27851    const descriptiveString = bufferedRangesToString(bufferedRangesForType);
27852    sourceUpdater.logger_(`received "updateend" event for ${type} Source Buffer: `, descriptiveString);
27853
27854    if (sourceUpdater.queuePending[type]) {
27855      const doneFn = sourceUpdater.queuePending[type].doneFn;
27856      sourceUpdater.queuePending[type] = null;
27857
27858      if (doneFn) {
27859        // if there's an error, report it
27860        doneFn(sourceUpdater[`${type}Error_`]);
27861      }
27862    }
27863
27864    shiftQueue(type, sourceUpdater);
27865  };
27866  /**
27867   * A queue of callbacks to be serialized and applied when a
27868   * MediaSource and its associated SourceBuffers are not in the
27869   * updating state. It is used by the segment loader to update the
27870   * underlying SourceBuffers when new data is loaded, for instance.
27871   *
27872   * @class SourceUpdater
27873   * @param {MediaSource} mediaSource the MediaSource to create the SourceBuffer from
27874   * @param {string} mimeType the desired MIME type of the underlying SourceBuffer
27875   */
27876
27877
27878  class SourceUpdater extends videojs__default["default"].EventTarget {
27879    constructor(mediaSource) {
27880      super();
27881      this.mediaSource = mediaSource;
27882
27883      this.sourceopenListener_ = () => shiftQueue('mediaSource', this);
27884
27885      this.mediaSource.addEventListener('sourceopen', this.sourceopenListener_);
27886      this.logger_ = logger('SourceUpdater'); // initial timestamp offset is 0
27887
27888      this.audioTimestampOffset_ = 0;
27889      this.videoTimestampOffset_ = 0;
27890      this.queue = [];
27891      this.queuePending = {
27892        audio: null,
27893        video: null
27894      };
27895      this.delayedAudioAppendQueue_ = [];
27896      this.videoAppendQueued_ = false;
27897      this.codecs = {};
27898      this.onVideoUpdateEnd_ = onUpdateend('video', this);
27899      this.onAudioUpdateEnd_ = onUpdateend('audio', this);
27900
27901      this.onVideoError_ = e => {
27902        // used for debugging
27903        this.videoError_ = e;
27904      };
27905
27906      this.onAudioError_ = e => {
27907        // used for debugging
27908        this.audioError_ = e;
27909      };
27910
27911      this.createdSourceBuffers_ = false;
27912      this.initializedEme_ = false;
27913      this.triggeredReady_ = false;
27914    }
27915
27916    initializedEme() {
27917      this.initializedEme_ = true;
27918      this.triggerReady();
27919    }
27920
27921    hasCreatedSourceBuffers() {
27922      // if false, likely waiting on one of the segment loaders to get enough data to create
27923      // source buffers
27924      return this.createdSourceBuffers_;
27925    }
27926
27927    hasInitializedAnyEme() {
27928      return this.initializedEme_;
27929    }
27930
27931    ready() {
27932      return this.hasCreatedSourceBuffers() && this.hasInitializedAnyEme();
27933    }
27934
27935    createSourceBuffers(codecs) {
27936      if (this.hasCreatedSourceBuffers()) {
27937        // already created them before
27938        return;
27939      } // the intial addOrChangeSourceBuffers will always be
27940      // two add buffers.
27941
27942
27943      this.addOrChangeSourceBuffers(codecs);
27944      this.createdSourceBuffers_ = true;
27945      this.trigger('createdsourcebuffers');
27946      this.triggerReady();
27947    }
27948
27949    triggerReady() {
27950      // only allow ready to be triggered once, this prevents the case
27951      // where:
27952      // 1. we trigger createdsourcebuffers
27953      // 2. ie 11 synchronously initializates eme
27954      // 3. the synchronous initialization causes us to trigger ready
27955      // 4. We go back to the ready check in createSourceBuffers and ready is triggered again.
27956      if (this.ready() && !this.triggeredReady_) {
27957        this.triggeredReady_ = true;
27958        this.trigger('ready');
27959      }
27960    }
27961    /**
27962     * Add a type of source buffer to the media source.
27963     *
27964     * @param {string} type
27965     *        The type of source buffer to add.
27966     *
27967     * @param {string} codec
27968     *        The codec to add the source buffer with.
27969     */
27970
27971
27972    addSourceBuffer(type, codec) {
27973      pushQueue({
27974        type: 'mediaSource',
27975        sourceUpdater: this,
27976        action: actions.addSourceBuffer(type, codec),
27977        name: 'addSourceBuffer'
27978      });
27979    }
27980    /**
27981     * call abort on a source buffer.
27982     *
27983     * @param {string} type
27984     *        The type of source buffer to call abort on.
27985     */
27986
27987
27988    abort(type) {
27989      pushQueue({
27990        type,
27991        sourceUpdater: this,
27992        action: actions.abort(type),
27993        name: 'abort'
27994      });
27995    }
27996    /**
27997     * Call removeSourceBuffer and remove a specific type
27998     * of source buffer on the mediaSource.
27999     *
28000     * @param {string} type
28001     *        The type of source buffer to remove.
28002     */
28003
28004
28005    removeSourceBuffer(type) {
28006      if (!this.canRemoveSourceBuffer()) {
28007        videojs__default["default"].log.error('removeSourceBuffer is not supported!');
28008        return;
28009      }
28010
28011      pushQueue({
28012        type: 'mediaSource',
28013        sourceUpdater: this,
28014        action: actions.removeSourceBuffer(type),
28015        name: 'removeSourceBuffer'
28016      });
28017    }
28018    /**
28019     * Whether or not the removeSourceBuffer function is supported
28020     * on the mediaSource.
28021     *
28022     * @return {boolean}
28023     *          if removeSourceBuffer can be called.
28024     */
28025
28026
28027    canRemoveSourceBuffer() {
28028      // As of Firefox 83 removeSourceBuffer
28029      // throws errors, so we report that it does not support this.
28030      return !videojs__default["default"].browser.IS_FIREFOX && window.MediaSource && window.MediaSource.prototype && typeof window.MediaSource.prototype.removeSourceBuffer === 'function';
28031    }
28032    /**
28033     * Whether or not the changeType function is supported
28034     * on our SourceBuffers.
28035     *
28036     * @return {boolean}
28037     *         if changeType can be called.
28038     */
28039
28040
28041    static canChangeType() {
28042      return window.SourceBuffer && window.SourceBuffer.prototype && typeof window.SourceBuffer.prototype.changeType === 'function';
28043    }
28044    /**
28045     * Whether or not the changeType function is supported
28046     * on our SourceBuffers.
28047     *
28048     * @return {boolean}
28049     *         if changeType can be called.
28050     */
28051
28052
28053    canChangeType() {
28054      return this.constructor.canChangeType();
28055    }
28056    /**
28057     * Call the changeType function on a source buffer, given the code and type.
28058     *
28059     * @param {string} type
28060     *        The type of source buffer to call changeType on.
28061     *
28062     * @param {string} codec
28063     *        The codec string to change type with on the source buffer.
28064     */
28065
28066
28067    changeType(type, codec) {
28068      if (!this.canChangeType()) {
28069        videojs__default["default"].log.error('changeType is not supported!');
28070        return;
28071      }
28072
28073      pushQueue({
28074        type,
28075        sourceUpdater: this,
28076        action: actions.changeType(codec),
28077        name: 'changeType'
28078      });
28079    }
28080    /**
28081     * Add source buffers with a codec or, if they are already created,
28082     * call changeType on source buffers using changeType.
28083     *
28084     * @param {Object} codecs
28085     *        Codecs to switch to
28086     */
28087
28088
28089    addOrChangeSourceBuffers(codecs) {
28090      if (!codecs || typeof codecs !== 'object' || Object.keys(codecs).length === 0) {
28091        throw new Error('Cannot addOrChangeSourceBuffers to undefined codecs');
28092      }
28093
28094      Object.keys(codecs).forEach(type => {
28095        const codec = codecs[type];
28096
28097        if (!this.hasCreatedSourceBuffers()) {
28098          return this.addSourceBuffer(type, codec);
28099        }
28100
28101        if (this.canChangeType()) {
28102          this.changeType(type, codec);
28103        }
28104      });
28105    }
28106    /**
28107     * Queue an update to append an ArrayBuffer.
28108     *
28109     * @param {MediaObject} object containing audioBytes and/or videoBytes
28110     * @param {Function} done the function to call when done
28111     * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
28112     */
28113
28114
28115    appendBuffer(options, doneFn) {
28116      const {
28117        segmentInfo,
28118        type,
28119        bytes
28120      } = options;
28121      this.processedAppend_ = true;
28122
28123      if (type === 'audio' && this.videoBuffer && !this.videoAppendQueued_) {
28124        this.delayedAudioAppendQueue_.push([options, doneFn]);
28125        this.logger_(`delayed audio append of ${bytes.length} until video append`);
28126        return;
28127      } // In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will
28128      // not be fired. This means that the queue will be blocked until the next action
28129      // taken by the segment-loader. Provide a mechanism for segment-loader to handle
28130      // these errors by calling the doneFn with the specific error.
28131
28132
28133      const onError = doneFn;
28134      pushQueue({
28135        type,
28136        sourceUpdater: this,
28137        action: actions.appendBuffer(bytes, segmentInfo || {
28138          mediaIndex: -1
28139        }, onError),
28140        doneFn,
28141        name: 'appendBuffer'
28142      });
28143
28144      if (type === 'video') {
28145        this.videoAppendQueued_ = true;
28146
28147        if (!this.delayedAudioAppendQueue_.length) {
28148          return;
28149        }
28150
28151        const queue = this.delayedAudioAppendQueue_.slice();
28152        this.logger_(`queuing delayed audio ${queue.length} appendBuffers`);
28153        this.delayedAudioAppendQueue_.length = 0;
28154        queue.forEach(que => {
28155          this.appendBuffer.apply(this, que);
28156        });
28157      }
28158    }
28159    /**
28160     * Get the audio buffer's buffered timerange.
28161     *
28162     * @return {TimeRange}
28163     *         The audio buffer's buffered time range
28164     */
28165
28166
28167    audioBuffered() {
28168      // no media source/source buffer or it isn't in the media sources
28169      // source buffer list
28170      if (!inSourceBuffers(this.mediaSource, this.audioBuffer)) {
28171        return createTimeRanges();
28172      }
28173
28174      return this.audioBuffer.buffered ? this.audioBuffer.buffered : createTimeRanges();
28175    }
28176    /**
28177     * Get the video buffer's buffered timerange.
28178     *
28179     * @return {TimeRange}
28180     *         The video buffer's buffered time range
28181     */
28182
28183
28184    videoBuffered() {
28185      // no media source/source buffer or it isn't in the media sources
28186      // source buffer list
28187      if (!inSourceBuffers(this.mediaSource, this.videoBuffer)) {
28188        return createTimeRanges();
28189      }
28190
28191      return this.videoBuffer.buffered ? this.videoBuffer.buffered : createTimeRanges();
28192    }
28193    /**
28194     * Get a combined video/audio buffer's buffered timerange.
28195     *
28196     * @return {TimeRange}
28197     *         the combined time range
28198     */
28199
28200
28201    buffered() {
28202      const video = inSourceBuffers(this.mediaSource, this.videoBuffer) ? this.videoBuffer : null;
28203      const audio = inSourceBuffers(this.mediaSource, this.audioBuffer) ? this.audioBuffer : null;
28204
28205      if (audio && !video) {
28206        return this.audioBuffered();
28207      }
28208
28209      if (video && !audio) {
28210        return this.videoBuffered();
28211      }
28212
28213      return bufferIntersection(this.audioBuffered(), this.videoBuffered());
28214    }
28215    /**
28216     * Add a callback to the queue that will set duration on the mediaSource.
28217     *
28218     * @param {number} duration
28219     *        The duration to set
28220     *
28221     * @param {Function} [doneFn]
28222     *        function to run after duration has been set.
28223     */
28224
28225
28226    setDuration(duration, doneFn = noop) {
28227      // In order to set the duration on the media source, it's necessary to wait for all
28228      // source buffers to no longer be updating. "If the updating attribute equals true on
28229      // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and
28230      // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes).
28231      pushQueue({
28232        type: 'mediaSource',
28233        sourceUpdater: this,
28234        action: actions.duration(duration),
28235        name: 'duration',
28236        doneFn
28237      });
28238    }
28239    /**
28240     * Add a mediaSource endOfStream call to the queue
28241     *
28242     * @param {Error} [error]
28243     *        Call endOfStream with an error
28244     *
28245     * @param {Function} [doneFn]
28246     *        A function that should be called when the
28247     *        endOfStream call has finished.
28248     */
28249
28250
28251    endOfStream(error = null, doneFn = noop) {
28252      if (typeof error !== 'string') {
28253        error = undefined;
28254      } // In order to set the duration on the media source, it's necessary to wait for all
28255      // source buffers to no longer be updating. "If the updating attribute equals true on
28256      // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and
28257      // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes).
28258
28259
28260      pushQueue({
28261        type: 'mediaSource',
28262        sourceUpdater: this,
28263        action: actions.endOfStream(error),
28264        name: 'endOfStream',
28265        doneFn
28266      });
28267    }
28268    /**
28269     * Queue an update to remove a time range from the buffer.
28270     *
28271     * @param {number} start where to start the removal
28272     * @param {number} end where to end the removal
28273     * @param {Function} [done=noop] optional callback to be executed when the remove
28274     * operation is complete
28275     * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
28276     */
28277
28278
28279    removeAudio(start, end, done = noop) {
28280      if (!this.audioBuffered().length || this.audioBuffered().end(0) === 0) {
28281        done();
28282        return;
28283      }
28284
28285      pushQueue({
28286        type: 'audio',
28287        sourceUpdater: this,
28288        action: actions.remove(start, end),
28289        doneFn: done,
28290        name: 'remove'
28291      });
28292    }
28293    /**
28294     * Queue an update to remove a time range from the buffer.
28295     *
28296     * @param {number} start where to start the removal
28297     * @param {number} end where to end the removal
28298     * @param {Function} [done=noop] optional callback to be executed when the remove
28299     * operation is complete
28300     * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
28301     */
28302
28303
28304    removeVideo(start, end, done = noop) {
28305      if (!this.videoBuffered().length || this.videoBuffered().end(0) === 0) {
28306        done();
28307        return;
28308      }
28309
28310      pushQueue({
28311        type: 'video',
28312        sourceUpdater: this,
28313        action: actions.remove(start, end),
28314        doneFn: done,
28315        name: 'remove'
28316      });
28317    }
28318    /**
28319     * Whether the underlying sourceBuffer is updating or not
28320     *
28321     * @return {boolean} the updating status of the SourceBuffer
28322     */
28323
28324
28325    updating() {
28326      // the audio/video source buffer is updating
28327      if (updating('audio', this) || updating('video', this)) {
28328        return true;
28329      }
28330
28331      return false;
28332    }
28333    /**
28334     * Set/get the timestampoffset on the audio SourceBuffer
28335     *
28336     * @return {number} the timestamp offset
28337     */
28338
28339
28340    audioTimestampOffset(offset) {
28341      if (typeof offset !== 'undefined' && this.audioBuffer && // no point in updating if it's the same
28342      this.audioTimestampOffset_ !== offset) {
28343        pushQueue({
28344          type: 'audio',
28345          sourceUpdater: this,
28346          action: actions.timestampOffset(offset),
28347          name: 'timestampOffset'
28348        });
28349        this.audioTimestampOffset_ = offset;
28350      }
28351
28352      return this.audioTimestampOffset_;
28353    }
28354    /**
28355     * Set/get the timestampoffset on the video SourceBuffer
28356     *
28357     * @return {number} the timestamp offset
28358     */
28359
28360
28361    videoTimestampOffset(offset) {
28362      if (typeof offset !== 'undefined' && this.videoBuffer && // no point in updating if it's the same
28363      this.videoTimestampOffset_ !== offset) {
28364        pushQueue({
28365          type: 'video',
28366          sourceUpdater: this,
28367          action: actions.timestampOffset(offset),
28368          name: 'timestampOffset'
28369        });
28370        this.videoTimestampOffset_ = offset;
28371      }
28372
28373      return this.videoTimestampOffset_;
28374    }
28375    /**
28376     * Add a function to the queue that will be called
28377     * when it is its turn to run in the audio queue.
28378     *
28379     * @param {Function} callback
28380     *        The callback to queue.
28381     */
28382
28383
28384    audioQueueCallback(callback) {
28385      if (!this.audioBuffer) {
28386        return;
28387      }
28388
28389      pushQueue({
28390        type: 'audio',
28391        sourceUpdater: this,
28392        action: actions.callback(callback),
28393        name: 'callback'
28394      });
28395    }
28396    /**
28397     * Add a function to the queue that will be called
28398     * when it is its turn to run in the video queue.
28399     *
28400     * @param {Function} callback
28401     *        The callback to queue.
28402     */
28403
28404
28405    videoQueueCallback(callback) {
28406      if (!this.videoBuffer) {
28407        return;
28408      }
28409
28410      pushQueue({
28411        type: 'video',
28412        sourceUpdater: this,
28413        action: actions.callback(callback),
28414        name: 'callback'
28415      });
28416    }
28417    /**
28418     * dispose of the source updater and the underlying sourceBuffer
28419     */
28420
28421
28422    dispose() {
28423      this.trigger('dispose');
28424      bufferTypes.forEach(type => {
28425        this.abort(type);
28426
28427        if (this.canRemoveSourceBuffer()) {
28428          this.removeSourceBuffer(type);
28429        } else {
28430          this[`${type}QueueCallback`](() => cleanupBuffer(type, this));
28431        }
28432      });
28433      this.videoAppendQueued_ = false;
28434      this.delayedAudioAppendQueue_.length = 0;
28435
28436      if (this.sourceopenListener_) {
28437        this.mediaSource.removeEventListener('sourceopen', this.sourceopenListener_);
28438      }
28439
28440      this.off();
28441    }
28442
28443  }
28444
28445  const uint8ToUtf8 = uintArray => decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray)));
28446  const bufferToHexString = buffer => {
28447    const uInt8Buffer = new Uint8Array(buffer);
28448    return Array.from(uInt8Buffer).map(byte => byte.toString(16).padStart(2, '0')).join('');
28449  };
28450
28451  /**
28452   * @file vtt-segment-loader.js
28453   */
28454  const VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0)));
28455
28456  class NoVttJsError extends Error {
28457    constructor() {
28458      super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.');
28459    }
28460
28461  }
28462  /**
28463   * An object that manages segment loading and appending.
28464   *
28465   * @class VTTSegmentLoader
28466   * @param {Object} options required and optional options
28467   * @extends videojs.EventTarget
28468   */
28469
28470
28471  class VTTSegmentLoader extends SegmentLoader {
28472    constructor(settings, options = {}) {
28473      super(settings, options); // SegmentLoader requires a MediaSource be specified or it will throw an error;
28474      // however, VTTSegmentLoader has no need of a media source, so delete the reference
28475
28476      this.mediaSource_ = null;
28477      this.subtitlesTrack_ = null;
28478      this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks;
28479      this.loadVttJs = settings.loadVttJs; // The VTT segment will have its own time mappings. Saving VTT segment timing info in
28480      // the sync controller leads to improper behavior.
28481
28482      this.shouldSaveSegmentTimingInfo_ = false;
28483    }
28484    /**
28485     * Indicates which time ranges are buffered
28486     *
28487     * @return {TimeRange}
28488     *         TimeRange object representing the current buffered ranges
28489     */
28490
28491
28492    buffered_() {
28493      if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) {
28494        return createTimeRanges();
28495      }
28496
28497      const cues = this.subtitlesTrack_.cues;
28498      const start = cues[0].startTime;
28499      const end = cues[cues.length - 1].startTime;
28500      return createTimeRanges([[start, end]]);
28501    }
28502    /**
28503     * Gets and sets init segment for the provided map
28504     *
28505     * @param {Object} map
28506     *        The map object representing the init segment to get or set
28507     * @param {boolean=} set
28508     *        If true, the init segment for the provided map should be saved
28509     * @return {Object}
28510     *         map object for desired init segment
28511     */
28512
28513
28514    initSegmentForMap(map, set = false) {
28515      if (!map) {
28516        return null;
28517      }
28518
28519      const id = initSegmentId(map);
28520      let storedMap = this.initSegments_[id];
28521
28522      if (set && !storedMap && map.bytes) {
28523        // append WebVTT line terminators to the media initialization segment if it exists
28524        // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that
28525        // requires two or more WebVTT line terminators between the WebVTT header and the
28526        // rest of the file
28527        const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength;
28528        const combinedSegment = new Uint8Array(combinedByteLength);
28529        combinedSegment.set(map.bytes);
28530        combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength);
28531        this.initSegments_[id] = storedMap = {
28532          resolvedUri: map.resolvedUri,
28533          byterange: map.byterange,
28534          bytes: combinedSegment
28535        };
28536      }
28537
28538      return storedMap || map;
28539    }
28540    /**
28541     * Returns true if all configuration required for loading is present, otherwise false.
28542     *
28543     * @return {boolean} True if the all configuration is ready for loading
28544     * @private
28545     */
28546
28547
28548    couldBeginLoading_() {
28549      return this.playlist_ && this.subtitlesTrack_ && !this.paused();
28550    }
28551    /**
28552     * Once all the starting parameters have been specified, begin
28553     * operation. This method should only be invoked from the INIT
28554     * state.
28555     *
28556     * @private
28557     */
28558
28559
28560    init_() {
28561      this.state = 'READY';
28562      this.resetEverything();
28563      return this.monitorBuffer_();
28564    }
28565    /**
28566     * Set a subtitle track on the segment loader to add subtitles to
28567     *
28568     * @param {TextTrack=} track
28569     *        The text track to add loaded subtitles to
28570     * @return {TextTrack}
28571     *        Returns the subtitles track
28572     */
28573
28574
28575    track(track) {
28576      if (typeof track === 'undefined') {
28577        return this.subtitlesTrack_;
28578      }
28579
28580      this.subtitlesTrack_ = track; // if we were unpaused but waiting for a sourceUpdater, start
28581      // buffering now
28582
28583      if (this.state === 'INIT' && this.couldBeginLoading_()) {
28584        this.init_();
28585      }
28586
28587      return this.subtitlesTrack_;
28588    }
28589    /**
28590     * Remove any data in the source buffer between start and end times
28591     *
28592     * @param {number} start - the start time of the region to remove from the buffer
28593     * @param {number} end - the end time of the region to remove from the buffer
28594     */
28595
28596
28597    remove(start, end) {
28598      removeCuesFromTrack(start, end, this.subtitlesTrack_);
28599    }
28600    /**
28601     * fill the buffer with segements unless the sourceBuffers are
28602     * currently updating
28603     *
28604     * Note: this function should only ever be called by monitorBuffer_
28605     * and never directly
28606     *
28607     * @private
28608     */
28609
28610
28611    fillBuffer_() {
28612      // see if we need to begin loading immediately
28613      const segmentInfo = this.chooseNextRequest_();
28614
28615      if (!segmentInfo) {
28616        return;
28617      }
28618
28619      if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) {
28620        // We don't have the timestamp offset that we need to sync subtitles.
28621        // Rerun on a timestamp offset or user interaction.
28622        const checkTimestampOffset = () => {
28623          this.state = 'READY';
28624
28625          if (!this.paused()) {
28626            // if not paused, queue a buffer check as soon as possible
28627            this.monitorBuffer_();
28628          }
28629        };
28630
28631        this.syncController_.one('timestampoffset', checkTimestampOffset);
28632        this.state = 'WAITING_ON_TIMELINE';
28633        return;
28634      }
28635
28636      this.loadSegment_(segmentInfo);
28637    } // never set a timestamp offset for vtt segments.
28638
28639
28640    timestampOffsetForSegment_() {
28641      return null;
28642    }
28643
28644    chooseNextRequest_() {
28645      return this.skipEmptySegments_(super.chooseNextRequest_());
28646    }
28647    /**
28648     * Prevents the segment loader from requesting segments we know contain no subtitles
28649     * by walking forward until we find the next segment that we don't know whether it is
28650     * empty or not.
28651     *
28652     * @param {Object} segmentInfo
28653     *        a segment info object that describes the current segment
28654     * @return {Object}
28655     *         a segment info object that describes the current segment
28656     */
28657
28658
28659    skipEmptySegments_(segmentInfo) {
28660      while (segmentInfo && segmentInfo.segment.empty) {
28661        // stop at the last possible segmentInfo
28662        if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) {
28663          segmentInfo = null;
28664          break;
28665        }
28666
28667        segmentInfo = this.generateSegmentInfo_({
28668          playlist: segmentInfo.playlist,
28669          mediaIndex: segmentInfo.mediaIndex + 1,
28670          startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration,
28671          isSyncRequest: segmentInfo.isSyncRequest
28672        });
28673      }
28674
28675      return segmentInfo;
28676    }
28677
28678    stopForError(error) {
28679      this.error(error);
28680      this.state = 'READY';
28681      this.pause();
28682      this.trigger('error');
28683    }
28684    /**
28685     * append a decrypted segement to the SourceBuffer through a SourceUpdater
28686     *
28687     * @private
28688     */
28689
28690
28691    segmentRequestFinished_(error, simpleSegment, result) {
28692      if (!this.subtitlesTrack_) {
28693        this.state = 'READY';
28694        return;
28695      }
28696
28697      this.saveTransferStats_(simpleSegment.stats); // the request was aborted
28698
28699      if (!this.pendingSegment_) {
28700        this.state = 'READY';
28701        this.mediaRequestsAborted += 1;
28702        return;
28703      }
28704
28705      if (error) {
28706        if (error.code === REQUEST_ERRORS.TIMEOUT) {
28707          this.handleTimeout_();
28708        }
28709
28710        if (error.code === REQUEST_ERRORS.ABORTED) {
28711          this.mediaRequestsAborted += 1;
28712        } else {
28713          this.mediaRequestsErrored += 1;
28714        }
28715
28716        this.stopForError(error);
28717        return;
28718      }
28719
28720      const segmentInfo = this.pendingSegment_;
28721      const isMp4WebVttSegmentWithCues = result.mp4VttCues && result.mp4VttCues.length;
28722
28723      if (isMp4WebVttSegmentWithCues) {
28724        segmentInfo.mp4VttCues = result.mp4VttCues;
28725      } // although the VTT segment loader bandwidth isn't really used, it's good to
28726      // maintain functionality between segment loaders
28727
28728
28729      this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); // if this request included a segment key, save that data in the cache
28730
28731      if (simpleSegment.key) {
28732        this.segmentKey(simpleSegment.key, true);
28733      }
28734
28735      this.state = 'APPENDING'; // used for tests
28736
28737      this.trigger('appending');
28738      const segment = segmentInfo.segment;
28739
28740      if (segment.map) {
28741        segment.map.bytes = simpleSegment.map.bytes;
28742      }
28743
28744      segmentInfo.bytes = simpleSegment.bytes; // Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading
28745
28746      if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') {
28747        this.state = 'WAITING_ON_VTTJS'; // should be fine to call multiple times
28748        // script will be loaded once but multiple listeners will be added to the queue, which is expected.
28749
28750        this.loadVttJs().then(() => this.segmentRequestFinished_(error, simpleSegment, result), () => this.stopForError({
28751          message: 'Error loading vtt.js'
28752        }));
28753        return;
28754      }
28755
28756      segment.requested = true;
28757
28758      try {
28759        this.parseVTTCues_(segmentInfo);
28760      } catch (e) {
28761        this.stopForError({
28762          message: e.message,
28763          metadata: {
28764            errorType: videojs__default["default"].Error.StreamingVttParserError,
28765            error: e
28766          }
28767        });
28768        return;
28769      }
28770
28771      if (!isMp4WebVttSegmentWithCues) {
28772        this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_);
28773      }
28774
28775      if (segmentInfo.cues.length) {
28776        segmentInfo.timingInfo = {
28777          start: segmentInfo.cues[0].startTime,
28778          end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime
28779        };
28780      } else {
28781        segmentInfo.timingInfo = {
28782          start: segmentInfo.startOfSegment,
28783          end: segmentInfo.startOfSegment + segmentInfo.duration
28784        };
28785      }
28786
28787      if (segmentInfo.isSyncRequest) {
28788        this.trigger('syncinfoupdate');
28789        this.pendingSegment_ = null;
28790        this.state = 'READY';
28791        return;
28792      }
28793
28794      segmentInfo.byteLength = segmentInfo.bytes.byteLength;
28795      this.mediaSecondsLoaded += segment.duration; // Create VTTCue instances for each cue in the new segment and add them to
28796      // the subtitle track
28797
28798      segmentInfo.cues.forEach(cue => {
28799        this.subtitlesTrack_.addCue(this.featuresNativeTextTracks_ ? new window.VTTCue(cue.startTime, cue.endTime, cue.text) : cue);
28800      }); // Remove any duplicate cues from the subtitle track. The WebVTT spec allows
28801      // cues to have identical time-intervals, but if the text is also identical
28802      // we can safely assume it is a duplicate that can be removed (ex. when a cue
28803      // "overlaps" VTT segments)
28804
28805      removeDuplicateCuesFromTrack(this.subtitlesTrack_);
28806      this.handleAppendsDone_();
28807    }
28808
28809    handleData_(simpleSegment, result) {
28810      const isVttType = simpleSegment && simpleSegment.type === 'vtt';
28811      const isTextResult = result && result.type === 'text';
28812      const isFmp4VttSegment = isVttType && isTextResult; // handle segment data for fmp4 encapsulated webvtt
28813
28814      if (isFmp4VttSegment) {
28815        super.handleData_(simpleSegment, result);
28816      }
28817    }
28818
28819    updateTimingInfoEnd_() {// noop
28820    }
28821    /**
28822     * Utility function for converting mp4 webvtt cue objects into VTTCues.
28823     *
28824     * @param {Object} segmentInfo with mp4 webvtt cues for parsing into VTTCue objecs
28825     */
28826
28827
28828    parseMp4VttCues_(segmentInfo) {
28829      const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset();
28830      segmentInfo.mp4VttCues.forEach(cue => {
28831        const start = cue.start + timestampOffset;
28832        const end = cue.end + timestampOffset;
28833        const vttCue = new window.VTTCue(start, end, cue.cueText);
28834
28835        if (cue.settings) {
28836          cue.settings.split(' ').forEach(cueSetting => {
28837            const keyValString = cueSetting.split(':');
28838            const key = keyValString[0];
28839            const value = keyValString[1];
28840            vttCue[key] = isNaN(value) ? value : Number(value);
28841          });
28842        }
28843
28844        segmentInfo.cues.push(vttCue);
28845      });
28846    }
28847    /**
28848     * Uses the WebVTT parser to parse the segment response
28849     *
28850     * @throws NoVttJsError
28851     *
28852     * @param {Object} segmentInfo
28853     *        a segment info object that describes the current segment
28854     * @private
28855     */
28856
28857
28858    parseVTTCues_(segmentInfo) {
28859      let decoder;
28860      let decodeBytesToString = false;
28861
28862      if (typeof window.WebVTT !== 'function') {
28863        // caller is responsible for exception handling.
28864        throw new NoVttJsError();
28865      }
28866
28867      segmentInfo.cues = [];
28868      segmentInfo.timestampmap = {
28869        MPEGTS: 0,
28870        LOCAL: 0
28871      };
28872
28873      if (segmentInfo.mp4VttCues) {
28874        this.parseMp4VttCues_(segmentInfo);
28875        return;
28876      }
28877
28878      if (typeof window.TextDecoder === 'function') {
28879        decoder = new window.TextDecoder('utf8');
28880      } else {
28881        decoder = window.WebVTT.StringDecoder();
28882        decodeBytesToString = true;
28883      }
28884
28885      const parser = new window.WebVTT.Parser(window, window.vttjs, decoder);
28886      parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues);
28887
28888      parser.ontimestampmap = map => {
28889        segmentInfo.timestampmap = map;
28890      };
28891
28892      parser.onparsingerror = error => {
28893        videojs__default["default"].log.warn('Error encountered when parsing cues: ' + error.message);
28894      };
28895
28896      if (segmentInfo.segment.map) {
28897        let mapData = segmentInfo.segment.map.bytes;
28898
28899        if (decodeBytesToString) {
28900          mapData = uint8ToUtf8(mapData);
28901        }
28902
28903        parser.parse(mapData);
28904      }
28905
28906      let segmentData = segmentInfo.bytes;
28907
28908      if (decodeBytesToString) {
28909        segmentData = uint8ToUtf8(segmentData);
28910      }
28911
28912      parser.parse(segmentData);
28913      parser.flush();
28914    }
28915    /**
28916     * Updates the start and end times of any cues parsed by the WebVTT parser using
28917     * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping
28918     * from the SyncController
28919     *
28920     * @param {Object} segmentInfo
28921     *        a segment info object that describes the current segment
28922     * @param {Object} mappingObj
28923     *        object containing a mapping from TS to media time
28924     * @param {Object} playlist
28925     *        the playlist object containing the segment
28926     * @private
28927     */
28928
28929
28930    updateTimeMapping_(segmentInfo, mappingObj, playlist) {
28931      const segment = segmentInfo.segment;
28932
28933      if (!mappingObj) {
28934        // If the sync controller does not have a mapping of TS to Media Time for the
28935        // timeline, then we don't have enough information to update the cue
28936        // start/end times
28937        return;
28938      }
28939
28940      if (!segmentInfo.cues.length) {
28941        // If there are no cues, we also do not have enough information to figure out
28942        // segment timing. Mark that the segment contains no cues so we don't re-request
28943        // an empty segment.
28944        segment.empty = true;
28945        return;
28946      }
28947
28948      const {
28949        MPEGTS,
28950        LOCAL
28951      } = segmentInfo.timestampmap;
28952      /**
28953       * From the spec:
28954       * The MPEGTS media timestamp MUST use a 90KHz timescale,
28955       * even when non-WebVTT Media Segments use a different timescale.
28956       */
28957
28958      const mpegTsInSeconds = MPEGTS / clock.ONE_SECOND_IN_TS;
28959      const diff = mpegTsInSeconds - LOCAL + mappingObj.mapping;
28960      segmentInfo.cues.forEach(cue => {
28961        const duration = cue.endTime - cue.startTime;
28962        const startTime = this.handleRollover_(cue.startTime + diff, mappingObj.time);
28963        cue.startTime = Math.max(startTime, 0);
28964        cue.endTime = Math.max(startTime + duration, 0);
28965      });
28966
28967      if (!playlist.syncInfo) {
28968        const firstStart = segmentInfo.cues[0].startTime;
28969        const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime;
28970        playlist.syncInfo = {
28971          mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex,
28972          time: Math.min(firstStart, lastStart - segment.duration)
28973        };
28974      }
28975    }
28976    /**
28977     * MPEG-TS PES timestamps are limited to 2^33.
28978     * Once they reach 2^33, they roll over to 0.
28979     * mux.js handles PES timestamp rollover for the following scenarios:
28980     * [forward rollover(right)] ->
28981     *    PES timestamps monotonically increase, and once they reach 2^33, they roll over to 0
28982     * [backward rollover(left)] -->
28983     *    we seek back to position before rollover.
28984     *
28985     * According to the HLS SPEC:
28986     * When synchronizing WebVTT with PES timestamps, clients SHOULD account
28987     * for cases where the 33-bit PES timestamps have wrapped and the WebVTT
28988     * cue times have not.  When the PES timestamp wraps, the WebVTT Segment
28989     * SHOULD have a X-TIMESTAMP-MAP header that maps the current WebVTT
28990     * time to the new (low valued) PES timestamp.
28991     *
28992     * So we want to handle rollover here and align VTT Cue start/end time to the player's time.
28993     */
28994
28995
28996    handleRollover_(value, reference) {
28997      if (reference === null) {
28998        return value;
28999      }
29000
29001      let valueIn90khz = value * clock.ONE_SECOND_IN_TS;
29002      const referenceIn90khz = reference * clock.ONE_SECOND_IN_TS;
29003      let offset;
29004
29005      if (referenceIn90khz < valueIn90khz) {
29006        // - 2^33
29007        offset = -8589934592;
29008      } else {
29009        // + 2^33
29010        offset = 8589934592;
29011      } // distance(value - reference) > 2^32
29012
29013
29014      while (Math.abs(valueIn90khz - referenceIn90khz) > 4294967296) {
29015        valueIn90khz += offset;
29016      }
29017
29018      return valueIn90khz / clock.ONE_SECOND_IN_TS;
29019    }
29020
29021  }
29022
29023  /**
29024   * @file ad-cue-tags.js
29025   */
29026
29027  /**
29028   * Searches for an ad cue that overlaps with the given mediaTime
29029   *
29030   * @param {Object} track
29031   *        the track to find the cue for
29032   *
29033   * @param {number} mediaTime
29034   *        the time to find the cue at
29035   *
29036   * @return {Object|null}
29037   *         the found cue or null
29038   */
29039  const findAdCue = function (track, mediaTime) {
29040    const cues = track.cues;
29041
29042    for (let i = 0; i < cues.length; i++) {
29043      const cue = cues[i];
29044
29045      if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) {
29046        return cue;
29047      }
29048    }
29049
29050    return null;
29051  };
29052  const updateAdCues = function (media, track, offset = 0) {
29053    if (!media.segments) {
29054      return;
29055    }
29056
29057    let mediaTime = offset;
29058    let cue;
29059
29060    for (let i = 0; i < media.segments.length; i++) {
29061      const segment = media.segments[i];
29062
29063      if (!cue) {
29064        // Since the cues will span for at least the segment duration, adding a fudge
29065        // factor of half segment duration will prevent duplicate cues from being
29066        // created when timing info is not exact (e.g. cue start time initialized
29067        // at 10.006677, but next call mediaTime is 10.003332 )
29068        cue = findAdCue(track, mediaTime + segment.duration / 2);
29069      }
29070
29071      if (cue) {
29072        if ('cueIn' in segment) {
29073          // Found a CUE-IN so end the cue
29074          cue.endTime = mediaTime;
29075          cue.adEndTime = mediaTime;
29076          mediaTime += segment.duration;
29077          cue = null;
29078          continue;
29079        }
29080
29081        if (mediaTime < cue.endTime) {
29082          // Already processed this mediaTime for this cue
29083          mediaTime += segment.duration;
29084          continue;
29085        } // otherwise extend cue until a CUE-IN is found
29086
29087
29088        cue.endTime += segment.duration;
29089      } else {
29090        if ('cueOut' in segment) {
29091          cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut);
29092          cue.adStartTime = mediaTime; // Assumes tag format to be
29093          // #EXT-X-CUE-OUT:30
29094
29095          cue.adEndTime = mediaTime + parseFloat(segment.cueOut);
29096          track.addCue(cue);
29097        }
29098
29099        if ('cueOutCont' in segment) {
29100          // Entered into the middle of an ad cue
29101          // Assumes tag formate to be
29102          // #EXT-X-CUE-OUT-CONT:10/30
29103          const [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat);
29104          cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, '');
29105          cue.adStartTime = mediaTime - adOffset;
29106          cue.adEndTime = cue.adStartTime + adTotal;
29107          track.addCue(cue);
29108        }
29109      }
29110
29111      mediaTime += segment.duration;
29112    }
29113  };
29114
29115  class SyncInfo {
29116    /**
29117     * @param {number} start - media sequence start
29118     * @param {number} end - media sequence end
29119     * @param {number} segmentIndex - index for associated segment
29120     * @param {number|null} [partIndex] - index for associated part
29121     * @param {boolean} [appended] - appended indicator
29122     *
29123     */
29124    constructor({
29125      start,
29126      end,
29127      segmentIndex,
29128      partIndex = null,
29129      appended = false
29130    }) {
29131      this.start_ = start;
29132      this.end_ = end;
29133      this.segmentIndex_ = segmentIndex;
29134      this.partIndex_ = partIndex;
29135      this.appended_ = appended;
29136    }
29137
29138    isInRange(targetTime) {
29139      return targetTime >= this.start && targetTime < this.end;
29140    }
29141
29142    markAppended() {
29143      this.appended_ = true;
29144    }
29145
29146    resetAppendedStatus() {
29147      this.appended_ = false;
29148    }
29149
29150    get isAppended() {
29151      return this.appended_;
29152    }
29153
29154    get start() {
29155      return this.start_;
29156    }
29157
29158    get end() {
29159      return this.end_;
29160    }
29161
29162    get segmentIndex() {
29163      return this.segmentIndex_;
29164    }
29165
29166    get partIndex() {
29167      return this.partIndex_;
29168    }
29169
29170  }
29171
29172  class SyncInfoData {
29173    /**
29174     *
29175     * @param {SyncInfo} segmentSyncInfo - sync info for a given segment
29176     * @param {Array<SyncInfo>} [partsSyncInfo] - sync infos for a list of parts for a given segment
29177     */
29178    constructor(segmentSyncInfo, partsSyncInfo = []) {
29179      this.segmentSyncInfo_ = segmentSyncInfo;
29180      this.partsSyncInfo_ = partsSyncInfo;
29181    }
29182
29183    get segmentSyncInfo() {
29184      return this.segmentSyncInfo_;
29185    }
29186
29187    get partsSyncInfo() {
29188      return this.partsSyncInfo_;
29189    }
29190
29191    get hasPartsSyncInfo() {
29192      return this.partsSyncInfo_.length > 0;
29193    }
29194
29195    resetAppendStatus() {
29196      this.segmentSyncInfo_.resetAppendedStatus();
29197      this.partsSyncInfo_.forEach(partSyncInfo => partSyncInfo.resetAppendedStatus());
29198    }
29199
29200  }
29201
29202  class MediaSequenceSync {
29203    constructor() {
29204      /**
29205       * @type {Map<number, SyncInfoData>}
29206       * @protected
29207       */
29208      this.storage_ = new Map();
29209      this.diagnostics_ = '';
29210      this.isReliable_ = false;
29211      this.start_ = -Infinity;
29212      this.end_ = Infinity;
29213    }
29214
29215    get start() {
29216      return this.start_;
29217    }
29218
29219    get end() {
29220      return this.end_;
29221    }
29222
29223    get diagnostics() {
29224      return this.diagnostics_;
29225    }
29226
29227    get isReliable() {
29228      return this.isReliable_;
29229    }
29230
29231    resetAppendedStatus() {
29232      this.storage_.forEach(syncInfoData => syncInfoData.resetAppendStatus());
29233    }
29234    /**
29235     * update sync storage
29236     *
29237     * @param {Object} playlist
29238     * @param {number} currentTime
29239     *
29240     * @return {void}
29241     */
29242
29243
29244    update(playlist, currentTime) {
29245      const {
29246        mediaSequence,
29247        segments
29248      } = playlist;
29249      this.isReliable_ = this.isReliablePlaylist_(mediaSequence, segments);
29250
29251      if (!this.isReliable_) {
29252        return;
29253      }
29254
29255      return this.updateStorage_(segments, mediaSequence, this.calculateBaseTime_(mediaSequence, segments, currentTime));
29256    }
29257    /**
29258     * @param {number} targetTime
29259     * @return {SyncInfo|null}
29260     */
29261
29262
29263    getSyncInfoForTime(targetTime) {
29264      for (const {
29265        segmentSyncInfo,
29266        partsSyncInfo
29267      } of this.storage_.values()) {
29268        // Normal segment flow:
29269        if (!partsSyncInfo.length) {
29270          if (segmentSyncInfo.isInRange(targetTime)) {
29271            return segmentSyncInfo;
29272          }
29273        } else {
29274          // Low latency flow:
29275          for (const partSyncInfo of partsSyncInfo) {
29276            if (partSyncInfo.isInRange(targetTime)) {
29277              return partSyncInfo;
29278            }
29279          }
29280        }
29281      }
29282
29283      return null;
29284    }
29285
29286    getSyncInfoForMediaSequence(mediaSequence) {
29287      return this.storage_.get(mediaSequence);
29288    }
29289
29290    updateStorage_(segments, startingMediaSequence, startingTime) {
29291      const newStorage = new Map();
29292      let newDiagnostics = '\n';
29293      let currentStart = startingTime;
29294      let currentMediaSequence = startingMediaSequence;
29295      this.start_ = currentStart;
29296      segments.forEach((segment, segmentIndex) => {
29297        const prevSyncInfoData = this.storage_.get(currentMediaSequence);
29298        const segmentStart = currentStart;
29299        const segmentEnd = segmentStart + segment.duration;
29300        const segmentIsAppended = Boolean(prevSyncInfoData && prevSyncInfoData.segmentSyncInfo && prevSyncInfoData.segmentSyncInfo.isAppended);
29301        const segmentSyncInfo = new SyncInfo({
29302          start: segmentStart,
29303          end: segmentEnd,
29304          appended: segmentIsAppended,
29305          segmentIndex
29306        });
29307        segment.syncInfo = segmentSyncInfo;
29308        let currentPartStart = currentStart;
29309        const partsSyncInfo = (segment.parts || []).map((part, partIndex) => {
29310          const partStart = currentPartStart;
29311          const partEnd = currentPartStart + part.duration;
29312          const partIsAppended = Boolean(prevSyncInfoData && prevSyncInfoData.partsSyncInfo && prevSyncInfoData.partsSyncInfo[partIndex] && prevSyncInfoData.partsSyncInfo[partIndex].isAppended);
29313          const partSyncInfo = new SyncInfo({
29314            start: partStart,
29315            end: partEnd,
29316            appended: partIsAppended,
29317            segmentIndex,
29318            partIndex
29319          });
29320          currentPartStart = partEnd;
29321          newDiagnostics += `Media Sequence: ${currentMediaSequence}.${partIndex} | Range: ${partStart} --> ${partEnd} | Appended: ${partIsAppended}\n`;
29322          part.syncInfo = partSyncInfo;
29323          return partSyncInfo;
29324        });
29325        newStorage.set(currentMediaSequence, new SyncInfoData(segmentSyncInfo, partsSyncInfo));
29326        newDiagnostics += `${compactSegmentUrlDescription(segment.resolvedUri)} | Media Sequence: ${currentMediaSequence} | Range: ${segmentStart} --> ${segmentEnd} | Appended: ${segmentIsAppended}\n`;
29327        currentMediaSequence++;
29328        currentStart = segmentEnd;
29329      });
29330      this.end_ = currentStart;
29331      this.storage_ = newStorage;
29332      this.diagnostics_ = newDiagnostics;
29333    }
29334
29335    calculateBaseTime_(mediaSequence, segments, fallback) {
29336      if (!this.storage_.size) {
29337        // Initial setup flow.
29338        return 0;
29339      }
29340
29341      if (this.storage_.has(mediaSequence)) {
29342        // Normal flow.
29343        return this.storage_.get(mediaSequence).segmentSyncInfo.start;
29344      }
29345
29346      const minMediaSequenceFromStorage = Math.min(...this.storage_.keys()); // This case captures a race condition that can occur if we switch to a new media playlist that is out of date
29347      // and still has an older Media Sequence. If this occurs, we extrapolate backwards to get the base time.
29348
29349      if (mediaSequence < minMediaSequenceFromStorage) {
29350        const mediaSequenceDiff = minMediaSequenceFromStorage - mediaSequence;
29351        let baseTime = this.storage_.get(minMediaSequenceFromStorage).segmentSyncInfo.start;
29352
29353        for (let i = 0; i < mediaSequenceDiff; i++) {
29354          const segment = segments[i];
29355          baseTime -= segment.duration;
29356        }
29357
29358        return baseTime;
29359      } // Fallback flow.
29360      // There is a gap between last recorded playlist and a new one received.
29361
29362
29363      return fallback;
29364    }
29365
29366    isReliablePlaylist_(mediaSequence, segments) {
29367      return mediaSequence !== undefined && mediaSequence !== null && Array.isArray(segments) && segments.length;
29368    }
29369
29370  }
29371  class DependantMediaSequenceSync extends MediaSequenceSync {
29372    constructor(parent) {
29373      super();
29374      this.parent_ = parent;
29375    }
29376
29377    calculateBaseTime_(mediaSequence, segments, fallback) {
29378      if (!this.storage_.size) {
29379        const info = this.parent_.getSyncInfoForMediaSequence(mediaSequence);
29380
29381        if (info) {
29382          return info.segmentSyncInfo.start;
29383        }
29384
29385        return 0;
29386      }
29387
29388      return super.calculateBaseTime_(mediaSequence, segments, fallback);
29389    }
29390
29391  }
29392
29393  /**
29394   * @file sync-controller.js
29395   */
29396  // synchronize expired playlist segments.
29397  // the max media sequence diff is 48 hours of live stream
29398  // content with two second segments. Anything larger than that
29399  // will likely be invalid.
29400
29401  const MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC = 86400;
29402  const syncPointStrategies = [// Stategy "VOD": Handle the VOD-case where the sync-point is *always*
29403  //                the equivalence display-time 0 === segment-index 0
29404  {
29405    name: 'VOD',
29406    run: (syncController, playlist, duration, currentTimeline, currentTime) => {
29407      if (duration !== Infinity) {
29408        const syncPoint = {
29409          time: 0,
29410          segmentIndex: 0,
29411          partIndex: null
29412        };
29413        return syncPoint;
29414      }
29415
29416      return null;
29417    }
29418  }, {
29419    name: 'MediaSequence',
29420
29421    /**
29422     * run media sequence strategy
29423     *
29424     * @param {SyncController} syncController
29425     * @param {Object} playlist
29426     * @param {number} duration
29427     * @param {number} currentTimeline
29428     * @param {number} currentTime
29429     * @param {string} type
29430     */
29431    run: (syncController, playlist, duration, currentTimeline, currentTime, type) => {
29432      const mediaSequenceSync = syncController.getMediaSequenceSync(type);
29433
29434      if (!mediaSequenceSync) {
29435        return null;
29436      }
29437
29438      if (!mediaSequenceSync.isReliable) {
29439        return null;
29440      }
29441
29442      const syncInfo = mediaSequenceSync.getSyncInfoForTime(currentTime);
29443
29444      if (!syncInfo) {
29445        return null;
29446      }
29447
29448      return {
29449        time: syncInfo.start,
29450        partIndex: syncInfo.partIndex,
29451        segmentIndex: syncInfo.segmentIndex
29452      };
29453    }
29454  }, // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
29455  {
29456    name: 'ProgramDateTime',
29457    run: (syncController, playlist, duration, currentTimeline, currentTime) => {
29458      if (!Object.keys(syncController.timelineToDatetimeMappings).length) {
29459        return null;
29460      }
29461
29462      let syncPoint = null;
29463      let lastDistance = null;
29464      const partsAndSegments = getPartsAndSegments(playlist);
29465      currentTime = currentTime || 0;
29466
29467      for (let i = 0; i < partsAndSegments.length; i++) {
29468        // start from the end and loop backwards for live
29469        // or start from the front and loop forwards for non-live
29470        const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1);
29471        const partAndSegment = partsAndSegments[index];
29472        const segment = partAndSegment.segment;
29473        const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline];
29474
29475        if (!datetimeMapping || !segment.dateTimeObject) {
29476          continue;
29477        }
29478
29479        const segmentTime = segment.dateTimeObject.getTime() / 1000;
29480        let start = segmentTime + datetimeMapping; // take part duration into account.
29481
29482        if (segment.parts && typeof partAndSegment.partIndex === 'number') {
29483          for (let z = 0; z < partAndSegment.partIndex; z++) {
29484            start += segment.parts[z].duration;
29485          }
29486        }
29487
29488        const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed
29489        // currentTime and can stop looking for better candidates
29490
29491        if (lastDistance !== null && (distance === 0 || lastDistance < distance)) {
29492          break;
29493        }
29494
29495        lastDistance = distance;
29496        syncPoint = {
29497          time: start,
29498          segmentIndex: partAndSegment.segmentIndex,
29499          partIndex: partAndSegment.partIndex
29500        };
29501      }
29502
29503      return syncPoint;
29504    }
29505  }, // Stategy "Segment": We have a known time mapping for a timeline and a
29506  //                    segment in the current timeline with timing data
29507  {
29508    name: 'Segment',
29509    run: (syncController, playlist, duration, currentTimeline, currentTime) => {
29510      let syncPoint = null;
29511      let lastDistance = null;
29512      currentTime = currentTime || 0;
29513      const partsAndSegments = getPartsAndSegments(playlist);
29514
29515      for (let i = 0; i < partsAndSegments.length; i++) {
29516        // start from the end and loop backwards for live
29517        // or start from the front and loop forwards for non-live
29518        const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1);
29519        const partAndSegment = partsAndSegments[index];
29520        const segment = partAndSegment.segment;
29521        const start = partAndSegment.part && partAndSegment.part.start || segment && segment.start;
29522
29523        if (segment.timeline === currentTimeline && typeof start !== 'undefined') {
29524          const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed
29525          // currentTime and can stop looking for better candidates
29526
29527          if (lastDistance !== null && lastDistance < distance) {
29528            break;
29529          }
29530
29531          if (!syncPoint || lastDistance === null || lastDistance >= distance) {
29532            lastDistance = distance;
29533            syncPoint = {
29534              time: start,
29535              segmentIndex: partAndSegment.segmentIndex,
29536              partIndex: partAndSegment.partIndex
29537            };
29538          }
29539        }
29540      }
29541
29542      return syncPoint;
29543    }
29544  }, // Stategy "Discontinuity": We have a discontinuity with a known
29545  //                          display-time
29546  {
29547    name: 'Discontinuity',
29548    run: (syncController, playlist, duration, currentTimeline, currentTime) => {
29549      let syncPoint = null;
29550      currentTime = currentTime || 0;
29551
29552      if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
29553        let lastDistance = null;
29554
29555        for (let i = 0; i < playlist.discontinuityStarts.length; i++) {
29556          const segmentIndex = playlist.discontinuityStarts[i];
29557          const discontinuity = playlist.discontinuitySequence + i + 1;
29558          const discontinuitySync = syncController.discontinuities[discontinuity];
29559
29560          if (discontinuitySync) {
29561            const distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed
29562            // currentTime and can stop looking for better candidates
29563
29564            if (lastDistance !== null && lastDistance < distance) {
29565              break;
29566            }
29567
29568            if (!syncPoint || lastDistance === null || lastDistance >= distance) {
29569              lastDistance = distance;
29570              syncPoint = {
29571                time: discontinuitySync.time,
29572                segmentIndex,
29573                partIndex: null
29574              };
29575            }
29576          }
29577        }
29578      }
29579
29580      return syncPoint;
29581    }
29582  }, // Stategy "Playlist": We have a playlist with a known mapping of
29583  //                     segment index to display time
29584  {
29585    name: 'Playlist',
29586    run: (syncController, playlist, duration, currentTimeline, currentTime) => {
29587      if (playlist.syncInfo) {
29588        const syncPoint = {
29589          time: playlist.syncInfo.time,
29590          segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence,
29591          partIndex: null
29592        };
29593        return syncPoint;
29594      }
29595
29596      return null;
29597    }
29598  }];
29599  class SyncController extends videojs__default["default"].EventTarget {
29600    constructor(options = {}) {
29601      super(); // ...for synching across variants
29602
29603      this.timelines = [];
29604      this.discontinuities = [];
29605      this.timelineToDatetimeMappings = {}; // TODO: this map should be only available for HLS. Since only HLS has MediaSequence.
29606      //  For some reason this map helps with syncing between quality switch for MPEG-DASH as well.
29607      //  Moreover if we disable this map for MPEG-DASH - quality switch will be broken.
29608      //  MPEG-DASH should have its own separate sync strategy
29609
29610      const main = new MediaSequenceSync();
29611      const audio = new DependantMediaSequenceSync(main);
29612      const vtt = new DependantMediaSequenceSync(main);
29613      this.mediaSequenceStorage_ = {
29614        main,
29615        audio,
29616        vtt
29617      };
29618      this.logger_ = logger('SyncController');
29619    }
29620    /**
29621     *
29622     * @param {string} loaderType
29623     * @return {MediaSequenceSync|null}
29624     */
29625
29626
29627    getMediaSequenceSync(loaderType) {
29628      return this.mediaSequenceStorage_[loaderType] || null;
29629    }
29630    /**
29631     * Find a sync-point for the playlist specified
29632     *
29633     * A sync-point is defined as a known mapping from display-time to
29634     * a segment-index in the current playlist.
29635     *
29636     * @param {Playlist} playlist
29637     *        The playlist that needs a sync-point
29638     * @param {number} duration
29639     *        Duration of the MediaSource (Infinite if playing a live source)
29640     * @param {number} currentTimeline
29641     *        The last timeline from which a segment was loaded
29642     * @param {number} currentTime
29643     *        Current player's time
29644     * @param {string} type
29645     *        Segment loader type
29646     * @return {Object}
29647     *          A sync-point object
29648     */
29649
29650
29651    getSyncPoint(playlist, duration, currentTimeline, currentTime, type) {
29652      // Always use VOD sync point for VOD
29653      if (duration !== Infinity) {
29654        const vodSyncPointStrategy = syncPointStrategies.find(({
29655          name
29656        }) => name === 'VOD');
29657        return vodSyncPointStrategy.run(this, playlist, duration);
29658      }
29659
29660      const syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime, type);
29661
29662      if (!syncPoints.length) {
29663        // Signal that we need to attempt to get a sync-point manually
29664        // by fetching a segment in the playlist and constructing
29665        // a sync-point from that information
29666        return null;
29667      } // If we have exact match just return it instead of finding the nearest distance
29668
29669
29670      for (const syncPointInfo of syncPoints) {
29671        const {
29672          syncPoint,
29673          strategy
29674        } = syncPointInfo;
29675        const {
29676          segmentIndex,
29677          time
29678        } = syncPoint;
29679
29680        if (segmentIndex < 0) {
29681          continue;
29682        }
29683
29684        const selectedSegment = playlist.segments[segmentIndex];
29685        const start = time;
29686        const end = start + selectedSegment.duration;
29687        this.logger_(`Strategy: ${strategy}. Current time: ${currentTime}. selected segment: ${segmentIndex}. Time: [${start} -> ${end}]}`);
29688
29689        if (currentTime >= start && currentTime < end) {
29690          this.logger_('Found sync point with exact match: ', syncPoint);
29691          return syncPoint;
29692        }
29693      } // Now find the sync-point that is closest to the currentTime because
29694      // that should result in the most accurate guess about which segment
29695      // to fetch
29696
29697
29698      return this.selectSyncPoint_(syncPoints, {
29699        key: 'time',
29700        value: currentTime
29701      });
29702    }
29703    /**
29704     * Calculate the amount of time that has expired off the playlist during playback
29705     *
29706     * @param {Playlist} playlist
29707     *        Playlist object to calculate expired from
29708     * @param {number} duration
29709     *        Duration of the MediaSource (Infinity if playling a live source)
29710     * @return {number|null}
29711     *          The amount of time that has expired off the playlist during playback. Null
29712     *          if no sync-points for the playlist can be found.
29713     */
29714
29715
29716    getExpiredTime(playlist, duration) {
29717      if (!playlist || !playlist.segments) {
29718        return null;
29719      }
29720
29721      const syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0); // Without sync-points, there is not enough information to determine the expired time
29722
29723      if (!syncPoints.length) {
29724        return null;
29725      }
29726
29727      const syncPoint = this.selectSyncPoint_(syncPoints, {
29728        key: 'segmentIndex',
29729        value: 0
29730      }); // If the sync-point is beyond the start of the playlist, we want to subtract the
29731      // duration from index 0 to syncPoint.segmentIndex instead of adding.
29732
29733      if (syncPoint.segmentIndex > 0) {
29734        syncPoint.time *= -1;
29735      }
29736
29737      return Math.abs(syncPoint.time + sumDurations({
29738        defaultDuration: playlist.targetDuration,
29739        durationList: playlist.segments,
29740        startIndex: syncPoint.segmentIndex,
29741        endIndex: 0
29742      }));
29743    }
29744    /**
29745     * Runs each sync-point strategy and returns a list of sync-points returned by the
29746     * strategies
29747     *
29748     * @private
29749     * @param {Playlist} playlist
29750     *        The playlist that needs a sync-point
29751     * @param {number} duration
29752     *        Duration of the MediaSource (Infinity if playing a live source)
29753     * @param {number} currentTimeline
29754     *        The last timeline from which a segment was loaded
29755     * @param {number} currentTime
29756     *        Current player's time
29757     * @param {string} type
29758     *        Segment loader type
29759     * @return {Array}
29760     *          A list of sync-point objects
29761     */
29762
29763
29764    runStrategies_(playlist, duration, currentTimeline, currentTime, type) {
29765      const syncPoints = []; // Try to find a sync-point in by utilizing various strategies...
29766
29767      for (let i = 0; i < syncPointStrategies.length; i++) {
29768        const strategy = syncPointStrategies[i];
29769        const syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime, type);
29770
29771        if (syncPoint) {
29772          syncPoint.strategy = strategy.name;
29773          syncPoints.push({
29774            strategy: strategy.name,
29775            syncPoint
29776          });
29777        }
29778      }
29779
29780      return syncPoints;
29781    }
29782    /**
29783     * Selects the sync-point nearest the specified target
29784     *
29785     * @private
29786     * @param {Array} syncPoints
29787     *        List of sync-points to select from
29788     * @param {Object} target
29789     *        Object specifying the property and value we are targeting
29790     * @param {string} target.key
29791     *        Specifies the property to target. Must be either 'time' or 'segmentIndex'
29792     * @param {number} target.value
29793     *        The value to target for the specified key.
29794     * @return {Object}
29795     *          The sync-point nearest the target
29796     */
29797
29798
29799    selectSyncPoint_(syncPoints, target) {
29800      let bestSyncPoint = syncPoints[0].syncPoint;
29801      let bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value);
29802      let bestStrategy = syncPoints[0].strategy;
29803
29804      for (let i = 1; i < syncPoints.length; i++) {
29805        const newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value);
29806
29807        if (newDistance < bestDistance) {
29808          bestDistance = newDistance;
29809          bestSyncPoint = syncPoints[i].syncPoint;
29810          bestStrategy = syncPoints[i].strategy;
29811        }
29812      }
29813
29814      this.logger_(`syncPoint for [${target.key}: ${target.value}] chosen with strategy` + ` [${bestStrategy}]: [time:${bestSyncPoint.time},` + ` segmentIndex:${bestSyncPoint.segmentIndex}` + (typeof bestSyncPoint.partIndex === 'number' ? `,partIndex:${bestSyncPoint.partIndex}` : '') + ']');
29815      return bestSyncPoint;
29816    }
29817    /**
29818     * Save any meta-data present on the segments when segments leave
29819     * the live window to the playlist to allow for synchronization at the
29820     * playlist level later.
29821     *
29822     * @param {Playlist} oldPlaylist - The previous active playlist
29823     * @param {Playlist} newPlaylist - The updated and most current playlist
29824     */
29825
29826
29827    saveExpiredSegmentInfo(oldPlaylist, newPlaylist) {
29828      const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // Ignore large media sequence gaps
29829
29830      if (mediaSequenceDiff > MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC) {
29831        videojs__default["default"].log.warn(`Not saving expired segment info. Media sequence gap ${mediaSequenceDiff} is too large.`);
29832        return;
29833      } // When a segment expires from the playlist and it has a start time
29834      // save that information as a possible sync-point reference in future
29835
29836
29837      for (let i = mediaSequenceDiff - 1; i >= 0; i--) {
29838        const lastRemovedSegment = oldPlaylist.segments[i];
29839
29840        if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') {
29841          newPlaylist.syncInfo = {
29842            mediaSequence: oldPlaylist.mediaSequence + i,
29843            time: lastRemovedSegment.start
29844          };
29845          this.logger_(`playlist refresh sync: [time:${newPlaylist.syncInfo.time},` + ` mediaSequence: ${newPlaylist.syncInfo.mediaSequence}]`);
29846          this.trigger('syncinfoupdate');
29847          break;
29848        }
29849      }
29850    }
29851    /**
29852     * Save the mapping from playlist's ProgramDateTime to display. This should only happen
29853     * before segments start to load.
29854     *
29855     * @param {Playlist} playlist - The currently active playlist
29856     */
29857
29858
29859    setDateTimeMappingForStart(playlist) {
29860      // It's possible for the playlist to be updated before playback starts, meaning time
29861      // zero is not yet set. If, during these playlist refreshes, a discontinuity is
29862      // crossed, then the old time zero mapping (for the prior timeline) would be retained
29863      // unless the mappings are cleared.
29864      this.timelineToDatetimeMappings = {};
29865
29866      if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) {
29867        const firstSegment = playlist.segments[0];
29868        const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000;
29869        this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp;
29870      }
29871    }
29872    /**
29873     * Calculates and saves timeline mappings, playlist sync info, and segment timing values
29874     * based on the latest timing information.
29875     *
29876     * @param {Object} options
29877     *        Options object
29878     * @param {SegmentInfo} options.segmentInfo
29879     *        The current active request information
29880     * @param {boolean} options.shouldSaveTimelineMapping
29881     *        If there's a timeline change, determines if the timeline mapping should be
29882     *        saved for timeline mapping and program date time mappings.
29883     */
29884
29885
29886    saveSegmentTimingInfo({
29887      segmentInfo,
29888      shouldSaveTimelineMapping
29889    }) {
29890      const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_(segmentInfo, segmentInfo.timingInfo, shouldSaveTimelineMapping);
29891      const segment = segmentInfo.segment;
29892
29893      if (didCalculateSegmentTimeMapping) {
29894        this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information
29895        // now with segment timing information
29896
29897        if (!segmentInfo.playlist.syncInfo) {
29898          segmentInfo.playlist.syncInfo = {
29899            mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex,
29900            time: segment.start
29901          };
29902        }
29903      }
29904
29905      const dateTime = segment.dateTimeObject;
29906
29907      if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) {
29908        this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000);
29909      }
29910    }
29911
29912    timestampOffsetForTimeline(timeline) {
29913      if (typeof this.timelines[timeline] === 'undefined') {
29914        return null;
29915      }
29916
29917      return this.timelines[timeline].time;
29918    }
29919
29920    mappingForTimeline(timeline) {
29921      if (typeof this.timelines[timeline] === 'undefined') {
29922        return null;
29923      }
29924
29925      return this.timelines[timeline].mapping;
29926    }
29927    /**
29928     * Use the "media time" for a segment to generate a mapping to "display time" and
29929     * save that display time to the segment.
29930     *
29931     * @private
29932     * @param {SegmentInfo} segmentInfo
29933     *        The current active request information
29934     * @param {Object} timingInfo
29935     *        The start and end time of the current segment in "media time"
29936     * @param {boolean} shouldSaveTimelineMapping
29937     *        If there's a timeline change, determines if the timeline mapping should be
29938     *        saved in timelines.
29939     * @return {boolean}
29940     *          Returns false if segment time mapping could not be calculated
29941     */
29942
29943
29944    calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) {
29945      // TODO: remove side effects
29946      const segment = segmentInfo.segment;
29947      const part = segmentInfo.part;
29948      let mappingObj = this.timelines[segmentInfo.timeline];
29949      let start;
29950      let end;
29951
29952      if (typeof segmentInfo.timestampOffset === 'number') {
29953        mappingObj = {
29954          time: segmentInfo.startOfSegment,
29955          mapping: segmentInfo.startOfSegment - timingInfo.start
29956        };
29957
29958        if (shouldSaveTimelineMapping) {
29959          this.timelines[segmentInfo.timeline] = mappingObj;
29960          this.trigger('timestampoffset');
29961          this.logger_(`time mapping for timeline ${segmentInfo.timeline}: ` + `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]`);
29962        }
29963
29964        start = segmentInfo.startOfSegment;
29965        end = timingInfo.end + mappingObj.mapping;
29966      } else if (mappingObj) {
29967        start = timingInfo.start + mappingObj.mapping;
29968        end = timingInfo.end + mappingObj.mapping;
29969      } else {
29970        return false;
29971      }
29972
29973      if (part) {
29974        part.start = start;
29975        part.end = end;
29976      } // If we don't have a segment start yet or the start value we got
29977      // is less than our current segment.start value, save a new start value.
29978      // We have to do this because parts will have segment timing info saved
29979      // multiple times and we want segment start to be the earliest part start
29980      // value for that segment.
29981
29982
29983      if (!segment.start || start < segment.start) {
29984        segment.start = start;
29985      }
29986
29987      segment.end = end;
29988      return true;
29989    }
29990    /**
29991     * Each time we have discontinuity in the playlist, attempt to calculate the location
29992     * in display of the start of the discontinuity and save that. We also save an accuracy
29993     * value so that we save values with the most accuracy (closest to 0.)
29994     *
29995     * @private
29996     * @param {SegmentInfo} segmentInfo - The current active request information
29997     */
29998
29999
30000    saveDiscontinuitySyncInfo_(segmentInfo) {
30001      const playlist = segmentInfo.playlist;
30002      const segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where
30003      // the start of the range and it's accuracy is 0 (greater accuracy values
30004      // mean more approximation)
30005
30006      if (segment.discontinuity) {
30007        this.discontinuities[segment.timeline] = {
30008          time: segment.start,
30009          accuracy: 0
30010        };
30011      } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) {
30012        // Search for future discontinuities that we can provide better timing
30013        // information for and save that information for sync purposes
30014        for (let i = 0; i < playlist.discontinuityStarts.length; i++) {
30015          const segmentIndex = playlist.discontinuityStarts[i];
30016          const discontinuity = playlist.discontinuitySequence + i + 1;
30017          const mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex;
30018          const accuracy = Math.abs(mediaIndexDiff);
30019
30020          if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) {
30021            let time;
30022
30023            if (mediaIndexDiff < 0) {
30024              time = segment.start - sumDurations({
30025                defaultDuration: playlist.targetDuration,
30026                durationList: playlist.segments,
30027                startIndex: segmentInfo.mediaIndex,
30028                endIndex: segmentIndex
30029              });
30030            } else {
30031              time = segment.end + sumDurations({
30032                defaultDuration: playlist.targetDuration,
30033                durationList: playlist.segments,
30034                startIndex: segmentInfo.mediaIndex + 1,
30035                endIndex: segmentIndex
30036              });
30037            }
30038
30039            this.discontinuities[discontinuity] = {
30040              time,
30041              accuracy
30042            };
30043          }
30044        }
30045      }
30046    }
30047
30048    dispose() {
30049      this.trigger('dispose');
30050      this.off();
30051    }
30052
30053  }
30054
30055  /**
30056   * The TimelineChangeController acts as a source for segment loaders to listen for and
30057   * keep track of latest and pending timeline changes. This is useful to ensure proper
30058   * sync, as each loader may need to make a consideration for what timeline the other
30059   * loader is on before making changes which could impact the other loader's media.
30060   *
30061   * @class TimelineChangeController
30062   * @extends videojs.EventTarget
30063   */
30064
30065  class TimelineChangeController extends videojs__default["default"].EventTarget {
30066    constructor() {
30067      super();
30068      this.pendingTimelineChanges_ = {};
30069      this.lastTimelineChanges_ = {};
30070    }
30071
30072    clearPendingTimelineChange(type) {
30073      this.pendingTimelineChanges_[type] = null;
30074      this.trigger('pendingtimelinechange');
30075    }
30076
30077    pendingTimelineChange({
30078      type,
30079      from,
30080      to
30081    }) {
30082      if (typeof from === 'number' && typeof to === 'number') {
30083        this.pendingTimelineChanges_[type] = {
30084          type,
30085          from,
30086          to
30087        };
30088        this.trigger('pendingtimelinechange');
30089      }
30090
30091      return this.pendingTimelineChanges_[type];
30092    }
30093
30094    lastTimelineChange({
30095      type,
30096      from,
30097      to
30098    }) {
30099      if (typeof from === 'number' && typeof to === 'number') {
30100        this.lastTimelineChanges_[type] = {
30101          type,
30102          from,
30103          to
30104        };
30105        delete this.pendingTimelineChanges_[type];
30106        const metadata = {
30107          timelineChangeInfo: {
30108            from,
30109            to
30110          }
30111        };
30112        this.trigger({
30113          type: 'timelinechange',
30114          metadata
30115        });
30116      }
30117
30118      return this.lastTimelineChanges_[type];
30119    }
30120
30121    dispose() {
30122      this.trigger('dispose');
30123      this.pendingTimelineChanges_ = {};
30124      this.lastTimelineChanges_ = {};
30125      this.off();
30126    }
30127
30128  }
30129
30130  /* rollup-plugin-worker-factory start for worker!/
30130home/runner/work/http-streaming
vendor: 21,606 bytes, lines 30130-30783
30130/http-streaming/src/decrypter-worker.js */
30131  const workerCode = transform(getWorkerString(function () {
30132    /**
30133     * @file stream.js
30134     */
30135
30136    /**
30137     * A lightweight readable stream implemention that handles event dispatching.
30138     *
30139     * @class Stream
30140     */
30141
30142    var Stream = /*#__PURE__*/function () {
30143      function Stream() {
30144        this.listeners = {};
30145      }
30146      /**
30147       * Add a listener for a specified event type.
30148       *
30149       * @param {string} type the event name
30150       * @param {Function} listener the callback to be invoked when an event of
30151       * the specified type occurs
30152       */
30153
30154
30155      var _proto = Stream.prototype;
30156
30157      _proto.on = function on(type, listener) {
30158        if (!this.listeners[type]) {
30159          this.listeners[type] = [];
30160        }
30161
30162        this.listeners[type].push(listener);
30163      }
30164      /**
30165       * Remove a listener for a specified event type.
30166       *
30167       * @param {string} type the event name
30168       * @param {Function} listener  a function previously registered for this
30169       * type of event through `on`
30170       * @return {boolean} if we could turn it off or not
30171       */
30172      ;
30173
30174      _proto.off = function off(type, listener) {
30175        if (!this.listeners[type]) {
30176          return false;
30177        }
30178
30179        var index = this.listeners[type].indexOf(listener); // TODO: which is better?
30180        // In Video.js we slice listener functions
30181        // on trigger so that it does not mess up the order
30182        // while we loop through.
30183        //
30184        // Here we slice on off so that the loop in trigger
30185        // can continue using it's old reference to loop without
30186        // messing up the order.
30187
30188        this.listeners[type] = this.listeners[type].slice(0);
30189        this.listeners[type].splice(index, 1);
30190        return index > -1;
30191      }
30192      /**
30193       * Trigger an event of the specified type on this stream. Any additional
30194       * arguments to this function are passed as parameters to event listeners.
30195       *
30196       * @param {string} type the event name
30197       */
30198      ;
30199
30200      _proto.trigger = function trigger(type) {
30201        var callbacks = this.listeners[type];
30202
30203        if (!callbacks) {
30204          return;
30205        } // Slicing the arguments on every invocation of this method
30206        // can add a significant amount of overhead. Avoid the
30207        // intermediate object creation for the common case of a
30208        // single callback argument
30209
30210
30211        if (arguments.length === 2) {
30212          var length = callbacks.length;
30213
30214          for (var i = 0; i < length; ++i) {
30215            callbacks[i].call(this, arguments[1]);
30216          }
30217        } else {
30218          var args = Array.prototype.slice.call(arguments, 1);
30219          var _length = callbacks.length;
30220
30221          for (var _i = 0; _i < _length; ++_i) {
30222            callbacks[_i].apply(this, args);
30223          }
30224        }
30225      }
30226      /**
30227       * Destroys the stream and cleans up.
30228       */
30229      ;
30230
30231      _proto.dispose = function dispose() {
30232        this.listeners = {};
30233      }
30234      /**
30235       * Forwards all `data` events on this stream to the destination stream. The
30236       * destination stream should provide a method `push` to receive the data
30237       * events as they arrive.
30238       *
30239       * @param {Stream} destination the stream that will receive all `data` events
30240       * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
30241       */
30242      ;
30243
30244      _proto.pipe = function pipe(destination) {
30245        this.on('data', function (data) {
30246          destination.push(data);
30247        });
30248      };
30249
30250      return Stream;
30251    }();
30252    /*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */
30253
30254    /**
30255     * Returns the subarray of a Uint8Array without PKCS#7 padding.
30256     *
30257     * @param padded {Uint8Array} unencrypted bytes that have been padded
30258     * @return {Uint8Array} the unpadded bytes
30259     * @see http://tools.ietf.org/html/rfc5652
30260     */
30261
30262
30263    function unpad(padded) {
30264      return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]);
30265    }
30266    /*! @name aes-decrypter @version 4.0.2 @license Apache-2.0 */
30267
30268    /**
30269     * @file aes.js
30270     *
30271     * This file contains an adaptation of the AES decryption algorithm
30272     * from the Standford Javascript Cryptography Library. That work is
30273     * covered by the following copyright and permissions notice:
30274     *
30275     * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh.
30276     * All rights reserved.
30277     *
30278     * Redistribution and use in source and binary forms, with or without
30279     * modification, are permitted provided that the following conditions are
30280     * met:
30281     *
30282     * 1. Redistributions of source code must retain the above copyright
30283     *    notice, this list of conditions and the following disclaimer.
30284     *
30285     * 2. Redistributions in binary form must reproduce the above
30286     *    copyright notice, this list of conditions and the following
30287     *    disclaimer in the documentation and/or other materials provided
30288     *    with the distribution.
30289     *
30290     * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
30291     * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
30292     * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30293     * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
30294     * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
30295     * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30296     * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
30297     * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
30298     * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
30299     * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30300     * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30301     *
30302     * The views and conclusions contained in the software and documentation
30303     * are those of the authors and should not be interpreted as representing
30304     * official policies, either expressed or implied, of the authors.
30305     */
30306
30307    /**
30308     * Expand the S-box tables.
30309     *
30310     * @private
30311     */
30312
30313
30314    const precompute = function () {
30315      const tables = [[[], [], [], [], []], [[], [], [], [], []]];
30316      const encTable = tables[0];
30317      const decTable = tables[1];
30318      const sbox = encTable[4];
30319      const sboxInv = decTable[4];
30320      let i;
30321      let x;
30322      let xInv;
30323      const d = [];
30324      const th = [];
30325      let x2;
30326      let x4;
30327      let x8;
30328      let s;
30329      let tEnc;
30330      let tDec; // Compute double and third tables
30331
30332      for (i = 0; i < 256; i++) {
30333        th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i;
30334      }
30335
30336      for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
30337        // Compute sbox
30338        s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4;
30339        s = s >> 8 ^ s & 255 ^ 99;
30340        sbox[x] = s;
30341        sboxInv[s] = x; // Compute MixColumns
30342
30343        x8 = d[x4 = d[x2 = d[x]]];
30344        tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100;
30345        tEnc = d[s] * 0x101 ^ s * 0x1010100;
30346
30347        for (i = 0; i < 4; i++) {
30348          encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8;
30349          decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8;
30350        }
30351      } // Compactify. Considerable speedup on Firefox.
30352
30353
30354      for (i = 0; i < 5; i++) {
30355        encTable[i] = encTable[i].slice(0);
30356        decTable[i] = decTable[i].slice(0);
30357      }
30358
30359      return tables;
30360    };
30361
30362    let aesTables = null;
30363    /**
30364     * Schedule out an AES key for both encryption and decryption. This
30365     * is a low-level class. Use a cipher mode to do bulk encryption.
30366     *
30367     * @class AES
30368     * @param key {Array} The key as an array of 4, 6 or 8 words.
30369     */
30370
30371    class AES {
30372      constructor(key) {
30373        /**
30374        * The expanded S-box and inverse S-box tables. These will be computed
30375        * on the client so that we don't have to send them down the wire.
30376        *
30377        * There are two tables, _tables[0] is for encryption and
30378        * _tables[1] is for decryption.
30379        *
30380        * The first 4 sub-tables are the expanded S-box with MixColumns. The
30381        * last (_tables[01][4]) is the S-box itself.
30382        *
30383        * @private
30384        */
30385        // if we have yet to precompute the S-box tables
30386        // do so now
30387        if (!aesTables) {
30388          aesTables = precompute();
30389        } // then make a copy of that object for use
30390
30391
30392        this._tables = [[aesTables[0][0].slice(), aesTables[0][1].slice(), aesTables[0][2].slice(), aesTables[0][3].slice(), aesTables[0][4].slice()], [aesTables[1][0].slice(), aesTables[1][1].slice(), aesTables[1][2].slice(), aesTables[1][3].slice(), aesTables[1][4].slice()]];
30393        let i;
30394        let j;
30395        let tmp;
30396        const sbox = this._tables[0][4];
30397        const decTable = this._tables[1];
30398        const keyLen = key.length;
30399        let rcon = 1;
30400
30401        if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
30402          throw new Error('Invalid aes key size');
30403        }
30404
30405        const encKey = key.slice(0);
30406        const decKey = [];
30407        this._key = [encKey, decKey]; // schedule encryption keys
30408
30409        for (i = keyLen; i < 4 * keyLen + 28; i++) {
30410          tmp = encKey[i - 1]; // apply sbox
30411
30412          if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) {
30413            tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon
30414
30415            if (i % keyLen === 0) {
30416              tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24;
30417              rcon = rcon << 1 ^ (rcon >> 7) * 283;
30418            }
30419          }
30420
30421          encKey[i] = encKey[i - keyLen] ^ tmp;
30422        } // schedule decryption keys
30423
30424
30425        for (j = 0; i; j++, i--) {
30426          tmp = encKey[j & 3 ? i : i - 4];
30427
30428          if (i <= 4 || j < 4) {
30429            decKey[j] = tmp;
30430          } else {
30431            decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]];
30432          }
30433        }
30434      }
30435      /**
30436       * Decrypt 16 bytes, specified as four 32-bit words.
30437       *
30438       * @param {number} encrypted0 the first word to decrypt
30439       * @param {number} encrypted1 the second word to decrypt
30440       * @param {number} encrypted2 the third word to decrypt
30441       * @param {number} encrypted3 the fourth word to decrypt
30442       * @param {Int32Array} out the array to write the decrypted words
30443       * into
30444       * @param {number} offset the offset into the output array to start
30445       * writing results
30446       * @return {Array} The plaintext.
30447       */
30448
30449
30450      decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) {
30451        const key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data
30452
30453        let a = encrypted0 ^ key[0];
30454        let b = encrypted3 ^ key[1];
30455        let c = encrypted2 ^ key[2];
30456        let d = encrypted1 ^ key[3];
30457        let a2;
30458        let b2;
30459        let c2; // key.length === 2 ?
30460
30461        const nInnerRounds = key.length / 4 - 2;
30462        let i;
30463        let kIndex = 4;
30464        const table = this._tables[1]; // load up the tables
30465
30466        const table0 = table[0];
30467        const table1 = table[1];
30468        const table2 = table[2];
30469        const table3 = table[3];
30470        const sbox = table[4]; // Inner rounds. Cribbed from OpenSSL.
30471
30472        for (i = 0; i < nInnerRounds; i++) {
30473          a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex];
30474          b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1];
30475          c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2];
30476          d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3];
30477          kIndex += 4;
30478          a = a2;
30479          b = b2;
30480          c = c2;
30481        } // Last round.
30482
30483
30484        for (i = 0; i < 4; i++) {
30485          out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++];
30486          a2 = a;
30487          a = b;
30488          b = c;
30489          c = d;
30490          d = a2;
30491        }
30492      }
30493
30494    }
30495    /**
30496     * @file async-stream.js
30497     */
30498
30499    /**
30500     * A wrapper around the Stream class to use setTimeout
30501     * and run stream "jobs" Asynchronously
30502     *
30503     * @class AsyncStream
30504     * @extends Stream
30505     */
30506
30507
30508    class AsyncStream extends Stream {
30509      constructor() {
30510        super(Stream);
30511        this.jobs = [];
30512        this.delay = 1;
30513        this.timeout_ = null;
30514      }
30515      /**
30516       * process an async job
30517       *
30518       * @private
30519       */
30520
30521
30522      processJob_() {
30523        this.jobs.shift()();
30524
30525        if (this.jobs.length) {
30526          this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
30527        } else {
30528          this.timeout_ = null;
30529        }
30530      }
30531      /**
30532       * push a job into the stream
30533       *
30534       * @param {Function} job the job to push into the stream
30535       */
30536
30537
30538      push(job) {
30539        this.jobs.push(job);
30540
30541        if (!this.timeout_) {
30542          this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay);
30543        }
30544      }
30545
30546    }
30547    /**
30548     * @file decrypter.js
30549     *
30550     * An asynchronous implementation of AES-128 CBC decryption with
30551     * PKCS#7 padding.
30552     */
30553
30554    /**
30555     * Convert network-order (big-endian) bytes into their little-endian
30556     * representation.
30557     */
30558
30559
30560    const ntoh = function (word) {
30561      return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24;
30562    };
30563    /**
30564     * Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
30565     *
30566     * @param {Uint8Array} encrypted the encrypted bytes
30567     * @param {Uint32Array} key the bytes of the decryption key
30568     * @param {Uint32Array} initVector the initialization vector (IV) to
30569     * use for the first round of CBC.
30570     * @return {Uint8Array} the decrypted bytes
30571     *
30572     * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
30573     * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
30574     * @see https://tools.ietf.org/html/rfc2315
30575     */
30576
30577
30578    const decrypt = function (encrypted, key, initVector) {
30579      // word-level access to the encrypted bytes
30580      const encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2);
30581      const decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output
30582
30583      const decrypted = new Uint8Array(encrypted.byteLength);
30584      const decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and
30585      // decrypted data
30586
30587      let init0;
30588      let init1;
30589      let init2;
30590      let init3;
30591      let encrypted0;
30592      let encrypted1;
30593      let encrypted2;
30594      let encrypted3; // iteration variable
30595
30596      let wordIx; // pull out the words of the IV to ensure we don't modify the
30597      // passed-in reference and easier access
30598
30599      init0 = initVector[0];
30600      init1 = initVector[1];
30601      init2 = initVector[2];
30602      init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC)
30603      // to each decrypted block
30604
30605      for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) {
30606        // convert big-endian (network order) words into little-endian
30607        // (javascript order)
30608        encrypted0 = ntoh(encrypted32[wordIx]);
30609        encrypted1 = ntoh(encrypted32[wordIx + 1]);
30610        encrypted2 = ntoh(encrypted32[wordIx + 2]);
30611        encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block
30612
30613        decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the
30614        // plaintext
30615
30616        decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0);
30617        decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1);
30618        decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2);
30619        decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round
30620
30621        init0 = encrypted0;
30622        init1 = encrypted1;
30623        init2 = encrypted2;
30624        init3 = encrypted3;
30625      }
30626
30627      return decrypted;
30628    };
30629    /**
30630     * The `Decrypter` class that manages decryption of AES
30631     * data through `AsyncStream` objects and the `decrypt`
30632     * function
30633     *
30634     * @param {Uint8Array} encrypted the encrypted bytes
30635     * @param {Uint32Array} key the bytes of the decryption key
30636     * @param {Uint32Array} initVector the initialization vector (IV) to
30637     * @param {Function} done the function to run when done
30638     * @class Decrypter
30639     */
30640
30641
30642    class Decrypter {
30643      constructor(encrypted, key, initVector, done) {
30644        const step = Decrypter.STEP;
30645        const encrypted32 = new Int32Array(encrypted.buffer);
30646        const decrypted = new Uint8Array(encrypted.byteLength);
30647        let i = 0;
30648        this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously
30649
30650        this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
30651
30652        for (i = step; i < encrypted32.length; i += step) {
30653          initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]);
30654          this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted));
30655        } // invoke the done() callback when everything is finished
30656
30657
30658        this.asyncStream_.push(function () {
30659          // remove pkcs#7 padding from the decrypted bytes
30660          done(null, unpad(decrypted));
30661        });
30662      }
30663      /**
30664       * a getter for step the maximum number of bytes to process at one time
30665       *
30666       * @return {number} the value of step 32000
30667       */
30668
30669
30670      static get STEP() {
30671        // 4 * 8000;
30672        return 32000;
30673      }
30674      /**
30675       * @private
30676       */
30677
30678
30679      decryptChunk_(encrypted, key, initVector, decrypted) {
30680        return function () {
30681          const bytes = decrypt(encrypted, key, initVector);
30682          decrypted.set(bytes, encrypted.byteOffset);
30683        };
30684      }
30685
30686    }
30687
30688    var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
30689    var win;
30690
30691    if (typeof window !== "undefined") {
30692      win = window;
30693    } else if (typeof commonjsGlobal !== "undefined") {
30694      win = commonjsGlobal;
30695    } else if (typeof self !== "undefined") {
30696      win = self;
30697    } else {
30698      win = {};
30699    }
30700
30701    var window_1 = win;
30702
30703    var isArrayBufferView = function isArrayBufferView(obj) {
30704      if (ArrayBuffer.isView === 'function') {
30705        return ArrayBuffer.isView(obj);
30706      }
30707
30708      return obj && obj.buffer instanceof ArrayBuffer;
30709    };
30710
30711    var BigInt = window_1.BigInt || Number;
30712    [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
30713
30714    (function () {
30715      var a = new Uint16Array([0xFFCC]);
30716      var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
30717
30718      if (b[0] === 0xFF) {
30719        return 'big';
30720      }
30721
30722      if (b[0] === 0xCC) {
30723        return 'little';
30724      }
30725
30726      return 'unknown';
30727    })();
30728    /**
30729     * Creates an object for sending to a web worker modifying properties that are TypedArrays
30730     * into a new object with seperated properties for the buffer, byteOffset, and byteLength.
30731     *
30732     * @param {Object} message
30733     *        Object of properties and values to send to the web worker
30734     * @return {Object}
30735     *         Modified message with TypedArray values expanded
30736     * @function createTransferableMessage
30737     */
30738
30739
30740    const createTransferableMessage = function (message) {
30741      const transferable = {};
30742      Object.keys(message).forEach(key => {
30743        const value = message[key];
30744
30745        if (isArrayBufferView(value)) {
30746          transferable[key] = {
30747            bytes: value.buffer,
30748            byteOffset: value.byteOffset,
30749            byteLength: value.byteLength
30750          };
30751        } else {
30752          transferable[key] = value;
30753        }
30754      });
30755      return transferable;
30756    };
30757    /* global self */
30758
30759    /**
30760     * Our web worker interface so that things can talk to aes-decrypter
30761     * that will be running in a web worker. the scope is passed to this by
30762     * webworkify.
30763     */
30764
30765
30766    self.onmessage = function (event) {
30767      const data = event.data;
30768      const encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength);
30769      const key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4);
30770      const iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4);
30771      /* eslint-disable no-new, handle-callback-err */
30772
30773      new Decrypter(encrypted, key, iv, function (err, bytes) {
30774        self.postMessage(createTransferableMessage({
30775          source: data.source,
30776          decrypted: bytes
30777        }), [bytes.buffer]);
30778      });
30779      /* eslint-enable */
30780    };
30781  }));
30782  var Decrypter = factory(workerCode);
30783  /* rollup-plugin-worker-factory end for worker!/
30783home/runner/work/http-streaming
vendor: 89,495 bytes, lines 30783-33368
30783/http-streaming/src/decrypter-worker.js */
30784
30785  /**
30786   * Convert the properties of an HLS track into an audioTrackKind.
30787   *
30788   * @private
30789   */
30790
30791  const audioTrackKind_ = properties => {
30792    let kind = properties.default ? 'main' : 'alternative';
30793
30794    if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) {
30795      kind = 'main-desc';
30796    }
30797
30798    return kind;
30799  };
30800  /**
30801   * Pause provided segment loader and playlist loader if active
30802   *
30803   * @param {SegmentLoader} segmentLoader
30804   *        SegmentLoader to pause
30805   * @param {Object} mediaType
30806   *        Active media type
30807   * @function stopLoaders
30808   */
30809
30810
30811  const stopLoaders = (segmentLoader, mediaType) => {
30812    segmentLoader.abort();
30813    segmentLoader.pause();
30814
30815    if (mediaType && mediaType.activePlaylistLoader) {
30816      mediaType.activePlaylistLoader.pause();
30817      mediaType.activePlaylistLoader = null;
30818    }
30819  };
30820  /**
30821   * Start loading provided segment loader and playlist loader
30822   *
30823   * @param {PlaylistLoader} playlistLoader
30824   *        PlaylistLoader to start loading
30825   * @param {Object} mediaType
30826   *        Active media type
30827   * @function startLoaders
30828   */
30829
30830  const startLoaders = (playlistLoader, mediaType) => {
30831    // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the
30832    // playlist loader
30833    mediaType.activePlaylistLoader = playlistLoader;
30834    playlistLoader.load();
30835  };
30836  /**
30837   * Returns a function to be called when the media group changes. It performs a
30838   * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a
30839   * change of group is merely a rendition switch of the same content at another encoding,
30840   * rather than a change of content, such as switching audio from English to Spanish.
30841   *
30842   * @param {string} type
30843   *        MediaGroup type
30844   * @param {Object} settings
30845   *        Object containing required information for media groups
30846   * @return {Function}
30847   *         Handler for a non-destructive resync of SegmentLoader when the active media
30848   *         group changes.
30849   * @function onGroupChanged
30850   */
30851
30852  const onGroupChanged = (type, settings) => () => {
30853    const {
30854      segmentLoaders: {
30855        [type]: segmentLoader,
30856        main: mainSegmentLoader
30857      },
30858      mediaTypes: {
30859        [type]: mediaType
30860      }
30861    } = settings;
30862    const activeTrack = mediaType.activeTrack();
30863    const activeGroup = mediaType.getActiveGroup();
30864    const previousActiveLoader = mediaType.activePlaylistLoader;
30865    const lastGroup = mediaType.lastGroup_; // the group did not change do nothing
30866
30867    if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) {
30868      return;
30869    }
30870
30871    mediaType.lastGroup_ = activeGroup;
30872    mediaType.lastTrack_ = activeTrack;
30873    stopLoaders(segmentLoader, mediaType);
30874
30875    if (!activeGroup || activeGroup.isMainPlaylist) {
30876      // there is no group active or active group is a main playlist and won't change
30877      return;
30878    }
30879
30880    if (!activeGroup.playlistLoader) {
30881      if (previousActiveLoader) {
30882        // The previous group had a playlist loader but the new active group does not
30883        // this means we are switching from demuxed to muxed audio. In this case we want to
30884        // do a destructive reset of the main segment loader and not restart the audio
30885        // loaders.
30886        mainSegmentLoader.resetEverything();
30887      }
30888
30889      return;
30890    } // Non-destructive resync
30891
30892
30893    segmentLoader.resyncLoader();
30894    startLoaders(activeGroup.playlistLoader, mediaType);
30895  };
30896  const onGroupChanging = (type, settings) => () => {
30897    const {
30898      segmentLoaders: {
30899        [type]: segmentLoader
30900      },
30901      mediaTypes: {
30902        [type]: mediaType
30903      }
30904    } = settings;
30905    mediaType.lastGroup_ = null;
30906    segmentLoader.abort();
30907    segmentLoader.pause();
30908  };
30909  /**
30910   * Returns a function to be called when the media track changes. It performs a
30911   * destructive reset of the SegmentLoader to ensure we start loading as close to
30912   * currentTime as possible.
30913   *
30914   * @param {string} type
30915   *        MediaGroup type
30916   * @param {Object} settings
30917   *        Object containing required information for media groups
30918   * @return {Function}
30919   *         Handler for a destructive reset of SegmentLoader when the active media
30920   *         track changes.
30921   * @function onTrackChanged
30922   */
30923
30924  const onTrackChanged = (type, settings) => () => {
30925    const {
30926      mainPlaylistLoader,
30927      segmentLoaders: {
30928        [type]: segmentLoader,
30929        main: mainSegmentLoader
30930      },
30931      mediaTypes: {
30932        [type]: mediaType
30933      }
30934    } = settings;
30935    const activeTrack = mediaType.activeTrack();
30936    const activeGroup = mediaType.getActiveGroup();
30937    const previousActiveLoader = mediaType.activePlaylistLoader;
30938    const lastTrack = mediaType.lastTrack_; // track did not change, do nothing
30939
30940    if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) {
30941      return;
30942    }
30943
30944    mediaType.lastGroup_ = activeGroup;
30945    mediaType.lastTrack_ = activeTrack;
30946    stopLoaders(segmentLoader, mediaType);
30947
30948    if (!activeGroup) {
30949      // there is no group active so we do not want to restart loaders
30950      return;
30951    }
30952
30953    if (activeGroup.isMainPlaylist) {
30954      // track did not change, do nothing
30955      if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) {
30956        return;
30957      }
30958
30959      const pc = settings.vhs.playlistController_;
30960      const newPlaylist = pc.selectPlaylist(); // media will not change do nothing
30961
30962      if (pc.media() === newPlaylist) {
30963        return;
30964      }
30965
30966      mediaType.logger_(`track change. Switching main audio from ${lastTrack.id} to ${activeTrack.id}`);
30967      mainPlaylistLoader.pause();
30968      mainSegmentLoader.resetEverything();
30969      pc.fastQualityChange_(newPlaylist);
30970      return;
30971    }
30972
30973    if (type === 'AUDIO') {
30974      if (!activeGroup.playlistLoader) {
30975        // when switching from demuxed audio/video to muxed audio/video (noted by no
30976        // playlist loader for the audio group), we want to do a destructive reset of the
30977        // main segment loader and not restart the audio loaders
30978        mainSegmentLoader.setAudio(true); // don't have to worry about disabling the audio of the audio segment loader since
30979        // it should be stopped
30980
30981        mainSegmentLoader.resetEverything();
30982        return;
30983      } // although the segment loader is an audio segment loader, call the setAudio
30984      // function to ensure it is prepared to re-append the init segment (or handle other
30985      // config changes)
30986
30987
30988      segmentLoader.setAudio(true);
30989      mainSegmentLoader.setAudio(false);
30990    }
30991
30992    if (previousActiveLoader === activeGroup.playlistLoader) {
30993      // Nothing has actually changed. This can happen because track change events can fire
30994      // multiple times for a "single" change. One for enabling the new active track, and
30995      // one for disabling the track that was active
30996      startLoaders(activeGroup.playlistLoader, mediaType);
30997      return;
30998    }
30999
31000    if (segmentLoader.track) {
31001      // For WebVTT, set the new text track in the segmentloader
31002      segmentLoader.track(activeTrack);
31003    } // destructive reset
31004
31005
31006    segmentLoader.resetEverything();
31007    startLoaders(activeGroup.playlistLoader, mediaType);
31008  };
31009  const onError = {
31010    /**
31011     * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
31012     * an error.
31013     *
31014     * @param {string} type
31015     *        MediaGroup type
31016     * @param {Object} settings
31017     *        Object containing required information for media groups
31018     * @return {Function}
31019     *         Error handler. Logs warning (or error if the playlist is excluded) to
31020     *         console and switches back to default audio track.
31021     * @function onError.AUDIO
31022     */
31023    AUDIO: (type, settings) => () => {
31024      const {
31025        mediaTypes: {
31026          [type]: mediaType
31027        },
31028        excludePlaylist
31029      } = settings; // switch back to default audio track
31030
31031      const activeTrack = mediaType.activeTrack();
31032      const activeGroup = mediaType.activeGroup();
31033      const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id;
31034      const defaultTrack = mediaType.tracks[id];
31035
31036      if (activeTrack === defaultTrack) {
31037        // Default track encountered an error. All we can do now is exclude the current
31038        // rendition and hope another will switch audio groups
31039        excludePlaylist({
31040          error: {
31041            message: 'Problem encountered loading the default audio track.'
31042          }
31043        });
31044        return;
31045      }
31046
31047      videojs__default["default"].log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.');
31048
31049      for (const trackId in mediaType.tracks) {
31050        mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack;
31051      }
31052
31053      mediaType.onTrackChanged();
31054    },
31055
31056    /**
31057     * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters
31058     * an error.
31059     *
31060     * @param {string} type
31061     *        MediaGroup type
31062     * @param {Object} settings
31063     *        Object containing required information for media groups
31064     * @return {Function}
31065     *         Error handler. Logs warning to console and disables the active subtitle track
31066     * @function onError.SUBTITLES
31067     */
31068    SUBTITLES: (type, settings) => () => {
31069      const {
31070        mediaTypes: {
31071          [type]: mediaType
31072        }
31073      } = settings;
31074      videojs__default["default"].log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.');
31075      const track = mediaType.activeTrack();
31076
31077      if (track) {
31078        track.mode = 'disabled';
31079      }
31080
31081      mediaType.onTrackChanged();
31082    }
31083  };
31084  const setupListeners = {
31085    /**
31086     * Setup event listeners for audio playlist loader
31087     *
31088     * @param {string} type
31089     *        MediaGroup type
31090     * @param {PlaylistLoader|null} playlistLoader
31091     *        PlaylistLoader to register listeners on
31092     * @param {Object} settings
31093     *        Object containing required information for media groups
31094     * @function setupListeners.AUDIO
31095     */
31096    AUDIO: (type, playlistLoader, settings) => {
31097      if (!playlistLoader) {
31098        // no playlist loader means audio will be muxed with the video
31099        return;
31100      }
31101
31102      const {
31103        tech,
31104        requestOptions,
31105        segmentLoaders: {
31106          [type]: segmentLoader
31107        }
31108      } = settings;
31109      playlistLoader.on('loadedmetadata', () => {
31110        const media = playlistLoader.media();
31111        segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload
31112        // permits, start downloading segments
31113
31114        if (!tech.paused() || media.endList && tech.preload() !== 'none') {
31115          segmentLoader.load();
31116        }
31117      });
31118      playlistLoader.on('loadedplaylist', () => {
31119        segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running
31120
31121        if (!tech.paused()) {
31122          segmentLoader.load();
31123        }
31124      });
31125      playlistLoader.on('error', onError[type](type, settings));
31126    },
31127
31128    /**
31129     * Setup event listeners for subtitle playlist loader
31130     *
31131     * @param {string} type
31132     *        MediaGroup type
31133     * @param {PlaylistLoader|null} playlistLoader
31134     *        PlaylistLoader to register listeners on
31135     * @param {Object} settings
31136     *        Object containing required information for media groups
31137     * @function setupListeners.SUBTITLES
31138     */
31139    SUBTITLES: (type, playlistLoader, settings) => {
31140      const {
31141        tech,
31142        requestOptions,
31143        segmentLoaders: {
31144          [type]: segmentLoader
31145        },
31146        mediaTypes: {
31147          [type]: mediaType
31148        }
31149      } = settings;
31150      playlistLoader.on('loadedmetadata', () => {
31151        const media = playlistLoader.media();
31152        segmentLoader.playlist(media, requestOptions);
31153        segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload
31154        // permits, start downloading segments
31155
31156        if (!tech.paused() || media.endList && tech.preload() !== 'none') {
31157          segmentLoader.load();
31158        }
31159      });
31160      playlistLoader.on('loadedplaylist', () => {
31161        segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running
31162
31163        if (!tech.paused()) {
31164          segmentLoader.load();
31165        }
31166      });
31167      playlistLoader.on('error', onError[type](type, settings));
31168    }
31169  };
31170  const initialize = {
31171    /**
31172     * Setup PlaylistLoaders and AudioTracks for the audio groups
31173     *
31174     * @param {string} type
31175     *        MediaGroup type
31176     * @param {Object} settings
31177     *        Object containing required information for media groups
31178     * @function initialize.AUDIO
31179     */
31180    'AUDIO': (type, settings) => {
31181      const {
31182        vhs,
31183        sourceType,
31184        segmentLoaders: {
31185          [type]: segmentLoader
31186        },
31187        requestOptions,
31188        main: {
31189          mediaGroups
31190        },
31191        mediaTypes: {
31192          [type]: {
31193            groups,
31194            tracks,
31195            logger_
31196          }
31197        },
31198        mainPlaylistLoader
31199      } = settings;
31200      const audioOnlyMain = isAudioOnly(mainPlaylistLoader.main); // force a default if we have none
31201
31202      if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) {
31203        mediaGroups[type] = {
31204          main: {
31205            default: {
31206              default: true
31207            }
31208          }
31209        };
31210
31211        if (audioOnlyMain) {
31212          mediaGroups[type].main.default.playlists = mainPlaylistLoader.main.playlists;
31213        }
31214      }
31215
31216      for (const groupId in mediaGroups[type]) {
31217        if (!groups[groupId]) {
31218          groups[groupId] = [];
31219        }
31220
31221        for (const variantLabel in mediaGroups[type][groupId]) {
31222          let properties = mediaGroups[type][groupId][variantLabel];
31223          let playlistLoader;
31224
31225          if (audioOnlyMain) {
31226            logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a main playlist`);
31227            properties.isMainPlaylist = true;
31228            playlistLoader = null; // if vhs-json was provided as the source, and the media playlist was resolved,
31229            // use the resolved media playlist object
31230          } else if (sourceType === 'vhs-json' && properties.playlists) {
31231            playlistLoader = new PlaylistLoader(properties.playlists[0], vhs, requestOptions);
31232          } else if (properties.resolvedUri) {
31233            playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); // TODO: dash isn't the only type with properties.playlists
31234            // should we even have properties.playlists in this check.
31235          } else if (properties.playlists && sourceType === 'dash') {
31236            playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader);
31237          } else {
31238            // no resolvedUri means the audio is muxed with the video when using this
31239            // audio track
31240            playlistLoader = null;
31241          }
31242
31243          properties = merge$1({
31244            id: variantLabel,
31245            playlistLoader
31246          }, properties);
31247          setupListeners[type](type, properties.playlistLoader, settings);
31248          groups[groupId].push(properties);
31249
31250          if (typeof tracks[variantLabel] === 'undefined') {
31251            const track = new videojs__default["default"].AudioTrack({
31252              id: variantLabel,
31253              kind: audioTrackKind_(properties),
31254              enabled: false,
31255              language: properties.language,
31256              default: properties.default,
31257              label: variantLabel
31258            });
31259            tracks[variantLabel] = track;
31260          }
31261        }
31262      } // setup single error event handler for the segment loader
31263
31264
31265      segmentLoader.on('error', onError[type](type, settings));
31266    },
31267
31268    /**
31269     * Setup PlaylistLoaders and TextTracks for the subtitle groups
31270     *
31271     * @param {string} type
31272     *        MediaGroup type
31273     * @param {Object} settings
31274     *        Object containing required information for media groups
31275     * @function initialize.SUBTITLES
31276     */
31277    'SUBTITLES': (type, settings) => {
31278      const {
31279        tech,
31280        vhs,
31281        sourceType,
31282        segmentLoaders: {
31283          [type]: segmentLoader
31284        },
31285        requestOptions,
31286        main: {
31287          mediaGroups
31288        },
31289        mediaTypes: {
31290          [type]: {
31291            groups,
31292            tracks
31293          }
31294        },
31295        mainPlaylistLoader
31296      } = settings;
31297
31298      for (const groupId in mediaGroups[type]) {
31299        if (!groups[groupId]) {
31300          groups[groupId] = [];
31301        }
31302
31303        for (const variantLabel in mediaGroups[type][groupId]) {
31304          if (!vhs.options_.useForcedSubtitles && mediaGroups[type][groupId][variantLabel].forced) {
31305            // Subtitle playlists with the forced attribute are not selectable in Safari.
31306            // According to Apple's HLS Authoring Specification:
31307            //   If content has forced subtitles and regular subtitles in a given language,
31308            //   the regular subtitles track in that language MUST contain both the forced
31309            //   subtitles and the regular subtitles for that language.
31310            // Because of this requirement and that Safari does not add forced subtitles,
31311            // forced subtitles are skipped here to maintain consistent experience across
31312            // all platforms
31313            continue;
31314          }
31315
31316          let properties = mediaGroups[type][groupId][variantLabel];
31317          let playlistLoader;
31318
31319          if (sourceType === 'hls') {
31320            playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions);
31321          } else if (sourceType === 'dash') {
31322            const playlists = properties.playlists.filter(p => p.excludeUntil !== Infinity);
31323
31324            if (!playlists.length) {
31325              return;
31326            }
31327
31328            playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader);
31329          } else if (sourceType === 'vhs-json') {
31330            playlistLoader = new PlaylistLoader( // if the vhs-json object included the media playlist, use the media playlist
31331            // as provided, otherwise use the resolved URI to load the playlist
31332            properties.playlists ? properties.playlists[0] : properties.resolvedUri, vhs, requestOptions);
31333          }
31334
31335          properties = merge$1({
31336            id: variantLabel,
31337            playlistLoader
31338          }, properties);
31339          setupListeners[type](type, properties.playlistLoader, settings);
31340          groups[groupId].push(properties);
31341
31342          if (typeof tracks[variantLabel] === 'undefined') {
31343            const track = tech.addRemoteTextTrack({
31344              id: variantLabel,
31345              kind: 'subtitles',
31346              default: properties.default && properties.autoselect,
31347              language: properties.language,
31348              label: variantLabel
31349            }, false).track;
31350            tracks[variantLabel] = track;
31351          }
31352        }
31353      } // setup single error event handler for the segment loader
31354
31355
31356      segmentLoader.on('error', onError[type](type, settings));
31357    },
31358
31359    /**
31360     * Setup TextTracks for the closed-caption groups
31361     *
31362     * @param {String} type
31363     *        MediaGroup type
31364     * @param {Object} settings
31365     *        Object containing required information for media groups
31366     * @function initialize['CLOSED-CAPTIONS']
31367     */
31368    'CLOSED-CAPTIONS': (type, settings) => {
31369      const {
31370        tech,
31371        main: {
31372          mediaGroups
31373        },
31374        mediaTypes: {
31375          [type]: {
31376            groups,
31377            tracks
31378          }
31379        }
31380      } = settings;
31381
31382      for (const groupId in mediaGroups[type]) {
31383        if (!groups[groupId]) {
31384          groups[groupId] = [];
31385        }
31386
31387        for (const variantLabel in mediaGroups[type][groupId]) {
31388          const properties = mediaGroups[type][groupId][variantLabel]; // Look for either 608 (CCn) or 708 (SERVICEn) caption services
31389
31390          if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) {
31391            continue;
31392          }
31393
31394          const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {};
31395          let newProps = {
31396            label: variantLabel,
31397            language: properties.language,
31398            instreamId: properties.instreamId,
31399            default: properties.default && properties.autoselect
31400          };
31401
31402          if (captionServices[newProps.instreamId]) {
31403            newProps = merge$1(newProps, captionServices[newProps.instreamId]);
31404          }
31405
31406          if (newProps.default === undefined) {
31407            delete newProps.default;
31408          } // No PlaylistLoader is required for Closed-Captions because the captions are
31409          // embedded within the video stream
31410
31411
31412          groups[groupId].push(merge$1({
31413            id: variantLabel
31414          }, properties));
31415
31416          if (typeof tracks[variantLabel] === 'undefined') {
31417            const track = tech.addRemoteTextTrack({
31418              id: newProps.instreamId,
31419              kind: 'captions',
31420              default: newProps.default,
31421              language: newProps.language,
31422              label: newProps.label
31423            }, false).track;
31424            tracks[variantLabel] = track;
31425          }
31426        }
31427      }
31428    }
31429  };
31430
31431  const groupMatch = (list, media) => {
31432    for (let i = 0; i < list.length; i++) {
31433      if (playlistMatch(media, list[i])) {
31434        return true;
31435      }
31436
31437      if (list[i].playlists && groupMatch(list[i].playlists, media)) {
31438        return true;
31439      }
31440    }
31441
31442    return false;
31443  };
31444  /**
31445   * Returns a function used to get the active group of the provided type
31446   *
31447   * @param {string} type
31448   *        MediaGroup type
31449   * @param {Object} settings
31450   *        Object containing required information for media groups
31451   * @return {Function}
31452   *         Function that returns the active media group for the provided type. Takes an
31453   *         optional parameter {TextTrack} track. If no track is provided, a list of all
31454   *         variants in the group, otherwise the variant corresponding to the provided
31455   *         track is returned.
31456   * @function activeGroup
31457   */
31458
31459
31460  const activeGroup = (type, settings) => track => {
31461    const {
31462      mainPlaylistLoader,
31463      mediaTypes: {
31464        [type]: {
31465          groups
31466        }
31467      }
31468    } = settings;
31469    const media = mainPlaylistLoader.media();
31470
31471    if (!media) {
31472      return null;
31473    }
31474
31475    let variants = null; // set to variants to main media active group
31476
31477    if (media.attributes[type]) {
31478      variants = groups[media.attributes[type]];
31479    }
31480
31481    const groupKeys = Object.keys(groups);
31482
31483    if (!variants) {
31484      // find the mainPlaylistLoader media
31485      // that is in a media group if we are dealing
31486      // with audio only
31487      if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.main)) {
31488        for (let i = 0; i < groupKeys.length; i++) {
31489          const groupPropertyList = groups[groupKeys[i]];
31490
31491          if (groupMatch(groupPropertyList, media)) {
31492            variants = groupPropertyList;
31493            break;
31494          }
31495        } // use the main group if it exists
31496
31497      } else if (groups.main) {
31498        variants = groups.main; // only one group, use that one
31499      } else if (groupKeys.length === 1) {
31500        variants = groups[groupKeys[0]];
31501      }
31502    }
31503
31504    if (typeof track === 'undefined') {
31505      return variants;
31506    }
31507
31508    if (track === null || !variants) {
31509      // An active track was specified so a corresponding group is expected. track === null
31510      // means no track is currently active so there is no corresponding group
31511      return null;
31512    }
31513
31514    return variants.filter(props => props.id === track.id)[0] || null;
31515  };
31516  const activeTrack = {
31517    /**
31518     * Returns a function used to get the active track of type provided
31519     *
31520     * @param {string} type
31521     *        MediaGroup type
31522     * @param {Object} settings
31523     *        Object containing required information for media groups
31524     * @return {Function}
31525     *         Function that returns the active media track for the provided type. Returns
31526     *         null if no track is active
31527     * @function activeTrack.AUDIO
31528     */
31529    AUDIO: (type, settings) => () => {
31530      const {
31531        mediaTypes: {
31532          [type]: {
31533            tracks
31534          }
31535        }
31536      } = settings;
31537
31538      for (const id in tracks) {
31539        if (tracks[id].enabled) {
31540          return tracks[id];
31541        }
31542      }
31543
31544      return null;
31545    },
31546
31547    /**
31548     * Returns a function used to get the active track of type provided
31549     *
31550     * @param {string} type
31551     *        MediaGroup type
31552     * @param {Object} settings
31553     *        Object containing required information for media groups
31554     * @return {Function}
31555     *         Function that returns the active media track for the provided type. Returns
31556     *         null if no track is active
31557     * @function activeTrack.SUBTITLES
31558     */
31559    SUBTITLES: (type, settings) => () => {
31560      const {
31561        mediaTypes: {
31562          [type]: {
31563            tracks
31564          }
31565        }
31566      } = settings;
31567
31568      for (const id in tracks) {
31569        if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') {
31570          return tracks[id];
31571        }
31572      }
31573
31574      return null;
31575    }
31576  };
31577  const getActiveGroup = (type, {
31578    mediaTypes
31579  }) => () => {
31580    const activeTrack_ = mediaTypes[type].activeTrack();
31581
31582    if (!activeTrack_) {
31583      return null;
31584    }
31585
31586    return mediaTypes[type].activeGroup(activeTrack_);
31587  };
31588  /**
31589   * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles,
31590   * Closed-Captions) specified in the main manifest.
31591   *
31592   * @param {Object} settings
31593   *        Object containing required information for setting up the media groups
31594   * @param {Tech} settings.tech
31595   *        The tech of the player
31596   * @param {Object} settings.requestOptions
31597   *        XHR request options used by the segment loaders
31598   * @param {PlaylistLoader} settings.mainPlaylistLoader
31599   *        PlaylistLoader for the main source
31600   * @param {VhsHandler} settings.vhs
31601   *        VHS SourceHandler
31602   * @param {Object} settings.main
31603   *        The parsed main manifest
31604   * @param {Object} settings.mediaTypes
31605   *        Object to store the loaders, tracks, and utility methods for each media type
31606   * @param {Function} settings.excludePlaylist
31607   *        Excludes the current rendition and forces a rendition switch.
31608   * @function setupMediaGroups
31609   */
31610
31611  const setupMediaGroups = settings => {
31612    ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => {
31613      initialize[type](type, settings);
31614    });
31615    const {
31616      mediaTypes,
31617      mainPlaylistLoader,
31618      tech,
31619      vhs,
31620      segmentLoaders: {
31621        ['AUDIO']: audioSegmentLoader,
31622        main: mainSegmentLoader
31623      }
31624    } = settings; // setup active group and track getters and change event handlers
31625
31626    ['AUDIO', 'SUBTITLES'].forEach(type => {
31627      mediaTypes[type].activeGroup = activeGroup(type, settings);
31628      mediaTypes[type].activeTrack = activeTrack[type](type, settings);
31629      mediaTypes[type].onGroupChanged = onGroupChanged(type, settings);
31630      mediaTypes[type].onGroupChanging = onGroupChanging(type, settings);
31631      mediaTypes[type].onTrackChanged = onTrackChanged(type, settings);
31632      mediaTypes[type].getActiveGroup = getActiveGroup(type, settings);
31633    }); // DO NOT enable the default subtitle or caption track.
31634    // DO enable the default audio track
31635
31636    const audioGroup = mediaTypes.AUDIO.activeGroup();
31637
31638    if (audioGroup) {
31639      const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id;
31640      mediaTypes.AUDIO.tracks[groupId].enabled = true;
31641      mediaTypes.AUDIO.onGroupChanged();
31642      mediaTypes.AUDIO.onTrackChanged();
31643      const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup(); // a similar check for handling setAudio on each loader is run again each time the
31644      // track is changed, but needs to be handled here since the track may not be considered
31645      // changed on the first call to onTrackChanged
31646
31647      if (!activeAudioGroup.playlistLoader) {
31648        // either audio is muxed with video or the stream is audio only
31649        mainSegmentLoader.setAudio(true);
31650      } else {
31651        // audio is demuxed
31652        mainSegmentLoader.setAudio(false);
31653        audioSegmentLoader.setAudio(true);
31654      }
31655    }
31656
31657    mainPlaylistLoader.on('mediachange', () => {
31658      ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged());
31659    });
31660    mainPlaylistLoader.on('mediachanging', () => {
31661      ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanging());
31662    }); // custom audio track change event handler for usage event
31663
31664    const onAudioTrackChanged = () => {
31665      mediaTypes.AUDIO.onTrackChanged();
31666      tech.trigger({
31667        type: 'usage',
31668        name: 'vhs-audio-change'
31669      });
31670    };
31671
31672    tech.audioTracks().addEventListener('change', onAudioTrackChanged);
31673    tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged);
31674    vhs.on('dispose', () => {
31675      tech.audioTracks().removeEventListener('change', onAudioTrackChanged);
31676      tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged);
31677    }); // clear existing audio tracks and add the ones we just created
31678
31679    tech.clearTracks('audio');
31680
31681    for (const id in mediaTypes.AUDIO.tracks) {
31682      tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]);
31683    }
31684  };
31685  /**
31686   * Creates skeleton object used to store the loaders, tracks, and utility methods for each
31687   * media type
31688   *
31689   * @return {Object}
31690   *         Object to store the loaders, tracks, and utility methods for each media type
31691   * @function createMediaTypes
31692   */
31693
31694  const createMediaTypes = () => {
31695    const mediaTypes = {};
31696    ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => {
31697      mediaTypes[type] = {
31698        groups: {},
31699        tracks: {},
31700        activePlaylistLoader: null,
31701        activeGroup: noop,
31702        activeTrack: noop,
31703        getActiveGroup: noop,
31704        onGroupChanged: noop,
31705        onTrackChanged: noop,
31706        lastTrack_: null,
31707        logger_: logger(`MediaGroups[${type}]`)
31708      };
31709    });
31710    return mediaTypes;
31711  };
31712
31713  /**
31714   * A utility class for setting properties and maintaining the state of the content steering manifest.
31715   *
31716   * Content Steering manifest format:
31717   * VERSION: number (required) currently only version 1 is supported.
31718   * TTL: number in seconds (optional) until the next content steering manifest reload.
31719   * RELOAD-URI: string (optional) uri to fetch the next content steering manifest.
31720   * SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values.
31721   * PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists.
31722   */
31723
31724  class SteeringManifest {
31725    constructor() {
31726      this.priority_ = [];
31727      this.pathwayClones_ = new Map();
31728    }
31729
31730    set version(number) {
31731      // Only version 1 is currently supported for both DASH and HLS.
31732      if (number === 1) {
31733        this.version_ = number;
31734      }
31735    }
31736
31737    set ttl(seconds) {
31738      // TTL = time-to-live, default = 300 seconds.
31739      this.ttl_ = seconds || 300;
31740    }
31741
31742    set reloadUri(uri) {
31743      if (uri) {
31744        // reload URI can be relative to the previous reloadUri.
31745        this.reloadUri_ = resolveUrl(this.reloadUri_, uri);
31746      }
31747    }
31748
31749    set priority(array) {
31750      // priority must be non-empty and unique values.
31751      if (array && array.length) {
31752        this.priority_ = array;
31753      }
31754    }
31755
31756    set pathwayClones(array) {
31757      // pathwayClones must be non-empty.
31758      if (array && array.length) {
31759        this.pathwayClones_ = new Map(array.map(clone => [clone.ID, clone]));
31760      }
31761    }
31762
31763    get version() {
31764      return this.version_;
31765    }
31766
31767    get ttl() {
31768      return this.ttl_;
31769    }
31770
31771    get reloadUri() {
31772      return this.reloadUri_;
31773    }
31774
31775    get priority() {
31776      return this.priority_;
31777    }
31778
31779    get pathwayClones() {
31780      return this.pathwayClones_;
31781    }
31782
31783  }
31784  /**
31785   * This class represents a content steering manifest and associated state. See both HLS and DASH specifications.
31786   * HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and
31787   * https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6.
31788   * DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf
31789   *
31790   * @param {function} xhr for making a network request from the browser.
31791   * @param {function} bandwidth for fetching the current bandwidth from the main segment loader.
31792   */
31793
31794
31795  class ContentSteeringController extends videojs__default["default"].EventTarget {
31796    constructor(xhr, bandwidth) {
31797      super();
31798      this.currentPathway = null;
31799      this.defaultPathway = null;
31800      this.queryBeforeStart = false;
31801      this.availablePathways_ = new Set();
31802      this.steeringManifest = new SteeringManifest();
31803      this.proxyServerUrl_ = null;
31804      this.manifestType_ = null;
31805      this.ttlTimeout_ = null;
31806      this.request_ = null;
31807      this.currentPathwayClones = new Map();
31808      this.nextPathwayClones = new Map();
31809      this.excludedSteeringManifestURLs = new Set();
31810      this.logger_ = logger('Content Steering');
31811      this.xhr_ = xhr;
31812      this.getBandwidth_ = bandwidth;
31813    }
31814    /**
31815     * Assigns the content steering tag properties to the steering controller
31816     *
31817     * @param {string} baseUrl the baseURL from the main manifest for resolving the steering manifest url
31818     * @param {Object} steeringTag the content steering tag from the main manifest
31819     */
31820
31821
31822    assignTagProperties(baseUrl, steeringTag) {
31823      this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH'; // serverUri is HLS serverURL is DASH
31824
31825      const steeringUri = steeringTag.serverUri || steeringTag.serverURL;
31826
31827      if (!steeringUri) {
31828        this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`);
31829        this.trigger('error');
31830        return;
31831      } // Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case.
31832
31833
31834      if (steeringUri.startsWith('data:')) {
31835        this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1));
31836        return;
31837      } // reloadUri is the resolution of the main manifest URL and steering URL.
31838
31839
31840      this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri); // pathwayId is HLS defaultServiceLocation is DASH
31841
31842      this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation; // currently only DASH supports the following properties on <ContentSteering> tags.
31843
31844      this.queryBeforeStart = steeringTag.queryBeforeStart;
31845      this.proxyServerUrl_ = steeringTag.proxyServerURL; // trigger a steering event if we have a pathway from the content steering tag.
31846      // this tells VHS which segment pathway to start with.
31847      // If queryBeforeStart is true we need to wait for the steering manifest response.
31848
31849      if (this.defaultPathway && !this.queryBeforeStart) {
31850        this.trigger('content-steering');
31851      }
31852    }
31853    /**
31854     * Requests the content steering manifest and parse the response. This should only be called after
31855     * assignTagProperties was called with a content steering tag.
31856     *
31857     * @param {string} initialUri The optional uri to make the request with.
31858     *    If set, the request should be made with exactly what is passed in this variable.
31859     *    This scenario should only happen once on initalization.
31860     */
31861
31862
31863    requestSteeringManifest(initial) {
31864      const reloadUri = this.steeringManifest.reloadUri;
31865
31866      if (!reloadUri) {
31867        return;
31868      } // We currently don't support passing MPD query parameters directly to the content steering URL as this requires
31869      // ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1.
31870      // This request URI accounts for manifest URIs that have been excluded.
31871
31872
31873      const uri = initial ? reloadUri : this.getRequestURI(reloadUri); // If there are no valid manifest URIs, we should stop content steering.
31874
31875      if (!uri) {
31876        this.logger_('No valid content steering manifest URIs. Stopping content steering.');
31877        this.trigger('error');
31878        this.dispose();
31879        return;
31880      }
31881
31882      const metadata = {
31883        contentSteeringInfo: {
31884          uri
31885        }
31886      };
31887      this.trigger({
31888        type: 'contentsteeringloadstart',
31889        metadata
31890      });
31891      this.request_ = this.xhr_({
31892        uri,
31893        requestType: 'content-steering-manifest'
31894      }, (error, errorInfo) => {
31895        if (error) {
31896          // If the client receives HTTP 410 Gone in response to a manifest request,
31897          // it MUST NOT issue another request for that URI for the remainder of the
31898          // playback session. It MAY continue to use the most-recently obtained set
31899          // of Pathways.
31900          if (errorInfo.status === 410) {
31901            this.logger_(`manifest request 410 ${error}.`);
31902            this.logger_(`There will be no more content steering requests to ${uri} this session.`);
31903            this.excludedSteeringManifestURLs.add(uri);
31904            return;
31905          } // If the client receives HTTP 429 Too Many Requests with a Retry-After
31906          // header in response to a manifest request, it SHOULD wait until the time
31907          // specified by the Retry-After header to reissue the request.
31908
31909
31910          if (errorInfo.status === 429) {
31911            const retrySeconds = errorInfo.responseHeaders['retry-after'];
31912            this.logger_(`manifest request 429 ${error}.`);
31913            this.logger_(`content steering will retry in ${retrySeconds} seconds.`);
31914            this.startTTLTimeout_(parseInt(retrySeconds, 10));
31915            return;
31916          } // If the Steering Manifest cannot be loaded and parsed correctly, the
31917          // client SHOULD continue to use the previous values and attempt to reload
31918          // it after waiting for the previously-specified TTL (or 5 minutes if
31919          // none).
31920
31921
31922          this.logger_(`manifest failed to load ${error}.`);
31923          this.startTTLTimeout_();
31924          return;
31925        }
31926
31927        this.trigger({
31928          type: 'contentsteeringloadcomplete',
31929          metadata
31930        });
31931        let steeringManifestJson;
31932
31933        try {
31934          steeringManifestJson = JSON.parse(this.request_.responseText);
31935        } catch (parseError) {
31936          const errorMetadata = {
31937            errorType: videojs__default["default"].Error.StreamingContentSteeringParserError,
31938            error: parseError
31939          };
31940          this.trigger({
31941            type: 'error',
31942            metadata: errorMetadata
31943          });
31944        }
31945
31946        this.assignSteeringProperties_(steeringManifestJson);
31947        const parsedMetadata = {
31948          contentSteeringInfo: metadata.contentSteeringInfo,
31949          contentSteeringManifest: {
31950            version: this.steeringManifest.version,
31951            reloadUri: this.steeringManifest.reloadUri,
31952            priority: this.steeringManifest.priority
31953          }
31954        };
31955        this.trigger({
31956          type: 'contentsteeringparsed',
31957          metadata: parsedMetadata
31958        });
31959        this.startTTLTimeout_();
31960      });
31961    }
31962    /**
31963     * Set the proxy server URL and add the steering manifest url as a URI encoded parameter.
31964     *
31965     * @param {string} steeringUrl the steering manifest url
31966     * @return the steering manifest url to a proxy server with all parameters set
31967     */
31968
31969
31970    setProxyServerUrl_(steeringUrl) {
31971      const steeringUrlObject = new window.URL(steeringUrl);
31972      const proxyServerUrlObject = new window.URL(this.proxyServerUrl_);
31973      proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString()));
31974      return this.setSteeringParams_(proxyServerUrlObject.toString());
31975    }
31976    /**
31977     * Decodes and parses the data uri encoded steering manifest
31978     *
31979     * @param {string} dataUri the data uri to be decoded and parsed.
31980     */
31981
31982
31983    decodeDataUriManifest_(dataUri) {
31984      const steeringManifestJson = JSON.parse(window.atob(dataUri));
31985      this.assignSteeringProperties_(steeringManifestJson);
31986    }
31987    /**
31988     * Set the HLS or DASH content steering manifest request query parameters. For example:
31989     * _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT>
31990     * _DASH_pathway and _DASH_throughput
31991     *
31992     * @param {string} uri to add content steering server parameters to.
31993     * @return a new uri as a string with the added steering query parameters.
31994     */
31995
31996
31997    setSteeringParams_(url) {
31998      const urlObject = new window.URL(url);
31999      const path = this.getPathway();
32000      const networkThroughput = this.getBandwidth_();
32001
32002      if (path) {
32003        const pathwayKey = `_${this.manifestType_}_pathway`;
32004        urlObject.searchParams.set(pathwayKey, path);
32005      }
32006
32007      if (networkThroughput) {
32008        const throughputKey = `_${this.manifestType_}_throughput`;
32009        urlObject.searchParams.set(throughputKey, networkThroughput);
32010      }
32011
32012      return urlObject.toString();
32013    }
32014    /**
32015     * Assigns the current steering manifest properties and to the SteeringManifest object
32016     *
32017     * @param {Object} steeringJson the raw JSON steering manifest
32018     */
32019
32020
32021    assignSteeringProperties_(steeringJson) {
32022      this.steeringManifest.version = steeringJson.VERSION;
32023
32024      if (!this.steeringManifest.version) {
32025        this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`);
32026        this.trigger('error');
32027        return;
32028      }
32029
32030      this.steeringManifest.ttl = steeringJson.TTL;
32031      this.steeringManifest.reloadUri = steeringJson['RELOAD-URI']; // HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional
32032
32033      this.steeringManifest.priority = steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY']; // Pathway clones to be created/updated in HLS.
32034      // See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
32035
32036      this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES'];
32037      this.nextPathwayClones = this.steeringManifest.pathwayClones; // 1. apply first pathway from the array.
32038      // 2. if first pathway doesn't exist in manifest, try next pathway.
32039      //    a. if all pathways are exhausted, ignore the steering manifest priority.
32040      // 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway.
32041      //    a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response,
32042      //       the excluded pathway will be ignored.
32043      //       See excludePathway usage in excludePlaylist().
32044      // If there are no available pathways, we need to stop content steering.
32045
32046      if (!this.availablePathways_.size) {
32047        this.logger_('There are no available pathways for content steering. Ending content steering.');
32048        this.trigger('error');
32049        this.dispose();
32050      }
32051
32052      const chooseNextPathway = pathwaysByPriority => {
32053        for (const path of pathwaysByPriority) {
32054          if (this.availablePathways_.has(path)) {
32055            return path;
32056          }
32057        } // If no pathway matches, ignore the manifest and choose the first available.
32058
32059
32060        return [...this.availablePathways_][0];
32061      };
32062
32063      const nextPathway = chooseNextPathway(this.steeringManifest.priority);
32064
32065      if (this.currentPathway !== nextPathway) {
32066        this.currentPathway = nextPathway;
32067        this.trigger('content-steering');
32068      }
32069    }
32070    /**
32071     * Returns the pathway to use for steering decisions
32072     *
32073     * @return {string} returns the current pathway or the default
32074     */
32075
32076
32077    getPathway() {
32078      return this.currentPathway || this.defaultPathway;
32079    }
32080    /**
32081     * Chooses the manifest request URI based on proxy URIs and server URLs.
32082     * Also accounts for exclusion on certain manifest URIs.
32083     *
32084     * @param {string} reloadUri the base uri before parameters
32085     *
32086     * @return {string} the final URI for the request to the manifest server.
32087     */
32088
32089
32090    getRequestURI(reloadUri) {
32091      if (!reloadUri) {
32092        return null;
32093      }
32094
32095      const isExcluded = uri => this.excludedSteeringManifestURLs.has(uri);
32096
32097      if (this.proxyServerUrl_) {
32098        const proxyURI = this.setProxyServerUrl_(reloadUri);
32099
32100        if (!isExcluded(proxyURI)) {
32101          return proxyURI;
32102        }
32103      }
32104
32105      const steeringURI = this.setSteeringParams_(reloadUri);
32106
32107      if (!isExcluded(steeringURI)) {
32108        return steeringURI;
32109      } // Return nothing if all valid manifest URIs are excluded.
32110
32111
32112      return null;
32113    }
32114    /**
32115     * Start the timeout for re-requesting the steering manifest at the TTL interval.
32116     *
32117     * @param {number} ttl time in seconds of the timeout. Defaults to the
32118     *        ttl interval in the steering manifest
32119     */
32120
32121
32122    startTTLTimeout_(ttl = this.steeringManifest.ttl) {
32123      // 300 (5 minutes) is the default value.
32124      const ttlMS = ttl * 1000;
32125      this.ttlTimeout_ = window.setTimeout(() => {
32126        this.requestSteeringManifest();
32127      }, ttlMS);
32128    }
32129    /**
32130     * Clear the TTL timeout if necessary.
32131     */
32132
32133
32134    clearTTLTimeout_() {
32135      window.clearTimeout(this.ttlTimeout_);
32136      this.ttlTimeout_ = null;
32137    }
32138    /**
32139     * aborts any current steering xhr and sets the current request object to null
32140     */
32141
32142
32143    abort() {
32144      if (this.request_) {
32145        this.request_.abort();
32146      }
32147
32148      this.request_ = null;
32149    }
32150    /**
32151     * aborts steering requests clears the ttl timeout and resets all properties.
32152     */
32153
32154
32155    dispose() {
32156      this.off('content-steering');
32157      this.off('error');
32158      this.abort();
32159      this.clearTTLTimeout_();
32160      this.currentPathway = null;
32161      this.defaultPathway = null;
32162      this.queryBeforeStart = null;
32163      this.proxyServerUrl_ = null;
32164      this.manifestType_ = null;
32165      this.ttlTimeout_ = null;
32166      this.request_ = null;
32167      this.excludedSteeringManifestURLs = new Set();
32168      this.availablePathways_ = new Set();
32169      this.steeringManifest = new SteeringManifest();
32170    }
32171    /**
32172     * adds a pathway to the available pathways set
32173     *
32174     * @param {string} pathway the pathway string to add
32175     */
32176
32177
32178    addAvailablePathway(pathway) {
32179      if (pathway) {
32180        this.availablePathways_.add(pathway);
32181      }
32182    }
32183    /**
32184     * Clears all pathways from the available pathways set
32185     */
32186
32187
32188    clearAvailablePathways() {
32189      this.availablePathways_.clear();
32190    }
32191    /**
32192     * Removes a pathway from the available pathways set.
32193     */
32194
32195
32196    excludePathway(pathway) {
32197      return this.availablePathways_.delete(pathway);
32198    }
32199    /**
32200     * Checks the refreshed DASH manifest content steering tag for changes.
32201     *
32202     * @param {string} baseURL new steering tag on DASH manifest refresh
32203     * @param {Object} newTag the new tag to check for changes
32204     * @return a true or false whether the new tag has different values
32205     */
32206
32207
32208    didDASHTagChange(baseURL, newTag) {
32209      return !newTag && this.steeringManifest.reloadUri || newTag && (resolveUrl(baseURL, newTag.serverURL) !== this.steeringManifest.reloadUri || newTag.defaultServiceLocation !== this.defaultPathway || newTag.queryBeforeStart !== this.queryBeforeStart || newTag.proxyServerURL !== this.proxyServerUrl_);
32210    }
32211
32212    getAvailablePathways() {
32213      return this.availablePathways_;
32214    }
32215
32216  }
32217
32218  const debounce = (callback, wait) => {
32219    let timeoutId = null;
32220    return (...args) => {
32221      clearTimeout(timeoutId);
32222      timeoutId = setTimeout(() => {
32223        callback.apply(null, args);
32224      }, wait);
32225    };
32226  };
32227
32228  const ABORT_EARLY_EXCLUSION_SECONDS = 10;
32229  let Vhs$1; // SegmentLoader stats that need to have each loader's
32230  // values summed to calculate the final value
32231
32232  const loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred', 'mediaAppends'];
32233
32234  const sumLoaderStat = function (stat) {
32235    return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat];
32236  };
32237
32238  const shouldSwitchToMedia = function ({
32239    currentPlaylist,
32240    buffered,
32241    currentTime,
32242    nextPlaylist,
32243    bufferLowWaterLine,
32244    bufferHighWaterLine,
32245    duration,
32246    bufferBasedABR,
32247    log
32248  }) {
32249    // we have no other playlist to switch to
32250    if (!nextPlaylist) {
32251      videojs__default["default"].log.warn('We received no playlist to switch to. Please check your stream.');
32252      return false;
32253    }
32254
32255    const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`;
32256
32257    if (!currentPlaylist) {
32258      log(`${sharedLogLine} as current playlist is not set`);
32259      return true;
32260    } // no need to switch if playlist is the same
32261
32262
32263    if (nextPlaylist.id === currentPlaylist.id) {
32264      return false;
32265    } // determine if current time is in a buffered range.
32266
32267
32268    const isBuffered = Boolean(findRange(buffered, currentTime).length); // If the playlist is live, then we want to not take low water line into account.
32269    // This is because in LIVE, the player plays 3 segments from the end of the
32270    // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
32271    // in those segments, a viewer will never experience a rendition upswitch.
32272
32273    if (!currentPlaylist.endList) {
32274      // For LLHLS live streams, don't switch renditions before playback has started, as it almost
32275      // doubles the time to first playback.
32276      if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') {
32277        log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`);
32278        return false;
32279      }
32280
32281      log(`${sharedLogLine} as current playlist is live`);
32282      return true;
32283    }
32284
32285    const forwardBuffer = timeAheadOf(buffered, currentTime);
32286    const maxBufferLowWaterLine = bufferBasedABR ? Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE; // For the same reason as LIVE, we ignore the low water line when the VOD
32287    // duration is below the max potential low water line
32288
32289    if (duration < maxBufferLowWaterLine) {
32290      log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`);
32291      return true;
32292    }
32293
32294    const nextBandwidth = nextPlaylist.attributes.BANDWIDTH;
32295    const currBandwidth = currentPlaylist.attributes.BANDWIDTH; // when switching down, if our buffer is lower than the high water line,
32296    // we can switch down
32297
32298    if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) {
32299      let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`;
32300
32301      if (bufferBasedABR) {
32302        logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`;
32303      }
32304
32305      log(logLine);
32306      return true;
32307    } // and if our buffer is higher than the low water line,
32308    // we can switch up
32309
32310
32311    if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) {
32312      let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`;
32313
32314      if (bufferBasedABR) {
32315        logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`;
32316      }
32317
32318      log(logLine);
32319      return true;
32320    }
32321
32322    log(`not ${sharedLogLine} as no switching criteria met`);
32323    return false;
32324  };
32325  /**
32326   * the main playlist controller controller all interactons
32327   * between playlists and segmentloaders. At this time this mainly
32328   * involves a main playlist and a series of audio playlists
32329   * if they are available
32330   *
32331   * @class PlaylistController
32332   * @extends videojs.EventTarget
32333   */
32334
32335
32336  class PlaylistController extends videojs__default["default"].EventTarget {
32337    constructor(options) {
32338      super(); // Adding a slight debounce to avoid duplicate calls during rapid quality changes, for example:
32339      // When selecting quality from the quality list,
32340      // where we may have multiple bandwidth profiles for the same vertical resolution.
32341
32342      this.fastQualityChange_ = debounce(this.fastQualityChange_.bind(this), 100);
32343      const {
32344        src,
32345        withCredentials,
32346        tech,
32347        bandwidth,
32348        externVhs,
32349        useCueTags,
32350        playlistExclusionDuration,
32351        enableLowInitialPlaylist,
32352        sourceType,
32353        cacheEncryptionKeys,
32354        bufferBasedABR,
32355        leastPixelDiffSelector,
32356        captionServices,
32357        experimentalUseMMS
32358      } = options;
32359
32360      if (!src) {
32361        throw new Error('A non-empty playlist URL or JSON manifest string is required');
32362      }
32363
32364      let {
32365        maxPlaylistRetries
32366      } = options;
32367
32368      if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') {
32369        maxPlaylistRetries = Infinity;
32370      }
32371
32372      Vhs$1 = externVhs;
32373      this.bufferBasedABR = Boolean(bufferBasedABR);
32374      this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector);
32375      this.withCredentials = withCredentials;
32376      this.tech_ = tech;
32377      this.vhs_ = tech.vhs;
32378      this.player_ = options.player_;
32379      this.sourceType_ = sourceType;
32380      this.useCueTags_ = useCueTags;
32381      this.playlistExclusionDuration = playlistExclusionDuration;
32382      this.maxPlaylistRetries = maxPlaylistRetries;
32383      this.enableLowInitialPlaylist = enableLowInitialPlaylist;
32384      this.usingManagedMediaSource_ = false;
32385
32386      if (this.useCueTags_) {
32387        this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues');
32388        this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
32389      }
32390
32391      this.requestOptions_ = {
32392        withCredentials,
32393        maxPlaylistRetries,
32394        timeout: null
32395      };
32396      this.on('error', this.pauseLoading);
32397      this.mediaTypes_ = createMediaTypes();
32398
32399      if (experimentalUseMMS && window.ManagedMediaSource) {
32400        // Airplay source not yet implemented. Remote playback must be disabled.
32401        this.tech_.el_.disableRemotePlayback = true;
32402        this.mediaSource = new window.ManagedMediaSource();
32403        this.usingManagedMediaSource_ = true;
32404        videojs__default["default"].log('Using ManagedMediaSource');
32405      } else if (window.MediaSource) {
32406        this.mediaSource = new window.MediaSource();
32407      }
32408
32409      this.handleDurationChange_ = this.handleDurationChange_.bind(this);
32410      this.handleSourceOpen_ = this.handleSourceOpen_.bind(this);
32411      this.handleSourceEnded_ = this.handleSourceEnded_.bind(this);
32412      this.load = this.load.bind(this);
32413      this.pause = this.pause.bind(this);
32414      this.mediaSource.addEventListener('durationchange', this.handleDurationChange_); // load the media source into the player
32415
32416      this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_);
32417      this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_);
32418      this.mediaSource.addEventListener('startstreaming', this.load);
32419      this.mediaSource.addEventListener('endstreaming', this.pause); // we don't have to handle sourceclose since dispose will handle termination of
32420      // everything, and the MediaSource should not be detached without a proper disposal
32421
32422      this.seekable_ = createTimeRanges();
32423      this.hasPlayed_ = false;
32424      this.syncController_ = new SyncController(options);
32425      this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
32426        kind: 'metadata',
32427        label: 'segment-metadata'
32428      }, false).track;
32429      this.segmentMetadataTrack_.mode = 'hidden';
32430      this.decrypter_ = new Decrypter();
32431      this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
32432      this.inbandTextTracks_ = {};
32433      this.timelineChangeController_ = new TimelineChangeController();
32434      this.keyStatusMap_ = new Map();
32435      const segmentLoaderSettings = {
32436        vhs: this.vhs_,
32437        parse708captions: options.parse708captions,
32438        useDtsForTimestampOffset: options.useDtsForTimestampOffset,
32439        captionServices,
32440        mediaSource: this.mediaSource,
32441        currentTime: this.tech_.currentTime.bind(this.tech_),
32442        seekable: () => this.seekable(),
32443        seeking: () => this.tech_.seeking(),
32444        duration: () => this.duration(),
32445        hasPlayed: () => this.hasPlayed_,
32446        goalBufferLength: () => this.goalBufferLength(),
32447        bandwidth,
32448        syncController: this.syncController_,
32449        decrypter: this.decrypter_,
32450        sourceType: this.sourceType_,
32451        inbandTextTracks: this.inbandTextTracks_,
32452        cacheEncryptionKeys,
32453        sourceUpdater: this.sourceUpdater_,
32454        timelineChangeController: this.timelineChangeController_,
32455        exactManifestTimings: options.exactManifestTimings,
32456        addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this)
32457      }; // The source type check not only determines whether a special DASH playlist loader
32458      // should be used, but also covers the case where the provided src is a vhs-json
32459      // manifest object (instead of a URL). In the case of vhs-json, the default
32460      // PlaylistLoader should be used.
32461
32462      this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ? new DashPlaylistLoader(src, this.vhs_, merge$1(this.requestOptions_, {
32463        addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this)
32464      })) : new PlaylistLoader(src, this.vhs_, merge$1(this.requestOptions_, {
32465        addDateRangesToTextTrack: this.addDateRangesToTextTrack_.bind(this)
32466      }));
32467      this.setupMainPlaylistLoaderListeners_(); // setup segment loaders
32468      // combined audio/video or just video when alternate audio track is selected
32469
32470      this.mainSegmentLoader_ = new SegmentLoader(merge$1(segmentLoaderSettings, {
32471        segmentMetadataTrack: this.segmentMetadataTrack_,
32472        loaderType: 'main'
32473      }), options); // alternate audio track
32474
32475      this.audioSegmentLoader_ = new SegmentLoader(merge$1(segmentLoaderSettings, {
32476        loaderType: 'audio'
32477      }), options);
32478      this.subtitleSegmentLoader_ = new VTTSegmentLoader(merge$1(segmentLoaderSettings, {
32479        loaderType: 'vtt',
32480        featuresNativeTextTracks: this.tech_.featuresNativeTextTracks,
32481        loadVttJs: () => new Promise((resolve, reject) => {
32482          function onLoad() {
32483            tech.off('vttjserror', onError);
32484            resolve();
32485          }
32486
32487          function onError() {
32488            tech.off('vttjsloaded', onLoad);
32489            reject();
32490          }
32491
32492          tech.one('vttjsloaded', onLoad);
32493          tech.one('vttjserror', onError); // safe to call multiple times, script will be loaded only once:
32494
32495          tech.addWebVttScript_();
32496        })
32497      }), options);
32498
32499      const getBandwidth = () => {
32500        return this.mainSegmentLoader_.bandwidth;
32501      };
32502
32503      this.contentSteeringController_ = new ContentSteeringController(this.vhs_.xhr, getBandwidth);
32504      this.setupSegmentLoaderListeners_();
32505
32506      if (this.bufferBasedABR) {
32507        this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_());
32508        this.tech_.on('pause', () => this.stopABRTimer_());
32509        this.tech_.on('play', () => this.startABRTimer_());
32510      } // Create SegmentLoader stat-getters
32511      // mediaRequests_
32512      // mediaRequestsAborted_
32513      // mediaRequestsTimedout_
32514      // mediaRequestsErrored_
32515      // mediaTransferDuration_
32516      // mediaBytesTransferred_
32517      // mediaAppends_
32518
32519
32520      loaderStats.forEach(stat => {
32521        this[stat + '_'] = sumLoaderStat.bind(this, stat);
32522      });
32523      this.logger_ = logger('pc');
32524      this.triggeredFmp4Usage = false;
32525
32526      if (this.tech_.preload() === 'none') {
32527        this.loadOnPlay_ = () => {
32528          this.loadOnPlay_ = null;
32529          this.mainPlaylistLoader_.load();
32530        };
32531
32532        this.tech_.one('play', this.loadOnPlay_);
32533      } else {
32534        this.mainPlaylistLoader_.load();
32535      }
32536
32537      this.timeToLoadedData__ = -1;
32538      this.mainAppendsToLoadedData__ = -1;
32539      this.audioAppendsToLoadedData__ = -1;
32540      const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart'; // start the first frame timer on loadstart or play (for preload none)
32541
32542      this.tech_.one(event, () => {
32543        const timeToLoadedDataStart = Date.now();
32544        this.tech_.one('loadeddata', () => {
32545          this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart;
32546          this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends;
32547          this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends;
32548        });
32549      });
32550    }
32551
32552    mainAppendsToLoadedData_() {
32553      return this.mainAppendsToLoadedData__;
32554    }
32555
32556    audioAppendsToLoadedData_() {
32557      return this.audioAppendsToLoadedData__;
32558    }
32559
32560    appendsToLoadedData_() {
32561      const main = this.mainAppendsToLoadedData_();
32562      const audio = this.audioAppendsToLoadedData_();
32563
32564      if (main === -1 || audio === -1) {
32565        return -1;
32566      }
32567
32568      return main + audio;
32569    }
32570
32571    timeToLoadedData_() {
32572      return this.timeToLoadedData__;
32573    }
32574    /**
32575     * Run selectPlaylist and switch to the new playlist if we should
32576     *
32577     * @param {string} [reason=abr] a reason for why the ABR check is made
32578     * @private
32579     */
32580
32581
32582    checkABR_(reason = 'abr') {
32583      const nextPlaylist = this.selectPlaylist();
32584
32585      if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) {
32586        this.switchMedia_(nextPlaylist, reason);
32587      }
32588    }
32589
32590    switchMedia_(playlist, cause, delay) {
32591      const oldMedia = this.media();
32592      const oldId = oldMedia && (oldMedia.id || oldMedia.uri);
32593      const newId = playlist && (playlist.id || playlist.uri);
32594
32595      if (oldId && oldId !== newId) {
32596        this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`);
32597        const metadata = {
32598          renditionInfo: {
32599            id: newId,
32600            bandwidth: playlist.attributes.BANDWIDTH,
32601            resolution: playlist.attributes.RESOLUTION,
32602            codecs: playlist.attributes.CODECS
32603          },
32604          cause
32605        };
32606        this.trigger({
32607          type: 'renditionselected',
32608          metadata
32609        });
32610        this.tech_.trigger({
32611          type: 'usage',
32612          name: `vhs-rendition-change-${cause}`
32613        });
32614      }
32615
32616      this.mainPlaylistLoader_.media(playlist, delay);
32617    }
32618    /**
32619     * A function that ensures we switch our playlists inside of `mediaTypes`
32620     * to match the current `serviceLocation` provided by the contentSteering controller.
32621     * We want to check media types of `AUDIO`, `SUBTITLES`, and `CLOSED-CAPTIONS`.
32622     *
32623     * This should only be called on a DASH playback scenario while using content steering.
32624     * This is necessary due to differences in how media in HLS manifests are generally tied to
32625     * a video playlist, where in DASH that is not always the case.
32626     */
32627
32628
32629    switchMediaForDASHContentSteering_() {
32630      ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => {
32631        const mediaType = this.mediaTypes_[type];
32632        const activeGroup = mediaType ? mediaType.activeGroup() : null;
32633        const pathway = this.contentSteeringController_.getPathway();
32634
32635        if (activeGroup && pathway) {
32636          // activeGroup can be an array or a single group
32637          const mediaPlaylists = activeGroup.length ? activeGroup[0].playlists : activeGroup.playlists;
32638          const dashMediaPlaylists = mediaPlaylists.filter(p => p.attributes.serviceLocation === pathway); // Switch the current active playlist to the correct CDN
32639
32640          if (dashMediaPlaylists.length) {
32641            this.mediaTypes_[type].activePlaylistLoader.media(dashMediaPlaylists[0]);
32642          }
32643        }
32644      });
32645    }
32646    /**
32647     * Start a timer that periodically calls checkABR_
32648     *
32649     * @private
32650     */
32651
32652
32653    startABRTimer_() {
32654      this.stopABRTimer_();
32655      this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250);
32656    }
32657    /**
32658     * Stop the timer that periodically calls checkABR_
32659     *
32660     * @private
32661     */
32662
32663
32664    stopABRTimer_() {
32665      // if we're scrubbing, we don't need to pause.
32666      // This getter will be added to Video.js in version 7.11.
32667      if (this.tech_.scrubbing && this.tech_.scrubbing()) {
32668        return;
32669      }
32670
32671      window.clearInterval(this.abrTimer_);
32672      this.abrTimer_ = null;
32673    }
32674    /**
32675     * Get a list of playlists for the currently selected audio playlist
32676     *
32677     * @return {Array} the array of audio playlists
32678     */
32679
32680
32681    getAudioTrackPlaylists_() {
32682      const main = this.main();
32683      const defaultPlaylists = main && main.playlists || []; // if we don't have any audio groups then we can only
32684      // assume that the audio tracks are contained in main
32685      // playlist array, use that or an empty array.
32686
32687      if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) {
32688        return defaultPlaylists;
32689      }
32690
32691      const AUDIO = main.mediaGroups.AUDIO;
32692      const groupKeys = Object.keys(AUDIO);
32693      let track; // get the current active track
32694
32695      if (Object.keys(this.mediaTypes_.AUDIO.groups).length) {
32696        track = this.mediaTypes_.AUDIO.activeTrack(); // or get the default track from main if mediaTypes_ isn't setup yet
32697      } else {
32698        // default group is `main` or just the first group.
32699        const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]];
32700
32701        for (const label in defaultGroup) {
32702          if (defaultGroup[label].default) {
32703            track = {
32704              label
32705            };
32706            break;
32707          }
32708        }
32709      } // no active track no playlists.
32710
32711
32712      if (!track) {
32713        return defaultPlaylists;
32714      }
32715
32716      const playlists = []; // get all of the playlists that are possible for the
32717      // active track.
32718
32719      for (const group in AUDIO) {
32720        if (AUDIO[group][track.label]) {
32721          const properties = AUDIO[group][track.label];
32722
32723          if (properties.playlists && properties.playlists.length) {
32724            playlists.push.apply(playlists, properties.playlists);
32725          } else if (properties.uri) {
32726            playlists.push(properties);
32727          } else if (main.playlists.length) {
32728            // if an audio group does not have a uri
32729            // see if we have main playlists that use it as a group.
32730            // if we do then add those to the playlists list.
32731            for (let i = 0; i < main.playlists.length; i++) {
32732              const playlist = main.playlists[i];
32733
32734              if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) {
32735                playlists.push(playlist);
32736              }
32737            }
32738          }
32739        }
32740      }
32741
32742      if (!playlists.length) {
32743        return defaultPlaylists;
32744      }
32745
32746      return playlists;
32747    }
32748    /**
32749     * Register event handlers on the main playlist loader. A helper
32750     * function for construction time.
32751     *
32752     * @private
32753     */
32754
32755
32756    setupMainPlaylistLoaderListeners_() {
32757      this.mainPlaylistLoader_.on('loadedmetadata', () => {
32758        const media = this.mainPlaylistLoader_.media();
32759        const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to
32760        // timeout the request.
32761
32762        if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) {
32763          this.requestOptions_.timeout = 0;
32764        } else {
32765          this.requestOptions_.timeout = requestTimeout;
32766        } // if this isn't a live video and preload permits, start
32767        // downloading segments
32768
32769
32770        if (media.endList && this.tech_.preload() !== 'none') {
32771          this.mainSegmentLoader_.playlist(media, this.requestOptions_);
32772          this.mainSegmentLoader_.load();
32773        }
32774
32775        setupMediaGroups({
32776          sourceType: this.sourceType_,
32777          segmentLoaders: {
32778            AUDIO: this.audioSegmentLoader_,
32779            SUBTITLES: this.subtitleSegmentLoader_,
32780            main: this.mainSegmentLoader_
32781          },
32782          tech: this.tech_,
32783          requestOptions: this.requestOptions_,
32784          mainPlaylistLoader: this.mainPlaylistLoader_,
32785          vhs: this.vhs_,
32786          main: this.main(),
32787          mediaTypes: this.mediaTypes_,
32788          excludePlaylist: this.excludePlaylist.bind(this)
32789        });
32790        this.triggerPresenceUsage_(this.main(), media);
32791        this.setupFirstPlay();
32792
32793        if (!this.mediaTypes_.AUDIO.activePlaylistLoader || this.mediaTypes_.AUDIO.activePlaylistLoader.media()) {
32794          this.trigger('selectedinitialmedia');
32795        } else {
32796          // We must wait for the active audio playlist loader to
32797          // finish setting up before triggering this event so the
32798          // representations API and EME setup is correct
32799          this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => {
32800            this.trigger('selectedinitialmedia');
32801          });
32802        }
32803      });
32804      this.mainPlaylistLoader_.on('loadedplaylist', () => {
32805        if (this.loadOnPlay_) {
32806          this.tech_.off('play', this.loadOnPlay_);
32807        }
32808
32809        let updatedPlaylist = this.mainPlaylistLoader_.media();
32810
32811        if (!updatedPlaylist) {
32812          // Add content steering listeners on first load and init.
32813          this.attachContentSteeringListeners_();
32814          this.initContentSteeringController_(); // exclude any variants that are not supported by the browser before selecting
32815          // an initial media as the playlist selectors do not consider browser support
32816
32817          this.excludeUnsupportedVariants_();
32818          let selectedMedia;
32819
32820          if (this.enableLowInitialPlaylist) {
32821            selectedMedia = this.selectInitialPlaylist();
32822          }
32823
32824          if (!selectedMedia) {
32825            selectedMedia = this.selectPlaylist();
32826          }
32827
32828          if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) {
32829            return;
32830          }
32831
32832          this.initialMedia_ = selectedMedia;
32833          this.switchMedia_(this.initialMedia_, 'initial'); // Under the standard case where a source URL is provided, loadedplaylist will
32834          // fire again since the playlist will be requested. In the case of vhs-json
32835          // (where the manifest object is provided as the source), when the media
32836          // playlist's `segments` list is already available, a media playlist won't be
32837          // requested, and loadedplaylist won't fire again, so the playlist handler must be
32838          // called on its own here.
32839
32840          const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments;
32841
32842          if (!haveJsonSource) {
32843            return;
32844          }
32845
32846          updatedPlaylist = this.initialMedia_;
32847        }
32848
32849        this.handleUpdatedMediaPlaylist(updatedPlaylist);
32850      });
32851      this.mainPlaylistLoader_.on('error', () => {
32852        const error = this.mainPlaylistLoader_.error;
32853        this.excludePlaylist({
32854          playlistToExclude: error.playlist,
32855          error
32856        });
32857      });
32858      this.mainPlaylistLoader_.on('mediachanging', () => {
32859        this.mainSegmentLoader_.abort();
32860        this.mainSegmentLoader_.pause();
32861      });
32862      this.mainPlaylistLoader_.on('mediachange', () => {
32863        const media = this.mainPlaylistLoader_.media();
32864        const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to
32865        // timeout the request.
32866
32867        if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) {
32868          this.requestOptions_.timeout = 0;
32869        } else {
32870          this.requestOptions_.timeout = requestTimeout;
32871        }
32872
32873        if (this.sourceType_ === 'dash') {
32874          // we don't want to re-request the same hls playlist right after it was changed
32875          // Initially it was implemented as workaround to restart playlist loader for live
32876          // when playlist loader is paused because of playlist exclusions:
32877          // see: https://github.com/videojs/http-streaming/pull/1339
32878          // but this introduces duplicate "loadedplaylist" event.
32879          // Ideally we want to re-think playlist loader life-cycle events,
32880          // but simply checking "paused" state should help a lot
32881          if (this.mainPlaylistLoader_.isPaused) {
32882            this.mainPlaylistLoader_.load();
32883          }
32884        } // TODO: Create a new event on the PlaylistLoader that signals
32885        // that the segments have changed in some way and use that to
32886        // update the SegmentLoader instead of doing it twice here and
32887        // on `loadedplaylist`
32888
32889
32890        this.mainSegmentLoader_.pause();
32891        this.mainSegmentLoader_.playlist(media, this.requestOptions_);
32892
32893        if (this.waitingForFastQualityPlaylistReceived_) {
32894          this.runFastQualitySwitch_();
32895        } else {
32896          this.mainSegmentLoader_.load();
32897        }
32898
32899        this.tech_.trigger({
32900          type: 'mediachange',
32901          bubbles: true
32902        });
32903      });
32904      this.mainPlaylistLoader_.on('playlistunchanged', () => {
32905        const updatedPlaylist = this.mainPlaylistLoader_.media(); // ignore unchanged playlists that have already been
32906        // excluded for not-changing. We likely just have a really slowly updating
32907        // playlist.
32908
32909        if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') {
32910          return;
32911        }
32912
32913        const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist);
32914
32915        if (playlistOutdated) {
32916          // Playlist has stopped updating and we're stuck at its end. Try to
32917          // exclude it and switch to another playlist in the hope that that
32918          // one is updating (and give the player a chance to re-adjust to the
32919          // safe live point).
32920          this.excludePlaylist({
32921            error: {
32922              message: 'Playlist no longer updating.',
32923              reason: 'playlist-unchanged'
32924            }
32925          }); // useful for monitoring QoS
32926
32927          this.tech_.trigger('playliststuck');
32928        }
32929      });
32930      this.mainPlaylistLoader_.on('renditiondisabled', () => {
32931        this.tech_.trigger({
32932          type: 'usage',
32933          name: 'vhs-rendition-disabled'
32934        });
32935      });
32936      this.mainPlaylistLoader_.on('renditionenabled', () => {
32937        this.tech_.trigger({
32938          type: 'usage',
32939          name: 'vhs-rendition-enabled'
32940        });
32941      });
32942      const playlistLoaderEvents = ['manifestrequeststart', 'manifestrequestcomplete', 'manifestparsestart', 'manifestparsecomplete', 'playlistrequeststart', 'playlistrequestcomplete', 'playlistparsestart', 'playlistparsecomplete', 'renditiondisabled', 'renditionenabled'];
32943      playlistLoaderEvents.forEach(eventName => {
32944        this.mainPlaylistLoader_.on(eventName, metadata => {
32945          // trigger directly on the player to ensure early events are fired.
32946          this.player_.trigger(_extends({}, metadata));
32947        });
32948      });
32949    }
32950    /**
32951     * Given an updated media playlist (whether it was loaded for the first time, or
32952     * refreshed for live playlists), update any relevant properties and state to reflect
32953     * changes in the media that should be accounted for (e.g., cues and duration).
32954     *
32955     * @param {Object} updatedPlaylist the updated media playlist object
32956     *
32957     * @private
32958     */
32959
32960
32961    handleUpdatedMediaPlaylist(updatedPlaylist) {
32962      if (this.useCueTags_) {
32963        this.updateAdCues_(updatedPlaylist);
32964      } // TODO: Create a new event on the PlaylistLoader that signals
32965      // that the segments have changed in some way and use that to
32966      // update the SegmentLoader instead of doing it twice here and
32967      // on `mediachange`
32968
32969
32970      this.mainSegmentLoader_.pause();
32971      this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
32972
32973      if (this.waitingForFastQualityPlaylistReceived_) {
32974        this.runFastQualitySwitch_();
32975      }
32976
32977      this.updateDuration(!updatedPlaylist.endList); // If the player isn't paused, ensure that the segment loader is running,
32978      // as it is possible that it was temporarily stopped while waiting for
32979      // a playlist (e.g., in case the playlist errored and we re-requested it).
32980
32981      if (!this.tech_.paused()) {
32982        this.mainSegmentLoader_.load();
32983
32984        if (this.audioSegmentLoader_) {
32985          this.audioSegmentLoader_.load();
32986        }
32987      }
32988    }
32989    /**
32990     * A helper function for triggerring presence usage events once per source
32991     *
32992     * @private
32993     */
32994
32995
32996    triggerPresenceUsage_(main, media) {
32997      const mediaGroups = main.mediaGroups || {};
32998      let defaultDemuxed = true;
32999      const audioGroupKeys = Object.keys(mediaGroups.AUDIO);
33000
33001      for (const mediaGroup in mediaGroups.AUDIO) {
33002        for (const label in mediaGroups.AUDIO[mediaGroup]) {
33003          const properties = mediaGroups.AUDIO[mediaGroup][label];
33004
33005          if (!properties.uri) {
33006            defaultDemuxed = false;
33007          }
33008        }
33009      }
33010
33011      if (defaultDemuxed) {
33012        this.tech_.trigger({
33013          type: 'usage',
33014          name: 'vhs-demuxed'
33015        });
33016      }
33017
33018      if (Object.keys(mediaGroups.SUBTITLES).length) {
33019        this.tech_.trigger({
33020          type: 'usage',
33021          name: 'vhs-webvtt'
33022        });
33023      }
33024
33025      if (Vhs$1.Playlist.isAes(media)) {
33026        this.tech_.trigger({
33027          type: 'usage',
33028          name: 'vhs-aes'
33029        });
33030      }
33031
33032      if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
33033        this.tech_.trigger({
33034          type: 'usage',
33035          name: 'vhs-alternate-audio'
33036        });
33037      }
33038
33039      if (this.useCueTags_) {
33040        this.tech_.trigger({
33041          type: 'usage',
33042          name: 'vhs-playlist-cue-tags'
33043        });
33044      }
33045    }
33046
33047    shouldSwitchToMedia_(nextPlaylist) {
33048      const currentPlaylist = this.mainPlaylistLoader_.media() || this.mainPlaylistLoader_.pendingMedia_;
33049      const currentTime = this.tech_.currentTime();
33050      const bufferLowWaterLine = this.bufferLowWaterLine();
33051      const bufferHighWaterLine = this.bufferHighWaterLine();
33052      const buffered = this.tech_.buffered();
33053      return shouldSwitchToMedia({
33054        buffered,
33055        currentTime,
33056        currentPlaylist,
33057        nextPlaylist,
33058        bufferLowWaterLine,
33059        bufferHighWaterLine,
33060        duration: this.duration(),
33061        bufferBasedABR: this.bufferBasedABR,
33062        log: this.logger_
33063      });
33064    }
33065    /**
33066     * Register event handlers on the segment loaders. A helper function
33067     * for construction time.
33068     *
33069     * @private
33070     */
33071
33072
33073    setupSegmentLoaderListeners_() {
33074      this.mainSegmentLoader_.on('bandwidthupdate', () => {
33075        // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's
33076        // useful to check to see if a rendition switch should be made.
33077        this.checkABR_('bandwidthupdate');
33078        this.tech_.trigger('bandwidthupdate');
33079      });
33080      this.mainSegmentLoader_.on('timeout', () => {
33081        if (this.bufferBasedABR) {
33082          // If a rendition change is needed, then it would've be done on `bandwidthupdate`.
33083          // Here the only consideration is that for buffer based ABR there's no guarantee
33084          // of an immediate switch (since the bandwidth is averaged with a timeout
33085          // bandwidth value of 1), so force a load on the segment loader to keep it going.
33086          this.mainSegmentLoader_.load();
33087        }
33088      }); // `progress` events are not reliable enough of a bandwidth measure to trigger buffer
33089      // based ABR.
33090
33091      if (!this.bufferBasedABR) {
33092        this.mainSegmentLoader_.on('progress', () => {
33093          this.trigger('progress');
33094        });
33095      }
33096
33097      this.mainSegmentLoader_.on('error', () => {
33098        const error = this.mainSegmentLoader_.error();
33099        this.excludePlaylist({
33100          playlistToExclude: error.playlist,
33101          error
33102        });
33103      });
33104      this.mainSegmentLoader_.on('appenderror', () => {
33105        this.error = this.mainSegmentLoader_.error_;
33106        this.trigger('error');
33107      });
33108      this.mainSegmentLoader_.on('syncinfoupdate', () => {
33109        this.onSyncInfoUpdate_();
33110      });
33111      this.mainSegmentLoader_.on('timestampoffset', () => {
33112        this.tech_.trigger({
33113          type: 'usage',
33114          name: 'vhs-timestamp-offset'
33115        });
33116      });
33117      this.audioSegmentLoader_.on('syncinfoupdate', () => {
33118        this.onSyncInfoUpdate_();
33119      });
33120      this.audioSegmentLoader_.on('appenderror', () => {
33121        this.error = this.audioSegmentLoader_.error_;
33122        this.trigger('error');
33123      });
33124      this.mainSegmentLoader_.on('ended', () => {
33125        this.logger_('main segment loader ended');
33126        this.onEndOfStream();
33127      }); // There is the possibility of the video segment and the audio segment
33128      // at a current time to be on different timelines. When this occurs, the player
33129      // forwards playback to a point where these two segment types are back on the same
33130      // timeline. This time will be just after the end of the audio segment that is on
33131      // a previous timeline.
33132
33133      this.timelineChangeController_.on('audioTimelineBehind', () => {
33134        const segmentInfo = this.audioSegmentLoader_.pendingSegment_;
33135
33136        if (!segmentInfo || !segmentInfo.segment || !segmentInfo.segment.syncInfo) {
33137          return;
33138        } // Update the current time to just after the faulty audio segment.
33139        // This moves playback to a spot where both audio and video segments
33140        // are on the same timeline.
33141
33142
33143        const newTime = segmentInfo.segment.syncInfo.end + 0.01;
33144        this.tech_.setCurrentTime(newTime);
33145      });
33146      this.timelineChangeController_.on('fixBadTimelineChange', () => {
33147        // pause, reset-everything and load for all segment-loaders
33148        this.logger_('Fix bad timeline change. Restarting al segment loaders...');
33149        this.mainSegmentLoader_.pause();
33150        this.mainSegmentLoader_.resetEverything();
33151
33152        if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33153          this.audioSegmentLoader_.pause();
33154          this.audioSegmentLoader_.resetEverything();
33155        }
33156
33157        if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
33158          this.subtitleSegmentLoader_.pause();
33159          this.subtitleSegmentLoader_.resetEverything();
33160        } // start segment loader loading in case they are paused
33161
33162
33163        this.load();
33164      });
33165      this.mainSegmentLoader_.on('earlyabort', event => {
33166        // never try to early abort with the new ABR algorithm
33167        if (this.bufferBasedABR) {
33168          return;
33169        }
33170
33171        this.delegateLoaders_('all', ['abort']);
33172        this.excludePlaylist({
33173          error: {
33174            message: 'Aborted early because there isn\'t enough bandwidth to complete ' + 'the request without rebuffering.'
33175          },
33176          playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS
33177        });
33178      });
33179
33180      const updateCodecs = () => {
33181        if (!this.sourceUpdater_.hasCreatedSourceBuffers()) {
33182          return this.tryToCreateSourceBuffers_();
33183        }
33184
33185        const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded
33186
33187        if (!codecs) {
33188          return;
33189        }
33190
33191        this.sourceUpdater_.addOrChangeSourceBuffers(codecs);
33192      };
33193
33194      this.mainSegmentLoader_.on('trackinfo', updateCodecs);
33195      this.audioSegmentLoader_.on('trackinfo', updateCodecs);
33196      this.mainSegmentLoader_.on('fmp4', () => {
33197        if (!this.triggeredFmp4Usage) {
33198          this.tech_.trigger({
33199            type: 'usage',
33200            name: 'vhs-fmp4'
33201          });
33202          this.triggeredFmp4Usage = true;
33203        }
33204      });
33205      this.audioSegmentLoader_.on('fmp4', () => {
33206        if (!this.triggeredFmp4Usage) {
33207          this.tech_.trigger({
33208            type: 'usage',
33209            name: 'vhs-fmp4'
33210          });
33211          this.triggeredFmp4Usage = true;
33212        }
33213      });
33214      this.audioSegmentLoader_.on('ended', () => {
33215        this.logger_('audioSegmentLoader ended');
33216        this.onEndOfStream();
33217      });
33218      const segmentLoaderEvents = ['segmentselected', 'segmentloadstart', 'segmentloaded', 'segmentkeyloadstart', 'segmentkeyloadcomplete', 'segmentdecryptionstart', 'segmentdecryptioncomplete', 'segmenttransmuxingstart', 'segmenttransmuxingcomplete', 'segmenttransmuxingtrackinfoavailable', 'segmenttransmuxingtiminginfoavailable', 'segmentappendstart', 'appendsdone', 'bandwidthupdated', 'timelinechange', 'codecschange'];
33219      segmentLoaderEvents.forEach(eventName => {
33220        this.mainSegmentLoader_.on(eventName, metadata => {
33221          this.player_.trigger(_extends({}, metadata));
33222        });
33223        this.audioSegmentLoader_.on(eventName, metadata => {
33224          this.player_.trigger(_extends({}, metadata));
33225        });
33226        this.subtitleSegmentLoader_.on(eventName, metadata => {
33227          this.player_.trigger(_extends({}, metadata));
33228        });
33229      });
33230    }
33231
33232    mediaSecondsLoaded_() {
33233      return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded);
33234    }
33235    /**
33236     * Call load on our SegmentLoaders
33237     */
33238
33239
33240    load() {
33241      this.mainSegmentLoader_.load();
33242
33243      if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33244        this.audioSegmentLoader_.load();
33245      }
33246
33247      if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
33248        this.subtitleSegmentLoader_.load();
33249      }
33250    }
33251    /**
33252     * Call pause on our SegmentLoaders
33253     */
33254
33255
33256    pause() {
33257      this.mainSegmentLoader_.pause();
33258
33259      if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33260        this.audioSegmentLoader_.pause();
33261      }
33262
33263      if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
33264        this.subtitleSegmentLoader_.pause();
33265      }
33266    }
33267    /**
33268     * Re-tune playback quality level for the current player
33269     * conditions. This method will perform destructive actions like removing
33270     * already buffered content in order to readjust the currently active
33271     * playlist quickly. This is good for manual quality changes
33272     *
33273     * @private
33274     */
33275
33276
33277    fastQualityChange_(media = this.selectPlaylist()) {
33278      if (media && media === this.mainPlaylistLoader_.media()) {
33279        this.logger_('skipping fastQualityChange because new media is same as old');
33280        return;
33281      }
33282
33283      this.switchMedia_(media, 'fast-quality'); // we would like to avoid race condition when we call fastQuality,
33284      // reset everything and start loading segments from prev segments instead of new because new playlist is not received yet
33285
33286      this.waitingForFastQualityPlaylistReceived_ = true;
33287    }
33288
33289    runFastQualitySwitch_() {
33290      this.waitingForFastQualityPlaylistReceived_ = false;
33291      this.mainSegmentLoader_.pause();
33292      this.mainSegmentLoader_.resetEverything();
33293
33294      if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33295        this.audioSegmentLoader_.pause();
33296        this.audioSegmentLoader_.resetEverything();
33297      }
33298
33299      if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
33300        this.subtitleSegmentLoader_.pause();
33301        this.subtitleSegmentLoader_.resetEverything();
33302      } // start segment loader loading in case they are paused
33303
33304
33305      this.load();
33306    }
33307    /**
33308     * Begin playback.
33309     */
33310
33311
33312    play() {
33313      if (this.setupFirstPlay()) {
33314        return;
33315      }
33316
33317      if (this.tech_.ended()) {
33318        this.tech_.setCurrentTime(0);
33319      }
33320
33321      if (this.hasPlayed_) {
33322        this.load();
33323      }
33324
33325      const seekable = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window,
33326      // seek forward to the live point
33327
33328      if (this.tech_.duration() === Infinity) {
33329        if (this.tech_.currentTime() < seekable.start(0)) {
33330          return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
33331        }
33332      }
33333    }
33334    /**
33335     * Seek to the latest media position if this is a live video and the
33336     * player and video are loaded and initialized.
33337     */
33338
33339
33340    setupFirstPlay() {
33341      const media = this.mainPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play
33342      //  If 1) there is no active media
33343      //     2) the player is paused
33344      //     3) the first play has already been setup
33345      // then exit early
33346
33347      if (!media || this.tech_.paused() || this.hasPlayed_) {
33348        return false;
33349      } // when the video is a live stream and/or has a start time
33350
33351
33352      if (!media.endList || media.start) {
33353        const seekable = this.seekable();
33354
33355        if (!seekable.length) {
33356          // without a seekable range, the player cannot seek to begin buffering at the
33357          // live or start point
33358          return false;
33359        }
33360
33361        const seekableEnd = seekable.end(0);
33362        let startPoint = seekableEnd;
33363
33364        if (media.start) {
33365          const offset = media.start.timeOffset;
33366
33367          if (offset < 0) {
33368            
33368startPoint = Math.max(seekableEnd + offset
vendor: 82,959 bytes, lines 33368-35649
33368, seekable.start(0));
33369          } else {
33370            startPoint = Math.min(seekableEnd, offset);
33371          }
33372        } // trigger firstplay to inform the source handler to ignore the next seek event
33373
33374
33375        this.trigger('firstplay'); // seek to the live point
33376
33377        this.tech_.setCurrentTime(startPoint);
33378      }
33379
33380      this.hasPlayed_ = true; // we can begin loading now that everything is ready
33381
33382      this.load();
33383      return true;
33384    }
33385    /**
33386     * handle the sourceopen event on the MediaSource
33387     *
33388     * @private
33389     */
33390
33391
33392    handleSourceOpen_() {
33393      // Only attempt to create the source buffer if none already exist.
33394      // handleSourceOpen is also called when we are "re-opening" a source buffer
33395      // after `endOfStream` has been called (in response to a seek for instance)
33396      this.tryToCreateSourceBuffers_(); // if autoplay is enabled, begin playback. This is duplicative of
33397      // code in video.js but is required because play() must be invoked
33398      // *after* the media source has opened.
33399
33400      if (this.tech_.autoplay()) {
33401        const playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request
33402        // on browsers which return a promise
33403
33404        if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') {
33405          playPromise.then(null, e => {});
33406        }
33407      }
33408
33409      this.trigger('sourceopen');
33410    }
33411    /**
33412     * handle the sourceended event on the MediaSource
33413     *
33414     * @private
33415     */
33416
33417
33418    handleSourceEnded_() {
33419      if (!this.inbandTextTracks_.metadataTrack_) {
33420        return;
33421      }
33422
33423      const cues = this.inbandTextTracks_.metadataTrack_.cues;
33424
33425      if (!cues || !cues.length) {
33426        return;
33427      }
33428
33429      const duration = this.duration();
33430      cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ? Number.MAX_VALUE : duration;
33431    }
33432    /**
33433     * handle the durationchange event on the MediaSource
33434     *
33435     * @private
33436     */
33437
33438
33439    handleDurationChange_() {
33440      this.tech_.trigger('durationchange');
33441    }
33442    /**
33443     * Calls endOfStream on the media source when all active stream types have called
33444     * endOfStream
33445     *
33446     * @param {string} streamType
33447     *        Stream type of the segment loader that called endOfStream
33448     * @private
33449     */
33450
33451
33452    onEndOfStream() {
33453      let isEndOfStream = this.mainSegmentLoader_.ended_;
33454
33455      if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33456        const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_(); // if the audio playlist loader exists, then alternate audio is active
33457
33458        if (!mainMediaInfo || mainMediaInfo.hasVideo) {
33459          // if we do not know if the main segment loader contains video yet or if we
33460          // definitively know the main segment loader contains video, then we need to wait
33461          // for both main and audio segment loaders to call endOfStream
33462          isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
33463        } else {
33464          // otherwise just rely on the audio loader
33465          isEndOfStream = this.audioSegmentLoader_.ended_;
33466        }
33467      }
33468
33469      if (!isEndOfStream) {
33470        return;
33471      }
33472
33473      this.stopABRTimer_();
33474      this.sourceUpdater_.endOfStream();
33475    }
33476    /**
33477     * Check if a playlist has stopped being updated
33478     *
33479     * @param {Object} playlist the media playlist object
33480     * @return {boolean} whether the playlist has stopped being updated or not
33481     */
33482
33483
33484    stuckAtPlaylistEnd_(playlist) {
33485      const seekable = this.seekable();
33486
33487      if (!seekable.length) {
33488        // playlist doesn't have enough information to determine whether we are stuck
33489        return false;
33490      }
33491
33492      const expired = this.syncController_.getExpiredTime(playlist, this.duration());
33493
33494      if (expired === null) {
33495        return false;
33496      } // does not use the safe live end to calculate playlist end, since we
33497      // don't want to say we are stuck while there is still content
33498
33499
33500      const absolutePlaylistEnd = Vhs$1.Playlist.playlistEnd(playlist, expired);
33501      const currentTime = this.tech_.currentTime();
33502      const buffered = this.tech_.buffered();
33503
33504      if (!buffered.length) {
33505        // return true if the playhead reached the absolute end of the playlist
33506        return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA;
33507      }
33508
33509      const bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute
33510      // end of playlist
33511
33512      return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA;
33513    }
33514    /**
33515     * Exclude a playlist for a set amount of time, making it unavailable for selection by
33516     * the rendition selection algorithm, then force a new playlist (rendition) selection.
33517     *
33518     * @param {Object=} playlistToExclude
33519     *                  the playlist to exclude, defaults to the currently selected playlist
33520     * @param {Object=} error
33521     *                  an optional error
33522     * @param {number=} playlistExclusionDuration
33523     *                  an optional number of seconds to exclude the playlist
33524     */
33525
33526
33527    excludePlaylist({
33528      playlistToExclude = this.mainPlaylistLoader_.media(),
33529      error = {},
33530      playlistExclusionDuration
33531    }) {
33532      // If the `error` was generated by the playlist loader, it will contain
33533      // the playlist we were trying to load (but failed) and that should be
33534      // excluded instead of the currently selected playlist which is likely
33535      // out-of-date in this scenario
33536      playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media();
33537      playlistExclusionDuration = playlistExclusionDuration || error.playlistExclusionDuration || this.playlistExclusionDuration; // If there is no current playlist, then an error occurred while we were
33538      // trying to load the main OR while we were disposing of the tech
33539
33540      if (!playlistToExclude) {
33541        this.error = error;
33542
33543        if (this.mediaSource.readyState !== 'open') {
33544          this.trigger('error');
33545        } else {
33546          this.sourceUpdater_.endOfStream('network');
33547        }
33548
33549        return;
33550      }
33551
33552      playlistToExclude.playlistErrors_++;
33553      const playlists = this.mainPlaylistLoader_.main.playlists;
33554      const enabledPlaylists = playlists.filter(isEnabled);
33555      const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded
33556      // forever
33557
33558      if (playlists.length === 1 && playlistExclusionDuration !== Infinity) {
33559        videojs__default["default"].log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` + 'Trying again since it is the only playlist.');
33560        this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay
33561
33562        return this.mainPlaylistLoader_.load(isFinalRendition);
33563      }
33564
33565      if (isFinalRendition) {
33566        // If we're content steering, try other pathways.
33567        if (this.main().contentSteering) {
33568          const pathway = this.pathwayAttribute_(playlistToExclude); // Ignore at least 1 steering manifest refresh.
33569
33570          const reIncludeDelay = this.contentSteeringController_.steeringManifest.ttl * 1000;
33571          this.contentSteeringController_.excludePathway(pathway);
33572          this.excludeThenChangePathway_();
33573          setTimeout(() => {
33574            this.contentSteeringController_.addAvailablePathway(pathway);
33575          }, reIncludeDelay);
33576          return;
33577        } // Since we're on the final non-excluded playlist, and we're about to exclude
33578        // it, instead of erring the player or retrying this playlist, clear out the current
33579        // exclusion list. This allows other playlists to be attempted in case any have been
33580        // fixed.
33581
33582
33583        let reincluded = false;
33584        playlists.forEach(playlist => {
33585          // skip current playlist which is about to be excluded
33586          if (playlist === playlistToExclude) {
33587            return;
33588          }
33589
33590          const excludeUntil = playlist.excludeUntil; // a playlist cannot be reincluded if it wasn't excluded to begin with.
33591
33592          if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) {
33593            reincluded = true;
33594            delete playlist.excludeUntil;
33595          }
33596        });
33597
33598        if (reincluded) {
33599          videojs__default["default"].log.warn('Removing other playlists from the exclusion list because the last ' + 'rendition is about to be excluded.'); // Technically we are retrying a playlist, in that we are simply retrying a previous
33600          // playlist. This is needed for users relying on the retryplaylist event to catch a
33601          // case where the player might be stuck and looping through "dead" playlists.
33602
33603          this.tech_.trigger('retryplaylist');
33604        }
33605      } // Exclude this playlist
33606
33607
33608      let excludeUntil;
33609
33610      if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) {
33611        excludeUntil = Infinity;
33612      } else {
33613        excludeUntil = Date.now() + playlistExclusionDuration * 1000;
33614      }
33615
33616      playlistToExclude.excludeUntil = excludeUntil;
33617
33618      if (error.reason) {
33619        playlistToExclude.lastExcludeReason_ = error.reason;
33620      }
33621
33622      this.tech_.trigger('excludeplaylist');
33623      this.tech_.trigger({
33624        type: 'usage',
33625        name: 'vhs-rendition-excluded'
33626      }); // TODO: only load a new playlist if we're excluding the current playlist
33627      // If this function was called with a playlist that's not the current active playlist
33628      // (e.g., media().id !== playlistToExclude.id),
33629      // then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist.
33630
33631      const nextPlaylist = this.selectPlaylist();
33632
33633      if (!nextPlaylist) {
33634        this.error = 'Playback cannot continue. No available working or supported playlists.';
33635        this.trigger('error');
33636        return;
33637      }
33638
33639      const logFn = error.internal ? this.logger_ : videojs__default["default"].log.warn;
33640      const errorMessage = error.message ? ' ' + error.message : '';
33641      logFn(`${error.internal ? 'Internal problem' : 'Problem'} encountered with playlist ${playlistToExclude.id}.` + `${errorMessage} Switching to playlist ${nextPlaylist.id}.`); // if audio group changed reset audio loaders
33642
33643      if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) {
33644        this.delegateLoaders_('audio', ['abort', 'pause']);
33645      } // if subtitle group changed reset subtitle loaders
33646
33647
33648      if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) {
33649        this.delegateLoaders_('subtitle', ['abort', 'pause']);
33650      }
33651
33652      this.delegateLoaders_('main', ['abort', 'pause']);
33653      const delayDuration = nextPlaylist.targetDuration / 2 * 1000 || 5 * 1000;
33654      const shouldDelay = typeof nextPlaylist.lastRequest === 'number' && Date.now() - nextPlaylist.lastRequest <= delayDuration; // delay if it's a final rendition or if the last refresh is sooner than half targetDuration
33655
33656      return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay);
33657    }
33658    /**
33659     * Pause all segment/playlist loaders
33660     */
33661
33662
33663    pauseLoading() {
33664      this.delegateLoaders_('all', ['abort', 'pause']);
33665      this.stopABRTimer_();
33666    }
33667    /**
33668     * Call a set of functions in order on playlist loaders, segment loaders,
33669     * or both types of loaders.
33670     *
33671     * @param {string} filter
33672     *        Filter loaders that should call fnNames using a string. Can be:
33673     *        * all - run on all loaders
33674     *        * audio - run on all audio loaders
33675     *        * subtitle - run on all subtitle loaders
33676     *        * main - run on the main loaders
33677     *
33678     * @param {Array|string} fnNames
33679     *        A string or array of function names to call.
33680     */
33681
33682
33683    delegateLoaders_(filter, fnNames) {
33684      const loaders = [];
33685      const dontFilterPlaylist = filter === 'all';
33686
33687      if (dontFilterPlaylist || filter === 'main') {
33688        loaders.push(this.mainPlaylistLoader_);
33689      }
33690
33691      const mediaTypes = [];
33692
33693      if (dontFilterPlaylist || filter === 'audio') {
33694        mediaTypes.push('AUDIO');
33695      }
33696
33697      if (dontFilterPlaylist || filter === 'subtitle') {
33698        mediaTypes.push('CLOSED-CAPTIONS');
33699        mediaTypes.push('SUBTITLES');
33700      }
33701
33702      mediaTypes.forEach(mediaType => {
33703        const loader = this.mediaTypes_[mediaType] && this.mediaTypes_[mediaType].activePlaylistLoader;
33704
33705        if (loader) {
33706          loaders.push(loader);
33707        }
33708      });
33709      ['main', 'audio', 'subtitle'].forEach(name => {
33710        const loader = this[`${name}SegmentLoader_`];
33711
33712        if (loader && (filter === name || filter === 'all')) {
33713          loaders.push(loader);
33714        }
33715      });
33716      loaders.forEach(loader => fnNames.forEach(fnName => {
33717        if (typeof loader[fnName] === 'function') {
33718          loader[fnName]();
33719        }
33720      }));
33721    }
33722    /**
33723     * set the current time on all segment loaders
33724     *
33725     * @param {TimeRange} currentTime the current time to set
33726     * @return {TimeRange} the current time
33727     */
33728
33729
33730    setCurrentTime(currentTime) {
33731      const buffered = findRange(this.tech_.buffered(), currentTime);
33732
33733      if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) {
33734        // return immediately if the metadata is not ready yet
33735        return 0;
33736      } // it's clearly an edge-case but don't thrown an error if asked to
33737      // seek within an empty playlist
33738
33739
33740      if (!this.mainPlaylistLoader_.media().segments) {
33741        return 0;
33742      } // if the seek location is already buffered, continue buffering as usual
33743
33744
33745      if (buffered && buffered.length) {
33746        return currentTime;
33747      } // cancel outstanding requests so we begin buffering at the new
33748      // location
33749
33750
33751      this.mainSegmentLoader_.pause();
33752      this.mainSegmentLoader_.resetEverything();
33753
33754      if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33755        this.audioSegmentLoader_.pause();
33756        this.audioSegmentLoader_.resetEverything();
33757      }
33758
33759      if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
33760        this.subtitleSegmentLoader_.pause();
33761        this.subtitleSegmentLoader_.resetEverything();
33762      } // start segment loader loading in case they are paused
33763
33764
33765      this.load();
33766    }
33767    /**
33768     * get the current duration
33769     *
33770     * @return {TimeRange} the duration
33771     */
33772
33773
33774    duration() {
33775      if (!this.mainPlaylistLoader_) {
33776        return 0;
33777      }
33778
33779      const media = this.mainPlaylistLoader_.media();
33780
33781      if (!media) {
33782        // no playlists loaded yet, so can't determine a duration
33783        return 0;
33784      } // Don't rely on the media source for duration in the case of a live playlist since
33785      // setting the native MediaSource's duration to infinity ends up with consequences to
33786      // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
33787      //
33788      // This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
33789      // however, few browsers have support for setLiveSeekableRange()
33790      // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
33791      //
33792      // Until a time when the duration of the media source can be set to infinity, and a
33793      // seekable range specified across browsers, just return Infinity.
33794
33795
33796      if (!media.endList) {
33797        return Infinity;
33798      } // Since this is a VOD video, it is safe to rely on the media source's duration (if
33799      // available). If it's not available, fall back to a playlist-calculated estimate.
33800
33801
33802      if (this.mediaSource) {
33803        return this.mediaSource.duration;
33804      }
33805
33806      return Vhs$1.Playlist.duration(media);
33807    }
33808    /**
33809     * check the seekable range
33810     *
33811     * @return {TimeRange} the seekable range
33812     */
33813
33814
33815    seekable() {
33816      return this.seekable_;
33817    }
33818
33819    getSeekableRange_(playlistLoader, mediaType) {
33820      const media = playlistLoader.media();
33821
33822      if (!media) {
33823        return null;
33824      }
33825
33826      const mediaSequenceSync = this.syncController_.getMediaSequenceSync(mediaType);
33827
33828      if (mediaSequenceSync && mediaSequenceSync.isReliable) {
33829        const start = mediaSequenceSync.start;
33830        const end = mediaSequenceSync.end;
33831
33832        if (!isFinite(start) || !isFinite(end)) {
33833          return null;
33834        }
33835
33836        const liveEdgeDelay = Vhs$1.Playlist.liveEdgeDelay(this.mainPlaylistLoader_.main, media); // Make sure our seekable end is not less than the seekable start
33837
33838        const calculatedEnd = Math.max(start, end - liveEdgeDelay);
33839        return createTimeRanges([[start, calculatedEnd]]);
33840      }
33841
33842      const expired = this.syncController_.getExpiredTime(media, this.duration());
33843
33844      if (expired === null) {
33845        return null;
33846      }
33847
33848      const seekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(this.mainPlaylistLoader_.main, media));
33849      return seekable.length ? seekable : null;
33850    }
33851
33852    computeFinalSeekable_(mainSeekable, audioSeekable) {
33853      if (!audioSeekable) {
33854        return mainSeekable;
33855      }
33856
33857      const mainStart = mainSeekable.start(0);
33858      const mainEnd = mainSeekable.end(0);
33859      const audioStart = audioSeekable.start(0);
33860      const audioEnd = audioSeekable.end(0);
33861
33862      if (audioStart > mainEnd || mainStart > audioEnd) {
33863        // Seekables are far apart, rely on main
33864        return mainSeekable;
33865      } // Return the overlapping seekable range
33866
33867
33868      return createTimeRanges([[Math.max(mainStart, audioStart), Math.min(mainEnd, audioEnd)]]);
33869    }
33870
33871    onSyncInfoUpdate_() {
33872      // TODO check for creation of both source buffers before updating seekable
33873      //
33874      // A fix was made to this function where a check for
33875      // this.sourceUpdater_.hasCreatedSourceBuffers
33876      // was added to ensure that both source buffers were created before seekable was
33877      // updated. However, it originally had a bug where it was checking for a true and
33878      // returning early instead of checking for false. Setting it to check for false to
33879      // return early though created other issues. A call to play() would check for seekable
33880      // end without verifying that a seekable range was present. In addition, even checking
33881      // for that didn't solve some issues, as handleFirstPlay is sometimes worked around
33882      // due to a media update calling load on the segment loaders, skipping a seek to live,
33883      // thereby starting live streams at the beginning of the stream rather than at the end.
33884      //
33885      // This conditional should be fixed to wait for the creation of two source buffers at
33886      // the same time as the other sections of code are fixed to properly seek to live and
33887      // not throw an error due to checking for a seekable end when no seekable range exists.
33888      //
33889      // For now, fall back to the older behavior, with the understanding that the seekable
33890      // range may not be completely correct, leading to a suboptimal initial live point.
33891      if (!this.mainPlaylistLoader_) {
33892        return;
33893      }
33894
33895      const mainSeekable = this.getSeekableRange_(this.mainPlaylistLoader_, 'main');
33896
33897      if (!mainSeekable) {
33898        return;
33899      }
33900
33901      let audioSeekable;
33902
33903      if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
33904        audioSeekable = this.getSeekableRange_(this.mediaTypes_.AUDIO.activePlaylistLoader, 'audio');
33905
33906        if (!audioSeekable) {
33907          return;
33908        }
33909      }
33910
33911      const oldSeekable = this.seekable_;
33912      this.seekable_ = this.computeFinalSeekable_(mainSeekable, audioSeekable);
33913
33914      if (!this.seekable_) {
33915        return;
33916      }
33917
33918      if (oldSeekable && oldSeekable.length && this.seekable_.length) {
33919        if (oldSeekable.start(0) === this.seekable_.start(0) && oldSeekable.end(0) === this.seekable_.end(0)) {
33920          // Seekable range hasn't changed
33921          return;
33922        }
33923      }
33924
33925      this.logger_(`seekable updated [${printableRange(this.seekable_)}]`);
33926      const metadata = {
33927        seekableRanges: this.seekable_
33928      };
33929      this.trigger({
33930        type: 'seekablerangeschanged',
33931        metadata
33932      });
33933      this.tech_.trigger('seekablechanged');
33934    }
33935    /**
33936     * Update the player duration
33937     */
33938
33939
33940    updateDuration(isLive) {
33941      if (this.updateDuration_) {
33942        this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
33943        this.updateDuration_ = null;
33944      }
33945
33946      if (this.mediaSource.readyState !== 'open') {
33947        this.updateDuration_ = this.updateDuration.bind(this, isLive);
33948        this.mediaSource.addEventListener('sourceopen', this.updateDuration_);
33949        return;
33950      }
33951
33952      if (isLive) {
33953        const seekable = this.seekable();
33954
33955        if (!seekable.length) {
33956          return;
33957        } // Even in the case of a live playlist, the native MediaSource's duration should not
33958        // be set to Infinity (even though this would be expected for a live playlist), since
33959        // setting the native MediaSource's duration to infinity ends up with consequences to
33960        // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
33961        //
33962        // This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
33963        // however, few browsers have support for setLiveSeekableRange()
33964        // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
33965        //
33966        // Until a time when the duration of the media source can be set to infinity, and a
33967        // seekable range specified across browsers, the duration should be greater than or
33968        // equal to the last possible seekable value.
33969        // MediaSource duration starts as NaN
33970        // It is possible (and probable) that this case will never be reached for many
33971        // sources, since the MediaSource reports duration as the highest value without
33972        // accounting for timestamp offset. For example, if the timestamp offset is -100 and
33973        // we buffered times 0 to 100 with real times of 100 to 200, even though current
33974        // time will be between 0 and 100, the native media source may report the duration
33975        // as 200. However, since we report duration separate from the media source (as
33976        // Infinity), and as long as the native media source duration value is greater than
33977        // our reported seekable range, seeks will work as expected. The large number as
33978        // duration for live is actually a strategy used by some players to work around the
33979        // issue of live seekable ranges cited above.
33980
33981
33982        if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) {
33983          this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1));
33984        }
33985
33986        return;
33987      }
33988
33989      const buffered = this.tech_.buffered();
33990      let duration = Vhs$1.Playlist.duration(this.mainPlaylistLoader_.media());
33991
33992      if (buffered.length > 0) {
33993        duration = Math.max(duration, buffered.end(buffered.length - 1));
33994      }
33995
33996      if (this.mediaSource.duration !== duration) {
33997        this.sourceUpdater_.setDuration(duration);
33998      }
33999    }
34000    /**
34001     * dispose of the PlaylistController and everything
34002     * that it controls
34003     */
34004
34005
34006    dispose() {
34007      this.trigger('dispose');
34008      this.decrypter_.terminate();
34009      this.mainPlaylistLoader_.dispose();
34010      this.mainSegmentLoader_.dispose();
34011      this.contentSteeringController_.dispose();
34012      this.keyStatusMap_.clear();
34013
34014      if (this.loadOnPlay_) {
34015        this.tech_.off('play', this.loadOnPlay_);
34016      }
34017
34018      ['AUDIO', 'SUBTITLES'].forEach(type => {
34019        const groups = this.mediaTypes_[type].groups;
34020
34021        for (const id in groups) {
34022          groups[id].forEach(group => {
34023            if (group.playlistLoader) {
34024              group.playlistLoader.dispose();
34025            }
34026          });
34027        }
34028      });
34029      this.audioSegmentLoader_.dispose();
34030      this.subtitleSegmentLoader_.dispose();
34031      this.sourceUpdater_.dispose();
34032      this.timelineChangeController_.dispose();
34033      this.stopABRTimer_();
34034
34035      if (this.updateDuration_) {
34036        this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
34037      }
34038
34039      this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_); // load the media source into the player
34040
34041      this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_);
34042      this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_);
34043      this.off();
34044    }
34045    /**
34046     * return the main playlist object if we have one
34047     *
34048     * @return {Object} the main playlist object that we parsed
34049     */
34050
34051
34052    main() {
34053      return this.mainPlaylistLoader_.main;
34054    }
34055    /**
34056     * return the currently selected playlist
34057     *
34058     * @return {Object} the currently selected playlist object that we parsed
34059     */
34060
34061
34062    media() {
34063      // playlist loader will not return media if it has not been fully loaded
34064      return this.mainPlaylistLoader_.media() || this.initialMedia_;
34065    }
34066
34067    areMediaTypesKnown_() {
34068      const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
34069      const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_(); // if we are not using an audio loader, then we have audio media info
34070      // otherwise check on the segment loader.
34071
34072      const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_(); // one or both loaders has not loaded sufficently to get codecs
34073
34074      if (!hasMainMediaInfo || !hasAudioMediaInfo) {
34075        return false;
34076      }
34077
34078      return true;
34079    } // find from and to for codec switch event
34080
34081
34082    getCodecsOrExclude_() {
34083      const media = {
34084        main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {},
34085        audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {}
34086      };
34087      const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); // set "main" media equal to video
34088
34089      media.video = media.main;
34090      const playlistCodecs = codecsForPlaylist(this.main(), playlist);
34091      const codecs = {};
34092      const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
34093
34094      if (media.main.hasVideo) {
34095        codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC;
34096      }
34097
34098      if (media.main.isMuxed) {
34099        codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`;
34100      }
34101
34102      if (media.main.hasAudio && !media.main.isMuxed || media.audio.hasAudio || usingAudioLoader) {
34103        codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC; // set audio isFmp4 so we use the correct "supports" function below
34104
34105        media.audio.isFmp4 = media.main.hasAudio && !media.main.isMuxed ? media.main.isFmp4 : media.audio.isFmp4;
34106      } // no codecs, no playback.
34107
34108
34109      if (!codecs.audio && !codecs.video) {
34110        this.excludePlaylist({
34111          playlistToExclude: playlist,
34112          error: {
34113            message: 'Could not determine codecs for playlist.'
34114          },
34115          playlistExclusionDuration: Infinity
34116        });
34117        return;
34118      } // fmp4 relies on browser support, while ts relies on muxer support
34119
34120
34121      const supportFunction = (isFmp4, codec) => isFmp4 ? browserSupportsCodec(codec, this.usingManagedMediaSource_) : muxerSupportsCodec(codec);
34122
34123      const unsupportedCodecs = {};
34124      let unsupportedAudio;
34125      ['video', 'audio'].forEach(function (type) {
34126        if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) {
34127          const supporter = media[type].isFmp4 ? 'browser' : 'muxer';
34128          unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || [];
34129          unsupportedCodecs[supporter].push(codecs[type]);
34130
34131          if (type === 'audio') {
34132            unsupportedAudio = supporter;
34133          }
34134        }
34135      });
34136
34137      if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) {
34138        const audioGroup = playlist.attributes.AUDIO;
34139        this.main().playlists.forEach(variant => {
34140          const variantAudioGroup = variant.attributes && variant.attributes.AUDIO;
34141
34142          if (variantAudioGroup === audioGroup && variant !== playlist) {
34143            variant.excludeUntil = Infinity;
34144          }
34145        });
34146        this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`);
34147      } // if we have any unsupported codecs exclude this playlist.
34148
34149
34150      if (Object.keys(unsupportedCodecs).length) {
34151        const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => {
34152          if (acc) {
34153            acc += ', ';
34154          }
34155
34156          acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`;
34157          return acc;
34158        }, '') + '.';
34159        this.excludePlaylist({
34160          playlistToExclude: playlist,
34161          error: {
34162            internal: true,
34163            message
34164          },
34165          playlistExclusionDuration: Infinity
34166        });
34167        return;
34168      } // check if codec switching is happening
34169
34170
34171      if (this.sourceUpdater_.hasCreatedSourceBuffers() && !this.sourceUpdater_.canChangeType()) {
34172        const switchMessages = [];
34173        ['video', 'audio'].forEach(type => {
34174          const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type;
34175          const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type;
34176
34177          if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) {
34178            switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`);
34179          }
34180        });
34181
34182        if (switchMessages.length) {
34183          this.excludePlaylist({
34184            playlistToExclude: playlist,
34185            error: {
34186              message: `Codec switching not supported: ${switchMessages.join(', ')}.`,
34187              internal: true
34188            },
34189            playlistExclusionDuration: Infinity
34190          });
34191          return;
34192        }
34193      } // TODO: when using the muxer shouldn't we just return
34194      // the codecs that the muxer outputs?
34195
34196
34197      return codecs;
34198    }
34199    /**
34200     * Create source buffers and exlude any incompatible renditions.
34201     *
34202     * @private
34203     */
34204
34205
34206    tryToCreateSourceBuffers_() {
34207      // media source is not ready yet or sourceBuffers are already
34208      // created.
34209      if (this.mediaSource.readyState !== 'open' || this.sourceUpdater_.hasCreatedSourceBuffers()) {
34210        return;
34211      }
34212
34213      if (!this.areMediaTypesKnown_()) {
34214        return;
34215      }
34216
34217      const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded
34218
34219      if (!codecs) {
34220        return;
34221      }
34222
34223      this.sourceUpdater_.createSourceBuffers(codecs);
34224      const codecString = [codecs.video, codecs.audio].filter(Boolean).join(',');
34225      this.excludeIncompatibleVariants_(codecString);
34226    }
34227    /**
34228     * Excludes playlists with codecs that are unsupported by the muxer and browser.
34229     */
34230
34231
34232    excludeUnsupportedVariants_() {
34233      const playlists = this.main().playlists;
34234      const ids = []; // TODO: why don't we have a property to loop through all
34235      // playlist? Why did we ever mix indexes and keys?
34236
34237      Object.keys(playlists).forEach(key => {
34238        const variant = playlists[key]; // check if we already processed this playlist.
34239
34240        if (ids.indexOf(variant.id) !== -1) {
34241          return;
34242        }
34243
34244        ids.push(variant.id);
34245        const codecs = codecsForPlaylist(this.main, variant);
34246        const unsupported = [];
34247
34248        if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio, this.usingManagedMediaSource_)) {
34249          unsupported.push(`audio codec ${codecs.audio}`);
34250        }
34251
34252        if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video, this.usingManagedMediaSource_)) {
34253          unsupported.push(`video codec ${codecs.video}`);
34254        }
34255
34256        if (codecs.text && codecs.text === 'stpp.ttml.im1t') {
34257          unsupported.push(`text codec ${codecs.text}`);
34258        }
34259
34260        if (unsupported.length) {
34261          variant.excludeUntil = Infinity;
34262          this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`);
34263        }
34264      });
34265    }
34266    /**
34267     * Exclude playlists that are known to be codec or
34268     * stream-incompatible with the SourceBuffer configuration. For
34269     * instance, Media Source Extensions would cause the video element to
34270     * stall waiting for video data if you switched from a variant with
34271     * video and audio to an audio-only one.
34272     *
34273     * @param {Object} media a media playlist compatible with the current
34274     * set of SourceBuffers. Variants in the current main playlist that
34275     * do not appear to have compatible codec or stream configurations
34276     * will be excluded from the default playlist selection algorithm
34277     * indefinitely.
34278     * @private
34279     */
34280
34281
34282    excludeIncompatibleVariants_(codecString) {
34283      const ids = [];
34284      const playlists = this.main().playlists;
34285      const codecs = unwrapCodecList(parseCodecs(codecString));
34286      const codecCount_ = codecCount(codecs);
34287      const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null;
34288      const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null;
34289      Object.keys(playlists).forEach(key => {
34290        const variant = playlists[key]; // check if we already processed this playlist.
34291        // or it if it is already excluded forever.
34292
34293        if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) {
34294          return;
34295        }
34296
34297        ids.push(variant.id);
34298        const exclusionReasons = []; // get codecs from the playlist for this variant
34299
34300        const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant);
34301        const variantCodecCount = codecCount(variantCodecs); // if no codecs are listed, we cannot determine that this
34302        // variant is incompatible. Wait for mux.js to probe
34303
34304        if (!variantCodecs.audio && !variantCodecs.video) {
34305          return;
34306        } // TODO: we can support this by removing the
34307        // old media source and creating a new one, but it will take some work.
34308        // The number of streams cannot change
34309
34310
34311        if (variantCodecCount !== codecCount_) {
34312          exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`);
34313        } // only exclude playlists by codec change, if codecs cannot switch
34314        // during playback.
34315
34316
34317        if (!this.sourceUpdater_.canChangeType()) {
34318          const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null;
34319          const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null; // the video codec cannot change
34320
34321          if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) {
34322            exclusionReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`);
34323          } // the audio codec cannot change
34324
34325
34326          if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) {
34327            exclusionReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`);
34328          }
34329        }
34330
34331        if (exclusionReasons.length) {
34332          variant.excludeUntil = Infinity;
34333          this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`);
34334        }
34335      });
34336    }
34337
34338    updateAdCues_(media) {
34339      let offset = 0;
34340      const seekable = this.seekable();
34341
34342      if (seekable.length) {
34343        offset = seekable.start(0);
34344      }
34345
34346      updateAdCues(media, this.cueTagsTrack_, offset);
34347    }
34348    /**
34349     * Calculates the desired forward buffer length based on current time
34350     *
34351     * @return {number} Desired forward buffer length in seconds
34352     */
34353
34354
34355    goalBufferLength() {
34356      const currentTime = this.tech_.currentTime();
34357      const initial = Config.GOAL_BUFFER_LENGTH;
34358      const rate = Config.GOAL_BUFFER_LENGTH_RATE;
34359      const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH);
34360      return Math.min(initial + currentTime * rate, max);
34361    }
34362    /**
34363     * Calculates the desired buffer low water line based on current time
34364     *
34365     * @return {number} Desired buffer low water line in seconds
34366     */
34367
34368
34369    bufferLowWaterLine() {
34370      const currentTime = this.tech_.currentTime();
34371      const initial = Config.BUFFER_LOW_WATER_LINE;
34372      const rate = Config.BUFFER_LOW_WATER_LINE_RATE;
34373      const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE);
34374      const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE);
34375      return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max);
34376    }
34377
34378    bufferHighWaterLine() {
34379      return Config.BUFFER_HIGH_WATER_LINE;
34380    }
34381
34382    addDateRangesToTextTrack_(dateRanges) {
34383      createMetadataTrackIfNotExists(this.inbandTextTracks_, 'com.apple.streaming', this.tech_);
34384      addDateRangeMetadata({
34385        inbandTextTracks: this.inbandTextTracks_,
34386        dateRanges
34387      });
34388    }
34389
34390    addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) {
34391      const timestampOffset = this.sourceUpdater_.videoBuffer ? this.sourceUpdater_.videoTimestampOffset() : this.sourceUpdater_.audioTimestampOffset(); // There's potentially an issue where we could double add metadata if there's a muxed
34392      // audio/video source with a metadata track, and an alt audio with a metadata track.
34393      // However, this probably won't happen, and if it does it can be handled then.
34394
34395      createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_);
34396      addMetadata({
34397        inbandTextTracks: this.inbandTextTracks_,
34398        metadataArray,
34399        timestampOffset,
34400        videoDuration
34401      });
34402    }
34403    /**
34404     * Utility for getting the pathway or service location from an HLS or DASH playlist.
34405     *
34406     * @param {Object} playlist for getting pathway from.
34407     * @return the pathway attribute of a playlist
34408     */
34409
34410
34411    pathwayAttribute_(playlist) {
34412      return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation;
34413    }
34414    /**
34415     * Initialize available pathways and apply the tag properties.
34416     */
34417
34418
34419    initContentSteeringController_() {
34420      const main = this.main();
34421
34422      if (!main.contentSteering) {
34423        return;
34424      }
34425
34426      for (const playlist of main.playlists) {
34427        this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist));
34428      }
34429
34430      this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering); // request the steering manifest immediately if queryBeforeStart is set.
34431
34432      if (this.contentSteeringController_.queryBeforeStart) {
34433        // When queryBeforeStart is true, initial request should omit steering parameters.
34434        this.contentSteeringController_.requestSteeringManifest(true);
34435        return;
34436      } // otherwise start content steering after playback starts
34437
34438
34439      this.tech_.one('canplay', () => {
34440        this.contentSteeringController_.requestSteeringManifest();
34441      });
34442    }
34443    /**
34444     * Reset the content steering controller and re-init.
34445     */
34446
34447
34448    resetContentSteeringController_() {
34449      this.contentSteeringController_.clearAvailablePathways();
34450      this.contentSteeringController_.dispose();
34451      this.initContentSteeringController_();
34452    }
34453    /**
34454     * Attaches the listeners for content steering.
34455     */
34456
34457
34458    attachContentSteeringListeners_() {
34459      this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this));
34460      const contentSteeringEvents = ['contentsteeringloadstart', 'contentsteeringloadcomplete', 'contentsteeringparsed'];
34461      contentSteeringEvents.forEach(eventName => {
34462        this.contentSteeringController_.on(eventName, metadata => {
34463          this.trigger(_extends({}, metadata));
34464        });
34465      });
34466
34467      if (this.sourceType_ === 'dash') {
34468        this.mainPlaylistLoader_.on('loadedplaylist', () => {
34469          const main = this.main(); // check if steering tag or pathways changed.
34470
34471          const didDashTagChange = this.contentSteeringController_.didDASHTagChange(main.uri, main.contentSteering);
34472
34473          const didPathwaysChange = () => {
34474            const availablePathways = this.contentSteeringController_.getAvailablePathways();
34475            const newPathways = [];
34476
34477            for (const playlist of main.playlists) {
34478              const serviceLocation = playlist.attributes.serviceLocation;
34479
34480              if (serviceLocation) {
34481                newPathways.push(serviceLocation);
34482
34483                if (!availablePathways.has(serviceLocation)) {
34484                  return true;
34485                }
34486              }
34487            } // If we have no new serviceLocations and previously had availablePathways
34488
34489
34490            if (!newPathways.length && availablePathways.size) {
34491              return true;
34492            }
34493
34494            return false;
34495          };
34496
34497          if (didDashTagChange || didPathwaysChange()) {
34498            this.resetContentSteeringController_();
34499          }
34500        });
34501      }
34502    }
34503    /**
34504     * Simple exclude and change playlist logic for content steering.
34505     */
34506
34507
34508    excludeThenChangePathway_() {
34509      const currentPathway = this.contentSteeringController_.getPathway();
34510
34511      if (!currentPathway) {
34512        return;
34513      }
34514
34515      this.handlePathwayClones_();
34516      const main = this.main();
34517      const playlists = main.playlists;
34518      const ids = new Set();
34519      let didEnablePlaylists = false;
34520      Object.keys(playlists).forEach(key => {
34521        const variant = playlists[key];
34522        const pathwayId = this.pathwayAttribute_(variant);
34523        const differentPathwayId = pathwayId && currentPathway !== pathwayId;
34524        const steeringExclusion = variant.excludeUntil === Infinity && variant.lastExcludeReason_ === 'content-steering';
34525
34526        if (steeringExclusion && !differentPathwayId) {
34527          delete variant.excludeUntil;
34528          delete variant.lastExcludeReason_;
34529          didEnablePlaylists = true;
34530        }
34531
34532        const noExcludeUntil = !variant.excludeUntil && variant.excludeUntil !== Infinity;
34533        const shouldExclude = !ids.has(variant.id) && differentPathwayId && noExcludeUntil;
34534
34535        if (!shouldExclude) {
34536          return;
34537        }
34538
34539        ids.add(variant.id);
34540        variant.excludeUntil = Infinity;
34541        variant.lastExcludeReason_ = 'content-steering'; // TODO: kind of spammy, maybe move this.
34542
34543        this.logger_(`excluding ${variant.id} for ${variant.lastExcludeReason_}`);
34544      });
34545
34546      if (this.contentSteeringController_.manifestType_ === 'DASH') {
34547        Object.keys(this.mediaTypes_).forEach(key => {
34548          const type = this.mediaTypes_[key];
34549
34550          if (type.activePlaylistLoader) {
34551            const currentPlaylist = type.activePlaylistLoader.media_; // Check if the current media playlist matches the current CDN
34552
34553            if (currentPlaylist && currentPlaylist.attributes.serviceLocation !== currentPathway) {
34554              didEnablePlaylists = true;
34555            }
34556          }
34557        });
34558      }
34559
34560      if (didEnablePlaylists) {
34561        this.changeSegmentPathway_();
34562      }
34563    }
34564    /**
34565     * Add, update, or delete playlists and media groups for
34566     * the pathway clones for HLS Content Steering.
34567     *
34568     * See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
34569     *
34570     * NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and
34571     * `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or
34572     * `STABLE-RENDITION-ID` values.
34573     */
34574
34575
34576    handlePathwayClones_() {
34577      const main = this.main();
34578      const playlists = main.playlists;
34579      const currentPathwayClones = this.contentSteeringController_.currentPathwayClones;
34580      const nextPathwayClones = this.contentSteeringController_.nextPathwayClones;
34581      const hasClones = currentPathwayClones && currentPathwayClones.size || nextPathwayClones && nextPathwayClones.size;
34582
34583      if (!hasClones) {
34584        return;
34585      }
34586
34587      for (const [id, clone] of currentPathwayClones.entries()) {
34588        const newClone = nextPathwayClones.get(id); // Delete the old pathway clone.
34589
34590        if (!newClone) {
34591          this.mainPlaylistLoader_.updateOrDeleteClone(clone);
34592          this.contentSteeringController_.excludePathway(id);
34593        }
34594      }
34595
34596      for (const [id, clone] of nextPathwayClones.entries()) {
34597        const oldClone = currentPathwayClones.get(id); // Create a new pathway if it is a new pathway clone object.
34598
34599        if (!oldClone) {
34600          const playlistsToClone = playlists.filter(p => {
34601            return p.attributes['PATHWAY-ID'] === clone['BASE-ID'];
34602          });
34603          playlistsToClone.forEach(p => {
34604            this.mainPlaylistLoader_.addClonePathway(clone, p);
34605          });
34606          this.contentSteeringController_.addAvailablePathway(id);
34607          continue;
34608        } // There have not been changes to the pathway clone object, so skip.
34609
34610
34611        if (this.equalPathwayClones_(oldClone, clone)) {
34612          continue;
34613        } // Update a preexisting cloned pathway.
34614        // True is set for the update flag.
34615
34616
34617        this.mainPlaylistLoader_.updateOrDeleteClone(clone, true);
34618        this.contentSteeringController_.addAvailablePathway(id);
34619      } // Deep copy contents of next to current pathways.
34620
34621
34622      this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones])));
34623    }
34624    /**
34625     * Determines whether two pathway clone objects are equivalent.
34626     *
34627     * @param {Object} a The first pathway clone object.
34628     * @param {Object} b The second pathway clone object.
34629     * @return {boolean} True if the pathway clone objects are equal, false otherwise.
34630     */
34631
34632
34633    equalPathwayClones_(a, b) {
34634      if (a['BASE-ID'] !== b['BASE-ID'] || a.ID !== b.ID || a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST) {
34635        return false;
34636      }
34637
34638      const aParams = a['URI-REPLACEMENT'].PARAMS;
34639      const bParams = b['URI-REPLACEMENT'].PARAMS; // We need to iterate through both lists of params because one could be
34640      // missing a parameter that the other has.
34641
34642      for (const p in aParams) {
34643        if (aParams[p] !== bParams[p]) {
34644          return false;
34645        }
34646      }
34647
34648      for (const p in bParams) {
34649        if (aParams[p] !== bParams[p]) {
34650          return false;
34651        }
34652      }
34653
34654      return true;
34655    }
34656    /**
34657     * Changes the current playlists for audio, video and subtitles after a new pathway
34658     * is chosen from content steering.
34659     */
34660
34661
34662    changeSegmentPathway_() {
34663      const nextPlaylist = this.selectPlaylist();
34664      this.pauseLoading(); // Switch audio and text track playlists if necessary in DASH
34665
34666      if (this.contentSteeringController_.manifestType_ === 'DASH') {
34667        this.switchMediaForDASHContentSteering_();
34668      }
34669
34670      this.switchMedia_(nextPlaylist, 'content-steering');
34671    }
34672    /**
34673     * Iterates through playlists and check their keyId set and compare with the
34674     * keyStatusMap, only enable playlists that have a usable key. If the playlist
34675     * has no keyId leave it enabled by default.
34676     */
34677
34678
34679    excludeNonUsablePlaylistsByKeyId_() {
34680      if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) {
34681        return;
34682      }
34683
34684      let nonUsableKeyStatusCount = 0;
34685      const NON_USABLE = 'non-usable';
34686      this.mainPlaylistLoader_.main.playlists.forEach(playlist => {
34687        const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); // If the playlist doesn't have keyIDs lets not exclude it.
34688
34689        if (!keyIdSet || !keyIdSet.size) {
34690          return;
34691        }
34692
34693        keyIdSet.forEach(key => {
34694          const USABLE = 'usable';
34695          const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE;
34696          const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity;
34697
34698          if (!hasUsableKeyStatus) {
34699            // Only exclude playlists that haven't already been excluded as non-usable.
34700            if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) {
34701              playlist.excludeUntil = Infinity;
34702              playlist.lastExcludeReason_ = NON_USABLE;
34703              this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`);
34704            } // count all nonUsableKeyStatus
34705
34706
34707            nonUsableKeyStatusCount++;
34708          } else if (hasUsableKeyStatus && nonUsableExclusion) {
34709            delete playlist.excludeUntil;
34710            delete playlist.lastExcludeReason_;
34711            this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`);
34712          }
34713        });
34714      }); // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe.
34715
34716      if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) {
34717        this.mainPlaylistLoader_.main.playlists.forEach(playlist => {
34718          const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720;
34719          const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE;
34720
34721          if (isNonHD && excludedForNonUsableKey) {
34722            // Only delete the excludeUntil so we don't try and re-exclude these playlists.
34723            delete playlist.excludeUntil;
34724            videojs__default["default"].log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`);
34725          }
34726        });
34727      }
34728    }
34729    /**
34730     * Adds a keystatus to the keystatus map, tries to convert to string if necessary.
34731     *
34732     * @param {any} keyId the keyId to add a status for
34733     * @param {string} status the status of the keyId
34734     */
34735
34736
34737    addKeyStatus_(keyId, status) {
34738      const isString = typeof keyId === 'string';
34739      const keyIdHexString = isString ? keyId : bufferToHexString(keyId);
34740      const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase();
34741      this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`);
34742      this.keyStatusMap_.set(formattedKeyIdString, status);
34743    }
34744    /**
34745     * Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists.
34746     *
34747     * @param {any} keyId the keyId from the keystatuschange event
34748     * @param {string} status the key status string
34749     */
34750
34751
34752    updatePlaylistByKeyStatus(keyId, status) {
34753      this.addKeyStatus_(keyId, status);
34754
34755      if (!this.waitingForFastQualityPlaylistReceived_) {
34756        this.excludeNonUsableThenChangePlaylist_();
34757      } // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated.
34758
34759
34760      this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this));
34761      this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this));
34762    }
34763
34764    excludeNonUsableThenChangePlaylist_() {
34765      this.excludeNonUsablePlaylistsByKeyId_();
34766      this.fastQualityChange_();
34767    }
34768
34769  }
34770
34771  /**
34772   * Returns a function that acts as the Enable/disable playlist function.
34773   *
34774   * @param {PlaylistLoader} loader - The main playlist loader
34775   * @param {string} playlistID - id of the playlist
34776   * @param {Function} changePlaylistFn - A function to be called after a
34777   * playlist's enabled-state has been changed. Will NOT be called if a
34778   * playlist's enabled-state is unchanged
34779   * @param {boolean=} enable - Value to set the playlist enabled-state to
34780   * or if undefined returns the current enabled-state for the playlist
34781   * @return {Function} Function for setting/getting enabled
34782   */
34783
34784  const enableFunction = (loader, playlistID, changePlaylistFn) => enable => {
34785    const playlist = loader.main.playlists[playlistID];
34786    const incompatible = isIncompatible(playlist);
34787    const currentlyEnabled = isEnabled(playlist);
34788
34789    if (typeof enable === 'undefined') {
34790      return currentlyEnabled;
34791    }
34792
34793    if (enable) {
34794      delete playlist.disabled;
34795    } else {
34796      playlist.disabled = true;
34797    }
34798
34799    const metadata = {
34800      renditionInfo: {
34801        id: playlistID,
34802        bandwidth: playlist.attributes.BANDWIDTH,
34803        resolution: playlist.attributes.RESOLUTION,
34804        codecs: playlist.attributes.CODECS
34805      },
34806      cause: 'fast-quality'
34807    };
34808
34809    if (enable !== currentlyEnabled && !incompatible) {
34810      // Ensure the outside world knows about our changes
34811      if (enable) {
34812        // call fast quality change only when the playlist is enabled
34813        changePlaylistFn(playlist);
34814        loader.trigger({
34815          type: 'renditionenabled',
34816          metadata
34817        });
34818      } else {
34819        loader.trigger({
34820          type: 'renditiondisabled',
34821          metadata
34822        });
34823      }
34824    }
34825
34826    return enable;
34827  };
34828  /**
34829   * The representation object encapsulates the publicly visible information
34830   * in a media playlist along with a setter/getter-type function (enabled)
34831   * for changing the enabled-state of a particular playlist entry
34832   *
34833   * @class Representation
34834   */
34835
34836
34837  class Representation {
34838    constructor(vhsHandler, playlist, id) {
34839      const {
34840        playlistController_: pc
34841      } = vhsHandler;
34842      const qualityChangeFunction = pc.fastQualityChange_.bind(pc); // some playlist attributes are optional
34843
34844      if (playlist.attributes) {
34845        const resolution = playlist.attributes.RESOLUTION;
34846        this.width = resolution && resolution.width;
34847        this.height = resolution && resolution.height;
34848        this.bandwidth = playlist.attributes.BANDWIDTH;
34849        this.frameRate = playlist.attributes['FRAME-RATE'];
34850      }
34851
34852      this.codecs = codecsForPlaylist(pc.main(), playlist);
34853      this.playlist = playlist; // The id is simply the ordinality of the media playlist
34854      // within the main playlist
34855
34856      this.id = id; // Partially-apply the enableFunction to create a playlist-
34857      // specific variant
34858
34859      this.enabled = enableFunction(vhsHandler.playlists, playlist.id, qualityChangeFunction);
34860    }
34861
34862  }
34863  /**
34864   * A mixin function that adds the `representations` api to an instance
34865   * of the VhsHandler class
34866   *
34867   * @param {VhsHandler} vhsHandler - An instance of VhsHandler to add the
34868   * representation API into
34869   */
34870
34871
34872  const renditionSelectionMixin = function (vhsHandler) {
34873    // Add a single API-specific function to the VhsHandler instance
34874    vhsHandler.representations = () => {
34875      const main = vhsHandler.playlistController_.main();
34876      const playlists = isAudioOnly(main) ? vhsHandler.playlistController_.getAudioTrackPlaylists_() : main.playlists;
34877
34878      if (!playlists) {
34879        return [];
34880      }
34881
34882      return playlists.filter(media => !isIncompatible(media)).map((e, i) => new Representation(vhsHandler, e, e.id));
34883    };
34884  };
34885
34886  /**
34887   * @file playback-watcher.js
34888   *
34889   * Playback starts, and now my watch begins. It shall not end until my death. I shall
34890   * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns
34891   * and win no glory. I shall live and die at my post. I am the corrector of the underflow.
34892   * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge
34893   * my life and honor to the Playback Watch, for this Player and all the Players to come.
34894   */
34895
34896  const timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error'];
34897  /**
34898   * @class PlaybackWatcher
34899   */
34900
34901  class PlaybackWatcher extends videojs__default["default"].EventTarget {
34902    /**
34903     * Represents an PlaybackWatcher object.
34904     *
34905     * @class
34906     * @param {Object} options an object that includes the tech and settings
34907     */
34908    constructor(options) {
34909      super();
34910      this.playlistController_ = options.playlistController;
34911      this.tech_ = options.tech;
34912      this.seekable = options.seekable;
34913      this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow;
34914      this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta;
34915      this.media = options.media;
34916      this.playedRanges_ = [];
34917      this.consecutiveUpdates = 0;
34918      this.lastRecordedTime = null;
34919      this.checkCurrentTimeTimeout_ = null;
34920      this.logger_ = logger('PlaybackWatcher');
34921      this.logger_('initialize');
34922
34923      const playHandler = () => this.monitorCurrentTime_();
34924
34925      const canPlayHandler = () => this.monitorCurrentTime_();
34926
34927      const waitingHandler = () => this.techWaiting_();
34928
34929      const cancelTimerHandler = () => this.resetTimeUpdate_();
34930
34931      const pc = this.playlistController_;
34932      const loaderTypes = ['main', 'subtitle', 'audio'];
34933      const loaderChecks = {};
34934      loaderTypes.forEach(type => {
34935        loaderChecks[type] = {
34936          reset: () => this.resetSegmentDownloads_(type),
34937          updateend: () => this.checkSegmentDownloads_(type)
34938        };
34939        pc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend); // If a rendition switch happens during a playback stall where the buffer
34940        // isn't changing we want to reset. We cannot assume that the new rendition
34941        // will also be stalled, until after new appends.
34942
34943        pc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset); // Playback stalls should not be detected right after seeking.
34944        // This prevents one segment playlists (single vtt or single segment content)
34945        // from being detected as stalling. As the buffer will not change in those cases, since
34946        // the buffer is the entire video duration.
34947
34948        this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset);
34949      });
34950      /**
34951       * We check if a seek was into a gap through the following steps:
34952       * 1. We get a seeking event and we do not get a seeked event. This means that
34953       *    a seek was attempted but not completed.
34954       * 2. We run `fixesBadSeeks_` on segment loader appends. This means that we already
34955       *    removed everything from our buffer and appended a segment, and should be ready
34956       *    to check for gaps.
34957       */
34958
34959      const setSeekingHandlers = fn => {
34960        ['main', 'audio'].forEach(type => {
34961          pc[`${type}SegmentLoader_`][fn]('appended', this.seekingAppendCheck_);
34962        });
34963      };
34964
34965      this.seekingAppendCheck_ = () => {
34966        if (this.fixesBadSeeks_()) {
34967          this.consecutiveUpdates = 0;
34968          this.lastRecordedTime = this.tech_.currentTime();
34969          setSeekingHandlers('off');
34970        }
34971      };
34972
34973      this.clearSeekingAppendCheck_ = () => setSeekingHandlers('off');
34974
34975      this.watchForBadSeeking_ = () => {
34976        this.clearSeekingAppendCheck_();
34977        setSeekingHandlers('on');
34978      };
34979
34980      this.tech_.on('seeked', this.clearSeekingAppendCheck_);
34981      this.tech_.on('seeking', this.watchForBadSeeking_);
34982      this.tech_.on('waiting', waitingHandler);
34983      this.tech_.on(timerCancelEvents, cancelTimerHandler);
34984      this.tech_.on('canplay', canPlayHandler);
34985      /*
34986        An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case
34987        is surfaced in one of two ways:
34988         1)  The `waiting` event is fired before the player has buffered content, making it impossible
34989            to find or skip the gap. The `waiting` event is followed by a `play` event. On first play
34990            we can check if playback is stalled due to a gap, and skip the gap if necessary.
34991        2)  A source with a gap at the beginning of the stream is loaded programatically while the player
34992            is in a playing state. To catch this case, it's important that our one-time play listener is setup
34993            even if the player is in a playing state
34994      */
34995
34996      this.tech_.one('play', playHandler); // Define the dispose function to clean up our events
34997
34998      this.dispose = () => {
34999        this.clearSeekingAppendCheck_();
35000        this.logger_('dispose');
35001        this.tech_.off('waiting', waitingHandler);
35002        this.tech_.off(timerCancelEvents, cancelTimerHandler);
35003        this.tech_.off('canplay', canPlayHandler);
35004        this.tech_.off('play', playHandler);
35005        this.tech_.off('seeking', this.watchForBadSeeking_);
35006        this.tech_.off('seeked', this.clearSeekingAppendCheck_);
35007        loaderTypes.forEach(type => {
35008          pc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend);
35009          pc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset);
35010          this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset);
35011        });
35012
35013        if (this.checkCurrentTimeTimeout_) {
35014          window.clearTimeout(this.checkCurrentTimeTimeout_);
35015        }
35016
35017        this.resetTimeUpdate_();
35018      };
35019    }
35020    /**
35021     * Periodically check current time to see if playback stopped
35022     *
35023     * @private
35024     */
35025
35026
35027    monitorCurrentTime_() {
35028      this.checkCurrentTime_();
35029
35030      if (this.checkCurrentTimeTimeout_) {
35031        window.clearTimeout(this.checkCurrentTimeTimeout_);
35032      } // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
35033
35034
35035      this.checkCurrentTimeTimeout_ = window.setTimeout(this.monitorCurrentTime_.bind(this), 250);
35036    }
35037    /**
35038     * Reset stalled download stats for a specific type of loader
35039     *
35040     * @param {string} type
35041     *        The segment loader type to check.
35042     *
35043     * @listens SegmentLoader#playlistupdate
35044     * @listens Tech#seeking
35045     * @listens Tech#seeked
35046     */
35047
35048
35049    resetSegmentDownloads_(type) {
35050      const loader = this.playlistController_[`${type}SegmentLoader_`];
35051
35052      if (this[`${type}StalledDownloads_`] > 0) {
35053        this.logger_(`resetting possible stalled download count for ${type} loader`);
35054      }
35055
35056      this[`${type}StalledDownloads_`] = 0;
35057      this[`${type}Buffered_`] = loader.buffered_();
35058    }
35059    /**
35060     * Checks on every segment `appendsdone` to see
35061     * if segment appends are making progress. If they are not
35062     * and we are still downloading bytes. We exclude the playlist.
35063     *
35064     * @param {string} type
35065     *        The segment loader type to check.
35066     *
35067     * @listens SegmentLoader#appendsdone
35068     */
35069
35070
35071    checkSegmentDownloads_(type) {
35072      const pc = this.playlistController_;
35073      const loader = pc[`${type}SegmentLoader_`];
35074      const buffered = loader.buffered_();
35075      const isBufferedDifferent = isRangeDifferent(this[`${type}Buffered_`], buffered);
35076      this[`${type}Buffered_`] = buffered; // if another watcher is going to fix the issue or
35077      // the buffered value for this loader changed
35078      // appends are working
35079
35080      if (isBufferedDifferent) {
35081        const metadata = {
35082          bufferedRanges: buffered
35083        };
35084        pc.trigger({
35085          type: 'bufferedrangeschanged',
35086          metadata
35087        });
35088        this.resetSegmentDownloads_(type);
35089        return;
35090      }
35091
35092      this[`${type}StalledDownloads_`]++;
35093      this.logger_(`found #${this[`${type}StalledDownloads_`]} ${type} appends that did not increase buffer (possible stalled download)`, {
35094        playlistId: loader.playlist_ && loader.playlist_.id,
35095        buffered: timeRangesToArray(buffered)
35096      }); // after 10 possibly stalled appends with no reset, exclude
35097
35098      if (this[`${type}StalledDownloads_`] < 10) {
35099        return;
35100      }
35101
35102      this.logger_(`${type} loader stalled download exclusion`);
35103      this.resetSegmentDownloads_(type);
35104      this.tech_.trigger({
35105        type: 'usage',
35106        name: `vhs-${type}-download-exclusion`
35107      });
35108
35109      if (type === 'subtitle') {
35110        return;
35111      } // TODO: should we exclude audio tracks rather than main tracks
35112      // when type is audio?
35113
35114
35115      pc.excludePlaylist({
35116        error: {
35117          message: `Excessive ${type} segment downloading detected.`
35118        },
35119        playlistExclusionDuration: Infinity
35120      });
35121    }
35122    /**
35123     * The purpose of this function is to emulate the "waiting" event on
35124     * browsers that do not emit it when they are waiting for more
35125     * data to continue playback
35126     *
35127     * @private
35128     */
35129
35130
35131    checkCurrentTime_() {
35132      if (this.tech_.paused() || this.tech_.seeking()) {
35133        return;
35134      }
35135
35136      const currentTime = this.tech_.currentTime();
35137      const buffered = this.tech_.buffered();
35138
35139      if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) {
35140        // If current time is at the end of the final buffered region, then any playback
35141        // stall is most likely caused by buffering in a low bandwidth environment. The tech
35142        // should fire a `waiting` event in this scenario, but due to browser and tech
35143        // inconsistencies. Calling `techWaiting_` here allows us to simulate
35144        // responding to a native `waiting` event when the tech fails to emit one.
35145        return this.techWaiting_();
35146      }
35147
35148      if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) {
35149        this.consecutiveUpdates++;
35150        this.waiting_();
35151      } else if (currentTime === this.lastRecordedTime) {
35152        this.consecutiveUpdates++;
35153      } else {
35154        this.playedRanges_.push(createTimeRanges([this.lastRecordedTime, currentTime]));
35155        const metadata = {
35156          playedRanges: this.playedRanges_
35157        };
35158        this.playlistController_.trigger({
35159          type: 'playedrangeschanged',
35160          metadata
35161        });
35162        this.consecutiveUpdates = 0;
35163        this.lastRecordedTime = currentTime;
35164      }
35165    }
35166    /**
35167     * Resets the 'timeupdate' mechanism designed to detect that we are stalled
35168     *
35169     * @private
35170     */
35171
35172
35173    resetTimeUpdate_() {
35174      this.consecutiveUpdates = 0;
35175    }
35176    /**
35177     * Fixes situations where there's a bad seek
35178     *
35179     * @return {boolean} whether an action was taken to fix the seek
35180     * @private
35181     */
35182
35183
35184    fixesBadSeeks_() {
35185      const seeking = this.tech_.seeking();
35186
35187      if (!seeking) {
35188        return false;
35189      } // TODO: It's possible that these seekable checks should be moved out of this function
35190      // and into a function that runs on seekablechange. It's also possible that we only need
35191      // afterSeekableWindow as the buffered check at the bottom is good enough to handle before
35192      // seekable range.
35193
35194
35195      const seekable = this.seekable();
35196      const currentTime = this.tech_.currentTime();
35197      const isAfterSeekableRange = this.afterSeekableWindow_(seekable, currentTime, this.media(), this.allowSeeksWithinUnsafeLiveWindow);
35198      let seekTo;
35199
35200      if (isAfterSeekableRange) {
35201        const seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting)
35202
35203        seekTo = seekableEnd;
35204      }
35205
35206      if (this.beforeSeekableWindow_(seekable, currentTime)) {
35207        const seekableStart = seekable.start(0); // sync to the beginning of the live window
35208        // provide a buffer of .1 seconds to handle rounding/imprecise numbers
35209
35210        seekTo = seekableStart + ( // if the playlist is too short and the seekable range is an exact time (can
35211        // happen in live with a 3 segment playlist), then don't use a time delta
35212        seekableStart === seekable.end(0) ? 0 : SAFE_TIME_DELTA);
35213      }
35214
35215      if (typeof seekTo !== 'undefined') {
35216        this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` + `seekable range ${printableRange(seekable)}. Seeking to ` + `${seekTo}.`);
35217        this.tech_.setCurrentTime(seekTo);
35218        return true;
35219      }
35220
35221      const sourceUpdater = this.playlistController_.sourceUpdater_;
35222      const buffered = this.tech_.buffered();
35223      const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null;
35224      const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null;
35225      const media = this.media(); // verify that at least two segment durations or one part duration have been
35226      // appended before checking for a gap.
35227
35228      const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration : (media.targetDuration - TIME_FUDGE_FACTOR) * 2; // verify that at least two segment durations have been
35229      // appended before checking for a gap.
35230
35231      const bufferedToCheck = [audioBuffered, videoBuffered];
35232
35233      for (let i = 0; i < bufferedToCheck.length; i++) {
35234        // skip null buffered
35235        if (!bufferedToCheck[i]) {
35236          continue;
35237        }
35238
35239        const timeAhead = timeAheadOf(bufferedToCheck[i], currentTime); // if we are less than two video/audio segment durations or one part
35240        // duration behind we haven't appended enough to call this a bad seek.
35241
35242        if (timeAhead < minAppendedDuration) {
35243          return false;
35244        }
35245      }
35246
35247      const nextRange = findNextRange(buffered, currentTime); // we have appended enough content, but we don't have anything buffered
35248      // to seek over the gap
35249
35250      if (nextRange.length === 0) {
35251        return false;
35252      }
35253
35254      seekTo = nextRange.start(0) + SAFE_TIME_DELTA;
35255      this.logger_(`Buffered region starts (${nextRange.start(0)}) ` + ` just beyond seek point (${currentTime}). Seeking to ${seekTo}.`);
35256      this.tech_.setCurrentTime(seekTo);
35257      return true;
35258    }
35259    /**
35260     * Handler for situations when we determine the player is waiting.
35261     *
35262     * @private
35263     */
35264
35265
35266    waiting_() {
35267      if (this.techWaiting_()) {
35268        return;
35269      } // All tech waiting checks failed. Use last resort correction
35270
35271
35272      const currentTime = this.tech_.currentTime();
35273      const buffered = this.tech_.buffered();
35274      const currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered
35275      // region with no indication that anything is amiss (seen in Firefox). Seeking to
35276      // currentTime is usually enough to kickstart the player. This checks that the player
35277      // is currently within a buffered region before attempting a corrective seek.
35278      // Chrome does not appear to continue `timeupdate` events after a `waiting` event
35279      // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also
35280      // make sure there is ~3 seconds of forward buffer before taking any corrective action
35281      // to avoid triggering an `unknownwaiting` event when the network is slow.
35282
35283      if (currentRange.length && currentTime + 3 <= currentRange.end(0)) {
35284        this.resetTimeUpdate_();
35285        this.tech_.setCurrentTime(currentTime);
35286        this.logger_(`Stopped at ${currentTime} while inside a buffered region ` + `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` + 'playback by seeking to the current time.'); // unknown waiting corrections may be useful for monitoring QoS
35287
35288        this.tech_.trigger({
35289          type: 'usage',
35290          name: 'vhs-unknown-waiting'
35291        });
35292        return;
35293      }
35294    }
35295    /**
35296     * Handler for situations when the tech fires a `waiting` event
35297     *
35298     * @return {boolean}
35299     *         True if an action (or none) was needed to correct the waiting. False if no
35300     *         checks passed
35301     * @private
35302     */
35303
35304
35305    techWaiting_() {
35306      const seekable = this.seekable();
35307      const currentTime = this.tech_.currentTime();
35308
35309      if (this.tech_.seeking()) {
35310        // Tech is seeking or already waiting on another action, no action needed
35311        return true;
35312      }
35313
35314      if (this.beforeSeekableWindow_(seekable, currentTime)) {
35315        const livePoint = seekable.end(seekable.length - 1);
35316        this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` + `live point (seekable end) ${livePoint}`);
35317        this.resetTimeUpdate_();
35318        this.tech_.setCurrentTime(livePoint); // live window resyncs may be useful for monitoring QoS
35319
35320        this.tech_.trigger({
35321          type: 'usage',
35322          name: 'vhs-live-resync'
35323        });
35324        return true;
35325      }
35326
35327      const sourceUpdater = this.tech_.vhs.playlistController_.sourceUpdater_;
35328      const buffered = this.tech_.buffered();
35329      const videoUnderflow = this.videoUnderflow_({
35330        audioBuffered: sourceUpdater.audioBuffered(),
35331        videoBuffered: sourceUpdater.videoBuffered(),
35332        currentTime
35333      });
35334
35335      if (videoUnderflow) {
35336        // Even though the video underflowed and was stuck in a gap, the audio overplayed
35337        // the gap, leading currentTime into a buffered range. Seeking to currentTime
35338        // allows the video to catch up to the audio position without losing any audio
35339        // (only suffering ~3 seconds of frozen video and a pause in audio playback).
35340        this.resetTimeUpdate_();
35341        this.tech_.setCurrentTime(currentTime); // video underflow may be useful for monitoring QoS
35342
35343        this.tech_.trigger({
35344          type: 'usage',
35345          name: 'vhs-video-underflow'
35346        });
35347        return true;
35348      }
35349
35350      const nextRange = findNextRange(buffered, currentTime); // check for gap
35351
35352      if (nextRange.length > 0) {
35353        this.logger_(`Stopped at ${currentTime} and seeking to ${nextRange.start(0)}`);
35354        this.resetTimeUpdate_();
35355        this.skipTheGap_(currentTime);
35356        return true;
35357      } // All checks failed. Returning false to indicate failure to correct waiting
35358
35359
35360      return false;
35361    }
35362
35363    afterSeekableWindow_(seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) {
35364      if (!seekable.length) {
35365        // we can't make a solid case if there's no seekable, default to false
35366        return false;
35367      }
35368
35369      let allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA;
35370      const isLive = !playlist.endList;
35371      const isLLHLS = typeof playlist.partTargetDuration === 'number';
35372
35373      if (isLive && (isLLHLS || allowSeeksWithinUnsafeLiveWindow)) {
35374        allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3;
35375      }
35376
35377      if (currentTime > allowedEnd) {
35378        return true;
35379      }
35380
35381      return false;
35382    }
35383
35384    beforeSeekableWindow_(seekable, currentTime) {
35385      if (seekable.length && // can't fall before 0 and 0 seekable start identifies VOD stream
35386      seekable.start(0) > 0 && currentTime < seekable.start(0) - this.liveRangeSafeTimeDelta) {
35387        return true;
35388      }
35389
35390      return false;
35391    }
35392
35393    videoUnderflow_({
35394      videoBuffered,
35395      audioBuffered,
35396      currentTime
35397    }) {
35398      // audio only content will not have video underflow :)
35399      if (!videoBuffered) {
35400        return;
35401      }
35402
35403      let gap; // find a gap in demuxed content.
35404
35405      if (videoBuffered.length && audioBuffered.length) {
35406        // in Chrome audio will continue to play for ~3s when we run out of video
35407        // so we have to check that the video buffer did have some buffer in the
35408        // past.
35409        const lastVideoRange = findRange(videoBuffered, currentTime - 3);
35410        const videoRange = findRange(videoBuffered, currentTime);
35411        const audioRange = findRange(audioBuffered, currentTime);
35412
35413        if (audioRange.length && !videoRange.length && lastVideoRange.length) {
35414          gap = {
35415            start: lastVideoRange.end(0),
35416            end: audioRange.end(0)
35417          };
35418        } // find a gap in muxed content.
35419
35420      } else {
35421        const nextRange = findNextRange(videoBuffered, currentTime); // Even if there is no available next range, there is still a possibility we are
35422        // stuck in a gap due to video underflow.
35423
35424        if (!nextRange.length) {
35425          gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime);
35426        }
35427      }
35428
35429      if (gap) {
35430        this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` + `Seeking to current time ${currentTime}`);
35431        return true;
35432      }
35433
35434      return false;
35435    }
35436    /**
35437     * Timer callback. If playback still has not proceeded, then we seek
35438     * to the start of the next buffered region.
35439     *
35440     * @private
35441     */
35442
35443
35444    skipTheGap_(scheduledCurrentTime) {
35445      const buffered = this.tech_.buffered();
35446      const currentTime = this.tech_.currentTime();
35447      const nextRange = findNextRange(buffered, currentTime);
35448      this.resetTimeUpdate_();
35449
35450      if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) {
35451        return;
35452      }
35453
35454      this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played
35455
35456      this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR);
35457      const metadata = {
35458        gapInfo: {
35459          from: currentTime,
35460          to: nextRange.start(0)
35461        }
35462      };
35463      this.playlistController_.trigger({
35464        type: 'gapjumped',
35465        metadata
35466      });
35467      this.tech_.trigger({
35468        type: 'usage',
35469        name: 'vhs-gap-skip'
35470      });
35471    }
35472
35473    gapFromVideoUnderflow_(buffered, currentTime) {
35474      // At least in Chrome, if there is a gap in the video buffer, the audio will continue
35475      // playing for ~3 seconds after the video gap starts. This is done to account for
35476      // video buffer underflow/underrun (note that this is not done when there is audio
35477      // buffer underflow/underrun -- in that case the video will stop as soon as it
35478      // encounters the gap, as audio stalls are more noticeable/jarring to a user than
35479      // video stalls). The player's time will reflect the playthrough of audio, so the
35480      // time will appear as if we are in a buffered region, even if we are stuck in a
35481      // "gap."
35482      //
35483      // Example:
35484      // video buffer:   0 => 10.1, 10.2 => 20
35485      // audio buffer:   0 => 20
35486      // overall buffer: 0 => 10.1, 10.2 => 20
35487      // current time: 13
35488      //
35489      // Chrome's video froze at 10 seconds, where the video buffer encountered the gap,
35490      // however, the audio continued playing until it reached ~3 seconds past the gap
35491      // (13 seconds), at which point it stops as well. Since current time is past the
35492      // gap, findNextRange will return no ranges.
35493      //
35494      // To check for this issue, we see if there is a gap that starts somewhere within
35495      // a 3 second range (3 seconds +/- 1 second) back from our current time.
35496      const gaps = findGaps(buffered);
35497
35498      for (let i = 0; i < gaps.length; i++) {
35499        const start = gaps.start(i);
35500        const end = gaps.end(i); // gap is starts no more than 4 seconds back
35501
35502        if (currentTime - start < 4 && currentTime - start > 2) {
35503          return {
35504            start,
35505            end
35506          };
35507        }
35508      }
35509
35510      return null;
35511    }
35512
35513  }
35514
35515  const defaultOptions = {
35516    errorInterval: 30,
35517
35518    getSource(next) {
35519      const tech = this.tech({
35520        IWillNotUseThisInPlugins: true
35521      });
35522      const sourceObj = tech.currentSource_ || this.currentSource();
35523      return next(sourceObj);
35524    }
35525
35526  };
35527  /**
35528   * Main entry point for the plugin
35529   *
35530   * @param {Player} player a reference to a videojs Player instance
35531   * @param {Object} [options] an object with plugin options
35532   * @private
35533   */
35534
35535  const initPlugin = function (player, options) {
35536    let lastCalled = 0;
35537    let seekTo = 0;
35538    const localOptions = merge$1(defaultOptions, options);
35539    player.ready(() => {
35540      player.trigger({
35541        type: 'usage',
35542        name: 'vhs-error-reload-initialized'
35543      });
35544    });
35545    /**
35546     * Player modifications to perform that must wait until `loadedmetadata`
35547     * has been triggered
35548     *
35549     * @private
35550     */
35551
35552    const loadedMetadataHandler = function () {
35553      if (seekTo) {
35554        player.currentTime(seekTo);
35555      }
35556    };
35557    /**
35558     * Set the source on the player element, play, and seek if necessary
35559     *
35560     * @param {Object} sourceObj An object specifying the source url and mime-type to play
35561     * @private
35562     */
35563
35564
35565    const setSource = function (sourceObj) {
35566      if (sourceObj === null || sourceObj === undefined) {
35567        return;
35568      }
35569
35570      seekTo = player.duration() !== Infinity && player.currentTime() || 0;
35571      player.one('loadedmetadata', loadedMetadataHandler);
35572      player.src(sourceObj);
35573      player.trigger({
35574        type: 'usage',
35575        name: 'vhs-error-reload'
35576      });
35577      player.play();
35578    };
35579    /**
35580     * Attempt to get a source from either the built-in getSource function
35581     * or a custom function provided via the options
35582     *
35583     * @private
35584     */
35585
35586
35587    const errorHandler = function () {
35588      // Do not attempt to reload the source if a source-reload occurred before
35589      // 'errorInterval' time has elapsed since the last source-reload
35590      if (Date.now() - lastCalled < localOptions.errorInterval * 1000) {
35591        player.trigger({
35592          type: 'usage',
35593          name: 'vhs-error-reload-canceled'
35594        });
35595        return;
35596      }
35597
35598      if (!localOptions.getSource || typeof localOptions.getSource !== 'function') {
35599        videojs__default["default"].log.error('ERROR: reloadSourceOnError - The option getSource must be a function!');
35600        return;
35601      }
35602
35603      lastCalled = Date.now();
35604      return localOptions.getSource.call(player, setSource);
35605    };
35606    /**
35607     * Unbind any event handlers that were bound by the plugin
35608     *
35609     * @private
35610     */
35611
35612
35613    const cleanupEvents = function () {
35614      player.off('loadedmetadata', loadedMetadataHandler);
35615      player.off('error', errorHandler);
35616      player.off('dispose', cleanupEvents);
35617    };
35618    /**
35619     * Cleanup before re-initializing the plugin
35620     *
35621     * @param {Object} [newOptions] an object with plugin options
35622     * @private
35623     */
35624
35625
35626    const reinitPlugin = function (newOptions) {
35627      cleanupEvents();
35628      initPlugin(player, newOptions);
35629    };
35630
35631    player.on('error', errorHandler);
35632    player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before
35633    // initializing the plugin
35634
35635    player.reloadSourceOnError = reinitPlugin;
35636  };
35637  /**
35638   * Reload the source when an error is detected as long as there
35639   * wasn't an error previously within the last 30 seconds
35640   *
35641   * @param {Object} [options] an object with plugin options
35642   */
35643
35644
35645  const reloadSourceOnError = function (options) {
35646    initPlugin(this, options);
35647  };
35648
35649  var version$4 = "3.17.
356492";
35650
35651  var version$3 = "7.1.0";
35652
35653  var version$2 = "1.3.1
vendor: 47,376 bytes, lines 35653-37013
35653";
35654
35655  var version$1 = "7.2.0";
35656
35657  var version = "4.0.2";
35658
35659  const Vhs = {
35660    PlaylistLoader,
35661    Playlist,
35662    utils,
35663    STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector,
35664    INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector,
35665    lastBandwidthSelector,
35666    movingAverageBandwidthSelector,
35667    comparePlaylistBandwidth,
35668    comparePlaylistResolution,
35669    xhr: xhrFactory()
35670  }; // Define getter/setters for config properties
35671
35672  Object.keys(Config).forEach(prop => {
35673    Object.defineProperty(Vhs, prop, {
35674      get() {
35675        videojs__default["default"].log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
35676        return Config[prop];
35677      },
35678
35679      set(value) {
35680        videojs__default["default"].log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`);
35681
35682        if (typeof value !== 'number' || value < 0) {
35683          videojs__default["default"].log.warn(`value of Vhs.${prop} must be greater than or equal to 0`);
35684          return;
35685        }
35686
35687        Config[prop] = value;
35688      }
35689
35690    });
35691  });
35692  const LOCAL_STORAGE_KEY = 'videojs-vhs';
35693  /**
35694   * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs.
35695   *
35696   * @param {QualityLevelList} qualityLevels The QualityLevelList to update.
35697   * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info.
35698   * @function handleVhsMediaChange
35699   */
35700
35701  const handleVhsMediaChange = function (qualityLevels, playlistLoader) {
35702    const newPlaylist = playlistLoader.media();
35703    let selectedIndex = -1;
35704
35705    for (let i = 0; i < qualityLevels.length; i++) {
35706      if (qualityLevels[i].id === newPlaylist.id) {
35707        selectedIndex = i;
35708        break;
35709      }
35710    }
35711
35712    qualityLevels.selectedIndex_ = selectedIndex;
35713    qualityLevels.trigger({
35714      selectedIndex,
35715      type: 'change'
35716    });
35717  };
35718  /**
35719   * Adds quality levels to list once playlist metadata is available
35720   *
35721   * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to.
35722   * @param {Object} vhs Vhs object to listen to for media events.
35723   * @function handleVhsLoadedMetadata
35724   */
35725
35726
35727  const handleVhsLoadedMetadata = function (qualityLevels, vhs) {
35728    vhs.representations().forEach(rep => {
35729      qualityLevels.addQualityLevel(rep);
35730    });
35731    handleVhsMediaChange(qualityLevels, vhs.playlists);
35732  }; // VHS is a source handler, not a tech. Make sure attempts to use it
35733  // as one do not cause exceptions.
35734
35735
35736  Vhs.canPlaySource = function () {
35737    return videojs__default["default"].log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.');
35738  };
35739
35740  const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => {
35741    if (!keySystemOptions) {
35742      return keySystemOptions;
35743    }
35744
35745    let codecs = {};
35746
35747    if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) {
35748      codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS));
35749    }
35750
35751    if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) {
35752      codecs.audio = audioPlaylist.attributes.CODECS;
35753    }
35754
35755    const videoContentType = getMimeForCodec(codecs.video);
35756    const audioContentType = getMimeForCodec(codecs.audio); // upsert the content types based on the selected playlist
35757
35758    const keySystemContentTypes = {};
35759
35760    for (const keySystem in keySystemOptions) {
35761      keySystemContentTypes[keySystem] = {};
35762
35763      if (audioContentType) {
35764        keySystemContentTypes[keySystem].audioContentType = audioContentType;
35765      }
35766
35767      if (videoContentType) {
35768        keySystemContentTypes[keySystem].videoContentType = videoContentType;
35769      } // Default to using the video playlist's PSSH even though they may be different, as
35770      // videojs-contrib-eme will only accept one in the options.
35771      //
35772      // This shouldn't be an issue for most cases as early intialization will handle all
35773      // unique PSSH values, and if they aren't, then encrypted events should have the
35774      // specific information needed for the unique license.
35775
35776
35777      if (mainPlaylist.contentProtection && mainPlaylist.contentProtection[keySystem] && mainPlaylist.contentProtection[keySystem].pssh) {
35778        keySystemContentTypes[keySystem].pssh = mainPlaylist.contentProtection[keySystem].pssh;
35779      } // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url'
35780      // so we need to prevent overwriting the URL entirely
35781
35782
35783      if (typeof keySystemOptions[keySystem] === 'string') {
35784        keySystemContentTypes[keySystem].url = keySystemOptions[keySystem];
35785      }
35786    }
35787
35788    return merge$1(keySystemOptions, keySystemContentTypes);
35789  };
35790  /**
35791   * @typedef {Object} KeySystems
35792   *
35793   * keySystems configuration for https://github.com/videojs/videojs-contrib-eme
35794   * Note: not all options are listed here.
35795   *
35796   * @property {Uint8Array} [pssh]
35797   *           Protection System Specific Header
35798   */
35799
35800  /**
35801   * Goes through all the playlists and collects an array of KeySystems options objects
35802   * containing each playlist's keySystems and their pssh values, if available.
35803   *
35804   * @param {Object[]} playlists
35805   *        The playlists to look through
35806   * @param {string[]} keySystems
35807   *        The keySystems to collect pssh values for
35808   *
35809   * @return {KeySystems[]}
35810   *         An array of KeySystems objects containing available key systems and their
35811   *         pssh values
35812   */
35813
35814
35815  const getAllPsshKeySystemsOptions = (playlists, keySystems) => {
35816    return playlists.reduce((keySystemsArr, playlist) => {
35817      if (!playlist.contentProtection) {
35818        return keySystemsArr;
35819      }
35820
35821      const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => {
35822        const keySystemOptions = playlist.contentProtection[keySystem];
35823
35824        if (keySystemOptions && keySystemOptions.pssh) {
35825          keySystemsObj[keySystem] = {
35826            pssh: keySystemOptions.pssh
35827          };
35828        }
35829
35830        return keySystemsObj;
35831      }, {});
35832
35833      if (Object.keys(keySystemsOptions).length) {
35834        keySystemsArr.push(keySystemsOptions);
35835      }
35836
35837      return keySystemsArr;
35838    }, []);
35839  };
35840  /**
35841   * Returns a promise that waits for the
35842   * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session.
35843   *
35844   * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11
35845   * browsers.
35846   *
35847   * As per the above ticket, this is particularly important for Chrome, where, if
35848   * unencrypted content is appended before encrypted content and the key session has not
35849   * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached
35850   * during playback.
35851   *
35852   * @param {Object} player
35853   *        The player instance
35854   * @param {Object[]} sourceKeySystems
35855   *        The key systems options from the player source
35856   * @param {Object} [audioMedia]
35857   *        The active audio media playlist (optional)
35858   * @param {Object[]} mainPlaylists
35859   *        The playlists found on the main playlist object
35860   *
35861   * @return {Object}
35862   *         Promise that resolves when the key session has been created
35863   */
35864
35865
35866  const waitForKeySessionCreation = ({
35867    player,
35868    sourceKeySystems,
35869    audioMedia,
35870    mainPlaylists
35871  }) => {
35872    if (!player.eme.initializeMediaKeys) {
35873      return Promise.resolve();
35874    } // TODO should all audio PSSH values be initialized for DRM?
35875    //
35876    // All unique video rendition pssh values are initialized for DRM, but here only
35877    // the initial audio playlist license is initialized. In theory, an encrypted
35878    // event should be fired if the user switches to an alternative audio playlist
35879    // where a license is required, but this case hasn't yet been tested. In addition, there
35880    // may be many alternate audio playlists unlikely to be used (e.g., multiple different
35881    // languages).
35882
35883
35884    const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists;
35885    const keySystemsOptionsArr = getAllPsshKeySystemsOptions(playlists, Object.keys(sourceKeySystems));
35886    const initializationFinishedPromises = [];
35887    const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The
35888    // only place where it should not be deduped is for ms-prefixed APIs, but
35889    // the existence of modern EME APIs in addition to
35890    // ms-prefixed APIs on Edge should prevent this from being a concern.
35891    // initializeMediaKeys also won't use the webkit-prefixed APIs.
35892
35893    keySystemsOptionsArr.forEach(keySystemsOptions => {
35894      keySessionCreatedPromises.push(new Promise((resolve, reject) => {
35895        player.tech_.one('keysessioncreated', resolve);
35896      }));
35897      initializationFinishedPromises.push(new Promise((resolve, reject) => {
35898        player.eme.initializeMediaKeys({
35899          keySystems: keySystemsOptions
35900        }, err => {
35901          if (err) {
35902            reject(err);
35903            return;
35904          }
35905
35906          resolve();
35907        });
35908      }));
35909    }); // The reasons Promise.race is chosen over Promise.any:
35910    //
35911    // * Promise.any is only available in Safari 14+.
35912    // * None of these promises are expected to reject. If they do reject, it might be
35913    //   better here for the race to surface the rejection, rather than mask it by using
35914    //   Promise.any.
35915
35916    return Promise.race([// If a session was previously created, these will all finish resolving without
35917    // creating a new session, otherwise it will take until the end of all license
35918    // requests, which is why the key session check is used (to make setup much faster).
35919    Promise.all(initializationFinishedPromises), // Once a single session is created, the browser knows DRM will be used.
35920    Promise.race(keySessionCreatedPromises)]);
35921  };
35922  /**
35923   * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and
35924   * there are keySystems on the source, sets up source options to prepare the source for
35925   * eme.
35926   *
35927   * @param {Object} player
35928   *        The player instance
35929   * @param {Object[]} sourceKeySystems
35930   *        The key systems options from the player source
35931   * @param {Object} media
35932   *        The active media playlist
35933   * @param {Object} [audioMedia]
35934   *        The active audio media playlist (optional)
35935   *
35936   * @return {boolean}
35937   *         Whether or not options were configured and EME is available
35938   */
35939
35940  const setupEmeOptions = ({
35941    player,
35942    sourceKeySystems,
35943    media,
35944    audioMedia
35945  }) => {
35946    const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia);
35947
35948    if (!sourceOptions) {
35949      return false;
35950    }
35951
35952    player.currentSource().keySystems = sourceOptions; // eme handles the rest of the setup, so if it is missing
35953    // do nothing.
35954
35955    if (sourceOptions && !player.eme) {
35956      videojs__default["default"].log.warn('DRM encrypted source cannot be decrypted without a DRM plugin');
35957      return false;
35958    }
35959
35960    return true;
35961  };
35962
35963  const getVhsLocalStorage = () => {
35964    if (!window.localStorage) {
35965      return null;
35966    }
35967
35968    const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY);
35969
35970    if (!storedObject) {
35971      return null;
35972    }
35973
35974    try {
35975      return JSON.parse(storedObject);
35976    } catch (e) {
35977      // someone may have tampered with the value
35978      return null;
35979    }
35980  };
35981
35982  const updateVhsLocalStorage = options => {
35983    if (!window.localStorage) {
35984      return false;
35985    }
35986
35987    let objectToStore = getVhsLocalStorage();
35988    objectToStore = objectToStore ? merge$1(objectToStore, options) : options;
35989
35990    try {
35991      window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore));
35992    } catch (e) {
35993      // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where
35994      // storage is set to 0).
35995      // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions
35996      // No need to perform any operation.
35997      return false;
35998    }
35999
36000    return objectToStore;
36001  };
36002  /**
36003   * Parses VHS-supported media types from data URIs. See
36004   * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
36005   * for information on data URIs.
36006   *
36007   * @param {string} dataUri
36008   *        The data URI
36009   *
36010   * @return {string|Object}
36011   *         The parsed object/string, or the original string if no supported media type
36012   *         was found
36013   */
36014
36015
36016  const expandDataUri = dataUri => {
36017    if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) {
36018      return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1));
36019    } // no known case for this data URI, return the string as-is
36020
36021
36022    return dataUri;
36023  };
36024  /**
36025   * Adds a request hook to an xhr object
36026   *
36027   * @param {Object} xhr object to add the onRequest hook to
36028   * @param {function} callback hook function for an xhr request
36029   */
36030
36031
36032  const addOnRequestHook = (xhr, callback) => {
36033    if (!xhr._requestCallbackSet) {
36034      xhr._requestCallbackSet = new Set();
36035    }
36036
36037    xhr._requestCallbackSet.add(callback);
36038  };
36039  /**
36040   * Adds a response hook to an xhr object
36041   *
36042   * @param {Object} xhr object to add the onResponse hook to
36043   * @param {function} callback hook function for an xhr response
36044   */
36045
36046
36047  const addOnResponseHook = (xhr, callback) => {
36048    if (!xhr._responseCallbackSet) {
36049      xhr._responseCallbackSet = new Set();
36050    }
36051
36052    xhr._responseCallbackSet.add(callback);
36053  };
36054  /**
36055   * Removes a request hook on an xhr object, deletes the onRequest set if empty.
36056   *
36057   * @param {Object} xhr object to remove the onRequest hook from
36058   * @param {function} callback hook function to remove
36059   */
36060
36061
36062  const removeOnRequestHook = (xhr, callback) => {
36063    if (!xhr._requestCallbackSet) {
36064      return;
36065    }
36066
36067    xhr._requestCallbackSet.delete(callback);
36068
36069    if (!xhr._requestCallbackSet.size) {
36070      delete xhr._requestCallbackSet;
36071    }
36072  };
36073  /**
36074   * Removes a response hook on an xhr object, deletes the onResponse set if empty.
36075   *
36076   * @param {Object} xhr object to remove the onResponse hook from
36077   * @param {function} callback hook function to remove
36078   */
36079
36080
36081  const removeOnResponseHook = (xhr, callback) => {
36082    if (!xhr._responseCallbackSet) {
36083      return;
36084    }
36085
36086    xhr._responseCallbackSet.delete(callback);
36087
36088    if (!xhr._responseCallbackSet.size) {
36089      delete xhr._responseCallbackSet;
36090    }
36091  };
36092  /**
36093   * Whether the browser has built-in HLS support.
36094   */
36095
36096
36097  Vhs.supportsNativeHls = function () {
36098    if (!document || !document.createElement) {
36099      return false;
36100    }
36101
36102    const video = document.createElement('video'); // native HLS is definitely not supported if HTML5 video isn't
36103
36104    if (!videojs__default["default"].getTech('Html5').isSupported()) {
36105      return false;
36106    } // HLS manifests can go by many mime-types
36107
36108
36109    const canPlay = [// Apple santioned
36110    'application/vnd.apple.mpegurl', // Apple sanctioned for backwards compatibility
36111    'audio/mpegurl', // Very common
36112    'audio/x-mpegurl', // Very common
36113    'application/x-mpegurl', // Included for completeness
36114    'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl'];
36115    return canPlay.some(function (canItPlay) {
36116      return /maybe|probably/i.test(video.canPlayType(canItPlay));
36117    });
36118  }();
36119
36120  Vhs.supportsNativeDash = function () {
36121    if (!document || !document.createElement || !videojs__default["default"].getTech('Html5').isSupported()) {
36122      return false;
36123    }
36124
36125    return /maybe|probably/i.test(document.createElement('video').canPlayType('application/dash+xml'));
36126  }();
36127
36128  Vhs.supportsTypeNatively = type => {
36129    if (type === 'hls') {
36130      return Vhs.supportsNativeHls;
36131    }
36132
36133    if (type === 'dash') {
36134      return Vhs.supportsNativeDash;
36135    }
36136
36137    return false;
36138  };
36139  /**
36140   * VHS is a source handler, not a tech. Make sure attempts to use it
36141   * as one do not cause exceptions.
36142   */
36143
36144
36145  Vhs.isSupported = function () {
36146    return videojs__default["default"].log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.');
36147  };
36148  /**
36149   * A global function for setting an onRequest hook
36150   *
36151   * @param {function} callback for request modifiction
36152   */
36153
36154
36155  Vhs.xhr.onRequest = function (callback) {
36156    addOnRequestHook(Vhs.xhr, callback);
36157  };
36158  /**
36159   * A global function for setting an onResponse hook
36160   *
36161   * @param {callback} callback for response data retrieval
36162   */
36163
36164
36165  Vhs.xhr.onResponse = function (callback) {
36166    addOnResponseHook(Vhs.xhr, callback);
36167  };
36168  /**
36169   * Deletes a global onRequest callback if it exists
36170   *
36171   * @param {function} callback to delete from the global set
36172   */
36173
36174
36175  Vhs.xhr.offRequest = function (callback) {
36176    removeOnRequestHook(Vhs.xhr, callback);
36177  };
36178  /**
36179   * Deletes a global onResponse callback if it exists
36180   *
36181   * @param {function} callback to delete from the global set
36182   */
36183
36184
36185  Vhs.xhr.offResponse = function (callback) {
36186    removeOnResponseHook(Vhs.xhr, callback);
36187  };
36188
36189  const Component = videojs__default["default"].getComponent('Component');
36190  /**
36191   * The Vhs Handler object, where we orchestrate all of the parts
36192   * of VHS to interact with video.js
36193   *
36194   * @class VhsHandler
36195   * @extends videojs.Component
36196   * @param {Object} source the soruce object
36197   * @param {Tech} tech the parent tech object
36198   * @param {Object} options optional and required options
36199   */
36200
36201  class VhsHandler extends Component {
36202    constructor(source, tech, options) {
36203      super(tech, options.vhs); // if a tech level `initialBandwidth` option was passed
36204      // use that over the VHS level `bandwidth` option
36205
36206      if (typeof options.initialBandwidth === 'number') {
36207        this.options_.bandwidth = options.initialBandwidth;
36208      }
36209
36210      this.logger_ = logger('VhsHandler'); // we need access to the player in some cases,
36211      // so, get it from Video.js via the `playerId`
36212
36213      if (tech.options_ && tech.options_.playerId) {
36214        const _player = videojs__default["default"].getPlayer(tech.options_.playerId);
36215
36216        this.player_ = _player;
36217      }
36218
36219      this.tech_ = tech;
36220      this.source_ = source;
36221      this.stats = {};
36222      this.ignoreNextSeekingEvent_ = false;
36223      this.setOptions_();
36224
36225      if (this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) {
36226        tech.overrideNativeAudioTracks(true);
36227        tech.overrideNativeVideoTracks(true);
36228      } else if (this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) {
36229        // overriding native VHS only works if audio tracks have been emulated
36230        // error early if we're misconfigured
36231        throw new Error('Overriding native VHS requires emulated tracks. ' + 'See https://git.io/vMpjB');
36232      } // listen for fullscreenchange events for this player so that we
36233      // can adjust our quality selection quickly
36234
36235
36236      this.on(document, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], event => {
36237        const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement;
36238
36239        if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) {
36240          this.playlistController_.fastQualityChange_();
36241        } else {
36242          // When leaving fullscreen, since the in page pixel dimensions should be smaller
36243          // than full screen, see if there should be a rendition switch down to preserve
36244          // bandwidth.
36245          this.playlistController_.checkABR_();
36246        }
36247      });
36248      this.on(this.tech_, 'seeking', function () {
36249        if (this.ignoreNextSeekingEvent_) {
36250          this.ignoreNextSeekingEvent_ = false;
36251          return;
36252        }
36253
36254        this.setCurrentTime(this.tech_.currentTime());
36255      });
36256      this.on(this.tech_, 'error', function () {
36257        // verify that the error was real and we are loaded
36258        // enough to have pc loaded.
36259        if (this.tech_.error() && this.playlistController_) {
36260          this.playlistController_.pauseLoading();
36261        }
36262      });
36263      this.on(this.tech_, 'play', this.play);
36264    }
36265    /**
36266     * Set VHS options based on options from configuration, as well as partial
36267     * options to be passed at a later time.
36268     *
36269     * @param {Object} options A partial chunk of config options
36270     */
36271
36272
36273    setOptions_(options = {}) {
36274      this.options_ = merge$1(this.options_, options); // defaults
36275
36276      this.options_.withCredentials = this.options_.withCredentials || false;
36277      this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true;
36278      this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false;
36279      this.options_.usePlayerObjectFit = this.options_.usePlayerObjectFit || false;
36280      this.options_.useBandwidthFromLocalStorage = typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false;
36281      this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false;
36282      this.options_.useNetworkInformationApi = typeof this.options_.useNetworkInformationApi !== 'undefined' ? this.options_.useNetworkInformationApi : true;
36283      this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false;
36284      this.options_.customTagParsers = this.options_.customTagParsers || [];
36285      this.options_.customTagMappers = this.options_.customTagMappers || [];
36286      this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;
36287      this.options_.llhls = this.options_.llhls === false ? false : true;
36288      this.options_.bufferBasedABR = this.options_.bufferBasedABR || false;
36289
36290      if (typeof this.options_.playlistExclusionDuration !== 'number') {
36291        this.options_.playlistExclusionDuration = 60;
36292      }
36293
36294      if (typeof this.options_.bandwidth !== 'number') {
36295        if (this.options_.useBandwidthFromLocalStorage) {
36296          const storedObject = getVhsLocalStorage();
36297
36298          if (storedObject && storedObject.bandwidth) {
36299            this.options_.bandwidth = storedObject.bandwidth;
36300            this.tech_.trigger({
36301              type: 'usage',
36302              name: 'vhs-bandwidth-from-local-storage'
36303            });
36304          }
36305
36306          if (storedObject && storedObject.throughput) {
36307            this.options_.throughput = storedObject.throughput;
36308            this.tech_.trigger({
36309              type: 'usage',
36310              name: 'vhs-throughput-from-local-storage'
36311            });
36312          }
36313        }
36314      } // if bandwidth was not set by options or pulled from local storage, start playlist
36315      // selection at a reasonable bandwidth
36316
36317
36318      if (typeof this.options_.bandwidth !== 'number') {
36319        this.options_.bandwidth = Config.INITIAL_BANDWIDTH;
36320      } // If the bandwidth number is unchanged from the initial setting
36321      // then this takes precedence over the enableLowInitialPlaylist option
36322
36323
36324      this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src
36325
36326      ['withCredentials', 'useDevicePixelRatio', 'usePlayerObjectFit', 'customPixelRatio', 'limitRenditionByPlayerDimensions', 'bandwidth', 'customTagParsers', 'customTagMappers', 'cacheEncryptionKeys', 'playlistSelector', 'initialPlaylistSelector', 'bufferBasedABR', 'liveRangeSafeTimeDelta', 'llhls', 'useForcedSubtitles', 'useNetworkInformationApi', 'useDtsForTimestampOffset', 'exactManifestTimings', 'leastPixelDiffSelector'].forEach(option => {
36327        if (typeof this.source_[option] !== 'undefined') {
36328          this.options_[option] = this.source_[option];
36329        }
36330      });
36331      this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions;
36332      this.useDevicePixelRatio = this.options_.useDevicePixelRatio;
36333      this.usePlayerObjectFit = this.options_.usePlayerObjectFit;
36334      const customPixelRatio = this.options_.customPixelRatio; // Ensure the custom pixel ratio is a number greater than or equal to 0
36335
36336      if (typeof customPixelRatio === 'number' && customPixelRatio >= 0) {
36337        this.customPixelRatio = customPixelRatio;
36338      }
36339    } // alias for public method to set options
36340
36341
36342    setOptions(options = {}) {
36343      this.setOptions_(options);
36344    }
36345    /**
36346     * called when player.src gets called, handle a new source
36347     *
36348     * @param {Object} src the source object to handle
36349     */
36350
36351
36352    src(src, type) {
36353      // do nothing if the src is falsey
36354      if (!src) {
36355        return;
36356      }
36357
36358      this.setOptions_(); // add main playlist controller options
36359
36360      this.options_.src = expandDataUri(this.source_.src);
36361      this.options_.tech = this.tech_;
36362      this.options_.externVhs = Vhs;
36363      this.options_.sourceType = simpleTypeFromSourceType(type); // Whenever we seek internally, we should update the tech
36364
36365      this.options_.seekTo = time => {
36366        this.tech_.setCurrentTime(time);
36367      }; // pass player to allow for player level eventing on construction.
36368
36369
36370      this.options_.player_ = this.player_;
36371      this.playlistController_ = new PlaylistController(this.options_);
36372      const playbackWatcherOptions = merge$1({
36373        liveRangeSafeTimeDelta: SAFE_TIME_DELTA
36374      }, this.options_, {
36375        seekable: () => this.seekable(),
36376        media: () => this.playlistController_.media(),
36377        playlistController: this.playlistController_
36378      });
36379      this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions);
36380      this.attachStreamingEventListeners_();
36381      this.playlistController_.on('error', () => {
36382        const player = videojs__default["default"].players[this.tech_.options_.playerId];
36383        let error = this.playlistController_.error;
36384
36385        if (typeof error === 'object' && !error.code) {
36386          error.code = 3;
36387        } else if (typeof error === 'string') {
36388          error = {
36389            message: error,
36390            code: 3
36391          };
36392        }
36393
36394        player.error(error);
36395      });
36396      const defaultSelector = this.options_.bufferBasedABR ? Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR; // `this` in selectPlaylist should be the VhsHandler for backwards
36397      // compatibility with < v2
36398
36399      this.playlistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : defaultSelector.bind(this);
36400      this.playlistController_.selectInitialPlaylist = Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2
36401
36402      this.playlists = this.playlistController_.mainPlaylistLoader_;
36403      this.mediaSource = this.playlistController_.mediaSource; // Proxy assignment of some properties to the main playlist
36404      // controller. Using a custom property for backwards compatibility
36405      // with < v2
36406
36407      Object.defineProperties(this, {
36408        selectPlaylist: {
36409          get() {
36410            return this.playlistController_.selectPlaylist;
36411          },
36412
36413          set(selectPlaylist) {
36414            this.playlistController_.selectPlaylist = selectPlaylist.bind(this);
36415          }
36416
36417        },
36418        throughput: {
36419          get() {
36420            return this.playlistController_.mainSegmentLoader_.throughput.rate;
36421          },
36422
36423          set(throughput) {
36424            this.playlistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value
36425            // for the cumulative average
36426
36427            this.playlistController_.mainSegmentLoader_.throughput.count = 1;
36428          }
36429
36430        },
36431        bandwidth: {
36432          get() {
36433            let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth;
36434            const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
36435            const tenMbpsAsBitsPerSecond = 10e6;
36436
36437            if (this.options_.useNetworkInformationApi && networkInformation) {
36438              // downlink returns Mbps
36439              // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink
36440              const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player
36441              // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that
36442              // high quality streams are not filtered out.
36443
36444              if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) {
36445                playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec);
36446              } else {
36447                playerBandwidthEst = networkInfoBandwidthEstBitsPerSec;
36448              }
36449            }
36450
36451            return playerBandwidthEst;
36452          },
36453
36454          set(bandwidth) {
36455            this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter
36456            // `count` is set to zero that current value of `rate` isn't included
36457            // in the cumulative average
36458
36459            this.playlistController_.mainSegmentLoader_.throughput = {
36460              rate: 0,
36461              count: 0
36462            };
36463          }
36464
36465        },
36466
36467        /**
36468         * `systemBandwidth` is a combination of two serial processes bit-rates. The first
36469         * is the network bitrate provided by `bandwidth` and the second is the bitrate of
36470         * the entire process after that - decryption, transmuxing, and appending - provided
36471         * by `throughput`.
36472         *
36473         * Since the two process are serial, the overall system bandwidth is given by:
36474         *   sysBandwidth = 1 / (1 / bandwidth + 1 / throughput)
36475         */
36476        systemBandwidth: {
36477          get() {
36478            const invBandwidth = 1 / (this.bandwidth || 1);
36479            let invThroughput;
36480
36481            if (this.throughput > 0) {
36482              invThroughput = 1 / this.throughput;
36483            } else {
36484              invThroughput = 0;
36485            }
36486
36487            const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput));
36488            return systemBitrate;
36489          },
36490
36491          set() {
36492            videojs__default["default"].log.error('The "systemBandwidth" property is read-only');
36493          }
36494
36495        }
36496      });
36497
36498      if (this.options_.bandwidth) {
36499        this.bandwidth = this.options_.bandwidth;
36500      }
36501
36502      if (this.options_.throughput) {
36503        this.throughput = this.options_.throughput;
36504      }
36505
36506      Object.defineProperties(this.stats, {
36507        bandwidth: {
36508          get: () => this.bandwidth || 0,
36509          enumerable: true
36510        },
36511        mediaRequests: {
36512          get: () => this.playlistController_.mediaRequests_() || 0,
36513          enumerable: true
36514        },
36515        mediaRequestsAborted: {
36516          get: () => this.playlistController_.mediaRequestsAborted_() || 0,
36517          enumerable: true
36518        },
36519        mediaRequestsTimedout: {
36520          get: () => this.playlistController_.mediaRequestsTimedout_() || 0,
36521          enumerable: true
36522        },
36523        mediaRequestsErrored: {
36524          get: () => this.playlistController_.mediaRequestsErrored_() || 0,
36525          enumerable: true
36526        },
36527        mediaTransferDuration: {
36528          get: () => this.playlistController_.mediaTransferDuration_() || 0,
36529          enumerable: true
36530        },
36531        mediaBytesTransferred: {
36532          get: () => this.playlistController_.mediaBytesTransferred_() || 0,
36533          enumerable: true
36534        },
36535        mediaSecondsLoaded: {
36536          get: () => this.playlistController_.mediaSecondsLoaded_() || 0,
36537          enumerable: true
36538        },
36539        mediaAppends: {
36540          get: () => this.playlistController_.mediaAppends_() || 0,
36541          enumerable: true
36542        },
36543        mainAppendsToLoadedData: {
36544          get: () => this.playlistController_.mainAppendsToLoadedData_() || 0,
36545          enumerable: true
36546        },
36547        audioAppendsToLoadedData: {
36548          get: () => this.playlistController_.audioAppendsToLoadedData_() || 0,
36549          enumerable: true
36550        },
36551        appendsToLoadedData: {
36552          get: () => this.playlistController_.appendsToLoadedData_() || 0,
36553          enumerable: true
36554        },
36555        timeToLoadedData: {
36556          get: () => this.playlistController_.timeToLoadedData_() || 0,
36557          enumerable: true
36558        },
36559        buffered: {
36560          get: () => timeRangesToArray(this.tech_.buffered()),
36561          enumerable: true
36562        },
36563        currentTime: {
36564          get: () => this.tech_.currentTime(),
36565          enumerable: true
36566        },
36567        currentSource: {
36568          get: () => this.tech_.currentSource_,
36569          enumerable: true
36570        },
36571        currentTech: {
36572          get: () => this.tech_.name_,
36573          enumerable: true
36574        },
36575        duration: {
36576          get: () => this.tech_.duration(),
36577          enumerable: true
36578        },
36579        main: {
36580          get: () => this.playlists.main,
36581          enumerable: true
36582        },
36583        playerDimensions: {
36584          get: () => this.tech_.currentDimensions(),
36585          enumerable: true
36586        },
36587        seekable: {
36588          get: () => timeRangesToArray(this.tech_.seekable()),
36589          enumerable: true
36590        },
36591        timestamp: {
36592          get: () => Date.now(),
36593          enumerable: true
36594        },
36595        videoPlaybackQuality: {
36596          get: () => this.tech_.getVideoPlaybackQuality(),
36597          enumerable: true
36598        }
36599      });
36600      this.tech_.one('canplay', this.playlistController_.setupFirstPlay.bind(this.playlistController_));
36601      this.tech_.on('bandwidthupdate', () => {
36602        if (this.options_.useBandwidthFromLocalStorage) {
36603          updateVhsLocalStorage({
36604            bandwidth: this.bandwidth,
36605            throughput: Math.round(this.throughput)
36606          });
36607        }
36608      });
36609      this.playlistController_.on('selectedinitialmedia', () => {
36610        // Add the manual rendition mix-in to VhsHandler
36611        renditionSelectionMixin(this);
36612      });
36613      this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => {
36614        this.setupEme_();
36615      }); // the bandwidth of the primary segment loader is our best
36616      // estimate of overall bandwidth
36617
36618      this.on(this.playlistController_, 'progress', function () {
36619        this.tech_.trigger('progress');
36620      }); // In the live case, we need to ignore the very first `seeking` event since
36621      // that will be the result of the seek-to-live behavior
36622
36623      this.on(this.playlistController_, 'firstplay', function () {
36624        this.ignoreNextSeekingEvent_ = true;
36625      });
36626      this.setupQualityLevels_(); // do nothing if the tech has been disposed already
36627      // this can occur if someone sets the src in player.ready(), for instance
36628
36629      if (!this.tech_.el()) {
36630        return;
36631      }
36632
36633      this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource); // If we are playing HLS with MSE in Safari, add source elements for both the blob and manifest URLs.
36634      // The latter will enable Airplay playback on receiver devices.
36635
36636      if ((videojs__default["default"].browser.IS_ANY_SAFARI || videojs__default["default"].browser.IS_IOS) && this.options_.overrideNative && this.options_.sourceType === 'hls' && typeof this.tech_.addSourceElement === 'function') {
36637        this.tech_.addSourceElement(this.mediaSourceUrl_);
36638        this.tech_.addSourceElement(this.source_.src);
36639      } else {
36640        this.tech_.src(this.mediaSourceUrl_);
36641      }
36642    }
36643
36644    createKeySessions_() {
36645      const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
36646      this.logger_('waiting for EME key session creation');
36647      waitForKeySessionCreation({
36648        player: this.player_,
36649        sourceKeySystems: this.source_.keySystems,
36650        audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(),
36651        mainPlaylists: this.playlists.main.playlists
36652      }).then(() => {
36653        this.logger_('created EME key session');
36654        this.playlistController_.sourceUpdater_.initializedEme();
36655      }).catch(err => {
36656        this.logger_('error while creating EME key session', err);
36657        this.player_.error({
36658          message: 'Failed to initialize media keys for EME',
36659          code: 3
36660        });
36661      });
36662    }
36663
36664    handleWaitingForKey_() {
36665      // If waitingforkey is fired, it's possible that the data that's necessary to retrieve
36666      // the key is in the manifest. While this should've happened on initial source load, it
36667      // may happen again in live streams where the keys change, and the manifest info
36668      // reflects the update.
36669      //
36670      // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's
36671      // already requested keys for, we don't have to worry about this generating extraneous
36672      // requests.
36673      this.logger_('waitingforkey fired, attempting to create any new key sessions');
36674      this.createKeySessions_();
36675    }
36676    /**
36677     * If necessary and EME is available, sets up EME options and waits for key session
36678     * creation.
36679     *
36680     * This function also updates the source updater so taht it can be used, as for some
36681     * browsers, EME must be configured before content is appended (if appending unencrypted
36682     * content before encrypted content).
36683     */
36684
36685
36686    setupEme_() {
36687      const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader;
36688      const didSetupEmeOptions = setupEmeOptions({
36689        player: this.player_,
36690        sourceKeySystems: this.source_.keySystems,
36691        media: this.playlists.media(),
36692        audioMedia: audioPlaylistLoader && audioPlaylistLoader.media()
36693      });
36694      this.player_.tech_.on('keystatuschange', e => {
36695        this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status);
36696      });
36697      this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this);
36698      this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_);
36699
36700      if (!didSetupEmeOptions) {
36701        // If EME options were not set up, we've done all we could to initialize EME.
36702        this.playlistController_.sourceUpdater_.initializedEme();
36703        return;
36704      }
36705
36706      this.createKeySessions_();
36707    }
36708    /**
36709     * Initializes the quality levels and sets listeners to update them.
36710     *
36711     * @method setupQualityLevels_
36712     * @private
36713     */
36714
36715
36716    setupQualityLevels_() {
36717      const player = videojs__default["default"].players[this.tech_.options_.playerId]; // if there isn't a player or there isn't a qualityLevels plugin
36718      // or qualityLevels_ listeners have already been setup, do nothing.
36719
36720      if (!player || !player.qualityLevels || this.qualityLevels_) {
36721        return;
36722      }
36723
36724      this.qualityLevels_ = player.qualityLevels();
36725      this.playlistController_.on('selectedinitialmedia', () => {
36726        handleVhsLoadedMetadata(this.qualityLevels_, this);
36727      });
36728      this.playlists.on('mediachange', () => {
36729        handleVhsMediaChange(this.qualityLevels_, this.playlists);
36730      });
36731    }
36732    /**
36733     * return the version
36734     */
36735
36736
36737    static version() {
36738      return {
36739        '@videojs/http-streaming': version$4,
36740        'mux.js': version$3,
36741        'mpd-parser': version$2,
36742        'm3u8-parser': version$1,
36743        'aes-decrypter': version
36744      };
36745    }
36746    /**
36747     * return the version
36748     */
36749
36750
36751    version() {
36752      return this.constructor.version();
36753    }
36754
36755    canChangeType() {
36756      return SourceUpdater.canChangeType();
36757    }
36758    /**
36759     * Begin playing the video.
36760     */
36761
36762
36763    play() {
36764      this.playlistController_.play();
36765    }
36766    /**
36767     * a wrapper around the function in PlaylistController
36768     */
36769
36770
36771    setCurrentTime(currentTime) {
36772      this.playlistController_.setCurrentTime(currentTime);
36773    }
36774    /**
36775     * a wrapper around the function in PlaylistController
36776     */
36777
36778
36779    duration() {
36780      return this.playlistController_.duration();
36781    }
36782    /**
36783     * a wrapper around the function in PlaylistController
36784     */
36785
36786
36787    seekable() {
36788      return this.playlistController_.seekable();
36789    }
36790    /**
36791     * Abort all outstanding work and cleanup.
36792     */
36793
36794
36795    dispose() {
36796      if (this.playbackWatcher_) {
36797        this.playbackWatcher_.dispose();
36798      }
36799
36800      if (this.playlistController_) {
36801        this.playlistController_.dispose();
36802      }
36803
36804      if (this.qualityLevels_) {
36805        this.qualityLevels_.dispose();
36806      }
36807
36808      if (this.tech_ && this.tech_.vhs) {
36809        delete this.tech_.vhs;
36810      }
36811
36812      if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) {
36813        window.URL.revokeObjectURL(this.mediaSourceUrl_);
36814        this.mediaSourceUrl_ = null;
36815      }
36816
36817      if (this.tech_) {
36818        this.tech_.off('waitingforkey', this.handleWaitingForKey_);
36819      }
36820
36821      super.dispose();
36822    }
36823
36824    convertToProgramTime(time, callback) {
36825      return getProgramTime({
36826        playlist: this.playlistController_.media(),
36827        time,
36828        callback
36829      });
36830    } // the player must be playing before calling this
36831
36832
36833    seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) {
36834      return seekToProgramTime({
36835        programTime,
36836        playlist: this.playlistController_.media(),
36837        retryCount,
36838        pauseAfterSeek,
36839        seekTo: this.options_.seekTo,
36840        tech: this.options_.tech,
36841        callback
36842      });
36843    }
36844    /**
36845     * Adds the onRequest, onResponse, offRequest and offResponse functions
36846     * to the VhsHandler xhr Object.
36847     */
36848
36849
36850    setupXhrHooks_() {
36851      /**
36852       * A player function for setting an onRequest hook
36853       *
36854       * @param {function} callback for request modifiction
36855       */
36856      this.xhr.onRequest = callback => {
36857        addOnRequestHook(this.xhr, callback);
36858      };
36859      /**
36860       * A player function for setting an onResponse hook
36861       *
36862       * @param {callback} callback for response data retrieval
36863       */
36864
36865
36866      this.xhr.onResponse = callback => {
36867        addOnResponseHook(this.xhr, callback);
36868      };
36869      /**
36870       * Deletes a player onRequest callback if it exists
36871       *
36872       * @param {function} callback to delete from the player set
36873       */
36874
36875
36876      this.xhr.offRequest = callback => {
36877        removeOnRequestHook(this.xhr, callback);
36878      };
36879      /**
36880       * Deletes a player onResponse callback if it exists
36881       *
36882       * @param {function} callback to delete from the player set
36883       */
36884
36885
36886      this.xhr.offResponse = callback => {
36887        removeOnResponseHook(this.xhr, callback);
36888      }; // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks.
36889      // This allows hooks to be set before the source is set to vhs when handleSource is called.
36890
36891
36892      this.player_.trigger('xhr-hooks-ready');
36893    }
36894
36895    attachStreamingEventListeners_() {
36896      const playlistControllerEvents = ['seekablerangeschanged', 'bufferedrangeschanged', 'contentsteeringloadstart', 'contentsteeringloadcomplete', 'contentsteeringparsed'];
36897      const playbackWatcher = ['gapjumped', 'playedrangeschanged']; // re-emit streaming events and payloads on the player.
36898
36899      playlistControllerEvents.forEach(eventName => {
36900        this.playlistController_.on(eventName, metadata => {
36901          this.player_.trigger(_extends({}, metadata));
36902        });
36903      });
36904      playbackWatcher.forEach(eventName => {
36905        this.playbackWatcher_.on(eventName, metadata => {
36906          this.player_.trigger(_extends({}, metadata));
36907        });
36908      });
36909    }
36910
36911  }
36912  /**
36913   * The Source Handler object, which informs video.js what additional
36914   * MIME types are supported and sets up playback. It is registered
36915   * automatically to the appropriate tech based on the capabilities of
36916   * the browser it is running in. It is not necessary to use or modify
36917   * this object in normal usage.
36918   */
36919
36920
36921  const VhsSourceHandler = {
36922    name: 'videojs-http-streaming',
36923    VERSION: version$4,
36924
36925    canHandleSource(srcObj, options = {}) {
36926      const localOptions = merge$1(videojs__default["default"].options, options); // If not opting to experimentalUseMMS, and playback is only supported with MediaSource, cannot handle source
36927
36928      if (!localOptions.vhs.experimentalUseMMS && !browserSupportsCodec('avc1.4d400d,mp4a.40.2', false)) {
36929        return false;
36930      }
36931
36932      return VhsSourceHandler.canPlayType(srcObj.type, localOptions);
36933    },
36934
36935    handleSource(source, tech, options = {}) {
36936      const localOptions = merge$1(videojs__default["default"].options, options);
36937      tech.vhs = new VhsHandler(source, tech, localOptions);
36938      tech.vhs.xhr = xhrFactory();
36939      tech.vhs.setupXhrHooks_();
36940      tech.vhs.src(source.src, source.type);
36941      return tech.vhs;
36942    },
36943
36944    canPlayType(type, options) {
36945      const simpleType = simpleTypeFromSourceType(type);
36946
36947      if (!simpleType) {
36948        return '';
36949      }
36950
36951      const overrideNative = VhsSourceHandler.getOverrideNative(options);
36952      const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType);
36953      const canUseMsePlayback = !supportsTypeNatively || overrideNative;
36954      return canUseMsePlayback ? 'maybe' : '';
36955    },
36956
36957    getOverrideNative(options = {}) {
36958      const {
36959        vhs = {}
36960      } = options;
36961      const defaultOverrideNative = !(videojs__default["default"].browser.IS_ANY_SAFARI || videojs__default["default"].browser.IS_IOS);
36962      const {
36963        overrideNative = defaultOverrideNative
36964      } = vhs;
36965      return overrideNative;
36966    }
36967
36968  };
36969  /**
36970   * Check to see if either the native MediaSource or ManagedMediaSource
36971   * objectx exist and support an MP4 container with both H.264 video
36972   * and AAC-LC audio.
36973   *
36974   * @return {boolean} if  native media sources are supported
36975   */
36976
36977  const supportsNativeMediaSources = () => {
36978    return browserSupportsCodec('avc1.4d400d,mp4a.40.2', true);
36979  }; // register source handlers with the appropriate techs
36980
36981
36982  if (supportsNativeMediaSources()) {
36983    videojs__default["default"].getTech('Html5').registerSourceHandler(VhsSourceHandler, 0);
36984  }
36985
36986  videojs__default["default"].VhsHandler = VhsHandler;
36987  videojs__default["default"].VhsSourceHandler = VhsSourceHandler;
36988  videojs__default["default"].Vhs = Vhs;
36989
36990  if (!videojs__default["default"].use) {
36991    videojs__default["default"].registerComponent('Vhs', Vhs);
36992  }
36993
36994  videojs__default["default"].options.vhs = videojs__default["default"].options.vhs || {};
36995
36996  if (!videojs__default["default"].getPlugin || !videojs__default["default"].getPlugin('reloadSourceOnError')) {
36997    videojs__default["default"].registerPlugin('reloadSourceOnError', reloadSourceOnError);
36998  }
36999
37000  exports.LOCAL_STORAGE_KEY = LOCAL_STORAGE_KEY;
37001  exports.Vhs = Vhs;
37002  exports.VhsHandler = VhsHandler;
37003  exports.VhsSourceHandler = VhsSourceHandler;
37004  exports.emeKeySystems = emeKeySystems;
37005  exports.expandDataUri = expandDataUri;
37006  exports.getAllPsshKeySystemsOptions = getAllPsshKeySystemsOptions;
37007  exports.setupEmeOptions = setupEmeOptions;
37008  exports.simpleTypeFromSourceType = simpleTypeFromSourceType;
37009  exports.waitForKeySessionCreation = waitForKeySessionCreation;
37010
37011  Object.defineProperty(exports, '__esModule', { value: true });
37012
37013}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.