typeof navigator === "object" && (function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
        typeof define === 'function' && define.amd ? define('Plyr', factory) :
            (global.Plyr = factory());
}(this, (function () { 'use strict';

    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) {
            throw new TypeError("Cannot call a class as a function");
        }
    }

    function _defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }

    function _createClass(Constructor, protoProps, staticProps) {
        if (protoProps) _defineProperties(Constructor.prototype, protoProps);
        if (staticProps) _defineProperties(Constructor, staticProps);
        return Constructor;
    }

    function _defineProperty(obj, key, value) {
        if (key in obj) {
            Object.defineProperty(obj, key, {
                value: value,
                enumerable: true,
                configurable: true,
                writable: true
            });
        } else {
            obj[key] = value;
        }

        return obj;
    }

    function _slicedToArray(arr, i) {
        return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
    }

    function _toConsumableArray(arr) {
        return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
    }

    function _arrayWithoutHoles(arr) {
        if (Array.isArray(arr)) {
            for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];

            return arr2;
        }
    }

    function _arrayWithHoles(arr) {
        if (Array.isArray(arr)) return arr;
    }

    function _iterableToArray(iter) {
        if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
    }

    function _iterableToArrayLimit(arr, i) {
        var _arr = [];
        var _n = true;
        var _d = false;
        var _e = undefined;

        try {
            for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
                _arr.push(_s.value);

                if (i && _arr.length === i) break;
            }
        } catch (err) {
            _d = true;
            _e = err;
        } finally {
            try {
                if (!_n && _i["return"] != null) _i["return"]();
            } finally {
                if (_d) throw _e;
            }
        }

        return _arr;
    }

    function _nonIterableSpread() {
        throw new TypeError("Invalid attempt to spread non-iterable instance");
    }

    function _nonIterableRest() {
        throw new TypeError("Invalid attempt to destructure non-iterable instance");
    }

    // ==========================================================================
    // Type checking utils
    // ==========================================================================
    var getConstructor = function getConstructor(input) {
        return input !== null && typeof input !== 'undefined' ? input.constructor : null;
    };

    var instanceOf = function instanceOf(input, constructor) {
        return Boolean(input && constructor && input instanceof constructor);
    };

    var isNullOrUndefined = function isNullOrUndefined(input) {
        return input === null || typeof input === 'undefined';
    };

    var isObject = function isObject(input) {
        return getConstructor(input) === Object;
    };

    var isNumber = function isNumber(input) {
        return getConstructor(input) === Number && !Number.isNaN(input);
    };

    var isString = function isString(input) {
        return getConstructor(input) === String;
    };

    var isBoolean = function isBoolean(input) {
        return getConstructor(input) === Boolean;
    };

    var isFunction = function isFunction(input) {
        return getConstructor(input) === Function;
    };

    var isArray = function isArray(input) {
        return Array.isArray(input);
    };

    var isWeakMap = function isWeakMap(input) {
        return instanceOf(input, WeakMap);
    };

    var isNodeList = function isNodeList(input) {
        return instanceOf(input, NodeList);
    };

    var isElement = function isElement(input) {
        return instanceOf(input, Element);
    };

    var isTextNode = function isTextNode(input) {
        return getConstructor(input) === Text;
    };

    var isEvent = function isEvent(input) {
        return instanceOf(input, Event);
    };

    var isKeyboardEvent = function isKeyboardEvent(input) {
        return instanceOf(input, KeyboardEvent);
    };

    var isCue = function isCue(input) {
        return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
    };

    var isTrack = function isTrack(input) {
        return instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);
    };

    var isEmpty = function isEmpty(input) {
        return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;
    };

    var isUrl = function isUrl(input) {
        // Accept a URL object
        if (instanceOf(input, window.URL)) {
            return true;
        } // Must be string from here


        if (!isString(input)) {
            return false;
        } // Add the protocol if required


        var string = input;

        if (!input.startsWith('http://') || !input.startsWith('https://')) {
            string = "http://".concat(input);
        }

        try {
            return !isEmpty(new URL(string).hostname);
        } catch (e) {
            return false;
        }
    };

    var is = {
        nullOrUndefined: isNullOrUndefined,
        object: isObject,
        number: isNumber,
        string: isString,
        boolean: isBoolean,
        function: isFunction,
        array: isArray,
        weakMap: isWeakMap,
        nodeList: isNodeList,
        element: isElement,
        textNode: isTextNode,
        event: isEvent,
        keyboardEvent: isKeyboardEvent,
        cue: isCue,
        track: isTrack,
        url: isUrl,
        empty: isEmpty
    };

    // ==========================================================================
    // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
    // https://www.youtube.com/watch?v=NPM6172J22g

    var supportsPassiveListeners = function () {
        // Test via a getter in the options object to see if the passive property is accessed
        var supported = false;

        try {
            var options = Object.defineProperty({}, 'passive', {
                get: function get() {
                    supported = true;
                    return null;
                }
            });
            window.addEventListener('test', null, options);
            window.removeEventListener('test', null, options);
        } catch (e) {// Do nothing
        }

        return supported;
    }(); // Toggle event listener


    function toggleListener(element, event, callback) {
        var _this = this;

        var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
        var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
        var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;

        // Bail if no element, event, or callback
        if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
            return;
        } // Allow multiple events


        var events = event.split(' '); // Build options
        // Default to just the capture boolean for browsers with no passive listener support

        var options = capture; // If passive events listeners are supported

        if (supportsPassiveListeners) {
            options = {
                // Whether the listener can be passive (i.e. default never prevented)
                passive: passive,
                // Whether the listener is a capturing listener or not
                capture: capture
            };
        } // If a single node is passed, bind the event listener


        events.forEach(function (type) {
            if (_this && _this.eventListeners && toggle) {
                // Cache event listener
                _this.eventListeners.push({
                    element: element,
                    type: type,
                    callback: callback,
                    options: options
                });
            }

            element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
        });
    } // Bind event handler

    function on(element) {
        var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
        var callback = arguments.length > 2 ? arguments[2] : undefined;
        var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
        var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
        toggleListener.call(this, element, events, callback, true, passive, capture);
    } // Unbind event handler

    function off(element) {
        var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
        var callback = arguments.length > 2 ? arguments[2] : undefined;
        var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
        var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
        toggleListener.call(this, element, events, callback, false, passive, capture);
    } // Bind once-only event handler

    function once(element) {
        var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
        var callback = arguments.length > 2 ? arguments[2] : undefined;
        var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
        var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;

        function onceCallback() {
            off(element, events, onceCallback, passive, capture);

            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
                args[_key] = arguments[_key];
            }

            callback.apply(this, args);
        }

        toggleListener.call(this, element, events, onceCallback, true, passive, capture);
    } // Trigger event

    function triggerEvent(element) {
        var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
        var bubbles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
        var detail = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};

        // Bail if no element
        if (!is.element(element) || is.empty(type)) {
            return;
        } // Create and dispatch the event


        var event = new CustomEvent(type, {
            bubbles: bubbles,
            detail: Object.assign({}, detail, {
                plyr: this
            })
        }); // Dispatch the event

        element.dispatchEvent(event);
    } // Unbind all cached event listeners

    function unbindListeners() {
        if (this && this.eventListeners) {
            this.eventListeners.forEach(function (item) {
                var element = item.element,
                    type = item.type,
                    callback = item.callback,
                    options = item.options;
                element.removeEventListener(type, callback, options);
            });
            this.eventListeners = [];
        }
    } // Run method when / if player is ready

    function ready() {
        var _this2 = this;

        return new Promise(function (resolve) {
            return _this2.ready ? setTimeout(resolve, 0) : on.call(_this2, _this2.elements.container, 'ready', resolve);
        }).then(function () {});
    }

    function wrap(elements, wrapper) {
        // Convert `elements` to an array, if necessary.
        var targets = elements.length ? elements : [elements]; // Loops backwards to prevent having to clone the wrapper on the
        // first element (see `child` below).

        Array.from(targets).reverse().forEach(function (element, index) {
            var child = index > 0 ? wrapper.cloneNode(true) : wrapper; // Cache the current parent and sibling.

            var parent = element.parentNode;
            var sibling = element.nextSibling; // Wrap the element (is automatically removed from its current
            // parent).

            child.appendChild(element); // If the element had a sibling, insert the wrapper before
            // the sibling to maintain the HTML structure; otherwise, just
            // append it to the parent.

            if (sibling) {
                parent.insertBefore(child, sibling);
            } else {
                parent.appendChild(child);
            }
        });
    } // Set attributes

    function setAttributes(element, attributes) {
        if (!is.element(element) || is.empty(attributes)) {
            return;
        } // Assume null and undefined attributes should be left out,
        // Setting them would otherwise convert them to "null" and "undefined"


        Object.entries(attributes).filter(function (_ref) {
            var _ref2 = _slicedToArray(_ref, 2),
                value = _ref2[1];

            return !is.nullOrUndefined(value);
        }).forEach(function (_ref3) {
            var _ref4 = _slicedToArray(_ref3, 2),
                key = _ref4[0],
                value = _ref4[1];

            return element.setAttribute(key, value);
        });
    } // Create a DocumentFragment

    function createElement(type, attributes, text) {
        // Create a new <element>
        var element = document.createElement(type); // Set all passed attributes

        if (is.object(attributes)) {
            setAttributes(element, attributes);
        } // Add text node


        if (is.string(text)) {
            element.innerText = text;
        } // Return built element


        return element;
    } // Inaert an element after another

    function insertAfter(element, target) {
        if (!is.element(element) || !is.element(target)) {
            return;
        }

        target.parentNode.insertBefore(element, target.nextSibling);
    } // Insert a DocumentFragment

    function insertElement(type, parent, attributes, text) {
        if (!is.element(parent)) {
            return;
        }

        parent.appendChild(createElement(type, attributes, text));
    } // Remove element(s)

    function removeElement(element) {
        if (is.nodeList(element) || is.array(element)) {
            Array.from(element).forEach(removeElement);
            return;
        }

        if (!is.element(element) || !is.element(element.parentNode)) {
            return;
        }

        element.parentNode.removeChild(element);
    } // Remove all child elements

    function emptyElement(element) {
        if (!is.element(element)) {
            return;
        }

        var length = element.childNodes.length;

        while (length > 0) {
            element.removeChild(element.lastChild);
            length -= 1;
        }
    } // Replace element

    function replaceElement(newChild, oldChild) {
        if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
            return null;
        }

        oldChild.parentNode.replaceChild(newChild, oldChild);
        return newChild;
    } // Get an attribute object from a string selector

    function getAttributesFromSelector(sel, existingAttributes) {
        // For example:
        // '.test' to { class: 'test' }
        // '#test' to { id: 'test' }
        // '[data-test="test"]' to { 'data-test': 'test' }
        if (!is.string(sel) || is.empty(sel)) {
            return {};
        }

        var attributes = {};
        var existing = existingAttributes;
        sel.split(',').forEach(function (s) {
            // Remove whitespace
            var selector = s.trim();
            var className = selector.replace('.', '');
            var stripped = selector.replace(/[[\]]/g, ''); // Get the parts and value

            var parts = stripped.split('=');
            var key = parts[0];
            var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; // Get the first character

            var start = selector.charAt(0);

            switch (start) {
                case '.':
                    // Add to existing classname
                    if (is.object(existing) && is.string(existing.class)) {
                        existing.class += " ".concat(className);
                    }

                    attributes.class = className;
                    break;

                case '#':
                    // ID selector
                    attributes.id = selector.replace('#', '');
                    break;

                case '[':
                    // Attribute selector
                    attributes[key] = value;
                    break;

                default:
                    break;
            }
        });
        return attributes;
    } // Toggle hidden

    function toggleHidden(element, hidden) {
        if (!is.element(element)) {
            return;
        }

        var hide = hidden;

        if (!is.boolean(hide)) {
            hide = !element.hidden;
        }

        if (hide) {
            element.setAttribute('hidden', '');
        } else {
            element.removeAttribute('hidden');
        }
    } // Mirror Element.classList.toggle, with IE compatibility for "force" argument

    function toggleClass(element, className, force) {
        if (is.nodeList(element)) {
            return Array.from(element).map(function (e) {
                return toggleClass(e, className, force);
            });
        }

        if (is.element(element)) {
            var method = 'toggle';

            if (typeof force !== 'undefined') {
                method = force ? 'add' : 'remove';
            }

            element.classList[method](className);
            return element.classList.contains(className);
        }

        return false;
    } // Has class name

    function hasClass(element, className) {
        return is.element(element) && element.classList.contains(className);
    } // Element matches selector

    function matches(element, selector) {

        function match() {
            return Array.from(document.querySelectorAll(selector)).includes(this);
        }

        var matches = match;
        return matches.call(element, selector);
    } // Find all elements

    function getElements(selector) {
        return this.elements.container.querySelectorAll(selector);
    } // Find a single element

    function getElement(selector) {
        return this.elements.container.querySelector(selector);
    } // Trap focus inside container

    function trapFocus() {
        var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
        var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

        if (!is.element(element)) {
            return;
        }

        var focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
        var first = focusable[0];
        var last = focusable[focusable.length - 1];

        var trap = function trap(event) {
            // Bail if not tab key or not fullscreen
            if (event.key !== 'Tab' || event.keyCode !== 9) {
                return;
            } // Get the current focused element


            var focused = document.activeElement;

            if (focused === last && !event.shiftKey) {
                // Move focus to first element that can be tabbed if Shift isn't used
                first.focus();
                event.preventDefault();
            } else if (focused === first && event.shiftKey) {
                // Move focus to last element that can be tabbed if Shift is used
                last.focus();
                event.preventDefault();
            }
        };

        toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
    } // Set focus and tab focus class

    function setFocus() {
        var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
        var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

        if (!is.element(element)) {
            return;
        } // Set regular focus


        element.focus({
            preventScroll: true
        }); // If we want to mimic keyboard focus via tab

        if (tabFocus) {
            toggleClass(element, this.config.classNames.tabFocus);
        }
    }

    // ==========================================================================
    var transitionEndEvent = function () {
        var element = document.createElement('span');
        var events = {
            WebkitTransition: 'webkitTransitionEnd',
            MozTransition: 'transitionend',
            OTransition: 'oTransitionEnd otransitionend',
            transition: 'transitionend'
        };
        var type = Object.keys(events).find(function (event) {
            return element.style[event] !== undefined;
        });
        return is.string(type) ? events[type] : false;
    }(); // Force repaint of element

    function repaint(element) {
        setTimeout(function () {
            try {
                toggleHidden(element, true);
                element.offsetHeight; // eslint-disable-line

                toggleHidden(element, false);
            } catch (e) {// Do nothing
            }
        }, 0);
    }

    // ==========================================================================
    // Browser sniffing
    // Unfortunately, due to mixed support, UA sniffing is required
    // ==========================================================================
    var browser = {
        isIE:
        /* @cc_on!@ */
            !!document.documentMode,
        isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
        isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
        isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform)
    };

    var defaultCodecs = {
        'audio/ogg': 'vorbis',
        'audio/wav': '1',
        'video/webm': 'vp8, vorbis',
        'video/mp4': 'avc1.42E01E, mp4a.40.2',
        'video/ogg': 'theora'
    }; // Check for feature support

    var support = {
        // Basic support
        audio: 'canPlayType' in document.createElement('audio'),
        video: 'canPlayType' in document.createElement('video'),
        // Check for support
        // Basic functionality vs full UI
        check: function check(type, provider, playsinline) {
            var canPlayInline = browser.isIPhone && playsinline && support.playsinline;
            var api = support[type] || provider !== 'html5';
            var ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
            return {
                api: api,
                ui: ui
            };
        },
        // Picture-in-picture support
        // Safari & Chrome only currently
        pip: function () {
            if (browser.isIPhone) {
                return false;
            } // Safari
            // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls


            if (is.function(createElement('video').webkitSetPresentationMode)) {
                return true;
            } // Chrome
            // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture


            if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
                return true;
            }

            return false;
        }(),
        // Airplay support
        // Safari only currently
        airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
        // Inline playback support
        // https://webkit.org/blog/6784/new-video-policies-for-ios/
        playsinline: 'playsInline' in document.createElement('video'),
        // Check for mime type support against a player instance
        // Credits: http://diveintohtml5.info/everything.html
        // Related: http://www.leanbackplayer.com/test/h5mt.html
        mime: function mime(inputType) {
            var _inputType$split = inputType.split('/'),
                _inputType$split2 = _slicedToArray(_inputType$split, 1),
                mediaType = _inputType$split2[0];

            var type = inputType; // Verify we're using HTML5 and there's no media type mismatch

            if (!this.isHTML5 || mediaType !== this.type) {
                return false;
            } // Add codec if required


            if (Object.keys(defaultCodecs).includes(type)) {
                type += "; codecs=\"".concat(defaultCodecs[inputType], "\"");
            }

            try {
                return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
            } catch (e) {
                return false;
            }
        },
        // Check for textTracks support
        textTracks: 'textTracks' in document.createElement('video'),
        // <input type="range"> Sliders
        rangeInput: function () {
            var range = document.createElement('input');
            range.type = 'range';
            return range.type === 'range';
        }(),
        // Touch
        // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
        touch: 'ontouchstart' in document.documentElement,
        // Detect transitions support
        transitions: transitionEndEvent !== false,
        // Reduced motion iOS & MacOS setting
        // https://webkit.org/blog/7551/responsive-design-for-motion/
        reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
    };

    // ==========================================================================
    var html5 = {
        getSources: function getSources() {
            var _this = this;

            if (!this.isHTML5) {
                return [];
            }

            var sources = Array.from(this.media.querySelectorAll('source')); // Filter out unsupported sources

            return sources.filter(function (source) {
                return support.mime.call(_this, source.getAttribute('type'));
            });
        },
        // Get quality levels
        getQualityOptions: function getQualityOptions() {
            // Get sizes from <source> elements
            return html5.getSources.call(this).map(function (source) {
                return Number(source.getAttribute('size'));
            }).filter(Boolean);
        },
        extend: function extend() {
            if (!this.isHTML5) {
                return;
            }

            var player = this; // Quality

            Object.defineProperty(player.media, 'quality', {
                get: function get() {
                    // Get sources
                    var sources = html5.getSources.call(player);
                    var source = sources.find(function (source) {
                        return source.getAttribute('src') === player.source;
                    }); // Return size, if match is found

                    return source && Number(source.getAttribute('size'));
                },
                set: function set(input) {
                    // Get sources
                    var sources = html5.getSources.call(player); // Get first match for requested size

                    var source = sources.find(function (source) {
                        return Number(source.getAttribute('size')) === input;
                    }); // No matching source found

                    if (!source) {
                        return;
                    } // Get current state


                    var _player$media = player.media,
                        currentTime = _player$media.currentTime,
                        paused = _player$media.paused,
                        preload = _player$media.preload,
                        readyState = _player$media.readyState; // Set new source

                    player.media.src = source.getAttribute('src'); // Prevent loading if preload="none" and the current source isn't loaded (#1044)

                    if (preload !== 'none' || readyState) {
                        // Restore time
                        player.once('loadedmetadata', function () {
                            player.currentTime = currentTime; // Resume playing

                            if (!paused) {
                                player.play();
                            }
                        }); // Load new source

                        player.media.load();
                    } // Trigger change event


                    triggerEvent.call(player, player.media, 'qualitychange', false, {
                        quality: input
                    });
                }
            });
        },
        // Cancel current network requests
        // See https://github.com/sampotts/plyr/issues/174
        cancelRequests: function cancelRequests() {
            if (!this.isHTML5) {
                return;
            } // Remove child sources


            removeElement(html5.getSources.call(this)); // Set blank video src attribute
            // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
            // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection

            this.media.setAttribute('src', this.config.blankVideo); // Load the new empty source
            // This will cancel existing requests
            // See https://github.com/sampotts/plyr/issues/174

            this.media.load(); // Debugging

            this.debug.log('Cancelled network requests');
        }
    };

    // ==========================================================================

    function dedupe(array) {
        if (!is.array(array)) {
            return array;
        }

        return array.filter(function (item, index) {
            return array.indexOf(item) === index;
        });
    } // Get the closest value in an array

    function closest(array, value) {
        if (!is.array(array) || !array.length) {
            return null;
        }

        return array.reduce(function (prev, curr) {
            return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
        });
    }

    function cloneDeep(object) {
        return JSON.parse(JSON.stringify(object));
    } // Get a nested value in an object

    function getDeep(object, path) {
        return path.split('.').reduce(function (obj, key) {
            return obj && obj[key];
        }, object);
    } // Deep extend destination object with N more objects

    function extend() {
        var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

        for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
            sources[_key - 1] = arguments[_key];
        }

        if (!sources.length) {
            return target;
        }

        var source = sources.shift();

        if (!is.object(source)) {
            return target;
        }

        Object.keys(source).forEach(function (key) {
            if (is.object(source[key])) {
                if (!Object.keys(target).includes(key)) {
                    Object.assign(target, _defineProperty({}, key, {}));
                }

                extend(target[key], source[key]);
            } else {
                Object.assign(target, _defineProperty({}, key, source[key]));
            }
        });
        return extend.apply(void 0, [target].concat(sources));
    }

    // ==========================================================================

    function generateId(prefix) {
        return "".concat(prefix, "-").concat(Math.floor(Math.random() * 10000));
    } // Format string

    function format(input) {
        for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
            args[_key - 1] = arguments[_key];
        }

        if (is.empty(input)) {
            return input;
        }

        return input.toString().replace(/{(\d+)}/g, function (match, i) {
            return args[i].toString();
        });
    } // Get percentage

    function getPercentage(current, max) {
        if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
            return 0;
        }

        return (current / max * 100).toFixed(2);
    } // Replace all occurances of a string in a string

    function replaceAll() {
        var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
        var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
        var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
        return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
    } // Convert to title case

    function toTitleCase() {
        var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
        return input.toString().replace(/\w\S*/g, function (text) {
            return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
        });
    } // Convert string to pascalCase

    function toPascalCase() {
        var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
        var string = input.toString(); // Convert kebab case

        string = replaceAll(string, '-', ' '); // Convert snake case

        string = replaceAll(string, '_', ' '); // Convert to title case

        string = toTitleCase(string); // Convert to pascal case

        return replaceAll(string, ' ', '');
    } // Convert string to pascalCase

    function toCamelCase() {
        var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
        var string = input.toString(); // Convert to pascal case

        string = toPascalCase(string); // Convert first character to lowercase

        return string.charAt(0).toLowerCase() + string.slice(1);
    } // Remove HTML from a string

    function stripHTML(source) {
        var fragment = document.createDocumentFragment();
        var element = document.createElement('div');
        fragment.appendChild(element);
        element.innerHTML = source;
        return fragment.firstChild.innerText;
    } // Like outerHTML, but also works for DocumentFragment

    function getHTML(element) {
        var wrapper = document.createElement('div');
        wrapper.appendChild(element);
        return wrapper.innerHTML;
    }

    var resources = {
        pip: 'PIP',
        airplay: 'AirPlay',
        html5: 'HTML5',
        vimeo: 'Vimeo',
        youtube: 'YouTube'
    };
    var i18n = {
        get: function get() {
            var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
            var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

            if (is.empty(key) || is.empty(config)) {
                return '';
            }

            var string = getDeep(config.i18n, key);

            if (is.empty(string)) {
                if (Object.keys(resources).includes(key)) {
                    return resources[key];
                }

                return '';
            }

            var replace = {
                '{seektime}': config.seekTime,
                '{title}': config.title
            };
            Object.entries(replace).forEach(function (_ref) {
                var _ref2 = _slicedToArray(_ref, 2),
                    key = _ref2[0],
                    value = _ref2[1];

                string = replaceAll(string, key, value);
            });
            return string;
        }
    };

    var Storage =
        /*#__PURE__*/
        function () {
            function Storage(player) {
                _classCallCheck(this, Storage);

                this.enabled = player.config.storage.enabled;
                this.key = player.config.storage.key;
            } // Check for actual support (see if we can use it)


            _createClass(Storage, [{
                key: "get",
                value: function get(key) {
                    if (!Storage.supported || !this.enabled) {
                        return null;
                    }

                    var store = window.localStorage.getItem(this.key);

                    if (is.empty(store)) {
                        return null;
                    }

                    var json = JSON.parse(store);
                    return is.string(key) && key.length ? json[key] : json;
                }
            }, {
                key: "set",
                value: function set(object) {
                    // Bail if we don't have localStorage support or it's disabled
                    if (!Storage.supported || !this.enabled) {
                        return;
                    } // Can only store objectst


                    if (!is.object(object)) {
                        return;
                    } // Get current storage


                    var storage = this.get(); // Default to empty object

                    if (is.empty(storage)) {
                        storage = {};
                    } // Update the working copy of the values


                    extend(storage, object); // Update storage

                    window.localStorage.setItem(this.key, JSON.stringify(storage));
                }
            }], [{
                key: "supported",
                get: function get() {
                    try {
                        if (!('localStorage' in window)) {
                            return false;
                        }

                        var test = '___test'; // Try to use it (it might be disabled, e.g. user is in private mode)
                        // see: https://github.com/sampotts/plyr/issues/131

                        window.localStorage.setItem(test, test);
                        window.localStorage.removeItem(test);
                        return true;
                    } catch (e) {
                        return false;
                    }
                }
            }]);

            return Storage;
        }();

    // ==========================================================================
    // Fetch wrapper
    // Using XHR to avoid issues with older browsers
    // ==========================================================================
    function fetch(url) {
        var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text';
        return new Promise(function (resolve, reject) {
            try {
                var request = new XMLHttpRequest(); // Check for CORS support

                if (!('withCredentials' in request)) {
                    return;
                }

                request.addEventListener('load', function () {
                    if (responseType === 'text') {
                        try {
                            resolve(JSON.parse(request.responseText));
                        } catch (e) {
                            resolve(request.responseText);
                        }
                    } else {
                        resolve(request.response);
                    }
                });
                request.addEventListener('error', function () {
                    throw new Error(request.status);
                });
                request.open('GET', url, true); // Set the required response type

                request.responseType = responseType;
                request.send();
            } catch (e) {
                reject(e);
            }
        });
    }

    // ==========================================================================

    function loadSprite(url, id) {
        if (!is.string(url)) {
            return;
        }

        var prefix = 'cache';
        var hasId = is.string(id);
        var isCached = false;

        var exists = function exists() {
            return document.getElementById(id) !== null;
        };

        var update = function update(container, data) {
            container.innerHTML = data; // Check again incase of race condition

            if (hasId && exists()) {
                return;
            } // Inject the SVG to the body


            document.body.insertAdjacentElement('afterbegin', container);
        }; // Only load once if ID set


        if (!hasId || !exists()) {
            var useStorage = Storage.supported; // Create container

            var container = document.createElement('div');
            container.setAttribute('hidden', '');

            if (hasId) {
                container.setAttribute('id', id);
            } // Check in cache


            if (useStorage) {
                var cached = window.localStorage.getItem("".concat(prefix, "-").concat(id));
                isCached = cached !== null;

                if (isCached) {
                    var data = JSON.parse(cached);
                    update(container, data.content);
                }
            } // Get the sprite


            fetch(url).then(function (result) {
                if (is.empty(result)) {
                    return;
                }

                if (useStorage) {
                    window.localStorage.setItem("".concat(prefix, "-").concat(id), JSON.stringify({
                        content: result
                    }));
                }

                update(container, result);
            }).catch(function () {});
        }
    }

    // ==========================================================================

    var getHours = function getHours(value) {
        return parseInt(value / 60 / 60 % 60, 10);
    };
    var getMinutes = function getMinutes(value) {
        return parseInt(value / 60 % 60, 10);
    };
    var getSeconds = function getSeconds(value) {
        return parseInt(value % 60, 10);
    }; // Format time to UI friendly string

    function formatTime() {
        var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
        var displayHours = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
        var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

        // Bail if the value isn't a number
        if (!is.number(time)) {
            return formatTime(null, displayHours, inverted);
        } // Format time component to add leading zero


        var format = function format(value) {
            return "0".concat(value).slice(-2);
        }; // Breakdown to hours, mins, secs


        var hours = getHours(time);
        var mins = getMinutes(time);
        var secs = getSeconds(time); // Do we need to display hours?

        if (displayHours || hours > 0) {
            hours = "".concat(hours, ":");
        } else {
            hours = '';
        } // Render


        return "".concat(inverted && time > 0 ? '-' : '').concat(hours).concat(format(mins), ":").concat(format(secs));
    }

    var controls = {
        // Get icon URL
        getIconUrl: function getIconUrl() {
            var url = new URL(this.config.iconUrl, window.location);
            var cors = url.host !== window.location.host || browser.isIE && !window.svg4everybody;
            return {
                url: this.config.iconUrl,
                cors: cors
            };
        },
        // Find the UI controls
        findElements: function findElements() {
            try {
                this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); // Buttons

                this.elements.buttons = {
                    play: getElements.call(this, this.config.selectors.buttons.play),
                    pause: getElement.call(this, this.config.selectors.buttons.pause),
                    restart: getElement.call(this, this.config.selectors.buttons.restart),
                    rewind: getElement.call(this, this.config.selectors.buttons.rewind),
                    fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),
                    mute: getElement.call(this, this.config.selectors.buttons.mute),
                    pip: getElement.call(this, this.config.selectors.buttons.pip),
                    airplay: getElement.call(this, this.config.selectors.buttons.airplay),
                    settings: getElement.call(this, this.config.selectors.buttons.settings),
                    captions: getElement.call(this, this.config.selectors.buttons.captions),
                    fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)
                }; // Progress

                this.elements.progress = getElement.call(this, this.config.selectors.progress); // Inputs

                this.elements.inputs = {
                    seek: getElement.call(this, this.config.selectors.inputs.seek),
                    volume: getElement.call(this, this.config.selectors.inputs.volume)
                }; // Display

                this.elements.display = {
                    buffer: getElement.call(this, this.config.selectors.display.buffer),
                    currentTime: getElement.call(this, this.config.selectors.display.currentTime),
                    duration: getElement.call(this, this.config.selectors.display.duration)
                }; // Seek tooltip

                if (is.element(this.elements.progress)) {
                    this.elements.display.seekTooltip = this.elements.progress.querySelector(".".concat(this.config.classNames.tooltip));
                }

                return true;
            } catch (error) {
                // Log it
                this.debug.warn('It looks like there is a problem with your custom controls HTML', error); // Restore native video controls

                this.toggleNativeControls(true);
                return false;
            }
        },
        // Create <svg> icon
        createIcon: function createIcon(type, attributes) {
            var namespace = 'http://www.w3.org/2000/svg';
            var iconUrl = controls.getIconUrl.call(this);
            var iconPath = "".concat(!iconUrl.cors ? iconUrl.url : '', "#").concat(this.config.iconPrefix); // Create <svg>

            var icon = document.createElementNS(namespace, 'svg');
            setAttributes(icon, extend(attributes, {
                role: 'presentation',
                focusable: 'false'
            })); // Create the <use> to reference sprite

            var use = document.createElementNS(namespace, 'use');
            var path = "".concat(iconPath, "-").concat(type); // Set `href` attributes
            // https://github.com/sampotts/plyr/issues/460
            // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href

            if ('href' in use) {
                use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
            } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)


            use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>

            icon.appendChild(use);
            return icon;
        },
        // Create hidden text label
        createLabel: function createLabel(key) {
            var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
            var text = i18n.get(key, this.config);
            var attributes = Object.assign({}, attr, {
                class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
            });
            return createElement('span', attributes, text);
        },
        // Create a badge
        createBadge: function createBadge(text) {
            if (is.empty(text)) {
                return null;
            }

            var badge = createElement('span', {
                class: this.config.classNames.menu.value
            });
            badge.appendChild(createElement('span', {
                class: this.config.classNames.menu.badge
            }, text));
            return badge;
        },
        // Create a <button>
        createButton: function createButton(buttonType, attr) {
            var attributes = Object.assign({}, attr);
            var type = toCamelCase(buttonType);
            var props = {
                element: 'button',
                toggle: false,
                label: null,
                icon: null,
                labelPressed: null,
                iconPressed: null
            };
            ['element', 'icon', 'label'].forEach(function (key) {
                if (Object.keys(attributes).includes(key)) {
                    props[key] = attributes[key];
                    delete attributes[key];
                }
            }); // Default to 'button' type to prevent form submission

            if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
                attributes.type = 'button';
            } // Set class name


            if (Object.keys(attributes).includes('class')) {
                if (!attributes.class.includes(this.config.classNames.control)) {
                    attributes.class += " ".concat(this.config.classNames.control);
                }
            } else {
                attributes.class = this.config.classNames.control;
            } // Large play button


            switch (buttonType) {
                case 'play':
                    props.toggle = true;
                    props.label = 'play';
                    props.labelPressed = 'pause';
                    props.icon = 'play';
                    props.iconPressed = 'pause';
                    break;

                case 'mute':
                    props.toggle = true;
                    props.label = 'mute';
                    props.labelPressed = 'unmute';
                    props.icon = 'volume';
                    props.iconPressed = 'muted';
                    break;

                case 'captions':
                    props.toggle = true;
                    props.label = 'enableCaptions';
                    props.labelPressed = 'disableCaptions';
                    props.icon = 'captions-off';
                    props.iconPressed = 'captions-on';
                    break;

                case 'fullscreen':
                    props.toggle = true;
                    props.label = 'enterFullscreen';
                    props.labelPressed = 'exitFullscreen';
                    props.icon = 'enter-fullscreen';
                    props.iconPressed = 'exit-fullscreen';
                    break;

                case 'play-large':
                    attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
                    type = 'play';
                    props.label = 'play';
                    props.icon = 'play';
                    break;

                default:
                    if (is.empty(props.label)) {
                        props.label = type;
                    }

                    if (is.empty(props.icon)) {
                        props.icon = buttonType;
                    }

            }

            var button = createElement(props.element); // Setup toggle icon and labels

            if (props.toggle) {
                // Icon
                button.appendChild(controls.createIcon.call(this, props.iconPressed, {
                    class: 'icon--pressed'
                }));
                button.appendChild(controls.createIcon.call(this, props.icon, {
                    class: 'icon--not-pressed'
                })); // Label/Tooltip

                button.appendChild(controls.createLabel.call(this, props.labelPressed, {
                    class: 'label--pressed'
                }));
                button.appendChild(controls.createLabel.call(this, props.label, {
                    class: 'label--not-pressed'
                }));
            } else {
                button.appendChild(controls.createIcon.call(this, props.icon));
                button.appendChild(controls.createLabel.call(this, props.label));
            } // Merge and set attributes


            extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
            setAttributes(button, attributes); // We have multiple play buttons

            if (type === 'play') {
                if (!is.array(this.elements.buttons[type])) {
                    this.elements.buttons[type] = [];
                }

                this.elements.buttons[type].push(button);
            } else {
                this.elements.buttons[type] = button;
            }

            return button;
        },
        // Create an <input type='range'>
        createRange: function createRange(type, attributes) {
            // Seek input
            var input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
                type: 'range',
                min: 0,
                max: 100,
                step: 0.01,
                value: 0,
                autocomplete: 'off',
                // A11y fixes for https://github.com/sampotts/plyr/issues/905
                role: 'slider',
                'aria-label': i18n.get(type, this.config),
                'aria-valuemin': 0,
                'aria-valuemax': 100,
                'aria-valuenow': 0
            }, attributes));
            this.elements.inputs[type] = input; // Set the fill for webkit now

            controls.updateRangeFill.call(this, input);
            return input;
        },
        // Create a <progress>
        createProgress: function createProgress(type, attributes) {
            var progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {
                min: 0,
                max: 100,
                value: 0,
                role: 'presentation',
                'aria-hidden': true
            }, attributes)); // Create the label inside

            if (type !== 'volume') {
                progress.appendChild(createElement('span', null, '0'));
                var suffixKey = {
                    played: 'played',
                    buffer: 'buffered'
                }[type];
                var suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
                progress.innerText = "% ".concat(suffix.toLowerCase());
            }

            this.elements.display[type] = progress;
            return progress;
        },
        // Create time display
        createTime: function createTime(type) {
            var attributes = getAttributesFromSelector(this.config.selectors.display[type]);
            var container = createElement('div', extend(attributes, {
                class: "".concat(this.config.classNames.display.time, " ").concat(attributes.class ? attributes.class : '').trim(),
                'aria-label': i18n.get(type, this.config)
            }), '00:00'); // Reference for updates

            this.elements.display[type] = container;
            return container;
        },
        // Bind keyboard shortcuts for a menu item
        // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
        bindMenuItemShortcuts: function bindMenuItemShortcuts(menuItem, type) {
            var _this = this;

            // Navigate through menus via arrow keys and space
            on(menuItem, 'keydown keyup', function (event) {
                // We only care about space and ⬆️ ⬇️️ ➡️
                if (![32, 38, 39, 40].includes(event.which)) {
                    return;
                } // Prevent play / seek


                event.preventDefault();
                event.stopPropagation(); // We're just here to prevent the keydown bubbling

                if (event.type === 'keydown') {
                    return;
                }

                var isRadioButton = matches(menuItem, '[role="menuitemradio"]'); // Show the respective menu

                if (!isRadioButton && [32, 39].includes(event.which)) {
                    controls.showMenuPanel.call(_this, type, true);
                } else {
                    var target;

                    if (event.which !== 32) {
                        if (event.which === 40 || isRadioButton && event.which === 39) {
                            target = menuItem.nextElementSibling;

                            if (!is.element(target)) {
                                target = menuItem.parentNode.firstElementChild;
                            }
                        } else {
                            target = menuItem.previousElementSibling;

                            if (!is.element(target)) {
                                target = menuItem.parentNode.lastElementChild;
                            }
                        }

                        setFocus.call(_this, target, true);
                    }
                }
            }, false); // Enter will fire a `click` event but we still need to manage focus
            // So we bind to keyup which fires after and set focus here

            on(menuItem, 'keyup', function (event) {
                if (event.which !== 13) {
                    return;
                }

                controls.focusFirstMenuItem.call(_this, null, true);
            });
        },
        // Create a settings menu item
        createMenuItem: function createMenuItem(_ref) {
            var _this2 = this;

            var value = _ref.value,
                list = _ref.list,
                type = _ref.type,
                title = _ref.title,
                _ref$badge = _ref.badge,
                badge = _ref$badge === void 0 ? null : _ref$badge,
                _ref$checked = _ref.checked,
                checked = _ref$checked === void 0 ? false : _ref$checked;
            var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
            var menuItem = createElement('button', extend(attributes, {
                type: 'button',
                role: 'menuitemradio',
                class: "".concat(this.config.classNames.control, " ").concat(attributes.class ? attributes.class : '').trim(),
                'aria-checked': checked,
                value: value
            }));
            var flex = createElement('span'); // We have to set as HTML incase of special characters

            flex.innerHTML = title;

            if (is.element(badge)) {
                flex.appendChild(badge);
            }

            menuItem.appendChild(flex); // Replicate radio button behaviour

            Object.defineProperty(menuItem, 'checked', {
                enumerable: true,
                get: function get() {
                    return menuItem.getAttribute('aria-checked') === 'true';
                },
                set: function set(checked) {
                    // Ensure exclusivity
                    if (checked) {
                        Array.from(menuItem.parentNode.children).filter(function (node) {
                            return matches(node, '[role="menuitemradio"]');
                        }).forEach(function (node) {
                            return node.setAttribute('aria-checked', 'false');
                        });
                    }

                    menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
                }
            });
            this.listeners.bind(menuItem, 'click keyup', function (event) {
                if (is.keyboardEvent(event) && event.which !== 32) {
                    return;
                }

                event.preventDefault();
                event.stopPropagation();
                menuItem.checked = true;

                switch (type) {
                    case 'language':
                        _this2.currentTrack = Number(value);
                        break;

                    case 'quality':
                        _this2.quality = value;
                        break;

                    case 'speed':
                        _this2.speed = parseFloat(value);
                        break;

                    default:
                        break;
                }

                controls.showMenuPanel.call(_this2, 'home', is.keyboardEvent(event));
            }, type, false);
            controls.bindMenuItemShortcuts.call(this, menuItem, type);
            list.appendChild(menuItem);
        },
        // Format a time for display
        formatTime: function formatTime$$1() {
            var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
            var inverted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

            // Bail if the value isn't a number
            if (!is.number(time)) {
                return time;
            } // Always display hours if duration is over an hour


            var forceHours = getHours(this.duration) > 0;
            return formatTime(time, forceHours, inverted);
        },
        // Update the displayed time
        updateTimeDisplay: function updateTimeDisplay() {
            var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
            var time = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
            var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

            // Bail if there's no element to display or the value isn't a number
            if (!is.element(target) || !is.number(time)) {
                return;
            } // eslint-disable-next-line no-param-reassign


            target.innerText = controls.formatTime(time, inverted);
        },
        // Update volume UI and storage
        updateVolume: function updateVolume() {
            if (!this.supported.ui) {
                return;
            } // Update range


            if (is.element(this.elements.inputs.volume)) {
                controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
            } // Update mute state


            if (is.element(this.elements.buttons.mute)) {
                this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
            }
        },
        // Update seek value and lower fill
        setRange: function setRange(target) {
            var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

            if (!is.element(target)) {
                return;
            } // eslint-disable-next-line


            target.value = value; // Webkit range fill

            controls.updateRangeFill.call(this, target);
        },
        // Update <progress> elements
        updateProgress: function updateProgress(event) {
            var _this3 = this;

            if (!this.supported.ui || !is.event(event)) {
                return;
            }

            var value = 0;

            var setProgress = function setProgress(target, input) {
                var value = is.number(input) ? input : 0;
                var progress = is.element(target) ? target : _this3.elements.display.buffer; // Update value and label

                if (is.element(progress)) {
                    progress.value = value; // Update text label inside

                    var label = progress.getElementsByTagName('span')[0];

                    if (is.element(label)) {
                        label.childNodes[0].nodeValue = value;
                    }
                }
            };

            if (event) {
                switch (event.type) {
                    // Video playing
                    case 'timeupdate':
                    case 'seeking':
                    case 'seeked':
                        value = getPercentage(this.currentTime, this.duration); // Set seek range value only if it's a 'natural' time event

                        if (event.type === 'timeupdate') {
                            controls.setRange.call(this, this.elements.inputs.seek, value);
                        }

                        break;
                    // Check buffer status

                    case 'playing':
                    case 'progress':
                        setProgress(this.elements.display.buffer, this.buffered * 100);
                        break;

                    default:
                        break;
                }
            }
        },
        // Webkit polyfill for lower fill range
        updateRangeFill: function updateRangeFill(target) {
            // Get range from event if event passed
            var range = is.event(target) ? target.target : target; // Needs to be a valid <input type='range'>

            if (!is.element(range) || range.getAttribute('type') !== 'range') {
                return;
            } // Set aria values for https://github.com/sampotts/plyr/issues/905


            if (matches(range, this.config.selectors.inputs.seek)) {
                range.setAttribute('aria-valuenow', this.currentTime);
                var currentTime = controls.formatTime(this.currentTime);
                var duration = controls.formatTime(this.duration);
                var format$$1 = i18n.get('seekLabel', this.config);
                range.setAttribute('aria-valuetext', format$$1.replace('{currentTime}', currentTime).replace('{duration}', duration));
            } else if (matches(range, this.config.selectors.inputs.volume)) {
                var percent = range.value * 100;
                range.setAttribute('aria-valuenow', percent);
                range.setAttribute('aria-valuetext', "".concat(percent.toFixed(1), "%"));
            } else {
                range.setAttribute('aria-valuenow', range.value);
            } // WebKit only


            if (!browser.isWebkit) {
                return;
            } // Set CSS custom property


            range.style.setProperty('--value', "".concat(range.value / range.max * 100, "%"));
        },
        // Update hover tooltip for seeking
        updateSeekTooltip: function updateSeekTooltip(event) {
            var _this4 = this;

            // Bail if setting not true
            if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {
                return;
            } // Calculate percentage


            var percent = 0;
            var clientRect = this.elements.progress.getBoundingClientRect();
            var visible = "".concat(this.config.classNames.tooltip, "--visible");

            var toggle = function toggle(_toggle) {
                toggleClass(_this4.elements.display.seekTooltip, visible, _toggle);
            }; // Hide on touch


            if (this.touch) {
                toggle(false);
                return;
            } // Determine percentage, if already visible


            if (is.event(event)) {
                percent = 100 / clientRect.width * (event.pageX - clientRect.left);
            } else if (hasClass(this.elements.display.seekTooltip, visible)) {
                percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
            } else {
                return;
            } // Set bounds


            if (percent < 0) {
                percent = 0;
            } else if (percent > 100) {
                percent = 100;
            } // Display the time a click would seek to


            controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position

            this.elements.display.seekTooltip.style.left = "".concat(percent, "%"); // Show/hide the tooltip
            // If the event is a moues in/out and percentage is inside bounds

            if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
                toggle(event.type === 'mouseenter');
            }
        },
        // Handle time change event
        timeUpdate: function timeUpdate(event) {
            // Only invert if only one time element is displayed and used for both duration and currentTime
            var invert = !is.element(this.elements.display.duration) && this.config.invertTime; // Duration

            controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); // Ignore updates while seeking

            if (event && event.type === 'timeupdate' && this.media.seeking) {
                return;
            } // Playing progress


            controls.updateProgress.call(this, event);
        },
        // Show the duration on metadataloaded or durationchange events
        durationUpdate: function durationUpdate() {
            // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
            if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
                return;
            } // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.
            // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
            // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
            // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338


            if (this.duration >= Math.pow(2, 32)) {
                toggleHidden(this.elements.display.currentTime, true);
                toggleHidden(this.elements.progress, true);
                return;
            } // Update ARIA values


            if (is.element(this.elements.inputs.seek)) {
                this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
            } // If there's a spot to display duration


            var hasDuration = is.element(this.elements.display.duration); // If there's only one time display, display duration there

            if (!hasDuration && this.config.displayDuration && this.paused) {
                controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
            } // If there's a duration element, update content


            if (hasDuration) {
                controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
            } // Update the tooltip (if visible)


            controls.updateSeekTooltip.call(this);
        },
        // Hide/show a tab
        toggleMenuButton: function toggleMenuButton(setting, toggle) {
            toggleHidden(this.elements.settings.buttons[setting], !toggle);
        },
        // Update the selected setting
        updateSetting: function updateSetting(setting, container, input) {
            var pane = this.elements.settings.panels[setting];
            var value = null;
            var list = container;

            if (setting === 'captions') {
                value = this.currentTrack;
            } else {
                value = !is.empty(input) ? input : this[setting]; // Get default

                if (is.empty(value)) {
                    value = this.config[setting].default;
                } // Unsupported value


                if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
                    this.debug.warn("Unsupported value of '".concat(value, "' for ").concat(setting));
                    return;
                } // Disabled value


                if (!this.config[setting].options.includes(value)) {
                    this.debug.warn("Disabled value of '".concat(value, "' for ").concat(setting));
                    return;
                }
            } // Get the list if we need to


            if (!is.element(list)) {
                list = pane && pane.querySelector('[role="menu"]');
            } // If there's no list it means it's not been rendered...


            if (!is.element(list)) {
                return;
            } // Update the label


            var label = this.elements.settings.buttons[setting].querySelector(".".concat(this.config.classNames.menu.value));
            label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it

            var target = list && list.querySelector("[value=\"".concat(value, "\"]"));

            if (is.element(target)) {
                target.checked = true;
            }
        },
        // Translate a value into a nice label
        getLabel: function getLabel(setting, value) {
            switch (setting) {
                case 'speed':
                    return value === 1 ? i18n.get('normal', this.config) : "".concat(value, "&times;");

                case 'quality':
                    if (is.number(value)) {
                        var label = i18n.get("qualityLabel.".concat(value), this.config);

                        if (!label.length) {
                            return "".concat(value, "p");
                        }

                        return label;
                    }

                    return toTitleCase(value);

                case 'captions':
                    return captions.getLabel.call(this);

                default:
                    return null;
            }
        },
        // Set the quality menu
        setQualityMenu: function setQualityMenu(options) {
            var _this5 = this;

            // Menu required
            if (!is.element(this.elements.settings.panels.quality)) {
                return;
            }

            var type = 'quality';
            var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config

            if (is.array(options)) {
                this.options.quality = dedupe(options).filter(function (quality) {
                    return _this5.config.quality.options.includes(quality);
                });
            } // Toggle the pane and tab


            var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
            controls.toggleMenuButton.call(this, type, toggle); // Empty the menu

            emptyElement(list); // Check if we need to toggle the parent

            controls.checkMenu.call(this); // If we're hiding, nothing more to do

            if (!toggle) {
                return;
            } // Get the badge HTML for HD, 4K etc


            var getBadge = function getBadge(quality) {
                var label = i18n.get("qualityBadge.".concat(quality), _this5.config);

                if (!label.length) {
                    return null;
                }

                return controls.createBadge.call(_this5, label);
            }; // Sort options by the config and then render options


            this.options.quality.sort(function (a, b) {
                var sorting = _this5.config.quality.options;
                return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
            }).forEach(function (quality) {
                controls.createMenuItem.call(_this5, {
                    value: quality,
                    list: list,
                    type: type,
                    title: controls.getLabel.call(_this5, 'quality', quality),
                    badge: getBadge(quality)
                });
            });
            controls.updateSetting.call(this, type, list);
        },
        // Set the looping options

        /* setLoopMenu() {
        // Menu required
        if (!is.element(this.elements.settings.panels.loop)) {
            return;
        }
         const options = ['start', 'end', 'all', 'reset'];
        const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
         // Show the pane and tab
        toggleHidden(this.elements.settings.buttons.loop, false);
        toggleHidden(this.elements.settings.panels.loop, false);
         // Toggle the pane and tab
        const toggle = !is.empty(this.loop.options);
        controls.toggleMenuButton.call(this, 'loop', toggle);
         // Empty the menu
        emptyElement(list);
         options.forEach(option => {
            const item = createElement('li');
             const button = createElement(
                'button',
                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
                    type: 'button',
                    class: this.config.classNames.control,
                    'data-plyr-loop-action': option,
                }),
                i18n.get(option, this.config)
            );
             if (['start', 'end'].includes(option)) {
                const badge = controls.createBadge.call(this, '00:00');
                button.appendChild(badge);
            }
             item.appendChild(button);
            list.appendChild(item);
        });
    }, */
        // Get current selected caption language
        // TODO: rework this to user the getter in the API?
        // Set a list of available captions languages
        setCaptionsMenu: function setCaptionsMenu() {
            var _this6 = this;

            // Menu required
            if (!is.element(this.elements.settings.panels.captions)) {
                return;
            } // TODO: Captions or language? Currently it's mixed


            var type = 'captions';
            var list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
            var tracks = captions.getTracks.call(this);
            var toggle = Boolean(tracks.length); // Toggle the pane and tab

            controls.toggleMenuButton.call(this, type, toggle); // Empty the menu

            emptyElement(list); // Check if we need to toggle the parent

            controls.checkMenu.call(this); // If there's no captions, bail

            if (!toggle) {
                return;
            } // Generate options data


            var options = tracks.map(function (track, value) {
                return {
                    value: value,
                    checked: _this6.captions.toggled && _this6.currentTrack === value,
                    title: captions.getLabel.call(_this6, track),
                    badge: track.language && controls.createBadge.call(_this6, track.language.toUpperCase()),
                    list: list,
                    type: 'language'
                };
            }); // Add the "Disabled" option to turn off captions

            options.unshift({
                value: -1,
                checked: !this.captions.toggled,
                title: i18n.get('disabled', this.config),
                list: list,
                type: 'language'
            }); // Generate options

            options.forEach(controls.createMenuItem.bind(this));
            controls.updateSetting.call(this, type, list);
        },
        // Set a list of available captions languages
        setSpeedMenu: function setSpeedMenu(options) {
            var _this7 = this;

            // Menu required
            if (!is.element(this.elements.settings.panels.speed)) {
                return;
            }

            var type = 'speed';
            var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options

            if (is.array(options)) {
                this.options.speed = options;
            } else if (this.isHTML5 || this.isVimeo) {
                this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
            } // Set options if passed and filter based on config


            this.options.speed = this.options.speed.filter(function (speed) {
                return _this7.config.speed.options.includes(speed);
            }); // Toggle the pane and tab

            var toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
            controls.toggleMenuButton.call(this, type, toggle); // Empty the menu

            emptyElement(list); // Check if we need to toggle the parent

            controls.checkMenu.call(this); // If we're hiding, nothing more to do

            if (!toggle) {
                return;
            } // Create items


            this.options.speed.forEach(function (speed) {
                controls.createMenuItem.call(_this7, {
                    value: speed,
                    list: list,
                    type: type,
                    title: controls.getLabel.call(_this7, 'speed', speed)
                });
            });
            controls.updateSetting.call(this, type, list);
        },
        // Check if we need to hide/show the settings menu
        checkMenu: function checkMenu() {
            var buttons = this.elements.settings.buttons;
            var visible = !is.empty(buttons) && Object.values(buttons).some(function (button) {
                return !button.hidden;
            });
            toggleHidden(this.elements.settings.menu, !visible);
        },
        // Focus the first menu item in a given (or visible) menu
        focusFirstMenuItem: function focusFirstMenuItem(pane) {
            var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

            if (this.elements.settings.popup.hidden) {
                return;
            }

            var target = pane;

            if (!is.element(target)) {
                target = Object.values(this.elements.settings.panels).find(function (pane) {
                    return !pane.hidden;
                });
            }

            var firstItem = target.querySelector('[role^="menuitem"]');
            setFocus.call(this, firstItem, tabFocus);
        },
        // Show/hide menu
        toggleMenu: function toggleMenu(input) {
            var popup = this.elements.settings.popup;
            var button = this.elements.buttons.settings; // Menu and button are required

            if (!is.element(popup) || !is.element(button)) {
                return;
            } // True toggle by default


            var hidden = popup.hidden;
            var show = hidden;

            if (is.boolean(input)) {
                show = input;
            } else if (is.keyboardEvent(input) && input.which === 27) {
                show = false;
            } else if (is.event(input)) {
                var isMenuItem = popup.contains(input.target); // If the click was inside the menu or if the click
                // wasn't the button or menu item and we're trying to
                // show the menu (a doc click shouldn't show the menu)

                if (isMenuItem || !isMenuItem && input.target !== button && show) {
                    return;
                }
            } // Set button attributes


            button.setAttribute('aria-expanded', show); // Show the actual popup

            toggleHidden(popup, !show); // Add class hook

            toggleClass(this.elements.container, this.config.classNames.menu.open, show); // Focus the first item if key interaction

            if (show && is.keyboardEvent(input)) {
                controls.focusFirstMenuItem.call(this, null, true);
            } else if (!show && !hidden) {
                // If closing, re-focus the button
                setFocus.call(this, button, is.keyboardEvent(input));
            }
        },
        // Get the natural size of a menu panel
        getMenuSize: function getMenuSize(tab) {
            var clone = tab.cloneNode(true);
            clone.style.position = 'absolute';
            clone.style.opacity = 0;
            clone.removeAttribute('hidden'); // Append to parent so we get the "real" size

            tab.parentNode.appendChild(clone); // Get the sizes before we remove

            var width = clone.scrollWidth;
            var height = clone.scrollHeight; // Remove from the DOM

            removeElement(clone);
            return {
                width: width,
                height: height
            };
        },
        // Show a panel in the menu
        showMenuPanel: function showMenuPanel() {
            var _this8 = this;

            var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
            var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
            var target = document.getElementById("plyr-settings-".concat(this.id, "-").concat(type)); // Nothing to show, bail

            if (!is.element(target)) {
                return;
            } // Hide all other panels


            var container = target.parentNode;
            var current = Array.from(container.children).find(function (node) {
                return !node.hidden;
            }); // If we can do fancy animations, we'll animate the height/width

            if (support.transitions && !support.reducedMotion) {
                // Set the current width as a base
                container.style.width = "".concat(current.scrollWidth, "px");
                container.style.height = "".concat(current.scrollHeight, "px"); // Get potential sizes

                var size = controls.getMenuSize.call(this, target); // Restore auto height/width

                var restore = function restore(event) {
                    // We're only bothered about height and width on the container
                    if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
                        return;
                    } // Revert back to auto


                    container.style.width = '';
                    container.style.height = ''; // Only listen once

                    off.call(_this8, container, transitionEndEvent, restore);
                }; // Listen for the transition finishing and restore auto height/width


                on.call(this, container, transitionEndEvent, restore); // Set dimensions to target

                container.style.width = "".concat(size.width, "px");
                container.style.height = "".concat(size.height, "px");
            } // Set attributes on current tab


            toggleHidden(current, true); // Set attributes on target

            toggleHidden(target, false); // Focus the first item

            controls.focusFirstMenuItem.call(this, target, tabFocus);
        },
        // Set the download link
        setDownloadLink: function setDownloadLink() {
            var button = this.elements.buttons.download; // Bail if no button

            if (!is.element(button)) {
                return;
            } // Set download link


            button.setAttribute('href', this.download);
        },
        // Build the default HTML
        // TODO: Set order based on order in the config.controls array?
        create: function create(data) {
            var _this9 = this;

            // Create the container
            var container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); // Restart button

            if (this.config.controls.includes('restart')) {
                container.appendChild(controls.createButton.call(this, 'restart'));
            } // Rewind button


            if (this.config.controls.includes('rewind')) {
                container.appendChild(controls.createButton.call(this, 'rewind'));
            } // Play/Pause button


            if (this.config.controls.includes('play')) {
                container.appendChild(controls.createButton.call(this, 'play'));
            } // Fast forward button


            if (this.config.controls.includes('fast-forward')) {
                container.appendChild(controls.createButton.call(this, 'fast-forward'));
            } // Progress


            if (this.config.controls.includes('progress')) {
                var progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); // Seek range slider

                progress.appendChild(controls.createRange.call(this, 'seek', {
                    id: "plyr-seek-".concat(data.id)
                })); // Buffer progress

                progress.appendChild(controls.createProgress.call(this, 'buffer')); // TODO: Add loop display indicator
                // Seek tooltip

                if (this.config.tooltips.seek) {
                    var tooltip = createElement('span', {
                        class: this.config.classNames.tooltip
                    }, '00:00');
                    progress.appendChild(tooltip);
                    this.elements.display.seekTooltip = tooltip;
                }

                this.elements.progress = progress;
                container.appendChild(this.elements.progress);
            } // Media current time display


            if (this.config.controls.includes('current-time')) {
                container.appendChild(controls.createTime.call(this, 'currentTime'));
            } // Media duration display


            if (this.config.controls.includes('duration')) {
                container.appendChild(controls.createTime.call(this, 'duration'));
            } // Volume controls


            if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) {
                var volume = createElement('div', {
                    class: 'plyr__volume'
                }); // Toggle mute button

                if (this.config.controls.includes('mute')) {
                    volume.appendChild(controls.createButton.call(this, 'mute'));
                } // Volume range control


                if (this.config.controls.includes('volume')) {
                    // Set the attributes
                    var attributes = {
                        max: 1,
                        step: 0.05,
                        value: this.config.volume
                    }; // Create the volume range slider

                    volume.appendChild(controls.createRange.call(this, 'volume', extend(attributes, {
                        id: "plyr-volume-".concat(data.id)
                    })));
                    this.elements.volume = volume;
                }

                container.appendChild(volume);
            } // Toggle captions button


            if (this.config.controls.includes('captions')) {
                container.appendChild(controls.createButton.call(this, 'captions'));
            } // Settings button / menu


            if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
                var control = createElement('div', {
                    class: 'plyr__menu',
                    hidden: ''
                });
                control.appendChild(controls.createButton.call(this, 'settings', {
                    'aria-haspopup': true,
                    'aria-controls': "plyr-settings-".concat(data.id),
                    'aria-expanded': false
                }));
                var popup = createElement('div', {
                    class: 'plyr__menu__container',
                    id: "plyr-settings-".concat(data.id),
                    hidden: ''
                });
                var inner = createElement('div');
                var home = createElement('div', {
                    id: "plyr-settings-".concat(data.id, "-home")
                }); // Create the menu

                var menu = createElement('div', {
                    role: 'menu'
                });
                home.appendChild(menu);
                inner.appendChild(home);
                this.elements.settings.panels.home = home; // Build the menu items

                this.config.settings.forEach(function (type) {
                    // TODO: bundle this with the createMenuItem helper and bindings
                    var menuItem = createElement('button', extend(getAttributesFromSelector(_this9.config.selectors.buttons.settings), {
                        type: 'button',
                        class: "".concat(_this9.config.classNames.control, " ").concat(_this9.config.classNames.control, "--forward"),
                        role: 'menuitem',
                        'aria-haspopup': true,
                        hidden: ''
                    })); // Bind menu shortcuts for keyboard users

                    controls.bindMenuItemShortcuts.call(_this9, menuItem, type); // Show menu on click

                    on(menuItem, 'click', function () {
                        controls.showMenuPanel.call(_this9, type, false);
                    });
                    var flex = createElement('span', null, i18n.get(type, _this9.config));
                    var value = createElement('span', {
                        class: _this9.config.classNames.menu.value
                    }); // Speed contains HTML entities

                    value.innerHTML = data[type];
                    flex.appendChild(value);
                    menuItem.appendChild(flex);
                    menu.appendChild(menuItem); // Build the panes

                    var pane = createElement('div', {
                        id: "plyr-settings-".concat(data.id, "-").concat(type),
                        hidden: ''
                    }); // Back button

                    var backButton = createElement('button', {
                        type: 'button',
                        class: "".concat(_this9.config.classNames.control, " ").concat(_this9.config.classNames.control, "--back")
                    }); // Visible label

                    backButton.appendChild(createElement('span', {
                        'aria-hidden': true
                    }, i18n.get(type, _this9.config))); // Screen reader label

                    backButton.appendChild(createElement('span', {
                        class: _this9.config.classNames.hidden
                    }, i18n.get('menuBack', _this9.config))); // Go back via keyboard

                    on(pane, 'keydown', function (event) {
                        // We only care about <-
                        if (event.which !== 37) {
                            return;
                        } // Prevent seek


                        event.preventDefault();
                        event.stopPropagation(); // Show the respective menu

                        controls.showMenuPanel.call(_this9, 'home', true);
                    }, false); // Go back via button click

                    on(backButton, 'click', function () {
                        controls.showMenuPanel.call(_this9, 'home', false);
                    }); // Add to pane

                    pane.appendChild(backButton); // Menu

                    pane.appendChild(createElement('div', {
                        role: 'menu'
                    }));
                    inner.appendChild(pane);
                    _this9.elements.settings.buttons[type] = menuItem;
                    _this9.elements.settings.panels[type] = pane;
                });
                popup.appendChild(inner);
                control.appendChild(popup);
                container.appendChild(control);
                this.elements.settings.popup = popup;
                this.elements.settings.menu = control;
            } // Picture in picture button


            if (this.config.controls.includes('pip') && support.pip) {
                container.appendChild(controls.createButton.call(this, 'pip'));
            } // Airplay button


            if (this.config.controls.includes('airplay') && support.airplay) {
                container.appendChild(controls.createButton.call(this, 'airplay'));
            } // Download button


            if (this.config.controls.includes('download')) {
                var _attributes = {
                    element: 'a',
                    href: this.download,
                    target: '_blank'
                };
                var download = this.config.urls.download;

                if (!is.url(download) && this.isEmbed) {
                    extend(_attributes, {
                        icon: "logo-".concat(this.provider),
                        label: this.provider
                    });
                }

                container.appendChild(controls.createButton.call(this, 'download', _attributes));
            } // Toggle fullscreen button


            if (this.config.controls.includes('fullscreen')) {
                container.appendChild(controls.createButton.call(this, 'fullscreen'));
            } // Larger overlaid play button


            if (this.config.controls.includes('play-large')) {
                this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
            }

            this.elements.controls = container; // Set available quality levels

            if (this.isHTML5) {
                controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
            }

            controls.setSpeedMenu.call(this);
            return container;
        },
        // Insert controls
        inject: function inject() {
            var _this10 = this;

            // Sprite
            if (this.config.loadSprite) {
                var icon = controls.getIconUrl.call(this); // Only load external sprite using AJAX

                if (icon.cors) {
                    loadSprite(icon.url, 'sprite-plyr');
                }
            } // Create a unique ID


            this.id = Math.floor(Math.random() * 10000); // Null by default

            var container = null;
            this.elements.controls = null; // Set template properties

            var props = {
                id: this.id,
                seektime: this.config.seekTime,
                title: this.config.title
            };
            var update = true; // If function, run it and use output

            if (is.function(this.config.controls)) {
                this.config.controls = this.config.controls.call(this.props);
            } // Convert falsy controls to empty array (primarily for empty strings)


            if (!this.config.controls) {
                this.config.controls = [];
            }

            if (is.element(this.config.controls) || is.string(this.config.controls)) {
                // HTMLElement or Non-empty string passed as the option
                container = this.config.controls;
            } else {
                // Create controls
                container = controls.create.call(this, {
                    id: this.id,
                    seektime: this.config.seekTime,
                    speed: this.speed,
                    quality: this.quality,
                    captions: captions.getLabel.call(this) // TODO: Looping
                    // loop: 'None',

                });
                update = false;
            } // Replace props with their value


            var replace = function replace(input) {
                var result = input;
                Object.entries(props).forEach(function (_ref2) {
                    var _ref3 = _slicedToArray(_ref2, 2),
                        key = _ref3[0],
                        value = _ref3[1];

                    result = replaceAll(result, "{".concat(key, "}"), value);
                });
                return result;
            }; // Update markup


            if (update) {
                if (is.string(this.config.controls)) {
                    container = replace(container);
                } else if (is.element(container)) {
                    container.innerHTML = replace(container.innerHTML);
                }
            } // Controls container


            var target; // Inject to custom location

            if (is.string(this.config.selectors.controls.container)) {
                target = document.querySelector(this.config.selectors.controls.container);
            } // Inject into the container by default


            if (!is.element(target)) {
                target = this.elements.container;
            } // Inject controls HTML (needs to be before captions, hence "afterbegin")


            var insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';
            target[insertMethod]('afterbegin', container); // Find the elements if need be

            if (!is.element(this.elements.controls)) {
                controls.findElements.call(this);
            } // Add pressed property to buttons


            if (!is.empty(this.elements.buttons)) {
                var addProperty = function addProperty(button) {
                    var className = _this10.config.classNames.controlPressed;
                    Object.defineProperty(button, 'pressed', {
                        enumerable: true,
                        get: function get() {
                            return hasClass(button, className);
                        },
                        set: function set() {
                            var pressed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
                            toggleClass(button, className, pressed);
                        }
                    });
                }; // Toggle classname when pressed property is set


                Object.values(this.elements.buttons).filter(Boolean).forEach(function (button) {
                    if (is.array(button) || is.nodeList(button)) {
                        Array.from(button).filter(Boolean).forEach(addProperty);
                    } else {
                        addProperty(button);
                    }
                });
            } // Edge sometimes doesn't finish the paint so force a redraw


            if (window.navigator.userAgent.includes('Edge')) {
                repaint(target);
            } // Setup tooltips


            if (this.config.tooltips.controls) {
                var _this$config = this.config,
                    classNames = _this$config.classNames,
                    selectors = _this$config.selectors;
                var selector = "".concat(selectors.controls.wrapper, " ").concat(selectors.labels, " .").concat(classNames.hidden);
                var labels = getElements.call(this, selector);
                Array.from(labels).forEach(function (label) {
                    toggleClass(label, _this10.config.classNames.hidden, false);
                    toggleClass(label, _this10.config.classNames.tooltip, true);
                });
            }
        }
    };

    /**
     * Parse a string to a URL object
     * @param {string} input - the URL to be parsed
     * @param {boolean} safe - failsafe parsing
     */

    function parseUrl(input) {
        var safe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
        var url = input;

        if (safe) {
            var parser = document.createElement('a');
            parser.href = url;
            url = parser.href;
        }

        try {
            return new URL(url);
        } catch (e) {
            return null;
        }
    } // Convert object to URLSearchParams

    function buildUrlParams(input) {
        var params = new URLSearchParams();

        if (is.object(input)) {
            Object.entries(input).forEach(function (_ref) {
                var _ref2 = _slicedToArray(_ref, 2),
                    key = _ref2[0],
                    value = _ref2[1];

                params.set(key, value);
            });
        }

        return params;
    }

    var captions = {
        // Setup captions
        setup: function setup() {
            // Requires UI support
            if (!this.supported.ui) {
                return;
            } // Only Vimeo and HTML5 video supported at this point


            if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {
                // Clear menu and hide
                if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
                    controls.setCaptionsMenu.call(this);
                }

                return;
            } // Inject the container


            if (!is.element(this.elements.captions)) {
                this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
                insertAfter(this.elements.captions, this.elements.wrapper);
            } // Fix IE captions if CORS is used
            // Fetch captions and inject as blobs instead (data URIs not supported!)


            if (browser.isIE && window.URL) {
                var elements = this.media.querySelectorAll('track');
                Array.from(elements).forEach(function (track) {
                    var src = track.getAttribute('src');
                    var url = parseUrl(src);

                    if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {
                        fetch(src, 'blob').then(function (blob) {
                            track.setAttribute('src', window.URL.createObjectURL(blob));
                        }).catch(function () {
                            removeElement(track);
                        });
                    }
                });
            } // Get and set initial data
            // The "preferred" options are not realized unless / until the wanted language has a match
            // * languages: Array of user's browser languages.
            // * language:  The language preferred by user settings or config
            // * active:    The state preferred by user settings or config
            // * toggled:   The real captions state


            var browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
            var languages = dedupe(browserLanguages.map(function (language) {
                return language.split('-')[0];
            }));
            var language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); // Use first browser language when language is 'auto'

            if (language === 'auto') {
                var _languages = _slicedToArray(languages, 1);

                language = _languages[0];
            }

            var active = this.storage.get('captions');

            if (!is.boolean(active)) {
                active = this.config.captions.active;
            }

            Object.assign(this.captions, {
                toggled: false,
                active: active,
                language: language,
                languages: languages
            }); // Watch changes to textTracks and update captions menu

            if (this.isHTML5) {
                var trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
                on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
            } // Update available languages in list next tick (the event must not be triggered before the listeners)


            setTimeout(captions.update.bind(this), 0);
        },
        // Update available language options in settings based on tracks
        update: function update() {
            var _this = this;

            var tracks = captions.getTracks.call(this, true); // Get the wanted language

            var _this$captions = this.captions,
                active = _this$captions.active,
                language = _this$captions.language,
                meta = _this$captions.meta,
                currentTrackNode = _this$captions.currentTrackNode;
            var languageExists = Boolean(tracks.find(function (track) {
                return track.language === language;
            })); // Handle tracks (add event listener and "pseudo"-default)

            if (this.isHTML5 && this.isVideo) {
                tracks.filter(function (track) {
                    return !meta.get(track);
                }).forEach(function (track) {
                    _this.debug.log('Track added', track); // Attempt to store if the original dom element was "default"


                    meta.set(track, {
                        default: track.mode === 'showing'
                    }); // Turn off native caption rendering to avoid double captions

                    track.mode = 'hidden'; // Add event listener for cue changes

                    on.call(_this, track, 'cuechange', function () {
                        return captions.updateCues.call(_this);
                    });
                });
            } // Update language first time it matches, or if the previous matching track was removed


            if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {
                captions.setLanguage.call(this, language);
                captions.toggle.call(this, active && languageExists);
            } // Enable or disable captions based on track length


            toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); // Update available languages in list

            if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
                controls.setCaptionsMenu.call(this);
            }
        },
        // Toggle captions display
        // Used internally for the toggleCaptions method, with the passive option forced to false
        toggle: function toggle(input) {
            var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

            // If there's no full support
            if (!this.supported.ui) {
                return;
            }

            var toggled = this.captions.toggled; // Current state

            var activeClass = this.config.classNames.captions.active; // Get the next state
            // If the method is called without parameter, toggle based on current value

            var active = is.nullOrUndefined(input) ? !toggled : input; // Update state and trigger event

            if (active !== toggled) {
                // When passive, don't override user preferences
                if (!passive) {
                    this.captions.active = active;
                    this.storage.set({
                        captions: active
                    });
                } // Force language if the call isn't passive and there is no matching language to toggle to


                if (!this.language && active && !passive) {
                    var tracks = captions.getTracks.call(this);
                    var track = captions.findTrack.call(this, [this.captions.language].concat(_toConsumableArray(this.captions.languages)), true); // Override user preferences to avoid switching languages if a matching track is added

                    this.captions.language = track.language; // Set caption, but don't store in localStorage as user preference

                    captions.set.call(this, tracks.indexOf(track));
                    return;
                } // Toggle button if it's enabled


                if (this.elements.buttons.captions) {
                    this.elements.buttons.captions.pressed = active;
                } // Add class hook


                toggleClass(this.elements.container, activeClass, active);
                this.captions.toggled = active; // Update settings menu

                controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)

                triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
            }
        },
        // Set captions by track index
        // Used internally for the currentTrack setter with the passive option forced to false
        set: function set(index) {
            var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
            var tracks = captions.getTracks.call(this); // Disable captions if setting to -1

            if (index === -1) {
                captions.toggle.call(this, false, passive);
                return;
            }

            if (!is.number(index)) {
                this.debug.warn('Invalid caption argument', index);
                return;
            }

            if (!(index in tracks)) {
                this.debug.warn('Track not found', index);
                return;
            }

            if (this.captions.currentTrack !== index) {
                this.captions.currentTrack = index;
                var track = tracks[index];

                var _ref = track || {},
                    language = _ref.language; // Store reference to node for invalidation on remove


                this.captions.currentTrackNode = track; // Update settings menu

                controls.updateSetting.call(this, 'captions'); // When passive, don't override user preferences

                if (!passive) {
                    this.captions.language = language;
                    this.storage.set({
                        language: language
                    });
                } // Handle Vimeo captions


                if (this.isVimeo) {
                    this.embed.enableTextTrack(language);
                } // Trigger event


                triggerEvent.call(this, this.media, 'languagechange');
            } // Show captions


            captions.toggle.call(this, true, passive);

            if (this.isHTML5 && this.isVideo) {
                // If we change the active track while a cue is already displayed we need to update it
                captions.updateCues.call(this);
            }
        },
        // Set captions by language
        // Used internally for the language setter with the passive option forced to false
        setLanguage: function setLanguage(input) {
            var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

            if (!is.string(input)) {
                this.debug.warn('Invalid language argument', input);
                return;
            } // Normalize


            var language = input.toLowerCase();
            this.captions.language = language; // Set currentTrack

            var tracks = captions.getTracks.call(this);
            var track = captions.findTrack.call(this, [language]);
            captions.set.call(this, tracks.indexOf(track), passive);
        },
        // Get current valid caption tracks
        // If update is false it will also ignore tracks without metadata
        // This is used to "freeze" the language options when captions.update is false
        getTracks: function getTracks() {
            var _this2 = this;

            var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
            // Handle media or textTracks missing or null
            var tracks = Array.from((this.media || {}).textTracks || []); // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
            // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)

            return tracks.filter(function (track) {
                return !_this2.isHTML5 || update || _this2.captions.meta.has(track);
            }).filter(function (track) {
                return ['captions', 'subtitles'].includes(track.kind);
            });
        },
        // Match tracks based on languages and get the first
        findTrack: function findTrack(languages) {
            var _this3 = this;

            var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
            var tracks = captions.getTracks.call(this);

            var sortIsDefault = function sortIsDefault(track) {
                return Number((_this3.captions.meta.get(track) || {}).default);
            };

            var sorted = Array.from(tracks).sort(function (a, b) {
                return sortIsDefault(b) - sortIsDefault(a);
            });
            var track;
            languages.every(function (language) {
                track = sorted.find(function (track) {
                    return track.language === language;
                });
                return !track; // Break iteration if there is a match
            }); // If no match is found but is required, get first

            return track || (force ? sorted[0] : undefined);
        },
        // Get the current track
        getCurrentTrack: function getCurrentTrack() {
            return captions.getTracks.call(this)[this.currentTrack];
        },
        // Get UI label for track
        getLabel: function getLabel(track) {
            var currentTrack = track;

            if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
                currentTrack = captions.getCurrentTrack.call(this);
            }

            if (is.track(currentTrack)) {
                if (!is.empty(currentTrack.label)) {
                    return currentTrack.label;
                }

                if (!is.empty(currentTrack.language)) {
                    return track.language.toUpperCase();
                }

                return i18n.get('enabled', this.config);
            }

            return i18n.get('disabled', this.config);
        },
        // Update captions using current track's active cues
        // Also optional array argument in case there isn't any track (ex: vimeo)
        updateCues: function updateCues(input) {
            // Requires UI
            if (!this.supported.ui) {
                return;
            }

            if (!is.element(this.elements.captions)) {
                this.debug.warn('No captions element to render to');
                return;
            } // Only accept array or empty input


            if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
                this.debug.warn('updateCues: Invalid input', input);
                return;
            }

            var cues = input; // Get cues from track

            if (!cues) {
                var track = captions.getCurrentTrack.call(this);
                cues = Array.from((track || {}).activeCues || []).map(function (cue) {
                    return cue.getCueAsHTML();
                }).map(getHTML);
            } // Set new caption text


            var content = cues.map(function (cueText) {
                return cueText.trim();
            }).join('\n');
            var changed = content !== this.elements.captions.innerHTML;

            if (changed) {
                // Empty the container and create a new child element
                emptyElement(this.elements.captions);
                var caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
                caption.innerHTML = content;
                this.elements.captions.appendChild(caption); // Trigger event

                triggerEvent.call(this, this.media, 'cuechange');
            }
        }
    };

    // ==========================================================================
    // Plyr default config
    // ==========================================================================
    var defaults = {
        // Disable
        enabled: true,
        // Custom media title
        title: '',
        // Logging to console
        debug: false,
        // Auto play (if supported)
        autoplay: false,
        // Only allow one media playing at once (vimeo only)
        autopause: true,
        // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
        // TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
        playsinline: true,
        // Default time to skip when rewind/fast forward
        seekTime: 10,
        // Default volume
        volume: 1,
        muted: false,
        // Pass a custom duration
        duration: null,
        // Display the media duration on load in the current time position
        // If you have opted to display both duration and currentTime, this is ignored
        displayDuration: true,
        // Invert the current time to be a countdown
        invertTime: true,
        // Clicking the currentTime inverts it's value to show time left rather than elapsed
        toggleInvert: true,
        // Aspect ratio (for embeds)
        ratio: '16:9',
        // Click video container to play/pause
        clickToPlay: true,
        // Auto hide the controls
        hideControls: true,
        // Reset to start when playback ended
        resetOnEnd: false,
        // Disable the standard context menu
        disableContextMenu: true,
        // Sprite (for icons)
        loadSprite: true,
        iconPrefix: 'plyr',
        iconUrl: 'https://cdn.plyr.io/3.4.7/plyr.svg',
        // Blank video (used to prevent errors on source change)
        blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
        // Quality default
        quality: {
            default: 576,
            options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240]
        },
        // Set loops
        loop: {
            active: false // start: null,
            // end: null,

        },
        // Speed default and options to display
        speed: {
            selected: 1,
            options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
        },
        // Keyboard shortcut settings
        keyboard: {
            focused: true,
            global: false
        },
        // Display tooltips
        tooltips: {
            controls: false,
            seek: true
        },
        // Captions settings
        captions: {
            active: false,
            language: 'auto',
            // Listen to new tracks added after Plyr is initialized.
            // This is needed for streaming captions, but may result in unselectable options
            update: false
        },
        // Fullscreen settings
        fullscreen: {
            enabled: true,
            // Allow fullscreen?
            fallback: true,
            // Fallback for vintage browsers
            iosNative: false // Use the native fullscreen in iOS (disables custom controls)

        },
        // Local storage
        storage: {
            enabled: true,
            key: 'plyr'
        },
        // Default controls
        controls: ['play-large', // 'restart',
            // 'rewind',
            'play', // 'fast-forward',
            'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
            'fullscreen'],
        settings: ['captions', 'quality', 'speed'],
        // Localisation
        i18n: {
            restart: 'Restart',
            rewind: 'Rewind {seektime}s',
            play: 'Play',
            pause: 'Pause',
            fastForward: 'Forward {seektime}s',
            seek: 'Seek',
            seekLabel: '{currentTime} of {duration}',
            played: 'Played',
            buffered: 'Buffered',
            currentTime: 'Current time',
            duration: 'Duration',
            volume: 'Volume',
            mute: 'Mute',
            unmute: 'Unmute',
            enableCaptions: 'Enable captions',
            disableCaptions: 'Disable captions',
            download: 'Download',
            enterFullscreen: 'Enter fullscreen',
            exitFullscreen: 'Exit fullscreen',
            frameTitle: 'Player for {title}',
            captions: 'Captions',
            settings: 'Settings',
            menuBack: 'Go back to previous menu',
            speed: 'Speed',
            normal: 'Normal',
            quality: 'Quality',
            loop: 'Loop',
            start: 'Start',
            end: 'End',
            all: 'All',
            reset: 'Reset',
            disabled: 'Disabled',
            enabled: 'Enabled',
            advertisement: 'Ad',
            qualityBadge: {
                2160: '4K',
                1440: 'HD',
                1080: 'HD',
                720: 'HD',
                576: 'SD',
                480: 'SD'
            }
        },
        // URLs
        urls: {
            download: null,
            vimeo: {
                sdk: 'https://player.vimeo.com/api/player.js',
                iframe: 'https://player.vimeo.com/video/{0}?{1}',
                api: 'https://vimeo.com/api/v2/video/{0}.json'
            },
            youtube: {
                sdk: 'https://www.youtube.com/iframe_api',
                api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet'
            },
            googleIMA: {
                sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'
            }
        },
        // Custom control listeners
        listeners: {
            seek: null,
            play: null,
            pause: null,
            restart: null,
            rewind: null,
            fastForward: null,
            mute: null,
            volume: null,
            captions: null,
            download: null,
            fullscreen: null,
            pip: null,
            airplay: null,
            speed: null,
            quality: null,
            loop: null,
            language: null
        },
        // Events to watch and bubble
        events: [// Events to watch on HTML5 media elements and bubble
            // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events
            'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
            'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
            'statechange', // Quality
            'qualitychange', // Ads
            'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
        // Selectors
        // Change these to match your template if using custom HTML
        selectors: {
            editable: 'input, textarea, select, [contenteditable]',
            container: '.plyr',
            controls: {
                container: null,
                wrapper: '.plyr__controls'
            },
            labels: '[data-plyr]',
            buttons: {
                play: '[data-plyr="play"]',
                pause: '[data-plyr="pause"]',
                restart: '[data-plyr="restart"]',
                rewind: '[data-plyr="rewind"]',
                fastForward: '[data-plyr="fast-forward"]',
                mute: '[data-plyr="mute"]',
                captions: '[data-plyr="captions"]',
                download: '[data-plyr="download"]',
                fullscreen: '[data-plyr="fullscreen"]',
                pip: '[data-plyr="pip"]',
                airplay: '[data-plyr="airplay"]',
                settings: '[data-plyr="settings"]',
                loop: '[data-plyr="loop"]'
            },
            inputs: {
                seek: '[data-plyr="seek"]',
                volume: '[data-plyr="volume"]',
                speed: '[data-plyr="speed"]',
                language: '[data-plyr="language"]',
                quality: '[data-plyr="quality"]'
            },
            display: {
                currentTime: '.plyr__time--current',
                duration: '.plyr__time--duration',
                buffer: '.plyr__progress__buffer',
                loop: '.plyr__progress__loop',
                // Used later
                volume: '.plyr__volume--display'
            },
            progress: '.plyr__progress',
            captions: '.plyr__captions',
            caption: '.plyr__caption',
            menu: {
                quality: '.js-plyr__menu__list--quality'
            }
        },
        // Class hooks added to the player in different states
        classNames: {
            type: 'plyr--{0}',
            provider: 'plyr--{0}',
            video: 'plyr__video-wrapper',
            embed: 'plyr__video-embed',
            embedContainer: 'plyr__video-embed__container',
            poster: 'plyr__poster',
            posterEnabled: 'plyr__poster-enabled',
            ads: 'plyr__ads',
            control: 'plyr__control',
            controlPressed: 'plyr__control--pressed',
            playing: 'plyr--playing',
            paused: 'plyr--paused',
            stopped: 'plyr--stopped',
            loading: 'plyr--loading',
            hover: 'plyr--hover',
            tooltip: 'plyr__tooltip',
            cues: 'plyr__cues',
            hidden: 'plyr__sr-only',
            hideControls: 'plyr--hide-controls',
            isIos: 'plyr--is-ios',
            isTouch: 'plyr--is-touch',
            uiSupported: 'plyr--full-ui',
            noTransition: 'plyr--no-transition',
            display: {
                time: 'plyr__time'
            },
            menu: {
                value: 'plyr__menu__value',
                badge: 'plyr__badge',
                open: 'plyr--menu-open'
            },
            captions: {
                enabled: 'plyr--captions-enabled',
                active: 'plyr--captions-active'
            },
            fullscreen: {
                enabled: 'plyr--fullscreen-enabled',
                fallback: 'plyr--fullscreen-fallback'
            },
            pip: {
                supported: 'plyr--pip-supported',
                active: 'plyr--pip-active'
            },
            airplay: {
                supported: 'plyr--airplay-supported',
                active: 'plyr--airplay-active'
            },
            tabFocus: 'plyr__tab-focus'
        },
        // Embed attributes
        attributes: {
            embed: {
                provider: 'data-plyr-provider',
                id: 'data-plyr-embed-id'
            }
        },
        // API keys
        keys: {
            google: null
        },
        // Advertisements plugin
        // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
        ads: {
            enabled: false,
            publisherId: ''
        }
    };

    // ==========================================================================
    // Plyr states
    // ==========================================================================
    var pip = {
        active: 'picture-in-picture',
        inactive: 'inline'
    };

    // ==========================================================================
    // Plyr supported types and providers
    // ==========================================================================
    var providers = {
        html5: 'html5',
        youtube: 'youtube',
        vimeo: 'vimeo'
    };
    var types = {
        audio: 'audio',
        video: 'video'
    };
    /**
     * Get provider by URL
     * @param {String} url
     */

    function getProviderByUrl(url) {
        // YouTube
        if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
            return providers.youtube;
        } // Vimeo


        if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
            return providers.vimeo;
        }

        return null;
    }

    // ==========================================================================
    // Console wrapper
    // ==========================================================================
    var noop = function noop() {};

    var Console =
        /*#__PURE__*/
        function () {
            function Console() {
                var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

                _classCallCheck(this, Console);

                this.enabled = window.console && enabled;

                if (this.enabled) {
                    this.log('Debugging enabled');
                }
            }

            _createClass(Console, [{
                key: "log",
                get: function get() {
                    // eslint-disable-next-line no-console
                    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
                }
            }, {
                key: "warn",
                get: function get() {
                    // eslint-disable-next-line no-console
                    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
                }
            }, {
                key: "error",
                get: function get() {
                    // eslint-disable-next-line no-console
                    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
                }
            }]);

            return Console;
        }();

    function onChange() {
        if (!this.enabled) {
            return;
        } // Update toggle button


        var button = this.player.elements.buttons.fullscreen;

        if (is.element(button)) {
            button.pressed = this.active;
        } // Trigger an event


        triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); // Trap focus in container

        if (!browser.isIos) {
            trapFocus.call(this.player, this.target, this.active);
        }
    }

    function toggleFallback() {
        var _this = this;

        var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

        // Store or restore scroll position
        if (toggle) {
            this.scrollPosition = {
                x: window.scrollX || 0,
                y: window.scrollY || 0
            };
        } else {
            window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
        } // Toggle scroll


        document.body.style.overflow = toggle ? 'hidden' : ''; // Toggle class hook

        toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); // Force full viewport on iPhone X+

        if (browser.isIos) {
            var viewport = document.head.querySelector('meta[name="viewport"]');
            var property = 'viewport-fit=cover'; // Inject the viewport meta if required

            if (!viewport) {
                viewport = document.createElement('meta');
                viewport.setAttribute('name', 'viewport');
            } // Check if the property already exists


            var hasProperty = is.string(viewport.content) && viewport.content.includes(property);

            if (toggle) {
                this.cleanupViewport = !hasProperty;

                if (!hasProperty) {
                    viewport.content += ",".concat(property);
                }
            } else if (this.cleanupViewport) {
                viewport.content = viewport.content.split(',').filter(function (part) {
                    return part.trim() !== property;
                }).join(',');
            } // Force a repaint as sometimes Safari doesn't want to fill the screen


            setTimeout(function () {
                return repaint(_this.target);
            }, 100);
        } // Toggle button and fire events


        onChange.call(this);
    }

    var Fullscreen =
        /*#__PURE__*/
        function () {
            function Fullscreen(player) {
                var _this2 = this;

                _classCallCheck(this, Fullscreen);

                // Keep reference to parent
                this.player = player; // Get prefix

                this.prefix = Fullscreen.prefix;
                this.property = Fullscreen.property; // Scroll position

                this.scrollPosition = {
                    x: 0,
                    y: 0
                }; // Register event listeners
                // Handle event (incase user presses escape etc)

                on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
                    // TODO: Filter for target??
                    onChange.call(_this2);
                }); // Fullscreen toggle on double click

                on.call(this.player, this.player.elements.container, 'dblclick', function (event) {
                    // Ignore double click in controls
                    if (is.element(_this2.player.elements.controls) && _this2.player.elements.controls.contains(event.target)) {
                        return;
                    }

                    _this2.toggle();
                }); // Update the UI

                this.update();
            } // Determine if native supported


            _createClass(Fullscreen, [{
                key: "update",
                // Update UI
                value: function update() {
                    if (this.enabled) {
                        this.player.debug.log("".concat(Fullscreen.native ? 'Native' : 'Fallback', " fullscreen enabled"));
                    } else {
                        this.player.debug.log('Fullscreen not supported and fallback disabled');
                    } // Add styling hook to show button


                    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
                } // Make an element fullscreen

            }, {
                key: "enter",
                value: function enter() {
                    if (!this.enabled) {
                        return;
                    } // iOS native fullscreen doesn't need the request step


                    if (browser.isIos && this.player.config.fullscreen.iosNative) {
                        this.target.webkitEnterFullscreen();
                    } else if (!Fullscreen.native) {
                        toggleFallback.call(this, true);
                    } else if (!this.prefix) {
                        this.target.requestFullscreen();
                    } else if (!is.empty(this.prefix)) {
                        this.target["".concat(this.prefix, "Request").concat(this.property)]();
                    }
                } // Bail from fullscreen

            }, {
                key: "exit",
                value: function exit() {
                    if (!this.enabled) {
                        return;
                    } // iOS native fullscreen


                    if (browser.isIos && this.player.config.fullscreen.iosNative) {
                        this.target.webkitExitFullscreen();
                        this.player.play();
                    } else if (!Fullscreen.native) {
                        toggleFallback.call(this, false);
                    } else if (!this.prefix) {
                        (document.cancelFullScreen || document.exitFullscreen).call(document);
                    } else if (!is.empty(this.prefix)) {
                        var action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
                        document["".concat(this.prefix).concat(action).concat(this.property)]();
                    }
                } // Toggle state

            }, {
                key: "toggle",
                value: function toggle() {
                    if (!this.active) {
                        this.enter();
                    } else {
                        this.exit();
                    }
                }
            }, {
                key: "enabled",
                // Determine if fullscreen is enabled
                get: function get() {
                    return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
                } // Get active state

            }, {
                key: "active",
                get: function get() {
                    if (!this.enabled) {
                        return false;
                    } // Fallback using classname


                    if (!Fullscreen.native) {
                        return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
                    }

                    var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
                    return element === this.target;
                } // Get target element

            }, {
                key: "target",
                get: function get() {
                    return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
                }
            }], [{
                key: "native",
                get: function get() {
                    return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
                } // Get the prefix for handlers

            }, {
                key: "prefix",
                get: function get() {
                    // No prefix
                    if (is.function(document.exitFullscreen)) {
                        return '';
                    } // Check for fullscreen support by vendor prefix


                    var value = '';
                    var prefixes = ['webkit', 'moz', 'ms'];
                    prefixes.some(function (pre) {
                        if (is.function(document["".concat(pre, "ExitFullscreen")]) || is.function(document["".concat(pre, "CancelFullScreen")])) {
                            value = pre;
                            return true;
                        }

                        return false;
                    });
                    return value;
                }
            }, {
                key: "property",
                get: function get() {
                    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
                }
            }]);

            return Fullscreen;
        }();

    // ==========================================================================
    // Load image avoiding xhr/fetch CORS issues
    // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
    // By default it checks if it is at least 1px, but you can add a second argument to change this
    // ==========================================================================
    function loadImage(src) {
        var minWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
        return new Promise(function (resolve, reject) {
            var image = new Image();

            var handler = function handler() {
                delete image.onload;
                delete image.onerror;
                (image.naturalWidth >= minWidth ? resolve : reject)(image);
            };

            Object.assign(image, {
                onload: handler,
                onerror: handler,
                src: src
            });
        });
    }

    // ==========================================================================
    var ui = {
        addStyleHook: function addStyleHook() {
            toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
            toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
        },
        // Toggle native HTML5 media controls
        toggleNativeControls: function toggleNativeControls() {
            var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

            if (toggle && this.isHTML5) {
                this.media.setAttribute('controls', '');
            } else {
                this.media.removeAttribute('controls');
            }
        },
        // Setup the UI
        build: function build() {
            var _this = this;

            // Re-attach media element listeners
            // TODO: Use event bubbling?
            this.listeners.media(); // Don't setup interface if no support

            if (!this.supported.ui) {
                this.debug.warn("Basic support only for ".concat(this.provider, " ").concat(this.type)); // Restore native controls

                ui.toggleNativeControls.call(this, true); // Bail

                return;
            } // Inject custom controls if not present


            if (!is.element(this.elements.controls)) {
                // Inject custom controls
                controls.inject.call(this); // Re-attach control listeners

                this.listeners.controls();
            } // Remove native controls


            ui.toggleNativeControls.call(this); // Setup captions for HTML5

            if (this.isHTML5) {
                captions.setup.call(this);
            } // Reset volume


            this.volume = null; // Reset mute state

            this.muted = null; // Reset speed

            this.speed = null; // Reset loop state

            this.loop = null; // Reset quality setting

            this.quality = null; // Reset volume display

            controls.updateVolume.call(this); // Reset time display

            controls.timeUpdate.call(this); // Update the UI

            ui.checkPlaying.call(this); // Check for picture-in-picture support

            toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support

            toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class

            toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class

            toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls

            this.ready = true; // Ready event at end of execution stack

            setTimeout(function () {
                triggerEvent.call(_this, _this.media, 'ready');
            }, 0); // Set the title

            ui.setTitle.call(this); // Assure the poster image is set, if the property was added before the element was created

            if (this.poster) {
                ui.setPoster.call(this, this.poster, false).catch(function () {});
            } // Manually set the duration if user has overridden it.
            // The event listeners for it doesn't get called if preload is disabled (#701)


            if (this.config.duration) {
                controls.durationUpdate.call(this);
            }
        },
        // Setup aria attribute for play and iframe title
        setTitle: function setTitle() {
            // Find the current text
            var label = i18n.get('play', this.config); // If there's a media title set, use that for the label

            if (is.string(this.config.title) && !is.empty(this.config.title)) {
                label += ", ".concat(this.config.title);
            } // If there's a play button, set label


            Array.from(this.elements.buttons.play || []).forEach(function (button) {
                button.setAttribute('aria-label', label);
            }); // Set iframe title
            // https://github.com/sampotts/plyr/issues/124

            if (this.isEmbed) {
                var iframe = getElement.call(this, 'iframe');

                if (!is.element(iframe)) {
                    return;
                } // Default to media type


                var title = !is.empty(this.config.title) ? this.config.title : 'video';
                var format = i18n.get('frameTitle', this.config);
                iframe.setAttribute('title', format.replace('{title}', title));
            }
        },
        // Toggle poster
        togglePoster: function togglePoster(enable) {
            toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
        },
        // Set the poster image (async)
        // Used internally for the poster setter, with the passive option forced to false
        setPoster: function setPoster(poster) {
            var _this2 = this;

            var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

            // Don't override if call is passive
            if (passive && this.poster) {
                return Promise.reject(new Error('Poster already set'));
            } // Set property synchronously to respect the call order


            this.media.setAttribute('poster', poster); // Wait until ui is ready

            return ready.call(this) // Load image
                .then(function () {
                    return loadImage(poster);
                }).catch(function (err) {
                    // Hide poster on error unless it's been set by another call
                    if (poster === _this2.poster) {
                        ui.togglePoster.call(_this2, false);
                    } // Rethrow


                    throw err;
                }).then(function () {
                    // Prevent race conditions
                    if (poster !== _this2.poster) {
                        throw new Error('setPoster cancelled by later call to setPoster');
                    }
                }).then(function () {
                    Object.assign(_this2.elements.poster.style, {
                        backgroundImage: "url('".concat(poster, "')"),
                        // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
                        backgroundSize: ''
                    });
                    ui.togglePoster.call(_this2, true);
                    return poster;
                });
        },
        // Check playing state
        checkPlaying: function checkPlaying(event) {
            var _this3 = this;

            // Class hooks
            toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
            toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
            toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set state

            Array.from(this.elements.buttons.play || []).forEach(function (target) {
                target.pressed = _this3.playing;
            }); // Only update controls on non timeupdate events

            if (is.event(event) && event.type === 'timeupdate') {
                return;
            } // Toggle controls


            ui.toggleControls.call(this);
        },
        // Check if media is loading
        checkLoading: function checkLoading(event) {
            var _this4 = this;

            this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer

            clearTimeout(this.timers.loading); // Timer to prevent flicker when seeking

            this.timers.loading = setTimeout(function () {
                // Update progress bar loading class state
                toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading); // Update controls visibility

                ui.toggleControls.call(_this4);
            }, this.loading ? 250 : 0);
        },
        // Toggle controls based on state and `force` argument
        toggleControls: function toggleControls(force) {
            var controls$$1 = this.elements.controls;

            if (controls$$1 && this.config.hideControls) {
                // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
                var recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide

                this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover || recentTouchSeek));
            }
        }
    };

    var Listeners =
        /*#__PURE__*/
        function () {
            function Listeners(player) {
                _classCallCheck(this, Listeners);

                this.player = player;
                this.lastKey = null;
                this.focusTimer = null;
                this.lastKeyDown = null;
                this.handleKey = this.handleKey.bind(this);
                this.toggleMenu = this.toggleMenu.bind(this);
                this.setTabFocus = this.setTabFocus.bind(this);
                this.firstTouch = this.firstTouch.bind(this);
            } // Handle key presses


            _createClass(Listeners, [{
                key: "handleKey",
                value: function handleKey(event) {
                    var player = this.player;
                    var elements = player.elements;
                    var code = event.keyCode ? event.keyCode : event.which;
                    var pressed = event.type === 'keydown';
                    var repeat = pressed && code === this.lastKey; // Bail if a modifier key is set

                    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
                        return;
                    } // If the event is bubbled from the media element
                    // Firefox doesn't get the keycode for whatever reason


                    if (!is.number(code)) {
                        return;
                    } // Seek by the number keys


                    var seekByKey = function seekByKey() {
                        // Divide the max duration into 10th's and times by the number value
                        player.currentTime = player.duration / 10 * (code - 48);
                    }; // Handle the key on keydown
                    // Reset on keyup


                    if (pressed) {
                        // Check focused element
                        // and if the focused element is not editable (e.g. text input)
                        // and any that accept key input http://webaim.org/techniques/keyboard/
                        var focused = document.activeElement;

                        if (is.element(focused)) {
                            var editable = player.config.selectors.editable;
                            var seek = elements.inputs.seek;

                            if (focused !== seek && matches(focused, editable)) {
                                return;
                            }

                            if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) {
                                return;
                            }
                        } // Which keycodes should we prevent default


                        var preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79]; // If the code is found prevent default (e.g. prevent scrolling for arrows)

                        if (preventDefault.includes(code)) {
                            event.preventDefault();
                            event.stopPropagation();
                        }

                        switch (code) {
                            case 48:
                            case 49:
                            case 50:
                            case 51:
                            case 52:
                            case 53:
                            case 54:
                            case 55:
                            case 56:
                            case 57:
                                // 0-9
                                if (!repeat) {
                                    seekByKey();
                                }

                                break;

                            case 32:
                            case 75:
                                // Space and K key
                                if (!repeat) {
                                    player.togglePlay();
                                }

                                break;

                            case 38:
                                // Arrow up
                                player.increaseVolume(0.1);
                                break;

                            case 40:
                                // Arrow down
                                player.decreaseVolume(0.1);
                                break;

                            case 77:
                                // M key
                                if (!repeat) {
                                    player.muted = !player.muted;
                                }

                                break;

                            case 39:
                                // Arrow forward
                                player.forward();
                                break;

                            case 37:
                                // Arrow back
                                player.rewind();
                                break;

                            case 70:
                                // F key
                                player.fullscreen.toggle();
                                break;

                            case 67:
                                // C key
                                if (!repeat) {
                                    player.toggleCaptions();
                                }

                                break;

                            case 76:
                                // L key
                                player.loop = !player.loop;
                                break;

                            /* case 73:
            this.setLoop('start');
            break;
            case 76:
            this.setLoop();
            break;
            case 79:
            this.setLoop('end');
            break; */

                            default:
                                break;
                        } // Escape is handle natively when in full screen
                        // So we only need to worry about non native


                        if (!player.fullscreen.enabled && player.fullscreen.active && code === 27) {
                            player.fullscreen.toggle();
                        } // Store last code for next cycle


                        this.lastKey = code;
                    } else {
                        this.lastKey = null;
                    }
                } // Toggle menu

            }, {
                key: "toggleMenu",
                value: function toggleMenu(event) {
                    controls.toggleMenu.call(this.player, event);
                } // Device is touch enabled

            }, {
                key: "firstTouch",
                value: function firstTouch() {
                    var player = this.player;
                    var elements = player.elements;
                    player.touch = true; // Add touch class

                    toggleClass(elements.container, player.config.classNames.isTouch, true);
                }
            }, {
                key: "setTabFocus",
                value: function setTabFocus(event) {
                    var player = this.player;
                    var elements = player.elements;
                    clearTimeout(this.focusTimer); // Ignore any key other than tab

                    if (event.type === 'keydown' && event.which !== 9) {
                        return;
                    } // Store reference to event timeStamp


                    if (event.type === 'keydown') {
                        this.lastKeyDown = event.timeStamp;
                    } // Remove current classes


                    var removeCurrent = function removeCurrent() {
                        var className = player.config.classNames.tabFocus;
                        var current = getElements.call(player, ".".concat(className));
                        toggleClass(current, className, false);
                    }; // Determine if a key was pressed to trigger this event


                    var wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; // Ignore focus events if a key was pressed prior

                    if (event.type === 'focus' && !wasKeyDown) {
                        return;
                    } // Remove all current


                    removeCurrent(); // Delay the adding of classname until the focus has changed
                    // This event fires before the focusin event

                    this.focusTimer = setTimeout(function () {
                        var focused = document.activeElement; // Ignore if current focus element isn't inside the player

                        if (!elements.container.contains(focused)) {
                            return;
                        }

                        toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
                    }, 10);
                } // Global window & document listeners

            }, {
                key: "global",
                value: function global() {
                    var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
                    var player = this.player; // Keyboard shortcuts

                    if (player.config.keyboard.global) {
                        toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);
                    } // Click anywhere closes menu


                    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events

                    once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection

                    toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true);
                } // Container listeners

            }, {
                key: "container",
                value: function container() {
                    var player = this.player;
                    var elements = player.elements; // Keyboard shortcuts

                    if (!player.config.keyboard.global && player.config.keyboard.focused) {
                        on.call(player, elements.container, 'keydown keyup', this.handleKey, false);
                    } // Toggle controls on mouse events and entering fullscreen


                    on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) {
                        var controls$$1 = elements.controls; // Remove button states for fullscreen

                        if (controls$$1 && event.type === 'enterfullscreen') {
                            controls$$1.pressed = false;
                            controls$$1.hover = false;
                        } // Show, then hide after a timeout unless another control event occurs


                        var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
                        var delay = 0;

                        if (show) {
                            ui.toggleControls.call(player, true); // Use longer timeout for touch devices

                            delay = player.touch ? 3000 : 2000;
                        } // Clear timer


                        clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking

                        player.timers.controls = setTimeout(function () {
                            return ui.toggleControls.call(player, false);
                        }, delay);
                    });
                } // Listen for media events

            }, {
                key: "media",
                value: function media() {
                    var player = this.player;
                    var elements = player.elements; // Time change on media

                    on.call(player, player.media, 'timeupdate seeking seeked', function (event) {
                        return controls.timeUpdate.call(player, event);
                    }); // Display duration

                    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', function (event) {
                        return controls.durationUpdate.call(player, event);
                    }); // Check for audio tracks on load
                    // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point

                    on.call(player, player.media, 'canplay loadeddata', function () {
                        toggleHidden(elements.volume, !player.hasAudio);
                        toggleHidden(elements.buttons.mute, !player.hasAudio);
                    }); // Handle the media finishing

                    on.call(player, player.media, 'ended', function () {
                        // Show poster on end
                        if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
                            // Restart
                            player.restart();
                        }
                    }); // Check for buffer progress

                    on.call(player, player.media, 'progress playing seeking seeked', function (event) {
                        return controls.updateProgress.call(player, event);
                    }); // Handle volume changes

                    on.call(player, player.media, 'volumechange', function (event) {
                        return controls.updateVolume.call(player, event);
                    }); // Handle play/pause

                    on.call(player, player.media, 'playing play pause ended emptied timeupdate', function (event) {
                        return ui.checkPlaying.call(player, event);
                    }); // Loading state

                    on.call(player, player.media, 'waiting canplay seeked playing', function (event) {
                        return ui.checkLoading.call(player, event);
                    }); // If autoplay, then load advertisement if required
                    // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows

                    on.call(player, player.media, 'playing', function () {
                        if (!player.ads) {
                            return;
                        } // If ads are enabled, wait for them first


                        if (player.ads.enabled && !player.ads.initialized) {
                            // Wait for manager response
                            player.ads.managerPromise.then(function () {
                                return player.ads.play();
                            }).catch(function () {
                                return player.play();
                            });
                        }
                    }); // Click video

                    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {
                        // Re-fetch the wrapper
                        var wrapper = getElement.call(player, ".".concat(player.config.classNames.video)); // Bail if there's no wrapper (this should never happen)

                        if (!is.element(wrapper)) {
                            return;
                        } // On click play, pause or restart


                        on.call(player, elements.container, 'click', function (event) {
                            var targets = [elements.container, wrapper]; // Ignore if click if not container or in video wrapper

                            if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
                                return;
                            } // Touch devices will just show controls (if hidden)


                            if (player.touch && player.config.hideControls) {
                                return;
                            }

                            if (player.ended) {
                                player.restart();
                                player.play();
                            } else {
                                player.togglePlay();
                            }
                        });
                    } // Disable right click


                    if (player.supported.ui && player.config.disableContextMenu) {
                        on.call(player, elements.wrapper, 'contextmenu', function (event) {
                            event.preventDefault();
                        }, false);
                    } // Volume change


                    on.call(player, player.media, 'volumechange', function () {
                        // Save to storage
                        player.storage.set({
                            volume: player.volume,
                            muted: player.muted
                        });
                    }); // Speed change

                    on.call(player, player.media, 'ratechange', function () {
                        // Update UI
                        controls.updateSetting.call(player, 'speed'); // Save to storage


                        player.storage.set({
                            speed: player.speed
                        });
                    }); // Quality change

                    on.call(player, player.media, 'qualitychange', function (event) {
                        // Update UI
                        controls.updateSetting.call(player, 'quality', null, event.detail.quality);
                    }); // Update download link when ready and if quality changes

                    on.call(player, player.media, 'ready qualitychange', function () {
                        controls.setDownloadLink.call(player);
                    }); // Proxy events to container
                    // Bubble up key events for Edge

                    var proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
                    on.call(player, player.media, proxyEvents, function (event) {
                        var _event$detail = event.detail,
                            detail = _event$detail === void 0 ? {} : _event$detail; // Get error details from media

                        if (event.type === 'error') {
                            detail = player.media.error;
                        }

                        triggerEvent.call(player, elements.container, event.type, true, detail);
                    });
                } // Run default and custom handlers

            }, {
                key: "proxy",
                value: function proxy(event, defaultHandler, customHandlerKey) {
                    var player = this.player;
                    var customHandler = player.config.listeners[customHandlerKey];
                    var hasCustomHandler = is.function(customHandler);
                    var returned = true; // Execute custom handler

                    if (hasCustomHandler) {
                        returned = customHandler.call(player, event);
                    } // Only call default handler if not prevented in custom handler


                    if (returned && is.function(defaultHandler)) {
                        defaultHandler.call(player, event);
                    }
                } // Trigger custom and default handlers

            }, {
                key: "bind",
                value: function bind(element, type, defaultHandler, customHandlerKey) {
                    var _this = this;

                    var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
                    var player = this.player;
                    var customHandler = player.config.listeners[customHandlerKey];
                    var hasCustomHandler = is.function(customHandler);
                    on.call(player, element, type, function (event) {
                        return _this.proxy(event, defaultHandler, customHandlerKey);
                    }, passive && !hasCustomHandler);
                } // Listen for control events

            }, {
                key: "controls",
                value: function controls$$1() {
                    var _this2 = this;

                    var player = this.player;
                    var elements = player.elements; // IE doesn't support input event, so we fallback to change

                    var inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle

                    if (elements.buttons.play) {
                        Array.from(elements.buttons.play).forEach(function (button) {
                            _this2.bind(button, 'click', player.togglePlay, 'play');
                        });
                    } // Pause


                    this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind

                    this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind

                    this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle

                    this.bind(elements.buttons.mute, 'click', function () {
                        player.muted = !player.muted;
                    }, 'mute'); // Captions toggle

                    this.bind(elements.buttons.captions, 'click', function () {
                        return player.toggleCaptions();
                    }); // Download

                    this.bind(elements.buttons.download, 'click', function () {
                        triggerEvent.call(player, player.media, 'download');
                    }, 'download'); // Fullscreen toggle

                    this.bind(elements.buttons.fullscreen, 'click', function () {
                        player.fullscreen.toggle();
                    }, 'fullscreen'); // Picture-in-Picture

                    this.bind(elements.buttons.pip, 'click', function () {
                        player.pip = 'toggle';
                    }, 'pip'); // Airplay

                    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle

                    this.bind(elements.buttons.settings, 'click', function (event) {
                        // Prevent the document click listener closing the menu
                        event.stopPropagation();

                        controls.toggleMenu.call(player, event);
                    }); // Settings menu - keyboard toggle
                    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
                    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143

                    this.bind(elements.buttons.settings, 'keyup', function (event) {
                            var code = event.which; // We only care about space and return

                            if (![13, 32].includes(code)) {
                                return;
                            } // Because return triggers a click anyway, all we need to do is set focus


                            if (code === 13) {
                                controls.focusFirstMenuItem.call(player, null, true);

                                return;
                            } // Prevent scroll


                            event.preventDefault(); // Prevent playing video (Firefox)

                            event.stopPropagation(); // Toggle menu

                            controls.toggleMenu.call(player, event);
                        }, null, false // Can't be passive as we're preventing default
                    ); // Escape closes menu

                    this.bind(elements.settings.menu, 'keydown', function (event) {
                        if (event.which === 27) {
                            controls.toggleMenu.call(player, event);
                        }
                    }); // Set range input alternative "value", which matches the tooltip time (#954)

                    this.bind(elements.inputs.seek, 'mousedown mousemove', function (event) {
                        var rect = elements.progress.getBoundingClientRect();
                        var percent = 100 / rect.width * (event.pageX - rect.left);
                        event.currentTarget.setAttribute('seek-value', percent);
                    }); // Pause while seeking

                    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) {
                        var seek = event.currentTarget;
                        var code = event.keyCode ? event.keyCode : event.which;
                        var attribute = 'play-on-seeked';

                        if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
                            return;
                        } // Record seek time so we can prevent hiding controls for a few seconds after seek


                        player.lastSeekTime = Date.now(); // Was playing before?

                        var play = seek.hasAttribute(attribute); // Done seeking

                        var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback

                        if (play && done) {
                            seek.removeAttribute(attribute);
                            player.play();
                        } else if (!done && player.playing) {
                            seek.setAttribute(attribute, '');
                            player.pause();
                        }
                    }); // Fix range inputs on iOS
                    // Super weird iOS bug where after you interact with an <input type="range">,
                    // it takes over further interactions on the page. This is a hack

                    if (browser.isIos) {
                        var inputs = getElements.call(player, 'input[type="range"]');
                        Array.from(inputs).forEach(function (input) {
                            return _this2.bind(input, inputEvent, function (event) {
                                return repaint(event.target);
                            });
                        });
                    } // Seek


                    this.bind(elements.inputs.seek, inputEvent, function (event) {
                        var seek = event.currentTarget; // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)

                        var seekTo = seek.getAttribute('seek-value');

                        if (is.empty(seekTo)) {
                            seekTo = seek.value;
                        }

                        seek.removeAttribute('seek-value');
                        player.currentTime = seekTo / seek.max * player.duration;
                    }, 'seek'); // Seek tooltip

                    this.bind(elements.progress, 'mouseenter mouseleave mousemove', function (event) {
                        return controls.updateSeekTooltip.call(player, event);
                    }); // Polyfill for lower fill in <input type="range"> for webkit

                    if (browser.isWebkit) {
                        Array.from(getElements.call(player, 'input[type="range"]')).forEach(function (element) {
                            _this2.bind(element, 'input', function (event) {
                                return controls.updateRangeFill.call(player, event.target);
                            });
                        });
                    } // Current time invert
                    // Only if one time element is used for both currentTime and duration


                    if (player.config.toggleInvert && !is.element(elements.display.duration)) {
                        this.bind(elements.display.currentTime, 'click', function () {
                            // Do nothing if we're at the start
                            if (player.currentTime === 0) {
                                return;
                            }

                            player.config.invertTime = !player.config.invertTime;

                            controls.timeUpdate.call(player);
                        });
                    } // Volume


                    this.bind(elements.inputs.volume, inputEvent, function (event) {
                        player.volume = event.target.value;
                    }, 'volume'); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)

                    this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
                        elements.controls.hover = !player.touch && event.type === 'mouseenter';
                    }); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)

                    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
                        elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
                    }); // Show controls when they receive focus (e.g., when using keyboard tab key)

                    this.bind(elements.controls, 'focusin', function () {
                        var config = player.config,
                            elements = player.elements,
                            timers = player.timers; // Skip transition to prevent focus from scrolling the parent element

                        toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle

                        ui.toggleControls.call(player, true); // Restore transition

                        setTimeout(function () {
                            toggleClass(elements.controls, config.classNames.noTransition, false);
                        }, 0); // Delay a little more for mouse users

                        var delay = _this2.touch ? 3000 : 4000; // Clear timer

                        clearTimeout(timers.controls); // Hide again after delay

                        timers.controls = setTimeout(function () {
                            return ui.toggleControls.call(player, false);
                        }, delay);
                    }); // Mouse wheel for volume

                    this.bind(elements.inputs.volume, 'wheel', function (event) {
                        // Detect "natural" scroll - suppored on OS X Safari only
                        // Other browsers on OS X will be inverted until support improves
                        var inverted = event.webkitDirectionInvertedFromDevice; // Get delta from event. Invert if `inverted` is true

                        var _map = [event.deltaX, -event.deltaY].map(function (value) {
                                return inverted ? -value : value;
                            }),
                            _map2 = _slicedToArray(_map, 2),
                            x = _map2[0],
                            y = _map2[1]; // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)


                        var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2%

                        player.increaseVolume(direction / 50); // Don't break page scrolling at max and min

                        var volume = player.media.volume;

                        if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {
                            event.preventDefault();
                        }
                    }, 'volume', false);
                }
            }]);

            return Listeners;
        }();

    var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

    function createCommonjsModule(fn, module) {
        return module = { exports: {} }, fn(module, module.exports), module.exports;
    }

    var loadjs_umd = createCommonjsModule(function (module, exports) {
        (function(root, factory) {
            {
                module.exports = factory();
            }
        }(commonjsGlobal, function() {
            /**
             * Global dependencies.
             * @global {Object} document - DOM
             */

            var devnull = function() {},
                bundleIdCache = {},
                bundleResultCache = {},
                bundleCallbackQueue = {};


            /**
             * Subscribe to bundle load event.
             * @param {string[]} bundleIds - Bundle ids
             * @param {Function} callbackFn - The callback function
             */
            function subscribe(bundleIds, callbackFn) {
                // listify
                bundleIds = bundleIds.push ? bundleIds : [bundleIds];

                var depsNotFound = [],
                    i = bundleIds.length,
                    numWaiting = i,
                    fn,
                    bundleId,
                    r,
                    q;

                // define callback function
                fn = function (bundleId, pathsNotFound) {
                    if (pathsNotFound.length) depsNotFound.push(bundleId);

                    numWaiting--;
                    if (!numWaiting) callbackFn(depsNotFound);
                };

                // register callback
                while (i--) {
                    bundleId = bundleIds[i];

                    // execute callback if in result cache
                    r = bundleResultCache[bundleId];
                    if (r) {
                        fn(bundleId, r);
                        continue;
                    }

                    // add to callback queue
                    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
                    q.push(fn);
                }
            }


            /**
             * Publish bundle load event.
             * @param {string} bundleId - Bundle id
             * @param {string[]} pathsNotFound - List of files not found
             */
            function publish(bundleId, pathsNotFound) {
                // exit if id isn't defined
                if (!bundleId) return;

                var q = bundleCallbackQueue[bundleId];

                // cache result
                bundleResultCache[bundleId] = pathsNotFound;

                // exit if queue is empty
                if (!q) return;

                // empty callback queue
                while (q.length) {
                    q[0](bundleId, pathsNotFound);
                    q.splice(0, 1);
                }
            }


            /**
             * Execute callbacks.
             * @param {Object or Function} args - The callback args
             * @param {string[]} depsNotFound - List of dependencies not found
             */
            function executeCallbacks(args, depsNotFound) {
                // accept function as argument
                if (args.call) args = {success: args};

                // success and error callbacks
                if (depsNotFound.length) (args.error || devnull)(depsNotFound);
                else (args.success || devnull)(args);
            }


            /**
             * Load individual file.
             * @param {string} path - The file path
             * @param {Function} callbackFn - The callback function
             */
            function loadFile(path, callbackFn, args, numTries) {
                var doc = document,
                    async = args.async,
                    maxTries = (args.numRetries || 0) + 1,
                    beforeCallbackFn = args.before || devnull,
                    pathStripped = path.replace(/^(css|img)!/, ''),
                    isCss,
                    e;

                numTries = numTries || 0;

                if (/(^css!|\.css$)/.test(path)) {
                    isCss = true;

                    // css
                    e = doc.createElement('link');
                    e.rel = 'stylesheet';
                    e.href = pathStripped; //.replace(/^css!/, '');  // remove "css!" prefix
                } else if (/(^img!|\.(png|gif|jpg|svg)$)/.test(path)) {
                    // image
                    e = doc.createElement('img');
                    e.src = pathStripped;
                } else {
                    // javascript
                    e = doc.createElement('script');
                    e.src = path;
                    e.async = async === undefined ? true : async;
                }

                e.onload = e.onerror = e.onbeforeload = function (ev) {
                    var result = ev.type[0];

                    // Note: The following code isolates IE using `hideFocus` and treats empty
                    // stylesheets as failures to get around lack of onerror support
                    if (isCss && 'hideFocus' in e) {
                        try {
                            if (!e.sheet.cssText.length) result = 'e';
                        } catch (x) {
                            // sheets objects created from load errors don't allow access to
                            // `cssText`
                            result = 'e';
                        }
                    }

                    // handle retries in case of load failure
                    if (result == 'e') {
                        // increment counter
                        numTries += 1;

                        // exit function and try again
                        if (numTries < maxTries) {
                            return loadFile(path, callbackFn, args, numTries);
                        }
                    }

                    // execute callback
                    callbackFn(path, result, ev.defaultPrevented);
                };

                // add to document (unless callback returns `false`)
                if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
            }


            /**
             * Load multiple files.
             * @param {string[]} paths - The file paths
             * @param {Function} callbackFn - The callback function
             */
            function loadFiles(paths, callbackFn, args) {
                // listify paths
                paths = paths.push ? paths : [paths];

                var numWaiting = paths.length,
                    x = numWaiting,
                    pathsNotFound = [],
                    fn,
                    i;

                // define callback function
                fn = function(path, result, defaultPrevented) {
                    // handle error
                    if (result == 'e') pathsNotFound.push(path);

                    // handle beforeload event. If defaultPrevented then that means the load
                    // will be blocked (ex. Ghostery/ABP on Safari)
                    if (result == 'b') {
                        if (defaultPrevented) pathsNotFound.push(path);
                        else return;
                    }

                    numWaiting--;
                    if (!numWaiting) callbackFn(pathsNotFound);
                };

                // load scripts
                for (i=0; i < x; i++) loadFile(paths[i], fn, args);
            }


            /**
             * Initiate script load and register bundle.
             * @param {(string|string[])} paths - The file paths
             * @param {(string|Function)} [arg1] - The bundleId or success callback
             * @param {Function} [arg2] - The success or error callback
             * @param {Function} [arg3] - The error callback
             */
            function loadjs(paths, arg1, arg2) {
                var bundleId,
                    args;

                // bundleId (if string)
                if (arg1 && arg1.trim) bundleId = arg1;

                // args (default is {})
                args = (bundleId ? arg2 : arg1) || {};

                // throw error if bundle is already defined
                if (bundleId) {
                    if (bundleId in bundleIdCache) {
                        throw "LoadJS";
                    } else {
                        bundleIdCache[bundleId] = true;
                    }
                }

                // load scripts
                loadFiles(paths, function (pathsNotFound) {
                    // execute callbacks
                    executeCallbacks(args, pathsNotFound);

                    // publish bundle load event
                    publish(bundleId, pathsNotFound);
                }, args);
            }


            /**
             * Execute callbacks when dependencies have been satisfied.
             * @param {(string|string[])} deps - List of bundle ids
             * @param {Object} args - success/error arguments
             */
            loadjs.ready = function ready(deps, args) {
                // subscribe to bundle load event
                subscribe(deps, function (depsNotFound) {
                    // execute callbacks
                    executeCallbacks(args, depsNotFound);
                });

                return loadjs;
            };


            /**
             * Manually satisfy bundle dependencies.
             * @param {string} bundleId - The bundle id
             */
            loadjs.done = function done(bundleId) {
                publish(bundleId, []);
            };


            /**
             * Reset loadjs dependencies statuses
             */
            loadjs.reset = function reset() {
                bundleIdCache = {};
                bundleResultCache = {};
                bundleCallbackQueue = {};
            };


            /**
             * Determine if bundle has already been defined
             * @param String} bundleId - The bundle id
             */
            loadjs.isDefined = function isDefined(bundleId) {
                return bundleId in bundleIdCache;
            };


            // export
            return loadjs;

        }));
    });

    // ==========================================================================
    function loadScript(url) {
        return new Promise(function (resolve, reject) {
            loadjs_umd(url, {
                success: resolve,
                error: reject
            });
        });
    }

    function parseId(url) {
        if (is.empty(url)) {
            return null;
        }

        if (is.number(Number(url))) {
            return url;
        }

        var regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
        return url.match(regex) ? RegExp.$2 : url;
    } // Get aspect ratio for dimensions


    function getAspectRatio(width, height) {
        var getRatio = function getRatio(w, h) {
            return h === 0 ? w : getRatio(h, w % h);
        };

        var ratio = getRatio(width, height);
        return "".concat(width / ratio, ":").concat(height / ratio);
    } // Set playback state and trigger change (only on actual change)


    function assurePlaybackState(play) {
        if (play && !this.embed.hasPlayed) {
            this.embed.hasPlayed = true;
        }

        if (this.media.paused === play) {
            this.media.paused = !play;
            triggerEvent.call(this, this.media, play ? 'play' : 'pause');
        }
    }

    var vimeo = {
        setup: function setup() {
            var _this = this;

            // Add embed class for responsive
            toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set intial ratio

            vimeo.setAspectRatio.call(this); // Load the API if not already

            if (!is.object(window.Vimeo)) {
                loadScript(this.config.urls.vimeo.sdk).then(function () {
                    vimeo.ready.call(_this);
                }).catch(function (error) {
                    _this.debug.warn('Vimeo API failed to load', error);
                });
            } else {
                vimeo.ready.call(this);
            }
        },
        // Set aspect ratio
        // For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
        setAspectRatio: function setAspectRatio(input) {
            var _split$map = (is.string(input) ? input : this.config.ratio).split(':').map(Number),
                _split$map2 = _slicedToArray(_split$map, 2),
                x = _split$map2[0],
                y = _split$map2[1];

            var padding = 100 / x * y;
            vimeo.padding = padding;
            this.elements.wrapper.style.paddingBottom = "".concat(padding, "%");

            if (this.supported.ui) {
                var height = 240;
                var offset = (height - padding) / (height / 50);
                this.media.style.transform = "translateY(-".concat(offset, "%)");
            }
        },
        // API Ready
        ready: function ready$$1() {
            var _this2 = this;

            var player = this; // Get Vimeo params for the iframe

            var options = {
                loop: player.config.loop.active,
                autoplay: player.autoplay,
                // muted: player.muted,
                byline: false,
                portrait: false,
                title: false,
                speed: true,
                transparent: 0,
                gesture: 'media',
                playsinline: !this.config.fullscreen.iosNative
            };
            var params = buildUrlParams(options); // Get the source URL or ID

            var source = player.media.getAttribute('src'); // Get from <div> if needed

            if (is.empty(source)) {
                source = player.media.getAttribute(player.config.attributes.embed.id);
            }

            var id = parseId(source); // Build an iframe

            var iframe = createElement('iframe');
            var src = format(player.config.urls.vimeo.iframe, id, params);
            iframe.setAttribute('src', src);
            iframe.setAttribute('allowfullscreen', '');
            iframe.setAttribute('allowtransparency', '');
            iframe.setAttribute('allow', 'autoplay'); // Get poster, if already set

            var poster = player.poster; // Inject the package

            var wrapper = createElement('div', {
                poster: poster,
                class: player.config.classNames.embedContainer
            });
            wrapper.appendChild(iframe);
            player.media = replaceElement(wrapper, player.media); // Get poster image

            fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
                if (is.empty(response)) {
                    return;
                } // Get the URL for thumbnail


                var url = new URL(response[0].thumbnail_large); // Get original image

                url.pathname = "".concat(url.pathname.split('_')[0], ".jpg"); // Set and show poster

                ui.setPoster.call(player, url.href).catch(function () {});
            }); // Setup instance
            // https://github.com/vimeo/player.js

            player.embed = new window.Vimeo.Player(iframe, {
                autopause: player.config.autopause,
                muted: player.muted
            });
            player.media.paused = true;
            player.media.currentTime = 0; // Disable native text track rendering

            if (player.supported.ui) {
                player.embed.disableTextTrack();
            } // Create a faux HTML5 API using the Vimeo API


            player.media.play = function () {
                assurePlaybackState.call(player, true);
                return player.embed.play();
            };

            player.media.pause = function () {
                assurePlaybackState.call(player, false);
                return player.embed.pause();
            };

            player.media.stop = function () {
                player.pause();
                player.currentTime = 0;
            }; // Seeking


            var currentTime = player.media.currentTime;
            Object.defineProperty(player.media, 'currentTime', {
                get: function get() {
                    return currentTime;
                },
                set: function set(time) {
                    // Vimeo will automatically play on seek if the video hasn't been played before
                    // Get current paused state and volume etc
                    var embed = player.embed,
                        media = player.media,
                        paused = player.paused,
                        volume = player.volume;
                    var restorePause = paused && !embed.hasPlayed; // Set seeking state and trigger event

                    media.seeking = true;
                    triggerEvent.call(player, media, 'seeking'); // If paused, mute until seek is complete

                    Promise.resolve(restorePause && embed.setVolume(0)) // Seek
                        .then(function () {
                            return embed.setCurrentTime(time);
                        }) // Restore paused
                        .then(function () {
                            return restorePause && embed.pause();
                        }) // Restore volume
                        .then(function () {
                            return restorePause && embed.setVolume(volume);
                        }).catch(function () {// Do nothing
                    });
                }
            }); // Playback speed

            var speed = player.config.speed.selected;
            Object.defineProperty(player.media, 'playbackRate', {
                get: function get() {
                    return speed;
                },
                set: function set(input) {
                    player.embed.setPlaybackRate(input).then(function () {
                        speed = input;
                        triggerEvent.call(player, player.media, 'ratechange');
                    }).catch(function (error) {
                        // Hide menu item (and menu if empty)
                        if (error.name === 'Error') {
                            controls.setSpeedMenu.call(player, []);
                        }
                    });
                }
            }); // Volume

            var volume = player.config.volume;
            Object.defineProperty(player.media, 'volume', {
                get: function get() {
                    return volume;
                },
                set: function set(input) {
                    player.embed.setVolume(input).then(function () {
                        volume = input;
                        triggerEvent.call(player, player.media, 'volumechange');
                    });
                }
            }); // Muted

            var muted = player.config.muted;
            Object.defineProperty(player.media, 'muted', {
                get: function get() {
                    return muted;
                },
                set: function set(input) {
                    var toggle = is.boolean(input) ? input : false;
                    player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () {
                        muted = toggle;
                        triggerEvent.call(player, player.media, 'volumechange');
                    });
                }
            }); // Loop

            var loop = player.config.loop;
            Object.defineProperty(player.media, 'loop', {
                get: function get() {
                    return loop;
                },
                set: function set(input) {
                    var toggle = is.boolean(input) ? input : player.config.loop.active;
                    player.embed.setLoop(toggle).then(function () {
                        loop = toggle;
                    });
                }
            }); // Source

            var currentSrc;
            player.embed.getVideoUrl().then(function (value) {
                currentSrc = value;
                controls.setDownloadLink.call(player);
            }).catch(function (error) {
                _this2.debug.warn(error);
            });
            Object.defineProperty(player.media, 'currentSrc', {
                get: function get() {
                    return currentSrc;
                }
            }); // Ended

            Object.defineProperty(player.media, 'ended', {
                get: function get() {
                    return player.currentTime === player.duration;
                }
            }); // Set aspect ratio based on video size

            Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) {
                vimeo.ratio = getAspectRatio(dimensions[0], dimensions[1]);
                vimeo.setAspectRatio.call(_this2, vimeo.ratio);
            }); // Set autopause

            player.embed.setAutopause(player.config.autopause).then(function (state) {
                player.config.autopause = state;
            }); // Get title

            player.embed.getVideoTitle().then(function (title) {
                player.config.title = title;
                ui.setTitle.call(_this2);
            }); // Get current time

            player.embed.getCurrentTime().then(function (value) {
                currentTime = value;
                triggerEvent.call(player, player.media, 'timeupdate');
            }); // Get duration

            player.embed.getDuration().then(function (value) {
                player.media.duration = value;
                triggerEvent.call(player, player.media, 'durationchange');
            }); // Get captions

            player.embed.getTextTracks().then(function (tracks) {
                player.media.textTracks = tracks;
                captions.setup.call(player);
            });
            player.embed.on('cuechange', function (_ref) {
                var _ref$cues = _ref.cues,
                    cues = _ref$cues === void 0 ? [] : _ref$cues;
                var strippedCues = cues.map(function (cue) {
                    return stripHTML(cue.text);
                });
                captions.updateCues.call(player, strippedCues);
            });
            player.embed.on('loaded', function () {
                // Assure state and events are updated on autoplay
                player.embed.getPaused().then(function (paused) {
                    assurePlaybackState.call(player, !paused);

                    if (!paused) {
                        triggerEvent.call(player, player.media, 'playing');
                    }
                });

                if (is.element(player.embed.element) && player.supported.ui) {
                    var frame = player.embed.element; // Fix keyboard focus issues
                    // https://github.com/sampotts/plyr/issues/317

                    frame.setAttribute('tabindex', -1);
                }
            });
            player.embed.on('play', function () {
                assurePlaybackState.call(player, true);
                triggerEvent.call(player, player.media, 'playing');
            });
            player.embed.on('pause', function () {
                assurePlaybackState.call(player, false);
            });
            player.embed.on('timeupdate', function (data) {
                player.media.seeking = false;
                currentTime = data.seconds;
                triggerEvent.call(player, player.media, 'timeupdate');
            });
            player.embed.on('progress', function (data) {
                player.media.buffered = data.percent;
                triggerEvent.call(player, player.media, 'progress'); // Check all loaded

                if (parseInt(data.percent, 10) === 1) {
                    triggerEvent.call(player, player.media, 'canplaythrough');
                } // Get duration as if we do it before load, it gives an incorrect value
                // https://github.com/sampotts/plyr/issues/891


                player.embed.getDuration().then(function (value) {
                    if (value !== player.media.duration) {
                        player.media.duration = value;
                        triggerEvent.call(player, player.media, 'durationchange');
                    }
                });
            });
            player.embed.on('seeked', function () {
                player.media.seeking = false;
                triggerEvent.call(player, player.media, 'seeked');
            });
            player.embed.on('ended', function () {
                player.media.paused = true;
                triggerEvent.call(player, player.media, 'ended');
            });
            player.embed.on('error', function (detail) {
                player.media.error = detail;
                triggerEvent.call(player, player.media, 'error');
            }); // Set height/width on fullscreen

            player.on('enterfullscreen exitfullscreen', function (event) {
                var target = player.fullscreen.target; // Ignore for iOS native

                if (target !== player.elements.container) {
                    return;
                }

                var toggle = event.type === 'enterfullscreen';

                var _vimeo$ratio$split$ma = vimeo.ratio.split(':').map(Number),
                    _vimeo$ratio$split$ma2 = _slicedToArray(_vimeo$ratio$split$ma, 2),
                    x = _vimeo$ratio$split$ma2[0],
                    y = _vimeo$ratio$split$ma2[1];

                var dimension = x > y ? 'width' : 'height';
                target.style[dimension] = toggle ? "".concat(vimeo.padding, "%") : null;
            }); // Rebuild UI

            setTimeout(function () {
                return ui.build.call(player);
            }, 0);
        }
    };

    // ==========================================================================

    function parseId$1(url) {
        if (is.empty(url)) {
            return null;
        }

        var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
        return url.match(regex) ? RegExp.$2 : url;
    } // Set playback state and trigger change (only on actual change)


    function assurePlaybackState$1(play) {
        if (play && !this.embed.hasPlayed) {
            this.embed.hasPlayed = true;
        }

        if (this.media.paused === play) {
            this.media.paused = !play;
            triggerEvent.call(this, this.media, play ? 'play' : 'pause');
        }
    }

    var youtube = {
        setup: function setup() {
            var _this = this;

            // Add embed class for responsive
            toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio

            youtube.setAspectRatio.call(this); // Setup API

            if (is.object(window.YT) && is.function(window.YT.Player)) {
                youtube.ready.call(this);
            } else {
                // Load the API
                loadScript(this.config.urls.youtube.sdk).catch(function (error) {
                    _this.debug.warn('YouTube API failed to load', error);
                }); // Setup callback for the API
                // YouTube has it's own system of course...

                window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; // Add to queue

                window.onYouTubeReadyCallbacks.push(function () {
                    youtube.ready.call(_this);
                }); // Set callback to process queue

                window.onYouTubeIframeAPIReady = function () {
                    window.onYouTubeReadyCallbacks.forEach(function (callback) {
                        callback();
                    });
                };
            }
        },
        // Get the media title
        getTitle: function getTitle(videoId) {
            var _this2 = this;

            // Try via undocumented API method first
            // This method disappears now and then though...
            // https://github.com/sampotts/plyr/issues/709
            if (is.function(this.embed.getVideoData)) {
                var _this$embed$getVideoD = this.embed.getVideoData(),
                    title = _this$embed$getVideoD.title;

                if (is.empty(title)) {
                    this.config.title = title;
                    ui.setTitle.call(this);
                    return;
                }
            } // Or via Google API


            var key = this.config.keys.google;

            if (is.string(key) && !is.empty(key)) {
                var url = format(this.config.urls.youtube.api, videoId, key);
                fetch(url).then(function (result) {
                    if (is.object(result)) {
                        _this2.config.title = result.items[0].snippet.title;
                        ui.setTitle.call(_this2);
                    }
                }).catch(function () {});
            }
        },
        // Set aspect ratio
        setAspectRatio: function setAspectRatio() {
            var ratio = this.config.ratio.split(':');
            this.elements.wrapper.style.paddingBottom = "".concat(100 / ratio[0] * ratio[1], "%");
        },
        // API ready
        ready: function ready$$1() {
            var player = this; // Ignore already setup (race condition)

            var currentId = player.media.getAttribute('id');

            if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
                return;
            } // Get the source URL or ID


            var source = player.media.getAttribute('src'); // Get from <div> if needed

            if (is.empty(source)) {
                source = player.media.getAttribute(this.config.attributes.embed.id);
            } // Replace the <iframe> with a <div> due to YouTube API issues


            var videoId = parseId$1(source);
            var id = generateId(player.provider); // Get poster, if already set

            var poster = player.poster; // Replace media element

            var container = createElement('div', {
                id: id,
                poster: poster
            });
            player.media = replaceElement(container, player.media); // Id to poster wrapper

            var posterSrc = function posterSrc(format$$1) {
                return "https://img.youtube.com/vi/".concat(videoId, "/").concat(format$$1, "default.jpg");
            }; // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)


            loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
                .catch(function () {
                    return loadImage(posterSrc('sd'), 121);
                }) // 480p padded 4:3
                .catch(function () {
                    return loadImage(posterSrc('hq'));
                }) // 360p padded 4:3. Always exists
                .then(function (image) {
                    return ui.setPoster.call(player, image.src);
                }).then(function (posterSrc) {
                // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
                if (!posterSrc.includes('maxres')) {
                    player.elements.poster.style.backgroundSize = 'cover';
                }
            }).catch(function () {}); // Setup instance
            // https://developers.google.com/youtube/iframe_api_reference

            player.embed = new window.YT.Player(id, {
                videoId: videoId,
                playerVars: {
                    autoplay: player.config.autoplay ? 1 : 0,
                    // Autoplay
                    hl: player.config.hl,
                    // iframe interface language
                    controls: player.supported.ui ? 0 : 1,
                    // Only show controls if not fully supported
                    rel: 0,
                    // No related vids
                    showinfo: 0,
                    // Hide info
                    iv_load_policy: 3,
                    // Hide annotations
                    modestbranding: 1,
                    // Hide logos as much as possible (they still show one in the corner when paused)
                    disablekb: 1,
                    // Disable keyboard as we handle it
                    playsinline: 1,
                    // Allow iOS inline playback
                    // Tracking for stats
                    // origin: window ? `${window.location.protocol}//${window.location.host}` : null,
                    widget_referrer: window ? window.location.href : null,
                    // Captions are flaky on YouTube
                    cc_load_policy: player.captions.active ? 1 : 0,
                    cc_lang_pref: player.config.captions.language
                },
                events: {
                    onError: function onError(event) {
                        // YouTube may fire onError twice, so only handle it once
                        if (!player.media.error) {
                            var code = event.data; // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError

                            var message = {
                                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
                                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
                                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
                                101: 'The owner of the requested video does not allow it to be played in embedded players.',
                                150: 'The owner of the requested video does not allow it to be played in embedded players.'
                            }[code] || 'An unknown error occured';
                            player.media.error = {
                                code: code,
                                message: message
                            };
                            triggerEvent.call(player, player.media, 'error');
                        }
                    },
                    onPlaybackRateChange: function onPlaybackRateChange(event) {
                        // Get the instance
                        var instance = event.target; // Get current speed

                        player.media.playbackRate = instance.getPlaybackRate();
                        triggerEvent.call(player, player.media, 'ratechange');
                    },
                    onReady: function onReady(event) {
                        // Bail if onReady has already been called. See issue #1108
                        if (is.function(player.media.play)) {
                            return;
                        } // Get the instance


                        var instance = event.target; // Get the title

                        youtube.getTitle.call(player, videoId); // Create a faux HTML5 API using the YouTube API

                        player.media.play = function () {
                            assurePlaybackState$1.call(player, true);
                            instance.playVideo();
                        };

                        player.media.pause = function () {
                            assurePlaybackState$1.call(player, false);
                            instance.pauseVideo();
                        };

                        player.media.stop = function () {
                            instance.stopVideo();
                        };

                        player.media.duration = instance.getDuration();
                        player.media.paused = true; // Seeking

                        player.media.currentTime = 0;
                        Object.defineProperty(player.media, 'currentTime', {
                            get: function get() {
                                return Number(instance.getCurrentTime());
                            },
                            set: function set(time) {
                                // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
                                if (player.paused && !player.embed.hasPlayed) {
                                    player.embed.mute();
                                } // Set seeking state and trigger event


                                player.media.seeking = true;
                                triggerEvent.call(player, player.media, 'seeking'); // Seek after events sent

                                instance.seekTo(time);
                            }
                        }); // Playback speed

                        Object.defineProperty(player.media, 'playbackRate', {
                            get: function get() {
                                return instance.getPlaybackRate();
                            },
                            set: function set(input) {
                                instance.setPlaybackRate(input);
                            }
                        }); // Volume

                        var volume = player.config.volume;
                        Object.defineProperty(player.media, 'volume', {
                            get: function get() {
                                return volume;
                            },
                            set: function set(input) {
                                volume = input;
                                instance.setVolume(volume * 100);
                                triggerEvent.call(player, player.media, 'volumechange');
                            }
                        }); // Muted

                        var muted = player.config.muted;
                        Object.defineProperty(player.media, 'muted', {
                            get: function get() {
                                return muted;
                            },
                            set: function set(input) {
                                var toggle = is.boolean(input) ? input : muted;
                                muted = toggle;
                                instance[toggle ? 'mute' : 'unMute']();
                                triggerEvent.call(player, player.media, 'volumechange');
                            }
                        }); // Source

                        Object.defineProperty(player.media, 'currentSrc', {
                            get: function get() {
                                return instance.getVideoUrl();
                            }
                        }); // Ended

                        Object.defineProperty(player.media, 'ended', {
                            get: function get() {
                                return player.currentTime === player.duration;
                            }
                        }); // Get available speeds

                        player.options.speed = instance.getAvailablePlaybackRates(); // Set the tabindex to avoid focus entering iframe

                        if (player.supported.ui) {
                            player.media.setAttribute('tabindex', -1);
                        }

                        triggerEvent.call(player, player.media, 'timeupdate');
                        triggerEvent.call(player, player.media, 'durationchange'); // Reset timer

                        clearInterval(player.timers.buffering); // Setup buffering

                        player.timers.buffering = setInterval(function () {
                            // Get loaded % from YouTube
                            player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something

                            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
                                triggerEvent.call(player, player.media, 'progress');
                            } // Set last buffer point


                            player.media.lastBuffered = player.media.buffered; // Bail if we're at 100%

                            if (player.media.buffered === 1) {
                                clearInterval(player.timers.buffering); // Trigger event

                                triggerEvent.call(player, player.media, 'canplaythrough');
                            }
                        }, 200); // Rebuild UI

                        setTimeout(function () {
                            return ui.build.call(player);
                        }, 50);
                    },
                    onStateChange: function onStateChange(event) {
                        // Get the instance
                        var instance = event.target; // Reset timer

                        clearInterval(player.timers.playing);
                        var seeked = player.media.seeking && [1, 2].includes(event.data);

                        if (seeked) {
                            // Unset seeking and fire seeked event
                            player.media.seeking = false;
                            triggerEvent.call(player, player.media, 'seeked');
                        } // Handle events
                        // -1   Unstarted
                        // 0    Ended
                        // 1    Playing
                        // 2    Paused
                        // 3    Buffering
                        // 5    Video cued


                        switch (event.data) {
                            case -1:
                                // Update scrubber
                                triggerEvent.call(player, player.media, 'timeupdate'); // Get loaded % from YouTube

                                player.media.buffered = instance.getVideoLoadedFraction();
                                triggerEvent.call(player, player.media, 'progress');
                                break;

                            case 0:
                                assurePlaybackState$1.call(player, false); // YouTube doesn't support loop for a single video, so mimick it.

                                if (player.media.loop) {
                                    // YouTube needs a call to `stopVideo` before playing again
                                    instance.stopVideo();
                                    instance.playVideo();
                                } else {
                                    triggerEvent.call(player, player.media, 'ended');
                                }

                                break;

                            case 1:
                                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
                                if (player.media.paused && !player.embed.hasPlayed) {
                                    player.media.pause();
                                } else {
                                    assurePlaybackState$1.call(player, true);
                                    triggerEvent.call(player, player.media, 'playing'); // Poll to get playback progress

                                    player.timers.playing = setInterval(function () {
                                        triggerEvent.call(player, player.media, 'timeupdate');
                                    }, 50); // Check duration again due to YouTube bug
                                    // https://github.com/sampotts/plyr/issues/374
                                    // https://code.google.com/p/gdata-issues/issues/detail?id=8690

                                    if (player.media.duration !== instance.getDuration()) {
                                        player.media.duration = instance.getDuration();
                                        triggerEvent.call(player, player.media, 'durationchange');
                                    }
                                }

                                break;

                            case 2:
                                // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
                                if (!player.muted) {
                                    player.embed.unMute();
                                }

                                assurePlaybackState$1.call(player, false);
                                break;

                            default:
                                break;
                        }

                        triggerEvent.call(player, player.elements.container, 'statechange', false, {
                            code: event.data
                        });
                    }
                }
            });
        }
    };

    // ==========================================================================
    var media = {
        // Setup media
        setup: function setup() {
            // If there's no media, bail
            if (!this.media) {
                this.debug.warn('No media element found!');
                return;
            } // Add type class


            toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); // Add provider class

            toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); // Add video class for embeds
            // This will require changes if audio embeds are added

            if (this.isEmbed) {
                toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
            } // Inject the player wrapper


            if (this.isVideo) {
                // Create the wrapper div
                this.elements.wrapper = createElement('div', {
                    class: this.config.classNames.video
                }); // Wrap the video in a container

                wrap(this.media, this.elements.wrapper); // Faux poster container

                this.elements.poster = createElement('div', {
                    class: this.config.classNames.poster
                });
                this.elements.wrapper.appendChild(this.elements.poster);
            }

            if (this.isHTML5) {
                html5.extend.call(this);
            } else if (this.isYouTube) {
                youtube.setup.call(this);
            } else if (this.isVimeo) {
                vimeo.setup.call(this);
            }
        }
    };

    var Ads =
        /*#__PURE__*/
        function () {
            /**
             * Ads constructor.
             * @param {object} player
             * @return {Ads}
             */
            function Ads(player) {
                var _this = this;

                _classCallCheck(this, Ads);

                this.player = player;
                this.publisherId = player.config.ads.publisherId;
                this.playing = false;
                this.initialized = false;
                this.elements = {
                    container: null,
                    displayContainer: null
                };
                this.manager = null;
                this.loader = null;
                this.cuePoints = null;
                this.events = {};
                this.safetyTimer = null;
                this.countdownTimer = null; // Setup a promise to resolve when the IMA manager is ready

                this.managerPromise = new Promise(function (resolve, reject) {
                    // The ad is loaded and ready
                    _this.on('loaded', resolve); // Ads failed


                    _this.on('error', reject);
                });
                this.load();
            }

            _createClass(Ads, [{
                key: "load",

                /**
                 * Load the IMA SDK
                 */
                value: function load() {
                    var _this2 = this;

                    if (this.enabled) {
                        // Check if the Google IMA3 SDK is loaded or load it ourselves
                        if (!is.object(window.google) || !is.object(window.google.ima)) {
                            loadScript(this.player.config.urls.googleIMA.sdk).then(function () {
                                _this2.ready();
                            }).catch(function () {
                                // Script failed to load or is blocked
                                _this2.trigger('error', new Error('Google IMA SDK failed to load'));
                            });
                        } else {
                            this.ready();
                        }
                    }
                }
                /**
                 * Get the ads instance ready
                 */

            }, {
                key: "ready",
                value: function ready$$1() {
                    var _this3 = this;

                    // Start ticking our safety timer. If the whole advertisement
                    // thing doesn't resolve within our set time; we bail
                    this.startSafetyTimer(12000, 'ready()'); // Clear the safety timer

                    this.managerPromise.then(function () {
                        _this3.clearSafetyTimer('onAdsManagerLoaded()');
                    }); // Set listeners on the Plyr instance

                    this.listeners(); // Setup the IMA SDK

                    this.setupIMA();
                } // Build the default tag URL

            }, {
                key: "setupIMA",

                /**
                 * In order for the SDK to display ads for our video, we need to tell it where to put them,
                 * so here we define our ad container. This div is set up to render on top of the video player.
                 * Using the code below, we tell the SDK to render ads within that div. We also provide a
                 * handle to the content video player - the SDK will poll the current time of our player to
                 * properly place mid-rolls. After we create the ad display container, we initialize it. On
                 * mobile devices, this initialization is done as the result of a user action.
                 */
                value: function setupIMA() {
                    // Create the container for our advertisements
                    this.elements.container = createElement('div', {
                        class: this.player.config.classNames.ads
                    });
                    this.player.elements.container.appendChild(this.elements.container); // So we can run VPAID2

                    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); // Set language

                    google.ima.settings.setLocale(this.player.config.ads.language); // We assume the adContainer is the video container of the plyr element
                    // that will house the ads

                    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); // Request video ads to be pre-loaded

                    this.requestAds();
                }
                /**
                 * Request advertisements
                 */

            }, {
                key: "requestAds",
                value: function requestAds() {
                    var _this4 = this;

                    var container = this.player.elements.container;

                    try {
                        // Create ads loader
                        this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events

                        this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
                            return _this4.onAdsManagerLoaded(event);
                        }, false);
                        this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
                            return _this4.onAdError(error);
                        }, false); // Request video ads

                        var request = new google.ima.AdsRequest();
                        request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
                        // to select the correct creative if multiple are returned

                        request.linearAdSlotWidth = container.offsetWidth;
                        request.linearAdSlotHeight = container.offsetHeight;
                        request.nonLinearAdSlotWidth = container.offsetWidth;
                        request.nonLinearAdSlotHeight = container.offsetHeight; // We only overlay ads as we only support video.

                        request.forceNonLinearFullSlot = false; // Mute based on current state

                        request.setAdWillPlayMuted(!this.player.muted);
                        this.loader.requestAds(request);
                    } catch (e) {
                        this.onAdError(e);
                    }
                }
                /**
                 * Update the ad countdown
                 * @param {boolean} start
                 */

            }, {
                key: "pollCountdown",
                value: function pollCountdown() {
                    var _this5 = this;

                    var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

                    if (!start) {
                        clearInterval(this.countdownTimer);
                        this.elements.container.removeAttribute('data-badge-text');
                        return;
                    }

                    var update = function update() {
                        var time = formatTime(Math.max(_this5.manager.getRemainingTime(), 0));
                        var label = "".concat(i18n.get('advertisement', _this5.player.config), " - ").concat(time);

                        _this5.elements.container.setAttribute('data-badge-text', label);
                    };

                    this.countdownTimer = setInterval(update, 100);
                }
                /**
                 * This method is called whenever the ads are ready inside the AdDisplayContainer
                 * @param {Event} adsManagerLoadedEvent
                 */

            }, {
                key: "onAdsManagerLoaded",
                value: function onAdsManagerLoaded(event) {
                    var _this6 = this;

                    // Load could occur after a source change (race condition)
                    if (!this.enabled) {
                        return;
                    } // Get the ads manager


                    var settings = new google.ima.AdsRenderingSettings(); // Tell the SDK to save and restore content video state on our behalf

                    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;
                    settings.enablePreloading = true; // The SDK is polling currentTime on the contentPlayback. And needs a duration
                    // so it can determine when to start the mid- and post-roll

                    this.manager = event.getAdsManager(this.player, settings); // Get the cue points for any mid-rolls by filtering out the pre- and post-roll

                    this.cuePoints = this.manager.getCuePoints(); // Add advertisement cue's within the time line if available

                    if (!is.empty(this.cuePoints)) {
                        this.cuePoints.forEach(function (cuePoint) {
                            if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < _this6.player.duration) {
                                var seekElement = _this6.player.elements.progress;

                                if (is.element(seekElement)) {
                                    var cuePercentage = 100 / _this6.player.duration * cuePoint;
                                    var cue = createElement('span', {
                                        class: _this6.player.config.classNames.cues
                                    });
                                    cue.style.left = "".concat(cuePercentage.toString(), "%");
                                    seekElement.appendChild(cue);
                                }
                            }
                        });
                    } // Set volume to match player


                    this.manager.setVolume(this.player.volume); // Add listeners to the required events
                    // Advertisement error events

                    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
                        return _this6.onAdError(error);
                    }); // Advertisement regular events

                    Object.keys(google.ima.AdEvent.Type).forEach(function (type) {
                        _this6.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) {
                            return _this6.onAdEvent(event);
                        });
                    }); // Resolve our adsManager

                    this.trigger('loaded');
                }
                /**
                 * This is where all the event handling takes place. Retrieve the ad from the event. Some
                 * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated
                 * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type
                 * @param {Event} event
                 */

            }, {
                key: "onAdEvent",
                value: function onAdEvent(event) {
                    var _this7 = this;

                    var container = this.player.elements.container; // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
                    // don't have ad object associated

                    var ad = event.getAd(); // Proxy event

                    var dispatchEvent = function dispatchEvent(type) {
                        var event = "ads".concat(type.replace(/_/g, '').toLowerCase());
                        triggerEvent.call(_this7.player, _this7.player.media, event);
                    };

                    switch (event.type) {
                        case google.ima.AdEvent.Type.LOADED:
                            // This is the first event sent for an ad - it is possible to determine whether the
                            // ad is a video ad or an overlay
                            this.trigger('loaded'); // Bubble event

                            dispatchEvent(event.type); // Start countdown

                            this.pollCountdown(true);

                            if (!ad.isLinear()) {
                                // Position AdDisplayContainer correctly for overlay
                                ad.width = container.offsetWidth;
                                ad.height = container.offsetHeight;
                            } // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
                            // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());


                            break;

                        case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
                            // All ads for the current videos are done. We can now request new advertisements
                            // in case the video is re-played
                            // Fire event
                            dispatchEvent(event.type); // TODO: Example for what happens when a next video in a playlist would be loaded.
                            // So here we load a new video when all ads are done.
                            // Then we load new ads within a new adsManager. When the video
                            // Is started - after - the ads are loaded, then we get ads.
                            // You can also easily test cancelling and reloading by running
                            // player.ads.cancel() and player.ads.play from the console I guess.
                            // this.player.source = {
                            //     type: 'video',
                            //     title: 'View From A Blue Moon',
                            //     sources: [{
                            //         src:
                            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:
                            // 'video/mp4', }], poster:
                            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:
                            // [ { kind: 'captions', label: 'English', srclang: 'en', src:
                            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
                            // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:
                            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],
                            // };
                            // TODO: So there is still this thing where a video should only be allowed to start
                            // playing when the IMA SDK is ready or has failed

                            this.loadAds();
                            break;

                        case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
                            // This event indicates the ad has started - the video player can adjust the UI,
                            // for example display a pause button and remaining time. Fired when content should
                            // be paused. This usually happens right before an ad is about to cover the content
                            dispatchEvent(event.type);
                            this.pauseContent();
                            break;

                        case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
                            // This event indicates the ad has finished - the video player can perform
                            // appropriate UI actions, such as removing the timer for remaining time detection.
                            // Fired when content should be resumed. This usually happens when an ad finishes
                            // or collapses
                            dispatchEvent(event.type);
                            this.pollCountdown();
                            this.resumeContent();
                            break;

                        case google.ima.AdEvent.Type.STARTED:
                        case google.ima.AdEvent.Type.MIDPOINT:
                        case google.ima.AdEvent.Type.COMPLETE:
                        case google.ima.AdEvent.Type.IMPRESSION:
                        case google.ima.AdEvent.Type.CLICK:
                            dispatchEvent(event.type);
                            break;

                        default:
                            break;
                    }
                }
                /**
                 * Any ad error handling comes through here
                 * @param {Event} event
                 */

            }, {
                key: "onAdError",
                value: function onAdError(event) {
                    this.cancel();
                    this.player.debug.warn('Ads error', event);
                }
                /**
                 * Setup hooks for Plyr and window events. This ensures
                 * the mid- and post-roll launch at the correct time. And
                 * resize the advertisement when the player resizes
                 */

            }, {
                key: "listeners",
                value: function listeners() {
                    var _this8 = this;

                    var container = this.player.elements.container;
                    var time; // Add listeners to the required events

                    this.player.on('ended', function () {
                        _this8.loader.contentComplete();
                    });
                    this.player.on('seeking', function () {
                        time = _this8.player.currentTime;
                        return time;
                    });
                    this.player.on('seeked', function () {
                        var seekedTime = _this8.player.currentTime;

                        if (is.empty(_this8.cuePoints)) {
                            return;
                        }

                        _this8.cuePoints.forEach(function (cuePoint, index) {
                            if (time < cuePoint && cuePoint < seekedTime) {
                                _this8.manager.discardAdBreak();

                                _this8.cuePoints.splice(index, 1);
                            }
                        });
                    }); // Listen to the resizing of the window. And resize ad accordingly
                    // TODO: eventually implement ResizeObserver

                    window.addEventListener('resize', function () {
                        if (_this8.manager) {
                            _this8.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
                        }
                    });
                }
                /**
                 * Initialize the adsManager and start playing advertisements
                 */

            }, {
                key: "play",
                value: function play() {
                    var _this9 = this;

                    var container = this.player.elements.container;

                    if (!this.managerPromise) {
                        this.resumeContent();
                    } // Play the requested advertisement whenever the adsManager is ready


                    this.managerPromise.then(function () {
                        // Initialize the container. Must be done via a user action on mobile devices
                        _this9.elements.displayContainer.initialize();

                        try {
                            if (!_this9.initialized) {
                                // Initialize the ads manager. Ad rules playlist will start at this time
                                _this9.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will
                                // start at this time; the call will be ignored for ad rules


                                _this9.manager.start();
                            }

                            _this9.initialized = true;
                        } catch (adError) {
                            // An error may be thrown if there was a problem with the
                            // VAST response
                            _this9.onAdError(adError);
                        }
                    }).catch(function () {});
                }
                /**
                 * Resume our video
                 */

            }, {
                key: "resumeContent",
                value: function resumeContent() {
                    // Hide the advertisement container
                    this.elements.container.style.zIndex = ''; // Ad is stopped

                    this.playing = false; // Play our video

                    if (this.player.currentTime < this.player.duration) {
                        this.player.play();
                    }
                }
                /**
                 * Pause our video
                 */

            }, {
                key: "pauseContent",
                value: function pauseContent() {
                    // Show the advertisement container
                    this.elements.container.style.zIndex = 3; // Ad is playing.

                    this.playing = true; // Pause our video.

                    this.player.pause();
                }
                /**
                 * Destroy the adsManager so we can grab new ads after this. If we don't then we're not
                 * allowed to call new ads based on google policies, as they interpret this as an accidental
                 * video requests. https://developers.google.com/interactive-
                 * media-ads/docs/sdks/android/faq#8
                 */

            }, {
                key: "cancel",
                value: function cancel() {
                    // Pause our video
                    if (this.initialized) {
                        this.resumeContent();
                    } // Tell our instance that we're done for now


                    this.trigger('error'); // Re-create our adsManager

                    this.loadAds();
                }
                /**
                 * Re-create our adsManager
                 */

            }, {
                key: "loadAds",
                value: function loadAds() {
                    var _this10 = this;

                    // Tell our adsManager to go bye bye
                    this.managerPromise.then(function () {
                        // Destroy our adsManager
                        if (_this10.manager) {
                            _this10.manager.destroy();
                        } // Re-set our adsManager promises


                        _this10.managerPromise = new Promise(function (resolve) {
                            _this10.on('loaded', resolve);

                            _this10.player.debug.log(_this10.manager);
                        }); // Now request some new advertisements

                        _this10.requestAds();
                    }).catch(function () {});
                }
                /**
                 * Handles callbacks after an ad event was invoked
                 * @param {string} event - Event type
                 */

            }, {
                key: "trigger",
                value: function trigger(event) {
                    var _this11 = this;

                    for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
                        args[_key - 1] = arguments[_key];
                    }

                    var handlers = this.events[event];

                    if (is.array(handlers)) {
                        handlers.forEach(function (handler) {
                            if (is.function(handler)) {
                                handler.apply(_this11, args);
                            }
                        });
                    }
                }
                /**
                 * Add event listeners
                 * @param {string} event - Event type
                 * @param {function} callback - Callback for when event occurs
                 * @return {Ads}
                 */

            }, {
                key: "on",
                value: function on$$1(event, callback) {
                    if (!is.array(this.events[event])) {
                        this.events[event] = [];
                    }

                    this.events[event].push(callback);
                    return this;
                }
                /**
                 * Setup a safety timer for when the ad network doesn't respond for whatever reason.
                 * The advertisement has 12 seconds to get its things together. We stop this timer when the
                 * advertisement is playing, or when a user action is required to start, then we clear the
                 * timer on ad ready
                 * @param {number} time
                 * @param {string} from
                 */

            }, {
                key: "startSafetyTimer",
                value: function startSafetyTimer(time, from) {
                    var _this12 = this;

                    this.player.debug.log("Safety timer invoked from: ".concat(from));
                    this.safetyTimer = setTimeout(function () {
                        _this12.cancel();

                        _this12.clearSafetyTimer('startSafetyTimer()');
                    }, time);
                }
                /**
                 * Clear our safety timer(s)
                 * @param {string} from
                 */

            }, {
                key: "clearSafetyTimer",
                value: function clearSafetyTimer(from) {
                    if (!is.nullOrUndefined(this.safetyTimer)) {
                        this.player.debug.log("Safety timer cleared from: ".concat(from));
                        clearTimeout(this.safetyTimer);
                        this.safetyTimer = null;
                    }
                }
            }, {
                key: "enabled",
                get: function get() {
                    return this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId);
                }
            }, {
                key: "tagUrl",
                get: function get() {
                    var params = {
                        AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
                        AV_CHANNELID: '5a0458dc28a06145e4519d21',
                        AV_URL: window.location.hostname,
                        cb: Date.now(),
                        AV_WIDTH: 640,
                        AV_HEIGHT: 480,
                        AV_CDIM2: this.publisherId
                    };
                    var base = 'https://go.aniview.com/api/adserver6/vast/';
                    return "".concat(base, "?").concat(buildUrlParams(params));
                }
            }]);

            return Ads;
        }();

    var source = {
        // Add elements to HTML5 media (source, tracks, etc)
        insertElements: function insertElements(type, attributes) {
            var _this = this;

            if (is.string(attributes)) {
                insertElement(type, this.media, {
                    src: attributes
                });
            } else if (is.array(attributes)) {
                attributes.forEach(function (attribute) {
                    insertElement(type, _this.media, attribute);
                });
            }
        },
        // Update source
        // Sources are not checked for support so be careful
        change: function change(input) {
            var _this2 = this;

            if (!getDeep(input, 'sources.length')) {
                this.debug.warn('Invalid source format');
                return;
            } // Cancel current network requests


            html5.cancelRequests.call(this); // Destroy instance and re-setup

            this.destroy.call(this, function () {
                // Reset quality options
                _this2.options.quality = []; // Remove elements

                removeElement(_this2.media);
                _this2.media = null; // Reset class name

                if (is.element(_this2.elements.container)) {
                    _this2.elements.container.removeAttribute('class');
                } // Set the type and provider


                var sources = input.sources,
                    type = input.type;

                var _sources = _slicedToArray(sources, 1),
                    _sources$ = _sources[0],
                    _sources$$provider = _sources$.provider,
                    provider = _sources$$provider === void 0 ? providers.html5 : _sources$$provider,
                    src = _sources$.src;

                var tagName = provider === 'html5' ? type : 'div';
                var attributes = provider === 'html5' ? {} : {
                    src: src
                };
                Object.assign(_this2, {
                    provider: provider,
                    type: type,
                    // Check for support
                    supported: support.check(type, provider, _this2.config.playsinline),
                    // Create new element
                    media: createElement(tagName, attributes)
                }); // Inject the new element

                _this2.elements.container.appendChild(_this2.media); // Autoplay the new source?


                if (is.boolean(input.autoplay)) {
                    _this2.config.autoplay = input.autoplay;
                } // Set attributes for audio and video


                if (_this2.isHTML5) {
                    if (_this2.config.crossorigin) {
                        _this2.media.setAttribute('crossorigin', '');
                    }

                    if (_this2.config.autoplay) {
                        _this2.media.setAttribute('autoplay', '');
                    }

                    if (!is.empty(input.poster)) {
                        _this2.poster = input.poster;
                    }

                    if (_this2.config.loop.active) {
                        _this2.media.setAttribute('loop', '');
                    }

                    if (_this2.config.muted) {
                        _this2.media.setAttribute('muted', '');
                    }

                    if (_this2.config.playsinline) {
                        _this2.media.setAttribute('playsinline', '');
                    }
                } // Restore class hook


                ui.addStyleHook.call(_this2); // Set new sources for html5

                if (_this2.isHTML5) {
                    source.insertElements.call(_this2, 'source', sources);
                } // Set video title


                _this2.config.title = input.title; // Set up from scratch

                media.setup.call(_this2); // HTML5 stuff

                if (_this2.isHTML5) {
                    // Setup captions
                    if (Object.keys(input).includes('tracks')) {
                        source.insertElements.call(_this2, 'track', input.tracks);
                    }
                } // If HTML5 or embed but not fully supported, setupInterface and call ready now


                if (_this2.isHTML5 || _this2.isEmbed && !_this2.supported.ui) {
                    // Setup interface
                    ui.build.call(_this2);
                }

                if (_this2.isHTML5) {
                    // Load HTML5 sources
                    _this2.media.load();
                } // Update the fullscreen support


                _this2.fullscreen.update();
            }, true);
        }
    };

    // TODO: Use a WeakMap for private globals
    // const globals = new WeakMap();
    // Plyr instance

    var Plyr =
        /*#__PURE__*/
        function () {
            function Plyr(target, options) {
                var _this = this;

                _classCallCheck(this, Plyr);

                this.timers = {}; // State

                this.ready = false;
                this.loading = false;
                this.failed = false; // Touch device

                this.touch = support.touch; // Set the media element

                this.media = target; // String selector passed

                if (is.string(this.media)) {
                    this.media = document.querySelectorAll(this.media);
                } // jQuery, NodeList or Array passed, use first element


                if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {
                    // eslint-disable-next-line
                    this.media = this.media[0];
                } // Set config


                this.config = extend({}, defaults, Plyr.defaults, options || {}, function () {
                    try {
                        return JSON.parse(_this.media.getAttribute('data-plyr-config'));
                    } catch (e) {
                        return {};
                    }
                }()); // Elements cache

                this.elements = {
                    container: null,
                    captions: null,
                    buttons: {},
                    display: {},
                    progress: {},
                    inputs: {},
                    settings: {
                        popup: null,
                        menu: null,
                        panels: {},
                        buttons: {}
                    }
                }; // Captions

                this.captions = {
                    active: null,
                    currentTrack: -1,
                    meta: new WeakMap()
                }; // Fullscreen

                this.fullscreen = {
                    active: false
                }; // Options

                this.options = {
                    speed: [],
                    quality: []
                }; // Debugging
                // TODO: move to globals

                this.debug = new Console(this.config.debug); // Log config options and support

                this.debug.log('Config', this.config);
                this.debug.log('Support', support); // We need an element to setup

                if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
                    this.debug.error('Setup failed: no suitable element passed');
                    return;
                } // Bail if the element is initialized


                if (this.media.plyr) {
                    this.debug.warn('Target already setup');
                    return;
                } // Bail if not enabled


                if (!this.config.enabled) {
                    this.debug.error('Setup failed: disabled by config');
                    return;
                } // Bail if disabled or no basic support
                // You may want to disable certain UAs etc


                if (!support.check().api) {
                    this.debug.error('Setup failed: no support');
                    return;
                } // Cache original element state for .destroy()


                var clone = this.media.cloneNode(true);
                clone.autoplay = false;
                this.elements.original = clone; // Set media type based on tag or data attribute
                // Supported: video, audio, vimeo, youtube

                var type = this.media.tagName.toLowerCase(); // Embed properties

                var iframe = null;
                var url = null; // Different setup based on type

                switch (type) {
                    case 'div':
                        // Find the frame
                        iframe = this.media.querySelector('iframe'); // <iframe> type

                        if (is.element(iframe)) {
                            // Detect provider
                            url = parseUrl(iframe.getAttribute('src'));
                            this.provider = getProviderByUrl(url.toString()); // Rework elements

                            this.elements.container = this.media;
                            this.media = iframe; // Reset classname

                            this.elements.container.className = ''; // Get attributes from URL and set config

                            if (url.search.length) {
                                var truthy = ['1', 'true'];

                                if (truthy.includes(url.searchParams.get('autoplay'))) {
                                    this.config.autoplay = true;
                                }

                                if (truthy.includes(url.searchParams.get('loop'))) {
                                    this.config.loop.active = true;
                                } // TODO: replace fullscreen.iosNative with this playsinline config option
                                // YouTube requires the playsinline in the URL


                                if (this.isYouTube) {
                                    this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
                                    this.config.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
                                } else {
                                    this.config.playsinline = true;
                                }
                            }
                        } else {
                            // <div> with attributes
                            this.provider = this.media.getAttribute(this.config.attributes.embed.provider); // Remove attribute

                            this.media.removeAttribute(this.config.attributes.embed.provider);
                        } // Unsupported or missing provider


                        if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
                            this.debug.error('Setup failed: Invalid provider');
                            return;
                        } // Audio will come later for external providers


                        this.type = types.video;
                        break;

                    case 'video':
                    case 'audio':
                        this.type = type;
                        this.provider = providers.html5; // Get config from attributes

                        if (this.media.hasAttribute('crossorigin')) {
                            this.config.crossorigin = true;
                        }

                        if (this.media.hasAttribute('autoplay')) {
                            this.config.autoplay = true;
                        }

                        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
                            this.config.playsinline = true;
                        }

                        if (this.media.hasAttribute('muted')) {
                            this.config.muted = true;
                        }

                        if (this.media.hasAttribute('loop')) {
                            this.config.loop.active = true;
                        }

                        break;

                    default:
                        this.debug.error('Setup failed: unsupported type');
                        return;
                } // Check for support again but with type


                this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail

                if (!this.supported.api) {
                    this.debug.error('Setup failed: no support');
                    return;
                }

                this.eventListeners = []; // Create listeners

                this.listeners = new Listeners(this); // Setup local storage for user settings

                this.storage = new Storage(this); // Store reference

                this.media.plyr = this; // Wrap media

                if (!is.element(this.elements.container)) {
                    this.elements.container = createElement('div');
                    wrap(this.media, this.elements.container);
                } // Add style hook


                ui.addStyleHook.call(this); // Setup media

                media.setup.call(this); // Listen for events if debugging

                if (this.config.debug) {
                    on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
                        _this.debug.log("event: ".concat(event.type));
                    });
                } // Setup interface
                // If embed but not fully supported, build interface now to avoid flash of controls


                if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
                    ui.build.call(this);
                } // Container listeners


                this.listeners.container(); // Global listeners

                this.listeners.global(); // Setup fullscreen

                this.fullscreen = new Fullscreen(this); // Setup ads if provided

                if (this.config.ads.enabled) {
                    this.ads = new Ads(this);
                } // Autoplay if required


                if (this.config.autoplay) {
                    this.play();
                } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek


                this.lastSeekTime = 0;
            } // ---------------------------------------
            // API
            // ---------------------------------------

            /**
             * Types and provider helpers
             */


            _createClass(Plyr, [{
                key: "play",

                /**
                 * Play the media, or play the advertisement (if they are not blocked)
                 */
                value: function play() {
                    if (!is.function(this.media.play)) {
                        return null;
                    } // Return the promise (for HTML5)


                    return this.media.play();
                }
                /**
                 * Pause the media
                 */

            }, {
                key: "pause",
                value: function pause() {
                    if (!this.playing || !is.function(this.media.pause)) {
                        return;
                    }

                    this.media.pause();
                }
                /**
                 * Get playing state
                 */

            }, {
                key: "togglePlay",

                /**
                 * Toggle playback based on current status
                 * @param {boolean} input
                 */
                value: function togglePlay(input) {
                    // Toggle based on current state if nothing passed
                    var toggle = is.boolean(input) ? input : !this.playing;

                    if (toggle) {
                        this.play();
                    } else {
                        this.pause();
                    }
                }
                /**
                 * Stop playback
                 */

            }, {
                key: "stop",
                value: function stop() {
                    if (this.isHTML5) {
                        this.pause();
                        this.restart();
                    } else if (is.function(this.media.stop)) {
                        this.media.stop();
                    }
                }
                /**
                 * Restart playback
                 */

            }, {
                key: "restart",
                value: function restart() {
                    this.currentTime = 0;
                }
                /**
                 * Rewind
                 * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
                 */

            }, {
                key: "rewind",
                value: function rewind(seekTime) {
                    this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
                }
                /**
                 * Fast forward
                 * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
                 */

            }, {
                key: "forward",
                value: function forward(seekTime) {
                    this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
                }
                /**
                 * Seek to a time
                 * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
                 */

            }, {
                key: "increaseVolume",

                /**
                 * Increase volume
                 * @param {boolean} step - How much to decrease by (between 0 and 1)
                 */
                value: function increaseVolume(step) {
                    var volume = this.media.muted ? 0 : this.volume;
                    this.volume = volume + (is.number(step) ? step : 0);
                }
                /**
                 * Decrease volume
                 * @param {boolean} step - How much to decrease by (between 0 and 1)
                 */

            }, {
                key: "decreaseVolume",
                value: function decreaseVolume(step) {
                    this.increaseVolume(-step);
                }
                /**
                 * Set muted state
                 * @param {boolean} mute
                 */

            }, {
                key: "toggleCaptions",

                /**
                 * Toggle captions
                 * @param {boolean} input - Whether to enable captions
                 */
                value: function toggleCaptions(input) {
                    captions.toggle.call(this, input, false);
                }
                /**
                 * Set the caption track by index
                 * @param {number} - Caption index
                 */

            }, {
                key: "airplay",

                /**
                 * Trigger the airplay dialog
                 * TODO: update player with state, support, enabled
                 */
                value: function airplay() {
                    // Show dialog if supported
                    if (support.airplay) {
                        this.media.webkitShowPlaybackTargetPicker();
                    }
                }
                /**
                 * Toggle the player controls
                 * @param {boolean} [toggle] - Whether to show the controls
                 */

            }, {
                key: "toggleControls",
                value: function toggleControls(toggle) {
                    // Don't toggle if missing UI support or if it's audio
                    if (this.supported.ui && !this.isAudio) {
                        // Get state before change
                        var isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); // Negate the argument if not undefined since adding the class to hides the controls

                        var force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state

                        var hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); // Close menu

                        if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
                            controls.toggleMenu.call(this, false);
                        } // Trigger event on change


                        if (hiding !== isHidden) {
                            var eventName = hiding ? 'controlshidden' : 'controlsshown';
                            triggerEvent.call(this, this.media, eventName);
                        }

                        return !hiding;
                    }

                    return false;
                }
                /**
                 * Add event listeners
                 * @param {string} event - Event type
                 * @param {function} callback - Callback for when event occurs
                 */

            }, {
                key: "on",
                value: function on$$1(event, callback) {
                    on.call(this, this.elements.container, event, callback);
                }
                /**
                 * Add event listeners once
                 * @param {string} event - Event type
                 * @param {function} callback - Callback for when event occurs
                 */

            }, {
                key: "once",
                value: function once$$1(event, callback) {
                    once.call(this, this.elements.container, event, callback);
                }
                /**
                 * Remove event listeners
                 * @param {string} event - Event type
                 * @param {function} callback - Callback for when event occurs
                 */

            }, {
                key: "off",
                value: function off$$1(event, callback) {
                    off(this.elements.container, event, callback);
                }
                /**
                 * Destroy an instance
                 * Event listeners are removed when elements are removed
                 * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
                 * @param {function} callback - Callback for when destroy is complete
                 * @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
                 */

            }, {
                key: "destroy",
                value: function destroy(callback) {
                    var _this2 = this;

                    var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;

                    if (!this.ready) {
                        return;
                    }

                    var done = function done() {
                        // Reset overflow (incase destroyed while in fullscreen)
                        document.body.style.overflow = ''; // GC for embed

                        _this2.embed = null; // If it's a soft destroy, make minimal changes

                        if (soft) {
                            if (Object.keys(_this2.elements).length) {
                                // Remove elements
                                removeElement(_this2.elements.buttons.play);
                                removeElement(_this2.elements.captions);
                                removeElement(_this2.elements.controls);
                                removeElement(_this2.elements.wrapper); // Clear for GC

                                _this2.elements.buttons.play = null;
                                _this2.elements.captions = null;
                                _this2.elements.controls = null;
                                _this2.elements.wrapper = null;
                            } // Callback


                            if (is.function(callback)) {
                                callback();
                            }
                        } else {
                            // Unbind listeners
                            unbindListeners.call(_this2); // Replace the container with the original element provided

                            replaceElement(_this2.elements.original, _this2.elements.container); // Event

                            triggerEvent.call(_this2, _this2.elements.original, 'destroyed', true); // Callback

                            if (is.function(callback)) {
                                callback.call(_this2.elements.original);
                            } // Reset state


                            _this2.ready = false; // Clear for garbage collection

                            setTimeout(function () {
                                _this2.elements = null;
                                _this2.media = null;
                            }, 200);
                        }
                    }; // Stop playback


                    this.stop(); // Provider specific stuff

                    if (this.isHTML5) {
                        // Clear timeout
                        clearTimeout(this.timers.loading); // Restore native video controls

                        ui.toggleNativeControls.call(this, true); // Clean up

                        done();
                    } else if (this.isYouTube) {
                        // Clear timers
                        clearInterval(this.timers.buffering);
                        clearInterval(this.timers.playing); // Destroy YouTube API

                        if (this.embed !== null && is.function(this.embed.destroy)) {
                            this.embed.destroy();
                        } // Clean up


                        done();
                    } else if (this.isVimeo) {
                        // Destroy Vimeo API
                        // then clean up (wait, to prevent postmessage errors)
                        if (this.embed !== null) {
                            this.embed.unload().then(done);
                        } // Vimeo does not always return


                        setTimeout(done, 200);
                    }
                }
                /**
                 * Check for support for a mime type (HTML5 only)
                 * @param {string} type - Mime type
                 */

            }, {
                key: "supports",
                value: function supports(type) {
                    return support.mime.call(this, type);
                }
                /**
                 * Check for support
                 * @param {string} type - Player type (audio/video)
                 * @param {string} provider - Provider (html5/youtube/vimeo)
                 * @param {bool} inline - Where player has `playsinline` sttribute
                 */

            }, {
                key: "isHTML5",
                get: function get() {
                    return Boolean(this.provider === providers.html5);
                }
            }, {
                key: "isEmbed",
                get: function get() {
                    return Boolean(this.isYouTube || this.isVimeo);
                }
            }, {
                key: "isYouTube",
                get: function get() {
                    return Boolean(this.provider === providers.youtube);
                }
            }, {
                key: "isVimeo",
                get: function get() {
                    return Boolean(this.provider === providers.vimeo);
                }
            }, {
                key: "isVideo",
                get: function get() {
                    return Boolean(this.type === types.video);
                }
            }, {
                key: "isAudio",
                get: function get() {
                    return Boolean(this.type === types.audio);
                }
            }, {
                key: "playing",
                get: function get() {
                    return Boolean(this.ready && !this.paused && !this.ended);
                }
                /**
                 * Get paused state
                 */

            }, {
                key: "paused",
                get: function get() {
                    return Boolean(this.media.paused);
                }
                /**
                 * Get stopped state
                 */

            }, {
                key: "stopped",
                get: function get() {
                    return Boolean(this.paused && this.currentTime === 0);
                }
                /**
                 * Get ended state
                 */

            }, {
                key: "ended",
                get: function get() {
                    return Boolean(this.media.ended);
                }
            }, {
                key: "currentTime",
                set: function set(input) {
                    // Bail if media duration isn't available yet
                    if (!this.duration) {
                        return;
                    } // Validate input


                    var inputIsValid = is.number(input) && input > 0; // Set

                    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging

                    this.debug.log("Seeking to ".concat(this.currentTime, " seconds"));
                }
                /**
                 * Get current time
                 */
                ,
                get: function get() {
                    return Number(this.media.currentTime);
                }
                /**
                 * Get buffered
                 */

            }, {
                key: "buffered",
                get: function get() {
                    var buffered = this.media.buffered; // YouTube / Vimeo return a float between 0-1

                    if (is.number(buffered)) {
                        return buffered;
                    } // HTML5
                    // TODO: Handle buffered chunks of the media
                    // (i.e. seek to another section buffers only that section)


                    if (buffered && buffered.length && this.duration > 0) {
                        return buffered.end(0) / this.duration;
                    }

                    return 0;
                }
                /**
                 * Get seeking status
                 */

            }, {
                key: "seeking",
                get: function get() {
                    return Boolean(this.media.seeking);
                }
                /**
                 * Get the duration of the current media
                 */

            }, {
                key: "duration",
                get: function get() {
                    // Faux duration set via config
                    var fauxDuration = parseFloat(this.config.duration); // Media duration can be NaN or Infinity before the media has loaded

                    var realDuration = (this.media || {}).duration;
                    var duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; // If config duration is funky, use regular duration

                    return fauxDuration || duration;
                }
                /**
                 * Set the player volume
                 * @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
                 */

            }, {
                key: "volume",
                set: function set(value) {
                    var volume = value;
                    var max = 1;
                    var min = 0;

                    if (is.string(volume)) {
                        volume = Number(volume);
                    } // Load volume from storage if no value specified


                    if (!is.number(volume)) {
                        volume = this.storage.get('volume');
                    } // Use config if all else fails


                    if (!is.number(volume)) {
                        volume = this.config.volume;
                    } // Maximum is volumeMax


                    if (volume > max) {
                        volume = max;
                    } // Minimum is volumeMin


                    if (volume < min) {
                        volume = min;
                    } // Update config


                    this.config.volume = volume; // Set the player volume

                    this.media.volume = volume; // If muted, and we're increasing volume manually, reset muted state

                    if (!is.empty(value) && this.muted && volume > 0) {
                        this.muted = false;
                    }
                }
                /**
                 * Get the current player volume
                 */
                ,
                get: function get() {
                    return Number(this.media.volume);
                }
            }, {
                key: "muted",
                set: function set(mute) {
                    var toggle = mute; // Load muted state from storage

                    if (!is.boolean(toggle)) {
                        toggle = this.storage.get('muted');
                    } // Use config if all else fails


                    if (!is.boolean(toggle)) {
                        toggle = this.config.muted;
                    } // Update config


                    this.config.muted = toggle; // Set mute on the player

                    this.media.muted = toggle;
                }
                /**
                 * Get current muted state
                 */
                ,
                get: function get() {
                    return Boolean(this.media.muted);
                }
                /**
                 * Check if the media has audio
                 */

            }, {
                key: "hasAudio",
                get: function get() {
                    // Assume yes for all non HTML5 (as we can't tell...)
                    if (!this.isHTML5) {
                        return true;
                    }

                    if (this.isAudio) {
                        return true;
                    } // Get audio tracks


                    return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
                }
                /**
                 * Set playback speed
                 * @param {number} speed - the speed of playback (0.5-2.0)
                 */

            }, {
                key: "speed",
                set: function set(input) {
                    var speed = null;

                    if (is.number(input)) {
                        speed = input;
                    }

                    if (!is.number(speed)) {
                        speed = this.storage.get('speed');
                    }

                    if (!is.number(speed)) {
                        speed = this.config.speed.selected;
                    } // Set min/max


                    if (speed < 0.1) {
                        speed = 0.1;
                    }

                    if (speed > 2.0) {
                        speed = 2.0;
                    }

                    if (!this.config.speed.options.includes(speed)) {
                        this.debug.warn("Unsupported speed (".concat(speed, ")"));
                        return;
                    } // Update config


                    this.config.speed.selected = speed; // Set media speed

                    this.media.playbackRate = speed;
                }
                /**
                 * Get current playback speed
                 */
                ,
                get: function get() {
                    return Number(this.media.playbackRate);
                }
                /**
                 * Set playback quality
                 * Currently HTML5 & YouTube only
                 * @param {number} input - Quality level
                 */

            }, {
                key: "quality",
                set: function set(input) {
                    var config = this.config.quality;
                    var options = this.options.quality;

                    if (!options.length) {
                        return;
                    }

                    var quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);
                    var updateStorage = true;

                    if (!options.includes(quality)) {
                        var value = closest(options, quality);
                        this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
                        quality = value; // Don't update storage if quality is not supported

                        updateStorage = false;
                    } // Update config


                    config.selected = quality; // Set quality

                    this.media.quality = quality; // Save to storage

                    if (updateStorage) {
                        this.storage.set({
                            quality: quality
                        });
                    }
                }
                /**
                 * Get current quality level
                 */
                ,
                get: function get() {
                    return this.media.quality;
                }
                /**
                 * Toggle loop
                 * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
                 * @param {boolean} input - Whether to loop or not
                 */

            }, {
                key: "loop",
                set: function set(input) {
                    var toggle = is.boolean(input) ? input : this.config.loop.active;
                    this.config.loop.active = toggle;
                    this.media.loop = toggle; // Set default to be a true toggle

                    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
         switch (type) {
            case 'start':
                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
                    this.config.loop.end = null;
                }
                this.config.loop.start = this.currentTime;
                // this.config.loop.indicator.start = this.elements.display.played.value;
                break;
             case 'end':
                if (this.config.loop.start >= this.currentTime) {
                    return this;
                }
                this.config.loop.end = this.currentTime;
                // this.config.loop.indicator.end = this.elements.display.played.value;
                break;
             case 'all':
                this.config.loop.start = 0;
                this.config.loop.end = this.duration - 2;
                this.config.loop.indicator.start = 0;
                this.config.loop.indicator.end = 100;
                break;
             case 'toggle':
                if (this.config.loop.active) {
                    this.config.loop.start = 0;
                    this.config.loop.end = null;
                } else {
                    this.config.loop.start = 0;
                    this.config.loop.end = this.duration - 2;
                }
                break;
             default:
                this.config.loop.start = 0;
                this.config.loop.end = null;
                break;
        } */
                }
                /**
                 * Get current loop state
                 */
                ,
                get: function get() {
                    return Boolean(this.media.loop);
                }
                /**
                 * Set new media source
                 * @param {object} input - The new source object (see docs)
                 */

            }, {
                key: "source",
                set: function set(input) {
                    source.change.call(this, input);
                }
                /**
                 * Get current source
                 */
                ,
                get: function get() {
                    return this.media.currentSrc;
                }
                /**
                 * Get a download URL (either source or custom)
                 */

            }, {
                key: "download",
                get: function get() {
                    var download = this.config.urls.download;
                    return is.url(download) ? download : this.source;
                }
                /**
                 * Set the poster image for a video
                 * @param {input} - the URL for the new poster image
                 */

            }, {
                key: "poster",
                set: function set(input) {
                    if (!this.isVideo) {
                        this.debug.warn('Poster can only be set for video');
                        return;
                    }

                    ui.setPoster.call(this, input, false).catch(function () {});
                }
                /**
                 * Get the current poster image
                 */
                ,
                get: function get() {
                    if (!this.isVideo) {
                        return null;
                    }

                    return this.media.getAttribute('poster');
                }
                /**
                 * Set the autoplay state
                 * @param {boolean} input - Whether to autoplay or not
                 */

            }, {
                key: "autoplay",
                set: function set(input) {
                    var toggle = is.boolean(input) ? input : this.config.autoplay;
                    this.config.autoplay = toggle;
                }
                /**
                 * Get the current autoplay state
                 */
                ,
                get: function get() {
                    return Boolean(this.config.autoplay);
                }
            }, {
                key: "currentTrack",
                set: function set(input) {
                    captions.set.call(this, input, false);
                }
                /**
                 * Get the current caption track index (-1 if disabled)
                 */
                ,
                get: function get() {
                    var _this$captions = this.captions,
                        toggled = _this$captions.toggled,
                        currentTrack = _this$captions.currentTrack;
                    return toggled ? currentTrack : -1;
                }
                /**
                 * Set the wanted language for captions
                 * Since tracks can be added later it won't update the actual caption track until there is a matching track
                 * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
                 */

            }, {
                key: "language",
                set: function set(input) {
                    captions.setLanguage.call(this, input, false);
                }
                /**
                 * Get the current track's language
                 */
                ,
                get: function get() {
                    return (captions.getCurrentTrack.call(this) || {}).language;
                }
                /**
                 * Toggle picture-in-picture playback on WebKit/MacOS
                 * TODO: update player with state, support, enabled
                 * TODO: detect outside changes
                 */

            }, {
                key: "pip",
                set: function set(input) {
                    // Bail if no support
                    if (!support.pip) {
                        return;
                    } // Toggle based on current state if not passed


                    var toggle = is.boolean(input) ? input : !this.pip; // Toggle based on current state
                    // Safari

                    if (is.function(this.media.webkitSetPresentationMode)) {
                        this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
                    } // Chrome


                    if (is.function(this.media.requestPictureInPicture)) {
                        if (!this.pip && toggle) {
                            this.media.requestPictureInPicture();
                        } else if (this.pip && !toggle) {
                            document.exitPictureInPicture();
                        }
                    }
                }
                /**
                 * Get the current picture-in-picture state
                 */
                ,
                get: function get() {
                    if (!support.pip) {
                        return null;
                    } // Safari


                    if (!is.empty(this.media.webkitPresentationMode)) {
                        return this.media.webkitPresentationMode === pip.active;
                    } // Chrome


                    return this.media === document.pictureInPictureElement;
                }
            }], [{
                key: "supported",
                value: function supported(type, provider, inline) {
                    return support.check(type, provider, inline);
                }
                /**
                 * Load an SVG sprite into the page
                 * @param {string} url - URL for the SVG sprite
                 * @param {string} [id] - Unique ID
                 */

            }, {
                key: "loadSprite",
                value: function loadSprite$$1(url, id) {
                    return loadSprite(url, id);
                }
                /**
                 * Setup multiple instances
                 * @param {*} selector
                 * @param {object} options
                 */

            }, {
                key: "setup",
                value: function setup(selector) {
                    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
                    var targets = null;

                    if (is.string(selector)) {
                        targets = Array.from(document.querySelectorAll(selector));
                    } else if (is.nodeList(selector)) {
                        targets = Array.from(selector);
                    } else if (is.array(selector)) {
                        targets = selector.filter(is.element);
                    }

                    if (is.empty(targets)) {
                        return null;
                    }

                    return targets.map(function (t) {
                        return new Plyr(t, options);
                    });
                }
            }]);

            return Plyr;
        }();

    Plyr.defaults = cloneDeep(defaults);

    return Plyr;

})));
