(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define('Plyr', factory) : (global.Plyr = factory()); }(this, (function () { 'use strict'; var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var loadjs_umd = createCommonjsModule(function (module, exports) { (function(root, factory) { 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 = {}; /** * Subscribe to bundle load event. * @param {string[]} bundleIds - Bundle ids * @param {Function} callbackFn - The callback function */ function subscribe(bundleIds, callbackFn) { // listify bundleIds = bundleIds.push ? bundleIds : [bundleIds]; var depsNotFound = [], i = bundleIds.length, numWaiting = i, fn, bundleId, r, q; // define callback function fn = function (bundleId, pathsNotFound) { if (pathsNotFound.length) depsNotFound.push(bundleId); numWaiting--; if (!numWaiting) callbackFn(depsNotFound); }; // register callback while (i--) { bundleId = bundleIds[i]; // execute callback if in result cache r = bundleResultCache[bundleId]; if (r) { fn(bundleId, r); continue; } // add to callback queue q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; q.push(fn); } } /** * Publish bundle load event. * @param {string} bundleId - Bundle id * @param {string[]} pathsNotFound - List of files not found */ function publish(bundleId, pathsNotFound) { // exit if id isn't defined if (!bundleId) return; var q = bundleCallbackQueue[bundleId]; // cache result bundleResultCache[bundleId] = pathsNotFound; // exit if queue is empty if (!q) return; // empty callback queue while (q.length) { q[0](bundleId, pathsNotFound); q.splice(0, 1); } } /** * Execute callbacks. * @param {Object or Function} args - The callback args * @param {string[]} depsNotFound - List of dependencies not found */ function executeCallbacks(args, depsNotFound) { // accept function as argument if (args.call) args = {success: args}; // success and error callbacks if (depsNotFound.length) (args.error || devnull)(depsNotFound); else (args.success || devnull)(args); } /** * Load individual file. * @param {string} path - The file path * @param {Function} callbackFn - The callback function */ function loadFile(path, callbackFn, args, numTries) { var doc = document, async = args.async, maxTries = (args.numRetries || 0) + 1, beforeCallbackFn = args.before || devnull, pathStripped = path.replace(/^(css|img)!/, ''), isCss, e; numTries = numTries || 0; if (/(^css!|\.css$)/.test(path)) { isCss = true; // css e = doc.createElement('link'); e.rel = 'stylesheet'; e.href = pathStripped; //.replace(/^css!/, ''); // remove "css!" prefix } else if (/(^img!|\.(png|gif|jpg|svg)$)/.test(path)) { // image e = doc.createElement('img'); e.src = pathStripped; } else { // javascript e = doc.createElement('script'); e.src = path; e.async = async === undefined ? true : async; } e.onload = e.onerror = e.onbeforeload = function (ev) { var result = ev.type[0]; // Note: The following code isolates IE using `hideFocus` and treats empty // stylesheets as failures to get around lack of onerror support if (isCss && 'hideFocus' in e) { try { if (!e.sheet.cssText.length) result = 'e'; } catch (x) { // sheets objects created from load errors don't allow access to // `cssText` result = 'e'; } } // handle retries in case of load failure if (result == 'e') { // increment counter numTries += 1; // exit function and try again if (numTries < maxTries) { return loadFile(path, callbackFn, args, numTries); } } // execute callback callbackFn(path, result, ev.defaultPrevented); }; // add to document (unless callback returns `false`) if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); } /** * Load multiple files. * @param {string[]} paths - The file paths * @param {Function} callbackFn - The callback function */ function loadFiles(paths, callbackFn, args) { // listify paths paths = paths.push ? paths : [paths]; var numWaiting = paths.length, x = numWaiting, pathsNotFound = [], fn, i; // define callback function fn = function(path, result, defaultPrevented) { // handle error if (result == 'e') pathsNotFound.push(path); // handle beforeload event. If defaultPrevented then that means the load // will be blocked (ex. Ghostery/ABP on Safari) if (result == 'b') { if (defaultPrevented) pathsNotFound.push(path); else return; } numWaiting--; if (!numWaiting) callbackFn(pathsNotFound); }; // load scripts for (i=0; i < x; i++) loadFile(paths[i], fn, args); } /** * Initiate script load and register bundle. * @param {(string|string[])} paths - The file paths * @param {(string|Function)} [arg1] - The bundleId or success callback * @param {Function} [arg2] - The success or error callback * @param {Function} [arg3] - The error callback */ function loadjs(paths, arg1, arg2) { var bundleId, args; // bundleId (if string) if (arg1 && arg1.trim) bundleId = arg1; // args (default is {}) args = (bundleId ? arg2 : arg1) || {}; // throw error if bundle is already defined if (bundleId) { if (bundleId in bundleIdCache) { throw "LoadJS"; } else { bundleIdCache[bundleId] = true; } } // load scripts loadFiles(paths, function (pathsNotFound) { // execute callbacks executeCallbacks(args, pathsNotFound); // publish bundle load event publish(bundleId, pathsNotFound); }, args); } /** * Execute callbacks when dependencies have been satisfied. * @param {(string|string[])} deps - List of bundle ids * @param {Object} args - success/error arguments */ loadjs.ready = function ready(deps, args) { // subscribe to bundle load event subscribe(deps, function (depsNotFound) { // execute callbacks executeCallbacks(args, depsNotFound); }); return loadjs; }; /** * Manually satisfy bundle dependencies. * @param {string} bundleId - The bundle id */ loadjs.done = function done(bundleId) { publish(bundleId, []); }; /** * Reset loadjs dependencies statuses */ loadjs.reset = function reset() { bundleIdCache = {}; bundleResultCache = {}; bundleCallbackQueue = {}; }; /** * Determine if bundle has already been defined * @param String} bundleId - The bundle id */ loadjs.isDefined = function isDefined(bundleId) { return bundleId in bundleIdCache; }; // export return loadjs; })); }); // ========================================================================== // Plyr supported types and providers // ========================================================================== var providers = { html5: 'html5', youtube: 'youtube', vimeo: 'vimeo' }; var types = { audio: 'audio', video: 'video' }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 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); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return 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; } return obj; }; var slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } 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 toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; // ========================================================================== var utils = { // Check variable types is: { object: function object(input) { return this.getConstructor(input) === Object; }, number: function number(input) { return this.getConstructor(input) === Number && !Number.isNaN(input); }, string: function string(input) { return this.getConstructor(input) === String; }, boolean: function boolean(input) { return this.getConstructor(input) === Boolean; }, function: function _function(input) { return this.getConstructor(input) === Function; }, array: function array(input) { return !this.nullOrUndefined(input) && Array.isArray(input); }, weakMap: function weakMap(input) { return this.instanceof(input, WeakMap); }, nodeList: function nodeList(input) { return this.instanceof(input, NodeList); }, element: function element(input) { return this.instanceof(input, Element); }, textNode: function textNode(input) { return this.getConstructor(input) === Text; }, event: function event(input) { return this.instanceof(input, Event); }, cue: function cue(input) { return this.instanceof(input, TextTrackCue) || this.instanceof(input, VTTCue); }, track: function track(input) { return this.instanceof(input, TextTrack) || !this.nullOrUndefined(input) && this.string(input.kind); }, url: function url(input) { return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input); }, nullOrUndefined: function nullOrUndefined(input) { return input === null || typeof input === 'undefined'; }, empty: function empty(input) { return this.nullOrUndefined(input) || (this.string(input) || this.array(input) || this.nodeList(input)) && !input.length || this.object(input) && !Object.keys(input).length; }, instanceof: function _instanceof$$1(input, constructor) { return Boolean(input && constructor && input instanceof constructor); }, getConstructor: function getConstructor(input) { return !this.nullOrUndefined(input) ? input.constructor : null; } }, // Unfortunately, due to mixed support, UA sniffing is required getBrowser: function getBrowser() { return { isIE: /* @cc_on!@ */false || !!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) }; }, // Fetch wrapper // Using XHR to avoid issues with older browsers fetch: function fetch(url) { var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text'; return new Promise(function (resolve, reject) { try { var request = new XMLHttpRequest(); // Check for CORS support if (!('withCredentials' in request)) { return; } request.addEventListener('load', function () { if (responseType === 'text') { try { resolve(JSON.parse(request.responseText)); } catch (e) { resolve(request.responseText); } } else { resolve(request.response); } }); request.addEventListener('error', function () { throw new Error(request.statusText); }); request.open('GET', url, true); // Set the required response type request.responseType = responseType; request.send(); } catch (e) { reject(e); } }); }, // Load an external script loadScript: function loadScript(url) { return new Promise(function (resolve, reject) { loadjs_umd(url, { success: resolve, error: reject }); }); }, // Load an external SVG sprite loadSprite: function loadSprite(url, id) { if (!utils.is.string(url)) { return; } var prefix = 'cache-'; var hasId = utils.is.string(id); var isCached = false; var exists = function exists() { return document.querySelectorAll('#' + id).length; }; function injectSprite(data) { // Check again incase of race condition if (hasId && exists()) { return; } // Inject content this.innerHTML = data; // Inject the SVG to the body document.body.insertBefore(this, document.body.childNodes[0]); } // Only load once if ID set if (!hasId || !exists()) { // Create container var container = document.createElement('div'); utils.toggleHidden(container, true); if (hasId) { container.setAttribute('id', id); } // Check in cache if (support.storage) { var cached = window.localStorage.getItem(prefix + id); isCached = cached !== null; if (isCached) { var data = JSON.parse(cached); injectSprite.call(container, data.content); return; } } // Get the sprite utils.fetch(url).then(function (result) { if (utils.is.empty(result)) { return; } if (support.storage) { window.localStorage.setItem(prefix + id, JSON.stringify({ content: result })); } injectSprite.call(container, result); }).catch(function () {}); } }, // Generate a random ID generateId: function generateId(prefix) { return prefix + '-' + Math.floor(Math.random() * 10000); }, // Wrap an element wrap: function wrap(elements, wrapper) { // Convert `elements` to an array, if necessary. var targets = elements.length ? elements : [elements]; // Loops backwards to prevent having to clone the wrapper on the // first element (see `child` below). Array.from(targets).reverse().forEach(function (element, index) { var child = index > 0 ? wrapper.cloneNode(true) : wrapper; // Cache the current parent and sibling. var parent = element.parentNode; var sibling = element.nextSibling; // Wrap the element (is automatically removed from its current // parent). child.appendChild(element); // If the element had a sibling, insert the wrapper before // the sibling to maintain the HTML structure; otherwise, just // append it to the parent. if (sibling) { parent.insertBefore(child, sibling); } else { parent.appendChild(child); } }); }, // Create a DocumentFragment createElement: function createElement(type, attributes, text) { // Create a new var element = document.createElement(type); // Set all passed attributes if (utils.is.object(attributes)) { utils.setAttributes(element, attributes); } // Add text node if (utils.is.string(text)) { element.textContent = text; } // Return built element return element; }, // Inaert an element after another insertAfter: function insertAfter(element, target) { target.parentNode.insertBefore(element, target.nextSibling); }, // Insert a DocumentFragment insertElement: function insertElement(type, parent, attributes, text) { // Inject the new parent.appendChild(utils.createElement(type, attributes, text)); }, // Remove element(s) removeElement: function removeElement(element) { if (utils.is.nodeList(element) || utils.is.array(element)) { Array.from(element).forEach(utils.removeElement); return; } if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { return; } element.parentNode.removeChild(element); }, // Remove all child elements emptyElement: function emptyElement(element) { var length = element.childNodes.length; while (length > 0) { element.removeChild(element.lastChild); length -= 1; } }, // Replace element replaceElement: function replaceElement(newChild, oldChild) { if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) { return null; } oldChild.parentNode.replaceChild(newChild, oldChild); return newChild; }, // Set attributes setAttributes: function setAttributes(element, attributes) { if (!utils.is.element(element) || utils.is.empty(attributes)) { return; } Object.entries(attributes).forEach(function (_ref) { var _ref2 = slicedToArray(_ref, 2), key = _ref2[0], value = _ref2[1]; element.setAttribute(key, value); }); }, // Get an attribute object from a string selector getAttributesFromSelector: function getAttributesFromSelector(sel, existingAttributes) { // For example: // '.test' to { class: 'test' } // '#test' to { id: 'test' } // '[data-test="test"]' to { 'data-test': 'test' } if (!utils.is.string(sel) || utils.is.empty(sel)) { return {}; } var attributes = {}; var existing = existingAttributes; sel.split(',').forEach(function (s) { // Remove whitespace var selector = s.trim(); var className = selector.replace('.', ''); var stripped = selector.replace(/[[\]]/g, ''); // Get the parts and value var parts = stripped.split('='); var key = parts[0]; var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; // Get the first character var start = selector.charAt(0); switch (start) { case '.': // Add to existing classname if (utils.is.object(existing) && utils.is.string(existing.class)) { existing.class += ' ' + className; } attributes.class = className; break; case '#': // ID selector attributes.id = selector.replace('#', ''); break; case '[': // Attribute selector attributes[key] = value; break; default: break; } }); return attributes; }, // Toggle hidden toggleHidden: function toggleHidden(element, hidden) { if (!utils.is.element(element)) { return; } var hide = hidden; if (!utils.is.boolean(hide)) { hide = !element.hasAttribute('hidden'); } if (hide) { element.setAttribute('hidden', ''); } else { element.removeAttribute('hidden'); } }, // Toggle class on an element toggleClass: function toggleClass(element, className, toggle) { if (utils.is.element(element)) { var contains = element.classList.contains(className); element.classList[toggle ? 'add' : 'remove'](className); return toggle && !contains || !toggle && contains; } return null; }, // Has class name hasClass: function hasClass(element, className) { return utils.is.element(element) && element.classList.contains(className); }, // Element matches selector matches: function matches(element, selector) { var prototype = { Element: Element }; function match() { return Array.from(document.querySelectorAll(selector)).includes(this); } var matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; return matches.call(element, selector); }, // Find all elements getElements: function getElements(selector) { return this.elements.container.querySelectorAll(selector); }, // Find a single element getElement: function getElement(selector) { return this.elements.container.querySelector(selector); }, // Get the focused element getFocusElement: function getFocusElement() { var focused = document.activeElement; if (!focused || focused === document.body) { focused = null; } else { focused = document.querySelector(':focus'); } return focused; }, // Trap focus inside container trapFocus: function trapFocus() { var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (!utils.is.element(element)) { return; } var focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]'); var first = focusable[0]; var last = focusable[focusable.length - 1]; var trap = function trap(event) { // Bail if not tab key or not fullscreen if (event.key !== 'Tab' || event.keyCode !== 9) { return; } // Get the current focused element var focused = utils.getFocusElement(); 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 (toggle) { utils.on(this.elements.container, 'keydown', trap, false); } else { utils.off(this.elements.container, 'keydown', trap, false); } }, // Toggle event listener toggleListener: function toggleListener(elements, event, callback) { var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false; // Bail if no elemetns, event, or callback if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) { return; } // If a nodelist is passed, call itself on each node if (utils.is.nodeList(elements) || utils.is.array(elements)) { // Create listener for each node Array.from(elements).forEach(function (element) { if (element instanceof Node) { utils.toggleListener.call(null, element, event, callback, toggle, passive, capture); } }); 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 (support.passiveListeners) { 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) { elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); }); }, // Bind event handler on: 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; utils.toggleListener(element, events, callback, true, passive, capture); }, // Unbind event handler off: 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; utils.toggleListener(element, events, callback, false, passive, capture); }, // Trigger event dispatchEvent: function dispatchEvent(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 (!utils.is.element(element) || utils.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); }, // Toggle aria-pressed state on a toggle button // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles toggleState: function toggleState(element, input) { // If multiple elements passed if (utils.is.array(element) || utils.is.nodeList(element)) { Array.from(element).forEach(function (target) { return utils.toggleState(target, input); }); return; } // Bail if no target if (!utils.is.element(element)) { return; } // Get state var pressed = element.getAttribute('aria-pressed') === 'true'; var state = utils.is.boolean(input) ? input : !pressed; // Set the attribute on target element.setAttribute('aria-pressed', state); }, // Format string format: 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]; } if (utils.is.empty(input)) { return input; } return input.toString().replace(/{(\d+)}/g, function (match, i) { return utils.is.string(args[i]) ? args[i] : ''; }); }, // Get percentage getPercentage: function getPercentage(current, max) { if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { return 0; } return (current / max * 100).toFixed(2); }, // Time helpers getHours: function getHours(value) { return parseInt(value / 60 / 60 % 60, 10); }, getMinutes: function getMinutes(value) { return parseInt(value / 60 % 60, 10); }, getSeconds: function getSeconds(value) { return parseInt(value % 60, 10); }, // Format time to UI friendly string formatTime: 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 (!utils.is.number(time)) { return this.formatTime(null, displayHours, inverted); } // Format time component to add leading zero var format = function format(value) { return ('0' + value).slice(-2); }; // Breakdown to hours, mins, secs var hours = this.getHours(time); var mins = this.getMinutes(time); var secs = this.getSeconds(time); // Do we need to display hours? if (displayHours || hours > 0) { hours = hours + ':'; } else { hours = ''; } // Render return '' + (inverted ? '-' : '') + hours + format(mins) + ':' + format(secs); }, // Replace all occurances of a string in a string replaceAll: 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 toTitleCase: 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 toPascalCase: function toPascalCase() { var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var string = input.toString(); // Convert kebab case string = utils.replaceAll(string, '-', ' '); // Convert snake case string = utils.replaceAll(string, '_', ' '); // Convert to title case string = utils.toTitleCase(string); // Convert to pascal case return utils.replaceAll(string, ' ', ''); }, // Convert string to pascalCase toCamelCase: function toCamelCase() { var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var string = input.toString(); // Convert to pascal case string = utils.toPascalCase(string); // Convert first character to lowercase return string.charAt(0).toLowerCase() + string.slice(1); }, // Deep extend destination object with N more objects extend: function extend() { var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; for (var _len2 = arguments.length, sources = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { sources[_key2 - 1] = arguments[_key2]; } if (!sources.length) { return target; } var source = sources.shift(); if (!utils.is.object(source)) { return target; } Object.keys(source).forEach(function (key) { if (utils.is.object(source[key])) { if (!Object.keys(target).includes(key)) { Object.assign(target, defineProperty({}, key, {})); } utils.extend(target[key], source[key]); } else { Object.assign(target, defineProperty({}, key, source[key])); } }); return utils.extend.apply(utils, [target].concat(toConsumableArray(sources))); }, // Remove duplicates in an array dedupe: function dedupe(array) { if (!utils.is.array(array)) { return array; } return array.filter(function (item, index) { return array.indexOf(item) === index; }); }, // Get the closest value in an array closest: function closest(array, value) { if (!utils.is.array(array) || !array.length) { return null; } return array.reduce(function (prev, curr) { return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev; }); }, // Get the provider for a given URL getProviderByUrl: function getProviderByUrl(url) { // YouTube if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) { return providers.youtube; } // Vimeo if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { return providers.vimeo; } return null; }, // Parse YouTube ID from URL parseYouTubeId: function parseYouTubeId(url) { if (utils.is.empty(url)) { return null; } var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; return url.match(regex) ? RegExp.$2 : url; }, // Parse Vimeo ID from URL parseVimeoId: function parseVimeoId(url) { if (utils.is.empty(url)) { return null; } if (utils.is.number(Number(url))) { return url; } var regex = /^.*(vimeo.com\/|video\/)(\d+).*/; return url.match(regex) ? RegExp.$2 : url; }, // Convert a URL to a location object parseUrl: function parseUrl(url) { var parser = document.createElement('a'); parser.href = url; return parser; }, // Get URL query parameters getUrlParams: function getUrlParams(input) { var search = input; // Parse URL if needed if (input.startsWith('http://') || input.startsWith('https://')) { var _parseUrl = this.parseUrl(input); search = _parseUrl.search; } if (this.is.empty(search)) { return null; } var hashes = search.slice(search.indexOf('?') + 1).split('&'); return hashes.reduce(function (params, hash) { var _hash$split = hash.split('='), _hash$split2 = slicedToArray(_hash$split, 2), key = _hash$split2[0], val = _hash$split2[1]; return Object.assign(params, defineProperty({}, key, decodeURIComponent(val))); }, {}); }, // Convert object to URL parameters buildUrlParams: function buildUrlParams(input) { if (!utils.is.object(input)) { return ''; } return Object.keys(input).map(function (key) { return encodeURIComponent(key) + '=' + encodeURIComponent(input[key]); }).join('&'); }, // Remove HTML from a string stripHTML: function stripHTML(source) { var fragment = document.createDocumentFragment(); var element = document.createElement('div'); fragment.appendChild(element); element.innerHTML = source; return fragment.firstChild.innerText; }, // Get aspect ratio for dimensions getAspectRatio: 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; }, // Get the transition end event get transitionEndEvent() { 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 utils.is.string(type) ? events[type] : false; }, // Force repaint of element repaint: function repaint(element) { setTimeout(function () { utils.toggleHidden(element, true); element.offsetHeight; // eslint-disable-line utils.toggleHidden(element, false); }, 0); } }; // ========================================================================== // 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 api = false; var ui = false; var browser = utils.getBrowser(); var canPlayInline = browser.isIPhone && playsinline && support.playsinline; switch (provider + ':' + type) { case 'html5:video': api = support.video; ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline); break; case 'html5:audio': api = support.audio; ui = api && support.rangeInput; break; case 'youtube:video': case 'vimeo:video': api = true; ui = support.rangeInput && (!browser.isIPhone || canPlayInline); break; default: api = support.audio && support.video; ui = api && support.rangeInput; } return { api: api, ui: ui }; }, // Picture-in-picture support // Safari only currently pip: function () { var browser = utils.getBrowser(); return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode); }(), // Airplay support // Safari only currently airplay: utils.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(type) { var media = this.media; try { // Bail if no checking function if (!this.isHTML5 || !utils.is.function(media.canPlayType)) { return false; } // Check directly if codecs specified if (type.includes('codecs=')) { return media.canPlayType(type).replace(/no/, ''); } // Type specific checks if (this.isVideo) { switch (type) { case 'video/webm': return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, ''); case 'video/mp4': return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''); case 'video/ogg': return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, ''); default: return false; } } else if (this.isAudio) { switch (type) { case 'audio/mpeg': return media.canPlayType('audio/mpeg;').replace(/no/, ''); case 'audio/ogg': return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, ''); case 'audio/wav': return media.canPlayType('audio/wav; codecs="1"').replace(/no/, ''); default: return false; } } } catch (e) { return false; } // If we got this far, we're stuffed return false; }, // Check for textTracks support textTracks: 'textTracks' in document.createElement('video'), // Check for passive event listener support // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md // https://www.youtube.com/watch?v=NPM6172J22g passiveListeners: 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); } catch (e) { // Do nothing } return supported; }(), // 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: utils.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() { if (!this.isHTML5) { return null; } return this.media.querySelectorAll('source'); }, // Get quality levels getQualityOptions: function getQualityOptions() { if (!this.isHTML5) { return null; } // Get sources var sources = html5.getSources.call(this); if (utils.is.empty(sources)) { return null; } // Get with size attribute var sizes = Array.from(sources).filter(function (source) { return !utils.is.empty(source.getAttribute('size')); }); // If none, bail if (utils.is.empty(sizes)) { return null; } // Reduce to unique list return utils.dedupe(sizes.map(function (source) { return Number(source.getAttribute('size')); })); }, extend: function extend() { if (!this.isHTML5) { return; } var player = this; // Quality Object.defineProperty(player.media, 'quality', { get: function get() { // Get sources var sources = html5.getSources.call(player); if (utils.is.empty(sources)) { return null; } var matches = Array.from(sources).filter(function (source) { return source.getAttribute('src') === player.source; }); if (utils.is.empty(matches)) { return null; } return Number(matches[0].getAttribute('size')); }, set: function set(input) { // Get sources var sources = html5.getSources.call(player); if (utils.is.empty(sources)) { return; } // Get matches for requested size var matches = Array.from(sources).filter(function (source) { return Number(source.getAttribute('size')) === input; }); // No matches for requested size if (utils.is.empty(matches)) { return; } // Get supported sources var supported = matches.filter(function (source) { return support.mime.call(player, source.getAttribute('type')); }); // No supported sources if (utils.is.empty(supported)) { return; } // Trigger change event utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { quality: input }); // Get current state var currentTime = player.currentTime, playing = player.playing; // Set new source player.media.src = supported[0].getAttribute('src'); // Load new source player.media.load(); // Resume playing if (playing) { player.play(); } // Restore time player.currentTime = currentTime; // Trigger change event utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { quality: input }); } }); }, // Cancel current network requests // See https://github.com/sampotts/plyr/issues/174 cancelRequests: function cancelRequests() { if (!this.isHTML5) { return; } // Remove child sources utils.removeElement(html5.getSources()); // Set blank video src attribute // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection this.media.setAttribute('src', this.config.blankVideo); // Load the new empty source // This will cancel existing requests // See https://github.com/sampotts/plyr/issues/174 this.media.load(); // Debugging this.debug.log('Cancelled network requests'); } }; // ========================================================================== 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] : {}; if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) { return ''; } var string = config.i18n[key]; 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 = utils.replaceAll(string, key, value); }); return string; } }; // ========================================================================== // Sniff out the browser var browser = utils.getBrowser(); var ui = { addStyleHook: function addStyleHook() { utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); }, // Toggle native HTML5 media controls toggleNativeControls: function toggleNativeControls() { var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (toggle && this.isHTML5) { this.media.setAttribute('controls', ''); } else { this.media.removeAttribute('controls'); } }, // Setup the UI build: function build() { var _this = this; // Re-attach media element listeners // TODO: Use event bubbling? this.listeners.media(); // Don't setup interface if no support if (!this.supported.ui) { this.debug.warn('Basic support only for ' + this.provider + ' ' + this.type); // Restore native controls ui.toggleNativeControls.call(this, true); // Bail return; } // Inject custom controls if not present if (!utils.is.element(this.elements.controls)) { // Inject custom controls controls.inject.call(this); // Re-attach control listeners this.listeners.controls(); } // Remove native controls ui.toggleNativeControls.call(this); // Captions captions.setup.call(this); // Reset volume this.volume = null; // Reset mute state this.muted = null; // Reset speed this.speed = null; // Reset loop state this.loop = null; // Reset quality setting this.quality = null; // Reset volume display ui.updateVolume.call(this); // Reset time display ui.timeUpdate.call(this); // Update the UI ui.checkPlaying.call(this); // Check for picture-in-picture support utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls this.ready = true; // Ready event at end of execution stack setTimeout(function () { utils.dispatchEvent.call(_this, _this.media, 'ready'); }, 0); // Set the title ui.setTitle.call(this); // Set the poster image ui.setPoster.call(this); }, // Setup aria attribute for play and iframe title setTitle: function setTitle() { // Find the current text var label = i18n.get('play', this.config); // If there's a media title set, use that for the label if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) { label += ', ' + this.config.title; // Set container label this.elements.container.setAttribute('aria-label', this.config.title); } // If there's a play button, set label if (utils.is.nodeList(this.elements.buttons.play)) { Array.from(this.elements.buttons.play).forEach(function (button) { button.setAttribute('aria-label', label); }); } // Set iframe title // https://github.com/sampotts/plyr/issues/124 if (this.isEmbed) { var iframe = utils.getElement.call(this, 'iframe'); if (!utils.is.element(iframe)) { return; } // Default to media type var title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; var format = i18n.get('frameTitle', this.config); iframe.setAttribute('title', format.replace('{title}', title)); } }, // Set the poster image setPoster: function setPoster() { if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) { return; } // Set the inline style var posters = this.poster.split(','); this.elements.poster.style.backgroundImage = posters.map(function (p) { return 'url(\'' + p + '\')'; }).join(','); }, // Check playing state checkPlaying: function checkPlaying(event) { // Class hooks utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused); utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set ARIA state utils.toggleState(this.elements.buttons.play, this.playing); // Only update controls on non timeupdate events if (utils.is.event(event) && event.type === 'timeupdate') { return; } // Toggle controls this.toggleControls(!this.playing); }, // Check if media is loading checkLoading: function checkLoading(event) { var _this2 = this; this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer clearTimeout(this.timers.loading); // Timer to prevent flicker when seeking this.timers.loading = setTimeout(function () { // Toggle container class hook utils.toggleClass(_this2.elements.container, _this2.config.classNames.loading, _this2.loading); // Show controls if loading, hide if done _this2.toggleControls(_this2.loading); }, this.loading ? 250 : 0); }, // Check if media failed to load checkFailed: function checkFailed() { var _this3 = this; // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState this.failed = this.media.networkState === 3; if (this.failed) { utils.toggleClass(this.elements.container, this.config.classNames.loading, false); utils.toggleClass(this.elements.container, this.config.classNames.error, true); } // Clear timer clearTimeout(this.timers.failed); // Timer to prevent flicker when seeking this.timers.loading = setTimeout(function () { // Toggle container class hook utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading); // Show controls if loading, hide if done _this3.toggleControls(_this3.loading); }, this.loading ? 250 : 0); }, // Update volume UI and storage updateVolume: function updateVolume() { if (!this.supported.ui) { return; } // Update range if (utils.is.element(this.elements.inputs.volume)) { ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); } // Update mute state if (utils.is.element(this.elements.buttons.mute)) { utils.toggleState(this.elements.buttons.mute, 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 (!utils.is.element(target)) { return; } // eslint-disable-next-line target.value = value; // Webkit range fill controls.updateRangeFill.call(this, target); }, // Set value setProgress: function setProgress(target, input) { var value = utils.is.number(input) ? input : 0; var progress = utils.is.element(target) ? target : this.elements.display.buffer; // Update value and label if (utils.is.element(progress)) { progress.value = value; // Update text label inside var label = progress.getElementsByTagName('span')[0]; if (utils.is.element(label)) { label.childNodes[0].nodeValue = value; } } }, // Update elements updateProgress: function updateProgress(event) { if (!this.supported.ui || !utils.is.event(event)) { return; } var value = 0; if (event) { switch (event.type) { // Video playing case 'timeupdate': case 'seeking': value = utils.getPercentage(this.currentTime, this.duration); // Set seek range value only if it's a 'natural' time event if (event.type === 'timeupdate') { ui.setRange.call(this, this.elements.inputs.seek, value); } break; // Check buffer status case 'playing': case 'progress': ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100); break; default: break; } } }, // 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 (!utils.is.element(target) || !utils.is.number(time)) { return; } // Always display hours if duration is over an hour var forceHours = utils.getHours(this.duration) > 0; // eslint-disable-next-line no-param-reassign target.textContent = utils.formatTime(time, forceHours, inverted); }, // 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 = !utils.is.element(this.elements.display.duration) && this.config.invertTime; // Duration ui.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 ui.updateProgress.call(this, event); }, // Show the duration on metadataloaded durationUpdate: function durationUpdate() { if (!this.supported.ui) { return; } // If there's a spot to display duration var hasDuration = utils.is.element(this.elements.display.duration); // If there's only one time display, display duration there if (!hasDuration && this.config.displayDuration && this.paused) { ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); } // If there's a duration element, update content if (hasDuration) { ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); } // Update the tooltip (if visible) controls.updateSeekTooltip.call(this); } }; // ========================================================================== // Sniff out the browser var browser$1 = utils.getBrowser(); var controls = { // Webkit polyfill for lower fill range updateRangeFill: function updateRangeFill(target) { // Get range from event if event passed var range = utils.is.event(target) ? target.target : target; // Needs to be a valid if (!utils.is.element(range) || range.getAttribute('type') !== 'range') { return; } // Set aria value for https://github.com/sampotts/plyr/issues/905 range.setAttribute('aria-valuenow', range.value); // WebKit only if (!browser$1.isWebkit) { return; } // Set CSS custom property range.style.setProperty('--value', range.value / range.max * 100 + '%'); }, // Get icon URL getIconUrl: function getIconUrl() { var url = new URL(this.config.iconUrl, window.location); var cors = url.host !== window.location.host || browser$1.isIE && !window.svg4everybody; return { url: this.config.iconUrl, cors: cors }; }, // Find the UI controls and store references in custom controls // TODO: Allow settings menus with custom controls findElements: function findElements() { try { this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper); // Buttons this.elements.buttons = { play: utils.getElements.call(this, this.config.selectors.buttons.play), pause: utils.getElement.call(this, this.config.selectors.buttons.pause), restart: utils.getElement.call(this, this.config.selectors.buttons.restart), rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind), fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward), mute: utils.getElement.call(this, this.config.selectors.buttons.mute), pip: utils.getElement.call(this, this.config.selectors.buttons.pip), airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay), settings: utils.getElement.call(this, this.config.selectors.buttons.settings), captions: utils.getElement.call(this, this.config.selectors.buttons.captions), fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen) }; // Progress this.elements.progress = utils.getElement.call(this, this.config.selectors.progress); // Inputs this.elements.inputs = { seek: utils.getElement.call(this, this.config.selectors.inputs.seek), volume: utils.getElement.call(this, this.config.selectors.inputs.volume) }; // Display this.elements.display = { buffer: utils.getElement.call(this, this.config.selectors.display.buffer), currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime), duration: utils.getElement.call(this, this.config.selectors.display.duration) }; // Seek tooltip if (utils.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 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 var icon = document.createElementNS(namespace, 'svg'); utils.setAttributes(icon, utils.extend(attributes, { role: 'presentation', focusable: 'false' })); // Create the 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 to icon.appendChild(use); return icon; }, // Create hidden text label createLabel: function createLabel(type, attr) { var text = i18n.get(type, this.config); var attributes = Object.assign({}, attr); switch (type) { case 'pip': text = 'PIP'; break; case 'airplay': text = 'AirPlay'; break; default: break; } if ('class' in attributes) { attributes.class += ' ' + this.config.classNames.hidden; } else { attributes.class = this.config.classNames.hidden; } return utils.createElement('span', attributes, text); }, // Create a badge createBadge: function createBadge(text) { if (utils.is.empty(text)) { return null; } var badge = utils.createElement('span', { class: this.config.classNames.menu.value }); badge.appendChild(utils.createElement('span', { class: this.config.classNames.menu.badge }, text)); return badge; }, // Create a
to hide the standard controls and UI setAspectRatio: function setAspectRatio(input) { var ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); var padding = 100 / ratio[0] * ratio[1]; this.elements.wrapper.style.paddingBottom = padding + '%'; if (this.supported.ui) { var height = 240; var offset = (height - padding) / (height / 50); this.media.style.transform = 'translateY(-' + offset + '%)'; } }, // API Ready ready: function ready() { 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 = utils.buildUrlParams(options); // Get the source URL or ID var source = player.media.getAttribute('src'); // Get from
if needed if (utils.is.empty(source)) { source = player.media.getAttribute(player.config.attributes.embed.id); } var id = utils.parseVimeoId(source); // Build an iframe var iframe = utils.createElement('iframe'); var src = utils.format(player.config.urls.vimeo.iframe, id, params); iframe.setAttribute('src', src); iframe.setAttribute('allowfullscreen', ''); iframe.setAttribute('allowtransparency', ''); iframe.setAttribute('allow', 'autoplay'); // Inject the package var wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer }); wrapper.appendChild(iframe); player.media = utils.replaceElement(wrapper, player.media); // Get poster image utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(function (response) { if (utils.is.empty(response)) { return; } // Get the URL for thumbnail var url = new URL(response[0].thumbnail_large); // Get original image url.pathname = url.pathname.split('_')[0] + '.jpg'; // Set attribute player.media.setAttribute('poster', url.href); // Update ui.setPoster.call(player); }); // Setup instance // https://github.com/vimeo/player.js player.embed = new window.Vimeo.Player(iframe, { autopause: player.config.autopause, muted: player.muted }); player.media.paused = true; player.media.currentTime = 0; // Disable native text track rendering if (player.supported.ui) { player.embed.disableTextTrack(); } // Create a faux HTML5 API using the Vimeo API player.media.play = function () { player.embed.play().then(function () { player.media.paused = false; }); }; player.media.pause = function () { player.embed.pause().then(function () { player.media.paused = true; }); }; player.media.stop = function () { player.pause(); player.currentTime = 0; }; // Seeking var currentTime = player.media.currentTime; Object.defineProperty(player.media, 'currentTime', { get: function get() { return currentTime; }, set: function set(time) { // Get current paused state // Vimeo will automatically play on seek var paused = player.media.paused; // Set seeking flag player.media.seeking = true; // Trigger seeking utils.dispatchEvent.call(player, player.media, 'seeking'); // Seek after events player.embed.setCurrentTime(time).catch(function () { // Do nothing }); // Restore pause state if (paused) { player.pause(); } } }); // 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; utils.dispatchEvent.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; utils.dispatchEvent.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 = utils.is.boolean(input) ? input : false; player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () { muted = toggle; utils.dispatchEvent.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 = utils.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); }); Object.defineProperty(player.media, 'currentSrc', { get: function get() { return currentSrc; } }); // Ended Object.defineProperty(player.media, 'ended', { get: function get() { return player.currentTime === player.duration; } }); // Set aspect ratio based on video size Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) { var ratio = utils.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; utils.dispatchEvent.call(player, player.media, 'timeupdate'); }); // Get duration player.embed.getDuration().then(function (value) { player.media.duration = value; utils.dispatchEvent.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 (data) { var cue = null; if (data.cues.length) { cue = utils.stripHTML(data.cues[0].text); } captions.setText.call(player, cue); }); player.embed.on('loaded', function () { if (utils.is.element(player.embed.element) && player.supported.ui) { var frame = player.embed.element; // Fix keyboard focus issues // https://github.com/sampotts/plyr/issues/317 frame.setAttribute('tabindex', -1); } }); player.embed.on('play', function () { // Only fire play if paused before if (player.media.paused) { utils.dispatchEvent.call(player, player.media, 'play'); } player.media.paused = false; utils.dispatchEvent.call(player, player.media, 'playing'); }); player.embed.on('pause', function () { player.media.paused = true; utils.dispatchEvent.call(player, player.media, 'pause'); }); player.embed.on('timeupdate', function (data) { player.media.seeking = false; currentTime = data.seconds; utils.dispatchEvent.call(player, player.media, 'timeupdate'); }); player.embed.on('progress', function (data) { player.media.buffered = data.percent; utils.dispatchEvent.call(player, player.media, 'progress'); // Check all loaded if (parseInt(data.percent, 10) === 1) { utils.dispatchEvent.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; utils.dispatchEvent.call(player, player.media, 'durationchange'); } }); }); player.embed.on('seeked', function () { player.media.seeking = false; utils.dispatchEvent.call(player, player.media, 'seeked'); utils.dispatchEvent.call(player, player.media, 'play'); }); player.embed.on('ended', function () { player.media.paused = true; utils.dispatchEvent.call(player, player.media, 'ended'); }); player.embed.on('error', function (detail) { player.media.error = detail; utils.dispatchEvent.call(player, player.media, 'error'); }); // Rebuild UI setTimeout(function () { return ui.build.call(player); }, 0); } }; // ========================================================================== // Standardise YouTube quality unit function mapQualityUnit(input) { switch (input) { case 'hd2160': return 2160; case 2160: return 'hd2160'; case 'hd1440': return 1440; case 1440: return 'hd1440'; case 'hd1080': return 1080; case 1080: return 'hd1080'; case 'hd720': return 720; case 720: return 'hd720'; case 'large': return 480; case 480: return 'large'; case 'medium': return 360; case 360: return 'medium'; case 'small': return 240; case 240: return 'small'; default: return 'default'; } } function mapQualityUnits(levels) { if (utils.is.empty(levels)) { return levels; } return utils.dedupe(levels.map(function (level) { return mapQualityUnit(level); })); } var youtube = { setup: function setup() { var _this = this; // Add embed class for responsive utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio youtube.setAspectRatio.call(this); // Setup API if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { youtube.ready.call(this); } else { // Load the API utils.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 (utils.is.function(this.embed.getVideoData)) { var _embed$getVideoData = this.embed.getVideoData(), title = _embed$getVideoData.title; if (utils.is.empty(title)) { this.config.title = title; ui.setTitle.call(this); return; } } // Or via Google API var key = this.config.keys.google; if (utils.is.string(key) && !utils.is.empty(key)) { var url = utils.format(this.config.urls.youtube.api, videoId, key); utils.fetch(url).then(function (result) { if (utils.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 = 100 / ratio[0] * ratio[1] + '%'; }, // API ready ready: function ready() { var player = this; // Ignore already setup (race condition) var currentId = player.media.getAttribute('id'); if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { return; } // Get the source URL or ID var source = player.media.getAttribute('src'); // Get from
if needed if (utils.is.empty(source)) { source = player.media.getAttribute(this.config.attributes.embed.id); } // Replace the