diff options
Diffstat (limited to 'dist/plyr.js')
-rw-r--r-- | dist/plyr.js | 13490 |
1 files changed, 6333 insertions, 7157 deletions
diff --git a/dist/plyr.js b/dist/plyr.js index 58de6f96..57702183 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -1,8440 +1,7616 @@ 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()); + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define('Plyr', factory) : + (global.Plyr = factory()); }(this, (function () { 'use strict'; - // ========================================================================== - // 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; - } - - // Add the protocol if required - var string = input; - if (!input.startsWith('http://') || !input.startsWith('https://')) { - string = 'http://' + 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 - }; - - // ========================================================================== - - // Check for passive event listener support - // 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 toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; - - var _this = this; - - 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); - }); + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); } - - // Bind event handler - function on(element) { - var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; - var callback = arguments[2]; - 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); + } + + 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); } - - // Unbind event handler - function off(element) { - var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; - var callback = arguments[2]; - 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); + } + + 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; } - // Bind once-only event handler - function once(element) { - var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; - var callback = arguments[2]; - var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; - var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + return obj; + } - function onceCallback() { - off(element, events, onceCallback, passive, capture); + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } - for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); + } - callback.apply(this, args); - } + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; - toggleListener.call(this, element, events, onceCallback, true, passive, capture); + return arr2; } + } - // 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 - }) - }); + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } - // Dispatch the event - element.dispatchEvent(event); - } + function _iterableToArray(iter) { + if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); + } - // 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; + function _iterableToArrayLimit(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; - element.removeEventListener(type, callback, options); - }); + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); - this.eventListeners = []; - } + 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; + } } - // Run method when / if player is ready - function ready() { - var _this2 = this; + return _arr; + } - return new Promise(function (resolve) { - return _this2.ready ? setTimeout(resolve, 0) : on.call(_this2, _this2.elements.container, 'ready', resolve); - }).then(function () {}); - } + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance"); + } - var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } - var createClass = 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); - } - } + // ========================================================================== + // Type checking utils + // ========================================================================== + var getConstructor = function getConstructor(input) { + return input !== null && typeof input !== 'undefined' ? input.constructor : null; + }; - return function (Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); + var instanceOf = function instanceOf(input, constructor) { + return Boolean(input && constructor && input instanceof constructor); + }; - var defineProperty = function (obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } + var isNullOrUndefined = function isNullOrUndefined(input) { + return input === null || typeof input === 'undefined'; + }; - return obj; - }; + var isObject = function isObject(input) { + return getConstructor(input) === Object; + }; - var slicedToArray = function () { - function sliceIterator(arr, i) { - var _arr = []; - var _n = true; - var _d = false; - var _e = undefined; + var isNumber = function isNumber(input) { + return getConstructor(input) === Number && !Number.isNaN(input); + }; - try { - for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { - _arr.push(_s.value); + var isString = function isString(input) { + return getConstructor(input) === String; + }; - if (i && _arr.length === i) break; - } - } catch (err) { - _d = true; - _e = err; - } finally { - try { - if (!_n && _i["return"]) _i["return"](); - } finally { - if (_d) throw _e; - } - } + var isBoolean = function isBoolean(input) { + return getConstructor(input) === Boolean; + }; - return _arr; - } + var isFunction = function isFunction(input) { + return getConstructor(input) === Function; + }; - return function (arr, i) { - if (Array.isArray(arr)) { - return arr; - } else if (Symbol.iterator in Object(arr)) { - return sliceIterator(arr, i); - } else { - throw new TypeError("Invalid attempt to destructure non-iterable instance"); - } - }; - }(); + var isArray = function isArray(input) { + return Array.isArray(input); + }; - var toConsumableArray = function (arr) { - if (Array.isArray(arr)) { - for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + var isWeakMap = function isWeakMap(input) { + return instanceOf(input, WeakMap); + }; - return arr2; - } else { - return Array.from(arr); - } - }; + var isNodeList = function isNodeList(input) { + return instanceOf(input, NodeList); + }; - // ========================================================================== + var isElement = function isElement(input) { + return instanceOf(input, Element); + }; - // Wrap an element - function wrap(elements, wrapper) { - // Convert `elements` to an array, if necessary. - var targets = elements.length ? elements : [elements]; + var isTextNode = function isTextNode(input) { + return getConstructor(input) === Text; + }; - // 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; + var isEvent = function isEvent(input) { + return instanceOf(input, Event); + }; - // Cache the current parent and sibling. - var parent = element.parentNode; - var sibling = element.nextSibling; + var isKeyboardEvent = function isKeyboardEvent(input) { + return instanceOf(input, KeyboardEvent); + }; - // Wrap the element (is automatically removed from its current - // parent). - child.appendChild(element); + var isCue = function isCue(input) { + return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); + }; - // 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); - } - }); - } + var isTrack = function isTrack(input) { + return instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind); + }; - // Set attributes - function setAttributes(element, attributes) { - if (!is.element(element) || is.empty(attributes)) { - return; - } + var isEmpty = function isEmpty(input) { + return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length; + }; - // 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]; + var isUrl = function isUrl(input) { + // Accept a URL object + if (instanceOf(input, window.URL)) { + return true; + } // Add the protocol if required - return !is.nullOrUndefined(value); - }).forEach(function (_ref3) { - var _ref4 = slicedToArray(_ref3, 2), - key = _ref4[0], - value = _ref4[1]; - return element.setAttribute(key, value); - }); - } + var string = input; - // Create a DocumentFragment - function createElement(type, attributes, text) { - // Create a new <element> - var element = document.createElement(type); + if (!input.startsWith('http://') || !input.startsWith('https://')) { + string = "http://".concat(input); + } - // Set all passed attributes - if (is.object(attributes)) { - setAttributes(element, attributes); + 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 + } - // Add text node - if (is.string(text)) { - element.innerText = text; - } + return supported; + }(); // Toggle event listener - // Return built element - return element; - } - // Inaert an element after another - function insertAfter(element, target) { - if (!is.element(element) || !is.element(target)) { - return; - } + function toggleListener(element, event, callback) { + var _this = this; - target.parentNode.insertBefore(element, target.nextSibling); - } + 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; - // Insert a DocumentFragment - function insertElement(type, parent, attributes, text) { - if (!is.element(parent)) { - return; - } + // Bail if no element, event, or callback + if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { + return; + } // Allow multiple events - 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; - } + var events = event.split(' '); // Build options + // Default to just the capture boolean for browsers with no passive listener support - if (!is.element(element) || !is.element(element.parentNode)) { - return; - } + var options = capture; // If passive events listeners are supported - element.parentNode.removeChild(element); - } + 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 - // Remove all child elements - function emptyElement(element) { - if (!is.element(element)) { - return; - } - var length = element.childNodes.length; + 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]; + } - while (length > 0) { - element.removeChild(element.lastChild); - length -= 1; - } + callback.apply(this, args); } - // Replace element - function replaceElement(newChild, oldChild) { - if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { - return null; - } + 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 - oldChild.parentNode.replaceChild(newChild, oldChild); + function ready() { + var _this2 = this; - return newChild; - } + return new Promise(function (resolve) { + return _this2.ready ? setTimeout(resolve, 0) : on.call(_this2, _this2.elements.container, 'ready', resolve); + }).then(function () {}); + } - // 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' } + 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). - if (!is.string(sel) || is.empty(sel)) { - return {}; - } + Array.from(targets).reverse().forEach(function (element, index) { + var child = index > 0 ? wrapper.cloneNode(true) : wrapper; // Cache the current parent and sibling. - var attributes = {}; - var existing = existingAttributes; + var parent = element.parentNode; + var sibling = element.nextSibling; // Wrap the element (is automatically removed from its current + // parent). - sel.split(',').forEach(function (s) { - // Remove whitespace - var selector = s.trim(); - var className = selector.replace('.', ''); - var stripped = selector.replace(/[[\]]/g, ''); + 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. - // Get the parts and value - var parts = stripped.split('='); - var key = parts[0]; - var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; + if (sibling) { + parent.insertBefore(child, sibling); + } else { + parent.appendChild(child); + } + }); + } // Set attributes - // Get the first character - var start = selector.charAt(0); + 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" - switch (start) { - case '.': - // Add to existing classname - if (is.object(existing) && is.string(existing.class)) { - existing.class += ' ' + className; - } - attributes.class = className; - break; + Object.entries(attributes).filter(function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + value = _ref2[1]; - case '#': - // ID selector - attributes.id = selector.replace('#', ''); - break; + return !is.nullOrUndefined(value); + }).forEach(function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + key = _ref4[0], + value = _ref4[1]; - case '[': - // Attribute selector - attributes[key] = value; + return element.setAttribute(key, value); + }); + } // Create a DocumentFragment - break; + function createElement(type, attributes, text) { + // Create a new <element> + var element = document.createElement(type); // Set all passed attributes - default: - break; - } - }); + if (is.object(attributes)) { + setAttributes(element, attributes); + } // Add text node - return attributes; - } - // Toggle hidden - function toggleHidden(element, hidden) { - if (!is.element(element)) { - return; - } + if (is.string(text)) { + element.innerText = text; + } // Return built element - var hide = hidden; - if (!is.boolean(hide)) { - hide = !element.hidden; - } + return element; + } // Inaert an element after another - if (hide) { - element.setAttribute('hidden', ''); - } else { - element.removeAttribute('hidden'); - } + function insertAfter(element, target) { + if (!is.element(element) || !is.element(target)) { + return; } - // 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); - }); - } + target.parentNode.insertBefore(element, target.nextSibling); + } // Insert a DocumentFragment - if (is.element(element)) { - var method = 'toggle'; - if (typeof force !== 'undefined') { - method = force ? 'add' : 'remove'; - } + function insertElement(type, parent, attributes, text) { + if (!is.element(parent)) { + return; + } - element.classList[method](className); - return element.classList.contains(className); - } + parent.appendChild(createElement(type, attributes, text)); + } // Remove element(s) - return false; + function removeElement(element) { + if (is.nodeList(element) || is.array(element)) { + Array.from(element).forEach(removeElement); + return; } - // Has class name - function hasClass(element, className) { - return is.element(element) && element.classList.contains(className); + if (!is.element(element) || !is.element(element.parentNode)) { + return; } - // Element matches selector - function matches(element, selector) { - var prototype = { Element: Element }; + element.parentNode.removeChild(element); + } // Remove all child elements - function match() { - return Array.from(document.querySelectorAll(selector)).includes(this); - } + function emptyElement(element) { + if (!is.element(element)) { + return; + } - var matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + var length = element.childNodes.length; - return matches.call(element, selector); + while (length > 0) { + element.removeChild(element.lastChild); + length -= 1; } + } // Replace element - // Find all elements - function getElements(selector) { - return this.elements.container.querySelectorAll(selector); + function replaceElement(newChild, oldChild) { + if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) { + return null; } - // Find a single element - function getElement(selector) { - return this.elements.container.querySelector(selector); + 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 {}; } - // 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; + 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); + } - if (!is.element(element)) { - return; - } + attributes.class = className; + break; - var focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); - var first = focusable[0]; - var last = focusable[focusable.length - 1]; + case '#': + // ID selector + attributes.id = selector.replace('#', ''); + break; - var trap = function trap(event) { - // Bail if not tab key or not fullscreen - if (event.key !== 'Tab' || event.keyCode !== 9) { - return; - } + case '[': + // Attribute selector + attributes[key] = value; + break; - // 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(); - } - }; + default: + break; + } + }); + return attributes; + } // Toggle hidden - toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); + function toggleHidden(element, hidden) { + if (!is.element(element)) { + return; } - // 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; + var hide = hidden; - if (!is.element(element)) { - return; - } + if (!is.boolean(hide)) { + hide = !element.hidden; + } - // Set regular focus - element.focus(); + if (hide) { + element.setAttribute('hidden', ''); + } else { + element.removeAttribute('hidden'); + } + } // Mirror Element.classList.toggle, with IE compatibility for "force" argument - // If we want to mimic keyboard focus via tab - if (tabFocus) { - toggleClass(element, this.config.classNames.tabFocus); - } + 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'; - var transitionEndEvent = function () { - var element = document.createElement('span'); + if (typeof force !== 'undefined') { + method = force ? 'add' : 'remove'; + } - var events = { - WebkitTransition: 'webkitTransitionEnd', - MozTransition: 'transitionend', - OTransition: 'oTransitionEnd otransitionend', - transition: 'transitionend' - }; + element.classList[method](className); + return element.classList.contains(className); + } - var type = Object.keys(events).find(function (event) { - return element.style[event] !== undefined; - }); + return false; + } // Has class name - return is.string(type) ? events[type] : false; - }(); + function hasClass(element, className) { + return is.element(element) && element.classList.contains(className); + } // Element matches selector - // 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); + function matches(element, selector) { + var prototype = { + Element: Element + }; + + function match() { + return Array.from(document.querySelectorAll(selector)).includes(this); } - // ========================================================================== - // Browser sniffing - // Unfortunately, due to mixed support, UA sniffing is required - // ========================================================================== + var matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; + return matches.call(element, selector); + } // Find all elements - 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) - }; + 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 - // Default codecs for checking mimetype support - var defaultCodecs = { - 'audio/ogg': 'vorbis', - 'audio/wav': '1', - 'video/webm': 'vp8, vorbis', - 'video/mp4': 'avc1.42E01E, mp4a.40.2', - 'video/ogg': 'theora' - }; + function trapFocus() { + var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - // 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 - }; - }, + 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]; - // Picture-in-picture support - // Safari only currently - pip: function () { - return !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode); - }(), + 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 - // 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'), + var focused = document.activeElement; - // 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]; + 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(); + } + }; - if (!this.isHTML5 || mediaType !== this.type) { - return false; - } + toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false); + } // Set focus and tab focus class - var type = void 0; - if (inputType && inputType.includes('codecs=')) { - // Use input directly - type = inputType; - } else if (inputType === 'audio/mpeg') { - // Skip codec - type = 'audio/mpeg;'; - } else if (inputType in defaultCodecs) { - // Use codec - type = inputType + '; codecs="' + defaultCodecs[inputType] + '"'; - } + function setFocus() { + var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - try { - return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); - } catch (err) { - return false; - } - }, + if (!is.element(element)) { + return; + } // Set regular focus - // Check for textTracks support - textTracks: 'textTracks' in document.createElement('video'), + element.focus(); // If we want to mimic keyboard focus via tab - // <input type="range"> Sliders - rangeInput: function () { - var range = document.createElement('input'); - range.type = 'range'; - return range.type === 'range'; - }(), + 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 - // Touch - // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event - touch: 'ontouchstart' in document.documentElement, + function repaint(element) { + setTimeout(function () { + try { + toggleHidden(element, true); + element.offsetHeight; // eslint-disable-line - // Detect transitions support - transitions: transitionEndEvent !== false, + 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 only currently + pip: function () { + return !browser.isIPhone && is.function(createElement('video').webkitSetPresentationMode); + }(), + // 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]; + + if (!this.isHTML5 || mediaType !== this.type) { + return 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 type; + + if (inputType && inputType.includes('codecs=')) { + // Use input directly + type = inputType; + } else if (inputType === 'audio/mpeg') { + // Skip codec + type = 'audio/mpeg;'; + } else if (inputType in defaultCodecs) { + // Use codec + type = "".concat(inputType, "; codecs=\"").concat(defaultCodecs[inputType], "\""); + } - // ========================================================================== + try { + return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); + } catch (err) { + 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 html5 = { - getSources: function getSources() { - var _this = this; + var sources = Array.from(this.media.querySelectorAll('source')); // Filter out unsupported sources - if (!this.isHTML5) { - return []; - } + 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 sources = Array.from(this.media.querySelectorAll('source')); + var player = this; // Quality - // Filter out unsupported sources - return sources.filter(function (source) { - return support.mime.call(_this, source.getAttribute('type')); - }); + 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 - // 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; - } + if (!source) { + return; + } // Get current state - 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 - }); - - // Save to storage - player.storage.set({ quality: input }); - } - }); - }, + var _player$media = player.media, + currentTime = _player$media.currentTime, + paused = _player$media.paused, + preload = _player$media.preload, + readyState = _player$media.readyState; // Set new source - // Cancel current network requests - // See https://github.com/sampotts/plyr/issues/174 - cancelRequests: function cancelRequests() { - if (!this.isHTML5) { - return; - } + player.media.src = source.getAttribute('src'); // Prevent loading if preload="none" and the current source isn't loaded (#1044) - // Remove child sources - removeElement(html5.getSources.call(this)); + if (preload !== 'none' || readyState) { + // Restore time + player.once('loadedmetadata', function () { + player.currentTime = currentTime; // Resume playing - // 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); + if (!paused) { + player.play(); + } + }); // Load new source - // Load the new empty source - // This will cancel existing requests - // See https://github.com/sampotts/plyr/issues/174 - this.media.load(); + player.media.load(); + } // Trigger change event - // Debugging - this.debug.log('Cancelled network requests'); - } - }; - // ========================================================================== + triggerEvent.call(player, player.media, 'qualitychange', false, { + quality: input + }); // Save to storage - // Remove duplicates in an array - function dedupe(array) { - if (!is.array(array)) { - return array; + player.storage.set({ + quality: input + }); } + }); + }, + // Cancel current network requests + // See https://github.com/sampotts/plyr/issues/174 + cancelRequests: function cancelRequests() { + if (!this.isHTML5) { + return; + } // Remove child sources - 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; - } + 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 - return array.reduce(function (prev, curr) { - return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; - }); - } + 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 - // Clone nested objects - function cloneDeep(object) { - return JSON.parse(JSON.stringify(object)); + this.debug.log('Cancelled network requests'); } + }; + + // ========================================================================== - // Get a nested value in an object - function getDeep(object, path) { - return path.split('.').reduce(function (obj, key) { - return obj && obj[key]; - }, object); + function dedupe(array) { + if (!is.array(array)) { + return array; } - // Deep extend destination object with N more objects - function extend() { - var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + return array.filter(function (item, index) { + return array.indexOf(item) === index; + }); + } // Get the closest value in an array - for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - sources[_key - 1] = arguments[_key]; - } + function closest(array, value) { + if (!is.array(array) || !array.length) { + return null; + } - if (!sources.length) { - return target; - } + return array.reduce(function (prev, curr) { + return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; + }); + } - var source = sources.shift(); + function cloneDeep(object) { + return JSON.parse(JSON.stringify(object)); + } // Get a nested value in an object - if (!is.object(source)) { - return target; - } + function getDeep(object, path) { + return path.split('.').reduce(function (obj, key) { + return obj && obj[key]; + }, object); + } // Deep extend destination object with N more objects - Object.keys(source).forEach(function (key) { - if (is.object(source[key])) { - if (!Object.keys(target).includes(key)) { - Object.assign(target, defineProperty({}, key, {})); - } + function extend() { + var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - extend(target[key], source[key]); - } else { - Object.assign(target, defineProperty({}, key, source[key])); - } - }); + for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + sources[_key - 1] = arguments[_key]; + } - return extend.apply(undefined, [target].concat(sources)); + if (!sources.length) { + return target; } - // ========================================================================== + var source = sources.shift(); - // Generate a random ID - function generateId(prefix) { - return prefix + '-' + Math.floor(Math.random() * 10000); + if (!is.object(source)) { + return target; } - // Format string - function format(input) { - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; + Object.keys(source).forEach(function (key) { + if (is.object(source[key])) { + if (!Object.keys(target).includes(key)) { + Object.assign(target, _defineProperty({}, key, {})); } - if (is.empty(input)) { - return input; - } + extend(target[key], source[key]); + } else { + Object.assign(target, _defineProperty({}, key, source[key])); + } + }); + return extend.apply(void 0, [target].concat(sources)); + } - 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; - } + function generateId(prefix) { + return "".concat(prefix, "-").concat(Math.floor(Math.random() * 10000)); + } // Format string - return (current / max * 100).toFixed(2); + 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]; } - // 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()); + if (is.empty(input)) { + return input; } - // Convert to title case - function toTitleCase() { - var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + return input.toString().replace(/{(\d+)}/g, function (match, i) { + return args[i].toString(); + }); + } // Get percentage - return input.toString().replace(/\w\S*/g, function (text) { - return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase(); - }); + function getPercentage(current, max) { + if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { + return 0; } - // Convert string to pascalCase - function toPascalCase() { - var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - - var string = input.toString(); + return (current / max * 100).toFixed(2); + } // Replace all occurances of a string in a string - // Convert kebab case - string = replaceAll(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 - // Convert snake case - string = replaceAll(string, '_', ' '); + 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 - // Convert to title case - string = toTitleCase(string); + function toPascalCase() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var string = input.toString(); // Convert kebab case - // Convert to pascal case - return replaceAll(string, ' ', ''); - } + string = replaceAll(string, '-', ' '); // Convert snake case - // Convert string to pascalCase - function toCamelCase() { - var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + string = replaceAll(string, '_', ' '); // Convert to title case - var string = input.toString(); + string = toTitleCase(string); // Convert to pascal case - // Convert to pascal case - string = toPascalCase(string); + return replaceAll(string, ' ', ''); + } // Convert string to pascalCase - // Convert first character to lowercase - return string.charAt(0).toLowerCase() + string.slice(1); - } + function toCamelCase() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var string = input.toString(); // Convert to pascal case - // 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; - } + string = toPascalCase(string); // Convert first character to lowercase - // Like outerHTML, but also works for DocumentFragment - function getHTML(element) { - var wrapper = document.createElement('div'); - wrapper.appendChild(element); - return wrapper.innerHTML; - } + 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 - var i18n = { - get: function get$$1() { - var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + function getHTML(element) { + var wrapper = document.createElement('div'); + wrapper.appendChild(element); + return wrapper.innerHTML; + } - if (is.empty(key) || is.empty(config)) { - return ''; - } + 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] : {}; - var string = getDeep(config.i18n, key); + if (is.empty(key) || is.empty(config)) { + return ''; + } - if (is.empty(string)) { - return ''; - } + var string = getDeep(config.i18n, key); - var replace = { - '{seektime}': config.seekTime, - '{title}': config.title - }; + if (is.empty(string)) { + return ''; + } - Object.entries(replace).forEach(function (_ref) { - var _ref2 = slicedToArray(_ref, 2), - key = _ref2[0], - value = _ref2[1]; + 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); - }); + string = replaceAll(string, key, value); + }); + return string; + } + }; - 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) - var Storage = function () { - function Storage(player) { - classCallCheck(this, Storage); - this.enabled = player.config.storage.enabled; - this.key = player.config.storage.key; + _createClass(Storage, [{ + key: "get", + value: function get(key) { + if (!Storage.supported || !this.enabled) { + return null; } - // Check for actual support (see if we can use it) + var store = window.localStorage.getItem(this.key); + if (is.empty(store)) { + return null; + } - createClass(Storage, [{ - key: 'get', - value: function get$$1(key) { - if (!Storage.supported || !this.enabled) { - return null; - } - - var store = window.localStorage.getItem(this.key); + 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.empty(store)) { - return null; - } - var json = JSON.parse(store); + if (!is.object(object)) { + return; + } // Get current storage - return is.string(key) && key.length ? json[key] : json; - } - }, { - key: 'set', - value: function set$$1(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; - } + var storage = this.get(); // Default to empty object - // Get current storage - var storage = this.get(); + if (is.empty(storage)) { + storage = {}; + } // Update the working copy of the values - // Default to empty object - if (is.empty(storage)) { - storage = {}; - } - // Update the working copy of the values - extend(storage, object); + extend(storage, object); // Update storage - // Update storage - window.localStorage.setItem(this.key, JSON.stringify(storage)); - } - }], [{ - key: 'supported', - get: function get$$1() { - 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; - }(); + window.localStorage.setItem(this.key, JSON.stringify(storage)); + } + }], [{ + key: "supported", + get: function get() { + try { + if (!('localStorage' in window)) { + return false; + } - // ========================================================================== - // Fetch wrapper - // Using XHR to avoid issues with older browsers - // ========================================================================== + 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 - function fetch(url) { - var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text'; + 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; + } - return new Promise(function (resolve, reject) { + request.addEventListener('load', function () { + if (responseType === 'text') { 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(); + resolve(JSON.parse(request.responseText)); } catch (e) { - reject(e); + resolve(request.responseText); } + } else { + resolve(request.response); + } }); - } - - // ========================================================================== - - // Load an external SVG sprite - function loadSprite(url, id) { - if (!is.string(url)) { - return; - } - - var prefix = 'cache'; - var hasId = is.string(id); - var isCached = false; + request.addEventListener('error', function () { + throw new Error(request.status); + }); + request.open('GET', url, true); // Set the required response type - var exists = function exists() { - return document.getElementById(id) !== null; - }; + request.responseType = responseType; + request.send(); + } catch (e) { + reject(e); + } + }); + } - var update = function update(container, data) { - container.innerHTML = data; + // ========================================================================== - // Check again incase of race condition - if (hasId && exists()) { - return; - } + function loadSprite(url, id) { + if (!is.string(url)) { + return; + } - // Inject the SVG to the body - document.body.insertAdjacentElement('afterbegin', container); - }; + var prefix = 'cache'; + var hasId = is.string(id); + var isCached = false; - // Only load once if ID set - if (!hasId || !exists()) { - var useStorage = Storage.supported; + var exists = function exists() { + return document.getElementById(id) !== null; + }; - // Create container - var container = document.createElement('div'); - container.setAttribute('hidden', ''); + var update = function update(container, data) { + container.innerHTML = data; // Check again incase of race condition - if (hasId) { - container.setAttribute('id', id); - } + if (hasId && exists()) { + return; + } // Inject the SVG to the body - // Check in cache - if (useStorage) { - var cached = window.localStorage.getItem(prefix + '-' + id); - isCached = cached !== null; - if (isCached) { - var data = JSON.parse(cached); - update(container, data.content); - } - } + document.body.insertAdjacentElement('afterbegin', container); + }; // Only load once if ID set - // Get the sprite - fetch(url).then(function (result) { - if (is.empty(result)) { - return; - } - if (useStorage) { - window.localStorage.setItem(prefix + '-' + id, JSON.stringify({ - content: result - })); - } + if (!hasId || !exists()) { + var useStorage = Storage.supported; // Create container - update(container, result); - }).catch(function () {}); - } - } + var container = document.createElement('div'); + container.setAttribute('hidden', ''); - // ========================================================================== + if (hasId) { + container.setAttribute('id', id); + } // Check in cache - // Time helpers - 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; + if (useStorage) { + var cached = window.localStorage.getItem("".concat(prefix, "-").concat(id)); + isCached = cached !== null; - // Bail if the value isn't a number - if (!is.number(time)) { - return formatTime(null, displayHours, inverted); + if (isCached) { + var data = JSON.parse(cached); + update(container, data.content); } + } // Get the sprite - // Format time component to add leading zero - var format = function format(value) { - return ('0' + value).slice(-2); - }; - // Breakdown to hours, mins, secs - var hours = getHours(time); - var mins = getMinutes(time); - var secs = getSeconds(time); + fetch(url).then(function (result) { + if (is.empty(result)) { + return; + } - // Do we need to display hours? - if (displayHours || hours > 0) { - hours = hours + ':'; - } else { - hours = ''; + if (useStorage) { + window.localStorage.setItem("".concat(prefix, "-").concat(id), JSON.stringify({ + content: result + })); } - // Render - return '' + (inverted && time > 0 ? '-' : '') + hours + format(mins) + ':' + format(secs); + 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)); + } - // ========================================================================== - - // TODO: Don't export a massive object - break down and create class - 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('.' + 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 = (!iconUrl.cors ? iconUrl.url : '') + '#' + 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 = iconPath + '-' + 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); - } else { - 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(type) { - var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - // Skip i18n for abbreviations and brand names - var universals = { - pip: 'PIP', - airplay: 'AirPlay' - }; - var text = universals[type] || i18n.get(type, 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 - }); + 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 - badge.appendChild(createElement('span', { - class: this.config.classNames.menu.badge - }, text)); + 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); + } else { + 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(type) { + var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + // Skip i18n for abbreviations and brand names + var universals = { + pip: 'PIP', + airplay: 'AirPlay' + }; + var text = universals[type] || i18n.get(type, 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; + } - return badge; - }, + 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 button = createElement('button'); + var attributes = Object.assign({}, attr); + var type = toCamelCase(buttonType); + var toggle = false; + var label; + var icon; + var labelPressed; + var iconPressed; + + if (!('type' in attributes)) { + attributes.type = 'button'; + } + if ('class' in attributes) { + 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': + toggle = true; + label = 'play'; + labelPressed = 'pause'; + icon = 'play'; + iconPressed = 'pause'; + break; + + case 'mute': + toggle = true; + label = 'mute'; + labelPressed = 'unmute'; + icon = 'volume'; + iconPressed = 'muted'; + break; + + case 'captions': + toggle = true; + label = 'enableCaptions'; + labelPressed = 'disableCaptions'; + icon = 'captions-off'; + iconPressed = 'captions-on'; + break; + + case 'fullscreen': + toggle = true; + label = 'enterFullscreen'; + labelPressed = 'exitFullscreen'; + icon = 'enter-fullscreen'; + iconPressed = 'exit-fullscreen'; + break; + + case 'play-large': + attributes.class += " ".concat(this.config.classNames.control, "--overlaid"); + type = 'play'; + label = 'play'; + icon = 'play'; + break; + + default: + label = type; + icon = buttonType; + } // Setup toggle icon and labels + + + if (toggle) { + // Icon + button.appendChild(controls.createIcon.call(this, iconPressed, { + class: 'icon--pressed' + })); + button.appendChild(controls.createIcon.call(this, icon, { + class: 'icon--not-pressed' + })); // Label/Tooltip + + button.appendChild(controls.createLabel.call(this, labelPressed, { + class: 'label--pressed' + })); + button.appendChild(controls.createLabel.call(this, label, { + class: 'label--not-pressed' + })); + } else { + button.appendChild(controls.createIcon.call(this, icon)); + button.appendChild(controls.createLabel.call(this, label)); + } // Merge attributes - // Create a <button> - createButton: function createButton(buttonType, attr) { - var button = createElement('button'); - var attributes = Object.assign({}, attr); - var type = toCamelCase(buttonType); - var toggle = false; - var label = void 0; - var icon = void 0; - var labelPressed = void 0; - var iconPressed = void 0; + extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); + setAttributes(button, attributes); // We have multiple play buttons - if (!('type' in attributes)) { - attributes.type = 'button'; - } + if (type === 'play') { + if (!is.array(this.elements.buttons[type])) { + this.elements.buttons[type] = []; + } - if ('class' in attributes) { - if (!attributes.class.includes(this.config.classNames.control)) { - attributes.class += ' ' + this.config.classNames.control; - } - } else { - attributes.class = this.config.classNames.control; - } + this.elements.buttons[type].push(button); + } else { + this.elements.buttons[type] = button; + } - // Large play button - switch (buttonType) { - case 'play': - toggle = true; - label = 'play'; - labelPressed = 'pause'; - icon = 'play'; - iconPressed = 'pause'; - break; - - case 'mute': - toggle = true; - label = 'mute'; - labelPressed = 'unmute'; - icon = 'volume'; - iconPressed = 'muted'; - break; - - case 'captions': - toggle = true; - label = 'enableCaptions'; - labelPressed = 'disableCaptions'; - icon = 'captions-off'; - iconPressed = 'captions-on'; - break; - - case 'fullscreen': - toggle = true; - label = 'enterFullscreen'; - labelPressed = 'exitFullscreen'; - icon = 'enter-fullscreen'; - iconPressed = 'exit-fullscreen'; - break; - - case 'play-large': - attributes.class += ' ' + this.config.classNames.control + '--overlaid'; - toggle = true; - type = 'play'; - label = 'play'; - labelPressed = 'pause'; - icon = 'play'; - iconPressed = 'pause'; - break; - - default: - label = type; - icon = buttonType; - } + 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()); + } - // Setup toggle icon and labels - if (toggle) { - // Icon - button.appendChild(controls.createIcon.call(this, iconPressed, { - class: 'icon--pressed' - })); - button.appendChild(controls.createIcon.call(this, icon, { - class: 'icon--not-pressed' - })); - - // Label/Tooltip - button.appendChild(controls.createLabel.call(this, labelPressed, { - class: 'label--pressed' - })); - button.appendChild(controls.createLabel.call(this, label, { - class: 'label--not-pressed' - })); - } else { - button.appendChild(controls.createIcon.call(this, icon)); - button.appendChild(controls.createLabel.call(this, label)); - } + 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; + } - // Merge attributes - extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes)); + var isRadioButton = matches(menuItem, '[role="menuitemradio"]'); // Show the respective menu - setAttributes(button, attributes); + if (!isRadioButton && [32, 39].includes(event.which)) { + controls.showMenuPanel.call(_this, type, true); + } else { + var target; - // We have multiple play buttons - if (type === 'play') { - if (!is.array(this.elements.buttons[type])) { - this.elements.buttons[type] = []; - } + if (event.which !== 32) { + if (event.which === 40 || isRadioButton && event.which === 39) { + target = menuItem.nextElementSibling; - this.elements.buttons[type].push(button); + if (!is.element(target)) { + target = menuItem.parentNode.firstElementChild; + } } 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) : ''; + target = menuItem.previousElementSibling; - progress.innerText = '% ' + suffix.toLowerCase(); + if (!is.element(target)) { + target = menuItem.parentNode.lastElementChild; + } } - 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: (this.config.classNames.display.time + ' ' + (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 = void 0; - - 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; + 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 - if (!is.element(target)) { - target = menuItem.parentNode.lastElementChild; - } - } + on(menuItem, 'keyup', function (event) { + if (event.which !== 13) { + return; + } - setFocus.call(_this, target, true); - } - } - }, false); + 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); + } - // 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; - } + menuItem.appendChild(flex); // Replicate radio button behaviour - controls.focusFirstMenuItem.call(_this, null, true); - }); + Object.defineProperty(menuItem, 'checked', { + enumerable: true, + get: function get() { + return menuItem.getAttribute('aria-checked') === '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 === undefined ? null : _ref$badge, - _ref$checked = _ref.checked, - checked = _ref$checked === undefined ? false : _ref$checked; - - var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]); - - var menuItem = createElement('button', extend(attributes, { - type: 'button', - role: 'menuitemradio', - class: (this.config.classNames.control + ' ' + (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$$1() { - return menuItem.getAttribute('aria-checked') === 'true'; - }, - set: function set$$1(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'); - } + 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'); }); + } - 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); - }, + 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; - // Update <progress> elements - updateProgress: function updateProgress(event) { - var _this3 = this; + switch (type) { + case 'language': + _this2.currentTrack = Number(value); + break; - if (!this.supported.ui || !is.event(event)) { - return; - } + case 'quality': + _this2.quality = value; + break; - var value = 0; + case 'speed': + _this2.speed = parseFloat(value); + break; - var setProgress = function setProgress(target, input) { - var value = is.number(input) ? input : 0; - var progress = is.element(target) ? target : _this3.elements.display.buffer; + default: + break; + } - // Update value and label - if (is.element(progress)) { - progress.value = value; + 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; - // Update text label inside - var label = progress.getElementsByTagName('span')[0]; - if (is.element(label)) { - label.childNodes[0].nodeValue = value; - } - } - }; + if (!is.element(target)) { + return; + } // eslint-disable-next-line - 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); - } + target.value = value; // Webkit range fill - break; + controls.updateRangeFill.call(this, target); + }, + // Update <progress> elements + updateProgress: function updateProgress(event) { + var _this3 = this; - // Check buffer status - case 'playing': - case 'progress': - setProgress(this.elements.display.buffer, this.buffered * 100); + if (!this.supported.ui || !is.event(event)) { + return; + } - break; + var value = 0; - default: - break; - } - } - }, + 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 - // 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; + var label = progress.getElementsByTagName('span')[0]; - // Needs to be a valid <input type='range'> - if (!is.element(range) || range.getAttribute('type') !== 'range') { - return; - } + if (is.element(label)) { + label.childNodes[0].nodeValue = value; + } + } + }; - // 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', percent.toFixed(1) + '%'); - } else { - range.setAttribute('aria-valuenow', range.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 - // WebKit only - if (!browser.isWebkit) { - return; + if (event.type === 'timeupdate') { + controls.setRange.call(this, this.elements.inputs.seek, value); } - // Set CSS custom property - range.style.setProperty('--value', range.value / range.max * 100 + '%'); - }, - + break; + // Check buffer status - // Update hover tooltip for seeking - updateSeekTooltip: function updateSeekTooltip(event) { - var _this4 = this; + case 'playing': + case 'progress': + setProgress(this.elements.display.buffer, this.buffered * 100); + break; - // 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 = this.config.classNames.tooltip + '--visible'; - - var toggle = function toggle(_toggle) { - toggleClass(_this4.elements.display.seekTooltip, visible, _toggle); - }; + 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 - // 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; - } + if (!browser.isWebkit) { + return; + } // Set CSS custom property - // 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); + range.style.setProperty('--value', "".concat(range.value / range.max * 100, "%")); + }, + // Update hover tooltip for seeking + updateSeekTooltip: function updateSeekTooltip(event) { + var _this4 = this; - // Set position - this.elements.display.seekTooltip.style.left = percent + '%'; + // 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 - // 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'); - } - }, + var percent = 0; + var clientRect = this.elements.progress.getBoundingClientRect(); + var visible = "".concat(this.config.classNames.tooltip, "--visible"); - // 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; + var toggle = function toggle(_toggle) { + toggleClass(_this4.elements.display.seekTooltip, visible, _toggle); + }; // Hide on touch - // 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; - } + if (this.touch) { + toggle(false); + return; + } // Determine percentage, if already visible - // Playing progress - controls.updateProgress.call(this, event); - }, + 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 - // 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; - } + if (percent < 0) { + percent = 0; + } else if (percent > 100) { + percent = 100; + } // Display the time a click would seek to - // 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); + controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position - // 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); - } + 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 there's a duration element, update content - if (hasDuration) { - controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); - } + 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 - // Update the tooltip (if visible) - controls.updateSeekTooltip.call(this); - }, + 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 - // Hide/show a tab - toggleMenuButton: function toggleMenuButton(setting, toggle) { - toggleHidden(this.elements.settings.buttons[setting], !toggle); - }, + 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 - // 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]; + if (this.duration >= Math.pow(2, 32)) { + toggleHidden(this.elements.display.currentTime, true); + toggleHidden(this.elements.progress, true); + return; + } // Update ARIA values - // 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 \'' + value + '\' for ' + setting); - return; - } + if (is.element(this.elements.inputs.seek)) { + this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration); + } // If there's a spot to display duration - // Disabled value - if (!this.config[setting].options.includes(value)) { - this.debug.warn('Disabled value of \'' + value + '\' for ' + setting); - return; - } - } - // Get the list if we need to - if (!is.element(list)) { - list = pane && pane.querySelector('[role="menu"]'); - } + var hasDuration = is.element(this.elements.display.duration); // If there's only one time display, display duration there - // If there's no list it means it's not been rendered... - if (!is.element(list)) { - return; - } + 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 - // Update the label - var label = this.elements.settings.buttons[setting].querySelector('.' + 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="' + value + '"]'); + if (hasDuration) { + controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); + } // Update the tooltip (if visible) - if (is.element(target)) { - target.checked = true; - } - }, + 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; - // Translate a value into a nice label - getLabel: function getLabel(setting, value) { - switch (setting) { - case 'speed': - return value === 1 ? i18n.get('normal', this.config) : value + '×'; + if (setting === 'captions') { + value = this.currentTrack; + } else { + value = !is.empty(input) ? input : this[setting]; // Get default - case 'quality': - if (is.number(value)) { - var label = i18n.get('qualityLabel.' + value, this.config); + if (is.empty(value)) { + value = this.config[setting].default; + } // Unsupported value - if (!label.length) { - return value + 'p'; - } - return label; - } + 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 - return toTitleCase(value); - case 'captions': - return captions.getLabel.call(this); + 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 - default: - return null; - } - }, + if (!is.element(list)) { + list = pane && pane.querySelector('[role="menu"]'); + } // If there's no list it means it's not been rendered... - // Set the quality menu - setQualityMenu: function setQualityMenu(options) { - var _this5 = this; - // Menu required - if (!is.element(this.elements.settings.panels.quality)) { - return; - } + if (!is.element(list)) { + return; + } // Update the label - 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); - }); - } + 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 - // Toggle the pane and tab - var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; - controls.toggleMenuButton.call(this, type, toggle); + var target = list && list.querySelector("[value=\"".concat(value, "\"]")); - // Empty the menu - emptyElement(list); + 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, "×"); - // Check if we need to toggle the parent - controls.checkMenu.call(this); + case 'quality': + if (is.number(value)) { + var label = i18n.get("qualityLabel.".concat(value), this.config); - // If we're hiding, nothing more to do - if (!toggle) { - return; + if (!label.length) { + return "".concat(value, "p"); } - // Get the badge HTML for HD, 4K etc - var getBadge = function getBadge(quality) { - var label = i18n.get('qualityBadge.' + 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); - }, - + return label; + } - // 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); - }); - }, */ + return toTitleCase(value); - // Get current selected caption language - // TODO: rework this to user the getter in the API? + case 'captions': + return captions.getLabel.call(this); - // Set a list of available captions languages - setCaptionsMenu: function setCaptionsMenu() { - var _this6 = 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; + } - // Menu required - if (!is.element(this.elements.settings.panels.captions)) { - 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 - // 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); + if (is.array(options)) { + this.options.quality = dedupe(options).filter(function (quality) { + return _this5.config.quality.options.includes(quality); + }); + } // Toggle the pane and tab - // Toggle the pane and tab - controls.toggleMenuButton.call(this, type, toggle); - // Empty the menu - emptyElement(list); + var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; + controls.toggleMenuButton.call(this, type, toggle); // Empty the menu - // Check if we need to toggle the parent - controls.checkMenu.call(this); + emptyElement(list); // Check if we need to toggle the parent - // If there's no captions, bail - if (!toggle) { - return; - } + controls.checkMenu.call(this); // If we're hiding, nothing more to do - // 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' - }; - }); + if (!toggle) { + return; + } // Get the badge HTML for HD, 4K etc - // 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)); + var getBadge = function getBadge(quality) { + var label = i18n.get("qualityBadge.".concat(quality), _this5.config); - controls.updateSetting.call(this, type, list); - }, + if (!label.length) { + return null; + } + return controls.createBadge.call(_this5, label); + }; // Sort options by the config and then render options - // 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; - } + 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 - var type = 'speed'; - var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); + /* 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; - // 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]; - } + // Menu required + if (!is.element(this.elements.settings.panels.captions)) { + return; + } // TODO: Captions or language? Currently it's mixed - // 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); + 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 - // Empty the menu - emptyElement(list); + controls.toggleMenuButton.call(this, type, toggle); // Empty the menu - // Check if we need to toggle the parent - controls.checkMenu.call(this); + emptyElement(list); // Check if we need to toggle the parent - // If we're hiding, nothing more to do - if (!toggle) { - return; - } + controls.checkMenu.call(this); // If there's no captions, bail - // 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) - }); - }); + if (!toggle) { + return; + } // Generate options data - controls.updateSetting.call(this, type, list); - }, + 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; + } - // Check if we need to hide/show the settings menu - checkMenu: function checkMenu() { - var buttons = this.elements.settings.buttons; + var type = 'speed'; + var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options - var visible = !is.empty(buttons) && Object.values(buttons).some(function (button) { - return !button.hidden; - }); + 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 - toggleHidden(this.elements.settings.menu, !visible); - }, + this.options.speed = this.options.speed.filter(function (speed) { + return _this7.config.speed.options.includes(speed); + }); // Toggle the pane and tab - // 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; + var toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; + controls.toggleMenuButton.call(this, type, toggle); // Empty the menu - if (this.elements.settings.popup.hidden) { - return; - } + emptyElement(list); // Check if we need to toggle the parent - var target = pane; + controls.checkMenu.call(this); // If we're hiding, nothing more to do - if (!is.element(target)) { - target = Object.values(this.elements.settings.panels).find(function (pane) { - return !pane.hidden; - }); - } + if (!toggle) { + return; + } // Create items - var firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, tabFocus); - }, + 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; - // Show/hide menu - toggleMenu: function toggleMenu(input) { - var popup = this.elements.settings.popup; + if (!is.element(target)) { + target = Object.values(this.elements.settings.panels).find(function (pane) { + return !pane.hidden; + }); + } - var button = this.elements.buttons.settings; + 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 - // Menu and button are required - if (!is.element(popup) || !is.element(button)) { - return; - } - // True toggle by default - var hidden = popup.hidden; + button.setAttribute('aria-expanded', show); // Show the actual popup - var show = hidden; + toggleHidden(popup, !show); // Add class hook - 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); + toggleClass(this.elements.container, this.config.classNames.menu.open, show); // Focus the first item if key interaction - // 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; - } - } + 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; - // Set button attributes - button.setAttribute('aria-expanded', show); + 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 - // Show the actual popup - toggleHidden(popup, !show); + if (!is.element(target)) { + return; + } // Hide all other panels - // 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)); - } - }, + 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 - // 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'); + var size = controls.getMenuSize.call(this, target); // Restore auto height/width - // Append to parent so we get the "real" size - tab.parentNode.appendChild(clone); + 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 - // Get the sizes before we remove - var width = clone.scrollWidth; - var height = clone.scrollHeight; - // Remove from the DOM - removeElement(clone); + container.style.width = ''; + container.style.height = ''; // Only listen once - return { - width: width, - height: height - }; - }, + off.call(_this8, container, transitionEndEvent, restore); + }; // Listen for the transition finishing and restore auto height/width - // Show a panel in the menu - showMenuPanel: function showMenuPanel() { - var _this8 = this; + on.call(this, container, transitionEndEvent, restore); // Set dimensions to target - var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + container.style.width = "".concat(size.width, "px"); + container.style.height = "".concat(size.height, "px"); + } // Set attributes on current tab - var target = document.getElementById('plyr-settings-' + this.id + '-' + type); - // Nothing to show, bail - if (!is.element(target)) { - return; - } + toggleHidden(current, true); // Set attributes on target - // Hide all other panels - var container = target.parentNode; - var current = Array.from(container.children).find(function (node) { - return !node.hidden; - }); + toggleHidden(target, false); // Focus the first item - // 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 = current.scrollWidth + 'px'; - container.style.height = current.scrollHeight + 'px'; + controls.focusFirstMenuItem.call(this, target, tabFocus); + }, + // Build the default HTML + // TODO: Set order based on order in the config.controls array? + create: function create(data) { + var _this9 = this; - // Get potential sizes - var size = controls.getMenuSize.call(this, target); + // Create the container + var container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); // Restart button - // 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; - } + if (this.config.controls.includes('restart')) { + container.appendChild(controls.createButton.call(this, 'restart')); + } // Rewind button - // Revert back to auto - container.style.width = ''; - container.style.height = ''; - // Only listen once - off.call(_this8, container, transitionEndEvent, restore); - }; + if (this.config.controls.includes('rewind')) { + container.appendChild(controls.createButton.call(this, 'rewind')); + } // Play/Pause button - // Listen for the transition finishing and restore auto height/width - on.call(this, container, transitionEndEvent, restore); - // Set dimensions to target - container.style.width = size.width + 'px'; - container.style.height = size.height + 'px'; - } + if (this.config.controls.includes('play')) { + container.appendChild(controls.createButton.call(this, 'play')); + } // Fast forward button - // Set attributes on current tab - toggleHidden(current, true); - // Set attributes on target - toggleHidden(target, false); + if (this.config.controls.includes('fast-forward')) { + container.appendChild(controls.createButton.call(this, 'fast-forward')); + } // Progress - // Focus the first item - controls.focusFirstMenuItem.call(this, target, tabFocus); - }, + if (this.config.controls.includes('progress')) { + var progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); // Seek range slider - // Build the default HTML - // TODO: Set order based on order in the config.controls array? - create: function create(data) { - var _this9 = this; + progress.appendChild(controls.createRange.call(this, 'seek', { + id: "plyr-seek-".concat(data.id) + })); // Buffer progress - // Create the container - var container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); + progress.appendChild(controls.createProgress.call(this, 'buffer')); // TODO: Add loop display indicator + // Seek tooltip - // Restart button - if (this.config.controls.includes('restart')) { - container.appendChild(controls.createButton.call(this, 'restart')); - } + if (this.config.tooltips.seek) { + var tooltip = createElement('span', { + class: this.config.classNames.tooltip + }, '00:00'); + progress.appendChild(tooltip); + this.elements.display.seekTooltip = tooltip; + } - // Rewind button - if (this.config.controls.includes('rewind')) { - container.appendChild(controls.createButton.call(this, 'rewind')); - } + this.elements.progress = progress; + container.appendChild(this.elements.progress); + } // Media current time display - // 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')); - } + if (this.config.controls.includes('current-time')) { + container.appendChild(controls.createTime.call(this, 'currentTime')); + } // Media duration display - // 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-' + data.id - })); + if (this.config.controls.includes('duration')) { + container.appendChild(controls.createTime.call(this, 'duration')); + } // Volume controls - // Buffer progress - progress.appendChild(controls.createProgress.call(this, 'buffer')); - // TODO: Add loop display indicator + if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) { + var volume = createElement('div', { + class: 'plyr__volume' + }); // Toggle mute button - // Seek tooltip - if (this.config.tooltips.seek) { - var tooltip = createElement('span', { - class: this.config.classNames.tooltip - }, '00:00'); + if (this.config.controls.includes('mute')) { + volume.appendChild(controls.createButton.call(this, 'mute')); + } // Volume range control - progress.appendChild(tooltip); - this.elements.display.seekTooltip = tooltip; - } - this.elements.progress = progress; - container.appendChild(this.elements.progress); - } + 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 - // Media current time display - if (this.config.controls.includes('current-time')) { - container.appendChild(controls.createTime.call(this, 'currentTime')); - } + volume.appendChild(controls.createRange.call(this, 'volume', extend(attributes, { + id: "plyr-volume-".concat(data.id) + }))); + this.elements.volume = volume; + } - // Media duration display - if (this.config.controls.includes('duration')) { - container.appendChild(controls.createTime.call(this, 'duration')); - } + container.appendChild(volume); + } // Toggle captions button - // 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')); - } + if (this.config.controls.includes('captions')) { + container.appendChild(controls.createButton.call(this, 'captions')); + } // Settings button / menu - // 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-' + data.id - }))); - - this.elements.volume = volume; - } - container.appendChild(volume); - } + 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 - // Toggle captions button - if (this.config.controls.includes('captions')) { - container.appendChild(controls.createButton.call(this, 'captions')); - } + 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 - // 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-' + data.id, - 'aria-expanded': false - })); + if (this.config.controls.includes('pip') && support.pip) { + container.appendChild(controls.createButton.call(this, 'pip')); + } // Airplay button - var popup = createElement('div', { - class: 'plyr__menu__container', - id: 'plyr-settings-' + data.id, - hidden: '' - }); - var inner = createElement('div'); + if (this.config.controls.includes('airplay') && support.airplay) { + container.appendChild(controls.createButton.call(this, 'airplay')); + } // Toggle fullscreen button - var home = createElement('div', { - id: 'plyr-settings-' + data.id + '-home' - }); - // Create the menu - var menu = createElement('div', { - role: 'menu' - }); + if (this.config.controls.includes('fullscreen')) { + container.appendChild(controls.createButton.call(this, 'fullscreen')); + } // Larger overlaid play button - 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: _this9.config.classNames.control + ' ' + _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-' + data.id + '-' + type, - hidden: '' - }); - - // Back button - var backButton = createElement('button', { - type: 'button', - class: _this9.config.classNames.control + ' ' + _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); + if (this.config.controls.includes('play-large')) { + this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); + } - this.elements.settings.popup = popup; - this.elements.settings.menu = control; - } + this.elements.controls = container; // Set available quality levels - // Picture in picture button - if (this.config.controls.includes('pip') && support.pip) { - container.appendChild(controls.createButton.call(this, 'pip')); - } + if (this.isHTML5) { + controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); + } - // Airplay button - if (this.config.controls.includes('airplay') && support.airplay) { - container.appendChild(controls.createButton.call(this, 'airplay')); - } + controls.setSpeedMenu.call(this); + return container; + }, + // Insert controls + inject: function inject() { + var _this10 = this; - // Toggle fullscreen button - if (this.config.controls.includes('fullscreen')) { - container.appendChild(controls.createButton.call(this, 'fullscreen')); - } + // Sprite + if (this.config.loadSprite) { + var icon = controls.getIconUrl.call(this); // Only load external sprite using AJAX - // Larger overlaid play button - if (this.config.controls.includes('play-large')) { - this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); - } + if (icon.cors) { + loadSprite(icon.url, 'sprite-plyr'); + } + } // Create a unique ID - this.elements.controls = container; - // Set available quality levels - if (this.isHTML5) { - controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); - } + this.id = Math.floor(Math.random() * 10000); // Null by default - controls.setSpeedMenu.call(this); + var container = null; + this.elements.controls = null; // Set template properties - return container; - }, + 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) - // Insert controls - inject: function inject() { - var _this10 = this; - // Sprite - if (this.config.loadSprite) { - var icon = controls.getIconUrl.call(this); + if (!this.config.controls) { + this.config.controls = []; + } - // Only load external sprite using AJAX - if (icon.cors) { - loadSprite(icon.url, 'sprite-plyr'); - } - } + 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', - // Create a unique ID - this.id = Math.floor(Math.random() * 10000); + }); + update = false; + } // Replace props with their value - // 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; + 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]; - // If function, run it and use output - if (is.function(this.config.controls)) { - this.config.controls = this.config.controls.call(this.props); - } + result = replaceAll(result, "{".concat(key, "}"), value); + }); + return result; + }; // Update markup - // 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; + if (update) { + if (is.string(this.config.controls)) { + container = replace(container); + } else if (is.element(container)) { + container.innerHTML = replace(container.innerHTML); + } + } // Controls container - Object.entries(props).forEach(function (_ref2) { - var _ref3 = slicedToArray(_ref2, 2), - key = _ref3[0], - value = _ref3[1]; - result = replaceAll(result, '{' + key + '}', value); - }); + var target; // Inject to custom location - return result; - }; + if (is.string(this.config.selectors.controls.container)) { + target = document.querySelector(this.config.selectors.controls.container); + } // Inject into the container by default - // 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 = void 0; + if (!is.element(target)) { + target = this.elements.container; + } // Inject controls HTML (needs to be before captions, hence "afterbegin") - // 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; - } + var insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML'; + target[insertMethod]('afterbegin', container); // Find the elements if need be - // Inject controls HTML (needs to be before captions, hence "afterbegin") - var insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML'; - target[insertMethod]('afterbegin', container); + if (!is.element(this.elements.controls)) { + controls.findElements.call(this); + } // Add pressed property to buttons - // 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$$1() { - return hasClass(button, className); - }, - set: function set$$1() { - 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); + 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 - // Setup tooltips - if (this.config.tooltips.controls) { - var _config = this.config, - classNames = _config.classNames, - selectors = _config.selectors; - var selector = selectors.controls.wrapper + ' ' + selectors.labels + ' .' + 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); - }); - } - } - }; - - // ========================================================================== + 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 - /** - * 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 (window.navigator.userAgent.includes('Edge')) { + repaint(target); + } // Setup tooltips - if (safe) { - var parser = document.createElement('a'); - parser.href = url; - url = parser.href; - } - try { - return new URL(url); - } catch (e) { - return null; - } + 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); + }); + } } - - // 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; + }; + + /** + * 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 - var captions = { - // Setup captions - setup: function setup() { - // Requires UI support - if (!this.supported.ui) { - return; - } + function buildUrlParams(input) { + var params = new URLSearchParams(); - // 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); - } + if (is.object(input)) { + Object.entries(input).forEach(function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + key = _ref2[0], + value = _ref2[1]; - return; - } + params.set(key, value); + }); + } - // Inject the container - if (!is.element(this.elements.captions)) { - this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); + return params; + } - insertAfter(this.elements.captions, this.elements.wrapper); - } + var captions = { + // Setup captions + setup: function setup() { + // Requires UI support + if (!this.supported.ui) { + return; + } // Only Vimeo and HTML5 video supported at this point - // 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 + 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); + } - var browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; - var languages = dedupe(browserLanguages.map(function (language) { - return language.split('-')[0]; - })); + return; + } // Inject the container - 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); + 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!) - language = _languages[0]; - } - var active = this.storage.get('captions'); - if (!is.boolean(active)) { - active = this.config.captions.active; - } + 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); - Object.assign(this.captions, { - toggled: false, - active: active, - language: language, - languages: languages + 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); }); - - // 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 _captions = this.captions, - active = _captions.active, - language = _captions.language, - meta = _captions.meta, - currentTrackNode = _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); - } - }, + } + }); + } // 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 - // 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; + 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 there's no full support - if (!this.supported.ui) { - return; - } + if (language === 'auto') { + var _languages = _slicedToArray(languages, 1); - var toggled = this.captions.toggled; // Current state + language = _languages[0]; + } - var activeClass = this.config.classNames.captions.active; + var active = this.storage.get('captions'); - // Get the next state - // If the method is called without parameter, toggle based on current value - var active = is.nullOrUndefined(input) ? !toggled : input; + if (!is.boolean(active)) { + active = this.config.captions.active; + } - // 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 }); - } + Object.assign(this.captions, { + toggled: false, + active: active, + language: language, + languages: languages + }); // Watch changes to textTracks and update captions menu - // 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); + 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) - // 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; - } + setTimeout(captions.update.bind(this), 0); + }, + // Update available language options in settings based on tracks + update: function update() { + var _this = this; - // Toggle button if it's enabled - if (this.elements.buttons.captions) { - this.elements.buttons.captions.pressed = active; - } + var tracks = captions.getTracks.call(this, true); // Get the wanted language - // Add class hook - toggleClass(this.elements.container, activeClass, active); + 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) - this.captions.toggled = active; + 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" - // Update settings menu - controls.updateSetting.call(this, 'captions'); - // Trigger event (not used internally) - triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); - } - }, + 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 - // Set captions by track index - // Used internally for the currentTrack setter with the passive option forced to false - set: function set$$1(index) { - var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + 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 - var tracks = captions.getTracks.call(this); - // Disable captions if setting to -1 - if (index === -1) { - captions.toggle.call(this, false, passive); - return; - } + 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 - if (!is.number(index)) { - this.debug.warn('Invalid caption argument', index); - return; - } - if (!(index in tracks)) { - this.debug.warn('Track not found', index); - return; - } + toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); // Update available languages in list - if (this.captions.currentTrack !== index) { - this.captions.currentTrack = index; - var track = tracks[index]; + 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 _ref = track || {}, - language = _ref.language; + var toggled = this.captions.toggled; // Current state - // Store reference to node for invalidation on remove + 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 - this.captions.currentTrackNode = track; + 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 - // 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 }); - } + 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 - // Handle Vimeo captions - if (this.isVimeo) { - this.embed.enableTextTrack(language); - } + this.captions.language = track.language; // Set caption, but don't store in localStorage as user preference - // Trigger event - triggerEvent.call(this, this.media, 'languagechange'); - } + captions.set.call(this, tracks.indexOf(track)); + return; + } // Toggle button if it's enabled - // 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); - } - }, + if (this.elements.buttons.captions) { + this.elements.buttons.captions.pressed = active; + } // Add class hook - // 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; + toggleClass(this.elements.container, activeClass, active); + this.captions.toggled = active; // Update settings menu - 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); - }, + 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; + } - // 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; + if (!is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; + } - var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; + } - // 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); - }); - }, + 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 - // 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; + this.captions.currentTrackNode = track; // Update settings menu - 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 = void 0; - 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); - }, + 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 - // Get the current track - getCurrentTrack: function getCurrentTrack() { - return captions.getTracks.call(this)[this.currentTrack]; - }, + if (this.isVimeo) { + this.embed.enableTextTrack(language); + } // Trigger event - // 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); - } + triggerEvent.call(this, this.media, 'languagechange'); + } // Show captions - if (is.track(currentTrack)) { - if (!is.empty(currentTrack.label)) { - return currentTrack.label; - } - if (!is.empty(currentTrack.language)) { - return track.language.toUpperCase(); - } + captions.toggle.call(this, true, passive); - return i18n.get('enabled', this.config); - } + 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; - return i18n.get('disabled', this.config); - }, + 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); + }; - // 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; - } + 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.element(this.elements.captions)) { - this.debug.warn('No captions element to render to'); - return; - } + if (is.track(currentTrack)) { + if (!is.empty(currentTrack.label)) { + return currentTrack.label; + } - // Only accept array or empty input - if (!is.nullOrUndefined(input) && !Array.isArray(input)) { - this.debug.warn('updateCues: Invalid input', input); - return; - } + if (!is.empty(currentTrack.language)) { + return track.language.toUpperCase(); + } - var cues = input; + return i18n.get('enabled', this.config); + } - // 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); - } + 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; + } - // 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'); - } - } - }; + if (!is.element(this.elements.captions)) { + this.debug.warn('No captions element to render to'); + return; + } // Only accept array or empty input - // ========================================================================== - // Plyr default config - // ========================================================================== - var defaults$1 = { - // Disable - enabled: true, + if (!is.nullOrUndefined(input) && !Array.isArray(input)) { + this.debug.warn('updateCues: Invalid input', input); + return; + } - // Custom media title - title: '', + var cues = input; // Get cues from track - // Logging to console - debug: false, + 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 - // Auto play (if supported) - autoplay: false, - // Only allow one media playing at once (vimeo only) - autopause: true, + var content = cues.map(function (cueText) { + return cueText.trim(); + }).join('\n'); + var changed = content !== this.elements.captions.innerHTML; - // 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, + 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 - // Default time to skip when rewind/fast forward - seekTime: 10, + 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.3.12/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', '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', + 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: { + 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, + 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 + '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"]', + 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 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; + } - // Default volume - volume: 1, - muted: false, + return null; + } - // Pass a custom duration - duration: null, + // ========================================================================== + // Console wrapper + // ========================================================================== + var noop = function noop() {}; - // 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, + var Console = + /*#__PURE__*/ + function () { + function Console() { + var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - // Invert the current time to be a countdown - invertTime: true, + _classCallCheck(this, Console); - // Clicking the currentTime inverts it's value to show time left rather than elapsed - toggleInvert: true, + this.enabled = window.console && enabled; - // Aspect ratio (for embeds) - ratio: '16:9', + if (this.enabled) { + this.log('Debugging enabled'); + } + } - // Click video container to play/pause - clickToPlay: true, + _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; + } + }]); - // Auto hide the controls - hideControls: true, + return Console; + }(); - // Reset to start when playback ended - resetOnEnd: false, + function onChange() { + if (!this.enabled) { + return; + } // Update toggle button - // Disable the standard context menu - disableContextMenu: true, - // Sprite (for icons) - loadSprite: true, - iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.3.12/plyr.svg', + var button = this.player.elements.buttons.fullscreen; - // Blank video (used to prevent errors on source change) - blankVideo: 'https://cdn.plyr.io/static/blank.mp4', + if (is.element(button)) { + button.pressed = this.active; + } // Trigger an event - // 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, - }, + triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); // Trap focus in container - // Speed default and options to display - speed: { - selected: 1, - options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] - }, + if (!browser.isIos) { + trapFocus.call(this.player, this.target, this.active); + } + } - // Keyboard shortcut settings - keyboard: { - focused: true, - global: false - }, + function toggleFallback() { + var _this = this; - // Display tooltips - tooltips: { - controls: false, - seek: true - }, + var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - // 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 - }, + // 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 - // 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' - }, + document.body.style.overflow = toggle ? 'hidden' : ''; // Toggle class hook - // Default controls - controls: ['play-large', - // 'restart', - // 'rewind', - 'play', - // 'fast-forward', - 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', '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', - 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' - } - }, + toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); // Force full viewport on iPhone X+ - // URLs - urls: { - 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' - } - }, + if (browser.isIos) { + var viewport = document.head.querySelector('meta[name="viewport"]'); + var property = 'viewport-fit=cover'; // Inject the viewport meta if required - // Custom control listeners - listeners: { - seek: null, - play: null, - pause: null, - restart: null, - rewind: null, - fastForward: null, - mute: null, - volume: null, - captions: null, - fullscreen: null, - pip: null, - airplay: null, - speed: null, - quality: null, - loop: null, - language: null - }, + if (!viewport) { + viewport = document.createElement('meta'); + viewport.setAttribute('name', 'viewport'); + } // Check if the property already exists - // 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 - 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', + var hasProperty = is.string(viewport.content) && viewport.content.includes(property); - // YouTube - 'statechange', + if (toggle) { + this.cleanupViewport = !hasProperty; - // Quality - 'qualitychange', + 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 - // 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"]', - 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' - } - }, + setTimeout(function () { + return repaint(_this.target); + }, 100); + } // Toggle button and fire events - // 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' - } - }, + onChange.call(this); + } - // API keys - keys: { - google: null - }, + var Fullscreen = + /*#__PURE__*/ + function () { + function Fullscreen(player) { + var _this2 = this; - // Advertisements plugin - // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio - ads: { - enabled: false, - publisherId: '' - } - }; + _classCallCheck(this, Fullscreen); - // ========================================================================== - // Plyr supported types and providers - // ========================================================================== + // Keep reference to parent + this.player = player; // Get prefix - var providers = { - html5: 'html5', - youtube: 'youtube', - vimeo: 'vimeo' - }; + this.prefix = Fullscreen.prefix; + this.property = Fullscreen.property; // Scroll position - var types = { - audio: 'audio', - video: 'video' - }; + this.scrollPosition = { + x: 0, + y: 0 + }; // Register event listeners + // Handle event (incase user presses escape etc) - /** - * Get provider by URL - * @param {string} url - */ - function getProviderByUrl(url) { - // YouTube - if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) { - return providers.youtube; - } + 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 - // Vimeo - if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { - return providers.vimeo; + 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; } - return null; - } - - // ========================================================================== - // Console wrapper - // ========================================================================== + _this2.toggle(); + }); // Update the UI - var noop = function noop() {}; + this.update(); + } // Determine if native supported - var Console = function () { - function Console() { - var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - classCallCheck(this, Console); - this.enabled = window.console && enabled; + _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 - if (this.enabled) { - this.log('Debugging enabled'); - } - } - createClass(Console, [{ - key: 'log', - get: function get$$1() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; - } - }, { - key: 'warn', - get: function get$$1() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; - } - }, { - key: 'error', - get: function get$$1() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; - } - }]); - return Console; - }(); - - // ========================================================================== + toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); + } // Make an element fullscreen - function onChange() { + }, { + key: "enter", + value: function enter() { if (!this.enabled) { - return; + 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 - // Update toggle button - var button = this.player.elements.buttons.fullscreen; - if (is.element(button)) { - button.pressed = this.active; + }, { + 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 - // 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); + }, { + 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 - function toggleFallback() { - 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); + if (!Fullscreen.native) { + return hasClass(this.target, this.player.config.classNames.fullscreen.fallback); } - // Toggle scroll - document.body.style.overflow = toggle ? 'hidden' : ''; + var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")]; + return element === this.target; + } // Get target element - // Toggle class hook - toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); + }, { + 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; + } - // Toggle button and fire events - onChange.call(this); - } + 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); + }; - var Fullscreen = function () { - function Fullscreen(player) { - var _this = this; + 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; - classCallCheck(this, Fullscreen); + // Re-attach media element listeners + // TODO: Use event bubbling? + this.listeners.media(); // Don't setup interface if no support - // Keep reference to parent - this.player = player; + if (!this.supported.ui) { + this.debug.warn("Basic support only for ".concat(this.provider, " ").concat(this.type)); // Restore native controls - // Get prefix - this.prefix = Fullscreen.prefix; - this.property = Fullscreen.property; + ui.toggleNativeControls.call(this, true); // Bail - // Scroll position - this.scrollPosition = { x: 0, y: 0 }; + return; + } // Inject custom controls if not present - // Register event listeners - // Handle event (incase user presses escape etc) - on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : this.prefix + 'fullscreenchange', function () { - // TODO: Filter for target?? - onChange.call(_this); - }); - // Fullscreen toggle on double click - on.call(this.player, this.player.elements.container, 'dblclick', function (event) { - // Ignore double click in controls - if (is.element(_this.player.elements.controls) && _this.player.elements.controls.contains(event.target)) { - return; - } + if (!is.element(this.elements.controls)) { + // Inject custom controls + controls.inject.call(this); // Re-attach control listeners - _this.toggle(); - }); + this.listeners.controls(); + } // Remove native controls - // Update the UI - this.update(); - } - // Determine if native supported + ui.toggleNativeControls.call(this); // Setup captions for HTML5 + if (this.isHTML5) { + captions.setup.call(this); + } // Reset volume - createClass(Fullscreen, [{ - key: 'update', + this.volume = null; // Reset mute state - // Update UI - value: function update() { - if (this.enabled) { - this.player.debug.log((Fullscreen.native ? 'Native' : 'Fallback') + ' fullscreen enabled'); - } else { - this.player.debug.log('Fullscreen not supported and fallback disabled'); - } + this.muted = null; // Reset speed - // Add styling hook to show button - toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); - } + this.speed = null; // Reset loop state - // Make an element fullscreen + this.loop = null; // Reset quality setting - }, { - key: 'enter', - value: function enter() { - if (!this.enabled) { - return; - } + this.quality = null; // Reset volume display - // 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[this.prefix + 'Request' + this.property](); - } - } + controls.updateVolume.call(this); // Reset time display - // Bail from fullscreen + controls.timeUpdate.call(this); // Update the UI - }, { - key: 'exit', - value: function exit() { - if (!this.enabled) { - return; - } + ui.checkPlaying.call(this); // Check for picture-in-picture support - // 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['' + this.prefix + action + this.property](); - } - } + toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support - // Toggle state + toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class - }, { - key: 'toggle', - value: function toggle() { - if (!this.active) { - this.enter(); - } else { - this.exit(); - } - } - }, { - key: 'enabled', + 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 - // Determine if fullscreen is enabled - get: function get$$1() { - return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo; - } + this.ready = true; // Ready event at end of execution stack - // Get active state + setTimeout(function () { + triggerEvent.call(_this, _this.media, 'ready'); + }, 0); // Set the title - }, { - key: 'active', - get: function get$$1() { - if (!this.enabled) { - return false; - } + ui.setTitle.call(this); // Assure the poster image is set, if the property was added before the element was created - // Fallback using classname - if (!Fullscreen.native) { - return hasClass(this.target, this.player.config.classNames.fullscreen.fallback); - } + 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) - var element = !this.prefix ? document.fullscreenElement : document['' + this.prefix + this.property + 'Element']; - return element === this.target; - } - - // Get target element - - }, { - key: 'target', - get: function get$$1() { - return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; - } - }], [{ - key: 'native', - get: function get$$1() { - return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); - } + 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 - // Get the prefix for handlers + if (is.string(this.config.title) && !is.empty(this.config.title)) { + label += ", ".concat(this.config.title); + } // If there's a play button, set label - }, { - key: 'prefix', - get: function get$$1() { - // No prefix - if (is.function(document.exitFullscreen)) { - return ''; - } - // Check for fullscreen support by vendor prefix - var value = ''; - var prefixes = ['webkit', 'moz', 'ms']; + Array.from(this.elements.buttons.play || []).forEach(function (button) { + button.setAttribute('aria-label', label); + }); // Set iframe title + // https://github.com/sampotts/plyr/issues/124 - prefixes.some(function (pre) { - if (is.function(document[pre + 'ExitFullscreen']) || is.function(document[pre + 'CancelFullScreen'])) { - value = pre; - return true; - } + if (this.isEmbed) { + var iframe = getElement.call(this, 'iframe'); - return false; - }); + if (!is.element(iframe)) { + return; + } // Default to media type - return value; - } - }, { - key: 'property', - get: function get$$1() { - 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 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: '' }); - } - - // ========================================================================== - - 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(); + ui.togglePoster.call(_this2, true); + return poster; + }); + }, + // Check playing state + checkPlaying: function checkPlaying(event) { + var _this3 = this; - // Don't setup interface if no support - if (!this.supported.ui) { - this.debug.warn('Basic support only for ' + this.provider + ' ' + this.type); + // 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 - // Restore native controls - ui.toggleNativeControls.call(this, true); + Array.from(this.elements.buttons.play || []).forEach(function (target) { + target.pressed = _this3.playing; + }); // Only update controls on non timeupdate events - // Bail - return; - } + if (is.event(event) && event.type === 'timeupdate') { + return; + } // Toggle controls - // 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(); - } + ui.toggleControls.call(this); + }, + // Check if media is loading + checkLoading: function checkLoading(event) { + var _this4 = this; - // Remove native controls - ui.toggleNativeControls.call(this); + this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer - // Setup captions for HTML5 - if (this.isHTML5) { - captions.setup.call(this); - } + clearTimeout(this.timers.loading); // Timer to prevent flicker when seeking - // Reset volume - this.volume = null; + this.timers.loading = setTimeout(function () { + // Update progress bar loading class state + toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading); // Update controls visibility - // Reset mute state - this.muted = null; + 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; - // Reset speed - this.speed = null; + if (controls$$1 && this.config.hideControls) { + // Show controls if force, loading, paused, or button interaction, otherwise hide + this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover)); + } + } + }; - // Reset loop state - this.loop = null; + var Listeners = + /*#__PURE__*/ + function () { + function Listeners(player) { + _classCallCheck(this, Listeners); - // Reset quality setting - this.quality = null; + 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 - // Reset volume display - controls.updateVolume.call(this); - // Reset time display - controls.timeUpdate.call(this); + _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 - // Update the UI - ui.checkPlaying.call(this); + 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 - // 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); + if (!is.number(code)) { + return; + } // Seek by the number keys - // 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); + 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 - // Ready for API calls - this.ready = true; - // Ready event at end of execution stack - setTimeout(function () { - triggerEvent.call(_this, _this.media, 'ready'); - }, 0); + 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; - // Set the title - ui.setTitle.call(this); + if (is.element(focused)) { + var editable = player.config.selectors.editable; + var seek = elements.inputs.seek; - // 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 () {}); + if (focused !== seek && matches(focused, editable)) { + return; } - // 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); + if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) { + return; } - }, + } // Which keycodes should we prevent default - // Setup aria attribute for play and iframe title - setTitle: function setTitle() { - // Find the current text - var label = i18n.get('play', this.config); + 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 there's a media title set, use that for the label - if (is.string(this.config.title) && !is.empty(this.config.title)) { - label += ', ' + this.config.title; - } + if (preventDefault.includes(code)) { + event.preventDefault(); + event.stopPropagation(); + } - // If there's a play button, set label - Array.from(this.elements.buttons.play || []).forEach(function (button) { - button.setAttribute('aria-label', label); - }); + 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 - // Set iframe title - // https://github.com/sampotts/plyr/issues/124 - if (this.isEmbed) { - var iframe = getElement.call(this, 'iframe'); + if (event.type === 'keydown' && event.which !== 9) { + return; + } // Store reference to event timeStamp - 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); + if (event.type === 'keydown') { + this.lastKeyDown = event.timeStamp; + } // Remove current classes - iframe.setAttribute('title', format.replace('{title}', title)); - } - }, + 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 - // Toggle poster - togglePoster: function togglePoster(enable) { - toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); - }, + var wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; // Ignore focus events if a key was pressed prior - // 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; + if (event.type === 'focus' && !wasKeyDown) { + return; + } // Remove all current - 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')); - } + removeCurrent(); // Delay the adding of classname until the focus has changed + // This event fires before the focusin event - // 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(\'' + 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; - }); - }, + this.focusTimer = setTimeout(function () { + var focused = document.activeElement; // Ignore if current focus element isn't inside the player + if (!elements.container.contains(focused)) { + return; + } - // Check playing state - checkPlaying: function checkPlaying(event) { - var _this3 = this; + toggleClass(document.activeElement, player.config.classNames.tabFocus, true); + }, 10); + } // Global window & document listeners - // 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); + }, { + key: "global", + value: function global() { + var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; + var player = this.player; // Keyboard shortcuts - // Set state - Array.from(this.elements.buttons.play || []).forEach(function (target) { - target.pressed = _this3.playing; - }); + if (player.config.keyboard.global) { + toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false); + } // Click anywhere closes menu - // Only update controls on non timeupdate events - if (is.event(event) && event.type === 'timeupdate') { - return; - } - // Toggle controls - ui.toggleControls.call(this); - }, + toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events + once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection - // Check if media is loading - checkLoading: function checkLoading(event) { - var _this4 = this; + toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); + } // Container listeners - this.loading = ['stalled', 'waiting'].includes(event.type); + }, { + key: "container", + value: function container() { + var player = this.player; + var elements = player.elements; // Keyboard shortcuts - // Clear timer - clearTimeout(this.timers.loading); + 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 - // 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); - }, + 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 - // Toggle controls based on state and `force` argument - toggleControls: function toggleControls(force) { - var controls$$1 = this.elements.controls; + var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); + var delay = 0; - if (controls$$1 && this.config.hideControls) { - // Show controls if force, loading, paused, or button interaction, otherwise hide - this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover)); - } - } - }; + if (show) { + ui.toggleControls.call(player, true); // Use longer timeout for touch devices - // ========================================================================== + delay = player.touch ? 3000 : 2000; + } // Clear timer - var Listeners = function () { - function Listeners(player) { - classCallCheck(this, Listeners); - this.player = player; - this.lastKey = null; - this.focusTimer = null; - this.lastKeyDown = null; + clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking - this.handleKey = this.handleKey.bind(this); - this.toggleMenu = this.toggleMenu.bind(this); - this.setTabFocus = this.setTabFocus.bind(this); - this.firstTouch = this.firstTouch.bind(this); - } + 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', 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 - // Handle key presses + 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 - createClass(Listeners, [{ - key: 'handleKey', - value: function handleKey(event) { - var player = this.player; - var elements = player.elements; + on.call(player, player.media, 'playing play pause ended emptied timeupdate', function (event) { + return ui.checkPlaying.call(player, event); + }); // Loading state - var code = event.keyCode ? event.keyCode : event.which; - var pressed = event.type === 'keydown'; - var repeat = pressed && code === this.lastKey; + 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 - // Bail if a modifier key is set - if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { - return; - } + on.call(player, player.media, 'playing', function () { + if (!player.ads) { + return; + } // If ads are enabled, wait for them first - // 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; - } - } + 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 - // Toggle menu + 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) - }, { - key: 'toggleMenu', - value: function toggleMenu(event) { - controls.toggleMenu.call(this.player, event); - } + if (!is.element(wrapper)) { + return; + } // On click play, pause ore restart - // Device is touch enabled - }, { - key: 'firstTouch', - value: function firstTouch() { - var player = this.player; - var elements = player.elements; + 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) - player.touch = true; - // Add touch class - toggleClass(elements.container, player.config.classNames.isTouch, true); + if (player.touch && player.config.hideControls) { + return; } - }, { - key: 'setTabFocus', - value: function setTabFocus(event) { - var player = this.player; - var elements = player.elements; + if (player.ended) { + player.restart(); + player.play(); + } else { + player.togglePlay(); + } + }); + } // Disable right click - clearTimeout(this.focusTimer); - // Ignore any key other than tab - if (event.type === 'keydown' && event.which !== 9) { - return; - } + if (player.supported.ui && player.config.disableContextMenu) { + on.call(player, elements.wrapper, 'contextmenu', function (event) { + event.preventDefault(); + }, false); + } // Volume change - // 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, '.' + className); - toggleClass(current, className, false); - }; + on.call(player, player.media, 'volumechange', function () { + // Save to storage + player.storage.set({ + volume: player.volume, + muted: player.muted + }); + }); // Speed change - // Determine if a key was pressed to trigger this event - var wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; + on.call(player, player.media, 'ratechange', function () { + // Update UI + controls.updateSetting.call(player, 'speed'); // Save to storage - // Ignore focus events if a key was pressed prior - if (event.type === 'focus' && !wasKeyDown) { - return; - } - // Remove all current - removeCurrent(); + player.storage.set({ + speed: player.speed + }); + }); // Quality change - // 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; + on.call(player, player.media, 'qualitychange', function (event) { + // Update UI + controls.updateSetting.call(player, 'quality', null, event.detail.quality); + }); // Proxy events to container + // Bubble up key events for Edge - // Ignore if current focus element isn't inside the player - if (!elements.container.contains(focused)) { - return; - } + 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 - toggleClass(document.activeElement, player.config.classNames.tabFocus, true); - }, 10); - } + if (event.type === 'error') { + detail = player.media.error; + } - // Global window & document listeners + triggerEvent.call(player, elements.container, event.type, true, detail); + }); + } // Run default and custom handlers - }, { - key: 'global', - value: function global() { - var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - var player = this.player; + }, { + 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 - // Keyboard shortcuts + if (hasCustomHandler) { + returned = customHandler.call(player, event); + } // Only call default handler if not prevented in custom handler - 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); + if (returned && is.function(defaultHandler)) { + defaultHandler.call(player, event); + } + } // Trigger custom and default handlers - // Detect touch by events - once.call(player, document.body, 'touchstart', this.firstTouch); + }, { + key: "bind", + value: function bind(element, type, defaultHandler, customHandlerKey) { + var _this = this; - // Tab focus detection - toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true); - } + 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; - // Container listeners + var player = this.player; + var elements = player.elements; // IE doesn't support input event, so we fallback to change - }, { - key: 'container', - value: function container() { - var player = this.player; - var elements = player.elements; + var inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle - // Keyboard shortcuts + if (elements.buttons.play) { + Array.from(elements.buttons.play).forEach(function (button) { + _this2.bind(button, 'click', player.togglePlay, 'play'); + }); + } // Pause - 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; + this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind - // Remove button states for fullscreen + this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind - if (controls$$1 && event.type === 'enterfullscreen') { - controls$$1.pressed = false; - controls$$1.hover = false; - } + this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle - // Show, then hide after a timeout unless another control event occurs - var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); + this.bind(elements.buttons.mute, 'click', function () { + player.muted = !player.muted; + }, 'mute'); // Captions toggle - var delay = 0; + this.bind(elements.buttons.captions, 'click', function () { + return player.toggleCaptions(); + }); // Fullscreen toggle - if (show) { - ui.toggleControls.call(player, true); - // Use longer timeout for touch devices - delay = player.touch ? 3000 : 2000; - } + this.bind(elements.buttons.fullscreen, 'click', function () { + player.fullscreen.toggle(); + }, 'fullscreen'); // Picture-in-Picture - // Clear timer - clearTimeout(player.timers.controls); + this.bind(elements.buttons.pip, 'click', function () { + player.pip = 'toggle'; + }, 'pip'); // Airplay - // Set new timer to prevent flicker when seeking - player.timers.controls = setTimeout(function () { - return ui.toggleControls.call(player, false); - }, delay); - }); - } + this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle - // Listen for media events + this.bind(elements.buttons.settings, 'click', function (event) { + // Prevent the document click listener closing the menu + event.stopPropagation(); - }, { - key: 'media', - value: function media() { - var player = this.player; - var elements = player.elements; + 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 - // Time change on media + this.bind(elements.buttons.settings, 'keyup', function (event) { + var code = event.which; // We only care about space and return - on.call(player, player.media, 'timeupdate seeking seeked', function (event) { - return controls.timeUpdate.call(player, event); - }); + if (![13, 32].includes(code)) { + return; + } // Because return triggers a click anyway, all we need to do is set focus - // 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', function () { - toggleHidden(elements.volume, !player.hasAudio); - toggleHidden(elements.buttons.mute, !player.hasAudio); - }); + if (code === 13) { + controls.focusFirstMenuItem.call(player, null, true); - // 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(); - } - }); + return; + } // Prevent scroll - // 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); - }); + event.preventDefault(); // Prevent playing video (Firefox) - // Handle play/pause - on.call(player, player.media, 'playing play pause ended emptied timeupdate', function (event) { - return ui.checkPlaying.call(player, event); - }); + event.stopPropagation(); // Toggle menu - // Loading state - on.call(player, player.media, 'waiting canplay seeked playing', function (event) { - return ui.checkLoading.call(player, event); - }); + controls.toggleMenu.call(player, event); + }, null, false // Can't be passive as we're preventing default + ); // Escape closes menu - // 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(); - }); - } - }); + 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) - // Click video - if (player.supported.ui && player.config.clickToPlay && !player.isAudio) { - // Re-fetch the wrapper - var wrapper = getElement.call(player, '.' + player.config.classNames.video); - - // Bail if there's no wrapper (this should never happen) - if (!is.element(wrapper)) { - return; - } - - // On click play, pause ore 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(); - } - }); - } + 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 - // Disable right click - if (player.supported.ui && player.config.disableContextMenu) { - on.call(player, elements.wrapper, 'contextmenu', function (event) { - event.preventDefault(); - }, false); - } + 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'; - // Volume change - on.call(player, player.media, 'volumechange', function () { - // Save to storage - player.storage.set({ - volume: player.volume, - muted: player.muted - }); - }); + if (is.keyboardEvent(event) && code !== 39 && code !== 37) { + return; + } // Was playing before? - // 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 }); - }); + var play = seek.hasAttribute(attribute); // Done seeking - // Quality change - on.call(player, player.media, 'qualitychange', function (event) { - // Update UI - controls.updateSetting.call(player, 'quality', null, event.detail.quality); - }); + var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback - // Proxy events to container - // Bubble up key events for Edge - var proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); + 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 - on.call(player, player.media, proxyEvents, function (event) { - var _event$detail = event.detail, - detail = _event$detail === undefined ? {} : _event$detail; - // Get error details from media + 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) - if (event.type === 'error') { - detail = player.media.error; - } + var seekTo = seek.getAttribute('seek-value'); - triggerEvent.call(player, elements.container, event.type, true, detail); - }); - } + if (is.empty(seekTo)) { + seekTo = seek.value; + } - // Run default and custom handlers + seek.removeAttribute('seek-value'); + player.currentTime = seekTo / seek.max * player.duration; + }, 'seek'); // Seek tooltip - }, { - key: 'proxy', - value: function proxy(event, defaultHandler, customHandlerKey) { - var player = this.player; + 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 - var customHandler = player.config.listeners[customHandlerKey]; - var hasCustomHandler = is.function(customHandler); - var returned = true; + 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 - // 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); - } + 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; } - // Trigger custom and default handlers + player.config.invertTime = !player.config.invertTime; - }, { - key: 'bind', - value: function bind(element, type, defaultHandler, customHandlerKey) { - var _this = this; + controls.timeUpdate.call(player); + }); + } // Volume - 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); + 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) - on.call(player, element, type, function (event) { - return _this.proxy(event, defaultHandler, customHandlerKey); - }, passive && !hasCustomHandler); - } + 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) - // Listen for control events + this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { + elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); + }); // Focus in/out on controls - }, { - key: 'controls', - value: function controls$$1() { - var _this2 = this; + this.bind(elements.controls, 'focusin focusout', function (event) { + var config = player.config, + elements = player.elements, + timers = player.timers; + var isFocusIn = event.type === 'focusin'; // Skip transition to prevent focus from scrolling the parent element - var player = this.player; - var elements = player.elements; + toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); // Toggle - // 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'); - }); - } + ui.toggleControls.call(player, isFocusIn); // If focusin, hide again after delay - // Pause - this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); + if (isFocusIn) { + // Restore transition + setTimeout(function () { + toggleClass(elements.controls, config.classNames.noTransition, false); + }, 0); // Delay a little more for keyboard users - // Rewind - this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); + var delay = _this2.touch ? 3000 : 4000; // Clear timer - // Rewind - this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); + clearTimeout(timers.controls); // Hide - // Mute toggle - this.bind(elements.buttons.mute, 'click', function () { - player.muted = !player.muted; - }, 'mute'); + timers.controls = setTimeout(function () { + return ui.toggleControls.call(player, false); + }, delay); + } + }); // Mouse wheel for volume - // Captions toggle - this.bind(elements.buttons.captions, 'click', function () { - return player.toggleCaptions(); - }); + 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 - // Fullscreen toggle - this.bind(elements.buttons.fullscreen, 'click', function () { - player.fullscreen.toggle(); - }, 'fullscreen'); + 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) - // Picture-in-Picture - this.bind(elements.buttons.pip, 'click', function () { - player.pip = 'toggle'; - }, 'pip'); - // Airplay - this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); + var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2% - // Settings menu - click toggle - this.bind(elements.buttons.settings, 'click', function (event) { - // Prevent the document click listener closing the menu - event.stopPropagation(); + player.increaseVolume(direction / 50); // Don't break page scrolling at max and min - controls.toggleMenu.call(player, event); - }); + var volume = player.media.volume; - // 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); - } - }); + if (direction === 1 && volume < 1 || direction === -1 && volume > 0) { + event.preventDefault(); + } + }, 'volume', false); + } + }]); - // 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); - }); + return Listeners; + }(); - // 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; - } - - // 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(); - } - }); + var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - // 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); - }); - }); - } + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } - // Seek - this.bind(elements.inputs.seek, inputEvent, function (event) { - var seek = event.currentTarget; + var loadjs_umd = createCommonjsModule(function (module, exports) { + (function(root, factory) { + if (typeof undefined === 'function' && undefined.amd) { + undefined([], factory); + } else { + module.exports = factory(); + } + }(commonjsGlobal, function() { + /** + * Global dependencies. + * @global {Object} document - DOM + */ + + var devnull = function() {}, + bundleIdCache = {}, + bundleResultCache = {}, + bundleCallbackQueue = {}; - // 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; - } + /** + * 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); + }; - seek.removeAttribute('seek-value'); + // register callback + while (i--) { + bundleId = bundleIds[i]; - player.currentTime = seekTo / seek.max * player.duration; - }, 'seek'); + // execute callback if in result cache + r = bundleResultCache[bundleId]; + if (r) { + fn(bundleId, r); + continue; + } - // Seek tooltip - this.bind(elements.progress, 'mouseenter mouseleave mousemove', function (event) { - return controls.updateSeekTooltip.call(player, event); - }); + // add to callback queue + q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; + q.push(fn); + } + } - // 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; - } + /** + * 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; - player.config.invertTime = !player.config.invertTime; + var q = bundleCallbackQueue[bundleId]; - controls.timeUpdate.call(player); - }); - } + // cache result + bundleResultCache[bundleId] = pathsNotFound; - // Volume - this.bind(elements.inputs.volume, inputEvent, function (event) { - player.volume = event.target.value; - }, 'volume'); + // exit if queue is empty + if (!q) return; - // 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'; - }); + // 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; + } - // 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); - }); + e.onload = e.onerror = e.onbeforeload = function (ev) { + var result = ev.type[0]; - // Focus in/out on controls - this.bind(elements.controls, 'focusin focusout', function (event) { - var config = player.config, - elements = player.elements, - timers = player.timers; + // 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'; + } + } - var isFocusIn = event.type === 'focusin'; + // handle retries in case of load failure + if (result == 'e') { + // increment counter + numTries += 1; - // Skip transition to prevent focus from scrolling the parent element - toggleClass(elements.controls, config.classNames.noTransition, isFocusIn); + // exit function and try again + if (numTries < maxTries) { + return loadFile(path, callbackFn, args, numTries); + } + } - // Toggle - ui.toggleControls.call(player, isFocusIn); + // execute callback + callbackFn(path, result, ev.defaultPrevented); + }; - // If focusin, hide again after delay - if (isFocusIn) { - // Restore transition - setTimeout(function () { - toggleClass(elements.controls, config.classNames.noTransition, false); - }, 0); + // 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; + } - // Delay a little more for keyboard users - var delay = _this2.touch ? 3000 : 4000; + numWaiting--; + if (!numWaiting) callbackFn(pathsNotFound); + }; - // Clear timer - clearTimeout(timers.controls); + // load scripts + for (i=0; i < x; i++) loadFile(paths[i], fn, args); + } - // Hide - 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; + /** + * 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; - // Get delta from event. Invert if `inverted` is true + // bundleId (if string) + if (arg1 && arg1.trim) bundleId = arg1; - var _map = [event.deltaX, -event.deltaY].map(function (value) { - return inverted ? -value : value; - }), - _map2 = slicedToArray(_map, 2), - x = _map2[0], - y = _map2[1]; + // args (default is {}) + args = (bundleId ? arg2 : arg1) || {}; - // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) + // 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); + }); - var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); + return loadjs; + }; - // Change the volume by 2% - player.increaseVolume(direction / 50); - // Don't break page scrolling at max and min - var volume = player.media.volume; + /** + * Manually satisfy bundle dependencies. + * @param {string} bundleId - The bundle id + */ + loadjs.done = function done(bundleId) { + publish(bundleId, []); + }; - 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 : {}; + /** + * Reset loadjs dependencies statuses + */ + loadjs.reset = function reset() { + bundleIdCache = {}; + bundleResultCache = {}; + bundleCallbackQueue = {}; + }; - function createCommonjsModule(fn, module) { - return module = { exports: {} }, fn(module, module.exports), module.exports; - } - var loadjs_umd = createCommonjsModule(function (module, exports) { - (function(root, factory) { - if (typeof undefined === 'function' && undefined.amd) { - undefined([], factory); - } else { - module.exports = factory(); - } - }(commonjsGlobal, function() { - /** - * Global dependencies. - * @global {Object} document - DOM - */ + /** + * Determine if bundle has already been defined + * @param String} bundleId - The bundle id + */ + loadjs.isDefined = function isDefined(bundleId) { + return bundleId in bundleIdCache; + }; - var devnull = function() {}, - bundleIdCache = {}, - bundleResultCache = {}, - bundleCallbackQueue = {}; + // export + return loadjs; - /** - * 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]; + // ========================================================================== + function loadScript(url) { + return new Promise(function (resolve, reject) { + loadjs_umd(url, { + success: resolve, + error: reject + }); + }); + } - // execute callback if in result cache - r = bundleResultCache[bundleId]; - if (r) { - fn(bundleId, r); - continue; - } + function parseId(url) { + if (is.empty(url)) { + return null; + } - // add to callback queue - q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; - q.push(fn); - } + 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 - /** - * 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]; + function getAspectRatio(width, height) { + var getRatio = function getRatio(w, h) { + return h === 0 ? w : getRatio(h, w % h); + }; - // cache result - bundleResultCache[bundleId] = pathsNotFound; + var ratio = getRatio(width, height); + return "".concat(width / ratio, ":").concat(height / ratio); + } // Set playback state and trigger change (only on actual change) - // exit if queue is empty - if (!q) return; - // empty callback queue - while (q.length) { - q[0](bundleId, pathsNotFound); - q.splice(0, 1); - } + 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'); + } + } - /** - * 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}; + var vimeo = { + setup: function setup() { + var _this = this; - // success and error callbacks - if (depsNotFound.length) (args.error || devnull)(depsNotFound); - else (args.success || devnull)(args); - } + // 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 - /** - * 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; + 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 { - // javascript - e = doc.createElement('script'); - e.src = path; - e.async = async === undefined ? true : async; + vimeo.ready.call(this); } - - 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); + }, + // 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 = (is.string(input) ? input : this.config.ratio).split(':'), + _split2 = _slicedToArray(_split, 2), + x = _split2[0], + y = _split2[1]; + + var padding = 100 / x * y; + 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 - // add to document (unless callback returns `false`) - if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); - } + var source = player.media.getAttribute('src'); // Get from <div> if needed + if (is.empty(source)) { + source = player.media.getAttribute(player.config.attributes.embed.id); + } - /** - * 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); - }; + var id = parseId(source); // Build an iframe - // load scripts - for (i=0; i < x; i++) loadFile(paths[i], fn, args); - } + 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 - /** - * 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; + var wrapper = createElement('div', { + poster: poster, + class: player.config.classNames.embedContainer + }); + wrapper.appendChild(iframe); + player.media = replaceElement(wrapper, player.media); // Get poster image - // args (default is {}) - args = (bundleId ? arg2 : arg1) || {}; + fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) { + if (is.empty(response)) { + return; + } // Get the URL for thumbnail - // 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); + var url = new URL(response[0].thumbnail_large); // Get original image - // publish bundle load event - publish(bundleId, pathsNotFound); - }, args); - } + 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 - /** - * 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); + 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 - return loadjs; - }; - - - /** - * Manually satisfy bundle dependencies. - * @param {string} bundleId - The bundle id - */ - loadjs.done = function done(bundleId) { - publish(bundleId, []); - }; + if (player.supported.ui) { + player.embed.disableTextTrack(); + } // Create a faux HTML5 API using the Vimeo API - /** - * Reset loadjs dependencies statuses - */ - loadjs.reset = function reset() { - bundleIdCache = {}; - bundleResultCache = {}; - bundleCallbackQueue = {}; - }; + player.media.play = function () { + assurePlaybackState.call(player, true); + return player.embed.play(); + }; + player.media.pause = function () { + assurePlaybackState.call(player, false); + return player.embed.pause(); + }; - /** - * Determine if bundle has already been defined - * @param String} bundleId - The bundle id - */ - loadjs.isDefined = function isDefined(bundleId) { - return bundleId in bundleIdCache; - }; + player.media.stop = function () { + player.pause(); + player.currentTime = 0; + }; // Seeking - // export - return loadjs; + 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 - function loadScript(url) { - return new Promise(function (resolve, reject) { - loadjs_umd(url, { - success: resolve, - error: reject - }); - }); - } + 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 - // Parse Vimeo ID from URL - function parseId(url) { - if (is.empty(url)) { - return null; + var currentSrc; + player.embed.getVideoUrl().then(function (value) { + currentSrc = value; + }).catch(function (error) { + _this2.debug.warn(error); + }); + Object.defineProperty(player.media, 'currentSrc', { + get: function get() { + return currentSrc; } + }); // Ended - if (is.number(Number(url))) { - return url; + 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) { + var ratio = getAspectRatio(dimensions[0], dimensions[1]); + vimeo.setAspectRatio.call(_this2, 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); - var regex = /^.*(vimeo.com\/|video\/)(\d+).*/; - return url.match(regex) ? RegExp.$2 : url; - } + if (!paused) { + triggerEvent.call(player, player.media, 'playing'); + } + }); - // 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 width / ratio + ':' + height / ratio; - } + 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 - // Set playback state and trigger change (only on actual change) - function assurePlaybackState(play) { - if (play && !this.embed.hasPlayed) { - this.embed.hasPlayed = true; + frame.setAttribute('tabindex', -1); } - if (this.media.paused === play) { - this.media.paused = !play; - triggerEvent.call(this, this.media, play ? 'play' : 'pause'); - } - } + }); + 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 - var vimeo = { - setup: function setup() { - var _this = this; + 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 - // Add embed class for responsive - toggleClass(this.elements.wrapper, this.config.classNames.embed, true); - // Set intial ratio - vimeo.setAspectRatio.call(this); + 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'); + }); // Rebuild UI + + setTimeout(function () { + return ui.build.call(player); + }, 0); + } + }; - // 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); - } - }, + // ========================================================================== + function parseId$1(url) { + if (is.empty(url)) { + return null; + } - // 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 = (is.string(input) ? input : this.config.ratio).split(':'), - _split2 = slicedToArray(_split, 2), - x = _split2[0], - y = _split2[1]; + 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) - var padding = 100 / x * y; - this.elements.wrapper.style.paddingBottom = padding + '%'; - if (this.supported.ui) { - var height = 240; - var offset = (height - padding) / (height / 50); + function assurePlaybackState$1(play) { + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } - this.media.style.transform = 'translateY(-' + offset + '%)'; - } - }, + 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; - // 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); + // Add embed class for responsive + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio - // Get the source URL or ID - var source = player.media.getAttribute('src'); + youtube.setAspectRatio.call(this); // Setup API - // Get from <div> if needed - if (is.empty(source)) { - source = player.media.getAttribute(player.config.attributes.embed.id); - } + 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 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'); + var key = this.config.keys.google; - // Get poster, if already set - var poster = player.poster; + 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) - // Inject the package + var currentId = player.media.getAttribute('id'); - var wrapper = createElement('div', { poster: poster, class: player.config.classNames.embedContainer }); - wrapper.appendChild(iframe); - player.media = replaceElement(wrapper, player.media); + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { + return; + } // Get the source URL or ID - // 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); + var source = player.media.getAttribute('src'); // Get from <div> if needed - // Get original image - url.pathname = url.pathname.split('_')[0] + '.jpg'; + if (is.empty(source)) { + source = player.media.getAttribute(this.config.attributes.embed.id); + } // Replace the <iframe> with a <div> due to YouTube API issues - // 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 - }); + var videoId = parseId$1(source); + var id = generateId(player.provider); // Get poster, if already set - player.media.paused = true; - player.media.currentTime = 0; + var poster = player.poster; // Replace media element - // Disable native text track rendering - if (player.supported.ui) { - player.embed.disableTextTrack(); - } + 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 - // Create a faux HTML5 API using the Vimeo API player.media.play = function () { - assurePlaybackState.call(player, true); - return player.embed.play(); + assurePlaybackState$1.call(player, true); + instance.playVideo(); }; player.media.pause = function () { - assurePlaybackState.call(player, false); - return player.embed.pause(); + assurePlaybackState$1.call(player, false); + instance.pauseVideo(); }; player.media.stop = function () { - player.pause(); - player.currentTime = 0; + instance.stopVideo(); }; - // Seeking - var currentTime = player.media.currentTime; + player.media.duration = instance.getDuration(); + player.media.paused = true; // Seeking + player.media.currentTime = 0; Object.defineProperty(player.media, 'currentTime', { - get: function get$$1() { - return currentTime; - }, - set: function set$$1(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 - }); - } - }); + 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 - // Playback speed - var speed = player.config.speed.selected; Object.defineProperty(player.media, 'playbackRate', { - get: function get$$1() { - return speed; - }, - set: function set$$1(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, []); - } - }); - } - }); + get: function get() { + return instance.getPlaybackRate(); + }, + set: function set(input) { + instance.setPlaybackRate(input); + } + }); // Volume - // Volume var volume = player.config.volume; - Object.defineProperty(player.media, 'volume', { - get: function get$$1() { - return volume; - }, - set: function set$$1(input) { - player.embed.setVolume(input).then(function () { - volume = input; - triggerEvent.call(player, player.media, 'volumechange'); - }); - } - }); + get: function get() { + return volume; + }, + set: function set(input) { + volume = input; + instance.setVolume(volume * 100); + triggerEvent.call(player, player.media, 'volumechange'); + } + }); // Muted - // Muted var muted = player.config.muted; - Object.defineProperty(player.media, 'muted', { - get: function get$$1() { - return muted; - }, - set: function set$$1(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$$1() { - return loop; - }, - set: function set$$1(input) { - var toggle = is.boolean(input) ? input : player.config.loop.active; - - player.embed.setLoop(toggle).then(function () { - loop = toggle; - }); - } - }); - - // Source - var currentSrc = void 0; - player.embed.getVideoUrl().then(function (value) { - currentSrc = value; - }).catch(function (error) { - _this2.debug.warn(error); - }); + 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$$1() { - return currentSrc; - } - }); + get: function get() { + return instance.getVideoUrl(); + } + }); // Ended - // Ended Object.defineProperty(player.media, 'ended', { - get: function get$$1() { - return player.currentTime === player.duration; - } - }); - - // Set aspect ratio based on video size - Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) { - var ratio = getAspectRatio(dimensions[0], dimensions[1]); - vimeo.setAspectRatio.call(_this2, ratio); - }); + get: function get() { + return player.currentTime === player.duration; + } + }); // Get available speeds - // 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 === undefined ? [] : _ref$cues; - - var strippedCues = cues.map(function (cue) { - return stripHTML(cue.text); - }); - captions.updateCues.call(player, strippedCues); - }); + player.options.speed = instance.getAvailablePlaybackRates(); // Set the tabindex to avoid focus entering iframe - 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); - } - }); + if (player.supported.ui) { + player.media.setAttribute('tabindex', -1); + } - player.embed.on('play', function () { - assurePlaybackState.call(player, true); - triggerEvent.call(player, player.media, 'playing'); - }); + triggerEvent.call(player, player.media, 'timeupdate'); + triggerEvent.call(player, player.media, 'durationchange'); // Reset timer - player.embed.on('pause', function () { - assurePlaybackState.call(player, false); - }); + clearInterval(player.timers.buffering); // Setup buffering - player.embed.on('timeupdate', function (data) { - player.media.seeking = false; - currentTime = data.seconds; - triggerEvent.call(player, player.media, 'timeupdate'); - }); + player.timers.buffering = setInterval(function () { + // Get loaded % from YouTube + player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something - player.embed.on('progress', function (data) { - player.media.buffered = data.percent; + if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) { triggerEvent.call(player, player.media, 'progress'); + } // Set last buffer point - // 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.media.lastBuffered = player.media.buffered; // Bail if we're at 100% - player.embed.on('ended', function () { - player.media.paused = true; - triggerEvent.call(player, player.media, 'ended'); - }); + if (player.media.buffered === 1) { + clearInterval(player.timers.buffering); // Trigger event - player.embed.on('error', function (detail) { - player.media.error = detail; - triggerEvent.call(player, player.media, 'error'); - }); + triggerEvent.call(player, player.media, 'canplaythrough'); + } + }, 200); // Rebuild UI - // Rebuild UI setTimeout(function () { - return ui.build.call(player); - }, 0); - } - }; - - // ========================================================================== + 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; - // Parse YouTube ID from URL - function parseId$1(url) { - if (is.empty(url)) { - return null; - } + case 0: + assurePlaybackState$1.call(player, false); // YouTube doesn't support loop for a single video, so mimick it. - var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - return url.match(regex) ? RegExp.$2 : url; - } + if (player.media.loop) { + // YouTube needs a call to `stopVideo` before playing again + instance.stopVideo(); + instance.playVideo(); + } else { + triggerEvent.call(player, player.media, 'ended'); + } - // 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'); - } - } + break; - var youtube = { - setup: function setup() { - var _this = this; + 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 - // Add embed class for responsive - toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + 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 - // Set aspect ratio - youtube.setAspectRatio.call(this); + if (player.media.duration !== instance.getDuration()) { + player.media.duration = instance.getDuration(); + triggerEvent.call(player, player.media, 'durationchange'); + } + } - // 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); - }); + break; - // Setup callback for the API - // YouTube has it's own system of course... - window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; + case 2: + // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) + if (!player.muted) { + player.embed.unMute(); + } - // Add to queue - window.onYouTubeReadyCallbacks.push(function () { - youtube.ready.call(_this); - }); + assurePlaybackState$1.call(player, false); + break; - // Set callback to process queue - window.onYouTubeIframeAPIReady = function () { - window.onYouTubeReadyCallbacks.forEach(function (callback) { - callback(); - }); - }; + default: + break; } - }, + triggerEvent.call(player, player.elements.container, 'statechange', false, { + code: event.data + }); + } + } + }); + } + }; - // Get the media title - getTitle: function getTitle(videoId) { - var _this2 = this; + // ========================================================================== + 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 - // 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 _embed$getVideoData = this.embed.getVideoData(), - title = _embed$getVideoData.title; - if (is.empty(title)) { - this.config.title = title; - ui.setTitle.call(this); - return; - } - } + toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); // Add provider class - // 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 () {}); - } - }, + 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 - // Set aspect ratio - setAspectRatio: function setAspectRatio() { - var ratio = this.config.ratio.split(':'); - this.elements.wrapper.style.paddingBottom = 100 / ratio[0] * ratio[1] + '%'; - }, + if (this.isVideo) { + // Create the wrapper div + this.elements.wrapper = createElement('div', { + class: this.config.classNames.video + }); // Wrap the video in a container - // API ready - ready: function ready$$1() { - var player = this; + wrap(this.media, this.elements.wrapper); // Faux poster container - // Ignore already setup (race condition) - var currentId = player.media.getAttribute('id'); - if (!is.empty(currentId) && currentId.startsWith('youtube-')) { - return; - } + this.elements.poster = createElement('div', { + class: this.config.classNames.poster + }); + this.elements.wrapper.appendChild(this.elements.poster); + } - // Get the source URL or ID - var source = player.media.getAttribute('src'); + if (this.isHTML5) { + html5.extend.call(this); + } else if (this.isYouTube) { + youtube.setup.call(this); + } else if (this.isVimeo) { + vimeo.setup.call(this); + } + } + }; - // Get from <div> if needed - if (is.empty(source)) { - source = player.media.getAttribute(this.config.attributes.embed.id); - } + 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 - // Replace the <iframe> with a <div> due to YouTube API issues - var videoId = parseId$1(source); - var id = generateId(player.provider); + this.managerPromise = new Promise(function (resolve, reject) { + // The ad is loaded and ready + _this.on('loaded', resolve); // Ads failed - // Get poster, if already set - var poster = player.poster; - // Replace media element + _this.on('error', reject); + }); + this.load(); + } - var container = createElement('div', { id: id, poster: poster }); - player.media = replaceElement(container, player.media); + _createClass(Ads, [{ + key: "load", - // Id to poster wrapper - var posterSrc = function posterSrc(format$$1) { - return 'https://img.youtube.com/vi/' + videoId + '/' + format$$1 + 'default.jpg'; - }; + /** + * Load the IMA SDK + */ + value: function load() { + var _this2 = this; - // 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 - }); - } - } + 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 - var media = { - // Setup media - setup: function setup() { - // If there's no media, bail - if (!this.media) { - this.debug.warn('No media element found!'); - return; - } + 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 - // Add type class - toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); + this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); // Request video ads to be pre-loaded - // Add provider class - toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); + this.requestAds(); + } + /** + * Request advertisements + */ - // 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); - } + }, { + key: "requestAds", + value: function requestAds() { + var _this4 = this; - // Inject the player wrapper - if (this.isVideo) { - // Create the wrapper div - this.elements.wrapper = createElement('div', { - class: this.config.classNames.video - }); + var container = this.player.elements.container; - // Wrap the video in a container - wrap(this.media, this.elements.wrapper); + try { + // Create ads loader + this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events - // Faux poster container - this.elements.poster = createElement('div', { - class: this.config.classNames.poster - }); + 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 - this.elements.wrapper.appendChild(this.elements.poster); - } + 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 - if (this.isHTML5) { - html5.extend.call(this); - } else if (this.isYouTube) { - youtube.setup.call(this); - } else if (this.isVimeo) { - vimeo.setup.call(this); - } - } - }; + 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. - // ========================================================================== - - var Ads = 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); - }); + request.forceNonLinearFullSlot = false; // Mute based on current state - this.load(); + 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; } - 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()'); + var update = function update() { + var time = formatTime(Math.max(_this5.manager.getRemainingTime(), 0)); + var label = "".concat(i18n.get('advertisement', _this5.player.config), " - ").concat(time); - // Clear the safety timer - this.managerPromise.then(function () { - _this3.clearSafetyTimer('onAdsManagerLoaded()'); - }); + _this5.elements.container.setAttribute('data-badge-text', label); + }; - // Set listeners on the Plyr instance - this.listeners(); + this.countdownTimer = setInterval(update, 100); + } + /** + * This method is called whenever the ads are ready inside the AdDisplayContainer + * @param {Event} adsManagerLoadedEvent + */ - // Setup the IMA SDK - this.setupIMA(); - } + }, { + key: "onAdsManagerLoaded", + value: function onAdsManagerLoaded(event) { + var _this6 = this; - // Build the default tag URL + // Load could occur after a source change (race condition) + if (!this.enabled) { + return; + } // Get the ads manager - }, { - key: 'setupIMA', + var settings = new google.ima.AdsRenderingSettings(); // Tell the SDK to save and restore content video state on our behalf - /** - * 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); + 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 - // So we can run VPAID2 - google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); + this.manager = event.getAdsManager(this.player, settings); // Get the cue points for any mid-rolls by filtering out the pre- and post-roll - // Set language - google.ima.settings.setLocale(this.player.config.ads.language); + this.cuePoints = this.manager.getCuePoints(); // Add advertisement cue's within the time line if available - // 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); + 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; - // Request video ads to be pre-loaded - this.requestAds(); + 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 - /** - * 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); + this.manager.setVolume(this.player.volume); // Add listeners to the required events + // Advertisement error events - // 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); - } - } + this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { + return _this6.onAdError(error); + }); // Advertisement regular events - /** - * Update the ad countdown - * @param {boolean} start - */ + 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 - }, { - key: 'pollCountdown', - value: function pollCountdown() { - var _this5 = this; + 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); + }; - var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + 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 (!start) { - clearInterval(this.countdownTimer); - this.elements.container.removeAttribute('data-badge-text'); - return; - } + if (is.empty(_this8.cuePoints)) { + return; + } - var update = function update() { - var time = formatTime(Math.max(_this5.manager.getRemainingTime(), 0)); - var label = i18n.get('advertisement', _this5.player.config) + ' - ' + time; - _this5.elements.container.setAttribute('data-badge-text', label); - }; + _this8.cuePoints.forEach(function (cuePoint, index) { + if (time < cuePoint && cuePoint < seekedTime) { + _this8.manager.discardAdBreak(); - this.countdownTimer = setInterval(update, 100); + _this8.cuePoints.splice(index, 1); } + }); + }); // Listen to the resizing of the window. And resize ad accordingly + // TODO: eventually implement ResizeObserver - /** - * 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; - } + 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 + */ - // 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 = cuePercentage.toString() + '%'; - seekElement.appendChild(cue); - } - } - }); - } + }, { + key: "play", + value: function play() { + var _this9 = this; - // Set volume to match player - this.manager.setVolume(this.player.volume); + var container = this.player.elements.container; - // Add listeners to the required events - // Advertisement error events - this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { - return _this6.onAdError(error); - }); + if (!this.managerPromise) { + this.resumeContent(); + } // Play the requested advertisement whenever the adsManager is ready - // 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.managerPromise.then(function () { + // Initialize the container. Must be done via a user action on mobile devices + _this9.elements.displayContainer.initialize(); - /** - * 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' + 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; - } - } + 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 - /** - * 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); + _this9.manager.start(); } - /** - * 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; + _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 + */ - var container = this.player.elements.container; + }, { + key: "resumeContent", + value: function resumeContent() { + // Hide the advertisement container + this.elements.container.style.zIndex = ''; // Ad is stopped - var time = void 0; + this.playing = false; // Play our video - // Add listeners to the required events - this.player.on('ended', function () { - _this8.loader.contentComplete(); - }); + if (this.player.currentTime < this.player.duration) { + this.player.play(); + } + } + /** + * Pause our video + */ - this.player.on('seeking', function () { - time = _this8.player.currentTime; - return time; - }); + }, { + key: "pauseContent", + value: function pauseContent() { + // Show the advertisement container + this.elements.container.style.zIndex = 3; // Ad is playing. - this.player.on('seeked', function () { - var seekedTime = _this8.player.currentTime; + this.playing = true; // Pause our video. - if (is.empty(_this8.cuePoints)) { - return; - } + 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 + */ - _this8.cuePoints.forEach(function (cuePoint, index) { - if (time < cuePoint && cuePoint < seekedTime) { - _this8.manager.discardAdBreak(); - _this8.cuePoints.splice(index, 1); - } - }); - }); + }, { + key: "cancel", + value: function cancel() { + // Pause our video + if (this.initialized) { + this.resumeContent(); + } // Tell our instance that we're done for now - // 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 - */ + this.trigger('error'); // Re-create our adsManager - }, { - key: 'play', - value: function play() { - var _this9 = this; + this.loadAds(); + } + /** + * Re-create our adsManager + */ - var container = this.player.elements.container; + }, { + 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 - 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 () {}); - } + _this10.managerPromise = new Promise(function (resolve) { + _this10.on('loaded', resolve); - /** - * Resume our video - */ + _this10.player.debug.log(_this10.manager); + }); // Now request some new advertisements - }, { - key: 'resumeContent', - value: function resumeContent() { - // Hide the advertisement container - this.elements.container.style.zIndex = ''; + _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]; + } - // Ad is stopped - this.playing = false; + var handlers = this.events[event]; - // Play our video - if (this.player.currentTime < this.player.duration) { - this.player.play(); - } + 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] = []; + } - /** - * Pause our video - */ + 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)); + } + }]); - }, { - key: 'pauseContent', - value: function pauseContent() { - // Show the advertisement container - this.elements.container.style.zIndex = 3; + return Ads; + }(); - // Ad is playing. - this.playing = true; + var source = { + // Add elements to HTML5 media (source, tracks, etc) + insertElements: function insertElements(type, attributes) { + var _this = this; - // Pause our video. - this.player.pause(); - } + 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; - /** - * 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(); - } + if (!getDeep(input, 'sources.length')) { + this.debug.warn('Invalid source format'); + return; + } // Cancel current network requests - // Tell our instance that we're done for now - this.trigger('error'); - // Re-create our adsManager - this.loadAds(); - } + html5.cancelRequests.call(this); // Destroy instance and re-setup - /** - * 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 () {}); - } + this.destroy.call(this, function () { + // Reset quality options + _this2.options.quality = []; // Remove elements - /** - * Handles callbacks after an ad event was invoked - * @param {string} event - Event type - */ + removeElement(_this2.media); + _this2.media = null; // Reset class name - }, { - key: 'trigger', - value: function trigger(event) { - var _this11 = this; + if (is.element(_this2.elements.container)) { + _this2.elements.container.removeAttribute('class'); + } // Set the type and provider - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - var handlers = this.events[event]; + var sources = input.sources, + type = input.type; - if (is.array(handlers)) { - handlers.forEach(function (handler) { - if (is.function(handler)) { - handler.apply(_this11, args); - } - }); - } - } + var _sources = _slicedToArray(sources, 1), + _sources$ = _sources[0], + _sources$$provider = _sources$.provider, + provider = _sources$$provider === void 0 ? providers.html5 : _sources$$provider, + src = _sources$.src; - /** - * 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] = []; - } + 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 - this.events[event].push(callback); + _this2.elements.container.appendChild(_this2.media); // Autoplay the new source? - 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: ' + from); - - this.safetyTimer = setTimeout(function () { - _this12.cancel(); - _this12.clearSafetyTimer('startSafetyTimer()'); - }, time); - } - - /** - * Clear our safety timer(s) - * @param {string} from - */ + if (is.boolean(input.autoplay)) { + _this2.config.autoplay = input.autoplay; + } // Set attributes for audio and video - }, { - key: 'clearSafetyTimer', - value: function clearSafetyTimer(from) { - if (!is.nullOrUndefined(this.safetyTimer)) { - this.player.debug.log('Safety timer cleared from: ' + from); - clearTimeout(this.safetyTimer); - this.safetyTimer = null; - } - } - }, { - key: 'enabled', - get: function get$$1() { - return this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !is.empty(this.publisherId); - } - }, { - key: 'tagUrl', - get: function get$$1() { - 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 base + '?' + buildUrlParams(params); - } - }]); - return Ads; - }(); + if (_this2.isHTML5) { + if (_this2.config.crossorigin) { + _this2.media.setAttribute('crossorigin', ''); + } - // ========================================================================== + if (_this2.config.autoplay) { + _this2.media.setAttribute('autoplay', ''); + } - var source = { - // Add elements to HTML5 media (source, tracks, etc) - insertElements: function insertElements(type, attributes) { - var _this = this; + if (!is.empty(input.poster)) { + _this2.poster = input.poster; + } - if (is.string(attributes)) { - insertElement(type, this.media, { - src: attributes - }); - } else if (is.array(attributes)) { - attributes.forEach(function (attribute) { - insertElement(type, _this.media, attribute); - }); - } - }, + if (_this2.config.loop.active) { + _this2.media.setAttribute('loop', ''); + } + if (_this2.config.muted) { + _this2.media.setAttribute('muted', ''); + } - // Update source - // Sources are not checked for support so be careful - change: function change(input) { - var _this2 = this; + if (_this2.config.playsinline) { + _this2.media.setAttribute('playsinline', ''); + } + } // Restore class hook - if (!getDeep(input, 'sources.length')) { - this.debug.warn('Invalid source format'); - return; - } - // Cancel current network requests - html5.cancelRequests.call(this); + ui.addStyleHook.call(_this2); // Set new sources for html5 - // Destroy instance and re-setup - this.destroy.call(this, function () { - // Reset quality options - _this2.options.quality = []; + if (_this2.isHTML5) { + source.insertElements.call(_this2, 'source', sources); + } // Set video title - // Remove elements - removeElement(_this2.media); - _this2.media = null; - // Reset class name - if (is.element(_this2.elements.container)) { - _this2.elements.container.removeAttribute('class'); - } + _this2.config.title = input.title; // Set up from scratch - // 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 === undefined ? 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) - }); + media.setup.call(_this2); // HTML5 stuff - // Inject the new element - _this2.elements.container.appendChild(_this2.media); + if (_this2.isHTML5) { + // Setup captions + if ('tracks' in input) { + source.insertElements.call(_this2, 'track', input.tracks); + } // Load HTML5 sources - // 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', ''); - } - } + _this2.media.load(); + } // If HTML5 or embed but not fully supported, setupInterface and call ready now - // Restore class hook - ui.addStyleHook.call(_this2); - // Set new sources for html5 - if (_this2.isHTML5) { - source.insertElements.call(_this2, 'source', sources); - } + if (_this2.isHTML5 || _this2.isEmbed && !_this2.supported.ui) { + // Setup interface + ui.build.call(_this2); + } // Update the fullscreen support - // Set video title - _this2.config.title = input.title; - // Set up from scratch - media.setup.call(_this2); + _this2.fullscreen.update(); + }, true); + } + }; - // HTML5 stuff - if (_this2.isHTML5) { - // Setup captions - if ('tracks' in input) { - source.insertElements.call(_this2, 'track', input.tracks); - } + // TODO: Use a WeakMap for private globals + // const globals = new WeakMap(); + // Plyr instance - // Load HTML5 sources - _this2.media.load(); - } + var Plyr = + /*#__PURE__*/ + function () { + function Plyr(target, options) { + var _this = this; - // 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); - } + _classCallCheck(this, Plyr); - // Update the fullscreen support - _this2.fullscreen.update(); - }, true); - } - }; + this.timers = {}; // State - // ========================================================================== + this.ready = false; + this.loading = false; + this.failed = false; // Touch device - // Private properties - // TODO: Use a WeakMap for private globals - // const globals = new WeakMap(); + this.touch = support.touch; // Set the media element - // Plyr instance + this.media = target; // String selector passed - var Plyr = function () { - function Plyr(target, options) { - var _this = this; + if (is.string(this.media)) { + this.media = document.querySelectorAll(this.media); + } // jQuery, NodeList or Array passed, use first element - classCallCheck(this, Plyr); - this.timers = {}; + 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 - // State - this.ready = false; - this.loading = false; - this.failed = false; - // Touch device - this.touch = support.touch; + 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 - // Set the media element - this.media = target; + this.captions = { + active: null, + currentTrack: -1, + meta: new WeakMap() + }; // Fullscreen - // String selector passed - if (is.string(this.media)) { - this.media = document.querySelectorAll(this.media); - } + this.fullscreen = { + active: false + }; // Options - // 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]; - } + this.options = { + speed: [], + quality: [] + }; // Debugging + // TODO: move to globals - // Set config - this.config = extend({}, defaults$1, 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: {} - } - }; + this.debug = new Console(this.config.debug); // Log config options and support - // Captions - this.captions = { - active: null, - currentTrack: -1, - meta: new WeakMap() - }; + this.debug.log('Config', this.config); + this.debug.log('Support', support); // We need an element to setup - // Fullscreen - this.fullscreen = { - active: false - }; + 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 - // Options - this.options = { - speed: [], - quality: [] - }; - // Debugging - // TODO: move to globals - this.debug = new Console(this.config.debug); + if (this.media.plyr) { + this.debug.warn('Target already setup'); + return; + } // Bail if not enabled - // 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; - } + 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 - // 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; - } + if (!support.check().api) { + this.debug.error('Setup failed: no support'); + return; + } // Cache original element state for .destroy() - // 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; - } + 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 - // Check for support again but with type - this.supported = support.check(this.type, this.provider, this.config.playsinline); + var type = this.media.tagName.toLowerCase(); // Embed properties - // If no support for even API, bail - if (!this.supported.api) { - this.debug.error('Setup failed: no support'); - return; - } + var iframe = null; + var url = null; // Different setup based on type - this.eventListeners = []; + switch (type) { + case 'div': + // Find the frame + iframe = this.media.querySelector('iframe'); // <iframe> type - // Create listeners - this.listeners = new Listeners(this); + if (is.element(iframe)) { + // Detect provider + url = parseUrl(iframe.getAttribute('src')); + this.provider = getProviderByUrl(url.toString()); // Rework elements - // Setup local storage for user settings - this.storage = new Storage(this); + this.elements.container = this.media; + this.media = iframe; // Reset classname - // Store reference - this.media.plyr = this; + this.elements.container.className = ''; // Get attributes from URL and set config - // Wrap media - if (!is.element(this.elements.container)) { - this.elements.container = createElement('div'); - wrap(this.media, this.elements.container); - } + if (url.search.length) { + var truthy = ['1', 'true']; - // Add style hook - ui.addStyleHook.call(this); + if (truthy.includes(url.searchParams.get('autoplay'))) { + this.config.autoplay = true; + } - // Setup media - media.setup.call(this); + 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 - // 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: ' + 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); + 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 - // Container listeners - this.listeners.container(); - - // Global listeners - this.listeners.global(); + this.media.removeAttribute(this.config.attributes.embed.provider); + } // Unsupported or missing provider - // 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(); - } - } - - // --------------------------------------- - // API - // --------------------------------------- - - /** - * Types and provider helpers - */ - - - createClass(Plyr, [{ - key: 'play', + 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 - /** - * Play the media, or play the advertisement (if they are not blocked) - */ - value: function play() { - if (!is.function(this.media.play)) { - return null; - } + this.type = types.video; + break; - // Return the promise (for HTML5) - return this.media.play(); - } + case 'video': + case 'audio': + this.type = type; + this.provider = providers.html5; // Get config from attributes - /** - * Pause the media - */ + if (this.media.hasAttribute('crossorigin')) { + this.config.crossorigin = true; + } - }, { - key: 'pause', - value: function pause() { - if (!this.playing || !is.function(this.media.pause)) { - return; - } + if (this.media.hasAttribute('autoplay')) { + this.config.autoplay = true; + } - this.media.pause(); - } + if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) { + this.config.playsinline = true; + } - /** - * Get playing state - */ + if (this.media.hasAttribute('muted')) { + this.config.muted = true; + } - }, { - key: 'togglePlay', + if (this.media.hasAttribute('loop')) { + this.config.loop.active = true; + } + break; - /** - * 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; + default: + this.debug.error('Setup failed: unsupported type'); + return; + } // Check for support again but with type - 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(); - } - } + this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail - /** - * Restart playback - */ + if (!this.supported.api) { + this.debug.error('Setup failed: no support'); + return; + } - }, { - key: 'restart', - value: function restart() { - this.currentTime = 0; - } + this.eventListeners = []; // Create listeners - /** - * Rewind - * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime - */ + this.listeners = new Listeners(this); // Setup local storage for user settings - }, { - key: 'rewind', - value: function rewind(seekTime) { - this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime); - } + this.storage = new Storage(this); // Store reference - /** - * Fast forward - * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime - */ + this.media.plyr = this; // Wrap media - }, { - key: 'forward', - value: function forward(seekTime) { - this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime); - } + if (!is.element(this.elements.container)) { + this.elements.container = createElement('div'); + wrap(this.media, this.elements.container); + } // Add style hook - /** - * Seek to a time - * @param {number} input - where to seek to in seconds. Defaults to 0 (the start) - */ - }, { - key: 'increaseVolume', + ui.addStyleHook.call(this); // Setup media + media.setup.call(this); // Listen for events if debugging - /** - * 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); - } + 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 - /** - * Decrease volume - * @param {boolean} step - How much to decrease by (between 0 and 1) - */ - }, { - key: 'decreaseVolume', - value: function decreaseVolume(step) { - this.increaseVolume(-step); - } + if (this.isHTML5 || this.isEmbed && !this.supported.ui) { + ui.build.call(this); + } // Container listeners - /** - * Set muted state - * @param {boolean} mute - */ - }, { - key: 'toggleCaptions', + this.listeners.container(); // Global listeners + this.listeners.global(); // Setup fullscreen - /** - * Toggle captions - * @param {boolean} input - Whether to enable captions - */ - value: function toggleCaptions(input) { - captions.toggle.call(this, input, false); - } + this.fullscreen = new Fullscreen(this); // Setup ads if provided - /** - * Set the caption track by index - * @param {number} - Caption index - */ + if (this.config.ads.enabled) { + this.ads = new Ads(this); + } // Autoplay if required - }, { - key: 'airplay', + if (this.config.autoplay) { + this.play(); + } + } // --------------------------------------- + // API + // --------------------------------------- - /** - * Trigger the airplay dialog - * TODO: update player with state, support, enabled - */ - value: function airplay() { - // Show dialog if supported - if (support.airplay) { - this.media.webkitShowPlaybackTargetPicker(); - } - } + /** + * Types and provider helpers + */ - /** - * 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; - } + _createClass(Plyr, [{ + key: "play", - /** - * Add event listeners - * @param {string} event - Event type - * @param {function} callback - Callback for when event occurs - */ + /** + * 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) - }, { - 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 - */ + return this.media.play(); + } + /** + * Pause the media + */ + + }, { + key: "pause", + value: function pause() { + if (!this.playing || !is.function(this.media.pause)) { + return; + } - }, { - key: 'once', - value: function once$$1(event, callback) { - once.call(this, this.elements.container, event, callback); - } + this.media.pause(); + } + /** + * Get playing state + */ - /** - * Remove event listeners - * @param {string} event - Event type - * @param {function} callback - Callback for when event occurs - */ + }, { + key: "togglePlay", - }, { - key: 'off', - value: function off$$1(event, callback) { - off(this.elements.container, event, callback); - } + /** + * 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; - /** - * 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) - */ + 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: 'destroy', - value: function destroy(callback) { - var _this2 = this; + }, { + 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 soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state - if (!this.ready) { - return; - } + var hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); // Close menu - 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); - } - } + if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) { + controls.toggleMenu.call(this, false); + } // Trigger event on change - /** - * 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); - } + if (hiding !== isHidden) { + var eventName = hiding ? 'controlshidden' : 'controlsshown'; + triggerEvent.call(this, this.media, eventName); + } - /** - * 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$$1() { - return Boolean(this.provider === providers.html5); - } - }, { - key: 'isEmbed', - get: function get$$1() { - return Boolean(this.isYouTube || this.isVimeo); - } - }, { - key: 'isYouTube', - get: function get$$1() { - return Boolean(this.provider === providers.youtube); - } - }, { - key: 'isVimeo', - get: function get$$1() { - return Boolean(this.provider === providers.vimeo); - } - }, { - key: 'isVideo', - get: function get$$1() { - return Boolean(this.type === types.video); - } - }, { - key: 'isAudio', - get: function get$$1() { - return Boolean(this.type === types.audio); - } - }, { - key: 'playing', - get: function get$$1() { - return Boolean(this.ready && !this.paused && !this.ended); - } + return !hiding; + } - /** - * Get paused state - */ + 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; - }, { - key: 'paused', - get: function get$$1() { - return Boolean(this.media.paused); - } + var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - /** - * Get stopped state - */ + if (!this.ready) { + return; + } - }, { - key: 'stopped', - get: function get$$1() { - return Boolean(this.paused && this.currentTime === 0); - } + var done = function done() { + // Reset overflow (incase destroyed while in fullscreen) + document.body.style.overflow = ''; // GC for embed - /** - * Get ended state - */ + _this2.embed = null; // If it's a soft destroy, make minimal changes - }, { - key: 'ended', - get: function get$$1() { - return Boolean(this.media.ended); - } - }, { - key: 'currentTime', - set: function set$$1(input) { - // Bail if media duration isn't available yet - if (!this.duration) { - return; - } + 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 - // Validate input - var inputIsValid = is.number(input) && input > 0; + _this2.elements.buttons.play = null; + _this2.elements.captions = null; + _this2.elements.controls = null; + _this2.elements.wrapper = null; + } // Callback - // Set - this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; - // Logging - this.debug.log('Seeking to ' + this.currentTime + ' seconds'); + if (is.function(callback)) { + callback(); } + } else { + // Unbind listeners + unbindListeners.call(_this2); // Replace the container with the original element provided - /** - * Get current time - */ - , - get: function get$$1() { - return Number(this.media.currentTime); - } + replaceElement(_this2.elements.original, _this2.elements.container); // Event - /** - * Get buffered - */ + triggerEvent.call(_this2, _this2.elements.original, 'destroyed', true); // Callback - }, { - key: 'buffered', - get: function get$$1() { - var buffered = this.media.buffered; + if (is.function(callback)) { + callback.call(_this2.elements.original); + } // Reset state - // YouTube / Vimeo return a float between 0-1 - if (is.number(buffered)) { - return buffered; - } + _this2.ready = false; // Clear for garbage collection - // 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; - } + setTimeout(function () { + _this2.elements = null; + _this2.media = null; + }, 200); + } + }; // Stop playback - return 0; - } - /** - * Get seeking status - */ + this.stop(); // Provider specific stuff - }, { - key: 'seeking', - get: function get$$1() { - return Boolean(this.media.seeking); - } + if (this.isHTML5) { + // Clear timeout + clearTimeout(this.timers.loading); // Restore native video controls - /** - * Get the duration of the current media - */ + ui.toggleNativeControls.call(this, true); // Clean up - }, { - key: 'duration', - get: function get$$1() { - // Faux duration set via config - var fauxDuration = parseFloat(this.config.duration); + done(); + } else if (this.isYouTube) { + // Clear timers + clearInterval(this.timers.buffering); + clearInterval(this.timers.playing); // Destroy YouTube API - // 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 (this.embed !== null && is.function(this.embed.destroy)) { + this.embed.destroy(); + } // Clean up - // 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 - */ + 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 - }, { - key: 'volume', - set: function set$$1(value) { - var volume = value; - var max = 1; - var min = 0; - if (is.string(volume)) { - volume = Number(volume); - } + 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 - // 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; - } + var inputIsValid = is.number(input) && input > 0; // Set - // Maximum is volumeMax - if (volume > max) { - volume = max; - } - // Minimum is volumeMin - if (volume < min) { - volume = min; - } + this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging - // Update config - this.config.volume = volume; + this.debug.log("Seeking to ".concat(this.currentTime, " seconds")); + } + /** + * Get current time + */ + , + get: function get() { + return Number(this.media.currentTime); + } + /** + * Get buffered + */ - // Set the player volume - this.media.volume = volume; + }, { + key: "buffered", + get: function get() { + var buffered = this.media.buffered; // YouTube / Vimeo return a float between 0-1 - // If muted, and we're increasing volume manually, reset muted state - if (!is.empty(value) && this.muted && volume > 0) { - this.muted = false; - } - } + if (is.number(buffered)) { + return buffered; + } // HTML5 + // TODO: Handle buffered chunks of the media + // (i.e. seek to another section buffers only that section) - /** - * Get the current player volume - */ - , - get: function get$$1() { - return Number(this.media.volume); - } - }, { - key: 'muted', - set: function set$$1(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; - } + if (buffered && buffered.length && this.duration > 0) { + return buffered.end(0) / this.duration; + } - // Update config - this.config.muted = toggle; + return 0; + } + /** + * Get seeking status + */ + + }, { + key: "seeking", + get: function get() { + return Boolean(this.media.seeking); + } + /** + * Get the duration of the current media + */ - // Set mute on the player - this.media.muted = toggle; - } + }, { + 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 - /** - * Get current muted state - */ - , - get: function get$$1() { - return Boolean(this.media.muted); - } + var realDuration = (this.media || {}).duration; + var duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; // If config duration is funky, use regular duration - /** - * Check if the media has audio - */ + 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: 'hasAudio', - get: function get$$1() { - // Assume yes for all non HTML5 (as we can't tell...) - if (!this.isHTML5) { - return true; - } + }, { + key: "volume", + set: function set(value) { + var volume = value; + var max = 1; + var min = 0; - if (this.isAudio) { - return true; - } + if (is.string(volume)) { + volume = Number(volume); + } // Load volume from storage if no value specified - // 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) - */ + if (!is.number(volume)) { + volume = this.storage.get('volume'); + } // Use config if all else fails - }, { - key: 'speed', - set: function set$$1(input) { - var speed = null; - if (is.number(input)) { - speed = input; - } + if (!is.number(volume)) { + volume = this.config.volume; + } // Maximum is volumeMax - if (!is.number(speed)) { - speed = this.storage.get('speed'); - } - if (!is.number(speed)) { - speed = this.config.speed.selected; - } + if (volume > max) { + volume = max; + } // Minimum is volumeMin - // 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 (' + speed + ')'); - return; - } + if (volume < min) { + volume = min; + } // Update config - // Update config - this.config.speed.selected = speed; - // Set media speed - this.media.playbackRate = speed; - } + this.config.volume = volume; // Set the player volume - /** - * Get current playback speed - */ - , - get: function get$$1() { - return Number(this.media.playbackRate); - } + this.media.volume = volume; // If muted, and we're increasing volume manually, reset muted state - /** - * Set playback quality - * Currently HTML5 & YouTube only - * @param {number} input - Quality level - */ + 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 - }, { - key: 'quality', - set: function set$$1(input) { - var config = this.config.quality; - var options = this.options.quality; + if (!is.boolean(toggle)) { + toggle = this.storage.get('muted'); + } // Use config if all else fails - if (!options.length) { - return; - } - var quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number); + if (!is.boolean(toggle)) { + toggle = this.config.muted; + } // Update config - if (!options.includes(quality)) { - var value = closest(options, quality); - this.debug.warn('Unsupported quality option: ' + quality + ', using ' + value + ' instead'); - quality = value; - } - // Update config - config.selected = quality; + this.config.muted = toggle; // Set mute on the player - // Set quality - this.media.quality = quality; - } + 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; + } - /** - * Get current quality level - */ - , - get: function get$$1() { - return this.media.quality; - } + if (this.isAudio) { + return true; + } // Get audio tracks - /** - * 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$$1(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$$1() { - return Boolean(this.media.loop); - } + 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; + } - /** - * Set new media source - * @param {object} input - The new source object (see docs) - */ + if (!is.number(speed)) { + speed = this.storage.get('speed'); + } - }, { - key: 'source', - set: function set$$1(input) { - source.change.call(this, input); - } + if (!is.number(speed)) { + speed = this.config.speed.selected; + } // Set min/max - /** - * Get current source - */ - , - get: function get$$1() { - return this.media.currentSrc; - } - /** - * Set the poster image for a video - * @param {input} - the URL for the new poster image - */ - - }, { - key: 'poster', - set: function set$$1(input) { - if (!this.isVideo) { - this.debug.warn('Poster can only be set for video'); - return; - } + if (speed < 0.1) { + speed = 0.1; + } - ui.setPoster.call(this, input, false).catch(function () {}); - } + if (speed > 2.0) { + speed = 2.0; + } - /** - * Get the current poster image - */ - , - get: function get$$1() { - if (!this.isVideo) { - return null; - } + if (!this.config.speed.options.includes(speed)) { + this.debug.warn("Unsupported speed (".concat(speed, ")")); + return; + } // Update config - return this.media.getAttribute('poster'); - } - /** - * Set the autoplay state - * @param {boolean} input - Whether to autoplay or not - */ + this.config.speed.selected = speed; // Set media speed - }, { - key: 'autoplay', - set: function set$$1(input) { - var toggle = is.boolean(input) ? input : this.config.autoplay; - this.config.autoplay = toggle; - } + 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; + } - /** - * Get the current autoplay state - */ - , - get: function get$$1() { - return Boolean(this.config.autoplay); - } - }, { - key: 'currentTrack', - set: function set$$1(input) { - captions.set.call(this, input, false); - } + var quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number); - /** - * Get the current caption track index (-1 if disabled) - */ - , - get: function get$$1() { - var _captions = this.captions, - toggled = _captions.toggled, - currentTrack = _captions.currentTrack; + if (!options.includes(quality)) { + var value = closest(options, quality); + this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead")); + quality = value; + } // Update config - 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) - */ + config.selected = quality; // Set quality - }, { - key: 'language', - set: function set$$1(input) { - captions.setLanguage.call(this, input, false); - } + this.media.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; + } + /** + * 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; + } - /** - * Get the current track's language - */ - , - get: function get$$1() { - return (captions.getCurrentTrack.call(this) || {}).language; - } + ui.setPoster.call(this, input, false).catch(function () {}); + } + /** + * Get the current poster image + */ + , + get: function get() { + if (!this.isVideo) { + return null; + } - /** - * Toggle picture-in-picture playback on WebKit/MacOS - * TODO: update player with state, support, enabled - * TODO: detect outside changes - */ - - }, { - key: 'pip', - set: function set$$1(input) { - var states = { - pip: 'picture-in-picture', - inline: 'inline' - }; - - // Bail if no support - if (!support.pip) { - return; - } + 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 + */ - // Toggle based on current state if not passed - var toggle = is.boolean(input) ? input : this.pip === states.inline; + }, { + key: "pip", + set: function set(input) { + var states = { + pip: 'picture-in-picture', + inline: 'inline' + }; // Bail if no support - // Toggle based on current state - this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); - } + if (!support.pip) { + return; + } // Toggle based on current state if not passed - /** - * Get the current picture-in-picture state - */ - , - get: function get$$1() { - if (!support.pip) { - return null; - } - return this.media.webkitPresentationMode; - } - }], [{ - key: 'supported', - value: function supported(type, provider, inline) { - return support.check(type, provider, inline); - } + var toggle = is.boolean(input) ? input : this.pip === states.inline; // Toggle based on current state - /** - * Load an SVG sprite into the page - * @param {string} url - URL for the SVG sprite - * @param {string} [id] - Unique ID - */ + this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline); + } + /** + * Get the current picture-in-picture state + */ + , + get: function get() { + if (!support.pip) { + return null; + } - }, { - key: 'loadSprite', - value: function loadSprite$$1(url, id) { - return loadSprite(url, id); - } + return this.media.webkitPresentationMode; + } + }], [{ + 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); + } - /** - * 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; + } - if (is.empty(targets)) { - return null; - } + return targets.map(function (t) { + return new Plyr(t, options); + }); + } + }]); - return targets.map(function (t) { - return new Plyr(t, options); - }); - } - }]); - return Plyr; - }(); + return Plyr; + }(); - Plyr.defaults = cloneDeep(defaults$1); + Plyr.defaults = cloneDeep(defaults); - return Plyr; + return Plyr; }))); |