PageSourceSearch

https://cialamonto.com/wp-content/themes/archee/assets/vendors/js/plyr.js?ver=1.1.0

js cialamonto.com collected 2026-10-08 18:24:50 UTC 299,221 bytes, 8,758 lines download raw bytes

1typeof navigator === "object" && (function (global, factory) {
2  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3  typeof define === 'function' && define.amd ? define('Plyr', factory) :
4  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());
5})(this, (function () { 'use strict';
6
7  function _defineProperty$1(obj, key, value) {
8    key = _toPropertyKey(key);
9    if (key in obj) {
10      Object.defineProperty(obj, key, {
11        value: value,
12        enumerable: true,
13        configurable: true,
14        writable: true
15      });
16    } else {
17      obj[key] = value;
18    }
19    return obj;
20  }
21  function _toPrimitive(input, hint) {
22    if (typeof input !== "object" || input === null) return input;
23    var prim = input[Symbol.toPrimitive];
24    if (prim !== undefined) {
25      var res = prim.call(input, hint || "default");
26      if (typeof res !== "object") return res;
27      throw new TypeError("@@toPrimitive must return a primitive value.");
28    }
29    return (hint === "string" ? String : Number)(input);
30  }
31  function _toPropertyKey(arg) {
32    var key = _toPrimitive(arg, "string");
33    return typeof key === "symbol" ? key : String(key);
34  }
35
36  function _classCallCheck(e, t) {
37    if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function");
38  }
39  function _defineProperties(e, t) {
40    for (var n = 0; n < t.length; n++) {
41      var r = t[n];
42      r.enumerable = r.enumerable || !1, r.configurable = !0, "value" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);
43    }
44  }
45  function _createClass(e, t, n) {
46    return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;
47  }
48  function _defineProperty(e, t, n) {
49    return t in e ? Object.defineProperty(e, t, {
50      value: n,
51      enumerable: !0,
52      configurable: !0,
53      writable: !0
54    }) : e[t] = n, e;
55  }
56  function ownKeys(e, t) {
57    var n = Object.keys(e);
58    if (Object.getOwnPropertySymbols) {
59      var r = Object.getOwnPropertySymbols(e);
60      t && (r = r.filter(function (t) {
61        return Object.getOwnPropertyDescriptor(e, t).enumerable;
62      })), n.push.apply(n, r);
63    }
64    return n;
65  }
66  function _objectSpread2(e) {
67    for (var t = 1; t < arguments.length; t++) {
68      var n = null != arguments[t] ? arguments[t] : {};
69      t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {
70        _defineProperty(e, t, n[t]);
71      }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {
72        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));
73      });
74    }
75    return e;
76  }
77  var defaults$1 = {
78    addCSS: !0,
79    thumbWidth: 15,
80    watch: !0
81  };
82  function matches$1(e, t) {
83    return function () {
84      return Array.from(document.querySelectorAll(t)).includes(this);
85    }.call(e, t);
86  }
87  function trigger(e, t) {
88    if (e && t) {
89      var n = new Event(t, {
90        bubbles: !0
91      });
92      e.dispatchEvent(n);
93    }
94  }
95  var getConstructor$1 = function (e) {
96      return null != e ? e.constructor : null;
97    },
98    instanceOf$1 = function (e, t) {
99      return !!(e && t && e instanceof t);
100    },
101    isNullOrUndefined$1 = function (e) {
102      return null == e;
103    },
104    isObject$1 = function (e) {
105      return getConstructor$1(e) === Object;
106    },
107    isNumber$1 = function (e) {
108      return getConstructor$1(e) === Number && !Number.isNaN(e);
109    },
110    isString$1 = function (e) {
111      return getConstructor$1(e) === String;
112    },
113    isBoolean$1 = function (e) {
114      return getConstructor$1(e) === Boolean;
115    },
116    isFunction$1 = function (e) {
117      return getConstructor$1(e) === Function;
118    },
119    isArray$1 = function (e) {
120      return Array.isArray(e);
121    },
122    isNodeList$1 = function (e) {
123      return instanceOf$1(e, NodeList);
124    },
125    isElement$1 = function (e) {
126      return instanceOf$1(e, Element);
127    },
128    isEvent$1 = function (e) {
129      return instanceOf$1(e, Event);
130    },
131    isEmpty$1 = function (e) {
132      return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;
133    },
134    is$1 = {
135      nullOrUndefined: isNullOrUndefined$1,
136      object: isObject$1,
137      number: isNumber$1,
vendor: 16,290 bytes, lines 138-609
138      string: isString$1,
139      boolean: isBoolean$1,
140      function: isFunction$1,
141      array: isArray$1,
142      nodeList: isNodeList$1,
143      element: isElement$1,
144      event: isEvent$1,
145      empty: isEmpty$1
146    };
147  function getDecimalPlaces(e) {
148    var t = "".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
149    return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;
150  }
151  function round(e, t) {
152    if (1 > t) {
153      var n = getDecimalPlaces(t);
154      return parseFloat(e.toFixed(n));
155    }
156    return Math.round(e / t) * t;
157  }
158  var RangeTouch = function () {
159    function e(t, n) {
160      _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());
161    }
162    return _createClass(e, [{
163      key: "init",
164      value: function () {
165        e.enabled && (this.config.addCSS && (this.element.style.userSelect = "none", this.element.style.webKitUserSelect = "none", this.element.style.touchAction = "manipulation"), this.listeners(!0), this.element.rangeTouch = this);
166      }
167    }, {
168      key: "destroy",
169      value: function () {
170        e.enabled && (this.config.addCSS && (this.element.style.userSelect = "", this.element.style.webKitUserSelect = "", this.element.style.touchAction = ""), this.listeners(!1), this.element.rangeTouch = null);
171      }
172    }, {
173      key: "listeners",
174      value: function (e) {
175        var t = this,
176          n = e ? "addEventListener" : "removeEventListener";
177        ["touchstart", "touchmove", "touchend"].forEach(function (e) {
178          t.element[n](e, function (e) {
179            return t.set(e);
180          }, !1);
181        });
182      }
183    }, {
184      key: "get",
185      value: function (t) {
186        if (!e.enabled || !is$1.event(t)) return null;
187        var n,
188          r = t.target,
189          i = t.changedTouches[0],
190          o = parseFloat(r.getAttribute("min")) || 0,
191          s = parseFloat(r.getAttribute("max")) || 100,
192          u = parseFloat(r.getAttribute("step")) || 1,
193          c = r.getBoundingClientRect(),
194          a = 100 / c.width * (this.config.thumbWidth / 2) / 100;
195        return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);
196      }
197    }, {
198      key: "set",
199      value: function (t) {
200        e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, "touchend" === t.type ? "change" : "input"));
201      }
202    }], [{
203      key: "setup",
204      value: function (t) {
205        var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},
206          r = null;
207        if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type="range"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;
208        var i = _objectSpread2({}, defaults$1, {}, n);
209        if (is$1.string(t) && i.watch) {
210          var o = new MutationObserver(function (n) {
211            Array.from(n).forEach(function (n) {
212              Array.from(n.addedNodes).forEach(function (n) {
213                is$1.element(n) && matches$1(n, t) && new e(n, i);
214              });
215            });
216          });
217          o.observe(document.body, {
218            childList: !0,
219            subtree: !0
220          });
221        }
222        return r.map(function (t) {
223          return new e(t, n);
224        });
225      }
226    }, {
227      key: "enabled",
228      get: function () {
229        return "ontouchstart" in document.documentElement;
230      }
231    }]), e;
232  }();
233
234  // ==========================================================================
235  // Type checking utils
236  // ==========================================================================
237
238  const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;
239  const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);
240  const isNullOrUndefined = input => input === null || typeof input === 'undefined';
241  const isObject = input => getConstructor(input) === Object;
242  const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);
243  const isString = input => getConstructor(input) === String;
244  const isBoolean = input => getConstructor(input) === Boolean;
245  const isFunction = input => typeof input === 'function';
246  const isArray = input => Array.isArray(input);
247  const isWeakMap = input => instanceOf(input, WeakMap);
248  const isNodeList = input => instanceOf(input, NodeList);
249  const isTextNode = input => getConstructor(input) === Text;
250  const isEvent = input => instanceOf(input, Event);
251  const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);
252  const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
253  const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);
254  const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);
255  const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';
256  const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;
257  const isUrl = input => {
258    // Accept a URL object
259    if (instanceOf(input, window.URL)) {
260      return true;
261    }
262
263    // Must be string from here
264    if (!isString(input)) {
265      return false;
266    }
267
268    // Add the protocol if required
269    let string = input;
270    if (!input.startsWith('http://') || !input.startsWith('https://')) {
271      string = `http://${input}`;
272    }
273    try {
274      return !isEmpty(new URL(string).hostname);
275    } catch (_) {
276      return false;
277    }
278  };
279  var is = {
280    nullOrUndefined: isNullOrUndefined,
281    object: isObject,
282    number: isNumber,
283    string: isString,
284    boolean: isBoolean,
285    function: isFunction,
286    array: isArray,
287    weakMap: isWeakMap,
288    nodeList: isNodeList,
289    element: isElement,
290    textNode: isTextNode,
291    event: isEvent,
292    keyboardEvent: isKeyboardEvent,
293    cue: isCue,
294    track: isTrack,
295    promise: isPromise,
296    url: isUrl,
297    empty: isEmpty
298  };
299
300  // ==========================================================================
301  const transitionEndEvent = (() => {
302    const element = document.createElement('span');
303    const events = {
304      WebkitTransition: 'webkitTransitionEnd',
305      MozTransition: 'transitionend',
306      OTransition: 'oTransitionEnd otransitionend',
307      transition: 'transitionend'
308    };
309    const type = Object.keys(events).find(event => element.style[event] !== undefined);
310    return is.string(type) ? events[type] : false;
311  })();
312
313  // Force repaint of element
314  function repaint(element, delay) {
315    setTimeout(() => {
316      try {
317        // eslint-disable-next-line no-param-reassign
318        element.hidden = true;
319
320        // eslint-disable-next-line no-unused-expressions
321        element.offsetHeight;
322
323        // eslint-disable-next-line no-param-reassign
324        element.hidden = false;
325      } catch (_) {
326        // Do nothing
327      }
328    }, delay);
329  }
330
331  // ==========================================================================
332  // Browser sniffing
333  // Unfortunately, due to mixed support, UA sniffing is required
334  // ==========================================================================
335
336  const isIE = Boolean(window.document.documentMode);
337  const isEdge = /Edge/g.test(navigator.userAgent);
338  const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);
339  const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
340  // navigator.platform may be deprecated but this check is still required
341  const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
342  const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;
343  var browser = {
344    isIE,
345    isEdge,
346    isWebKit,
347    isIPhone,
348    isIPadOS,
349    isIos
350  };
351
352  // ==========================================================================
353
354  // Clone nested objects
355  function cloneDeep(object) {
356    return JSON.parse(JSON.stringify(object));
357  }
358
359  // Get a nested value in an object
360  function getDeep(object, path) {
361    return path.split('.').reduce((obj, key) => obj && obj[key], object);
362  }
363
364  // Deep extend destination object with N more objects
365  function extend(target = {}, ...sources) {
366    if (!sources.length) {
367      return target;
368    }
369    const source = sources.shift();
370    if (!is.object(source)) {
371      return target;
372    }
373    Object.keys(source).forEach(key => {
374      if (is.object(source[key])) {
375        if (!Object.keys(target).includes(key)) {
376          Object.assign(target, {
377            [key]: {}
378          });
379        }
380        extend(target[key], source[key]);
381      } else {
382        Object.assign(target, {
383          [key]: source[key]
384        });
385      }
386    });
387    return extend(target, ...sources);
388  }
389
390  // ==========================================================================
391
392  // Wrap an element
393  function wrap(elements, wrapper) {
394    // Convert `elements` to an array, if necessary.
395    const targets = elements.length ? elements : [elements];
396
397    // Loops backwards to prevent having to clone the wrapper on the
398    // first element (see `child` below).
399    Array.from(targets).reverse().forEach((element, index) => {
400      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;
401      // Cache the current parent and sibling.
402      const parent = element.parentNode;
403      const sibling = element.nextSibling;
404
405      // Wrap the element (is automatically removed from its current
406      // parent).
407      child.appendChild(element);
408
409      // If the element had a sibling, insert the wrapper before
410      // the sibling to maintain the HTML structure; otherwise, just
411      // append it to the parent.
412      if (sibling) {
413        parent.insertBefore(child, sibling);
414      } else {
415        parent.appendChild(child);
416      }
417    });
418  }
419
420  // Set attributes
421  function setAttributes(element, attributes) {
422    if (!is.element(element) || is.empty(attributes)) return;
423
424    // Assume null and undefined attributes should be left out,
425    // Setting them would otherwise convert them to "null" and "undefined"
426    Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));
427  }
428
429  // Create a DocumentFragment
430  function createElement(type, attributes, text) {
431    // Create a new <element>
432    const element = document.createElement(type);
433
434    // Set all passed attributes
435    if (is.object(attributes)) {
436      setAttributes(element, attributes);
437    }
438
439    // Add text node
440    if (is.string(text)) {
441      element.innerText = text;
442    }
443
444    // Return built element
445    return element;
446  }
447
448  // Insert an element after another
449  function insertAfter(element, target) {
450    if (!is.element(element) || !is.element(target)) return;
451    target.parentNode.insertBefore(element, target.nextSibling);
452  }
453
454  // Insert a DocumentFragment
455  function insertElement(type, parent, attributes, text) {
456    if (!is.element(parent)) return;
457    parent.appendChild(createElement(type, attributes, text));
458  }
459
460  // Remove element(s)
461  function removeElement(element) {
462    if (is.nodeList(element) || is.array(element)) {
463      Array.from(element).forEach(removeElement);
464      return;
465    }
466    if (!is.element(element) || !is.element(element.parentNode)) {
467      return;
468    }
469    element.parentNode.removeChild(element);
470  }
471
472  // Remove all child elements
473  function emptyElement(element) {
474    if (!is.element(element)) return;
475    let {
476      length
477    } = element.childNodes;
478    while (length > 0) {
479      element.removeChild(element.lastChild);
480      length -= 1;
481    }
482  }
483
484  // Replace element
485  function replaceElement(newChild, oldChild) {
486    if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;
487    oldChild.parentNode.replaceChild(newChild, oldChild);
488    return newChild;
489  }
490
491  // Get an attribute object from a string selector
492  function getAttributesFromSelector(sel, existingAttributes) {
493    // For example:
494    // '.test' to { class: 'test' }
495    // '#test' to { id: 'test' }
496    // '[data-test="test"]' to { 'data-test': 'test' }
497
498    if (!is.string(sel) || is.empty(sel)) return {};
499    const attributes = {};
500    const existing = extend({}, existingAttributes);
501    sel.split(',').forEach(s => {
502      // Remove whitespace
503      const selector = s.trim();
504      const className = selector.replace('.', '');
505      const stripped = selector.replace(/[[\]]/g, '');
506      // Get the parts and value
507      const parts = stripped.split('=');
508      const [key] = parts;
509      const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
510      // Get the first character
511      const start = selector.charAt(0);
512      switch (start) {
513        case '.':
514          // Add to existing classname
515          if (is.string(existing.class)) {
516            attributes.class = `${existing.class} ${className}`;
517          } else {
518            attributes.class = className;
519          }
520          break;
521        case '#':
522          // ID selector
523          attributes.id = selector.replace('#', '');
524          break;
525        case '[':
526          // Attribute selector
527          attributes[key] = value;
528          break;
529      }
530    });
531    return extend(existing, attributes);
532  }
533
534  // Toggle hidden
535  function toggleHidden(element, hidden) {
536    if (!is.element(element)) return;
537    let hide = hidden;
538    if (!is.boolean(hide)) {
539      hide = !element.hidden;
540    }
541
542    // eslint-disable-next-line no-param-reassign
543    element.hidden = hide;
544  }
545
546  // Mirror Element.classList.toggle, with IE compatibility for "force" argument
547  function toggleClass(element, className, force) {
548    if (is.nodeList(element)) {
549      return Array.from(element).map(e => toggleClass(e, className, force));
550    }
551    if (is.element(element)) {
552      let method = 'toggle';
553      if (typeof force !== 'undefined') {
554        method = force ? 'add' : 'remove';
555      }
556      element.classList[method](className);
557      return element.classList.contains(className);
558    }
559    return false;
560  }
561
562  // Has class name
563  function hasClass(element, className) {
564    return is.element(element) && element.classList.contains(className);
565  }
566
567  // Element matches selector
568  function matches(element, selector) {
569    const {
570      prototype
571    } = Element;
572    function match() {
573      return Array.from(document.querySelectorAll(selector)).includes(this);
574    }
575    const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
576    return method.call(element, selector);
577  }
578
579  // Closest ancestor element matching selector (also tests element itself)
580  function closest$1(element, selector) {
581    const {
582      prototype
583    } = Element;
584
585    // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
586    function closestElement() {
587      let el = this;
588      do {
589        if (matches.matches(el, selector)) return el;
590        el = el.parentElement || el.parentNode;
591      } while (el !== null && el.nodeType === 1);
592      return null;
593    }
594    const method = prototype.closest || closestElement;
595    return method.call(element, selector);
596  }
597
598  // Find all elements
599  function getElements(selector) {
600    return this.elements.container.querySelectorAll(selector);
601  }
602
603  // Find a single element
604  function getElement(selector) {
605    return this.elements.container.querySelector(selector);
606  }
607
608  // Set focus and tab focus class
609  function setFocus(element = null, focusVisible = false) {
610    if (!is.element(element)) return;
611
612    // Set regular focus
613    element.focus({
614      preventScroll: true,
615      focusVisible
616    });
617  }
618
619  // ==========================================================================
620
621  // Default codecs for checking mimetype support
622  const defaultCodecs = {
623    'audio/ogg': 'vorbis',
624    'audio/wav': '1',
625    'video/webm': 'vp8, vorbis',
626    'video/mp4': 'avc1.42E01E, mp4a.40.2',
627    'video/ogg': 'theora'
628  };
629
630  // Check for feature support
631  const support = {
632    // Basic support
633    audio: 'canPlayType' in document.createElement('audio'),
634    video: 'canPlayType' in document.createElement('video'),
635    // Check for support
636    // Basic functionality vs full UI
637    check(type, provider) {
638      const api = support[type] || provider !== 'html5';
639      const ui = api && support.rangeInput;
640      return {
641        api,
642        ui
643      };
644    },
645    // Picture-in-picture support
646    // Safari & Chrome only currently
647    pip: (() => {
648      // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them
649      // It will throw the following error when trying to enter picture-in-picture
650      // `NotSupportedError: The Picture-in-Picture mode is not supported.`
651      if (browser.isIPhone) {
652        return false;
653      }
654
655      // Safari
656      // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
657      if (is.function(createElement('video').webkitSetPresentationMode)) {
658        return true;
659      }
660
661      // Chrome
662      // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
663      if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
664        return true;
665      }
666      return false;
667    })(),
668    // Airplay support
669    // Safari only currently
670    airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
671    // Inline playback support
672    // https://webkit.org/blog/6784/new-video-policies-for-ios/
673    playsinline: 'playsInline' in document.createElement('video'),
674    // Check for mime type support against a player instance
675    // Credits: http://diveintohtml5.info/everything.html
676    // Related: http://www.leanbackplayer.com/test/h5mt.html
677    mime(input) {
678      if (is.empty(input)) {
679        return false;
680      }
681      const [mediaType] = input.split('/');
682      let type = input;
683
684      // Verify we're using HTML5 and there's no media type mismatch
685      if (!this.isHTML5 || mediaType !== this.type) {
686        return false;
687      }
688
689      // Add codec if required
690      if (Object.keys(defaultCodecs).includes(type)) {
691        type += `; codecs="${defaultCodecs[input]}"`;
692      }
693      try {
694        return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
695      } catch (_) {
696        return false;
697      }
698    },
vendor: 41,894 bytes, lines 699-2000
699    // Check for textTracks support
700    textTracks: 'textTracks' in document.createElement('video'),
701    // <input type="range"> Sliders
702    rangeInput: (() => {
703      const range = document.createElement('input');
704      range.type = 'range';
705      return range.type === 'range';
706    })(),
707    // Touch
708    // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
709    touch: 'ontouchstart' in document.documentElement,
710    // Detect transitions support
711    transitions: transitionEndEvent !== false,
712    // Reduced motion iOS & MacOS setting
713    // https://webkit.org/blog/7551/responsive-design-for-motion/
714    reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
715  };
716
717  // ==========================================================================
718
719  // Check for passive event listener support
720  // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
721  // https://www.youtube.com/watch?v=NPM6172J22g
722  const supportsPassiveListeners = (() => {
723    // Test via a getter in the options object to see if the passive property is accessed
724    let supported = false;
725    try {
726      const options = Object.defineProperty({}, 'passive', {
727        get() {
728          supported = true;
729          return null;
730        }
731      });
732      window.addEventListener('test', null, options);
733      window.removeEventListener('test', null, options);
734    } catch (_) {
735      // Do nothing
736    }
737    return supported;
738  })();
739
740  // Toggle event listener
741  function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {
742    // Bail if no element, event, or callback
743    if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
744      return;
745    }
746
747    // Allow multiple events
748    const events = event.split(' ');
749    // Build options
750    // Default to just the capture boolean for browsers with no passive listener support
751    let options = capture;
752
753    // If passive events listeners are supported
754    if (supportsPassiveListeners) {
755      options = {
756        // Whether the listener can be passive (i.e. default never prevented)
757        passive,
758        // Whether the listener is a capturing listener or not
759        capture
760      };
761    }
762
763    // If a single node is passed, bind the event listener
764    events.forEach(type => {
765      if (this && this.eventListeners && toggle) {
766        // Cache event listener
767        this.eventListeners.push({
768          element,
769          type,
770          callback,
771          options
772        });
773      }
774      element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
775    });
776  }
777
778  // Bind event handler
779  function on(element, events = '', callback, passive = true, capture = false) {
780    toggleListener.call(this, element, events, callback, true, passive, capture);
781  }
782
783  // Unbind event handler
784  function off(element, events = '', callback, passive = true, capture = false) {
785    toggleListener.call(this, element, events, callback, false, passive, capture);
786  }
787
788  // Bind once-only event handler
789  function once(element, events = '', callback, passive = true, capture = false) {
790    const onceCallback = (...args) => {
791      off(element, events, onceCallback, passive, capture);
792      callback.apply(this, args);
793    };
794    toggleListener.call(this, element, events, onceCallback, true, passive, capture);
795  }
796
797  // Trigger event
798  function triggerEvent(element, type = '', bubbles = false, detail = {}) {
799    // Bail if no element
800    if (!is.element(element) || is.empty(type)) {
801      return;
802    }
803
804    // Create and dispatch the event
805    const event = new CustomEvent(type, {
806      bubbles,
807      detail: {
808        ...detail,
809        plyr: this
810      }
811    });
812
813    // Dispatch the event
814    element.dispatchEvent(event);
815  }
816
817  // Unbind all cached event listeners
818  function unbindListeners() {
819    if (this && this.eventListeners) {
820      this.eventListeners.forEach(item => {
821        const {
822          element,
823          type,
824          callback,
825          options
826        } = item;
827        element.removeEventListener(type, callback, options);
828      });
829      this.eventListeners = [];
830    }
831  }
832
833  // Run method when / if player is ready
834  function ready() {
835    return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});
836  }
837
838  /**
839   * Silence a Promise-like object.
840   * This is useful for avoiding non-harmful, but potentially confusing "uncaught
841   * play promise" rejection error messages.
842   * @param  {Object} value An object that may or may not be `Promise`-like.
843   */
844  function silencePromise(value) {
845    if (is.promise(value)) {
846      value.then(null, () => {});
847    }
848  }
849
850  // ==========================================================================
851
852  // Remove duplicates in an array
853  function dedupe(array) {
854    if (!is.array(array)) {
855      return array;
856    }
857    return array.filter((item, index) => array.indexOf(item) === index);
858  }
859
860  // Get the closest value in an array
861  function closest(array, value) {
862    if (!is.array(array) || !array.length) {
863      return null;
864    }
865    return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
866  }
867
868  // ==========================================================================
869
870  // Check support for a CSS declaration
871  function supportsCSS(declaration) {
872    if (!window || !window.CSS) {
873      return false;
874    }
875    return window.CSS.supports(declaration);
876  }
877
878  // Standard/common aspect ratios
879  const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({
880    ...out,
881    [x / y]: [x, y]
882  }), {});
883
884  // Validate an aspect ratio
885  function validateAspectRatio(input) {
886    if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {
887      return false;
888    }
889    const ratio = is.array(input) ? input : input.split(':');
890    return ratio.map(Number).every(is.number);
891  }
892
893  // Reduce an aspect ratio to it's lowest form
894  function reduceAspectRatio(ratio) {
895    if (!is.array(ratio) || !ratio.every(is.number)) {
896      return null;
897    }
898    const [width, height] = ratio;
899    const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);
900    const divider = getDivider(width, height);
901    return [width / divider, height / divider];
902  }
903
904  // Calculate an aspect ratio
905  function getAspectRatio(input) {
906    const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;
907    // Try provided ratio
908    let ratio = parse(input);
909
910    // Get from config
911    if (ratio === null) {
912      ratio = parse(this.config.ratio);
913    }
914
915    // Get from embed
916    if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {
917      ({
918        ratio
919      } = this.embed);
920    }
921
922    // Get from HTML5 video
923    if (ratio === null && this.isHTML5) {
924      const {
925        videoWidth,
926        videoHeight
927      } = this.media;
928      ratio = [videoWidth, videoHeight];
929    }
930    return reduceAspectRatio(ratio);
931  }
932
933  // Set aspect ratio for responsive container
934  function setAspectRatio(input) {
935    if (!this.isVideo) {
936      return {};
937    }
938    const {
939      wrapper
940    } = this.elements;
941    const ratio = getAspectRatio.call(this, input);
942    if (!is.array(ratio)) {
943      return {};
944    }
945    const [x, y] = reduceAspectRatio(ratio);
946    const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);
947    const padding = 100 / x * y;
948    if (useNative) {
949      wrapper.style.aspectRatio = `${x}/${y}`;
950    } else {
951      wrapper.style.paddingBottom = `${padding}%`;
952    }
953
954    // For Vimeo we have an extra <div> to hide the standard controls and UI
955    if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {
956      const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
957      const offset = (height - padding) / (height / 50);
958      if (this.fullscreen.active) {
959        wrapper.style.paddingBottom = null;
960      } else {
961        this.media.style.transform = `translateY(-${offset}%)`;
962      }
963    } else if (this.isHTML5) {
964      wrapper.classList.add(this.config.classNames.videoFixedRatio);
965    }
966    return {
967      padding,
968      ratio
969    };
970  }
971
972  // Round an aspect ratio to closest standard ratio
973  function roundAspectRatio(x, y, tolerance = 0.05) {
974    const ratio = x / y;
975    const closestRatio = closest(Object.keys(standardRatios), ratio);
976
977    // Check match is within tolerance
978    if (Math.abs(closestRatio - ratio) <= tolerance) {
979      return standardRatios[closestRatio];
980    }
981
982    // No match
983    return [x, y];
984  }
985
986  // Get the size of the viewport
987  // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions
988  function getViewportSize() {
989    const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
990    const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
991    return [width, height];
992  }
993
994  // ==========================================================================
995  const html5 = {
996    getSources() {
997      if (!this.isHTML5) {
998        return [];
999      }
1000      const sources = Array.from(this.media.querySelectorAll('source'));
1001
1002      // Filter out unsupported sources (if type is specified)
1003      return sources.filter(source => {
1004        const type = source.getAttribute('type');
1005        if (is.empty(type)) {
1006          return true;
1007        }
1008        return support.mime.call(this, type);
1009      });
1010    },
1011    // Get quality levels
1012    getQualityOptions() {
1013      // Whether we're forcing all options (e.g. for streaming)
1014      if (this.config.quality.forced) {
1015        return this.config.quality.options;
1016      }
1017
1018      // Get sizes from <source> elements
1019      return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);
1020    },
1021    setup() {
1022      if (!this.isHTML5) {
1023        return;
1024      }
1025      const player = this;
1026
1027      // Set speed options from config
1028      player.options.speed = player.config.speed.options;
1029
1030      // Set aspect ratio if fixed
1031      if (!is.empty(this.config.ratio)) {
1032        setAspectRatio.call(player);
1033      }
1034
1035      // Quality
1036      Object.defineProperty(player.media, 'quality', {
1037        get() {
1038          // Get sources
1039          const sources = html5.getSources.call(player);
1040          const source = sources.find(s => s.getAttribute('src') === player.source);
1041
1042          // Return size, if match is found
1043          return source && Number(source.getAttribute('size'));
1044        },
1045        set(input) {
1046          if (player.quality === input) {
1047            return;
1048          }
1049
1050          // If we're using an external handler...
1051          if (player.config.quality.forced && is.function(player.config.quality.onChange)) {
1052            player.config.quality.onChange(input);
1053          } else {
1054            // Get sources
1055            const sources = html5.getSources.call(player);
1056            // Get first match for requested size
1057            const source = sources.find(s => Number(s.getAttribute('size')) === input);
1058
1059            // No matching source found
1060            if (!source) {
1061              return;
1062            }
1063
1064            // Get current state
1065            const {
1066              currentTime,
1067              paused,
1068              preload,
1069              readyState,
1070              playbackRate
1071            } = player.media;
1072
1073            // Set new source
1074            player.media.src = source.getAttribute('src');
1075
1076            // Prevent loading if preload="none" and the current source isn't loaded (#1044)
1077            if (preload !== 'none' || readyState) {
1078              // Restore time
1079              player.once('loadedmetadata', () => {
1080                player.speed = playbackRate;
1081                player.currentTime = currentTime;
1082
1083                // Resume playing
1084                if (!paused) {
1085                  silencePromise(player.play());
1086                }
1087              });
1088
1089              // Load new source
1090              player.media.load();
1091            }
1092          }
1093
1094          // Trigger change event
1095          triggerEvent.call(player, player.media, 'qualitychange', false, {
1096            quality: input
1097          });
1098        }
1099      });
1100    },
1101    // Cancel current network requests
1102    // See https://github.com/sampotts/plyr/issues/174
1103    cancelRequests() {
1104      if (!this.isHTML5) {
1105        return;
1106      }
1107
1108      // Remove child sources
1109      removeElement(html5.getSources.call(this));
1110
1111      // Set blank video src attribute
1112      // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
1113      // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
1114      this.media.setAttribute('src', this.config.blankVideo);
1115
1116      // Load the new empty source
1117      // This will cancel existing requests
1118      // See https://github.com/sampotts/plyr/issues/174
1119      this.media.load();
1120
1121      // Debugging
1122      this.debug.log('Cancelled network requests');
1123    }
1124  };
1125
1126  // ==========================================================================
1127
1128  // Generate a random ID
1129  function generateId(prefix) {
1130    return `${prefix}-${Math.floor(Math.random() * 10000)}`;
1131  }
1132
1133  // Format string
1134  function format(input, ...args) {
1135    if (is.empty(input)) return input;
1136    return input.toString().replace(/{(\d+)}/g, (_, i) => args[i].toString());
1137  }
1138
1139  // Get percentage
1140  function getPercentage(current, max) {
1141    if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
1142      return 0;
1143    }
1144    return (current / max * 100).toFixed(2);
1145  }
1146
1147  // Replace all occurrences of a string in a string
1148  const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
1149
1150  // Convert to title case
1151  const toTitleCase = (input = '') => input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());
1152
1153  // Convert string to pascalCase
1154  function toPascalCase(input = '') {
1155    let string = input.toString();
1156
1157    // Convert kebab case
1158    string = replaceAll(string, '-', ' ');
1159
1160    // Convert snake case
1161    string = replaceAll(string, '_', ' ');
1162
1163    // Convert to title case
1164    string = toTitleCase(string);
1165
1166    // Convert to pascal case
1167    return replaceAll(string, ' ', '');
1168  }
1169
1170  // Convert string to pascalCase
1171  function toCamelCase(input = '') {
1172    let string = input.toString();
1173
1174    // Convert to pascal case
1175    string = toPascalCase(string);
1176
1177    // Convert first character to lowercase
1178    return string.charAt(0).toLowerCase() + string.slice(1);
1179  }
1180
1181  // Remove HTML from a string
1182  function stripHTML(source) {
1183    const fragment = document.createDocumentFragment();
1184    const element = document.createElement('div');
1185    fragment.appendChild(element);
1186    element.innerHTML = source;
1187    return fragment.firstChild.innerText;
1188  }
1189
1190  // Like outerHTML, but also works for DocumentFragment
1191  function getHTML(element) {
1192    const wrapper = document.createElement('div');
1193    wrapper.appendChild(element);
1194    return wrapper.innerHTML;
1195  }
1196
1197  // ==========================================================================
1198
1199  // Skip i18n for abbreviations and brand names
1200  const resources = {
1201    pip: 'PIP',
1202    airplay: 'AirPlay',
1203    html5: 'HTML5',
1204    vimeo: 'Vimeo',
1205    youtube: 'YouTube'
1206  };
1207  const i18n = {
1208    get(key = '', config = {}) {
1209      if (is.empty(key) || is.empty(config)) {
1210        return '';
1211      }
1212      let string = getDeep(config.i18n, key);
1213      if (is.empty(string)) {
1214        if (Object.keys(resources).includes(key)) {
1215          return resources[key];
1216        }
1217        return '';
1218      }
1219      const replace = {
1220        '{seektime}': config.seekTime,
1221        '{title}': config.title
1222      };
1223      Object.entries(replace).forEach(([k, v]) => {
1224        string = replaceAll(string, k, v);
1225      });
1226      return string;
1227    }
1228  };
1229
1230  class Storage {
1231    constructor(player) {
1232      _defineProperty$1(this, "get", key => {
1233        if (!Storage.supported || !this.enabled) {
1234          return null;
1235        }
1236        const store = window.localStorage.getItem(this.key);
1237        if (is.empty(store)) {
1238          return null;
1239        }
1240        const json = JSON.parse(store);
1241        return is.string(key) && key.length ? json[key] : json;
1242      });
1243      _defineProperty$1(this, "set", object => {
1244        // Bail if we don't have localStorage support or it's disabled
1245        if (!Storage.supported || !this.enabled) {
1246          return;
1247        }
1248
1249        // Can only store objectst
1250        if (!is.object(object)) {
1251          return;
1252        }
1253
1254        // Get current storage
1255        let storage = this.get();
1256
1257        // Default to empty object
1258        if (is.empty(storage)) {
1259          storage = {};
1260        }
1261
1262        // Update the working copy of the values
1263        extend(storage, object);
1264
1265        // Update storage
1266        try {
1267          window.localStorage.setItem(this.key, JSON.stringify(storage));
1268        } catch (_) {
1269          // Do nothing
1270        }
1271      });
1272      this.enabled = player.config.storage.enabled;
1273      this.key = player.config.storage.key;
1274    }
1275
1276    // Check for actual support (see if we can use it)
1277    static get supported() {
1278      try {
1279        if (!('localStorage' in window)) {
1280          return false;
1281        }
1282        const test = '___test';
1283
1284        // Try to use it (it might be disabled, e.g. user is in private mode)
1285        // see: https://github.com/sampotts/plyr/issues/131
1286        window.localStorage.setItem(test, test);
1287        window.localStorage.removeItem(test);
1288        return true;
1289      } catch (_) {
1290        return false;
1291      }
1292    }
1293  }
1294
1295  // ==========================================================================
1296  // Fetch wrapper
1297  // Using XHR to avoid issues with older browsers
1298  // ==========================================================================
1299
1300  function fetch(url, responseType = 'text') {
1301    return new Promise((resolve, reject) => {
1302      try {
1303        const request = new XMLHttpRequest();
1304
1305        // Check for CORS support
1306        if (!('withCredentials' in request)) {
1307          return;
1308        }
1309        request.addEventListener('load', () => {
1310          if (responseType === 'text') {
1311            try {
1312              resolve(JSON.parse(request.responseText));
1313            } catch (_) {
1314              resolve(request.responseText);
1315            }
1316          } else {
1317            resolve(request.response);
1318          }
1319        });
1320        request.addEventListener('error', () => {
1321          throw new Error(request.status);
1322        });
1323        request.open('GET', url, true);
1324
1325        // Set the required response type
1326        request.responseType = responseType;
1327        request.send();
1328      } catch (error) {
1329        reject(error);
1330      }
1331    });
1332  }
1333
1334  // ==========================================================================
1335
1336  // Load an external SVG sprite
1337  function loadSprite(url, id) {
1338    if (!is.string(url)) {
1339      return;
1340    }
1341    const prefix = 'cache';
1342    const hasId = is.string(id);
1343    let isCached = false;
1344    const exists = () => document.getElementById(id) !== null;
1345    const update = (container, data) => {
1346      // eslint-disable-next-line no-param-reassign
1347      container.innerHTML = data;
1348
1349      // Check again incase of race condition
1350      if (hasId && exists()) {
1351        return;
1352      }
1353
1354      // Inject the SVG to the body
1355      document.body.insertAdjacentElement('afterbegin', container);
1356    };
1357
1358    // Only load once if ID set
1359    if (!hasId || !exists()) {
1360      const useStorage = Storage.supported;
1361      // Create container
1362      const container = document.createElement('div');
1363      container.setAttribute('hidden', '');
1364      if (hasId) {
1365        container.setAttribute('id', id);
1366      }
1367
1368      // Check in cache
1369      if (useStorage) {
1370        const cached = window.localStorage.getItem(`${prefix}-${id}`);
1371        isCached = cached !== null;
1372        if (isCached) {
1373          const data = JSON.parse(cached);
1374          update(container, data.content);
1375        }
1376      }
1377
1378      // Get the sprite
1379      fetch(url).then(result => {
1380        if (is.empty(result)) {
1381          return;
1382        }
1383        if (useStorage) {
1384          try {
1385            window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({
1386              content: result
1387            }));
1388          } catch (_) {
1389            // Do nothing
1390          }
1391        }
1392        update(container, result);
1393      }).catch(() => {});
1394    }
1395  }
1396
1397  // ==========================================================================
1398
1399  // Time helpers
1400  const getHours = value => Math.trunc(value / 60 / 60 % 60, 10);
1401  const getMinutes = value => Math.trunc(value / 60 % 60, 10);
1402  const getSeconds = value => Math.trunc(value % 60, 10);
1403
1404  // Format time to UI friendly string
1405  function formatTime(time = 0, displayHours = false, inverted = false) {
1406    // Bail if the value isn't a number
1407    if (!is.number(time)) {
1408      return formatTime(undefined, displayHours, inverted);
1409    }
1410
1411    // Format time component to add leading zero
1412    const format = value => `0${value}`.slice(-2);
1413    // Breakdown to hours, mins, secs
1414    let hours = getHours(time);
1415    const mins = getMinutes(time);
1416    const secs = getSeconds(time);
1417
1418    // Do we need to display hours?
1419    if (displayHours || hours > 0) {
1420      hours = `${hours}:`;
1421    } else {
1422      hours = '';
1423    }
1424
1425    // Render
1426    return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
1427  }
1428
1429  // ==========================================================================
1430
1431  // TODO: Don't export a massive object - break down and create class
1432  const controls = {
1433    // Get icon URL
1434    getIconUrl() {
1435      const url = new URL(this.config.iconUrl, window.location);
1436      const host = window.location.host ? window.location.host : window.top.location.host;
1437      const cors = url.host !== host || browser.isIE && !window.svg4everybody;
1438      return {
1439        url: this.config.iconUrl,
1440        cors
1441      };
1442    },
1443    // Find the UI controls
1444    findElements() {
1445      try {
1446        this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);
1447
1448        // Buttons
1449        this.elements.buttons = {
1450          play: getElements.call(this, this.config.selectors.buttons.play),
1451          pause: getElement.call(this, this.config.selectors.buttons.pause),
1452          restart: getElement.call(this, this.config.selectors.buttons.restart),
1453          rewind: getElement.call(this, this.config.selectors.buttons.rewind),
1454          fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),
1455          mute: getElement.call(this, this.config.selectors.buttons.mute),
1456          pip: getElement.call(this, this.config.selectors.buttons.pip),
1457          airplay: getElement.call(this, this.config.selectors.buttons.airplay),
1458          settings: getElement.call(this, this.config.selectors.buttons.settings),
1459          captions: getElement.call(this, this.config.selectors.buttons.captions),
1460          fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)
1461        };
1462
1463        // Progress
1464        this.elements.progress = getElement.call(this, this.config.selectors.progress);
1465
1466        // Inputs
1467        this.elements.inputs = {
1468          seek: getElement.call(this, this.config.selectors.inputs.seek),
1469          volume: getElement.call(this, this.config.selectors.inputs.volume)
1470        };
1471
1472        // Display
1473        this.elements.display = {
1474          buffer: getElement.call(this, this.config.selectors.display.buffer),
1475          currentTime: getElement.call(this, this.config.selectors.display.currentTime),
1476          duration: getElement.call(this, this.config.selectors.display.duration)
1477        };
1478
1479        // Seek tooltip
1480        if (is.element(this.elements.progress)) {
1481          this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);
1482        }
1483        return true;
1484      } catch (error) {
1485        // Log it
1486        this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
1487
1488        // Restore native video controls
1489        this.toggleNativeControls(true);
1490        return false;
1491      }
1492    },
1493    // Create <svg> icon
1494    createIcon(type, attributes) {
1495      const namespace = 'http://www.w3.org/2000/svg';
1496      const iconUrl = controls.getIconUrl.call(this);
1497      const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;
1498      // Create <svg>
1499      const icon = document.createElementNS(namespace, 'svg');
1500      setAttributes(icon, extend(attributes, {
1501        'aria-hidden': 'true',
1502        focusable: 'false'
1503      }));
1504
1505      // Create the <use> to reference sprite
1506      const use = document.createElementNS(namespace, 'use');
1507      const path = `${iconPath}-${type}`;
1508
1509      // Set `href` attributes
1510      // https://github.com/sampotts/plyr/issues/460
1511      // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
1512      if ('href' in use) {
1513        use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
1514      }
1515
1516      // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
1517      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
1518
1519      // Add <use> to <svg>
1520      icon.appendChild(use);
1521      return icon;
1522    },
1523    // Create hidden text label
1524    createLabel(key, attr = {}) {
1525      const text = i18n.get(key, this.config);
1526      const attributes = {
1527        ...attr,
1528        class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
1529      };
1530      return createElement('span', attributes, text);
1531    },
1532    // Create a badge
1533    createBadge(text) {
1534      if (is.empty(text)) {
1535        return null;
1536      }
1537      const badge = createElement('span', {
1538        class: this.config.classNames.menu.value
1539      });
1540      badge.appendChild(createElement('span', {
1541        class: this.config.classNames.menu.badge
1542      }, text));
1543      return badge;
1544    },
1545    // Create a <button>
1546    createButton(buttonType, attr) {
1547      const attributes = extend({}, attr);
1548      let type = toCamelCase(buttonType);
1549      const props = {
1550        element: 'button',
1551        toggle: false,
1552        label: null,
1553        icon: null,
1554        labelPressed: null,
1555        iconPressed: null
1556      };
1557      ['element', 'icon', 'label'].forEach(key => {
1558        if (Object.keys(attributes).includes(key)) {
1559          props[key] = attributes[key];
1560          delete attributes[key];
1561        }
1562      });
1563
1564      // Default to 'button' type to prevent form submission
1565      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
1566        attributes.type = 'button';
1567      }
1568
1569      // Set class name
1570      if (Object.keys(attributes).includes('class')) {
1571        if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {
1572          extend(attributes, {
1573            class: `${attributes.class} ${this.config.classNames.control}`
1574          });
1575        }
1576      } else {
1577        attributes.class = this.config.classNames.control;
1578      }
1579
1580      // Large play button
1581      switch (buttonType) {
1582        case 'play':
1583          props.toggle = true;
1584          props.label = 'play';
1585          props.labelPressed = 'pause';
1586          props.icon = 'play';
1587          props.iconPressed = 'pause';
1588          break;
1589        case 'mute':
1590          props.toggle = true;
1591          props.label = 'mute';
1592          props.labelPressed = 'unmute';
1593          props.icon = 'volume';
1594          props.iconPressed = 'muted';
1595          break;
1596        case 'captions':
1597          props.toggle = true;
1598          props.label = 'enableCaptions';
1599          props.labelPressed = 'disableCaptions';
1600          props.icon = 'captions-off';
1601          props.iconPressed = 'captions-on';
1602          break;
1603        case 'fullscreen':
1604          props.toggle = true;
1605          props.label = 'enterFullscreen';
1606          props.labelPressed = 'exitFullscreen';
1607          props.icon = 'enter-fullscreen';
1608          props.iconPressed = 'exit-fullscreen';
1609          break;
1610        case 'play-large':
1611          attributes.class += ` ${this.config.classNames.control}--overlaid`;
1612          type = 'play';
1613          props.label = 'play';
1614          props.icon = 'play';
1615          break;
1616        default:
1617          if (is.empty(props.label)) {
1618            props.label = type;
1619          }
1620          if (is.empty(props.icon)) {
1621            props.icon = buttonType;
1622          }
1623      }
1624      const button = createElement(props.element);
1625
1626      // Setup toggle icon and labels
1627      if (props.toggle) {
1628        // Icon
1629        button.appendChild(controls.createIcon.call(this, props.iconPressed, {
1630          class: 'icon--pressed'
1631        }));
1632        button.appendChild(controls.createIcon.call(this, props.icon, {
1633          class: 'icon--not-pressed'
1634        }));
1635
1636        // Label/Tooltip
1637        button.appendChild(controls.createLabel.call(this, props.labelPressed, {
1638          class: 'label--pressed'
1639        }));
1640        button.appendChild(controls.createLabel.call(this, props.label, {
1641          class: 'label--not-pressed'
1642        }));
1643      } else {
1644        button.appendChild(controls.createIcon.call(this, props.icon));
1645        button.appendChild(controls.createLabel.call(this, props.label));
1646      }
1647
1648      // Merge and set attributes
1649      extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
1650      setAttributes(button, attributes);
1651
1652      // We have multiple play buttons
1653      if (type === 'play') {
1654        if (!is.array(this.elements.buttons[type])) {
1655          this.elements.buttons[type] = [];
1656        }
1657        this.elements.buttons[type].push(button);
1658      } else {
1659        this.elements.buttons[type] = button;
1660      }
1661      return button;
1662    },
1663    // Create an <input type='range'>
1664    createRange(type, attributes) {
1665      // Seek input
1666      const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
1667        type: 'range',
1668        min: 0,
1669        max: 100,
1670        step: 0.01,
1671        value: 0,
1672        autocomplete: 'off',
1673        // A11y fixes for https://github.com/sampotts/plyr/issues/905
1674        role: 'slider',
1675        'aria-label': i18n.get(type, this.config),
1676        'aria-valuemin': 0,
1677        'aria-valuemax': 100,
1678        'aria-valuenow': 0
1679      }, attributes));
1680      this.elements.inputs[type] = input;
1681
1682      // Set the fill for webkit now
1683      controls.updateRangeFill.call(this, input);
1684
1685      // Improve support on touch devices
1686      RangeTouch.setup(input);
1687      return input;
1688    },
1689    // Create a <progress>
1690    createProgress(type, attributes) {
1691      const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {
1692        min: 0,
1693        max: 100,
1694        value: 0,
1695        role: 'progressbar',
1696        'aria-hidden': true
1697      }, attributes));
1698
1699      // Create the label inside
1700      if (type !== 'volume') {
1701        progress.appendChild(createElement('span', null, '0'));
1702        const suffixKey = {
1703          played: 'played',
1704          buffer: 'buffered'
1705        }[type];
1706        const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
1707        progress.innerText = `% ${suffix.toLowerCase()}`;
1708      }
1709      this.elements.display[type] = progress;
1710      return progress;
1711    },
1712    // Create time display
1713    createTime(type, attrs) {
1714      const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);
1715      const container = createElement('div', extend(attributes, {
1716        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),
1717        'aria-label': i18n.get(type, this.config),
1718        role: 'timer'
1719      }), '00:00');
1720
1721      // Reference for updates
1722      this.elements.display[type] = container;
1723      return container;
1724    },
1725    // Bind keyboard shortcuts for a menu item
1726    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
1727    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
1728    bindMenuItemShortcuts(menuItem, type) {
1729      // Navigate through menus via arrow keys and space
1730      on.call(this, menuItem, 'keydown keyup', event => {
1731        // We only care about space and ⬆️ ⬇️️ ➡️
1732        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {
1733          return;
1734        }
1735
1736        // Prevent play / seek
1737        event.preventDefault();
1738        event.stopPropagation();
1739
1740        // We're just here to prevent the keydown bubbling
1741        if (event.type === 'keydown') {
1742          return;
1743        }
1744        const isRadioButton = matches(menuItem, '[role="menuitemradio"]');
1745
1746        // Show the respective menu
1747        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {
1748          controls.showMenuPanel.call(this, type, true);
1749        } else {
1750          let target;
1751          if (event.key !== ' ') {
1752            if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {
1753              target = menuItem.nextElementSibling;
1754              if (!is.element(target)) {
1755                target = menuItem.parentNode.firstElementChild;
1756              }
1757            } else {
1758              target = menuItem.previousElementSibling;
1759              if (!is.element(target)) {
1760                target = menuItem.parentNode.lastElementChild;
1761              }
1762            }
1763            setFocus.call(this, target, true);
1764          }
1765        }
1766      }, false);
1767
1768      // Enter will fire a `click` event but we still need to manage focus
1769      // So we bind to keyup which fires after and set focus here
1770      on.call(this, menuItem, 'keyup', event => {
1771        if (event.key !== 'Return') return;
1772        controls.focusFirstMenuItem.call(this, null, true);
1773      });
1774    },
1775    // Create a settings menu item
1776    createMenuItem({
1777      value,
1778      list,
1779      type,
1780      title,
1781      badge = null,
1782      checked = false
1783    }) {
1784      const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
1785      const menuItem = createElement('button', extend(attributes, {
1786        type: 'button',
1787        role: 'menuitemradio',
1788        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),
1789        'aria-checked': checked,
1790        value
1791      }));
1792      const flex = createElement('span');
1793
1794      // We have to set as HTML incase of special characters
1795      flex.innerHTML = title;
1796      if (is.element(badge)) {
1797        flex.appendChild(badge);
1798      }
1799      menuItem.appendChild(flex);
1800
1801      // Replicate radio button behavior
1802      Object.defineProperty(menuItem, 'checked', {
1803        enumerable: true,
1804        get() {
1805          return menuItem.getAttribute('aria-checked') === 'true';
1806        },
1807        set(check) {
1808          // Ensure exclusivity
1809          if (check) {
1810            Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role="menuitemradio"]')).forEach(node => node.setAttribute('aria-checked', 'false'));
1811          }
1812          menuItem.setAttribute('aria-checked', check ? 'true' : 'false');
1813        }
1814      });
1815      this.listeners.bind(menuItem, 'click keyup', event => {
1816        if (is.keyboardEvent(event) && event.key !== ' ') {
1817          return;
1818        }
1819        event.preventDefault();
1820        event.stopPropagation();
1821        menuItem.checked = true;
1822        switch (type) {
1823          case 'language':
1824            this.currentTrack = Number(value);
1825            break;
1826          case 'quality':
1827            this.quality = value;
1828            break;
1829          case 'speed':
1830            this.speed = parseFloat(value);
1831            break;
1832        }
1833        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));
1834      }, type, false);
1835      controls.bindMenuItemShortcuts.call(this, menuItem, type);
1836      list.appendChild(menuItem);
1837    },
1838    // Format a time for display
1839    formatTime(time = 0, inverted = false) {
1840      // Bail if the value isn't a number
1841      if (!is.number(time)) {
1842        return time;
1843      }
1844
1845      // Always display hours if duration is over an hour
1846      const forceHours = getHours(this.duration) > 0;
1847      return formatTime(time, forceHours, inverted);
1848    },
1849    // Update the displayed time
1850    updateTimeDisplay(target = null, time = 0, inverted = false) {
1851      // Bail if there's no element to display or the value isn't a number
1852      if (!is.element(target) || !is.number(time)) {
1853        return;
1854      }
1855
1856      // eslint-disable-next-line no-param-reassign
1857      target.innerText = controls.formatTime(time, inverted);
1858    },
1859    // Update volume UI and storage
1860    updateVolume() {
1861      if (!this.supported.ui) {
1862        return;
1863      }
1864
1865      // Update range
1866      if (is.element(this.elements.inputs.volume)) {
1867        controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
1868      }
1869
1870      // Update mute state
1871      if (is.element(this.elements.buttons.mute)) {
1872        this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
1873      }
1874    },
1875    // Update seek value and lower fill
1876    setRange(target, value = 0) {
1877      if (!is.element(target)) {
1878        return;
1879      }
1880
1881      // eslint-disable-next-line
1882      target.value = value;
1883
1884      // Webkit range fill
1885      controls.updateRangeFill.call(this, target);
1886    },
1887    // Update <progress> elements
1888    updateProgress(event) {
1889      if (!this.supported.ui || !is.event(event)) {
1890        return;
1891      }
1892      let value = 0;
1893      const setProgress = (target, input) => {
1894        const val = is.number(input) ? input : 0;
1895        const progress = is.element(target) ? target : this.elements.display.buffer;
1896
1897        // Update value and label
1898        if (is.element(progress)) {
1899          progress.value = val;
1900
1901          // Update text label inside
1902          const label = progress.getElementsByTagName('span')[0];
1903          if (is.element(label)) {
1904            label.childNodes[0].nodeValue = val;
1905          }
1906        }
1907      };
1908      if (event) {
1909        switch (event.type) {
1910          // Video playing
1911          case 'timeupdate':
1912          case 'seeking':
1913          case 'seeked':
1914            value = getPercentage(this.currentTime, this.duration);
1915
1916            // Set seek range value only if it's a 'natural' time event
1917            if (event.type === 'timeupdate') {
1918              controls.setRange.call(this, this.elements.inputs.seek, value);
1919            }
1920            break;
1921
1922          // Check buffer status
1923          case 'playing':
1924          case 'progress':
1925            setProgress(this.elements.display.buffer, this.buffered * 100);
1926            break;
1927        }
1928      }
1929    },
1930    // Webkit polyfill for lower fill range
1931    updateRangeFill(target) {
1932      // Get range from event if event passed
1933      const range = is.event(target) ? target.target : target;
1934
1935      // Needs to be a valid <input type='range'>
1936      if (!is.element(range) || range.getAttribute('type') !== 'range') {
1937        return;
1938      }
1939
1940      // Set aria values for https://github.com/sampotts/plyr/issues/905
1941      if (matches(range, this.config.selectors.inputs.seek)) {
1942        range.setAttribute('aria-valuenow', this.currentTime);
1943        const currentTime = controls.formatTime(this.currentTime);
1944        const duration = controls.formatTime(this.duration);
1945        const format = i18n.get('seekLabel', this.config);
1946        range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));
1947      } else if (matches(range, this.config.selectors.inputs.volume)) {
1948        const percent = range.value * 100;
1949        range.setAttribute('aria-valuenow', percent);
1950        range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);
1951      } else {
1952        range.setAttribute('aria-valuenow', range.value);
1953      }
1954
1955      // WebKit only
1956      if (!browser.isWebKit && !browser.isIPadOS) {
1957        return;
1958      }
1959
1960      // Set CSS custom property
1961      range.style.setProperty('--value', `${range.value / range.max * 100}%`);
1962    },
1963    // Update hover tooltip for seeking
1964    updateSeekTooltip(event) {
1965      var _this$config$markers, _this$config$markers$;
1966      // Bail if setting not true
1967      if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {
1968        return;
1969      }
1970      const tipElement = this.elements.display.seekTooltip;
1971      const visible = `${this.config.classNames.tooltip}--visible`;
1972      const toggle = show => toggleClass(tipElement, visible, show);
1973
1974      // Hide on touch
1975      if (this.touch) {
1976        toggle(false);
1977        return;
1978      }
1979
1980      // Determine percentage, if already visible
1981      let percent = 0;
1982      const clientRect = this.elements.progress.getBoundingClientRect();
1983      if (is.event(event)) {
1984        percent = 100 / clientRect.width * (event.pageX - clientRect.left);
1985      } else if (hasClass(tipElement, visible)) {
1986        percent = parseFloat(tipElement.style.left, 10);
1987      } else {
1988        return;
1989      }
1990
1991      // Set bounds
1992      if (percent < 0) {
1993        percent = 0;
1994      } else if (percent > 100) {
1995        percent = 100;
1996      }
1997      const time = this.duration / 100 * percent;
1998
1999      // Display the time a click would seek to
2000      tipElement.innerTex
vendor: 22,956 bytes, lines 2000-2655
2000t = controls.formatTime(time);
2001
2002      // Get marker point for time
2003      const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({
2004        time: t
2005      }) => t === Math.round(time));
2006
2007      // Append the point label to the tooltip
2008      if (point) {
2009        tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);
2010      }
2011
2012      // Set position
2013      tipElement.style.left = `${percent}%`;
2014
2015      // Show/hide the tooltip
2016      // If the event is a moues in/out and percentage is inside bounds
2017      if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
2018        toggle(event.type === 'mouseenter');
2019      }
2020    },
2021    // Handle time change event
2022    timeUpdate(event) {
2023      // Only invert if only one time element is displayed and used for both duration and currentTime
2024      const invert = !is.element(this.elements.display.duration) && this.config.invertTime;
2025
2026      // Duration
2027      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
2028
2029      // Ignore updates while seeking
2030      if (event && event.type === 'timeupdate' && this.media.seeking) {
2031        return;
2032      }
2033
2034      // Playing progress
2035      controls.updateProgress.call(this, event);
2036    },
2037    // Show the duration on metadataloaded or durationchange events
2038    durationUpdate() {
2039      // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
2040      if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
2041        return;
2042      }
2043
2044      // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.
2045      // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
2046      // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
2047      // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338
2048      if (this.duration >= 2 ** 32) {
2049        toggleHidden(this.elements.display.currentTime, true);
2050        toggleHidden(this.elements.progress, true);
2051        return;
2052      }
2053
2054      // Update ARIA values
2055      if (is.element(this.elements.inputs.seek)) {
2056        this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
2057      }
2058
2059      // If there's a spot to display duration
2060      const hasDuration = is.element(this.elements.display.duration);
2061
2062      // If there's only one time display, display duration there
2063      if (!hasDuration && this.config.displayDuration && this.paused) {
2064        controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
2065      }
2066
2067      // If there's a duration element, update content
2068      if (hasDuration) {
2069        controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
2070      }
2071      if (this.config.markers.enabled) {
2072        controls.setMarkers.call(this);
2073      }
2074
2075      // Update the tooltip (if visible)
2076      controls.updateSeekTooltip.call(this);
2077    },
2078    // Hide/show a tab
2079    toggleMenuButton(setting, toggle) {
2080      toggleHidden(this.elements.settings.buttons[setting], !toggle);
2081    },
2082    // Update the selected setting
2083    updateSetting(setting, container, input) {
2084      const pane = this.elements.settings.panels[setting];
2085      let value = null;
2086      let list = container;
2087      if (setting === 'captions') {
2088        value = this.currentTrack;
2089      } else {
2090        value = !is.empty(input) ? input : this[setting];
2091
2092        // Get default
2093        if (is.empty(value)) {
2094          value = this.config[setting].default;
2095        }
2096
2097        // Unsupported value
2098        if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
2099          this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
2100          return;
2101        }
2102
2103        // Disabled value
2104        if (!this.config[setting].options.includes(value)) {
2105          this.debug.warn(`Disabled value of '${value}' for ${setting}`);
2106          return;
2107        }
2108      }
2109
2110      // Get the list if we need to
2111      if (!is.element(list)) {
2112        list = pane && pane.querySelector('[role="menu"]');
2113      }
2114
2115      // If there's no list it means it's not been rendered...
2116      if (!is.element(list)) {
2117        return;
2118      }
2119
2120      // Update the label
2121      const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);
2122      label.innerHTML = controls.getLabel.call(this, setting, value);
2123
2124      // Find the radio option and check it
2125      const target = list && list.querySelector(`[value="${value}"]`);
2126      if (is.element(target)) {
2127        target.checked = true;
2128      }
2129    },
2130    // Translate a value into a nice label
2131    getLabel(setting, value) {
2132      switch (setting) {
2133        case 'speed':
2134          return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;
2135        case 'quality':
2136          if (is.number(value)) {
2137            const label = i18n.get(`qualityLabel.${value}`, this.config);
2138            if (!label.length) {
2139              return `${value}p`;
2140            }
2141            return label;
2142          }
2143          return toTitleCase(value);
2144        case 'captions':
2145          return captions.getLabel.call(this);
2146        default:
2147          return null;
2148      }
2149    },
2150    // Set the quality menu
2151    setQualityMenu(options) {
2152      // Menu required
2153      if (!is.element(this.elements.settings.panels.quality)) {
2154        return;
2155      }
2156      const type = 'quality';
2157      const list = this.elements.settings.panels.quality.querySelector('[role="menu"]');
2158
2159      // Set options if passed and filter based on uniqueness and config
2160      if (is.array(options)) {
2161        this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));
2162      }
2163
2164      // Toggle the pane and tab
2165      const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
2166      controls.toggleMenuButton.call(this, type, toggle);
2167
2168      // Empty the menu
2169      emptyElement(list);
2170
2171      // Check if we need to toggle the parent
2172      controls.checkMenu.call(this);
2173
2174      // If we're hiding, nothing more to do
2175      if (!toggle) {
2176        return;
2177      }
2178
2179      // Get the badge HTML for HD, 4K etc
2180      const getBadge = quality => {
2181        const label = i18n.get(`qualityBadge.${quality}`, this.config);
2182        if (!label.length) {
2183          return null;
2184        }
2185        return controls.createBadge.call(this, label);
2186      };
2187
2188      // Sort options by the config and then render options
2189      this.options.quality.sort((a, b) => {
2190        const sorting = this.config.quality.options;
2191        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
2192      }).forEach(quality => {
2193        controls.createMenuItem.call(this, {
2194          value: quality,
2195          list,
2196          type,
2197          title: controls.getLabel.call(this, 'quality', quality),
2198          badge: getBadge(quality)
2199        });
2200      });
2201      controls.updateSetting.call(this, type, list);
2202    },
2203    // Set the looping options
2204    /* setLoopMenu() {
2205          // Menu required
2206          if (!is.element(this.elements.settings.panels.loop)) {
2207              return;
2208          }
2209           const options = ['start', 'end', 'all', 'reset'];
2210          const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
2211           // Show the pane and tab
2212          toggleHidden(this.elements.settings.buttons.loop, false);
2213          toggleHidden(this.elements.settings.panels.loop, false);
2214           // Toggle the pane and tab
2215          const toggle = !is.empty(this.loop.options);
2216          controls.toggleMenuButton.call(this, 'loop', toggle);
2217           // Empty the menu
2218          emptyElement(list);
2219           options.forEach(option => {
2220              const item = createElement('li');
2221               const button = createElement(
2222                  'button',
2223                  extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
2224                      type: 'button',
2225                      class: this.config.classNames.control,
2226                      'data-plyr-loop-action': option,
2227                  }),
2228                  i18n.get(option, this.config)
2229              );
2230               if (['start', 'end'].includes(option)) {
2231                  const badge = controls.createBadge.call(this, '00:00');
2232                  button.appendChild(badge);
2233              }
2234               item.appendChild(button);
2235              list.appendChild(item);
2236          });
2237      }, */
2238
2239    // Get current selected caption language
2240    // TODO: rework this to user the getter in the API?
2241
2242    // Set a list of available captions languages
2243    setCaptionsMenu() {
2244      // Menu required
2245      if (!is.element(this.elements.settings.panels.captions)) {
2246        return;
2247      }
2248
2249      // TODO: Captions or language? Currently it's mixed
2250      const type = 'captions';
2251      const list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
2252      const tracks = captions.getTracks.call(this);
2253      const toggle = Boolean(tracks.length);
2254
2255      // Toggle the pane and tab
2256      controls.toggleMenuButton.call(this, type, toggle);
2257
2258      // Empty the menu
2259      emptyElement(list);
2260
2261      // Check if we need to toggle the parent
2262      controls.checkMenu.call(this);
2263
2264      // If there's no captions, bail
2265      if (!toggle) {
2266        return;
2267      }
2268
2269      // Generate options data
2270      const options = tracks.map((track, value) => ({
2271        value,
2272        checked: this.captions.toggled && this.currentTrack === value,
2273        title: captions.getLabel.call(this, track),
2274        badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),
2275        list,
2276        type: 'language'
2277      }));
2278
2279      // Add the "Disabled" option to turn off captions
2280      options.unshift({
2281        value: -1,
2282        checked: !this.captions.toggled,
2283        title: i18n.get('disabled', this.config),
2284        list,
2285        type: 'language'
2286      });
2287
2288      // Generate options
2289      options.forEach(controls.createMenuItem.bind(this));
2290      controls.updateSetting.call(this, type, list);
2291    },
2292    // Set a list of available captions languages
2293    setSpeedMenu() {
2294      // Menu required
2295      if (!is.element(this.elements.settings.panels.speed)) {
2296        return;
2297      }
2298      const type = 'speed';
2299      const list = this.elements.settings.panels.speed.querySelector('[role="menu"]');
2300
2301      // Filter out invalid speeds
2302      this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);
2303
2304      // Toggle the pane and tab
2305      const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
2306      controls.toggleMenuButton.call(this, type, toggle);
2307
2308      // Empty the menu
2309      emptyElement(list);
2310
2311      // Check if we need to toggle the parent
2312      controls.checkMenu.call(this);
2313
2314      // If we're hiding, nothing more to do
2315      if (!toggle) {
2316        return;
2317      }
2318
2319      // Create items
2320      this.options.speed.forEach(speed => {
2321        controls.createMenuItem.call(this, {
2322          value: speed,
2323          list,
2324          type,
2325          title: controls.getLabel.call(this, 'speed', speed)
2326        });
2327      });
2328      controls.updateSetting.call(this, type, list);
2329    },
2330    // Check if we need to hide/show the settings menu
2331    checkMenu() {
2332      const {
2333        buttons
2334      } = this.elements.settings;
2335      const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);
2336      toggleHidden(this.elements.settings.menu, !visible);
2337    },
2338    // Focus the first menu item in a given (or visible) menu
2339    focusFirstMenuItem(pane, focusVisible = false) {
2340      if (this.elements.settings.popup.hidden) {
2341        return;
2342      }
2343      let target = pane;
2344      if (!is.element(target)) {
2345        target = Object.values(this.elements.settings.panels).find(p => !p.hidden);
2346      }
2347      const firstItem = target.querySelector('[role^="menuitem"]');
2348      setFocus.call(this, firstItem, focusVisible);
2349    },
2350    // Show/hide menu
2351    toggleMenu(input) {
2352      const {
2353        popup
2354      } = this.elements.settings;
2355      const button = this.elements.buttons.settings;
2356
2357      // Menu and button are required
2358      if (!is.element(popup) || !is.element(button)) {
2359        return;
2360      }
2361
2362      // True toggle by default
2363      const {
2364        hidden
2365      } = popup;
2366      let show = hidden;
2367      if (is.boolean(input)) {
2368        show = input;
2369      } else if (is.keyboardEvent(input) && input.key === 'Escape') {
2370        show = false;
2371      } else if (is.event(input)) {
2372        // If Plyr is in a shadowDOM, the event target is set to the component, instead of the
2373        // Element in the shadowDOM. The path, if available, is complete.
2374        const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;
2375        const isMenuItem = popup.contains(target);
2376
2377        // If the click was inside the menu or if the click
2378        // wasn't the button or menu item and we're trying to
2379        // show the menu (a doc click shouldn't show the menu)
2380        if (isMenuItem || !isMenuItem && input.target !== button && show) {
2381          return;
2382        }
2383      }
2384
2385      // Set button attributes
2386      button.setAttribute('aria-expanded', show);
2387
2388      // Show the actual popup
2389      toggleHidden(popup, !show);
2390
2391      // Add class hook
2392      toggleClass(this.elements.container, this.config.classNames.menu.open, show);
2393
2394      // Focus the first item if key interaction
2395      if (show && is.keyboardEvent(input)) {
2396        controls.focusFirstMenuItem.call(this, null, true);
2397      } else if (!show && !hidden) {
2398        // If closing, re-focus the button
2399        setFocus.call(this, button, is.keyboardEvent(input));
2400      }
2401    },
2402    // Get the natural size of a menu panel
2403    getMenuSize(tab) {
2404      const clone = tab.cloneNode(true);
2405      clone.style.position = 'absolute';
2406      clone.style.opacity = 0;
2407      clone.removeAttribute('hidden');
2408
2409      // Append to parent so we get the "real" size
2410      tab.parentNode.appendChild(clone);
2411
2412      // Get the sizes before we remove
2413      const width = clone.scrollWidth;
2414      const height = clone.scrollHeight;
2415
2416      // Remove from the DOM
2417      removeElement(clone);
2418      return {
2419        width,
2420        height
2421      };
2422    },
2423    // Show a panel in the menu
2424    showMenuPanel(type = '', focusVisible = false) {
2425      const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);
2426
2427      // Nothing to show, bail
2428      if (!is.element(target)) {
2429        return;
2430      }
2431
2432      // Hide all other panels
2433      const container = target.parentNode;
2434      const current = Array.from(container.children).find(node => !node.hidden);
2435
2436      // If we can do fancy animations, we'll animate the height/width
2437      if (support.transitions && !support.reducedMotion) {
2438        // Set the current width as a base
2439        container.style.width = `${current.scrollWidth}px`;
2440        container.style.height = `${current.scrollHeight}px`;
2441
2442        // Get potential sizes
2443        const size = controls.getMenuSize.call(this, target);
2444
2445        // Restore auto height/width
2446        const restore = event => {
2447          // We're only bothered about height and width on the container
2448          if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
2449            return;
2450          }
2451
2452          // Revert back to auto
2453          container.style.width = '';
2454          container.style.height = '';
2455
2456          // Only listen once
2457          off.call(this, container, transitionEndEvent, restore);
2458        };
2459
2460        // Listen for the transition finishing and restore auto height/width
2461        on.call(this, container, transitionEndEvent, restore);
2462
2463        // Set dimensions to target
2464        container.style.width = `${size.width}px`;
2465        container.style.height = `${size.height}px`;
2466      }
2467
2468      // Set attributes on current tab
2469      toggleHidden(current, true);
2470
2471      // Set attributes on target
2472      toggleHidden(target, false);
2473
2474      // Focus the first item
2475      controls.focusFirstMenuItem.call(this, target, focusVisible);
2476    },
2477    // Set the download URL
2478    setDownloadUrl() {
2479      const button = this.elements.buttons.download;
2480
2481      // Bail if no button
2482      if (!is.element(button)) {
2483        return;
2484      }
2485
2486      // Set attribute
2487      button.setAttribute('href', this.download);
2488    },
2489    // Build the default HTML
2490    create(data) {
2491      const {
2492        bindMenuItemShortcuts,
2493        createButton,
2494        createProgress,
2495        createRange,
2496        createTime,
2497        setQualityMenu,
2498        setSpeedMenu,
2499        showMenuPanel
2500      } = controls;
2501      this.elements.controls = null;
2502
2503      // Larger overlaid play button
2504      if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {
2505        this.elements.container.appendChild(createButton.call(this, 'play-large'));
2506      }
2507
2508      // Create the container
2509      const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));
2510      this.elements.controls = container;
2511
2512      // Default item attributes
2513      const defaultAttributes = {
2514        class: 'plyr__controls__item'
2515      };
2516
2517      // Loop through controls in order
2518      dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {
2519        // Restart button
2520        if (control === 'restart') {
2521          container.appendChild(createButton.call(this, 'restart', defaultAttributes));
2522        }
2523
2524        // Rewind button
2525        if (control === 'rewind') {
2526          container.appendChild(createButton.call(this, 'rewind', defaultAttributes));
2527        }
2528
2529        // Play/Pause button
2530        if (control === 'play') {
2531          container.appendChild(createButton.call(this, 'play', defaultAttributes));
2532        }
2533
2534        // Fast forward button
2535        if (control === 'fast-forward') {
2536          container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));
2537        }
2538
2539        // Progress
2540        if (control === 'progress') {
2541          const progressContainer = createElement('div', {
2542            class: `${defaultAttributes.class} plyr__progress__container`
2543          });
2544          const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));
2545
2546          // Seek range slider
2547          progress.appendChild(createRange.call(this, 'seek', {
2548            id: `plyr-seek-${data.id}`
2549          }));
2550
2551          // Buffer progress
2552          progress.appendChild(createProgress.call(this, 'buffer'));
2553
2554          // TODO: Add loop display indicator
2555
2556          // Seek tooltip
2557          if (this.config.tooltips.seek) {
2558            const tooltip = createElement('span', {
2559              class: this.config.classNames.tooltip
2560            }, '00:00');
2561            progress.appendChild(tooltip);
2562            this.elements.display.seekTooltip = tooltip;
2563          }
2564          this.elements.progress = progress;
2565          progressContainer.appendChild(this.elements.progress);
2566          container.appendChild(progressContainer);
2567        }
2568
2569        // Media current time display
2570        if (control === 'current-time') {
2571          container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));
2572        }
2573
2574        // Media duration display
2575        if (control === 'duration') {
2576          container.appendChild(createTime.call(this, 'duration', defaultAttributes));
2577        }
2578
2579        // Volume controls
2580        if (control === 'mute' || control === 'volume') {
2581          let {
2582            volume
2583          } = this.elements;
2584
2585          // Create the volume container if needed
2586          if (!is.element(volume) || !container.contains(volume)) {
2587            volume = createElement('div', extend({}, defaultAttributes, {
2588              class: `${defaultAttributes.class} plyr__volume`.trim()
2589            }));
2590            this.elements.volume = volume;
2591            container.appendChild(volume);
2592          }
2593
2594          // Toggle mute button
2595          if (control === 'mute') {
2596            volume.appendChild(createButton.call(this, 'mute'));
2597          }
2598
2599          // Volume range control
2600          // Ignored on iOS as it's handled globally
2601          // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html
2602          if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {
2603            // Set the attributes
2604            const attributes = {
2605              max: 1,
2606              step: 0.05,
2607              value: this.config.volume
2608            };
2609
2610            // Create the volume range slider
2611            volume.appendChild(createRange.call(this, 'volume', extend(attributes, {
2612              id: `plyr-volume-${data.id}`
2613            })));
2614          }
2615        }
2616
2617        // Toggle captions button
2618        if (control === 'captions') {
2619          container.appendChild(createButton.call(this, 'captions', defaultAttributes));
2620        }
2621
2622        // Settings button / menu
2623        if (control === 'settings' && !is.empty(this.config.settings)) {
2624          const wrapper = createElement('div', extend({}, defaultAttributes, {
2625            class: `${defaultAttributes.class} plyr__menu`.trim(),
2626            hidden: ''
2627          }));
2628          wrapper.appendChild(createButton.call(this, 'settings', {
2629            'aria-haspopup': true,
2630            'aria-controls': `plyr-settings-${data.id}`,
2631            'aria-expanded': false
2632          }));
2633          const popup = createElement('div', {
2634            class: 'plyr__menu__container',
2635            id: `plyr-settings-${data.id}`,
2636            hidden: ''
2637          });
2638          const inner = createElement('div');
2639          const home = createElement('div', {
2640            id: `plyr-settings-${data.id}-home`
2641          });
2642
2643          // Create the menu
2644          const menu = createElement('div', {
2645            role: 'menu'
2646          });
2647          home.appendChild(menu);
2648          inner.appendChild(home);
2649          this.elements.settings.panels.home = home;
2650
2651          // Build the menu items
2652          this.config.settings.forEach(type => {
2653            // TODO: bundle this with the createMenuItem helper and bindings
2654            const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {
2655              
vendor: 28,614 bytes, lines 2655-3476
2655type: 'button',
2656              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,
2657              role: 'menuitem',
2658              'aria-haspopup': true,
2659              hidden: ''
2660            }));
2661
2662            // Bind menu shortcuts for keyboard users
2663            bindMenuItemShortcuts.call(this, menuItem, type);
2664
2665            // Show menu on click
2666            on.call(this, menuItem, 'click', () => {
2667              showMenuPanel.call(this, type, false);
2668            });
2669            const flex = createElement('span', null, i18n.get(type, this.config));
2670            const value = createElement('span', {
2671              class: this.config.classNames.menu.value
2672            });
2673
2674            // Speed contains HTML entities
2675            value.innerHTML = data[type];
2676            flex.appendChild(value);
2677            menuItem.appendChild(flex);
2678            menu.appendChild(menuItem);
2679
2680            // Build the panes
2681            const pane = createElement('div', {
2682              id: `plyr-settings-${data.id}-${type}`,
2683              hidden: ''
2684            });
2685
2686            // Back button
2687            const backButton = createElement('button', {
2688              type: 'button',
2689              class: `${this.config.classNames.control} ${this.config.classNames.control}--back`
2690            });
2691
2692            // Visible label
2693            backButton.appendChild(createElement('span', {
2694              'aria-hidden': true
2695            }, i18n.get(type, this.config)));
2696
2697            // Screen reader label
2698            backButton.appendChild(createElement('span', {
2699              class: this.config.classNames.hidden
2700            }, i18n.get('menuBack', this.config)));
2701
2702            // Go back via keyboard
2703            on.call(this, pane, 'keydown', event => {
2704              if (event.key !== 'ArrowLeft') return;
2705
2706              // Prevent seek
2707              event.preventDefault();
2708              event.stopPropagation();
2709
2710              // Show the respective menu
2711              showMenuPanel.call(this, 'home', true);
2712            }, false);
2713
2714            // Go back via button click
2715            on.call(this, backButton, 'click', () => {
2716              showMenuPanel.call(this, 'home', false);
2717            });
2718
2719            // Add to pane
2720            pane.appendChild(backButton);
2721
2722            // Menu
2723            pane.appendChild(createElement('div', {
2724              role: 'menu'
2725            }));
2726            inner.appendChild(pane);
2727            this.elements.settings.buttons[type] = menuItem;
2728            this.elements.settings.panels[type] = pane;
2729          });
2730          popup.appendChild(inner);
2731          wrapper.appendChild(popup);
2732          container.appendChild(wrapper);
2733          this.elements.settings.popup = popup;
2734          this.elements.settings.menu = wrapper;
2735        }
2736
2737        // Picture in picture button
2738        if (control === 'pip' && support.pip) {
2739          container.appendChild(createButton.call(this, 'pip', defaultAttributes));
2740        }
2741
2742        // Airplay button
2743        if (control === 'airplay' && support.airplay) {
2744          container.appendChild(createButton.call(this, 'airplay', defaultAttributes));
2745        }
2746
2747        // Download button
2748        if (control === 'download') {
2749          const attributes = extend({}, defaultAttributes, {
2750            element: 'a',
2751            href: this.download,
2752            target: '_blank'
2753          });
2754
2755          // Set download attribute for HTML5 only
2756          if (this.isHTML5) {
2757            attributes.download = '';
2758          }
2759          const {
2760            download
2761          } = this.config.urls;
2762          if (!is.url(download) && this.isEmbed) {
2763            extend(attributes, {
2764              icon: `logo-${this.provider}`,
2765              label: this.provider
2766            });
2767          }
2768          container.appendChild(createButton.call(this, 'download', attributes));
2769        }
2770
2771        // Toggle fullscreen button
2772        if (control === 'fullscreen') {
2773          container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));
2774        }
2775      });
2776
2777      // Set available quality levels
2778      if (this.isHTML5) {
2779        setQualityMenu.call(this, html5.getQualityOptions.call(this));
2780      }
2781      setSpeedMenu.call(this);
2782      return container;
2783    },
2784    // Insert controls
2785    inject() {
2786      // Sprite
2787      if (this.config.loadSprite) {
2788        const icon = controls.getIconUrl.call(this);
2789
2790        // Only load external sprite using AJAX
2791        if (icon.cors) {
2792          loadSprite(icon.url, 'sprite-plyr');
2793        }
2794      }
2795
2796      // Create a unique ID
2797      this.id = Math.floor(Math.random() * 10000);
2798
2799      // Null by default
2800      let container = null;
2801      this.elements.controls = null;
2802
2803      // Set template properties
2804      const props = {
2805        id: this.id,
2806        seektime: this.config.seekTime,
2807        title: this.config.title
2808      };
2809      let update = true;
2810
2811      // If function, run it and use output
2812      if (is.function(this.config.controls)) {
2813        this.config.controls = this.config.controls.call(this, props);
2814      }
2815
2816      // Convert falsy controls to empty array (primarily for empty strings)
2817      if (!this.config.controls) {
2818        this.config.controls = [];
2819      }
2820      if (is.element(this.config.controls) || is.string(this.config.controls)) {
2821        // HTMLElement or Non-empty string passed as the option
2822        container = this.config.controls;
2823      } else {
2824        // Create controls
2825        container = controls.create.call(this, {
2826          id: this.id,
2827          seektime: this.config.seekTime,
2828          speed: this.speed,
2829          quality: this.quality,
2830          captions: captions.getLabel.call(this)
2831          // TODO: Looping
2832          // loop: 'None',
2833        });
2834
2835        update = false;
2836      }
2837
2838      // Replace props with their value
2839      const replace = input => {
2840        let result = input;
2841        Object.entries(props).forEach(([key, value]) => {
2842          result = replaceAll(result, `{${key}}`, value);
2843        });
2844        return result;
2845      };
2846
2847      // Update markup
2848      if (update) {
2849        if (is.string(this.config.controls)) {
2850          container = replace(container);
2851        }
2852      }
2853
2854      // Controls container
2855      let target;
2856
2857      // Inject to custom location
2858      if (is.string(this.config.selectors.controls.container)) {
2859        target = document.querySelector(this.config.selectors.controls.container);
2860      }
2861
2862      // Inject into the container by default
2863      if (!is.element(target)) {
2864        target = this.elements.container;
2865      }
2866
2867      // Inject controls HTML (needs to be before captions, hence "afterbegin")
2868      const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';
2869      target[insertMethod]('afterbegin', container);
2870
2871      // Find the elements if need be
2872      if (!is.element(this.elements.controls)) {
2873        controls.findElements.call(this);
2874      }
2875
2876      // Add pressed property to buttons
2877      if (!is.empty(this.elements.buttons)) {
2878        const addProperty = button => {
2879          const className = this.config.classNames.controlPressed;
2880          button.setAttribute('aria-pressed', 'false');
2881          Object.defineProperty(button, 'pressed', {
2882            configurable: true,
2883            enumerable: true,
2884            get() {
2885              return hasClass(button, className);
2886            },
2887            set(pressed = false) {
2888              toggleClass(button, className, pressed);
2889              button.setAttribute('aria-pressed', pressed ? 'true' : 'false');
2890            }
2891          });
2892        };
2893
2894        // Toggle classname when pressed property is set
2895        Object.values(this.elements.buttons).filter(Boolean).forEach(button => {
2896          if (is.array(button) || is.nodeList(button)) {
2897            Array.from(button).filter(Boolean).forEach(addProperty);
2898          } else {
2899            addProperty(button);
2900          }
2901        });
2902      }
2903
2904      // Edge sometimes doesn't finish the paint so force a repaint
2905      if (browser.isEdge) {
2906        repaint(target);
2907      }
2908
2909      // Setup tooltips
2910      if (this.config.tooltips.controls) {
2911        const {
2912          classNames,
2913          selectors
2914        } = this.config;
2915        const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;
2916        const labels = getElements.call(this, selector);
2917        Array.from(labels).forEach(label => {
2918          toggleClass(label, this.config.classNames.hidden, false);
2919          toggleClass(label, this.config.classNames.tooltip, true);
2920        });
2921      }
2922    },
2923    // Set media metadata
2924    setMediaMetadata() {
2925      try {
2926        if ('mediaSession' in navigator) {
2927          navigator.mediaSession.metadata = new window.MediaMetadata({
2928            title: this.config.mediaMetadata.title,
2929            artist: this.config.mediaMetadata.artist,
2930            album: this.config.mediaMetadata.album,
2931            artwork: this.config.mediaMetadata.artwork
2932          });
2933        }
2934      } catch (_) {
2935        // Do nothing
2936      }
2937    },
2938    // Add markers
2939    setMarkers() {
2940      var _this$config$markers2, _this$config$markers3;
2941      if (!this.duration || this.elements.markers) return;
2942
2943      // Get valid points
2944      const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({
2945        time
2946      }) => time > 0 && time < this.duration);
2947      if (!(points !== null && points !== void 0 && points.length)) return;
2948      const containerFragment = document.createDocumentFragment();
2949      const pointsFragment = document.createDocumentFragment();
2950      let tipElement = null;
2951      const tipVisible = `${this.config.classNames.tooltip}--visible`;
2952      const toggleTip = show => toggleClass(tipElement, tipVisible, show);
2953
2954      // Inject markers to progress container
2955      points.forEach(point => {
2956        const markerElement = createElement('span', {
2957          class: this.config.classNames.marker
2958        }, '');
2959        const left = `${point.time / this.duration * 100}%`;
2960        if (tipElement) {
2961          // Show on hover
2962          markerElement.addEventListener('mouseenter', () => {
2963            if (point.label) return;
2964            tipElement.style.left = left;
2965            tipElement.innerHTML = point.label;
2966            toggleTip(true);
2967          });
2968
2969          // Hide on leave
2970          markerElement.addEventListener('mouseleave', () => {
2971            toggleTip(false);
2972          });
2973        }
2974        markerElement.addEventListener('click', () => {
2975          this.currentTime = point.time;
2976        });
2977        markerElement.style.left = left;
2978        pointsFragment.appendChild(markerElement);
2979      });
2980      containerFragment.appendChild(pointsFragment);
2981
2982      // Inject a tooltip if needed
2983      if (!this.config.tooltips.seek) {
2984        tipElement = createElement('span', {
2985          class: this.config.classNames.tooltip
2986        }, '');
2987        containerFragment.appendChild(tipElement);
2988      }
2989      this.elements.markers = {
2990        points: pointsFragment,
2991        tip: tipElement
2992      };
2993      this.elements.progress.appendChild(containerFragment);
2994    }
2995  };
2996
2997  // ==========================================================================
2998
2999  /**
3000   * Parse a string to a URL object
3001   * @param {String} input - the URL to be parsed
3002   * @param {Boolean} safe - failsafe parsing
3003   */
3004  function parseUrl(input, safe = true) {
3005    let url = input;
3006    if (safe) {
3007      const parser = document.createElement('a');
3008      parser.href = url;
3009      url = parser.href;
3010    }
3011    try {
3012      return new URL(url);
3013    } catch (_) {
3014      return null;
3015    }
3016  }
3017
3018  // Convert object to URLSearchParams
3019  function buildUrlParams(input) {
3020    const params = new URLSearchParams();
3021    if (is.object(input)) {
3022      Object.entries(input).forEach(([key, value]) => {
3023        params.set(key, value);
3024      });
3025    }
3026    return params;
3027  }
3028
3029  // ==========================================================================
3030  const captions = {
3031    // Setup captions
3032    setup() {
3033      // Requires UI support
3034      if (!this.supported.ui) {
3035        return;
3036      }
3037
3038      // Only Vimeo and HTML5 video supported at this point
3039      if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {
3040        // Clear menu and hide
3041        if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
3042          controls.setCaptionsMenu.call(this);
3043        }
3044        return;
3045      }
3046
3047      // Inject the container
3048      if (!is.element(this.elements.captions)) {
3049        this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
3050        this.elements.captions.setAttribute('dir', 'auto');
3051        insertAfter(this.elements.captions, this.elements.wrapper);
3052      }
3053
3054      // Fix IE captions if CORS is used
3055      // Fetch captions and inject as blobs instead (data URIs not supported!)
3056      if (browser.isIE && window.URL) {
3057        const elements = this.media.querySelectorAll('track');
3058        Array.from(elements).forEach(track => {
3059          const src = track.getAttribute('src');
3060          const url = parseUrl(src);
3061          if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {
3062            fetch(src, 'blob').then(blob => {
3063              track.setAttribute('src', window.URL.createObjectURL(blob));
3064            }).catch(() => {
3065              removeElement(track);
3066            });
3067          }
3068        });
3069      }
3070
3071      // Get and set initial data
3072      // The "preferred" options are not realized unless / until the wanted language has a match
3073      // * languages: Array of user's browser languages.
3074      // * language:  The language preferred by user settings or config
3075      // * active:    The state preferred by user settings or config
3076      // * toggled:   The real captions state
3077
3078      const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
3079      const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));
3080      let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();
3081
3082      // Use first browser language when language is 'auto'
3083      if (language === 'auto') {
3084        [language] = languages;
3085      }
3086      let active = this.storage.get('captions');
3087      if (!is.boolean(active)) {
3088        ({
3089          active
3090        } = this.config.captions);
3091      }
3092      Object.assign(this.captions, {
3093        toggled: false,
3094        active,
3095        language,
3096        languages
3097      });
3098
3099      // Watch changes to textTracks and update captions menu
3100      if (this.isHTML5) {
3101        const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
3102        on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
3103      }
3104
3105      // Update available languages in list next tick (the event must not be triggered before the listeners)
3106      setTimeout(captions.update.bind(this), 0);
3107    },
3108    // Update available language options in settings based on tracks
3109    update() {
3110      const tracks = captions.getTracks.call(this, true);
3111      // Get the wanted language
3112      const {
3113        active,
3114        language,
3115        meta,
3116        currentTrackNode
3117      } = this.captions;
3118      const languageExists = Boolean(tracks.find(track => track.language === language));
3119
3120      // Handle tracks (add event listener and "pseudo"-default)
3121      if (this.isHTML5 && this.isVideo) {
3122        tracks.filter(track => !meta.get(track)).forEach(track => {
3123          this.debug.log('Track added', track);
3124
3125          // Attempt to store if the original dom element was "default"
3126          meta.set(track, {
3127            default: track.mode === 'showing'
3128          });
3129
3130          // Turn off native caption rendering to avoid double captions
3131          // Note: mode='hidden' forces a track to download. To ensure every track
3132          // isn't downloaded at once, only 'showing' tracks should be reassigned
3133          // eslint-disable-next-line no-param-reassign
3134          if (track.mode === 'showing') {
3135            // eslint-disable-next-line no-param-reassign
3136            track.mode = 'hidden';
3137          }
3138
3139          // Add event listener for cue changes
3140          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));
3141        });
3142      }
3143
3144      // Update language first time it matches, or if the previous matching track was removed
3145      if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {
3146        captions.setLanguage.call(this, language);
3147        captions.toggle.call(this, active && languageExists);
3148      }
3149
3150      // Enable or disable captions based on track length
3151      if (this.elements) {
3152        toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));
3153      }
3154
3155      // Update available languages in list
3156      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
3157        controls.setCaptionsMenu.call(this);
3158      }
3159    },
3160    // Toggle captions display
3161    // Used internally for the toggleCaptions method, with the passive option forced to false
3162    toggle(input, passive = true) {
3163      // If there's no full support
3164      if (!this.supported.ui) {
3165        return;
3166      }
3167      const {
3168        toggled
3169      } = this.captions; // Current state
3170      const activeClass = this.config.classNames.captions.active;
3171      // Get the next state
3172      // If the method is called without parameter, toggle based on current value
3173      const active = is.nullOrUndefined(input) ? !toggled : input;
3174
3175      // Update state and trigger event
3176      if (active !== toggled) {
3177        // When passive, don't override user preferences
3178        if (!passive) {
3179          this.captions.active = active;
3180          this.storage.set({
3181            captions: active
3182          });
3183        }
3184
3185        // Force language if the call isn't passive and there is no matching language to toggle to
3186        if (!this.language && active && !passive) {
3187          const tracks = captions.getTracks.call(this);
3188          const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);
3189
3190          // Override user preferences to avoid switching languages if a matching track is added
3191          this.captions.language = track.language;
3192
3193          // Set caption, but don't store in localStorage as user preference
3194          captions.set.call(this, tracks.indexOf(track));
3195          return;
3196        }
3197
3198        // Toggle button if it's enabled
3199        if (this.elements.buttons.captions) {
3200          this.elements.buttons.captions.pressed = active;
3201        }
3202
3203        // Add class hook
3204        toggleClass(this.elements.container, activeClass, active);
3205        this.captions.toggled = active;
3206
3207        // Update settings menu
3208        controls.updateSetting.call(this, 'captions');
3209
3210        // Trigger event (not used internally)
3211        triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
3212      }
3213
3214      // Wait for the call stack to clear before setting mode='hidden'
3215      // on the active track - forcing the browser to download it
3216      setTimeout(() => {
3217        if (active && this.captions.toggled) {
3218          this.captions.currentTrackNode.mode = 'hidden';
3219        }
3220      });
3221    },
3222    // Set captions by track index
3223    // Used internally for the currentTrack setter with the passive option forced to false
3224    set(index, passive = true) {
3225      const tracks = captions.getTracks.call(this);
3226
3227      // Disable captions if setting to -1
3228      if (index === -1) {
3229        captions.toggle.call(this, false, passive);
3230        return;
3231      }
3232      if (!is.number(index)) {
3233        this.debug.warn('Invalid caption argument', index);
3234        return;
3235      }
3236      if (!(index in tracks)) {
3237        this.debug.warn('Track not found', index);
3238        return;
3239      }
3240      if (this.captions.currentTrack !== index) {
3241        this.captions.currentTrack = index;
3242        const track = tracks[index];
3243        const {
3244          language
3245        } = track || {};
3246
3247        // Store reference to node for invalidation on remove
3248        this.captions.currentTrackNode = track;
3249
3250        // Update settings menu
3251        controls.updateSetting.call(this, 'captions');
3252
3253        // When passive, don't override user preferences
3254        if (!passive) {
3255          this.captions.language = language;
3256          this.storage.set({
3257            language
3258          });
3259        }
3260
3261        // Handle Vimeo captions
3262        if (this.isVimeo) {
3263          this.embed.enableTextTrack(language);
3264        }
3265
3266        // Trigger event
3267        triggerEvent.call(this, this.media, 'languagechange');
3268      }
3269
3270      // Show captions
3271      captions.toggle.call(this, true, passive);
3272      if (this.isHTML5 && this.isVideo) {
3273        // If we change the active track while a cue is already displayed we need to update it
3274        captions.updateCues.call(this);
3275      }
3276    },
3277    // Set captions by language
3278    // Used internally for the language setter with the passive option forced to false
3279    setLanguage(input, passive = true) {
3280      if (!is.string(input)) {
3281        this.debug.warn('Invalid language argument', input);
3282        return;
3283      }
3284      // Normalize
3285      const language = input.toLowerCase();
3286      this.captions.language = language;
3287
3288      // Set currentTrack
3289      const tracks = captions.getTracks.call(this);
3290      const track = captions.findTrack.call(this, [language]);
3291      captions.set.call(this, tracks.indexOf(track), passive);
3292    },
3293    // Get current valid caption tracks
3294    // If update is false it will also ignore tracks without metadata
3295    // This is used to "freeze" the language options when captions.update is false
3296    getTracks(update = false) {
3297      // Handle media or textTracks missing or null
3298      const tracks = Array.from((this.media || {}).textTracks || []);
3299      // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
3300      // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
3301      return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));
3302    },
3303    // Match tracks based on languages and get the first
3304    findTrack(languages, force = false) {
3305      const tracks = captions.getTracks.call(this);
3306      const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);
3307      const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));
3308      let track;
3309      languages.every(language => {
3310        track = sorted.find(t => t.language === language);
3311        return !track; // Break iteration if there is a match
3312      });
3313
3314      // If no match is found but is required, get first
3315      return track || (force ? sorted[0] : undefined);
3316    },
3317    // Get the current track
3318    getCurrentTrack() {
3319      return captions.getTracks.call(this)[this.currentTrack];
3320    },
3321    // Get UI label for track
3322    getLabel(track) {
3323      let currentTrack = track;
3324      if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
3325        currentTrack = captions.getCurrentTrack.call(this);
3326      }
3327      if (is.track(currentTrack)) {
3328        if (!is.empty(currentTrack.label)) {
3329          return currentTrack.label;
3330        }
3331        if (!is.empty(currentTrack.language)) {
3332          return track.language.toUpperCase();
3333        }
3334        return i18n.get('enabled', this.config);
3335      }
3336      return i18n.get('disabled', this.config);
3337    },
3338    // Update captions using current track's active cues
3339    // Also optional array argument in case there isn't any track (ex: vimeo)
3340    updateCues(input) {
3341      // Requires UI
3342      if (!this.supported.ui) {
3343        return;
3344      }
3345      if (!is.element(this.elements.captions)) {
3346        this.debug.warn('No captions element to render to');
3347        return;
3348      }
3349
3350      // Only accept array or empty input
3351      if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
3352        this.debug.warn('updateCues: Invalid input', input);
3353        return;
3354      }
3355      let cues = input;
3356
3357      // Get cues from track
3358      if (!cues) {
3359        const track = captions.getCurrentTrack.call(this);
3360        cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);
3361      }
3362
3363      // Set new caption text
3364      const content = cues.map(cueText => cueText.trim()).join('\n');
3365      const changed = content !== this.elements.captions.innerHTML;
3366      if (changed) {
3367        // Empty the container and create a new child element
3368        emptyElement(this.elements.captions);
3369        const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
3370        caption.innerHTML = content;
3371        this.elements.captions.appendChild(caption);
3372
3373        // Trigger event
3374        triggerEvent.call(this, this.media, 'cuechange');
3375      }
3376    }
3377  };
3378
3379  // ==========================================================================
3380  // Plyr default config
3381  // ==========================================================================
3382
3383  const defaults = {
3384    // Disable
3385    enabled: true,
3386    // Custom media title
3387    title: '',
3388    // Logging to console
3389    debug: false,
3390    // Auto play (if supported)
3391    autoplay: false,
3392    // Only allow one media playing at once (vimeo only)
3393    autopause: true,
3394    // Allow inline playback on iOS
3395    playsinline: true,
3396    // Default time to skip when rewind/fast forward
3397    seekTime: 10,
3398    // Default volume
3399    volume: 1,
3400    muted: false,
3401    // Pass a custom duration
3402    duration: null,
3403    // Display the media duration on load in the current time position
3404    // If you have opted to display both duration and currentTime, this is ignored
3405    displayDuration: true,
3406    // Invert the current time to be a countdown
3407    invertTime: true,
3408    // Clicking the currentTime inverts it's value to show time left rather than elapsed
3409    toggleInvert: true,
3410    // Force an aspect ratio
3411    // The format must be `'w:h'` (e.g. `'16:9'`)
3412    ratio: null,
3413    // Click video container to play/pause
3414    clickToPlay: true,
3415    // Auto hide the controls
3416    hideControls: true,
3417    // Reset to start when playback ended
3418    resetOnEnd: false,
3419    // Disable the standard context menu
3420    disableContextMenu: true,
3421    // Sprite (for icons)
3422    loadSprite: true,
3423    iconPrefix: 'plyr',
3424    iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',
3425    // Blank video (used to prevent errors on source change)
3426    blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
3427    // Quality default
3428    quality: {
3429      default: 576,
3430      // The options to display in the UI, if available for the source media
3431      options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],
3432      forced: false,
3433      onChange: null
3434    },
3435    // Set loops
3436    loop: {
3437      active: false
3438      // start: null,
3439      // end: null,
3440    },
3441
3442    // Speed default and options to display
3443    speed: {
3444      selected: 1,
3445      // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)
3446      options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]
3447    },
3448    // Keyboard shortcut settings
3449    keyboard: {
3450      focused: true,
3451      global: false
3452    },
3453    // Display tooltips
3454    tooltips: {
3455      controls: false,
3456      seek: true
3457    },
3458    // Captions settings
3459    captions: {
3460      active: false,
3461      language: 'auto',
3462      // Listen to new tracks added after Plyr is initialized.
3463      // This is needed for streaming captions, but may result in unselectable options
3464      update: false
3465    },
3466    // Fullscreen settings
3467    fullscreen: {
3468      enabled: true,
3469      // Allow fullscreen?
3470      fallback: true,
3471      // Fallback using full viewport/window
3472      iosNative: false // Use the native fullscreen in iOS (disables custom controls)
3473      // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode
3474      // Non-ancestors of the player element will be ignored
3475      // container: null, // defaults to the player element
3476    },
vendor: 7,248 bytes, lines 3477-3713
3477
3478    // Local storage
3479    storage: {
3480      enabled: true,
3481      key: 'plyr'
3482    },
3483    // Default controls
3484    controls: ['play-large',
3485    // 'restart',
3486    // 'rewind',
3487    'play',
3488    // 'fast-forward',
3489    'progress', 'current-time',
3490    // 'duration',
3491    'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',
3492    // 'download',
3493    'fullscreen'],
3494    settings: ['captions', 'quality', 'speed'],
3495    // Localisation
3496    i18n: {
3497      restart: 'Restart',
3498      rewind: 'Rewind {seektime}s',
3499      play: 'Play',
3500      pause: 'Pause',
3501      fastForward: 'Forward {seektime}s',
3502      seek: 'Seek',
3503      seekLabel: '{currentTime} of {duration}',
3504      played: 'Played',
3505      buffered: 'Buffered',
3506      currentTime: 'Current time',
3507      duration: 'Duration',
3508      volume: 'Volume',
3509      mute: 'Mute',
3510      unmute: 'Unmute',
3511      enableCaptions: 'Enable captions',
3512      disableCaptions: 'Disable captions',
3513      download: 'Download',
3514      enterFullscreen: 'Enter fullscreen',
3515      exitFullscreen: 'Exit fullscreen',
3516      frameTitle: 'Player for {title}',
3517      captions: 'Captions',
3518      settings: 'Settings',
3519      pip: 'PIP',
3520      menuBack: 'Go back to previous menu',
3521      speed: 'Speed',
3522      normal: 'Normal',
3523      quality: 'Quality',
3524      loop: 'Loop',
3525      start: 'Start',
3526      end: 'End',
3527      all: 'All',
3528      reset: 'Reset',
3529      disabled: 'Disabled',
3530      enabled: 'Enabled',
3531      advertisement: 'Ad',
3532      qualityBadge: {
3533        2160: '4K',
3534        1440: 'HD',
3535        1080: 'HD',
3536        720: 'HD',
3537        576: 'SD',
3538        480: 'SD'
3539      }
3540    },
3541    // URLs
3542    urls: {
3543      download: null,
3544      vimeo: {
3545        sdk: 'https://player.vimeo.com/api/player.js',
3546        iframe: 'https://player.vimeo.com/video/{0}?{1}',
3547        api: 'https://vimeo.com/api/oembed.json?url={0}'
3548      },
3549      youtube: {
3550        sdk: 'https://www.youtube.com/iframe_api',
3551        api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'
3552      },
3553      googleIMA: {
3554        sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'
3555      }
3556    },
3557    // Custom control listeners
3558    listeners: {
3559      seek: null,
3560      play: null,
3561      pause: null,
3562      restart: null,
3563      rewind: null,
3564      fastForward: null,
3565      mute: null,
3566      volume: null,
3567      captions: null,
3568      download: null,
3569      fullscreen: null,
3570      pip: null,
3571      airplay: null,
3572      speed: null,
3573      quality: null,
3574      loop: null,
3575      language: null
3576    },
3577    // Events to watch and bubble
3578    events: [
3579    // Events to watch on HTML5 media elements and bubble
3580    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
3581    'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',
3582    // Custom events
3583    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',
3584    // YouTube
3585    'statechange',
3586    // Quality
3587    'qualitychange',
3588    // Ads
3589    'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
3590    // Selectors
3591    // Change these to match your template if using custom HTML
3592    selectors: {
3593      editable: 'input, textarea, select, [contenteditable]',
3594      container: '.plyr',
3595      controls: {
3596        container: null,
3597        wrapper: '.plyr__controls'
3598      },
3599      labels: '[data-plyr]',
3600      buttons: {
3601        play: '[data-plyr="play"]',
3602        pause: '[data-plyr="pause"]',
3603        restart: '[data-plyr="restart"]',
3604        rewind: '[data-plyr="rewind"]',
3605        fastForward: '[data-plyr="fast-forward"]',
3606        mute: '[data-plyr="mute"]',
3607        captions: '[data-plyr="captions"]',
3608        download: '[data-plyr="download"]',
3609        fullscreen: '[data-plyr="fullscreen"]',
3610        pip: '[data-plyr="pip"]',
3611        airplay: '[data-plyr="airplay"]',
3612        settings: '[data-plyr="settings"]',
3613        loop: '[data-plyr="loop"]'
3614      },
3615      inputs: {
3616        seek: '[data-plyr="seek"]',
3617        volume: '[data-plyr="volume"]',
3618        speed: '[data-plyr="speed"]',
3619        language: '[data-plyr="language"]',
3620        quality: '[data-plyr="quality"]'
3621      },
3622      display: {
3623        currentTime: '.plyr__time--current',
3624        duration: '.plyr__time--duration',
3625        buffer: '.plyr__progress__buffer',
3626        loop: '.plyr__progress__loop',
3627        // Used later
3628        volume: '.plyr__volume--display'
3629      },
3630      progress: '.plyr__progress',
3631      captions: '.plyr__captions',
3632      caption: '.plyr__caption'
3633    },
3634    // Class hooks added to the player in different states
3635    classNames: {
3636      type: 'plyr--{0}',
3637      provider: 'plyr--{0}',
3638      video: 'plyr__video-wrapper',
3639      embed: 'plyr__video-embed',
3640      videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',
3641      embedContainer: 'plyr__video-embed__container',
3642      poster: 'plyr__poster',
3643      posterEnabled: 'plyr__poster-enabled',
3644      ads: 'plyr__ads',
3645      control: 'plyr__control',
3646      controlPressed: 'plyr__control--pressed',
3647      playing: 'plyr--playing',
3648      paused: 'plyr--paused',
3649      stopped: 'plyr--stopped',
3650      loading: 'plyr--loading',
3651      hover: 'plyr--hover',
3652      tooltip: 'plyr__tooltip',
3653      cues: 'plyr__cues',
3654      marker: 'plyr__progress__marker',
3655      hidden: 'plyr__sr-only',
3656      hideControls: 'plyr--hide-controls',
3657      isTouch: 'plyr--is-touch',
3658      uiSupported: 'plyr--full-ui',
3659      noTransition: 'plyr--no-transition',
3660      display: {
3661        time: 'plyr__time'
3662      },
3663      menu: {
3664        value: 'plyr__menu__value',
3665        badge: 'plyr__badge',
3666        open: 'plyr--menu-open'
3667      },
3668      captions: {
3669        enabled: 'plyr--captions-enabled',
3670        active: 'plyr--captions-active'
3671      },
3672      fullscreen: {
3673        enabled: 'plyr--fullscreen-enabled',
3674        fallback: 'plyr--fullscreen-fallback'
3675      },
3676      pip: {
3677        supported: 'plyr--pip-supported',
3678        active: 'plyr--pip-active'
3679      },
3680      airplay: {
3681        supported: 'plyr--airplay-supported',
3682        active: 'plyr--airplay-active'
3683      },
3684      previewThumbnails: {
3685        // Tooltip thumbs
3686        thumbContainer: 'plyr__preview-thumb',
3687        thumbContainerShown: 'plyr__preview-thumb--is-shown',
3688        imageContainer: 'plyr__preview-thumb__image-container',
3689        timeContainer: 'plyr__preview-thumb__time-container',
3690        // Scrubbing
3691        scrubbingContainer: 'plyr__preview-scrubbing',
3692        scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'
3693      }
3694    },
3695    // Embed attributes
3696    attributes: {
3697      embed: {
3698        provider: 'data-plyr-provider',
3699        id: 'data-plyr-embed-id',
3700        hash: 'data-plyr-embed-hash'
3701      }
3702    },
3703    // Advertisements plugin
3704    // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
3705    ads: {
3706      enabled: false,
3707      publisherId: '',
3708      tagUrl: ''
3709    },
3710    // Preview Thumbnails plugin
3711    previewThumbnails: {
3712      enabled: false,
3713      src: ''
vendor: 3,931 bytes, lines 3713-3846
3713
3714    },
3715    // Vimeo plugin
3716    vimeo: {
3717      byline: false,
3718      portrait: false,
3719      title: false,
3720      speed: true,
3721      transparent: false,
3722      // Custom settings from Plyr
3723      customControls: true,
3724      referrerPolicy: null,
3725      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
3726      // Whether the owner of the video has a Pro or Business account
3727      // (which allows us to properly hide controls without CSS hacks, etc)
3728      premium: false
3729    },
3730    // YouTube plugin
3731    youtube: {
3732      rel: 0,
3733      // No related vids
3734      showinfo: 0,
3735      // Hide info
3736      iv_load_policy: 3,
3737      // Hide annotations
3738      modestbranding: 1,
3739      // Hide logos as much as possible (they still show one in the corner when paused)
3740      // Custom settings from Plyr
3741      customControls: true,
3742      noCookie: false // Whether to use an alternative version of YouTube without cookies
3743    },
3744
3745    // Media Metadata
3746    mediaMetadata: {
3747      title: '',
3748      artist: '',
3749      album: '',
3750      artwork: []
3751    },
3752    // Markers
3753    markers: {
3754      enabled: false,
3755      points: []
3756    }
3757  };
3758
3759  // ==========================================================================
3760  // Plyr states
3761  // ==========================================================================
3762
3763  const pip = {
3764    active: 'picture-in-picture',
3765    inactive: 'inline'
3766  };
3767
3768  // ==========================================================================
3769  // Plyr supported types and providers
3770  // ==========================================================================
3771
3772  const providers = {
3773    html5: 'html5',
3774    youtube: 'youtube',
3775    vimeo: 'vimeo'
3776  };
3777  const types = {
3778    audio: 'audio',
3779    video: 'video'
3780  };
3781
3782  /**
3783   * Get provider by URL
3784   * @param {String} url
3785   */
3786  function getProviderByUrl(url) {
3787    // YouTube
3788    if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
3789      return providers.youtube;
3790    }
3791
3792    // Vimeo
3793    if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
3794      return providers.vimeo;
3795    }
3796    return null;
3797  }
3798
3799  // ==========================================================================
3800  // Console wrapper
3801  // ==========================================================================
3802
3803  const noop = () => {};
3804  class Console {
3805    constructor(enabled = false) {
3806      this.enabled = window.console && enabled;
3807      if (this.enabled) {
3808        this.log('Debugging enabled');
3809      }
3810    }
3811    get log() {
3812      // eslint-disable-next-line no-console
3813      return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
3814    }
3815    get warn() {
3816      // eslint-disable-next-line no-console
3817      return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
3818    }
3819    get error() {
3820      // eslint-disable-next-line no-console
3821      return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
3822    }
3823  }
3824
3825  class Fullscreen {
3826    constructor(player) {
3827      _defineProperty$1(this, "onChange", () => {
3828        if (!this.supported) return;
3829
3830        // Update toggle button
3831        const button = this.player.elements.buttons.fullscreen;
3832        if (is.element(button)) {
3833          button.pressed = this.active;
3834        }
3835
3836        // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
3837        const target = this.target === this.player.media ? this.target : this.player.elements.container;
3838        // Trigger an event
3839        triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
3840      });
3841      _defineProperty$1(this, "toggleFallback", (toggle = false) => {
3842        // Store or restore scroll position
3843        if (toggle) {
3844          this.scrollPosition = {
3845            x: window.scrollX ?? 0,
3846            y: window.scrollY ??
vendor: 8,437 bytes, lines 3846-4063
3846 0
3847          };
3848        } else {
3849          window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
3850        }
3851
3852        // Toggle scroll
3853        document.body.style.overflow = toggle ? 'hidden' : '';
3854
3855        // Toggle class hook
3856        toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
3857
3858        // Force full viewport on iPhone X+
3859        if (browser.isIos) {
3860          let viewport = document.head.querySelector('meta[name="viewport"]');
3861          const property = 'viewport-fit=cover';
3862
3863          // Inject the viewport meta if required
3864          if (!viewport) {
3865            viewport = document.createElement('meta');
3866            viewport.setAttribute('name', 'viewport');
3867          }
3868
3869          // Check if the property already exists
3870          const hasProperty = is.string(viewport.content) && viewport.content.includes(property);
3871          if (toggle) {
3872            this.cleanupViewport = !hasProperty;
3873            if (!hasProperty) viewport.content += `,${property}`;
3874          } else if (this.cleanupViewport) {
3875            viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');
3876          }
3877        }
3878
3879        // Toggle button and fire events
3880        this.onChange();
3881      });
3882      // Trap focus inside container
3883      _defineProperty$1(this, "trapFocus", event => {
3884        // Bail if iOS/iPadOS, not active, not the tab key
3885        if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;
3886
3887        // Get the current focused element
3888        const focused = document.activeElement;
3889        const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');
3890        const [first] = focusable;
3891        const last = focusable[focusable.length - 1];
3892        if (focused === last && !event.shiftKey) {
3893          // Move focus to first element that can be tabbed if Shift isn't used
3894          first.focus();
3895          event.preventDefault();
3896        } else if (focused === first && event.shiftKey) {
3897          // Move focus to last element that can be tabbed if Shift is used
3898          last.focus();
3899          event.preventDefault();
3900        }
3901      });
3902      // Update UI
3903      _defineProperty$1(this, "update", () => {
3904        if (this.supported) {
3905          let mode;
3906          if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';
3907          this.player.debug.log(`${mode} fullscreen enabled`);
3908        } else {
3909          this.player.debug.log('Fullscreen not supported and fallback disabled');
3910        }
3911
3912        // Add styling hook to show button
3913        toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);
3914      });
3915      // Make an element fullscreen
3916      _defineProperty$1(this, "enter", () => {
3917        if (!this.supported) return;
3918
3919        // iOS native fullscreen doesn't need the request step
3920        if (browser.isIos && this.player.config.fullscreen.iosNative) {
3921          if (this.player.isVimeo) {
3922            this.player.embed.requestFullscreen();
3923          } else {
3924            this.target.webkitEnterFullscreen();
3925          }
3926        } else if (!Fullscreen.nativeSupported || this.forceFallback) {
3927          this.toggleFallback(true);
3928        } else if (!this.prefix) {
3929          this.target.requestFullscreen({
3930            navigationUI: 'hide'
3931          });
3932        } else if (!is.empty(this.prefix)) {
3933          this.target[`${this.prefix}Request${this.property}`]();
3934        }
3935      });
3936      // Bail from fullscreen
3937      _defineProperty$1(this, "exit", () => {
3938        if (!this.supported) return;
3939
3940        // iOS native fullscreen
3941        if (browser.isIos && this.player.config.fullscreen.iosNative) {
3942          if (this.player.isVimeo) {
3943            this.player.embed.exitFullscreen();
3944          } else {
3945            this.target.webkitEnterFullscreen();
3946          }
3947          silencePromise(this.player.play());
3948        } else if (!Fullscreen.nativeSupported || this.forceFallback) {
3949          this.toggleFallback(false);
3950        } else if (!this.prefix) {
3951          (document.cancelFullScreen || document.exitFullscreen).call(document);
3952        } else if (!is.empty(this.prefix)) {
3953          const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
3954          document[`${this.prefix}${action}${this.property}`]();
3955        }
3956      });
3957      // Toggle state
3958      _defineProperty$1(this, "toggle", () => {
3959        if (!this.active) this.enter();else this.exit();
3960      });
3961      // Keep reference to parent
3962      this.player = player;
3963
3964      // Get prefix
3965      this.prefix = Fullscreen.prefix;
3966      this.property = Fullscreen.property;
3967
3968      // Scroll position
3969      this.scrollPosition = {
3970        x: 0,
3971        y: 0
3972      };
3973
3974      // Force the use of 'full window/browser' rather than fullscreen
3975      this.forceFallback = player.config.fullscreen.fallback === 'force';
3976
3977      // Get the fullscreen element
3978      // Checks container is an ancestor, defaults to null
3979      this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);
3980
3981      // Register event listeners
3982      // Handle event (incase user presses escape etc)
3983      on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {
3984        // TODO: Filter for target??
3985        this.onChange();
3986      });
3987
3988      // Fullscreen toggle on double click
3989      on.call(this.player, this.player.elements.container, 'dblclick', event => {
3990        // Ignore double click in controls
3991        if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {
3992          return;
3993        }
3994        this.player.listeners.proxy(event, this.toggle, 'fullscreen');
3995      });
3996
3997      // Tap focus when in fullscreen
3998      on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));
3999
4000      // Update the UI
4001      this.update();
4002    }
4003
4004    // Determine if native supported
4005    static get nativeSupported() {
4006      return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
4007    }
4008
4009    // If we're actually using native
4010    get useNative() {
4011      return Fullscreen.nativeSupported && !this.forceFallback;
4012    }
4013
4014    // Get the prefix for handlers
4015    static get prefix() {
4016      // No prefix
4017      if (is.function(document.exitFullscreen)) return '';
4018
4019      // Check for fullscreen support by vendor prefix
4020      let value = '';
4021      const prefixes = ['webkit', 'moz', 'ms'];
4022      prefixes.some(pre => {
4023        if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {
4024          value = pre;
4025          return true;
4026        }
4027        return false;
4028      });
4029      return value;
4030    }
4031    static get property() {
4032      return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
4033    }
4034
4035    // Determine if fullscreen is supported
4036    get supported() {
4037      return [
4038      // Fullscreen is enabled in config
4039      this.player.config.fullscreen.enabled,
4040      // Must be a video
4041      this.player.isVideo,
4042      // Either native is supported or fallback enabled
4043      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,
4044      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline
4045      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback
4046      !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);
4047    }
4048
4049    // Get active state
4050    get active() {
4051      if (!this.supported) return false;
4052
4053      // Fallback using classname
4054      if (!Fullscreen.nativeSupported || this.forceFallback) {
4055        return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
4056      }
4057      const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];
4058      return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;
4059    }
4060
4061    // Get target element
4062    get target() {
4063      
4063return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;
4064    }
4065  }
4066
4067  // ==========================================================================
4068  // Load image avoiding xhr/fetch CORS issues
4069  // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
4070  // By default it checks if it is at least 1px, but you can add a second argument to change this
4071  // ==========================================================================
4072
4073  function loadImage(src, minWidth = 1) {
4074    return new Promise((resolve, reject) => {
4075      const image = new Image();
4076      const handler = () => {
4077        delete image.onload;
4078        delete image.onerror;
4079        (image.naturalWidth >= minWidth ? resolve : reject)(image);
4080      };
4081      Object.assign(image, {
4082        onload: handler,
4083        onerror: handler,
4084        src
4085      });
4086    });
4087  }
4088
4089  // ==========================================================================
vendor: 22,672 bytes, lines 4089-4748
4089
4090  const ui = {
4091    addStyleHook() {
4092      toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
4093      toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
4094    },
4095    // Toggle native HTML5 media controls
4096    toggleNativeControls(toggle = false) {
4097      if (toggle && this.isHTML5) {
4098        this.media.setAttribute('controls', '');
4099      } else {
4100        this.media.removeAttribute('controls');
4101      }
4102    },
4103    // Setup the UI
4104    build() {
4105      // Re-attach media element listeners
4106      // TODO: Use event bubbling?
4107      this.listeners.media();
4108
4109      // Don't setup interface if no support
4110      if (!this.supported.ui) {
4111        this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);
4112
4113        // Restore native controls
4114        ui.toggleNativeControls.call(this, true);
4115
4116        // Bail
4117        return;
4118      }
4119
4120      // Inject custom controls if not present
4121      if (!is.element(this.elements.controls)) {
4122        // Inject custom controls
4123        controls.inject.call(this);
4124
4125        // Re-attach control listeners
4126        this.listeners.controls();
4127      }
4128
4129      // Remove native controls
4130      ui.toggleNativeControls.call(this);
4131
4132      // Setup captions for HTML5
4133      if (this.isHTML5) {
4134        captions.setup.call(this);
4135      }
4136
4137      // Reset volume
4138      this.volume = null;
4139
4140      // Reset mute state
4141      this.muted = null;
4142
4143      // Reset loop state
4144      this.loop = null;
4145
4146      // Reset quality setting
4147      this.quality = null;
4148
4149      // Reset speed
4150      this.speed = null;
4151
4152      // Reset volume display
4153      controls.updateVolume.call(this);
4154
4155      // Reset time display
4156      controls.timeUpdate.call(this);
4157
4158      // Reset duration display
4159      controls.durationUpdate.call(this);
4160
4161      // Update the UI
4162      ui.checkPlaying.call(this);
4163
4164      // Check for picture-in-picture support
4165      toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
4166
4167      // Check for airplay support
4168      toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
4169
4170      // Add touch class
4171      toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
4172
4173      // Ready for API calls
4174      this.ready = true;
4175
4176      // Ready event at end of execution stack
4177      setTimeout(() => {
4178        triggerEvent.call(this, this.media, 'ready');
4179      }, 0);
4180
4181      // Set the title
4182      ui.setTitle.call(this);
4183
4184      // Assure the poster image is set, if the property was added before the element was created
4185      if (this.poster) {
4186        ui.setPoster.call(this, this.poster, false).catch(() => {});
4187      }
4188
4189      // Manually set the duration if user has overridden it.
4190      // The event listeners for it doesn't get called if preload is disabled (#701)
4191      if (this.config.duration) {
4192        controls.durationUpdate.call(this);
4193      }
4194
4195      // Media metadata
4196      if (this.config.mediaMetadata) {
4197        controls.setMediaMetadata.call(this);
4198      }
4199    },
4200    // Setup aria attribute for play and iframe title
4201    setTitle() {
4202      // Find the current text
4203      let label = i18n.get('play', this.config);
4204
4205      // If there's a media title set, use that for the label
4206      if (is.string(this.config.title) && !is.empty(this.config.title)) {
4207        label += `, ${this.config.title}`;
4208      }
4209
4210      // If there's a play button, set label
4211      Array.from(this.elements.buttons.play || []).forEach(button => {
4212        button.setAttribute('aria-label', label);
4213      });
4214
4215      // Set iframe title
4216      // https://github.com/sampotts/plyr/issues/124
4217      if (this.isEmbed) {
4218        const iframe = getElement.call(this, 'iframe');
4219        if (!is.element(iframe)) {
4220          return;
4221        }
4222
4223        // Default to media type
4224        const title = !is.empty(this.config.title) ? this.config.title : 'video';
4225        const format = i18n.get('frameTitle', this.config);
4226        iframe.setAttribute('title', format.replace('{title}', title));
4227      }
4228    },
4229    // Toggle poster
4230    togglePoster(enable) {
4231      toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
4232    },
4233    // Set the poster image (async)
4234    // Used internally for the poster setter, with the passive option forced to false
4235    setPoster(poster, passive = true) {
4236      // Don't override if call is passive
4237      if (passive && this.poster) {
4238        return Promise.reject(new Error('Poster already set'));
4239      }
4240
4241      // Set property synchronously to respect the call order
4242      this.media.setAttribute('data-poster', poster);
4243
4244      // Show the poster
4245      this.elements.poster.removeAttribute('hidden');
4246
4247      // Wait until ui is ready
4248      return ready.call(this)
4249      // Load image
4250      .then(() => loadImage(poster)).catch(error => {
4251        // Hide poster on error unless it's been set by another call
4252        if (poster === this.poster) {
4253          ui.togglePoster.call(this, false);
4254        }
4255        // Rethrow
4256        throw error;
4257      }).then(() => {
4258        // Prevent race conditions
4259        if (poster !== this.poster) {
4260          throw new Error('setPoster cancelled by later call to setPoster');
4261        }
4262      }).then(() => {
4263        Object.assign(this.elements.poster.style, {
4264          backgroundImage: `url('${poster}')`,
4265          // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
4266          backgroundSize: ''
4267        });
4268        ui.togglePoster.call(this, true);
4269        return poster;
4270      });
4271    },
4272    // Check playing state
4273    checkPlaying(event) {
4274      // Class hooks
4275      toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
4276      toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
4277      toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
4278
4279      // Set state
4280      Array.from(this.elements.buttons.play || []).forEach(target => {
4281        Object.assign(target, {
4282          pressed: this.playing
4283        });
4284        target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
4285      });
4286
4287      // Only update controls on non timeupdate events
4288      if (is.event(event) && event.type === 'timeupdate') {
4289        return;
4290      }
4291
4292      // Toggle controls
4293      ui.toggleControls.call(this);
4294    },
4295    // Check if media is loading
4296    checkLoading(event) {
4297      this.loading = ['stalled', 'waiting'].includes(event.type);
4298
4299      // Clear timer
4300      clearTimeout(this.timers.loading);
4301
4302      // Timer to prevent flicker when seeking
4303      this.timers.loading = setTimeout(() => {
4304        // Update progress bar loading class state
4305        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);
4306
4307        // Update controls visibility
4308        ui.toggleControls.call(this);
4309      }, this.loading ? 250 : 0);
4310    },
4311    // Toggle controls based on state and `force` argument
4312    toggleControls(force) {
4313      const {
4314        controls: controlsElement
4315      } = this.elements;
4316      if (controlsElement && this.config.hideControls) {
4317        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
4318        const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();
4319
4320        // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
4321        this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));
4322      }
4323    },
4324    // Migrate any custom properties from the media to the parent
4325    migrateStyles() {
4326      // Loop through values (as they are the keys when the object is spread 🤔)
4327      Object.values({
4328        ...this.media.style
4329      })
4330      // We're only fussed about Plyr specific properties
4331      .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {
4332        // Set on the container
4333        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
4334
4335        // Clean up from media element
4336        this.media.style.removeProperty(key);
4337      });
4338
4339      // Remove attribute if empty
4340      if (is.empty(this.media.style)) {
4341        this.media.removeAttribute('style');
4342      }
4343    }
4344  };
4345
4346  class Listeners {
4347    constructor(_player) {
4348      // Device is touch enabled
4349      _defineProperty$1(this, "firstTouch", () => {
4350        const {
4351          player
4352        } = this;
4353        const {
4354          elements
4355        } = player;
4356        player.touch = true;
4357
4358        // Add touch class
4359        toggleClass(elements.container, player.config.classNames.isTouch, true);
4360      });
4361      // Global window & document listeners
4362      _defineProperty$1(this, "global", (toggle = true) => {
4363        const {
4364          player
4365        } = this;
4366
4367        // Keyboard shortcuts
4368        if (player.config.keyboard.global) {
4369          toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);
4370        }
4371
4372        // Click anywhere closes menu
4373        toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);
4374
4375        // Detect touch by events
4376        once.call(player, document.body, 'touchstart', this.firstTouch);
4377      });
4378      // Container listeners
4379      _defineProperty$1(this, "container", () => {
4380        const {
4381          player
4382        } = this;
4383        const {
4384          config,
4385          elements,
4386          timers
4387        } = player;
4388
4389        // Keyboard shortcuts
4390        if (!config.keyboard.global && config.keyboard.focused) {
4391          on.call(player, elements.container, 'keydown keyup', this.handleKey, false);
4392        }
4393
4394        // Toggle controls on mouse events and entering fullscreen
4395        on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {
4396          const {
4397            controls: controlsElement
4398          } = elements;
4399
4400          // Remove button states for fullscreen
4401          if (controlsElement && event.type === 'enterfullscreen') {
4402            controlsElement.pressed = false;
4403            controlsElement.hover = false;
4404          }
4405
4406          // Show, then hide after a timeout unless another control event occurs
4407          const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
4408          let delay = 0;
4409          if (show) {
4410            ui.toggleControls.call(player, true);
4411            // Use longer timeout for touch devices
4412            delay = player.touch ? 3000 : 2000;
4413          }
4414
4415          // Clear timer
4416          clearTimeout(timers.controls);
4417
4418          // Set new timer to prevent flicker when seeking
4419          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
4420        });
4421
4422        // Set a gutter for Vimeo
4423        const setGutter = () => {
4424          if (!player.isVimeo || player.config.vimeo.premium) {
4425            return;
4426          }
4427          const target = elements.wrapper;
4428          const {
4429            active
4430          } = player.fullscreen;
4431          const [videoWidth, videoHeight] = getAspectRatio.call(player);
4432          const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);
4433
4434          // If not active, remove styles
4435          if (!active) {
4436            if (useNativeAspectRatio) {
4437              target.style.width = null;
4438              target.style.height = null;
4439            } else {
4440              target.style.maxWidth = null;
4441              target.style.margin = null;
4442            }
4443            return;
4444          }
4445
4446          // Determine which dimension will overflow and constrain view
4447          const [viewportWidth, viewportHeight] = getViewportSize();
4448          const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;
4449          if (useNativeAspectRatio) {
4450            target.style.width = overflow ? 'auto' : '100%';
4451            target.style.height = overflow ? '100%' : 'auto';
4452          } else {
4453            target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;
4454            target.style.margin = overflow ? '0 auto' : null;
4455          }
4456        };
4457
4458        // Handle resizing
4459        const resized = () => {
4460          clearTimeout(timers.resized);
4461          timers.resized = setTimeout(setGutter, 50);
4462        };
4463        on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {
4464          const {
4465            target
4466          } = player.fullscreen;
4467
4468          // Ignore events not from target
4469          if (target !== elements.container) {
4470            return;
4471          }
4472
4473          // If it's not an embed and no ratio specified
4474          if (!player.isEmbed && is.empty(player.config.ratio)) {
4475            return;
4476          }
4477
4478          // Set Vimeo gutter
4479          setGutter();
4480
4481          // Watch for resizes
4482          const method = event.type === 'enterfullscreen' ? on : off;
4483          method.call(player, window, 'resize', resized);
4484        });
4485      });
4486      // Listen for media events
4487      _defineProperty$1(this, "media", () => {
4488        const {
4489          player
4490        } = this;
4491        const {
4492          elements
4493        } = player;
4494
4495        // Time change on media
4496        on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));
4497
4498        // Display duration
4499        on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));
4500
4501        // Handle the media finishing
4502        on.call(player, player.media, 'ended', () => {
4503          // Show poster on end
4504          if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
4505            // Restart
4506            player.restart();
4507
4508            // Call pause otherwise IE11 will start playing the video again
4509            player.pause();
4510          }
4511        });
4512
4513        // Check for buffer progress
4514        on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));
4515
4516        // Handle volume changes
4517        on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));
4518
4519        // Handle play/pause
4520        on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));
4521
4522        // Loading state
4523        on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));
4524
4525        // Click video
4526        if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {
4527          // Re-fetch the wrapper
4528          const wrapper = getElement.call(player, `.${player.config.classNames.video}`);
4529
4530          // Bail if there's no wrapper (this should never happen)
4531          if (!is.element(wrapper)) {
4532            return;
4533          }
4534
4535          // On click play, pause or restart
4536          on.call(player, elements.container, 'click', event => {
4537            const targets = [elements.container, wrapper];
4538
4539            // Ignore if click if not container or in video wrapper
4540            if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
4541              return;
4542            }
4543
4544            // Touch devices will just show controls (if hidden)
4545            if (player.touch && player.config.hideControls) {
4546              return;
4547            }
4548            if (player.ended) {
4549              this.proxy(event, player.restart, 'restart');
4550              this.proxy(event, () => {
4551                silencePromise(player.play());
4552              }, 'play');
4553            } else {
4554              this.proxy(event, () => {
4555                silencePromise(player.togglePlay());
4556              }, 'play');
4557            }
4558          });
4559        }
4560
4561        // Disable right click
4562        if (player.supported.ui && player.config.disableContextMenu) {
4563          on.call(player, elements.wrapper, 'contextmenu', event => {
4564            event.preventDefault();
4565          }, false);
4566        }
4567
4568        // Volume change
4569        on.call(player, player.media, 'volumechange', () => {
4570          // Save to storage
4571          player.storage.set({
4572            volume: player.volume,
4573            muted: player.muted
4574          });
4575        });
4576
4577        // Speed change
4578        on.call(player, player.media, 'ratechange', () => {
4579          // Update UI
4580          controls.updateSetting.call(player, 'speed');
4581
4582          // Save to storage
4583          player.storage.set({
4584            speed: player.speed
4585          });
4586        });
4587
4588        // Quality change
4589        on.call(player, player.media, 'qualitychange', event => {
4590          // Update UI
4591          controls.updateSetting.call(player, 'quality', null, event.detail.quality);
4592        });
4593
4594        // Update download link when ready and if quality changes
4595        on.call(player, player.media, 'ready qualitychange', () => {
4596          controls.setDownloadUrl.call(player);
4597        });
4598
4599        // Proxy events to container
4600        // Bubble up key events for Edge
4601        const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
4602        on.call(player, player.media, proxyEvents, event => {
4603          let {
4604            detail = {}
4605          } = event;
4606
4607          // Get error details from media
4608          if (event.type === 'error') {
4609            detail = player.media.error;
4610          }
4611          triggerEvent.call(player, elements.container, event.type, true, detail);
4612        });
4613      });
4614      // Run default and custom handlers
4615      _defineProperty$1(this, "proxy", (event, defaultHandler, customHandlerKey) => {
4616        const {
4617          player
4618        } = this;
4619        const customHandler = player.config.listeners[customHandlerKey];
4620        const hasCustomHandler = is.function(customHandler);
4621        let returned = true;
4622
4623        // Execute custom handler
4624        if (hasCustomHandler) {
4625          returned = customHandler.call(player, event);
4626        }
4627
4628        // Only call default handler if not prevented in custom handler
4629        if (returned !== false && is.function(defaultHandler)) {
4630          defaultHandler.call(player, event);
4631        }
4632      });
4633      // Trigger custom and default handlers
4634      _defineProperty$1(this, "bind", (element, type, defaultHandler, customHandlerKey, passive = true) => {
4635        const {
4636          player
4637        } = this;
4638        const customHandler = player.config.listeners[customHandlerKey];
4639        const hasCustomHandler = is.function(customHandler);
4640        on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);
4641      });
4642      // Listen for control events
4643      _defineProperty$1(this, "controls", () => {
4644        const {
4645          player
4646        } = this;
4647        const {
4648          elements
4649        } = player;
4650        // IE doesn't support input event, so we fallback to change
4651        const inputEvent = browser.isIE ? 'change' : 'input';
4652
4653        // Play/pause toggle
4654        if (elements.buttons.play) {
4655          Array.from(elements.buttons.play).forEach(button => {
4656            this.bind(button, 'click', () => {
4657              silencePromise(player.togglePlay());
4658            }, 'play');
4659          });
4660        }
4661
4662        // Pause
4663        this.bind(elements.buttons.restart, 'click', player.restart, 'restart');
4664
4665        // Rewind
4666        this.bind(elements.buttons.rewind, 'click', () => {
4667          // Record seek time so we can prevent hiding controls for a few seconds after rewind
4668          player.lastSeekTime = Date.now();
4669          player.rewind();
4670        }, 'rewind');
4671
4672        // Rewind
4673        this.bind(elements.buttons.fastForward, 'click', () => {
4674          // Record seek time so we can prevent hiding controls for a few seconds after fast forward
4675          player.lastSeekTime = Date.now();
4676          player.forward();
4677        }, 'fastForward');
4678
4679        // Mute toggle
4680        this.bind(elements.buttons.mute, 'click', () => {
4681          player.muted = !player.muted;
4682        }, 'mute');
4683
4684        // Captions toggle
4685        this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());
4686
4687        // Download
4688        this.bind(elements.buttons.download, 'click', () => {
4689          triggerEvent.call(player, player.media, 'download');
4690        }, 'download');
4691
4692        // Fullscreen toggle
4693        this.bind(elements.buttons.fullscreen, 'click', () => {
4694          player.fullscreen.toggle();
4695        }, 'fullscreen');
4696
4697        // Picture-in-Picture
4698        this.bind(elements.buttons.pip, 'click', () => {
4699          player.pip = 'toggle';
4700        }, 'pip');
4701
4702        // Airplay
4703        this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');
4704
4705        // Settings menu - click toggle
4706        this.bind(elements.buttons.settings, 'click', event => {
4707          // Prevent the document click listener closing the menu
4708          event.stopPropagation();
4709          event.preventDefault();
4710          controls.toggleMenu.call(player, event);
4711        }, null, false); // Can't be passive as we're preventing default
4712
4713        // Settings menu - keyboard toggle
4714        // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
4715        // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
4716        this.bind(elements.buttons.settings, 'keyup', event => {
4717          if (![' ', 'Enter'].includes(event.key)) {
4718            return;
4719          }
4720
4721          // Because return triggers a click anyway, all we need to do is set focus
4722          if (event.key === 'Enter') {
4723            controls.focusFirstMenuItem.call(player, null, true);
4724            return;
4725          }
4726
4727          // Prevent scroll
4728          event.preventDefault();
4729
4730          // Prevent playing video (Firefox)
4731          event.stopPropagation();
4732
4733          // Toggle menu
4734          controls.toggleMenu.call(player, event);
4735        }, null, false // Can't be passive as we're preventing default
4736        );
4737
4738        // Escape closes menu
4739        this.bind(elements.settings.menu, 'keydown', event => {
4740          if (event.key === 'Escape') {
4741            controls.toggleMenu.call(player, event);
4742          }
4743        });
4744
4745        // Set range input alternative "value", which matches the tooltip time (#954)
4746        this.bind(elements.inputs.seek, 'mousedown mousemove', event => {
4747          const rect = elements.progress.getBoundingClientRect();
4748          const 
4748percent = 100 / rect.width * (event.pageX - rect.l
vendor: 10,136 bytes, lines 4748-5023
4748eft);
4749          event.currentTarget.setAttribute('seek-value', percent);
4750        });
4751
4752        // Pause while seeking
4753        this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {
4754          const seek = event.currentTarget;
4755          const attribute = 'play-on-seeked';
4756          if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {
4757            return;
4758          }
4759
4760          // Record seek time so we can prevent hiding controls for a few seconds after seek
4761          player.lastSeekTime = Date.now();
4762
4763          // Was playing before?
4764          const play = seek.hasAttribute(attribute);
4765          // Done seeking
4766          const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
4767
4768          // If we're done seeking and it was playing, resume playback
4769          if (play && done) {
4770            seek.removeAttribute(attribute);
4771            silencePromise(player.play());
4772          } else if (!done && player.playing) {
4773            seek.setAttribute(attribute, '');
4774            player.pause();
4775          }
4776        });
4777
4778        // Fix range inputs on iOS
4779        // Super weird iOS bug where after you interact with an <input type="range">,
4780        // it takes over further interactions on the page. This is a hack
4781        if (browser.isIos) {
4782          const inputs = getElements.call(player, 'input[type="range"]');
4783          Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));
4784        }
4785
4786        // Seek
4787        this.bind(elements.inputs.seek, inputEvent, event => {
4788          const seek = event.currentTarget;
4789          // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
4790          let seekTo = seek.getAttribute('seek-value');
4791          if (is.empty(seekTo)) {
4792            seekTo = seek.value;
4793          }
4794          seek.removeAttribute('seek-value');
4795          player.currentTime = seekTo / seek.max * player.duration;
4796        }, 'seek');
4797
4798        // Seek tooltip
4799        this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));
4800
4801        // Preview thumbnails plugin
4802        // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this
4803        this.bind(elements.progress, 'mousemove touchmove', event => {
4804          const {
4805            previewThumbnails
4806          } = player;
4807          if (previewThumbnails && previewThumbnails.loaded) {
4808            previewThumbnails.startMove(event);
4809          }
4810        });
4811
4812        // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
4813        this.bind(elements.progress, 'mouseleave touchend click', () => {
4814          const {
4815            previewThumbnails
4816          } = player;
4817          if (previewThumbnails && previewThumbnails.loaded) {
4818            previewThumbnails.endMove(false, true);
4819          }
4820        });
4821
4822        // Show scrubbing preview
4823        this.bind(elements.progress, 'mousedown touchstart', event => {
4824          const {
4825            previewThumbnails
4826          } = player;
4827          if (previewThumbnails && previewThumbnails.loaded) {
4828            previewThumbnails.startScrubbing(event);
4829          }
4830        });
4831        this.bind(elements.progress, 'mouseup touchend', event => {
4832          const {
4833            previewThumbnails
4834          } = player;
4835          if (previewThumbnails && previewThumbnails.loaded) {
4836            previewThumbnails.endScrubbing(event);
4837          }
4838        });
4839
4840        // Polyfill for lower fill in <input type="range"> for webkit
4841        if (browser.isWebKit) {
4842          Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => {
4843            this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));
4844          });
4845        }
4846
4847        // Current time invert
4848        // Only if one time element is used for both currentTime and duration
4849        if (player.config.toggleInvert && !is.element(elements.display.duration)) {
4850          this.bind(elements.display.currentTime, 'click', () => {
4851            // Do nothing if we're at the start
4852            if (player.currentTime === 0) {
4853              return;
4854            }
4855            player.config.invertTime = !player.config.invertTime;
4856            controls.timeUpdate.call(player);
4857          });
4858        }
4859
4860        // Volume
4861        this.bind(elements.inputs.volume, inputEvent, event => {
4862          player.volume = event.target.value;
4863        }, 'volume');
4864
4865        // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
4866        this.bind(elements.controls, 'mouseenter mouseleave', event => {
4867          elements.controls.hover = !player.touch && event.type === 'mouseenter';
4868        });
4869
4870        // Also update controls.hover state for any non-player children of fullscreen element (as above)
4871        if (elements.fullscreen) {
4872          Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {
4873            this.bind(child, 'mouseenter mouseleave', event => {
4874              if (elements.controls) {
4875                elements.controls.hover = !player.touch && event.type === 'mouseenter';
4876              }
4877            });
4878          });
4879        }
4880
4881        // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
4882        this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
4883          elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
4884        });
4885
4886        // Show controls when they receive focus (e.g., when using keyboard tab key)
4887        this.bind(elements.controls, 'focusin', () => {
4888          const {
4889            config,
4890            timers
4891          } = player;
4892
4893          // Skip transition to prevent focus from scrolling the parent element
4894          toggleClass(elements.controls, config.classNames.noTransition, true);
4895
4896          // Toggle
4897          ui.toggleControls.call(player, true);
4898
4899          // Restore transition
4900          setTimeout(() => {
4901            toggleClass(elements.controls, config.classNames.noTransition, false);
4902          }, 0);
4903
4904          // Delay a little more for mouse users
4905          const delay = this.touch ? 3000 : 4000;
4906
4907          // Clear timer
4908          clearTimeout(timers.controls);
4909
4910          // Hide again after delay
4911          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);
4912        });
4913
4914        // Mouse wheel for volume
4915        this.bind(elements.inputs.volume, 'wheel', event => {
4916          // Detect "natural" scroll - supported on OS X Safari only
4917          // Other browsers on OS X will be inverted until support improves
4918          const inverted = event.webkitDirectionInvertedFromDevice;
4919          // Get delta from event. Invert if `inverted` is true
4920          const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);
4921          // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
4922          const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);
4923
4924          // Change the volume by 2%
4925          player.increaseVolume(direction / 50);
4926
4927          // Don't break page scrolling at max and min
4928          const {
4929            volume
4930          } = player.media;
4931          if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {
4932            event.preventDefault();
4933          }
4934        }, 'volume', false);
4935      });
4936      this.player = _player;
4937      this.lastKey = null;
4938      this.focusTimer = null;
4939      this.lastKeyDown = null;
4940      this.handleKey = this.handleKey.bind(this);
4941      this.toggleMenu = this.toggleMenu.bind(this);
4942      this.firstTouch = this.firstTouch.bind(this);
4943    }
4944
4945    // Handle key presses
4946    handleKey(event) {
4947      const {
4948        player
4949      } = this;
4950      const {
4951        elements
4952      } = player;
4953      const {
4954        key,
4955        type,
4956        altKey,
4957        ctrlKey,
4958        metaKey,
4959        shiftKey
4960      } = event;
4961      const pressed = type === 'keydown';
4962      const repeat = pressed && key === this.lastKey;
4963
4964      // Bail if a modifier key is set
4965      if (altKey || ctrlKey || metaKey || shiftKey) {
4966        return;
4967      }
4968
4969      // If the event is bubbled from the media element
4970      // Firefox doesn't get the key for whatever reason
4971      if (!key) {
4972        return;
4973      }
4974
4975      // Seek by increment
4976      const seekByIncrement = increment => {
4977        // Divide the max duration into 10th's and times by the number value
4978        player.currentTime = player.duration / 10 * increment;
4979      };
4980
4981      // Handle the key on keydown
4982      // Reset on keyup
4983      if (pressed) {
4984        // Check focused element
4985        // and if the focused element is not editable (e.g. text input)
4986        // and any that accept key input http://webaim.org/techniques/keyboard/
4987        const focused = document.activeElement;
4988        if (is.element(focused)) {
4989          const {
4990            editable
4991          } = player.config.selectors;
4992          const {
4993            seek
4994          } = elements.inputs;
4995          if (focused !== seek && matches(focused, editable)) {
4996            return;
4997          }
4998          if (event.key === ' ' && matches(focused, 'button, [role^="menuitem"]')) {
4999            return;
5000          }
5001        }
5002
5003        // Which keys should we prevent default
5004        const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];
5005
5006        // If the key is found prevent default (e.g. prevent scrolling for arrows)
5007        if (preventDefault.includes(key)) {
5008          event.preventDefault();
5009          event.stopPropagation();
5010        }
5011        switch (key) {
5012          case '0':
5013          case '1':
5014          case '2':
5015          case '3':
5016          case '4':
5017          case '5':
5018          case '6':
5019          case '7':
5020          case '8':
5021          case '9':
5022            if (!repeat) {
5023              seekByIncrement(
vendor: 1,821 bytes, lines 5023-5087
5023parseInt(key, 10));
5024            }
5025            break;
5026          case ' ':
5027          case 'k':
5028            if (!repeat) {
5029              silencePromise(player.togglePlay());
5030            }
5031            break;
5032          case 'ArrowUp':
5033            player.increaseVolume(0.1);
5034            break;
5035          case 'ArrowDown':
5036            player.decreaseVolume(0.1);
5037            break;
5038          case 'm':
5039            if (!repeat) {
5040              player.muted = !player.muted;
5041            }
5042            break;
5043          case 'ArrowRight':
5044            player.forward();
5045            break;
5046          case 'ArrowLeft':
5047            player.rewind();
5048            break;
5049          case 'f':
5050            player.fullscreen.toggle();
5051            break;
5052          case 'c':
5053            if (!repeat) {
5054              player.toggleCaptions();
5055            }
5056            break;
5057          case 'l':
5058            player.loop = !player.loop;
5059            break;
5060        }
5061
5062        // Escape is handle natively when in full screen
5063        // So we only need to worry about non native
5064        if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {
5065          player.fullscreen.toggle();
5066        }
5067
5068        // Store last key for next cycle
5069        this.lastKey = key;
5070      } else {
5071        this.lastKey = null;
5072      }
5073    }
5074
5075    // Toggle menu
5076    toggleMenu(event) {
5077      controls.toggleMenu.call(this.player, event);
5078    }
5079  }
5080
5081  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
5082
5083  function createCommonjsModule(fn, module) {
5084  	return module = { exports: {} }, fn(module, module.exports), module.exports;
5085  }
5086
5087  var loadjs_umd = createCommonjsModule(function (module, exports
vendor: 6,402 bytes, lines 5087-5293
5087) {
5088    (function (root, factory) {
5089      {
5090        module.exports = factory();
5091      }
5092    })(commonjsGlobal, function () {
5093      /**
5094       * Global dependencies.
5095       * @global {Object} document - DOM
5096       */
5097
5098      var devnull = function () {},
5099        bundleIdCache = {},
5100        bundleResultCache = {},
5101        bundleCallbackQueue = {};
5102
5103      /**
5104       * Subscribe to bundle load event.
5105       * @param {string[]} bundleIds - Bundle ids
5106       * @param {Function} callbackFn - The callback function
5107       */
5108      function subscribe(bundleIds, callbackFn) {
5109        // listify
5110        bundleIds = bundleIds.push ? bundleIds : [bundleIds];
5111        var depsNotFound = [],
5112          i = bundleIds.length,
5113          numWaiting = i,
5114          fn,
5115          bundleId,
5116          r,
5117          q;
5118
5119        // define callback function
5120        fn = function (bundleId, pathsNotFound) {
5121          if (pathsNotFound.length) depsNotFound.push(bundleId);
5122          numWaiting--;
5123          if (!numWaiting) callbackFn(depsNotFound);
5124        };
5125
5126        // register callback
5127        while (i--) {
5128          bundleId = bundleIds[i];
5129
5130          // execute callback if in result cache
5131          r = bundleResultCache[bundleId];
5132          if (r) {
5133            fn(bundleId, r);
5134            continue;
5135          }
5136
5137          // add to callback queue
5138          q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
5139          q.push(fn);
5140        }
5141      }
5142
5143      /**
5144       * Publish bundle load event.
5145       * @param {string} bundleId - Bundle id
5146       * @param {string[]} pathsNotFound - List of files not found
5147       */
5148      function publish(bundleId, pathsNotFound) {
5149        // exit if id isn't defined
5150        if (!bundleId) return;
5151        var q = bundleCallbackQueue[bundleId];
5152
5153        // cache result
5154        bundleResultCache[bundleId] = pathsNotFound;
5155
5156        // exit if queue is empty
5157        if (!q) return;
5158
5159        // empty callback queue
5160        while (q.length) {
5161          q[0](bundleId, pathsNotFound);
5162          q.splice(0, 1);
5163        }
5164      }
5165
5166      /**
5167       * Execute callbacks.
5168       * @param {Object or Function} args - The callback args
5169       * @param {string[]} depsNotFound - List of dependencies not found
5170       */
5171      function executeCallbacks(args, depsNotFound) {
5172        // accept function as argument
5173        if (args.call) args = {
5174          success: args
5175        };
5176
5177        // success and error callbacks
5178        if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);
5179      }
5180
5181      /**
5182       * Load individual file.
5183       * @param {string} path - The file path
5184       * @param {Function} callbackFn - The callback function
5185       */
5186      function loadFile(path, callbackFn, args, numTries) {
5187        var doc = document,
5188          async = args.async,
5189          maxTries = (args.numRetries || 0) + 1,
5190          beforeCallbackFn = args.before || devnull,
5191          pathname = path.replace(/[\?|#].*$/, ''),
5192          pathStripped = path.replace(/^(css|img)!/, ''),
5193          isLegacyIECss,
5194          e;
5195        numTries = numTries || 0;
5196        if (/(^css!|\.css$)/.test(pathname)) {
5197          // css
5198          e = doc.createElement('link');
5199          e.rel = 'stylesheet';
5200          e.href = pathStripped;
5201
5202          // tag IE9+
5203          isLegacyIECss = 'hideFocus' in e;
5204
5205          // use preload in IE Edge (to detect load errors)
5206          if (isLegacyIECss && e.relList) {
5207            isLegacyIECss = 0;
5208            e.rel = 'preload';
5209            e.as = 'style';
5210          }
5211        } else if (/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {
5212          // image
5213          e = doc.createElement('img');
5214          e.src = pathStripped;
5215        } else {
5216          // javascript
5217          e = doc.createElement('script');
5218          e.src = path;
5219          e.async = async === undefined ? true : async;
5220        }
5221        e.onload = e.onerror = e.onbeforeload = function (ev) {
5222          var result = ev.type[0];
5223
5224          // treat empty stylesheets as failures to get around lack of onerror
5225          // support in IE9-11
5226          if (isLegacyIECss) {
5227            try {
5228              if (!e.sheet.cssText.length) result = 'e';
5229            } catch (x) {
5230              // sheets objects created from load errors don't allow access to
5231              // `cssText` (unless error is Code:18 SecurityError)
5232              if (x.code != 18) result = 'e';
5233            }
5234          }
5235
5236          // handle retries in case of load failure
5237          if (result == 'e') {
5238            // increment counter
5239            numTries += 1;
5240
5241            // exit function and try again
5242            if (numTries < maxTries) {
5243              return loadFile(path, callbackFn, args, numTries);
5244            }
5245          } else if (e.rel == 'preload' && e.as == 'style') {
5246            // activate preloaded stylesheets
5247            return e.rel = 'stylesheet'; // jshint ignore:line
5248          }
5249
5250          // execute callback
5251          callbackFn(path, result, ev.defaultPrevented);
5252        };
5253
5254        // add to document (unless callback returns `false`)
5255        if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
5256      }
5257
5258      /**
5259       * Load multiple files.
5260       * @param {string[]} paths - The file paths
5261       * @param {Function} callbackFn - The callback function
5262       */
5263      function loadFiles(paths, callbackFn, args) {
5264        // listify paths
5265        paths = paths.push ? paths : [paths];
5266        var numWaiting = paths.length,
5267          x = numWaiting,
5268          pathsNotFound = [],
5269          fn,
5270          i;
5271
5272        // define callback function
5273        fn = function (path, result, defaultPrevented) {
5274          // handle error
5275          if (result == 'e') pathsNotFound.push(path);
5276
5277          // handle beforeload event. If defaultPrevented then that means the load
5278          // will be blocked (ex. Ghostery/ABP on Safari)
5279          if (result == 'b') {
5280            if (defaultPrevented) pathsNotFound.push(path);else return;
5281          }
5282          numWaiting--;
5283          if (!numWaiting) callbackFn(pathsNotFound);
5284        };
5285
5286        // load scripts
5287        for (i = 0; i < x; i++) loadFile(paths[i], fn, args);
5288      }
5289
5290      /**
5291       * Initiate script load and register bundle.
5292       * @param {(string|string[])} paths - The file paths
5293       * @param {(string|Function|Object)}
5293 [arg1] - The (1) bundleId or (2) success
5294       *   callback or (3) object literal with success/error arguments, numRetries,
5295       *   etc.
5296       * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object
5297       *   literal with success/error arguments, numRetries, etc.
5298       */
5299      function loadjs(paths, arg1, arg2) {
5300        var bundleId, args;
5301
5302        // bundleId (if string)
5303        if (arg1 && arg1.trim) bundleId = arg1;
5304
5305        // args (default is {})
5306        args = (bundleId ? arg2 : arg1) || {};
5307
5308        // throw error if bundle is already defined
5309        if (bundleId) {
5310          if (bundleId in bundleIdCache) {
5311            throw "LoadJS";
5312          } else {
5313            bundleIdCache[bundleId] = true;
5314          }
5315        }
5316        function loadFn(resolve, reject) {
5317          loadFiles(paths, function (pathsNotFound) {
5318            // execute callbacks
5319            executeCallbacks(args, pathsNotFound);
5320
5321            // resolve Promise
5322            if (resolve) {
5323              executeCallbacks({
5324                success: resolve,
5325                error: reject
5326              }, pathsNotFound);
5327            }
5328
5329            // publish bundle load event
5330            publish(bundleId, pathsNotFound);
5331          }, args);
5332        }
5333        if (args.returnPromise) return new Promise(loadFn);else loadFn();
5334      }
5335
5336      /**
5337       * Execute callbacks when dependencies have been satisfied.
5338       * @param {(string|string[])} deps - List of bundle ids
5339       * @param {Object} args - success/error arguments
5340       */
5341      loadjs.ready = function ready(deps, args) {
5342        // subscribe to bundle load event
5343        subscribe(deps, function (depsNotFound) {
5344          // execute callbacks
5345          executeCallbacks(args, depsNotFound);
5346        });
5347        return loadjs;
5348      };
5349
5350      /**
5351       * Manually satisfy bundle dependencies.
5352       * @param {string} bundleId - The bundle id
5353       */
5354      loadjs.done = function done(bundleId) {
5355        publish(bundleId, []);
5356      };
5357
5358      /**
5359       * Reset loadjs dependencies statuses
5360       */
5361      loadjs.reset = function reset() {
5362        bundleIdCache = {};
5363        bundleResultCache = {};
5364        bundleCallbackQueue = {};
5365      };
5366
5367      /**
5368       * Determine if bundle has already been defined
5369       * @param String} bundleId - The bundle id
5370       */
5371      loadjs.isDefined = function isDefined(bundleId) {
5372        return bundleId in bundleIdCache;
5373      };
5374
5375      // export
5376      return loadjs;
5377    });
5378  });
5379
5380  // ==========================================================================
5381  function loadScript(url) {
5382    return new Promise((resolve, reject) => {
5383      loadjs_umd(url, {
5384        success: resolve,
5385        error: reject
5386      });
5387    });
5388  }
5389
5390  // ==========================================================================
5391
5392  // Parse Vimeo ID from URL
5393  function parseId$1(url) {
5394    if (is.empty(url)) {
5395      return null;
5396    }
5397    if (is.number(Number(url))) {
5398      return url;
5399    }
5400    const regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
5401    return url.match(regex) ? RegExp.$2 : url;
5402  }
5403
5404  // Try to extract a hash for private videos from the URL
5405  function parseHash(url) {
5406    /* This regex matches a hexadecimal hash if given in any of these forms:
5407     *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]
5408     *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]
5409     *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}
5410     *  - video/{id}/{hash}
5411     * If matched, the hash is available in capture group 4
5412     */
5413    const regex = /^.*(vimeo.com\/|video\/)(\d+)(\?.*&*h=|\/)+([\d,a-f]+)/;
5414    const found = url.match(regex);
5415    return found && found.length === 5 ? found[4] : null;
5416  }
5417
5418  // Set playback state and trigger change (only on actual change)
5419  function assurePlaybackState$1(play) {
5420    if (play && !this.embed.hasPlayed) {
5421      this.embed.hasPlayed = true;
5422    }
5423    if (this.media.paused === play) {
5424      this.media.paused = !play;
5425      triggerEvent.call(this, this.media, play ? 'play' : 'pause');
5426    }
5427  }
5428  const vimeo = {
5429    setup() {
5430      const player = this;
5431
5432      // Add embed class for responsive
5433      toggleClass(player.elements.wrapper, player.config.classNames.embed, true);
5434
5435      // Set speed options from config
5436      player.options.speed = player.config.speed.options;
5437
5438      // Set intial ratio
5439      setAspectRatio.call(player);
5440
5441      // Load the SDK if not already
5442      if (!is.object(window.Vimeo)) {
5443        loadScript(player.config.urls.vimeo.sdk).then(() => {
5444          vimeo.ready.call(player);
5445        }).catch(error => {
5446          player.debug.warn('Vimeo SDK (player.js) failed to load', error);
5447        });
5448      } else {
5449        vimeo.ready.call(player);
5450      }
5451    },
5452    // API Ready
5453    ready() {
5454      const player = this;
5455      const config = player.config.vimeo;
5456      const {
5457        premium,
5458        referrerPolicy,
vendor: 1,964 bytes, lines 5459-5514
5459        ...frameParams
5460      } = config;
5461      // Get the source URL or ID
5462      let source = player.media.getAttribute('src');
5463      let hash = '';
5464      // Get from <div> if needed
5465      if (is.empty(source)) {
5466        source = player.media.getAttribute(player.config.attributes.embed.id);
5467        // hash can also be set as attribute on the <div>
5468        hash = player.media.getAttribute(player.config.attributes.embed.hash);
5469      } else {
5470        hash = parseHash(source);
5471      }
5472      const hashParam = hash ? {
5473        h: hash
5474      } : {};
5475
5476      // If the owner has a pro or premium account then we can hide controls etc
5477      if (premium) {
5478        Object.assign(frameParams, {
5479          controls: false,
5480          sidedock: false
5481        });
5482      }
5483
5484      // Get Vimeo params for the iframe
5485      const params = buildUrlParams({
5486        loop: player.config.loop.active,
5487        autoplay: player.autoplay,
5488        muted: player.muted,
5489        gesture: 'media',
5490        playsinline: player.config.playsinline,
5491        // hash has to be added to iframe-URL
5492        ...hashParam,
5493        ...frameParams
5494      });
5495      const id = parseId$1(source);
5496      // Build an iframe
5497      const iframe = createElement('iframe');
5498      const src = format(player.config.urls.vimeo.iframe, id, params);
5499      iframe.setAttribute('src', src);
5500      iframe.setAttribute('allowfullscreen', '');
5501      iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));
5502
5503      // Set the referrer policy if required
5504      if (!is.empty(referrerPolicy)) {
5505        iframe.setAttribute('referrerPolicy', referrerPolicy);
5506      }
5507
5508      // Inject the package
5509      if (premium || !config.customControls) {
5510        iframe.setAttribute('data-poster', player.poster);
5511        player.media = replaceElement(iframe, player.media);
5512      } else {
5513        const wrapper = createElement('div', {
5514          class
vendor: 7,560 bytes, lines 5514-5765
5514: player.config.classNames.embedContainer,
5515          'data-poster': player.poster
5516        });
5517        wrapper.appendChild(iframe);
5518        player.media = replaceElement(wrapper, player.media);
5519      }
5520
5521      // Get poster image
5522      if (!config.customControls) {
5523        fetch(format(player.config.urls.vimeo.api, src)).then(response => {
5524          if (is.empty(response) || !response.thumbnail_url) {
5525            return;
5526          }
5527
5528          // Set and show poster
5529          ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
5530        });
5531      }
5532
5533      // Setup instance
5534      // https://github.com/vimeo/player.js
5535      player.embed = new window.Vimeo.Player(iframe, {
5536        autopause: player.config.autopause,
5537        muted: player.muted
5538      });
5539      player.media.paused = true;
5540      player.media.currentTime = 0;
5541
5542      // Disable native text track rendering
5543      if (player.supported.ui) {
5544        player.embed.disableTextTrack();
5545      }
5546
5547      // Create a faux HTML5 API using the Vimeo API
5548      player.media.play = () => {
5549        assurePlaybackState$1.call(player, true);
5550        return player.embed.play();
5551      };
5552      player.media.pause = () => {
5553        assurePlaybackState$1.call(player, false);
5554        return player.embed.pause();
5555      };
5556      player.media.stop = () => {
5557        player.pause();
5558        player.currentTime = 0;
5559      };
5560
5561      // Seeking
5562      let {
5563        currentTime
5564      } = player.media;
5565      Object.defineProperty(player.media, 'currentTime', {
5566        get() {
5567          return currentTime;
5568        },
5569        set(time) {
5570          // Vimeo will automatically play on seek if the video hasn't been played before
5571
5572          // Get current paused state and volume etc
5573          const {
5574            embed,
5575            media,
5576            paused,
5577            volume
5578          } = player;
5579          const restorePause = paused && !embed.hasPlayed;
5580
5581          // Set seeking state and trigger event
5582          media.seeking = true;
5583          triggerEvent.call(player, media, 'seeking');
5584
5585          // If paused, mute until seek is complete
5586          Promise.resolve(restorePause && embed.setVolume(0))
5587          // Seek
5588          .then(() => embed.setCurrentTime(time))
5589          // Restore paused
5590          .then(() => restorePause && embed.pause())
5591          // Restore volume
5592          .then(() => restorePause && embed.setVolume(volume)).catch(() => {
5593            // Do nothing
5594          });
5595        }
5596      });
5597
5598      // Playback speed
5599      let speed = player.config.speed.selected;
5600      Object.defineProperty(player.media, 'playbackRate', {
5601        get() {
5602          return speed;
5603        },
5604        set(input) {
5605          player.embed.setPlaybackRate(input).then(() => {
5606            speed = input;
5607            triggerEvent.call(player, player.media, 'ratechange');
5608          }).catch(() => {
5609            // Cannot set Playback Rate, Video is probably not on Pro account
5610            player.options.speed = [1];
5611          });
5612        }
5613      });
5614
5615      // Volume
5616      let {
5617        volume
5618      } = player.config;
5619      Object.defineProperty(player.media, 'volume', {
5620        get() {
5621          return volume;
5622        },
5623        set(input) {
5624          player.embed.setVolume(input).then(() => {
5625            volume = input;
5626            triggerEvent.call(player, player.media, 'volumechange');
5627          });
5628        }
5629      });
5630
5631      // Muted
5632      let {
5633        muted
5634      } = player.config;
5635      Object.defineProperty(player.media, 'muted', {
5636        get() {
5637          return muted;
5638        },
5639        set(input) {
5640          const toggle = is.boolean(input) ? input : false;
5641          player.embed.setMuted(toggle ? true : player.config.muted).then(() => {
5642            muted = toggle;
5643            triggerEvent.call(player, player.media, 'volumechange');
5644          });
5645        }
5646      });
5647
5648      // Loop
5649      let {
5650        loop
5651      } = player.config;
5652      Object.defineProperty(player.media, 'loop', {
5653        get() {
5654          return loop;
5655        },
5656        set(input) {
5657          const toggle = is.boolean(input) ? input : player.config.loop.active;
5658          player.embed.setLoop(toggle).then(() => {
5659            loop = toggle;
5660          });
5661        }
5662      });
5663
5664      // Source
5665      let currentSrc;
5666      player.embed.getVideoUrl().then(value => {
5667        currentSrc = value;
5668        controls.setDownloadUrl.call(player);
5669      }).catch(error => {
5670        this.debug.warn(error);
5671      });
5672      Object.defineProperty(player.media, 'currentSrc', {
5673        get() {
5674          return currentSrc;
5675        }
5676      });
5677
5678      // Ended
5679      Object.defineProperty(player.media, 'ended', {
5680        get() {
5681          return player.currentTime === player.duration;
5682        }
5683      });
5684
5685      // Set aspect ratio based on video size
5686      Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {
5687        const [width, height] = dimensions;
5688        player.embed.ratio = roundAspectRatio(width, height);
5689        setAspectRatio.call(this);
5690      });
5691
5692      // Set autopause
5693      player.embed.setAutopause(player.config.autopause).then(state => {
5694        player.config.autopause = state;
5695      });
5696
5697      // Get title
5698      player.embed.getVideoTitle().then(title => {
5699        player.config.title = title;
5700        ui.setTitle.call(this);
5701      });
5702
5703      // Get current time
5704      player.embed.getCurrentTime().then(value => {
5705        currentTime = value;
5706        triggerEvent.call(player, player.media, 'timeupdate');
5707      });
5708
5709      // Get duration
5710      player.embed.getDuration().then(value => {
5711        player.media.duration = value;
5712        triggerEvent.call(player, player.media, 'durationchange');
5713      });
5714
5715      // Get captions
5716      player.embed.getTextTracks().then(tracks => {
5717        player.media.textTracks = tracks;
5718        captions.setup.call(player);
5719      });
5720      player.embed.on('cuechange', ({
5721        cues = []
5722      }) => {
5723        const strippedCues = cues.map(cue => stripHTML(cue.text));
5724        captions.updateCues.call(player, strippedCues);
5725      });
5726      player.embed.on('loaded', () => {
5727        // Assure state and events are updated on autoplay
5728        player.embed.getPaused().then(paused => {
5729          assurePlaybackState$1.call(player, !paused);
5730          if (!paused) {
5731            triggerEvent.call(player, player.media, 'playing');
5732          }
5733        });
5734        if (is.element(player.embed.element) && player.supported.ui) {
5735          const frame = player.embed.element;
5736
5737          // Fix keyboard focus issues
5738          // https://github.com/sampotts/plyr/issues/317
5739          frame.setAttribute('tabindex', -1);
5740        }
5741      });
5742      player.embed.on('bufferstart', () => {
5743        triggerEvent.call(player, player.media, 'waiting');
5744      });
5745      player.embed.on('bufferend', () => {
5746        triggerEvent.call(player, player.media, 'playing');
5747      });
5748      player.embed.on('play', () => {
5749        assurePlaybackState$1.call(player, true);
5750        triggerEvent.call(player, player.media, 'playing');
5751      });
5752      player.embed.on('pause', () => {
5753        assurePlaybackState$1.call(player, false);
5754      });
5755      player.embed.on('timeupdate', data => {
5756        player.media.seeking = false;
5757        currentTime = data.seconds;
5758        triggerEvent.call(player, player.media, 'timeupdate');
5759      });
5760      player.embed.on('progress', data => {
5761        player.media.buffered = data.percent;
5762        triggerEvent.call(player, player.media, 'progress');
5763
5764        // Check all loaded
5765        if (
vendor: 1,374 bytes, lines 5765-5806
5765parseInt(data.percent, 10) === 1) {
5766          triggerEvent.call(player, player.media, 'canplaythrough');
5767        }
5768
5769        // Get duration as if we do it before load, it gives an incorrect value
5770        // https://github.com/sampotts/plyr/issues/891
5771        player.embed.getDuration().then(value => {
5772          if (value !== player.media.duration) {
5773            player.media.duration = value;
5774            triggerEvent.call(player, player.media, 'durationchange');
5775          }
5776        });
5777      });
5778      player.embed.on('seeked', () => {
5779        player.media.seeking = false;
5780        triggerEvent.call(player, player.media, 'seeked');
5781      });
5782      player.embed.on('ended', () => {
5783        player.media.paused = true;
5784        triggerEvent.call(player, player.media, 'ended');
5785      });
5786      player.embed.on('error', detail => {
5787        player.media.error = detail;
5788        triggerEvent.call(player, player.media, 'error');
5789      });
5790
5791      // Rebuild UI
5792      if (config.customControls) {
5793        setTimeout(() => ui.build.call(player), 0);
5794      }
5795    }
5796  };
5797
5798  // ==========================================================================
5799
5800  // Parse YouTube ID from URL
5801  function parseId(url) {
5802    if (is.empty(url)) {
5803      return null;
5804    }
5805    const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
5806    return url.match(regex) ? RegExp.$2
vendor: 14,998 bytes, lines 5806-6205
5806 : url;
5807  }
5808
5809  // Set playback state and trigger change (only on actual change)
5810  function assurePlaybackState(play) {
5811    if (play && !this.embed.hasPlayed) {
5812      this.embed.hasPlayed = true;
5813    }
5814    if (this.media.paused === play) {
5815      this.media.paused = !play;
5816      triggerEvent.call(this, this.media, play ? 'play' : 'pause');
5817    }
5818  }
5819  function getHost(config) {
5820    if (config.noCookie) {
5821      return 'https://www.youtube-nocookie.com';
5822    }
5823    if (window.location.protocol === 'http:') {
5824      return 'http://www.youtube.com';
5825    }
5826
5827    // Use YouTube's default
5828    return undefined;
5829  }
5830  const youtube = {
5831    setup() {
5832      // Add embed class for responsive
5833      toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
5834
5835      // Setup API
5836      if (is.object(window.YT) && is.function(window.YT.Player)) {
5837        youtube.ready.call(this);
5838      } else {
5839        // Reference current global callback
5840        const callback = window.onYouTubeIframeAPIReady;
5841
5842        // Set callback to process queue
5843        window.onYouTubeIframeAPIReady = () => {
5844          // Call global callback if set
5845          if (is.function(callback)) {
5846            callback();
5847          }
5848          youtube.ready.call(this);
5849        };
5850
5851        // Load the SDK
5852        loadScript(this.config.urls.youtube.sdk).catch(error => {
5853          this.debug.warn('YouTube API failed to load', error);
5854        });
5855      }
5856    },
5857    // Get the media title
5858    getTitle(videoId) {
5859      const url = format(this.config.urls.youtube.api, videoId);
5860      fetch(url).then(data => {
5861        if (is.object(data)) {
5862          const {
5863            title,
5864            height,
5865            width
5866          } = data;
5867
5868          // Set title
5869          this.config.title = title;
5870          ui.setTitle.call(this);
5871
5872          // Set aspect ratio
5873          this.embed.ratio = roundAspectRatio(width, height);
5874        }
5875        setAspectRatio.call(this);
5876      }).catch(() => {
5877        // Set aspect ratio
5878        setAspectRatio.call(this);
5879      });
5880    },
5881    // API ready
5882    ready() {
5883      const player = this;
5884      const config = player.config.youtube;
5885      // Ignore already setup (race condition)
5886      const currentId = player.media && player.media.getAttribute('id');
5887      if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
5888        return;
5889      }
5890
5891      // Get the source URL or ID
5892      let source = player.media.getAttribute('src');
5893
5894      // Get from <div> if needed
5895      if (is.empty(source)) {
5896        source = player.media.getAttribute(this.config.attributes.embed.id);
5897      }
5898
5899      // Replace the <iframe> with a <div> due to YouTube API issues
5900      const videoId = parseId(source);
5901      const id = generateId(player.provider);
5902      // Replace media element
5903      const container = createElement('div', {
5904        id,
5905        'data-poster': config.customControls ? player.poster : undefined
5906      });
5907      player.media = replaceElement(container, player.media);
5908
5909      // Only load the poster when using custom controls
5910      if (config.customControls) {
5911        const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
5912
5913        // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
5914        loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded
5915        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
5916        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
5917        .then(image => ui.setPoster.call(player, image.src)).then(src => {
5918          // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
5919          if (!src.includes('maxres')) {
5920            player.elements.poster.style.backgroundSize = 'cover';
5921          }
5922        }).catch(() => {});
5923      }
5924
5925      // Setup instance
5926      // https://developers.google.com/youtube/iframe_api_reference
5927      player.embed = new window.YT.Player(player.media, {
5928        videoId,
5929        host: getHost(config),
5930        playerVars: extend({}, {
5931          // Autoplay
5932          autoplay: player.config.autoplay ? 1 : 0,
5933          // iframe interface language
5934          hl: player.config.hl,
5935          // Only show controls if not fully supported or opted out
5936          controls: player.supported.ui && config.customControls ? 0 : 1,
5937          // Disable keyboard as we handle it
5938          disablekb: 1,
5939          // Allow iOS inline playback
5940          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,
5941          // Captions are flaky on YouTube
5942          cc_load_policy: player.captions.active ? 1 : 0,
5943          cc_lang_pref: player.config.captions.language,
5944          // Tracking for stats
5945          widget_referrer: window ? window.location.href : null
5946        }, config),
5947        events: {
5948          onError(event) {
5949            // YouTube may fire onError twice, so only handle it once
5950            if (!player.media.error) {
5951              const code = event.data;
5952              // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
5953              const message = {
5954                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
5955                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
5956                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
5957                101: 'The owner of the requested video does not allow it to be played in embedded players.',
5958                150: 'The owner of the requested video does not allow it to be played in embedded players.'
5959              }[code] || 'An unknown error occurred';
5960              player.media.error = {
5961                code,
5962                message
5963              };
5964              triggerEvent.call(player, player.media, 'error');
5965            }
5966          },
5967          onPlaybackRateChange(event) {
5968            // Get the instance
5969            const instance = event.target;
5970
5971            // Get current speed
5972            player.media.playbackRate = instance.getPlaybackRate();
5973            triggerEvent.call(player, player.media, 'ratechange');
5974          },
5975          onReady(event) {
5976            // Bail if onReady has already been called. See issue #1108
5977            if (is.function(player.media.play)) {
5978              return;
5979            }
5980            // Get the instance
5981            const instance = event.target;
5982
5983            // Get the title
5984            youtube.getTitle.call(player, videoId);
5985
5986            // Create a faux HTML5 API using the YouTube API
5987            player.media.play = () => {
5988              assurePlaybackState.call(player, true);
5989              instance.playVideo();
5990            };
5991            player.media.pause = () => {
5992              assurePlaybackState.call(player, false);
5993              instance.pauseVideo();
5994            };
5995            player.media.stop = () => {
5996              instance.stopVideo();
5997            };
5998            player.media.duration = instance.getDuration();
5999            player.media.paused = true;
6000
6001            // Seeking
6002            player.media.currentTime = 0;
6003            Object.defineProperty(player.media, 'currentTime', {
6004              get() {
6005                return Number(instance.getCurrentTime());
6006              },
6007              set(time) {
6008                // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
6009                if (player.paused && !player.embed.hasPlayed) {
6010                  player.embed.mute();
6011                }
6012
6013                // Set seeking state and trigger event
6014                player.media.seeking = true;
6015                triggerEvent.call(player, player.media, 'seeking');
6016
6017                // Seek after events sent
6018                instance.seekTo(time);
6019              }
6020            });
6021
6022            // Playback speed
6023            Object.defineProperty(player.media, 'playbackRate', {
6024              get() {
6025                return instance.getPlaybackRate();
6026              },
6027              set(input) {
6028                instance.setPlaybackRate(input);
6029              }
6030            });
6031
6032            // Volume
6033            let {
6034              volume
6035            } = player.config;
6036            Object.defineProperty(player.media, 'volume', {
6037              get() {
6038                return volume;
6039              },
6040              set(input) {
6041                volume = input;
6042                instance.setVolume(volume * 100);
6043                triggerEvent.call(player, player.media, 'volumechange');
6044              }
6045            });
6046
6047            // Muted
6048            let {
6049              muted
6050            } = player.config;
6051            Object.defineProperty(player.media, 'muted', {
6052              get() {
6053                return muted;
6054              },
6055              set(input) {
6056                const toggle = is.boolean(input) ? input : muted;
6057                muted = toggle;
6058                instance[toggle ? 'mute' : 'unMute']();
6059                instance.setVolume(volume * 100);
6060                triggerEvent.call(player, player.media, 'volumechange');
6061              }
6062            });
6063
6064            // Source
6065            Object.defineProperty(player.media, 'currentSrc', {
6066              get() {
6067                return instance.getVideoUrl();
6068              }
6069            });
6070
6071            // Ended
6072            Object.defineProperty(player.media, 'ended', {
6073              get() {
6074                return player.currentTime === player.duration;
6075              }
6076            });
6077
6078            // Get available speeds
6079            const speeds = instance.getAvailablePlaybackRates();
6080            // Filter based on config
6081            player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));
6082
6083            // Set the tabindex to avoid focus entering iframe
6084            if (player.supported.ui && config.customControls) {
6085              player.media.setAttribute('tabindex', -1);
6086            }
6087            triggerEvent.call(player, player.media, 'timeupdate');
6088            triggerEvent.call(player, player.media, 'durationchange');
6089
6090            // Reset timer
6091            clearInterval(player.timers.buffering);
6092
6093            // Setup buffering
6094            player.timers.buffering = setInterval(() => {
6095              // Get loaded % from YouTube
6096              player.media.buffered = instance.getVideoLoadedFraction();
6097
6098              // Trigger progress only when we actually buffer something
6099              if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
6100                triggerEvent.call(player, player.media, 'progress');
6101              }
6102
6103              // Set last buffer point
6104              player.media.lastBuffered = player.media.buffered;
6105
6106              // Bail if we're at 100%
6107              if (player.media.buffered === 1) {
6108                clearInterval(player.timers.buffering);
6109
6110                // Trigger event
6111                triggerEvent.call(player, player.media, 'canplaythrough');
6112              }
6113            }, 200);
6114
6115            // Rebuild UI
6116            if (config.customControls) {
6117              setTimeout(() => ui.build.call(player), 50);
6118            }
6119          },
6120          onStateChange(event) {
6121            // Get the instance
6122            const instance = event.target;
6123
6124            // Reset timer
6125            clearInterval(player.timers.playing);
6126            const seeked = player.media.seeking && [1, 2].includes(event.data);
6127            if (seeked) {
6128              // Unset seeking and fire seeked event
6129              player.media.seeking = false;
6130              triggerEvent.call(player, player.media, 'seeked');
6131            }
6132
6133            // Handle events
6134            // -1   Unstarted
6135            // 0    Ended
6136            // 1    Playing
6137            // 2    Paused
6138            // 3    Buffering
6139            // 5    Video cued
6140            switch (event.data) {
6141              case -1:
6142                // Update scrubber
6143                triggerEvent.call(player, player.media, 'timeupdate');
6144
6145                // Get loaded % from YouTube
6146                player.media.buffered = instance.getVideoLoadedFraction();
6147                triggerEvent.call(player, player.media, 'progress');
6148                break;
6149              case 0:
6150                assurePlaybackState.call(player, false);
6151
6152                // YouTube doesn't support loop for a single video, so mimick it.
6153                if (player.media.loop) {
6154                  // YouTube needs a call to `stopVideo` before playing again
6155                  instance.stopVideo();
6156                  instance.playVideo();
6157                } else {
6158                  triggerEvent.call(player, player.media, 'ended');
6159                }
6160                break;
6161              case 1:
6162                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
6163                if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
6164                  player.media.pause();
6165                } else {
6166                  assurePlaybackState.call(player, true);
6167                  triggerEvent.call(player, player.media, 'playing');
6168
6169                  // Poll to get playback progress
6170                  player.timers.playing = setInterval(() => {
6171                    triggerEvent.call(player, player.media, 'timeupdate');
6172                  }, 50);
6173
6174                  // Check duration again due to YouTube bug
6175                  // https://github.com/sampotts/plyr/issues/374
6176                  // https://code.google.com/p/gdata-issues/issues/detail?id=8690
6177                  if (player.media.duration !== instance.getDuration()) {
6178                    player.media.duration = instance.getDuration();
6179                    triggerEvent.call(player, player.media, 'durationchange');
6180                  }
6181                }
6182                break;
6183              case 2:
6184                // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
6185                if (!player.muted) {
6186                  player.embed.unMute();
6187                }
6188                assurePlaybackState.call(player, false);
6189                break;
6190              case 3:
6191                // Trigger waiting event to add loading classes to container as the video buffers.
6192                triggerEvent.call(player, player.media, 'waiting');
6193                break;
6194            }
6195            triggerEvent.call(player, player.elements.container, 'statechange', false, {
6196              code: event.data
6197            });
6198          }
6199        }
6200      });
6201    }
6202  };
6203
6204  // ==========================================================================
6205  
vendor: 24,988 bytes, lines 6205-6866
6205const media = {
6206    // Setup media
6207    setup() {
6208      // If there's no media, bail
6209      if (!this.media) {
6210        this.debug.warn('No media element found!');
6211        return;
6212      }
6213
6214      // Add type class
6215      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
6216
6217      // Add provider class
6218      toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
6219
6220      // Add video class for embeds
6221      // This will require changes if audio embeds are added
6222      if (this.isEmbed) {
6223        toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
6224      }
6225
6226      // Inject the player wrapper
6227      if (this.isVideo) {
6228        // Create the wrapper div
6229        this.elements.wrapper = createElement('div', {
6230          class: this.config.classNames.video
6231        });
6232
6233        // Wrap the video in a container
6234        wrap(this.media, this.elements.wrapper);
6235
6236        // Poster image container
6237        this.elements.poster = createElement('div', {
6238          class: this.config.classNames.poster
6239        });
6240        this.elements.wrapper.appendChild(this.elements.poster);
6241      }
6242      if (this.isHTML5) {
6243        html5.setup.call(this);
6244      } else if (this.isYouTube) {
6245        youtube.setup.call(this);
6246      } else if (this.isVimeo) {
6247        vimeo.setup.call(this);
6248      }
6249    }
6250  };
6251
6252  const destroy = instance => {
6253    // Destroy our adsManager
6254    if (instance.manager) {
6255      instance.manager.destroy();
6256    }
6257
6258    // Destroy our adsManager
6259    if (instance.elements.displayContainer) {
6260      instance.elements.displayContainer.destroy();
6261    }
6262    instance.elements.container.remove();
6263  };
6264  class Ads {
6265    /**
6266     * Ads constructor.
6267     * @param {Object} player
6268     * @return {Ads}
6269     */
6270    constructor(player) {
6271      /**
6272       * Load the IMA SDK
6273       */
6274      _defineProperty$1(this, "load", () => {
6275        if (!this.enabled) {
6276          return;
6277        }
6278
6279        // Check if the Google IMA3 SDK is loaded or load it ourselves
6280        if (!is.object(window.google) || !is.object(window.google.ima)) {
6281          loadScript(this.player.config.urls.googleIMA.sdk).then(() => {
6282            this.ready();
6283          }).catch(() => {
6284            // Script failed to load or is blocked
6285            this.trigger('error', new Error('Google IMA SDK failed to load'));
6286          });
6287        } else {
6288          this.ready();
6289        }
6290      });
6291      /**
6292       * Get the ads instance ready
6293       */
6294      _defineProperty$1(this, "ready", () => {
6295        // Double check we're enabled
6296        if (!this.enabled) {
6297          destroy(this);
6298        }
6299
6300        // Start ticking our safety timer. If the whole advertisement
6301        // thing doesn't resolve within our set time; we bail
6302        this.startSafetyTimer(12000, 'ready()');
6303
6304        // Clear the safety timer
6305        this.managerPromise.then(() => {
6306          this.clearSafetyTimer('onAdsManagerLoaded()');
6307        });
6308
6309        // Set listeners on the Plyr instance
6310        this.listeners();
6311
6312        // Setup the IMA SDK
6313        this.setupIMA();
6314      });
6315      /**
6316       * In order for the SDK to display ads for our video, we need to tell it where to put them,
6317       * so here we define our ad container. This div is set up to render on top of the video player.
6318       * Using the code below, we tell the SDK to render ads within that div. We also provide a
6319       * handle to the content video player - the SDK will poll the current time of our player to
6320       * properly place mid-rolls. After we create the ad display container, we initialize it. On
6321       * mobile devices, this initialization is done as the result of a user action.
6322       */
6323      _defineProperty$1(this, "setupIMA", () => {
6324        // Create the container for our advertisements
6325        this.elements.container = createElement('div', {
6326          class: this.player.config.classNames.ads
6327        });
6328        this.player.elements.container.appendChild(this.elements.container);
6329
6330        // So we can run VPAID2
6331        google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);
6332
6333        // Set language
6334        google.ima.settings.setLocale(this.player.config.ads.language);
6335
6336        // Set playback for iOS10+
6337        google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);
6338
6339        // We assume the adContainer is the video container of the plyr element that will house the ads
6340        this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);
6341
6342        // Create ads loader
6343        this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
6344
6345        // Listen and respond to ads loaded and error events
6346        this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);
6347        this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);
6348
6349        // Request video ads to be pre-loaded
6350        this.requestAds();
6351      });
6352      /**
6353       * Request advertisements
6354       */
6355      _defineProperty$1(this, "requestAds", () => {
6356        const {
6357          container
6358        } = this.player.elements;
6359        try {
6360          // Request video ads
6361          const request = new google.ima.AdsRequest();
6362          request.adTagUrl = this.tagUrl;
6363
6364          // Specify the linear and nonlinear slot sizes. This helps the SDK
6365          // to select the correct creative if multiple are returned
6366          request.linearAdSlotWidth = container.offsetWidth;
6367          request.linearAdSlotHeight = container.offsetHeight;
6368          request.nonLinearAdSlotWidth = container.offsetWidth;
6369          request.nonLinearAdSlotHeight = container.offsetHeight;
6370
6371          // We only overlay ads as we only support video.
6372          request.forceNonLinearFullSlot = false;
6373
6374          // Mute based on current state
6375          request.setAdWillPlayMuted(!this.player.muted);
6376          this.loader.requestAds(request);
6377        } catch (error) {
6378          this.onAdError(error);
6379        }
6380      });
6381      /**
6382       * Update the ad countdown
6383       * @param {Boolean} start
6384       */
6385      _defineProperty$1(this, "pollCountdown", (start = false) => {
6386        if (!start) {
6387          clearInterval(this.countdownTimer);
6388          this.elements.container.removeAttribute('data-badge-text');
6389          return;
6390        }
6391        const update = () => {
6392          const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));
6393          const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
6394          this.elements.container.setAttribute('data-badge-text', label);
6395        };
6396        this.countdownTimer = setInterval(update, 100);
6397      });
6398      /**
6399       * This method is called whenever the ads are ready inside the AdDisplayContainer
6400       * @param {Event} event - adsManagerLoadedEvent
6401       */
6402      _defineProperty$1(this, "onAdsManagerLoaded", event => {
6403        // Load could occur after a source change (race condition)
6404        if (!this.enabled) {
6405          return;
6406        }
6407
6408        // Get the ads manager
6409        const settings = new google.ima.AdsRenderingSettings();
6410
6411        // Tell the SDK to save and restore content video state on our behalf
6412        settings.restoreCustomPlaybackStateOnAdBreakComplete = true;
6413        settings.enablePreloading = true;
6414
6415        // The SDK is polling currentTime on the contentPlayback. And needs a duration
6416        // so it can determine when to start the mid- and post-roll
6417        this.manager = event.getAdsManager(this.player, settings);
6418
6419        // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
6420        this.cuePoints = this.manager.getCuePoints();
6421
6422        // Add listeners to the required events
6423        // Advertisement error events
6424        this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));
6425
6426        // Advertisement regular events
6427        Object.keys(google.ima.AdEvent.Type).forEach(type => {
6428          this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));
6429        });
6430
6431        // Resolve our adsManager
6432        this.trigger('loaded');
6433      });
6434      _defineProperty$1(this, "addCuePoints", () => {
6435        // Add advertisement cue's within the time line if available
6436        if (!is.empty(this.cuePoints)) {
6437          this.cuePoints.forEach(cuePoint => {
6438            if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
6439              const seekElement = this.player.elements.progress;
6440              if (is.element(seekElement)) {
6441                const cuePercentage = 100 / this.player.duration * cuePoint;
6442                const cue = createElement('span', {
6443                  class: this.player.config.classNames.cues
6444                });
6445                cue.style.left = `${cuePercentage.toString()}%`;
6446                seekElement.appendChild(cue);
6447              }
6448            }
6449          });
6450        }
6451      });
6452      /**
6453       * This is where all the event handling takes place. Retrieve the ad from the event. Some
6454       * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated
6455       * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type
6456       * @param {Event} event
6457       */
6458      _defineProperty$1(this, "onAdEvent", event => {
6459        const {
6460          container
6461        } = this.player.elements;
6462        // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
6463        // don't have ad object associated
6464        const ad = event.getAd();
6465        const adData = event.getAdData();
6466
6467        // Proxy event
6468        const dispatchEvent = type => {
6469          triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);
6470        };
6471
6472        // Bubble the event
6473        dispatchEvent(event.type);
6474        switch (event.type) {
6475          case google.ima.AdEvent.Type.LOADED:
6476            // This is the first event sent for an ad - it is possible to determine whether the
6477            // ad is a video ad or an overlay
6478            this.trigger('loaded');
6479
6480            // Start countdown
6481            this.pollCountdown(true);
6482            if (!ad.isLinear()) {
6483              // Position AdDisplayContainer correctly for overlay
6484              ad.width = container.offsetWidth;
6485              ad.height = container.offsetHeight;
6486            }
6487
6488            // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
6489            // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
6490
6491            break;
6492          case google.ima.AdEvent.Type.STARTED:
6493            // Set volume to match player
6494            this.manager.setVolume(this.player.volume);
6495            break;
6496          case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
6497            // All ads for the current videos are done. We can now request new advertisements
6498            // in case the video is re-played
6499
6500            // TODO: Example for what happens when a next video in a playlist would be loaded.
6501            // So here we load a new video when all ads are done.
6502            // Then we load new ads within a new adsManager. When the video
6503            // Is started - after - the ads are loaded, then we get ads.
6504            // You can also easily test cancelling and reloading by running
6505            // player.ads.cancel() and player.ads.play from the console I guess.
6506            // this.player.source = {
6507            //     type: 'video',
6508            //     title: 'View From A Blue Moon',
6509            //     sources: [{
6510            //         src:
6511            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:
6512            // 'video/mp4', }], poster:
6513            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:
6514            // [ { kind: 'captions', label: 'English', srclang: 'en', src:
6515            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
6516            // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:
6517            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],
6518            // };
6519
6520            // TODO: So there is still this thing where a video should only be allowed to start
6521            // playing when the IMA SDK is ready or has failed
6522
6523            if (this.player.ended) {
6524              this.loadAds();
6525            } else {
6526              // The SDK won't allow new ads to be called without receiving a contentComplete()
6527              this.loader.contentComplete();
6528            }
6529            break;
6530          case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
6531            // This event indicates the ad has started - the video player can adjust the UI,
6532            // for example display a pause button and remaining time. Fired when content should
6533            // be paused. This usually happens right before an ad is about to cover the content
6534
6535            this.pauseContent();
6536            break;
6537          case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
6538            // This event indicates the ad has finished - the video player can perform
6539            // appropriate UI actions, such as removing the timer for remaining time detection.
6540            // Fired when content should be resumed. This usually happens when an ad finishes
6541            // or collapses
6542
6543            this.pollCountdown();
6544            this.resumeContent();
6545            break;
6546          case google.ima.AdEvent.Type.LOG:
6547            if (adData.adError) {
6548              this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);
6549            }
6550            break;
6551        }
6552      });
6553      /**
6554       * Any ad error handling comes through here
6555       * @param {Event} event
6556       */
6557      _defineProperty$1(this, "onAdError", event => {
6558        this.cancel();
6559        this.player.debug.warn('Ads error', event);
6560      });
6561      /**
6562       * Setup hooks for Plyr and window events. This ensures
6563       * the mid- and post-roll launch at the correct time. And
6564       * resize the advertisement when the player resizes
6565       */
6566      _defineProperty$1(this, "listeners", () => {
6567        const {
6568          container
6569        } = this.player.elements;
6570        let time;
6571        this.player.on('canplay', () => {
6572          this.addCuePoints();
6573        });
6574        this.player.on('ended', () => {
6575          this.loader.contentComplete();
6576        });
6577        this.player.on('timeupdate', () => {
6578          time = this.player.currentTime;
6579        });
6580        this.player.on('seeked', () => {
6581          const seekedTime = this.player.currentTime;
6582          if (is.empty(this.cuePoints)) {
6583            return;
6584          }
6585          this.cuePoints.forEach((cuePoint, index) => {
6586            if (time < cuePoint && cuePoint < seekedTime) {
6587              this.manager.discardAdBreak();
6588              this.cuePoints.splice(index, 1);
6589            }
6590          });
6591        });
6592
6593        // Listen to the resizing of the window. And resize ad accordingly
6594        // TODO: eventually implement ResizeObserver
6595        window.addEventListener('resize', () => {
6596          if (this.manager) {
6597            this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
6598          }
6599        });
6600      });
6601      /**
6602       * Initialize the adsManager and start playing advertisements
6603       */
6604      _defineProperty$1(this, "play", () => {
6605        const {
6606          container
6607        } = this.player.elements;
6608        if (!this.managerPromise) {
6609          this.resumeContent();
6610        }
6611
6612        // Play the requested advertisement whenever the adsManager is ready
6613        this.managerPromise.then(() => {
6614          // Set volume to match player
6615          this.manager.setVolume(this.player.volume);
6616
6617          // Initialize the container. Must be done via a user action on mobile devices
6618          this.elements.displayContainer.initialize();
6619          try {
6620            if (!this.initialized) {
6621              // Initialize the ads manager. Ad rules playlist will start at this time
6622              this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
6623
6624              // Call play to start showing the ad. Single video and overlay ads will
6625              // start at this time; the call will be ignored for ad rules
6626              this.manager.start();
6627            }
6628            this.initialized = true;
6629          } catch (adError) {
6630            // An error may be thrown if there was a problem with the
6631            // VAST response
6632            this.onAdError(adError);
6633          }
6634        }).catch(() => {});
6635      });
6636      /**
6637       * Resume our video
6638       */
6639      _defineProperty$1(this, "resumeContent", () => {
6640        // Hide the advertisement container
6641        this.elements.container.style.zIndex = '';
6642
6643        // Ad is stopped
6644        this.playing = false;
6645
6646        // Play video
6647        silencePromise(this.player.media.play());
6648      });
6649      /**
6650       * Pause our video
6651       */
6652      _defineProperty$1(this, "pauseContent", () => {
6653        // Show the advertisement container
6654        this.elements.container.style.zIndex = 3;
6655
6656        // Ad is playing
6657        this.playing = true;
6658
6659        // Pause our video.
6660        this.player.media.pause();
6661      });
6662      /**
6663       * Destroy the adsManager so we can grab new ads after this. If we don't then we're not
6664       * allowed to call new ads based on google policies, as they interpret this as an accidental
6665       * video requests. https://developers.google.com/interactive-
6666       * media-ads/docs/sdks/android/faq#8
6667       */
6668      _defineProperty$1(this, "cancel", () => {
6669        // Pause our video
6670        if (this.initialized) {
6671          this.resumeContent();
6672        }
6673
6674        // Tell our instance that we're done for now
6675        this.trigger('error');
6676
6677        // Re-create our adsManager
6678        this.loadAds();
6679      });
6680      /**
6681       * Re-create our adsManager
6682       */
6683      _defineProperty$1(this, "loadAds", () => {
6684        // Tell our adsManager to go bye bye
6685        this.managerPromise.then(() => {
6686          // Destroy our adsManager
6687          if (this.manager) {
6688            this.manager.destroy();
6689          }
6690
6691          // Re-set our adsManager promises
6692          this.managerPromise = new Promise(resolve => {
6693            this.on('loaded', resolve);
6694            this.player.debug.log(this.manager);
6695          });
6696          // Now that the manager has been destroyed set it to also be un-initialized
6697          this.initialized = false;
6698
6699          // Now request some new advertisements
6700          this.requestAds();
6701        }).catch(() => {});
6702      });
6703      /**
6704       * Handles callbacks after an ad event was invoked
6705       * @param {String} event - Event type
6706       * @param args
6707       */
6708      _defineProperty$1(this, "trigger", (event, ...args) => {
6709        const handlers = this.events[event];
6710        if (is.array(handlers)) {
6711          handlers.forEach(handler => {
6712            if (is.function(handler)) {
6713              handler.apply(this, args);
6714            }
6715          });
6716        }
6717      });
6718      /**
6719       * Add event listeners
6720       * @param {String} event - Event type
6721       * @param {Function} callback - Callback for when event occurs
6722       * @return {Ads}
6723       */
6724      _defineProperty$1(this, "on", (event, callback) => {
6725        if (!is.array(this.events[event])) {
6726          this.events[event] = [];
6727        }
6728        this.events[event].push(callback);
6729        return this;
6730      });
6731      /**
6732       * Setup a safety timer for when the ad network doesn't respond for whatever reason.
6733       * The advertisement has 12 seconds to get its things together. We stop this timer when the
6734       * advertisement is playing, or when a user action is required to start, then we clear the
6735       * timer on ad ready
6736       * @param {Number} time
6737       * @param {String} from
6738       */
6739      _defineProperty$1(this, "startSafetyTimer", (time, from) => {
6740        this.player.debug.log(`Safety timer invoked from: ${from}`);
6741        this.safetyTimer = setTimeout(() => {
6742          this.cancel();
6743          this.clearSafetyTimer('startSafetyTimer()');
6744        }, time);
6745      });
6746      /**
6747       * Clear our safety timer(s)
6748       * @param {String} from
6749       */
6750      _defineProperty$1(this, "clearSafetyTimer", from => {
6751        if (!is.nullOrUndefined(this.safetyTimer)) {
6752          this.player.debug.log(`Safety timer cleared from: ${from}`);
6753          clearTimeout(this.safetyTimer);
6754          this.safetyTimer = null;
6755        }
6756      });
6757      this.player = player;
6758      this.config = player.config.ads;
6759      this.playing = false;
6760      this.initialized = false;
6761      this.elements = {
6762        container: null,
6763        displayContainer: null
6764      };
6765      this.manager = null;
6766      this.loader = null;
6767      this.cuePoints = null;
6768      this.events = {};
6769      this.safetyTimer = null;
6770      this.countdownTimer = null;
6771
6772      // Setup a promise to resolve when the IMA manager is ready
6773      this.managerPromise = new Promise((resolve, reject) => {
6774        // The ad is loaded and ready
6775        this.on('loaded', resolve);
6776
6777        // Ads failed
6778        this.on('error', reject);
6779      });
6780      this.load();
6781    }
6782    get enabled() {
6783      const {
6784        config
6785      } = this;
6786      return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));
6787    }
6788    // Build the tag URL
6789    get tagUrl() {
6790      const {
6791        config
6792      } = this;
6793      if (is.url(config.tagUrl)) {
6794        return config.tagUrl;
6795      }
6796      const params = {
6797        AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
6798        AV_CHANNELID: '5a0458dc28a06145e4519d21',
6799        AV_URL: window.location.hostname,
6800        cb: Date.now(),
6801        AV_WIDTH: 640,
6802        AV_HEIGHT: 480,
6803        AV_CDIM2: config.publisherId
6804      };
6805      const base = 'https://go.aniview.com/api/adserver6/vast/';
6806      return `${base}?${buildUrlParams(params)}`;
6807    }
6808  }
6809
6810  /**
6811   * Returns a number whose value is limited to the given range.
6812   *
6813   * Example: limit the output of this computation to between 0 and 255
6814   * (x * 255).clamp(0, 255)
6815   *
6816   * @param {Number} input
6817   * @param {Number} min The lower boundary of the output range
6818   * @param {Number} max The upper boundary of the output range
6819   * @returns A number within the bounds of min and max
6820   * @type Number
6821   */
6822  function clamp(input = 0, min = 0, max = 255) {
6823    return Math.min(Math.max(input, min), max);
6824  }
6825
6826  // Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg"
6827  const parseVtt = vttDataString => {
6828    const processedList = [];
6829    const frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
6830    frames.forEach(frame => {
6831      const result = {};
6832      const lines = frame.split(/\r\n|\n|\r/);
6833      lines.forEach(line => {
6834        if (!is.number(result.startTime)) {
6835          // The line with start and end times on it is the first line of interest
6836          const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
6837
6838          if (matchTimes) {
6839            result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);
6840            result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);
6841          }
6842        } else if (!is.empty(line.trim()) && is.empty(result.text)) {
6843          // If we already have the startTime, then we're definitely up to the text line(s)
6844          const lineSplit = line.trim().split('#xywh=');
6845          [result.text] = lineSplit;
6846
6847          // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
6848          if (lineSplit[1]) {
6849            [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');
6850          }
6851        }
6852      });
6853      if (result.text) {
6854        processedList.push(result);
6855      }
6856    });
6857    return processedList;
6858  };
6859
6860  /**
6861   * Preview thumbnails for seek hover and scrubbing
6862   * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
6863   * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
6864   *
6865   * Notes:
6866   * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property w
6866ould be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
6867   * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
6868   * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
6869   */
6870
6871  const fitRatio = (ratio, outer) => {
6872    const targetRatio = outer.width / outer.height;
6873    const result = {};
6874    if (ratio > targetRatio) {
6875      result.width = outer.width;
6876      result.height = 1 / ratio * outer.width;
6877    } else {
6878      result.height = outer.height;
6879      result.width = ratio * outer.height;
6880    }
6881    return result;
6882  };
6883  class PreviewThumbnails {
6884    /**
6885     * PreviewThumbnails constructor.
6886     * @param {Plyr} player
6887     * @return {PreviewThumbnails}
6888     */
6889    constructor(player) {
6890      _defineProperty$1(this, "load", () => {
6891        // Toggle the regular seek tooltip
6892        if (this.player.elements.display.seekTooltip) {
6893          this.player.elements.display.seekTooltip.hidden = this.enabled;
6894        }
6895        if (!this.enabled) return;
6896        this.getThumbnails().then(() => {
6897          if (!this.enabled) {
6898            return;
6899          }
6900
6901          // Render DOM elements
6902          this.render();
6903
6904          // Check to see if thumb container size was specified manually in CSS
6905          this.determineContainerAutoSizing();
6906
6907          // Set up listeners
6908          this.listeners();
6909          this.loaded = true;
6910        });
6911      });
6912      // Download VTT files and parse them
6913      _defineProperty$1(this, "getThumbnails", () => {
6914        return new Promise(resolve => {
6915          const {
6916            src
6917          } = this.player.config.previewThumbnails;
6918          if (is.empty(src)) {
6919            throw new Error('Missing previewThumbnails.src config attribute');
6920          }
6921
6922          // Resolve promise
6923          const sortAndResolve = () => {
6924            // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
6925            this.thumbnails.sort((x, y) => x.height - y.height);
6926            this.player.debug.log('Preview thumbnails', this.thumbnails);
6927            resolve();
6928          };
6929
6930          // Via callback()
6931          if (is.function(src)) {
6932            src(thumbnails => {
6933              this.thumbnails = thumbnails;
6934              sortAndResolve();
6935            });
6936          }
6937          // VTT urls
6938          else {
6939            // If string, convert into single-element list
6940            const urls = is.string(src) ? [src] : src;
6941            // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
6942            const promises = urls.map(u => this.getThumbnail(u));
6943            // Resolve
6944            Promise.all(promises).then(sortAndResolve);
6945          }
6946        });
6947      });
6948      // Process individual VTT file
6949      _defineProperty$1(this, "getThumbnail", url => {
6950        return new Promise(resolve => {
6951          fetch(url).then(response => {
6952            const thumbnail = {
6953              frames: parseVtt(response),
6954              height: null,
6955              urlPrefix: ''
6956            };
6957
6958            // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
6959            // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
vendor: 2,335 bytes, lines 6960-7003
6960            // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file
6961            if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {
6962              thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
6963            }
6964
6965            // Download the first frame, so that we can determine/set the height of this thumbnailsDef
6966            const tempImage = new Image();
6967            tempImage.onload = () => {
6968              thumbnail.height = tempImage.naturalHeight;
6969              thumbnail.width = tempImage.naturalWidth;
6970              this.thumbnails.push(thumbnail);
6971              resolve();
6972            };
6973            tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
6974          });
6975        });
6976      });
6977      _defineProperty$1(this, "startMove", event => {
6978        if (!this.loaded) return;
6979        if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;
6980
6981        // Wait until media has a duration
6982        if (!this.player.media.duration) return;
6983        if (event.type === 'touchmove') {
6984          // Calculate seek hover position as approx video seconds
6985          this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
6986        } else {
6987          var _this$player$config$m, _this$player$config$m2;
6988          // Calculate seek hover position as approx video seconds
6989          const clientRect = this.player.elements.progress.getBoundingClientRect();
6990          const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);
6991          this.seekTime = this.player.media.duration * (percentage / 100);
6992          if (this.seekTime < 0) {
6993            // The mousemove fires for 10+px out to the left
6994            this.seekTime = 0;
6995          }
6996          if (this.seekTime > this.player.media.duration - 1) {
6997            // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
6998            this.seekTime = this.player.media.duration - 1;
6999          }
7000          this.mousePosX = event.pageX;
7001
7002          // Set time text inside image container
7003          this.elements.thumb.time.innerTex
vendor: 10,103 bytes, lines 7003-7212
7003t = formatTime(this.seekTime);
7004
7005          // Get marker point for time
7006          const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({
7007            time: t
7008          }) => t === Math.round(this.seekTime));
7009
7010          // Append the point label to the tooltip
7011          if (point) {
7012            // this.elements.thumb.time.innerText.concat('\n');
7013            this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);
7014          }
7015        }
7016
7017        // Download and show image
7018        this.showImageAtCurrentTime();
7019      });
7020      _defineProperty$1(this, "endMove", () => {
7021        this.toggleThumbContainer(false, true);
7022      });
7023      _defineProperty$1(this, "startScrubbing", event => {
7024        // Only act on left mouse button (0), or touch device (event.button does not exist or is false)
7025        if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {
7026          this.mouseDown = true;
7027
7028          // Wait until media has a duration
7029          if (this.player.media.duration) {
7030            this.toggleScrubbingContainer(true);
7031            this.toggleThumbContainer(false, true);
7032
7033            // Download and show image
7034            this.showImageAtCurrentTime();
7035          }
7036        }
7037      });
7038      _defineProperty$1(this, "endScrubbing", () => {
7039        this.mouseDown = false;
7040
7041        // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
7042        if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
7043          // The video was already seeked/loaded at the chosen time - hide immediately
7044          this.toggleScrubbingContainer(false);
7045        } else {
7046          // The video hasn't seeked yet. Wait for that
7047          once.call(this.player, this.player.media, 'timeupdate', () => {
7048            // Re-check mousedown - we might have already started scrubbing again
7049            if (!this.mouseDown) {
7050              this.toggleScrubbingContainer(false);
7051            }
7052          });
7053        }
7054      });
7055      /**
7056       * Setup hooks for Plyr and window events
7057       */
7058      _defineProperty$1(this, "listeners", () => {
7059        // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
7060        this.player.on('play', () => {
7061          this.toggleThumbContainer(false, true);
7062        });
7063        this.player.on('seeked', () => {
7064          this.toggleThumbContainer(false);
7065        });
7066        this.player.on('timeupdate', () => {
7067          this.lastTime = this.player.media.currentTime;
7068        });
7069      });
7070      /**
7071       * Create HTML elements for image containers
7072       */
7073      _defineProperty$1(this, "render", () => {
7074        // Create HTML element: plyr__preview-thumbnail-container
7075        this.elements.thumb.container = createElement('div', {
7076          class: this.player.config.classNames.previewThumbnails.thumbContainer
7077        });
7078
7079        // Wrapper for the image for styling
7080        this.elements.thumb.imageContainer = createElement('div', {
7081          class: this.player.config.classNames.previewThumbnails.imageContainer
7082        });
7083        this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);
7084
7085        // Create HTML element, parent+span: time text (e.g., 01:32:00)
7086        const timeContainer = createElement('div', {
7087          class: this.player.config.classNames.previewThumbnails.timeContainer
7088        });
7089        this.elements.thumb.time = createElement('span', {}, '00:00');
7090        timeContainer.appendChild(this.elements.thumb.time);
7091        this.elements.thumb.imageContainer.appendChild(timeContainer);
7092
7093        // Inject the whole thumb
7094        if (is.element(this.player.elements.progress)) {
7095          this.player.elements.progress.appendChild(this.elements.thumb.container);
7096        }
7097
7098        // Create HTML element: plyr__preview-scrubbing-container
7099        this.elements.scrubbing.container = createElement('div', {
7100          class: this.player.config.classNames.previewThumbnails.scrubbingContainer
7101        });
7102        this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
7103      });
7104      _defineProperty$1(this, "destroy", () => {
7105        if (this.elements.thumb.container) {
7106          this.elements.thumb.container.remove();
7107        }
7108        if (this.elements.scrubbing.container) {
7109          this.elements.scrubbing.container.remove();
7110        }
7111      });
7112      _defineProperty$1(this, "showImageAtCurrentTime", () => {
7113        if (this.mouseDown) {
7114          this.setScrubbingContainerSize();
7115        } else {
7116          this.setThumbContainerSizeAndPos();
7117        }
7118
7119        // Find the desired thumbnail index
7120        // TODO: Handle a video longer than the thumbs where thumbNum is null
7121        const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);
7122        const hasThumb = thumbNum >= 0;
7123        let qualityIndex = 0;
7124
7125        // Show the thumb container if we're not scrubbing
7126        if (!this.mouseDown) {
7127          this.toggleThumbContainer(hasThumb);
7128        }
7129
7130        // No matching thumb found
7131        if (!hasThumb) {
7132          return;
7133        }
7134
7135        // Check to see if we've already downloaded higher quality versions of this image
7136        this.thumbnails.forEach((thumbnail, index) => {
7137          if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
7138            qualityIndex = index;
7139          }
7140        });
7141
7142        // Only proceed if either thumb num or thumbfilename has changed
7143        if (thumbNum !== this.showingThumb) {
7144          this.showingThumb = thumbNum;
7145          this.loadImage(qualityIndex);
7146        }
7147      });
7148      // Show the image that's currently specified in this.showingThumb
7149      _defineProperty$1(this, "loadImage", (qualityIndex = 0) => {
7150        const thumbNum = this.showingThumb;
7151        const thumbnail = this.thumbnails[qualityIndex];
7152        const {
7153          urlPrefix
7154        } = thumbnail;
7155        const frame = thumbnail.frames[thumbNum];
7156        const thumbFilename = thumbnail.frames[thumbNum].text;
7157        const thumbUrl = urlPrefix + thumbFilename;
7158        if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
7159          // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
7160          // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
7161          if (this.loadingImage && this.usingSprites) {
7162            this.loadingImage.onload = null;
7163          }
7164
7165          // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
7166          // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
7167          // images causes a flicker. Putting a new image over the top does not
7168          const previewImage = new Image();
7169          previewImage.src = thumbUrl;
7170          previewImage.dataset.index = thumbNum;
7171          previewImage.dataset.filename = thumbFilename;
7172          this.showingThumbFilename = thumbFilename;
7173          this.player.debug.log(`Loading image: ${thumbUrl}`);
7174
7175          // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
7176          previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
7177          this.loadingImage = previewImage;
7178          this.removeOldImages(previewImage);
7179        } else {
7180          // Update the existing image
7181          this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
7182          this.currentImageElement.dataset.index = thumbNum;
7183          this.removeOldImages(this.currentImageElement);
7184        }
7185      });
7186      _defineProperty$1(this, "showImage", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {
7187        this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);
7188        this.setImageSizeAndOffset(previewImage, frame);
7189        if (newImage) {
7190          this.currentImageContainer.appendChild(previewImage);
7191          this.currentImageElement = previewImage;
7192          if (!this.loadedImages.includes(thumbFilename)) {
7193            this.loadedImages.push(thumbFilename);
7194          }
7195        }
7196
7197        // Preload images before and after the current one
7198        // Show higher quality of the same frame
7199        // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
7200        this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
7201      });
7202      // Remove all preview images that aren't the designated current image
7203      _defineProperty$1(this, "removeOldImages", currentImage => {
7204        // Get a list of all images, convert it from a DOM list to an array
7205        Array.from(this.currentImageContainer.children).forEach(image => {
7206          if (image.tagName.toLowerCase() !== 'img') {
7207            return;
7208          }
7209          const removeDelay = this.usingSprites ? 500 : 1000;
7210          if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
7211            // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
7212            // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
7213            // eslint-disable-next-line no-param-reassign
vendor: 8,107 bytes, lines 7213-7370
7213
7214            image.dataset.deleting = true;
7215
7216            // This has to be set before the timeout - to prevent issues switching between hover and scrub
7217            const {
7218              currentImageContainer
7219            } = this;
7220            setTimeout(() => {
7221              currentImageContainer.removeChild(image);
7222              this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);
7223            }, removeDelay);
7224          }
7225        });
7226      });
7227      // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
7228      // This will only preload the lowest quality
7229      _defineProperty$1(this, "preloadNearby", (thumbNum, forward = true) => {
7230        return new Promise(resolve => {
7231          setTimeout(() => {
7232            const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;
7233            if (this.showingThumbFilename === oldThumbFilename) {
7234              // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
7235              let thumbnailsClone;
7236              if (forward) {
7237                thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);
7238              } else {
7239                thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();
7240              }
7241              let foundOne = false;
7242              thumbnailsClone.forEach(frame => {
7243                const newThumbFilename = frame.text;
7244                if (newThumbFilename !== oldThumbFilename) {
7245                  // Found one with a different filename. Make sure it hasn't already been loaded on this page visit
7246                  if (!this.loadedImages.includes(newThumbFilename)) {
7247                    foundOne = true;
7248                    this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);
7249                    const {
7250                      urlPrefix
7251                    } = this.thumbnails[0];
7252                    const thumbURL = urlPrefix + newThumbFilename;
7253                    const previewImage = new Image();
7254                    previewImage.src = thumbURL;
7255                    previewImage.onload = () => {
7256                      this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);
7257                      if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);
7258
7259                      // We don't resolve until the thumb is loaded
7260                      resolve();
7261                    };
7262                  }
7263                }
7264              });
7265
7266              // If there are none to preload then we want to resolve immediately
7267              if (!foundOne) {
7268                resolve();
7269              }
7270            }
7271          }, 300);
7272        });
7273      });
7274      // If user has been hovering current image for half a second, look for a higher quality one
7275      _defineProperty$1(this, "getHigherQuality", (currentQualityIndex, previewImage, frame, thumbFilename) => {
7276        if (currentQualityIndex < this.thumbnails.length - 1) {
7277          // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
7278          let previewImageHeight = previewImage.naturalHeight;
7279          if (this.usingSprites) {
7280            previewImageHeight = frame.h;
7281          }
7282          if (previewImageHeight < this.thumbContainerHeight) {
7283            // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
7284            setTimeout(() => {
7285              // Make sure the mouse hasn't already moved on and started hovering at another image
7286              if (this.showingThumbFilename === thumbFilename) {
7287                this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);
7288                this.loadImage(currentQualityIndex + 1);
7289              }
7290            }, 300);
7291          }
7292        }
7293      });
7294      _defineProperty$1(this, "toggleThumbContainer", (toggle = false, clearShowing = false) => {
7295        const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
7296        this.elements.thumb.container.classList.toggle(className, toggle);
7297        if (!toggle && clearShowing) {
7298          this.showingThumb = null;
7299          this.showingThumbFilename = null;
7300        }
7301      });
7302      _defineProperty$1(this, "toggleScrubbingContainer", (toggle = false) => {
7303        const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
7304        this.elements.scrubbing.container.classList.toggle(className, toggle);
7305        if (!toggle) {
7306          this.showingThumb = null;
7307          this.showingThumbFilename = null;
7308        }
7309      });
7310      _defineProperty$1(this, "determineContainerAutoSizing", () => {
7311        if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {
7312          // This will prevent auto sizing in this.setThumbContainerSizeAndPos()
7313          this.sizeSpecifiedInCSS = true;
7314        }
7315      });
7316      // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
7317      _defineProperty$1(this, "setThumbContainerSizeAndPos", () => {
7318        const {
7319          imageContainer
7320        } = this.elements.thumb;
7321        if (!this.sizeSpecifiedInCSS) {
7322          const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
7323          imageContainer.style.height = `${this.thumbContainerHeight}px`;
7324          imageContainer.style.width = `${thumbWidth}px`;
7325        } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {
7326          const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);
7327          imageContainer.style.width = `${thumbWidth}px`;
7328        } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {
7329          const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);
7330          imageContainer.style.height = `${thumbHeight}px`;
7331        }
7332        this.setThumbContainerPos();
7333      });
7334      _defineProperty$1(this, "setThumbContainerPos", () => {
7335        const scrubberRect = this.player.elements.progress.getBoundingClientRect();
7336        const containerRect = this.player.elements.container.getBoundingClientRect();
7337        const {
7338          container
7339        } = this.elements.thumb;
7340        // Find the lowest and highest desired left-position, so we don't slide out the side of the video container
7341        const min = containerRect.left - scrubberRect.left + 10;
7342        const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;
7343        // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
7344        const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;
7345        const clamped = clamp(position, min, max);
7346
7347        // Move the popover position
7348        container.style.left = `${clamped}px`;
7349
7350        // The arrow can follow the cursor
7351        container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);
7352      });
7353      // Can't use 100% width, in case the video is a different aspect ratio to the video container
7354      _defineProperty$1(this, "setScrubbingContainerSize", () => {
7355        const {
7356          width,
7357          height
7358        } = fitRatio(this.thumbAspectRatio, {
7359          width: this.player.media.clientWidth,
7360          height: this.player.media.clientHeight
7361        });
7362        this.elements.scrubbing.container.style.width = `${width}px`;
7363        this.elements.scrubbing.container.style.height = `${height}px`;
7364      });
7365      // Sprites need to be offset to the correct location
7366      _defineProperty$1(this, "setImageSizeAndOffset", (previewImage, frame) => {
7367        if (!this.usingSprites) return;
7368
7369        // Find difference between height and preview container height
7370        const multiplier = this.thumbContainerHeight / frame.h;
vendor: 16,285 bytes, lines 7371-7873
7371
7372        // eslint-disable-next-line no-param-reassign
7373        previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;
7374        // eslint-disable-next-line no-param-reassign
7375        previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;
7376        // eslint-disable-next-line no-param-reassign
7377        previewImage.style.left = `-${frame.x * multiplier}px`;
7378        // eslint-disable-next-line no-param-reassign
7379        previewImage.style.top = `-${frame.y * multiplier}px`;
7380      });
7381      this.player = player;
7382      this.thumbnails = [];
7383      this.loaded = false;
7384      this.lastMouseMoveTime = Date.now();
7385      this.mouseDown = false;
7386      this.loadedImages = [];
7387      this.elements = {
7388        thumb: {},
7389        scrubbing: {}
7390      };
7391      this.load();
7392    }
7393    get enabled() {
7394      return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
7395    }
7396    get currentImageContainer() {
7397      return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;
7398    }
7399    get usingSprites() {
7400      return Object.keys(this.thumbnails[0].frames[0]).includes('w');
7401    }
7402    get thumbAspectRatio() {
7403      if (this.usingSprites) {
7404        return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
7405      }
7406      return this.thumbnails[0].width / this.thumbnails[0].height;
7407    }
7408    get thumbContainerHeight() {
7409      if (this.mouseDown) {
7410        const {
7411          height
7412        } = fitRatio(this.thumbAspectRatio, {
7413          width: this.player.media.clientWidth,
7414          height: this.player.media.clientHeight
7415        });
7416        return height;
7417      }
7418
7419      // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)
7420      if (this.sizeSpecifiedInCSS) {
7421        return this.elements.thumb.imageContainer.clientHeight;
7422      }
7423      return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
7424    }
7425    get currentImageElement() {
7426      return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;
7427    }
7428    set currentImageElement(element) {
7429      if (this.mouseDown) {
7430        this.currentScrubbingImageElement = element;
7431      } else {
7432        this.currentThumbnailImageElement = element;
7433      }
7434    }
7435  }
7436
7437  // ==========================================================================
7438  const source = {
7439    // Add elements to HTML5 media (source, tracks, etc)
7440    insertElements(type, attributes) {
7441      if (is.string(attributes)) {
7442        insertElement(type, this.media, {
7443          src: attributes
7444        });
7445      } else if (is.array(attributes)) {
7446        attributes.forEach(attribute => {
7447          insertElement(type, this.media, attribute);
7448        });
7449      }
7450    },
7451    // Update source
7452    // Sources are not checked for support so be careful
7453    change(input) {
7454      if (!getDeep(input, 'sources.length')) {
7455        this.debug.warn('Invalid source format');
7456        return;
7457      }
7458
7459      // Cancel current network requests
7460      html5.cancelRequests.call(this);
7461
7462      // Destroy instance and re-setup
7463      this.destroy.call(this, () => {
7464        // Reset quality options
7465        this.options.quality = [];
7466
7467        // Remove elements
7468        removeElement(this.media);
7469        this.media = null;
7470
7471        // Reset class name
7472        if (is.element(this.elements.container)) {
7473          this.elements.container.removeAttribute('class');
7474        }
7475
7476        // Set the type and provider
7477        const {
7478          sources,
7479          type
7480        } = input;
7481        const [{
7482          provider = providers.html5,
7483          src
7484        }] = sources;
7485        const tagName = provider === 'html5' ? type : 'div';
7486        const attributes = provider === 'html5' ? {} : {
7487          src
7488        };
7489        Object.assign(this, {
7490          provider,
7491          type,
7492          // Check for support
7493          supported: support.check(type, provider, this.config.playsinline),
7494          // Create new element
7495          media: createElement(tagName, attributes)
7496        });
7497
7498        // Inject the new element
7499        this.elements.container.appendChild(this.media);
7500
7501        // Autoplay the new source?
7502        if (is.boolean(input.autoplay)) {
7503          this.config.autoplay = input.autoplay;
7504        }
7505
7506        // Set attributes for audio and video
7507        if (this.isHTML5) {
7508          if (this.config.crossorigin) {
7509            this.media.setAttribute('crossorigin', '');
7510          }
7511          if (this.config.autoplay) {
7512            this.media.setAttribute('autoplay', '');
7513          }
7514          if (!is.empty(input.poster)) {
7515            this.poster = input.poster;
7516          }
7517          if (this.config.loop.active) {
7518            this.media.setAttribute('loop', '');
7519          }
7520          if (this.config.muted) {
7521            this.media.setAttribute('muted', '');
7522          }
7523          if (this.config.playsinline) {
7524            this.media.setAttribute('playsinline', '');
7525          }
7526        }
7527
7528        // Restore class hook
7529        ui.addStyleHook.call(this);
7530
7531        // Set new sources for html5
7532        if (this.isHTML5) {
7533          source.insertElements.call(this, 'source', sources);
7534        }
7535
7536        // Set video title
7537        this.config.title = input.title;
7538
7539        // Set up from scratch
7540        media.setup.call(this);
7541
7542        // HTML5 stuff
7543        if (this.isHTML5) {
7544          // Setup captions
7545          if (Object.keys(input).includes('tracks')) {
7546            source.insertElements.call(this, 'track', input.tracks);
7547          }
7548        }
7549
7550        // If HTML5 or embed but not fully supported, setupInterface and call ready now
7551        if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
7552          // Setup interface
7553          ui.build.call(this);
7554        }
7555
7556        // Load HTML5 sources
7557        if (this.isHTML5) {
7558          this.media.load();
7559        }
7560
7561        // Update previewThumbnails config & reload plugin
7562        if (!is.empty(input.previewThumbnails)) {
7563          Object.assign(this.config.previewThumbnails, input.previewThumbnails);
7564
7565          // Cleanup previewThumbnails plugin if it was loaded
7566          if (this.previewThumbnails && this.previewThumbnails.loaded) {
7567            this.previewThumbnails.destroy();
7568            this.previewThumbnails = null;
7569          }
7570
7571          // Create new instance if it is still enabled
7572          if (this.config.previewThumbnails.enabled) {
7573            this.previewThumbnails = new PreviewThumbnails(this);
7574          }
7575        }
7576
7577        // Update the fullscreen support
7578        this.fullscreen.update();
7579      }, true);
7580    }
7581  };
7582
7583  // Private properties
7584  // TODO: Use a WeakMap for private globals
7585  // const globals = new WeakMap();
7586
7587  // Plyr instance
7588  class Plyr {
7589    constructor(target, options) {
7590      /**
7591       * Play the media, or play the advertisement (if they are not blocked)
7592       */
7593      _defineProperty$1(this, "play", () => {
7594        if (!is.function(this.media.play)) {
7595          return null;
7596        }
7597
7598        // Intecept play with ads
7599        if (this.ads && this.ads.enabled) {
7600          this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));
7601        }
7602
7603        // Return the promise (for HTML5)
7604        return this.media.play();
7605      });
7606      /**
7607       * Pause the media
7608       */
7609      _defineProperty$1(this, "pause", () => {
7610        if (!this.playing || !is.function(this.media.pause)) {
7611          return null;
7612        }
7613        return this.media.pause();
7614      });
7615      /**
7616       * Toggle playback based on current status
7617       * @param {Boolean} input
7618       */
7619      _defineProperty$1(this, "togglePlay", input => {
7620        // Toggle based on current state if nothing passed
7621        const toggle = is.boolean(input) ? input : !this.playing;
7622        if (toggle) {
7623          return this.play();
7624        }
7625        return this.pause();
7626      });
7627      /**
7628       * Stop playback
7629       */
7630      _defineProperty$1(this, "stop", () => {
7631        if (this.isHTML5) {
7632          this.pause();
7633          this.restart();
7634        } else if (is.function(this.media.stop)) {
7635          this.media.stop();
7636        }
7637      });
7638      /**
7639       * Restart playback
7640       */
7641      _defineProperty$1(this, "restart", () => {
7642        this.currentTime = 0;
7643      });
7644      /**
7645       * Rewind
7646       * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
7647       */
7648      _defineProperty$1(this, "rewind", seekTime => {
7649        this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;
7650      });
7651      /**
7652       * Fast forward
7653       * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
7654       */
7655      _defineProperty$1(this, "forward", seekTime => {
7656        this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;
7657      });
7658      /**
7659       * Increase volume
7660       * @param {Boolean} step - How much to decrease by (between 0 and 1)
7661       */
7662      _defineProperty$1(this, "increaseVolume", step => {
7663        const volume = this.media.muted ? 0 : this.volume;
7664        this.volume = volume + (is.number(step) ? step : 0);
7665      });
7666      /**
7667       * Decrease volume
7668       * @param {Boolean} step - How much to decrease by (between 0 and 1)
7669       */
7670      _defineProperty$1(this, "decreaseVolume", step => {
7671        this.increaseVolume(-step);
7672      });
7673      /**
7674       * Trigger the airplay dialog
7675       * TODO: update player with state, support, enabled
7676       */
7677      _defineProperty$1(this, "airplay", () => {
7678        // Show dialog if supported
7679        if (support.airplay) {
7680          this.media.webkitShowPlaybackTargetPicker();
7681        }
7682      });
7683      /**
7684       * Toggle the player controls
7685       * @param {Boolean} [toggle] - Whether to show the controls
7686       */
7687      _defineProperty$1(this, "toggleControls", toggle => {
7688        // Don't toggle if missing UI support or if it's audio
7689        if (this.supported.ui && !this.isAudio) {
7690          // Get state before change
7691          const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);
7692          // Negate the argument if not undefined since adding the class to hides the controls
7693          const force = typeof toggle === 'undefined' ? undefined : !toggle;
7694          // Apply and get updated state
7695          const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);
7696
7697          // Close menu
7698          if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
7699            controls.toggleMenu.call(this, false);
7700          }
7701
7702          // Trigger event on change
7703          if (hiding !== isHidden) {
7704            const eventName = hiding ? 'controlshidden' : 'controlsshown';
7705            triggerEvent.call(this, this.media, eventName);
7706          }
7707          return !hiding;
7708        }
7709        return false;
7710      });
7711      /**
7712       * Add event listeners
7713       * @param {String} event - Event type
7714       * @param {Function} callback - Callback for when event occurs
7715       */
7716      _defineProperty$1(this, "on", (event, callback) => {
7717        on.call(this, this.elements.container, event, callback);
7718      });
7719      /**
7720       * Add event listeners once
7721       * @param {String} event - Event type
7722       * @param {Function} callback - Callback for when event occurs
7723       */
7724      _defineProperty$1(this, "once", (event, callback) => {
7725        once.call(this, this.elements.container, event, callback);
7726      });
7727      /**
7728       * Remove event listeners
7729       * @param {String} event - Event type
7730       * @param {Function} callback - Callback for when event occurs
7731       */
7732      _defineProperty$1(this, "off", (event, callback) => {
7733        off(this.elements.container, event, callback);
7734      });
7735      /**
7736       * Destroy an instance
7737       * Event listeners are removed when elements are removed
7738       * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
7739       * @param {Function} callback - Callback for when destroy is complete
7740       * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)
7741       */
7742      _defineProperty$1(this, "destroy", (callback, soft = false) => {
7743        if (!this.ready) {
7744          return;
7745        }
7746        const done = () => {
7747          // Reset overflow (incase destroyed while in fullscreen)
7748          document.body.style.overflow = '';
7749
7750          // GC for embed
7751          this.embed = null;
7752
7753          // If it's a soft destroy, make minimal changes
7754          if (soft) {
7755            if (Object.keys(this.elements).length) {
7756              // Remove elements
7757              removeElement(this.elements.buttons.play);
7758              removeElement(this.elements.captions);
7759              removeElement(this.elements.controls);
7760              removeElement(this.elements.wrapper);
7761
7762              // Clear for GC
7763              this.elements.buttons.play = null;
7764              this.elements.captions = null;
7765              this.elements.controls = null;
7766              this.elements.wrapper = null;
7767            }
7768
7769            // Callback
7770            if (is.function(callback)) {
7771              callback();
7772            }
7773          } else {
7774            // Unbind listeners
7775            unbindListeners.call(this);
7776
7777            // Cancel current network requests
7778            html5.cancelRequests.call(this);
7779
7780            // Replace the container with the original element provided
7781            replaceElement(this.elements.original, this.elements.container);
7782
7783            // Event
7784            triggerEvent.call(this, this.elements.original, 'destroyed', true);
7785
7786            // Callback
7787            if (is.function(callback)) {
7788              callback.call(this.elements.original);
7789            }
7790
7791            // Reset state
7792            this.ready = false;
7793
7794            // Clear for garbage collection
7795            setTimeout(() => {
7796              this.elements = null;
7797              this.media = null;
7798            }, 200);
7799          }
7800        };
7801
7802        // Stop playback
7803        this.stop();
7804
7805        // Clear timeouts
7806        clearTimeout(this.timers.loading);
7807        clearTimeout(this.timers.controls);
7808        clearTimeout(this.timers.resized);
7809
7810        // Provider specific stuff
7811        if (this.isHTML5) {
7812          // Restore native video controls
7813          ui.toggleNativeControls.call(this, true);
7814
7815          // Clean up
7816          done();
7817        } else if (this.isYouTube) {
7818          // Clear timers
7819          clearInterval(this.timers.buffering);
7820          clearInterval(this.timers.playing);
7821
7822          // Destroy YouTube API
7823          if (this.embed !== null && is.function(this.embed.destroy)) {
7824            this.embed.destroy();
7825          }
7826
7827          // Clean up
7828          done();
7829        } else if (this.isVimeo) {
7830          // Destroy Vimeo API
7831          // then clean up (wait, to prevent postmessage errors)
7832          if (this.embed !== null) {
7833            this.embed.unload().then(done);
7834          }
7835
7836          // Vimeo does not always return
7837          setTimeout(done, 200);
7838        }
7839      });
7840      /**
7841       * Check for support for a mime type (HTML5 only)
7842       * @param {String} type - Mime type
7843       */
7844      _defineProperty$1(this, "supports", type => support.mime.call(this, type));
7845      this.timers = {};
7846
7847      // State
7848      this.ready = false;
7849      this.loading = false;
7850      this.failed = false;
7851
7852      // Touch device
7853      this.touch = support.touch;
7854
7855      // Set the media element
7856      this.media = target;
7857
7858      // String selector passed
7859      if (is.string(this.media)) {
7860        this.media = document.querySelectorAll(this.media);
7861      }
7862
7863      // jQuery, NodeList or Array passed, use first element
7864      if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {
7865        // eslint-disable-next-line
7866        this.media = this.media[0];
7867      }
7868
7869      // Set config
7870      this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {
7871        try {
7872          return JSON.parse(this.media.getAttribute('data-plyr-config'));
7873        } catch (_)
vendor: 8,226 bytes, lines 7873-8169
7873 {
7874          return {};
7875        }
7876      })());
7877
7878      // Elements cache
7879      this.elements = {
7880        container: null,
7881        fullscreen: null,
7882        captions: null,
7883        buttons: {},
7884        display: {},
7885        progress: {},
7886        inputs: {},
7887        settings: {
7888          popup: null,
7889          menu: null,
7890          panels: {},
7891          buttons: {}
7892        }
7893      };
7894
7895      // Captions
7896      this.captions = {
7897        active: null,
7898        currentTrack: -1,
7899        meta: new WeakMap()
7900      };
7901
7902      // Fullscreen
7903      this.fullscreen = {
7904        active: false
7905      };
7906
7907      // Options
7908      this.options = {
7909        speed: [],
7910        quality: []
7911      };
7912
7913      // Debugging
7914      // TODO: move to globals
7915      this.debug = new Console(this.config.debug);
7916
7917      // Log config options and support
7918      this.debug.log('Config', this.config);
7919      this.debug.log('Support', support);
7920
7921      // We need an element to setup
7922      if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
7923        this.debug.error('Setup failed: no suitable element passed');
7924        return;
7925      }
7926
7927      // Bail if the element is initialized
7928      if (this.media.plyr) {
7929        this.debug.warn('Target already setup');
7930        return;
7931      }
7932
7933      // Bail if not enabled
7934      if (!this.config.enabled) {
7935        this.debug.error('Setup failed: disabled by config');
7936        return;
7937      }
7938
7939      // Bail if disabled or no basic support
7940      // You may want to disable certain UAs etc
7941      if (!support.check().api) {
7942        this.debug.error('Setup failed: no support');
7943        return;
7944      }
7945
7946      // Cache original element state for .destroy()
7947      const clone = this.media.cloneNode(true);
7948      clone.autoplay = false;
7949      this.elements.original = clone;
7950
7951      // Set media type based on tag or data attribute
7952      // Supported: video, audio, vimeo, youtube
7953      const _type = this.media.tagName.toLowerCase();
7954      // Embed properties
7955      let iframe = null;
7956      let url = null;
7957
7958      // Different setup based on type
7959      switch (_type) {
7960        case 'div':
7961          // Find the frame
7962          iframe = this.media.querySelector('iframe');
7963
7964          // <iframe> type
7965          if (is.element(iframe)) {
7966            // Detect provider
7967            url = parseUrl(iframe.getAttribute('src'));
7968            this.provider = getProviderByUrl(url.toString());
7969
7970            // Rework elements
7971            this.elements.container = this.media;
7972            this.media = iframe;
7973
7974            // Reset classname
7975            this.elements.container.className = '';
7976
7977            // Get attributes from URL and set config
7978            if (url.search.length) {
7979              const truthy = ['1', 'true'];
7980              if (truthy.includes(url.searchParams.get('autoplay'))) {
7981                this.config.autoplay = true;
7982              }
7983              if (truthy.includes(url.searchParams.get('loop'))) {
7984                this.config.loop.active = true;
7985              }
7986
7987              // TODO: replace fullscreen.iosNative with this playsinline config option
7988              // YouTube requires the playsinline in the URL
7989              if (this.isYouTube) {
7990                this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
7991                this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
7992              } else {
7993                this.config.playsinline = true;
7994              }
7995            }
7996          } else {
7997            // <div> with attributes
7998            this.provider = this.media.getAttribute(this.config.attributes.embed.provider);
7999
8000            // Remove attribute
8001            this.media.removeAttribute(this.config.attributes.embed.provider);
8002          }
8003
8004          // Unsupported or missing provider
8005          if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {
8006            this.debug.error('Setup failed: Invalid provider');
8007            return;
8008          }
8009
8010          // Audio will come later for external providers
8011          this.type = types.video;
8012          break;
8013        case 'video':
8014        case 'audio':
8015          this.type = _type;
8016          this.provider = providers.html5;
8017
8018          // Get config from attributes
8019          if (this.media.hasAttribute('crossorigin')) {
8020            this.config.crossorigin = true;
8021          }
8022          if (this.media.hasAttribute('autoplay')) {
8023            this.config.autoplay = true;
8024          }
8025          if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
8026            this.config.playsinline = true;
8027          }
8028          if (this.media.hasAttribute('muted')) {
8029            this.config.muted = true;
8030          }
8031          if (this.media.hasAttribute('loop')) {
8032            this.config.loop.active = true;
8033          }
8034          break;
8035        default:
8036          this.debug.error('Setup failed: unsupported type');
8037          return;
8038      }
8039
8040      // Check for support again but with type
8041      this.supported = support.check(this.type, this.provider);
8042
8043      // If no support for even API, bail
8044      if (!this.supported.api) {
8045        this.debug.error('Setup failed: no support');
8046        return;
8047      }
8048      this.eventListeners = [];
8049
8050      // Create listeners
8051      this.listeners = new Listeners(this);
8052
8053      // Setup local storage for user settings
8054      this.storage = new Storage(this);
8055
8056      // Store reference
8057      this.media.plyr = this;
8058
8059      // Wrap media
8060      if (!is.element(this.elements.container)) {
8061        this.elements.container = createElement('div');
8062        wrap(this.media, this.elements.container);
8063      }
8064
8065      // Migrate custom properties from media to container (so they work 😉)
8066      ui.migrateStyles.call(this);
8067
8068      // Add style hook
8069      ui.addStyleHook.call(this);
8070
8071      // Setup media
8072      media.setup.call(this);
8073
8074      // Listen for events if debugging
8075      if (this.config.debug) {
8076        on.call(this, this.elements.container, this.config.events.join(' '), event => {
8077          this.debug.log(`event: ${event.type}`);
8078        });
8079      }
8080
8081      // Setup fullscreen
8082      this.fullscreen = new Fullscreen(this);
8083
8084      // Setup interface
8085      // If embed but not fully supported, build interface now to avoid flash of controls
8086      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
8087        ui.build.call(this);
8088      }
8089
8090      // Container listeners
8091      this.listeners.container();
8092
8093      // Global listeners
8094      this.listeners.global();
8095
8096      // Setup ads if provided
8097      if (this.config.ads.enabled) {
8098        this.ads = new Ads(this);
8099      }
8100
8101      // Autoplay if required
8102      if (this.isHTML5 && this.config.autoplay) {
8103        this.once('canplay', () => silencePromise(this.play()));
8104      }
8105
8106      // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
8107      this.lastSeekTime = 0;
8108
8109      // Setup preview thumbnails if enabled
8110      if (this.config.previewThumbnails.enabled) {
8111        this.previewThumbnails = new PreviewThumbnails(this);
8112      }
8113    }
8114
8115    // ---------------------------------------
8116    // API
8117    // ---------------------------------------
8118
8119    /**
8120     * Types and provider helpers
8121     */
8122    get isHTML5() {
8123      return this.provider === providers.html5;
8124    }
8125    get isEmbed() {
8126      return this.isYouTube || this.isVimeo;
8127    }
8128    get isYouTube() {
8129      return this.provider === providers.youtube;
8130    }
8131    get isVimeo() {
8132      return this.provider === providers.vimeo;
8133    }
8134    get isVideo() {
8135      return this.type === types.video;
8136    }
8137    get isAudio() {
8138      return this.type === types.audio;
8139    }
8140    /**
8141     * Get playing state
8142     */
8143    get playing() {
8144      return Boolean(this.ready && !this.paused && !this.ended);
8145    }
8146
8147    /**
8148     * Get paused state
8149     */
8150    get paused() {
8151      return Boolean(this.media.paused);
8152    }
8153
8154    /**
8155     * Get stopped state
8156     */
8157    get stopped() {
8158      return Boolean(this.paused && this.currentTime === 0);
8159    }
8160
8161    /**
8162     * Get ended state
8163     */
8164    get ended() {
8165      return Boolean(this.media.ended);
8166    }
8167    /**
8168     * Seek to a time
8169     * @param {
vendor: 15,340 bytes, lines 8169-8758
8169Number} input - where to seek to in seconds. Defaults to 0 (the start)
8170     */
8171    set currentTime(input) {
8172      // Bail if media duration isn't available yet
8173      if (!this.duration) {
8174        return;
8175      }
8176
8177      // Validate input
8178      const inputIsValid = is.number(input) && input > 0;
8179
8180      // Set
8181      this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;
8182
8183      // Logging
8184      this.debug.log(`Seeking to ${this.currentTime} seconds`);
8185    }
8186
8187    /**
8188     * Get current time
8189     */
8190    get currentTime() {
8191      return Number(this.media.currentTime);
8192    }
8193
8194    /**
8195     * Get buffered
8196     */
8197    get buffered() {
8198      const {
8199        buffered
8200      } = this.media;
8201
8202      // YouTube / Vimeo return a float between 0-1
8203      if (is.number(buffered)) {
8204        return buffered;
8205      }
8206
8207      // HTML5
8208      // TODO: Handle buffered chunks of the media
8209      // (i.e. seek to another section buffers only that section)
8210      if (buffered && buffered.length && this.duration > 0) {
8211        return buffered.end(0) / this.duration;
8212      }
8213      return 0;
8214    }
8215
8216    /**
8217     * Get seeking status
8218     */
8219    get seeking() {
8220      return Boolean(this.media.seeking);
8221    }
8222
8223    /**
8224     * Get the duration of the current media
8225     */
8226    get duration() {
8227      // Faux duration set via config
8228      const fauxDuration = parseFloat(this.config.duration);
8229      // Media duration can be NaN or Infinity before the media has loaded
8230      const realDuration = (this.media || {}).duration;
8231      const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;
8232
8233      // If config duration is funky, use regular duration
8234      return fauxDuration || duration;
8235    }
8236
8237    /**
8238     * Set the player volume
8239     * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
8240     */
8241    set volume(value) {
8242      let volume = value;
8243      const max = 1;
8244      const min = 0;
8245      if (is.string(volume)) {
8246        volume = Number(volume);
8247      }
8248
8249      // Load volume from storage if no value specified
8250      if (!is.number(volume)) {
8251        volume = this.storage.get('volume');
8252      }
8253
8254      // Use config if all else fails
8255      if (!is.number(volume)) {
8256        ({
8257          volume
8258        } = this.config);
8259      }
8260
8261      // Maximum is volumeMax
8262      if (volume > max) {
8263        volume = max;
8264      }
8265      // Minimum is volumeMin
8266      if (volume < min) {
8267        volume = min;
8268      }
8269
8270      // Update config
8271      this.config.volume = volume;
8272
8273      // Set the player volume
8274      this.media.volume = volume;
8275
8276      // If muted, and we're increasing volume manually, reset muted state
8277      if (!is.empty(value) && this.muted && volume > 0) {
8278        this.muted = false;
8279      }
8280    }
8281
8282    /**
8283     * Get the current player volume
8284     */
8285    get volume() {
8286      return Number(this.media.volume);
8287    }
8288    /**
8289     * Set muted state
8290     * @param {Boolean} mute
8291     */
8292    set muted(mute) {
8293      let toggle = mute;
8294
8295      // Load muted state from storage
8296      if (!is.boolean(toggle)) {
8297        toggle = this.storage.get('muted');
8298      }
8299
8300      // Use config if all else fails
8301      if (!is.boolean(toggle)) {
8302        toggle = this.config.muted;
8303      }
8304
8305      // Update config
8306      this.config.muted = toggle;
8307
8308      // Set mute on the player
8309      this.media.muted = toggle;
8310    }
8311
8312    /**
8313     * Get current muted state
8314     */
8315    get muted() {
8316      return Boolean(this.media.muted);
8317    }
8318
8319    /**
8320     * Check if the media has audio
8321     */
8322    get hasAudio() {
8323      // Assume yes for all non HTML5 (as we can't tell...)
8324      if (!this.isHTML5) {
8325        return true;
8326      }
8327      if (this.isAudio) {
8328        return true;
8329      }
8330
8331      // Get audio tracks
8332      return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
8333    }
8334
8335    /**
8336     * Set playback speed
8337     * @param {Number} input - the speed of playback (0.5-2.0)
8338     */
8339    set speed(input) {
8340      let speed = null;
8341      if (is.number(input)) {
8342        speed = input;
8343      }
8344      if (!is.number(speed)) {
8345        speed = this.storage.get('speed');
8346      }
8347      if (!is.number(speed)) {
8348        speed = this.config.speed.selected;
8349      }
8350
8351      // Clamp to min/max
8352      const {
8353        minimumSpeed: min,
8354        maximumSpeed: max
8355      } = this;
8356      speed = clamp(speed, min, max);
8357
8358      // Update config
8359      this.config.speed.selected = speed;
8360
8361      // Set media speed
8362      setTimeout(() => {
8363        if (this.media) {
8364          this.media.playbackRate = speed;
8365        }
8366      }, 0);
8367    }
8368
8369    /**
8370     * Get current playback speed
8371     */
8372    get speed() {
8373      return Number(this.media.playbackRate);
8374    }
8375
8376    /**
8377     * Get the minimum allowed speed
8378     */
8379    get minimumSpeed() {
8380      if (this.isYouTube) {
8381        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
8382        return Math.min(...this.options.speed);
8383      }
8384      if (this.isVimeo) {
8385        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
8386        return 0.5;
8387      }
8388
8389      // https://stackoverflow.com/a/32320020/1191319
8390      return 0.0625;
8391    }
8392
8393    /**
8394     * Get the maximum allowed speed
8395     */
8396    get maximumSpeed() {
8397      if (this.isYouTube) {
8398        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate
8399        return Math.max(...this.options.speed);
8400      }
8401      if (this.isVimeo) {
8402        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror
8403        return 2;
8404      }
8405
8406      // https://stackoverflow.com/a/32320020/1191319
8407      return 16;
8408    }
8409
8410    /**
8411     * Set playback quality
8412     * Currently HTML5 & YouTube only
8413     * @param {Number} input - Quality level
8414     */
8415    set quality(input) {
8416      const config = this.config.quality;
8417      const options = this.options.quality;
8418      if (!options.length) {
8419        return;
8420      }
8421      let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);
8422      let updateStorage = true;
8423      if (!options.includes(quality)) {
8424        const value = closest(options, quality);
8425        this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);
8426        quality = value;
8427
8428        // Don't update storage if quality is not supported
8429        updateStorage = false;
8430      }
8431
8432      // Update config
8433      config.selected = quality;
8434
8435      // Set quality
8436      this.media.quality = quality;
8437
8438      // Save to storage
8439      if (updateStorage) {
8440        this.storage.set({
8441          quality
8442        });
8443      }
8444    }
8445
8446    /**
8447     * Get current quality level
8448     */
8449    get quality() {
8450      return this.media.quality;
8451    }
8452
8453    /**
8454     * Toggle loop
8455     * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
8456     * @param {Boolean} input - Whether to loop or not
8457     */
8458    set loop(input) {
8459      const toggle = is.boolean(input) ? input : this.config.loop.active;
8460      this.config.loop.active = toggle;
8461      this.media.loop = toggle;
8462
8463      // Set default to be a true toggle
8464      /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
8465           switch (type) {
8466              case 'start':
8467                  if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
8468                      this.config.loop.end = null;
8469                  }
8470                  this.config.loop.start = this.currentTime;
8471                  // this.config.loop.indicator.start = this.elements.display.played.value;
8472                  break;
8473               case 'end':
8474                  if (this.config.loop.start >= this.currentTime) {
8475                      return this;
8476                  }
8477                  this.config.loop.end = this.currentTime;
8478                  // this.config.loop.indicator.end = this.elements.display.played.value;
8479                  break;
8480               case 'all':
8481                  this.config.loop.start = 0;
8482                  this.config.loop.end = this.duration - 2;
8483                  this.config.loop.indicator.start = 0;
8484                  this.config.loop.indicator.end = 100;
8485                  break;
8486               case 'toggle':
8487                  if (this.config.loop.active) {
8488                      this.config.loop.start = 0;
8489                      this.config.loop.end = null;
8490                  } else {
8491                      this.config.loop.start = 0;
8492                      this.config.loop.end = this.duration - 2;
8493                  }
8494                  break;
8495               default:
8496                  this.config.loop.start = 0;
8497                  this.config.loop.end = null;
8498                  break;
8499          } */
8500    }
8501
8502    /**
8503     * Get current loop state
8504     */
8505    get loop() {
8506      return Boolean(this.media.loop);
8507    }
8508
8509    /**
8510     * Set new media source
8511     * @param {Object} input - The new source object (see docs)
8512     */
8513    set source(input) {
8514      source.change.call(this, input);
8515    }
8516
8517    /**
8518     * Get current source
8519     */
8520    get source() {
8521      return this.media.currentSrc;
8522    }
8523
8524    /**
8525     * Get a download URL (either source or custom)
8526     */
8527    get download() {
8528      const {
8529        download
8530      } = this.config.urls;
8531      return is.url(download) ? download : this.source;
8532    }
8533
8534    /**
8535     * Set the download URL
8536     */
8537    set download(input) {
8538      if (!is.url(input)) {
8539        return;
8540      }
8541      this.config.urls.download = input;
8542      controls.setDownloadUrl.call(this);
8543    }
8544
8545    /**
8546     * Set the poster image for a video
8547     * @param {String} input - the URL for the new poster image
8548     */
8549    set poster(input) {
8550      if (!this.isVideo) {
8551        this.debug.warn('Poster can only be set for video');
8552        return;
8553      }
8554      ui.setPoster.call(this, input, false).catch(() => {});
8555    }
8556
8557    /**
8558     * Get the current poster image
8559     */
8560    get poster() {
8561      if (!this.isVideo) {
8562        return null;
8563      }
8564      return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');
8565    }
8566
8567    /**
8568     * Get the current aspect ratio in use
8569     */
8570    get ratio() {
8571      if (!this.isVideo) {
8572        return null;
8573      }
8574      const ratio = reduceAspectRatio(getAspectRatio.call(this));
8575      return is.array(ratio) ? ratio.join(':') : ratio;
8576    }
8577
8578    /**
8579     * Set video aspect ratio
8580     */
8581    set ratio(input) {
8582      if (!this.isVideo) {
8583        this.debug.warn('Aspect ratio can only be set for video');
8584        return;
8585      }
8586      if (!is.string(input) || !validateAspectRatio(input)) {
8587        this.debug.error(`Invalid aspect ratio specified (${input})`);
8588        return;
8589      }
8590      this.config.ratio = reduceAspectRatio(input);
8591      setAspectRatio.call(this);
8592    }
8593
8594    /**
8595     * Set the autoplay state
8596     * @param {Boolean} input - Whether to autoplay or not
8597     */
8598    set autoplay(input) {
8599      this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;
8600    }
8601
8602    /**
8603     * Get the current autoplay state
8604     */
8605    get autoplay() {
8606      return Boolean(this.config.autoplay);
8607    }
8608
8609    /**
8610     * Toggle captions
8611     * @param {Boolean} input - Whether to enable captions
8612     */
8613    toggleCaptions(input) {
8614      captions.toggle.call(this, input, false);
8615    }
8616
8617    /**
8618     * Set the caption track by index
8619     * @param {Number} input - Caption index
8620     */
8621    set currentTrack(input) {
8622      captions.set.call(this, input, false);
8623      captions.setup.call(this);
8624    }
8625
8626    /**
8627     * Get the current caption track index (-1 if disabled)
8628     */
8629    get currentTrack() {
8630      const {
8631        toggled,
8632        currentTrack
8633      } = this.captions;
8634      return toggled ? currentTrack : -1;
8635    }
8636
8637    /**
8638     * Set the wanted language for captions
8639     * Since tracks can be added later it won't update the actual caption track until there is a matching track
8640     * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)
8641     */
8642    set language(input) {
8643      captions.setLanguage.call(this, input, false);
8644    }
8645
8646    /**
8647     * Get the current track's language
8648     */
8649    get language() {
8650      return (captions.getCurrentTrack.call(this) || {}).language;
8651    }
8652
8653    /**
8654     * Toggle picture-in-picture playback on WebKit/MacOS
8655     * TODO: update player with state, support, enabled
8656     * TODO: detect outside changes
8657     */
8658    set pip(input) {
8659      // Bail if no support
8660      if (!support.pip) {
8661        return;
8662      }
8663
8664      // Toggle based on current state if not passed
8665      const toggle = is.boolean(input) ? input : !this.pip;
8666
8667      // Toggle based on current state
8668      // Safari
8669      if (is.function(this.media.webkitSetPresentationMode)) {
8670        this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
8671      }
8672
8673      // Chrome
8674      if (is.function(this.media.requestPictureInPicture)) {
8675        if (!this.pip && toggle) {
8676          this.media.requestPictureInPicture();
8677        } else if (this.pip && !toggle) {
8678          document.exitPictureInPicture();
8679        }
8680      }
8681    }
8682
8683    /**
8684     * Get the current picture-in-picture state
8685     */
8686    get pip() {
8687      if (!support.pip) {
8688        return null;
8689      }
8690
8691      // Safari
8692      if (!is.empty(this.media.webkitPresentationMode)) {
8693        return this.media.webkitPresentationMode === pip.active;
8694      }
8695
8696      // Chrome
8697      return this.media === document.pictureInPictureElement;
8698    }
8699
8700    /**
8701     * Sets the preview thumbnails for the current source
8702     */
8703    setPreviewThumbnails(thumbnailSource) {
8704      if (this.previewThumbnails && this.previewThumbnails.loaded) {
8705        this.previewThumbnails.destroy();
8706        this.previewThumbnails = null;
8707      }
8708      Object.assign(this.config.previewThumbnails, thumbnailSource);
8709
8710      // Create new instance if it is still enabled
8711      if (this.config.previewThumbnails.enabled) {
8712        this.previewThumbnails = new PreviewThumbnails(this);
8713      }
8714    }
8715    /**
8716     * Check for support
8717     * @param {String} type - Player type (audio/video)
8718     * @param {String} provider - Provider (html5/youtube/vimeo)
8719     */
8720    static supported(type, provider) {
8721      return support.check(type, provider);
8722    }
8723
8724    /**
8725     * Load an SVG sprite into the page
8726     * @param {String} url - URL for the SVG sprite
8727     * @param {String} [id] - Unique ID
8728     */
8729    static loadSprite(url, id) {
8730      return loadSprite(url, id);
8731    }
8732
8733    /**
8734     * Setup multiple instances
8735     * @param {*} selector
8736     * @param {Object} options
8737     */
8738    static setup(selector, options = {}) {
8739      let targets = null;
8740      if (is.string(selector)) {
8741        targets = Array.from(document.querySelectorAll(selector));
8742      } else if (is.nodeList(selector)) {
8743        targets = Array.from(selector);
8744      } else if (is.array(selector)) {
8745        targets = selector.filter(is.element);
8746      }
8747      if (is.empty(targets)) {
8748        return null;
8749      }
8750      return targets.map(t => new Plyr(t, options));
8751    }
8752  }
8753  Plyr.defaults = cloneDeep(defaults);
8754
8755  return Plyr;
8756
8757}));
8758

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.