(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'; // ========================================================================== // Plyr supported types and providers // ========================================================================== var providers = { html5: 'html5', youtube: 'youtube', vimeo: 'vimeo' }; var types = { audio: 'audio', video: 'video' }; // ========================================================================== // 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, // 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, // Revert to poster on finish (HTML5 - will cause reload) showPosterOnEnd: false, // Disable the standard context menu disableContextMenu: true, // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', iconUrl: 'https://cdn.plyr.io/3.0.3/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', // Quality default quality: { default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }, // 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: window.navigator.language.split('-')[0] }, // 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} secs', play: 'Play', pause: 'Pause', fastForward: 'Forward {seektime} secs', seek: 'Seek', 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', speed: 'Speed', quality: 'Quality', loop: 'Loop', start: 'Start', end: 'End', all: 'All', reset: 'Reset', none: 'None', disabled: 'Disabled', advertisement: 'Ad' }, // URLs urls: { vimeo: { api: 'https://player.vimeo.com/api/player.js' }, youtube: { api: 'https://www.youtube.com/iframe_api' }, googleIMA: { api: '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', 'qualitychange', 'qualityrequested', // 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', played: '.plyr__progress--played', loop: '.plyr__progress--loop', volume: '.plyr__volume--display' }, progress: '.plyr__progress', captions: '.plyr__captions', menu: { quality: '.js-plyr__menu__list--quality' } }, // Class hooks added to the player in different states classNames: { video: 'plyr__video-wrapper', embed: 'plyr__video-embed', ads: 'plyr__ads', control: 'plyr__control', type: 'plyr--{0}', provider: 'plyr--{0}', stopped: 'plyr--stopped', playing: 'plyr--playing', loading: 'plyr--loading', error: 'plyr--has-error', 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', 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: '' } }; 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, isCss, e; numTries = numTries || 0; if (/(^css!|\.css$)/.test(path)) { isCss = true; // css e = doc.createElement('link'); e.rel = 'stylesheet'; e.href = path.replace(/^css!/, ''); // remove "css!" prefix } 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; })); }); var asyncGenerator = function () { function AwaitValue(value) { this.value = value; } function AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; if (value instanceof AwaitValue) { Promise.resolve(value.value).then(function (arg) { resume("next", arg); }, function (arg) { resume("throw", arg); }); } else { settle(result.done ? "return" : "normal", result.value); } } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; return { wrap: function (fn) { return function () { return new AsyncGenerator(fn.apply(this, arguments)); }; }, await: function (value) { return new AwaitValue(value); } }; }(); 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); } }; // ========================================================================== // Plyr utils // ========================================================================== var utils = { // Check variable types is: { plyr: function plyr(input) { return this.instanceof(input, window.Plyr); }, 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, window.WeakMap); }, nodeList: function nodeList(input) { return this.instanceof(input, window.NodeList); }, element: function element(input) { return this.instanceof(input, window.Element); }, textNode: function textNode(input) { return this.getConstructor(input) === Text; }, event: function event(input) { return this.instanceof(input, window.Event); }, cue: function cue(input) { return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.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); }, // Determine if we're in an iframe inFrame: function inFrame() { try { return window.self !== window.top; } catch (e) { return true; } }, // 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 an element removeElement: function removeElement(element) { if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { return; } if (utils.is.nodeList(element) || utils.is.array(element)) { Array.from(element).forEach(utils.removeElement); 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 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); }, // Toggle hidden attribute on an element toggleHidden: function toggleHidden(element, toggle) { if (!utils.is.element(element)) { return; } if (toggle) { element.setAttribute('hidden', ''); } else { element.removeAttribute('hidden'); } }, // 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); }, // 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), duration: utils.getElement.call(this, this.config.selectors.display.duration), currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime) }; // 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; } }, // 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, type, bubbles, detail) { // Bail if no element if (!utils.is.element(element) || !utils.is.string(type)) { return; } // Create and dispatch the event var event = new CustomEvent(type, { bubbles: utils.is.boolean(bubbles) ? bubbles : false, detail: Object.assign({}, detail, { plyr: utils.is.plyr(this) ? this : null }) }); // 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); }, // 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 _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { sources[_key - 1] = arguments[_key]; } if (!sources.length) { return target; } var source = sources.shift(); if (!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))); }, // 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{8,}(?=\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); } }; // ========================================================================== // Plyr support checks // ========================================================================== // 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, inline) { var api = false; var ui = false; var browser = utils.getBrowser(); var playsInline = browser.isIPhone && inline && support.inline; switch (provider + ':' + type) { case 'html5:video': api = support.video; ui = api && support.rangeInput && (!browser.isIPhone || playsInline); 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 || playsInline); 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/ inline: '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; } // 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 // Remember a device can be moust + touch enabled 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 }; // ========================================================================== // Console wrapper // ========================================================================== var noop = function noop() {}; var Console = function () { function Console() { var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; classCallCheck(this, Console); this.enabled = window.console && enabled; if (this.enabled) { this.log('Debugging enabled'); } } createClass(Console, [{ key: 'log', get: function get$$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; }(); // ========================================================================== // Fullscreen wrapper // https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing // ========================================================================== var browser = utils.getBrowser(); function onChange() { if (!this.enabled) { return; } // Update toggle button var button = this.player.elements.buttons.fullscreen; if (utils.is.element(button)) { utils.toggleState(button, this.active); } // Trigger an event utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); // Trap focus in container if (!browser.isIos) { utils.trapFocus.call(this.player, this.target, this.active); } } 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); } // Toggle scroll document.body.style.overflow = toggle ? 'hidden' : ''; // Toggle class hook utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); // Toggle button and fire events onChange.call(this); } var Fullscreen = function () { function Fullscreen(player) { var _this = this; classCallCheck(this, Fullscreen); // Keep reference to parent this.player = player; // Get prefix this.prefix = Fullscreen.prefix; this.name = Fullscreen.name; // Scroll position this.scrollPosition = { x: 0, y: 0 }; // Register event listeners // Handle event (incase user presses escape etc) utils.on(document, this.prefix === 'ms' ? 'MSFullscreenChange' : this.prefix + 'fullscreenchange', function () { // TODO: Filter for target?? onChange.call(_this); }); // Fullscreen toggle on double click utils.on(this.player.elements.container, 'dblclick', function () { _this.toggle(); }); // Prevent double click on controls bubbling up utils.on(this.player.elements.controls, 'dblclick', function (event) { return event.stopPropagation(); }); // Update the UI this.update(); } // Determine if native supported createClass(Fullscreen, [{ key: 'update', // Update UI value: function update() { if (this.enabled) { this.player.debug.log((Fullscreen.native ? 'Native' : 'Fallback') + ' fullscreen enabled'); } else { this.player.debug.log('Fullscreen not supported and fallback disabled'); } // Add styling hook to show button utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled); } // Make an element fullscreen }, { key: 'enter', value: function enter() { if (!this.enabled) { return; } console.warn(this.prefix); // iOS native fullscreen doesn't need the request step if (browser.isIos && this.player.config.fullscreen.iosNative) { if (this.player.playing) { this.target.webkitEnterFullscreen(); } } else if (!Fullscreen.native) { toggleFallback.call(this, true); } else if (!this.prefix) { this.target.requestFullscreen(); } else if (!utils.is.empty(this.prefix)) { this.target[this.prefix + 'Request' + this.name](); } } // Bail from fullscreen }, { key: 'exit', value: function exit() { if (!this.enabled) { return; } // iOS native fullscreen if (browser.isIos && this.player.config.fullscreen.iosNative) { this.target.webkitExitFullscreen(); this.player.play(); } else if (!Fullscreen.native) { toggleFallback.call(this, false); } else if (!this.prefix) { document.cancelFullScreen(); } else if (!utils.is.empty(this.prefix)) { var action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; document['' + this.prefix + action + this.name](); } } // Toggle state }, { key: 'toggle', value: function toggle() { console.warn('TOGGLE'); if (!this.active) { this.enter(); } else { this.exit(); } } }, { key: 'enabled', // Determine if fullscreen is enabled get: function get$$1() { var fallback = this.player.config.fullscreen.fallback && !utils.inFrame(); return (Fullscreen.native || fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo; } // Get active state }, { key: 'active', get: function get$$1() { if (!this.enabled) { return false; } // Fallback using classname if (!Fullscreen.native) { return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); } var element = !this.prefix ? document.fullscreenElement : document['' + this.prefix + this.name + '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); } // Get the prefix for handlers }, { key: 'prefix', get: function get$$1() { // No prefix if (utils.is.function(document.exitFullscreen)) { return false; } // Check for fullscreen support by vendor prefix var value = ''; var prefixes = ['webkit', 'moz', 'ms']; prefixes.some(function (pre) { if (utils.is.function(document[pre + 'ExitFullscreen']) || utils.is.function(document[pre + 'CancelFullScreen'])) { value = pre; return true; } return false; }); return value; } }, { key: 'name', get: function get$$1() { return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; } }]); return Fullscreen; }(); // ========================================================================== // Plyr Captions // TODO: Create as class // ========================================================================== var captions = { // Setup captions setup: function setup() { // Requires UI support if (!this.supported.ui) { return; } // Set default language if not set var stored = this.storage.get('language'); if (!utils.is.empty(stored)) { this.captions.language = stored; } if (utils.is.empty(this.captions.language)) { this.captions.language = this.config.captions.language.toLowerCase(); } // Set captions enabled state if not set if (!utils.is.boolean(this.captions.active)) { var active = this.storage.get('captions'); if (utils.is.boolean(active)) { this.captions.active = active; } else { this.captions.active = this.config.captions.active; } } // Only Vimeo and HTML5 video supported at this point if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) { // Clear menu and hide if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { controls.setCaptionsMenu.call(this); } return; } // Inject the container if (!utils.is.element(this.elements.captions)) { this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); utils.insertAfter(this.elements.captions, this.elements.wrapper); } // Set the class hook utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); // Get tracks var tracks = captions.getTracks.call(this); // If no caption file exists, hide container for caption text if (utils.is.empty(tracks)) { return; } // Get browser info var browser = utils.getBrowser(); // 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 href = utils.parseUrl(src); if (href.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(href.protocol)) { utils.fetch(src, 'blob').then(function (blob) { track.setAttribute('src', window.URL.createObjectURL(blob)); }).catch(function () { utils.removeElement(track); }); } }); } // Set language captions.setLanguage.call(this); // Enable UI captions.show.call(this); // Set available languages in list if (utils.is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) { controls.setCaptionsMenu.call(this); } }, // Set the captions language setLanguage: function setLanguage() { var _this = this; // Setup HTML5 track rendering if (this.isHTML5 && this.isVideo) { captions.getTracks.call(this).forEach(function (track) { // Show track utils.on(track, 'cuechange', function (event) { return captions.setCue.call(_this, event); }); // Turn off native caption rendering to avoid double captions // eslint-disable-next-line track.mode = 'hidden'; }); // Get current track var currentTrack = captions.getCurrentTrack.call(this); // Check if suported kind if (utils.is.track(currentTrack)) { // If we change the active track while a cue is already displayed we need to update it if (Array.from(currentTrack.activeCues || []).length) { captions.setCue.call(this, currentTrack); } } } else if (this.isVimeo && this.captions.active) { this.embed.enableTextTrack(this.language); } }, // Get the tracks getTracks: function getTracks() { // Return empty array at least if (utils.is.nullOrUndefined(this.media)) { return []; } // Only get accepted kinds return Array.from(this.media.textTracks || []).filter(function (track) { return ['captions', 'subtitles'].includes(track.kind); }); }, // Get the current track for the current language getCurrentTrack: function getCurrentTrack() { var _this2 = this; return captions.getTracks.call(this).find(function (track) { return track.language.toLowerCase() === _this2.language; }); }, // Display active caption if it contains text setCue: function setCue(input) { // Get the track from the event if needed var track = utils.is.event(input) ? input.target : input; var activeCues = track.activeCues; var active = activeCues.length && activeCues[0]; var currentTrack = captions.getCurrentTrack.call(this); // Only display current track if (track !== currentTrack) { return; } // Display a cue, if there is one if (utils.is.cue(active)) { captions.setText.call(this, active.getCueAsHTML()); } else { captions.setText.call(this, null); } utils.dispatchEvent.call(this, this.media, 'cuechange'); }, // Set the current caption setText: function setText(input) { // Requires UI if (!this.supported.ui) { return; } if (utils.is.element(this.elements.captions)) { var content = utils.createElement('span'); // Empty the container utils.emptyElement(this.elements.captions); // Default to empty var caption = !utils.is.nullOrUndefined(input) ? input : ''; // Set the span content if (utils.is.string(caption)) { content.textContent = caption.trim(); } else { content.appendChild(caption); } // Set new caption text this.elements.captions.appendChild(content); } else { this.debug.warn('No captions element to render to'); } }, // Display captions container and button (for initialization) show: function show() { // If there's no caption toggle, bail if (!utils.is.element(this.elements.buttons.captions)) { return; } // Try to load the value from storage var active = this.storage.get('captions'); // Otherwise fall back to the default config if (!utils.is.boolean(active)) { active = this.config.captions.active; } else { this.captions.active = active; } if (active) { utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true); utils.toggleState(this.elements.buttons.captions, true); } } }; // ========================================================================== // Plyr internationalization // ========================================================================== 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; } }; // ========================================================================== // Plyr UI // ========================================================================== 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(); } // If there's no controls, bail if (!utils.is.element(this.elements.controls)) { return; } // 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 options this.options.quality = []; // Reset time display ui.timeUpdate.call(this); // Update the UI ui.checkPlaying.call(this); // 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); }, // 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'; iframe.setAttribute('title', i18n.get('frameTitle', this.config)); } }, // Check playing state checkPlaying: function checkPlaying() { // Class hooks utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused); // Set ARIA state utils.toggleState(this.elements.buttons.play, this.playing); // 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 displayHours = utils.getHours(this.duration) > 0; // eslint-disable-next-line no-param-reassign target.textContent = utils.formatTime(time, displayHours, 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); } }; // ========================================================================== // Plyr controls // ========================================================================== // Sniff out the browser var browser$2 = utils.getBrowser(); var controls = { // Webkit polyfill for lower fill range updateRangeFill: function updateRangeFill(target) { // WebKit only if (!browser$2.isWebkit) { return; } // 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 CSS custom property range.style.setProperty('--value', range.value / range.max * 100 + '%'); }, // Get icon URL getIconUrl: function getIconUrl() { return { url: this.config.iconUrl, absolute: this.config.iconUrl.indexOf('http') === 0 || browser$2.isIE && !window.svg4everybody }; }, // Create icon createIcon: function createIcon(type, attributes) { var namespace = 'http://www.w3.org/2000/svg'; var iconUrl = controls.getIconUrl.call(this); var iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + this.config.iconPrefix; // Create var icon = document.createElementNS(namespace, 'svg'); utils.setAttributes(icon, utils.extend(attributes, { role: 'presentation' })); // 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
if needed if (utils.is.empty(source)) { source = player.media.getAttribute(this.config.attributes.embed.id); } // Replace the