PageSourceSearch

https://www.schiffestival.com/wp-content/themes/amy-movie/js/vendor/plyr.js?ver=1.0.0

js schiffestival.com collected 2026-10-02 22:17:10 UTC 260,686 bytes, 8,086 lines download raw bytes

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

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.