(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.0-beta.17/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', 'play', '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', forward: '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, forward: 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', 'adsconentresume', '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"]', forward: '[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 // Tag is not required as publisher is determined by vi.ai using the domain ads: { enabled: false } }; 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, callback, error) { var current = document.querySelector('script[src="' + url + '"]'); // Check script is not already referenced, if so wait for load if (current !== null) { current.callbacks = current.callbacks || []; current.callbacks.push(callback); return; } // Build the element var element = document.createElement('script'); // Callback queue element.callbacks = element.callbacks || []; element.callbacks.push(callback); // Error queue element.errors = element.errors || []; element.errors.push(error); // Bind callback if (utils.is.function(callback)) { element.addEventListener('load', function (event) { element.callbacks.forEach(function (cb) { return cb.call(null, event); }); element.callbacks = null; }, false); } // Bind error handling element.addEventListener('error', function (event) { element.errors.forEach(function (err) { return err.call(null, event); }); element.errors = null; }, false); // Set the URL after binding callback element.src = url; // Inject var first = document.getElementsByTagName('script')[0]; first.parentNode.insertBefore(element, first); }, // 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; function updateSprite(data) { // Inject content this.innerHTML = data; // Inject the SVG to the body document.body.insertBefore(this, document.body.childNodes[0]); } // Only load once if (!hasId || !document.querySelectorAll('#' + id).length) { // 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); updateSprite.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 })); } updateSprite.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.keys(attributes).forEach(function (key) { element.setAttribute(key, attributes[key]); }); }, // 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), forward: utils.getElement.call(this, this.config.selectors.buttons.forward), 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, toggle, passive, capture) { // 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 capture boolean var options = utils.is.boolean(capture) ? capture : false; // If passive events listeners are supported if (support.passiveListeners) { options = { // Whether the listener can be passive (i.e. default never prevented) passive: utils.is.boolean(passive) ? passive : true, // Whether the listener is a capturing listener or not capture: utils.is.boolean(capture) ? capture : false }; } // 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, events, callback, passive, capture) { utils.toggleListener(element, events, callback, true, passive, capture); }, // Unbind event handler off: function off(element, events, callback, passive, capture) { 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); }, // 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 // ========================================================================== 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': api = true; ui = support.rangeInput && (!browser.isIPhone || playsInline); break; case 'vimeo:video': api = true; ui = support.rangeInput && !browser.isIPhone; 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 // ========================================================================== 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; // 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; } // 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 + (this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen')](); } } // 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)) { document['' + this.prefix + (this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen')](); } } // Toggle state }, { key: 'toggle', value: function toggle() { if (!this.active) { this.enter(); } else { this.exit(); } } }, { key: 'enabled', // Determine if fullscreen is enabled get: function get$$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 + 'FullscreenElement']; 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.cancelFullScreen)) { 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 + 'CancelFullScreen'])) { value = pre; return true; } else if (utils.is.function(document.msExitFullscreen)) { value = 'ms'; return true; } return false; }); return value; } }]); return Fullscreen; }(); // ========================================================================== // Plyr storage // ========================================================================== var Storage = function () { function Storage(player) { classCallCheck(this, Storage); this.enabled = player.config.storage.enabled; this.key = player.config.storage.key; } // Check for actual support (see if we can use it) createClass(Storage, [{ key: 'get', value: function get$$1(key) { var store = window.localStorage.getItem(this.key); if (!Storage.supported || utils.is.empty(store)) { return null; } var json = JSON.parse(store); return utils.is.string(key) && key.length ? json[key] : json; } }, { key: 'set', value: function set$$1(object) { // Bail if we don't have localStorage support or it's disabled if (!Storage.supported || !this.enabled) { return; } // Can only store objectst if (!utils.is.object(object)) { return; } // Get current storage var storage = this.get(); // Default to empty object if (utils.is.empty(storage)) { storage = {}; } // Update the working copy of the values utils.extend(storage, object); // Update storage window.localStorage.setItem(this.key, JSON.stringify(storage)); } }], [{ key: 'supported', get: function get$$1() { if (!('localStorage' in window)) { return false; } var test = '___test'; // Try to use it (it might be disabled, e.g. user is in private mode) // see: https://github.com/sampotts/plyr/issues/131 try { window.localStorage.setItem(test, test); window.localStorage.removeItem(test); return true; } catch (e) { return false; } } }]); return Storage; }(); // ========================================================================== // Advertisement plugin using Google IMA HTML5 SDK // Create an account with our ad partner, vi here: // https://www.vi.ai/publisher-video-monetization/ // ========================================================================== /* global google */ var getTagUrl = function getTagUrl() { var params = { AV_PUBLISHERID: '58c25bb0073ef448b1087ad6', AV_CHANNELID: '5a0458dc28a06145e4519d21', AV_URL: '127.0.0.1:3000', cb: 1, AV_WIDTH: 640, AV_HEIGHT: 480 }; var base = 'https://go.aniview.com/api/adserver6/vast/'; return base + '?' + utils.buildUrlParams(params); }; var Ads = function () { /** * Ads constructor. * @param {object} player * @return {Ads} */ function Ads(player) { var _this = this; classCallCheck(this, Ads); this.player = player; this.enabled = player.config.ads.enabled; this.playing = false; this.initialized = false; this.blocked = false; this.enabled = utils.is.url(player.config.ads.tag); // Check if a tag URL is provided. if (!this.enabled) { return; } // Check if the Google IMA3 SDK is loaded or load it ourselves if (!utils.is.object(window.google)) { utils.loadScript(player.config.urls.googleIMA.api, function () { _this.ready(); }, function () { // Script failed to load or is blocked _this.blocked = true; _this.player.debug.log('Ads error: Google IMA SDK failed to load'); }); } else { this.ready(); } } /** * Get the ads instance ready. */ createClass(Ads, [{ key: 'ready', value: function ready() { var _this2 = this; this.elements = { container: null, displayContainer: null }; this.manager = null; this.loader = null; this.cuePoints = null; this.events = {}; this.safetyTimer = null; this.countdownTimer = null; // Set listeners on the Plyr instance this.listeners(); // Start ticking our safety timer. If the whole advertisement // thing doesn't resolve within our set time; we bail this.startSafetyTimer(12000, 'ready()'); // Setup a simple promise to resolve if the IMA loader is ready this.loaderPromise = new Promise(function (resolve) { _this2.on('ADS_LOADER_LOADED', function () { return resolve(); }); }); // Setup a promise to resolve if the IMA manager is ready this.managerPromise = new Promise(function (resolve) { _this2.on('ADS_MANAGER_LOADED', function () { return resolve(); }); }); // Clear the safety timer this.managerPromise.then(function () { _this2.clearSafetyTimer('onAdsManagerLoaded()'); }); // Setup the IMA SDK this.setupIMA(); } /** * In order for the SDK to display ads for our video, we need to tell it where to put them, * so here we define our ad container. This div is set up to render on top of the video player. * Using the code below, we tell the SDK to render ads within that div. We also provide a * handle to the content video player - the SDK will poll the current time of our player to * properly place mid-rolls. After we create the ad display container, we initialize it. On * mobile devices, this initialization is done as the result of a user action. */ }, { key: 'setupIMA', value: function setupIMA() { // Create the container for our advertisements this.elements.container = utils.createElement('div', { class: this.player.config.classNames.ads, hidden: '' }); this.player.elements.container.appendChild(this.elements.container); // So we can run VPAID2 google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); // Set language google.ima.settings.setLocale(this.player.config.ads.language); // We assume the adContainer is the video container of the plyr element // that will house the ads this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); // Request video ads to be pre-loaded this.requestAds(); } /** * Request advertisements */ }, { key: 'requestAds', value: function requestAds() { var _this3 = this; var container = this.player.elements.container; try { // Create ads loader this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) { return _this3.onAdsManagerLoaded(event); }, false); this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { return _this3.onAdError(error); }, false); // Request video ads var request = new google.ima.AdsRequest(); request.adTagUrl = getTagUrl(); // Specify the linear and nonlinear slot sizes. This helps the SDK // to select the correct creative if multiple are returned request.linearAdSlotWidth = container.offsetWidth; request.linearAdSlotHeight = container.offsetHeight; request.nonLinearAdSlotWidth = container.offsetWidth; request.nonLinearAdSlotHeight = container.offsetHeight; // We only overlay ads as we only support video. request.forceNonLinearFullSlot = false; this.loader.requestAds(request); this.handleEventListeners('ADS_LOADER_LOADED'); } catch (e) { this.onAdError(e); } } /** * Update the ad countdown * @param {boolean} start */ }, { key: 'pollCountdown', value: function pollCountdown() { var _this4 = this; var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (!start) { window.clearInterval(this.countdownTimer); this.elements.container.removeAttribute('data-badge-text'); return; } var update = function update() { var time = utils.formatTime(_this4.manager.getRemainingTime()); var label = _this4.player.config.i18n.advertisement + ' - ' + time; _this4.elements.container.setAttribute('data-badge-text', label); }; this.countdownTimer = window.setInterval(update, 100); } /** * This method is called whenever the ads are ready inside the AdDisplayContainer * @param {Event} adsManagerLoadedEvent */ }, { key: 'onAdsManagerLoaded', value: function onAdsManagerLoaded(adsManagerLoadedEvent) { var _this5 = this; // Get the ads manager var settings = new google.ima.AdsRenderingSettings(); // Tell the SDK to save and restore content video state on our behalf settings.restoreCustomPlaybackStateOnAdBreakComplete = true; settings.enablePreloading = true; // The SDK is polling currentTime on the contentPlayback. And needs a duration // so it can determine when to start the mid- and post-roll this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings); // Get the cue points for any mid-rolls by filtering out the pre- and post-roll this.cuePoints = this.manager.getCuePoints(); // Add advertisement cue's within the time line if available this.cuePoints.forEach(function (cuePoint) { if (cuePoint !== 0 && cuePoint !== -1) { var seekElement = _this5.player.elements.progress; if (seekElement) { var cuePercentage = 100 / _this5.player.duration * cuePoint; var cue = utils.createElement('span', { class: _this5.player.config.classNames.cues }); cue.style.left = cuePercentage.toString() + '%'; seekElement.appendChild(cue); } } }); // Get skippable state // TODO: Skip button // this.manager.getAdSkippableState(); // Set volume to match player this.manager.setVolume(this.player.volume); // Add listeners to the required events // Advertisement error events this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { return _this5.onAdError(error); }); // Advertisement regular events Object.keys(google.ima.AdEvent.Type).forEach(function (type) { _this5.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) { return _this5.onAdEvent(event); }); }); // Resolve our adsManager this.handleEventListeners('ADS_MANAGER_LOADED'); } /** * This is where all the event handling takes place. Retrieve the ad from the event. Some * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type * @param {Event} event */ }, { key: 'onAdEvent', value: function onAdEvent(event) { var _this6 = this; var container = this.player.elements.container; // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) // don't have ad object associated var ad = event.getAd(); // Proxy event var dispatchEvent = function dispatchEvent(type) { utils.dispatchEvent.call(_this6.player, _this6.player.media, 'ads' + type); }; switch (event.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to determine whether the // ad is a video ad or an overlay this.handleEventListeners('LOADED'); // Bubble event dispatchEvent('loaded'); // Start countdown this.pollCountdown(true); if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay ad.width = container.offsetWidth; ad.height = container.offsetHeight; } // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: // All ads for the current videos are done. We can now request new advertisements // in case the video is re-played this.handleEventListeners('ALL_ADS_COMPLETED'); // Fire event dispatchEvent('allcomplete'); // TODO: Example for what happens when a next video in a playlist would be loaded. // So here we load a new video when all ads are done. // Then we load new ads within a new adsManager. When the video // Is started - after - the ads are loaded, then we get ads. // You can also easily test cancelling and reloading by running // player.ads.cancel() and player.ads.play from the console I guess. // this.player.source = { // type: 'video', // title: 'View From A Blue Moon', // sources: [{ // src: // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type: // 'video/mp4', }], poster: // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks: // [ { kind: 'captions', label: 'English', srclang: 'en', src: // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt', // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src: // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ], // }; // TODO: So there is still this thing where a video should only be allowed to start // playing when the IMA SDK is ready or has failed this.loadAds(); break; case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED: // This event indicates the ad has started - the video player can adjust the UI, // for example display a pause button and remaining time. Fired when content should // be paused. This usually happens right before an ad is about to cover the content this.handleEventListeners('CONTENT_PAUSE_REQUESTED'); dispatchEvent('contentpause'); this.pauseContent(); break; case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED: // This event indicates the ad has finished - the video player can perform // appropriate UI actions, such as removing the timer for remaining time detection. // Fired when content should be resumed. This usually happens when an ad finishes // or collapses this.handleEventListeners('CONTENT_RESUME_REQUESTED'); dispatchEvent('contentresume'); this.pollCountdown(); this.resumeContent(); break; case google.ima.AdEvent.Type.STARTED: dispatchEvent('started'); break; case google.ima.AdEvent.Type.MIDPOINT: dispatchEvent('midpoint'); break; case google.ima.AdEvent.Type.COMPLETE: dispatchEvent('complete'); break; case google.ima.AdEvent.Type.IMPRESSION: dispatchEvent('impression'); break; case google.ima.AdEvent.Type.CLICK: dispatchEvent('click'); break; default: break; } } /** * Any ad error handling comes through here * @param {Event} event */ }, { key: 'onAdError', value: function onAdError(event) { this.cancel(); this.player.debug.log('Ads error', event); } /** * Setup hooks for Plyr and window events. This ensures * the mid- and post-roll launch at the correct time. And * resize the advertisement when the player resizes */ }, { key: 'listeners', value: function listeners() { var _this7 = this; var container = this.player.elements.container; var time = void 0; // Add listeners to the required events this.player.on('ended', function () { _this7.loader.contentComplete(); }); this.player.on('seeking', function () { time = _this7.player.currentTime; return time; }); this.player.on('seeked', function () { var seekedTime = _this7.player.currentTime; _this7.cuePoints.forEach(function (cuePoint, index) { if (time < cuePoint && cuePoint < seekedTime) { _this7.manager.discardAdBreak(); _this7.cuePoints.splice(index, 1); } }); }); // Listen to the resizing of the window. And resize ad accordingly // TODO: eventually implement ResizeObserver window.addEventListener('resize', function () { _this7.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); }); } /** * Initialize the adsManager and start playing advertisements */ }, { key: 'play', value: function play() { var _this8 = this; var container = this.player.elements.container; if (!this.managerPromise) { return; } // Play the requested advertisement whenever the adsManager is ready this.managerPromise.then(function () { // Initialize the container. Must be done via a user action on mobile devices _this8.elements.displayContainer.initialize(); try { if (!_this8.initialized) { // Initialize the ads manager. Ad rules playlist will start at this time _this8.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules _this8.manager.start(); } _this8.initialized = true; } catch (adError) { // An error may be thrown if there was a problem with the // VAST response _this8.onAdError(adError); } }); } /** * Resume our video. */ }, { key: 'resumeContent', value: function resumeContent() { // Hide our ad container utils.toggleHidden(this.elements.container, true); // Ad is stopped this.playing = false; // Play our video if (this.player.currentTime < this.player.duration) { this.player.play(); } } /** * Pause our video */ }, { key: 'pauseContent', value: function pauseContent() { // Show our ad container. utils.toggleHidden(this.elements.container, false); // Ad is playing. this.playing = true; // Pause our video. this.player.pause(); } /** * Destroy the adsManager so we can grab new ads after this. If we don't then we're not * allowed to call new ads based on google policies, as they interpret this as an accidental * video requests. https://developers.google.com/interactive- * media-ads/docs/sdks/android/faq#8 */ }, { key: 'cancel', value: function cancel() { // Pause our video if (this.initialized) { this.resumeContent(); } // Tell our instance that we're done for now this.handleEventListeners('ERROR'); // Re-create our adsManager this.loadAds(); } /** * Re-create our adsManager */ }, { key: 'loadAds', value: function loadAds() { var _this9 = this; // Tell our adsManager to go bye bye this.managerPromise.then(function () { // Destroy our adsManager if (_this9.manager) { _this9.manager.destroy(); } // Re-set our adsManager promises _this9.managerPromise = new Promise(function (resolve) { _this9.on('ADS_MANAGER_LOADED', function () { return resolve(); }); _this9.player.debug.log(_this9.manager); }); // Now request some new advertisements _this9.requestAds(); }); } /** * Handles callbacks after an ad event was invoked * @param {string} event - Event type */ }, { key: 'handleEventListeners', value: function handleEventListeners(event) { if (utils.is.function(this.events[event])) { this.events[event].call(this); } } /** * Add event listeners * @param {string} event - Event type * @param {function} callback - Callback for when event occurs * @return {Ads} */ }, { key: 'on', value: function on(event, callback) { this.events[event] = callback; return this; } /** * Setup a safety timer for when the ad network doesn't respond for whatever reason. * The advertisement has 12 seconds to get its things together. We stop this timer when the * advertisement is playing, or when a user action is required to start, then we clear the * timer on ad ready * @param {number} time * @param {string} from */ }, { key: 'startSafetyTimer', value: function startSafetyTimer(time, from) { var _this10 = this; this.player.debug.log('Safety timer invoked from: ' + from); this.safetyTimer = setTimeout(function () { _this10.cancel(); _this10.clearSafetyTimer('startSafetyTimer()'); }, time); } /** * Clear our safety timer(s) * @param {string} from */ }, { key: 'clearSafetyTimer', value: function clearSafetyTimer(from) { if (!utils.is.nullOrUndefined(this.safetyTimer)) { this.player.debug.log('Safety timer cleared from: ' + from); clearTimeout(this.safetyTimer); this.safetyTimer = null; } } }]); return Ads; }(); // ========================================================================== // Plyr Event Listeners // ========================================================================== var browser$2 = utils.getBrowser(); var listeners = { // Global listeners global: function global() { var _this = this; var last = null; // Get the key code for an event var getKeyCode = function getKeyCode(event) { return event.keyCode ? event.keyCode : event.which; }; // Handle key press var handleKey = function handleKey(event) { var code = getKeyCode(event); var pressed = event.type === 'keydown'; var repeat = pressed && code === last; // Bail if a modifier key is set if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { return; } // If the event is bubbled from the media element // Firefox doesn't get the keycode for whatever reason if (!utils.is.number(code)) { return; } // Seek by the number keys var seekByKey = function seekByKey() { // Divide the max duration into 10th's and times by the number value _this.currentTime = _this.duration / 10 * (code - 48); }; // Handle the key on keydown // Reset on keyup if (pressed) { // Which keycodes should we prevent default var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67, 73, 76, 79]; // Check focused element // and if the focused element is not editable (e.g. text input) // and any that accept key input http://webaim.org/techniques/keyboard/ var focused = utils.getFocusElement(); if (utils.is.element(focused) && utils.matches(focused, _this.config.selectors.editable)) { return; } // If the code is found prevent default (e.g. prevent scrolling for arrows) if (preventDefault.includes(code)) { event.preventDefault(); event.stopPropagation(); } switch (code) { case 48: case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: // 0-9 if (!repeat) { seekByKey(); } break; case 32: case 75: // Space and K key if (!repeat) { _this.togglePlay(); } break; case 38: // Arrow up _this.increaseVolume(0.1); break; case 40: // Arrow down _this.decreaseVolume(0.1); break; case 77: // M key if (!repeat) { _this.muted = !_this.muted; } break; case 39: // Arrow forward _this.forward(); break; case 37: // Arrow back _this.rewind(); break; case 70: // F key _this.fullscreen.toggle(); break; case 67: // C key if (!repeat) { _this.toggleCaptions(); } break; case 76: // L key _this.loop = !_this.loop; break; /* case 73: this.setLoop('start'); break; case 76: this.setLoop(); break; case 79: this.setLoop('end'); break; */ default: break; } // Escape is handle natively when in full screen // So we only need to worry about non native if (!_this.fullscreen.enabled && _this.fullscreen.active && code === 27) { _this.fullscreen.toggle(); } // Store last code for next cycle last = code; } else { last = null; } }; // Keyboard shortcuts if (this.config.keyboard.global) { utils.on(window, 'keydown keyup', handleKey, false); } else if (this.config.keyboard.focused) { utils.on(this.elements.container, 'keydown keyup', handleKey, false); } // Detect tab focus // Remove class on blur/focusout utils.on(this.elements.container, 'focusout', function (event) { utils.toggleClass(event.target, _this.config.classNames.tabFocus, false); }); // Add classname to tabbed elements utils.on(this.elements.container, 'keydown', function (event) { if (event.keyCode !== 9) { return; } // Delay the adding of classname until the focus has changed // This event fires before the focusin event setTimeout(function () { utils.toggleClass(utils.getFocusElement(), _this.config.classNames.tabFocus, true); }, 0); }); // Toggle controls visibility based on mouse movement if (this.config.hideControls) { // Toggle controls on mouse events and entering fullscreen utils.on(this.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) { _this.toggleControls(event); }); } }, // Listen for media events media: function media() { var _this2 = this; // Time change on media utils.on(this.media, 'timeupdate seeking', function (event) { return ui.timeUpdate.call(_this2, event); }); // Display duration utils.on(this.media, 'durationchange loadedmetadata', function (event) { return ui.durationUpdate.call(_this2, event); }); // Check for audio tracks on load // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point utils.on(this.media, 'loadeddata', function () { utils.toggleHidden(_this2.elements.volume, !_this2.hasAudio); utils.toggleHidden(_this2.elements.buttons.mute, !_this2.hasAudio); }); // Handle the media finishing utils.on(this.media, 'ended', function () { // Show poster on end if (_this2.isHTML5 && _this2.isVideo && _this2.config.showPosterOnEnd) { // Restart _this2.restart(); // Re-load media _this2.media.load(); } }); // Check for buffer progress utils.on(this.media, 'progress playing', function (event) { return ui.updateProgress.call(_this2, event); }); // Handle native mute utils.on(this.media, 'volumechange', function (event) { return ui.updateVolume.call(_this2, event); }); // Handle native play/pause utils.on(this.media, 'playing play pause ended', function (event) { return ui.checkPlaying.call(_this2, event); }); // Loading utils.on(this.media, 'waiting canplay seeked playing', function (event) { return ui.checkLoading.call(_this2, event); }); // Check if media failed to load // utils.on(this.media, 'play', event => ui.checkFailed.call(this, event)); // Click video if (this.supported.ui && this.config.clickToPlay && !this.isAudio) { // Re-fetch the wrapper var wrapper = utils.getElement.call(this, '.' + this.config.classNames.video); // Bail if there's no wrapper (this should never happen) if (!utils.is.element(wrapper)) { return; } // On click play, pause ore restart utils.on(wrapper, 'click', function () { // Touch devices will just show controls (if we're hiding controls) if (_this2.config.hideControls && support.touch && !_this2.paused) { return; } if (_this2.paused) { _this2.play(); } else if (_this2.ended) { _this2.restart(); _this2.play(); } else { _this2.pause(); } }); } // Disable right click if (this.supported.ui && this.config.disableContextMenu) { utils.on(this.media, 'contextmenu', function (event) { event.preventDefault(); }, false); } // Volume change utils.on(this.media, 'volumechange', function () { // Save to storage _this2.storage.set({ volume: _this2.volume, muted: _this2.muted }); }); // Speed change utils.on(this.media, 'ratechange', function () { // Update UI controls.updateSetting.call(_this2, 'speed'); // Save to storage _this2.storage.set({ speed: _this2.speed }); }); // Quality change utils.on(this.media, 'qualitychange', function () { // Update UI controls.updateSetting.call(_this2, 'quality'); // Save to storage _this2.storage.set({ quality: _this2.quality }); }); // Caption language change utils.on(this.media, 'languagechange', function () { // Update UI controls.updateSetting.call(_this2, 'captions'); // Save to storage _this2.storage.set({ language: _this2.language }); }); // Captions toggle utils.on(this.media, 'captionsenabled captionsdisabled', function () { // Update UI controls.updateSetting.call(_this2, 'captions'); // Save to storage _this2.storage.set({ captions: _this2.captions.active }); }); // Proxy events to container // Bubble up key events for Edge utils.on(this.media, this.config.events.concat(['keyup', 'keydown']).join(' '), function (event) { var detail = {}; // Get error details from media if (event.type === 'error') { detail = _this2.media.error; } utils.dispatchEvent.call(_this2, _this2.elements.container, event.type, true, detail); }); }, // Listen for control events controls: function controls$$1() { var _this3 = this; // IE doesn't support input event, so we fallback to change var inputEvent = browser$2.isIE ? 'change' : 'input'; // Trigger custom and default handlers var proxy = function proxy(event, handlerKey, defaultHandler) { var customHandler = _this3.config.listeners[handlerKey]; // Execute custom handler if (utils.is.function(customHandler)) { customHandler.call(_this3, event); } // Only call default handler if not prevented in custom handler if (!event.defaultPrevented && utils.is.function(defaultHandler)) { defaultHandler.call(_this3, event); } }; // Play/pause toggle utils.on(this.elements.buttons.play, 'click', function (event) { return proxy(event, 'play', function () { _this3.togglePlay(); }); }); // Pause utils.on(this.elements.buttons.restart, 'click', function (event) { return proxy(event, 'restart', function () { _this3.restart(); }); }); // Rewind utils.on(this.elements.buttons.rewind, 'click', function (event) { return proxy(event, 'rewind', function () { _this3.rewind(); }); }); // Rewind utils.on(this.elements.buttons.forward, 'click', function (event) { return proxy(event, 'forward', function () { _this3.forward(); }); }); // Mute toggle utils.on(this.elements.buttons.mute, 'click', function (event) { return proxy(event, 'mute', function () { _this3.muted = !_this3.muted; }); }); // Captions toggle utils.on(this.elements.buttons.captions, 'click', function (event) { return proxy(event, 'captions', function () { _this3.toggleCaptions(); }); }); // Fullscreen toggle utils.on(this.elements.buttons.fullscreen, 'click', function (event) { return proxy(event, 'fullscreen', function () { _this3.fullscreen.toggle(); }); }); // Picture-in-Picture utils.on(this.elements.buttons.pip, 'click', function (event) { return proxy(event, 'pip', function () { _this3.pip = 'toggle'; }); }); // Airplay utils.on(this.elements.buttons.airplay, 'click', function (event) { return proxy(event, 'airplay', function () { _this3.airplay(); }); }); // Settings menu utils.on(this.elements.buttons.settings, 'click', function (event) { controls.toggleMenu.call(_this3, event); }); // Click anywhere closes menu utils.on(document.documentElement, 'click', function (event) { controls.toggleMenu.call(_this3, event); }); // Settings menu utils.on(this.elements.settings.form, 'click', function (event) { event.stopPropagation(); // Settings menu items - use event delegation as items are added/removed if (utils.matches(event.target, _this3.config.selectors.inputs.language)) { proxy(event, 'language', function () { _this3.language = event.target.value; }); } else if (utils.matches(event.target, _this3.config.selectors.inputs.quality)) { proxy(event, 'quality', function () { _this3.quality = event.target.value; }); } else if (utils.matches(event.target, _this3.config.selectors.inputs.speed)) { proxy(event, 'speed', function () { _this3.speed = parseFloat(event.target.value); }); } else { controls.showTab.call(_this3, event); } }); // Seek utils.on(this.elements.inputs.seek, inputEvent, function (event) { return proxy(event, 'seek', function () { _this3.currentTime = event.target.value / event.target.max * _this3.duration; }); }); // Current time invert // Only if one time element is used for both currentTime and duration if (this.config.toggleInvert && !utils.is.element(this.elements.display.duration)) { utils.on(this.elements.display.currentTime, 'click', function () { // Do nothing if we're at the start if (_this3.currentTime === 0) { return; } _this3.config.invertTime = !_this3.config.invertTime; ui.timeUpdate.call(_this3); }); } // Volume utils.on(this.elements.inputs.volume, inputEvent, function (event) { return proxy(event, 'volume', function () { _this3.volume = event.target.value; }); }); // Polyfill for lower fill in for webkit if (browser$2.isWebkit) { utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', function (event) { controls.updateRangeFill.call(_this3, event.target); }); } // Seek tooltip utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', function (event) { return controls.updateSeekTooltip.call(_this3, event); }); // Toggle controls visibility based on mouse movement if (this.config.hideControls) { // Watch for cursor over controls so they don't hide when trying to interact utils.on(this.elements.controls, 'mouseenter mouseleave', function (event) { _this3.elements.controls.hover = event.type === 'mouseenter'; }); // Watch for cursor over controls so they don't hide when trying to interact utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) { _this3.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); }); // Focus in/out on controls utils.on(this.elements.controls, 'focusin focusout', function (event) { _this3.toggleControls(event); }); } // Mouse wheel for volume utils.on(this.elements.inputs.volume, 'wheel', function (event) { return proxy(event, 'volume', function () { // Detect "natural" scroll - suppored on OS X Safari only // Other browsers on OS X will be inverted until support improves var inverted = event.webkitDirectionInvertedFromDevice; var step = 1 / 50; var direction = 0; // Scroll down (or up on natural) to decrease if (event.deltaY < 0 || event.deltaX > 0) { if (inverted) { _this3.decreaseVolume(step); direction = -1; } else { _this3.increaseVolume(step); direction = 1; } } // Scroll up (or down on natural) to increase if (event.deltaY > 0 || event.deltaX < 0) { if (inverted) { _this3.increaseVolume(step); direction = 1; } else { _this3.decreaseVolume(step); direction = -1; } } // Don't break page scrolling at max and min if (direction === 1 && _this3.media.volume < 1 || direction === -1 && _this3.media.volume > 0) { event.preventDefault(); } }); }, false); } }; // ========================================================================== // 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 listeners.media.call(this); // 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 listeners.controls.call(this); } // 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 = this.config.i18n.play; // 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', this.config.i18n.frameTitle.replace('{title}', title)); } }, // 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) { var _this4 = this; 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': value = function () { var buffered = _this4.media.buffered; if (buffered && buffered.length) { // HTML5 return utils.getPercentage(buffered.end(0), _this4.duration); } else if (utils.is.number(buffered)) { // YouTube returns between 0 and 1 return buffered * 100; } return 0; }(); ui.setProgress.call(this, this.elements.display.buffer, value); 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$1 = utils.getBrowser(); var controls = { // Webkit polyfill for lower fill range updateRangeFill: function updateRangeFill(target) { // WebKit only if (!browser$1.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$1.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 = this.config.i18n[type]; 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