From 72a71a605b2afe2df60a7fee178d3fc9e483d17d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Fri, 27 Apr 2018 20:06:14 +1000 Subject: Fix for default timestamp --- dist/plyr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index a6842ffd..fffa2756 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -77,7 +77,7 @@ var defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.2.3/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.2.4/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', @@ -3233,7 +3233,7 @@ var controls = { var container = utils.createElement('div', utils.extend(attributes, { class: 'plyr__time ' + attributes.class, 'aria-label': i18n.get(type, this.config) - }), '0:00'); + }), '00:00'); // Reference for updates this.elements.display[type] = container; -- cgit v1.2.3 From 9ebc2719d31e39b822eda42c2eb3272330e9fc5d Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 6 May 2018 00:49:12 +1000 Subject: v3.3.0 --- dist/plyr.js | 6092 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 3087 insertions(+), 3005 deletions(-) (limited to 'dist/plyr.js') diff --git a/dist/plyr.js b/dist/plyr.js index fffa2756..ce5737c1 100644 --- a/dist/plyr.js +++ b/dist/plyr.js @@ -4,602 +4,270 @@ (global.Plyr = factory()); }(this, (function () { 'use strict'; -// ========================================================================== -// Plyr supported types and providers -// ========================================================================== +var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; -var providers = { - html5: 'html5', - youtube: 'youtube', - vimeo: 'vimeo' -}; +function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; +} -var types = { - audio: 'audio', - video: 'video' -}; +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 + */ -// ========================================================================== -// Plyr default config -// ========================================================================== +var devnull = function() {}, + bundleIdCache = {}, + bundleResultCache = {}, + bundleCallbackQueue = {}; -var defaults = { - // Disable - enabled: true, - // Custom media title - title: '', +/** + * 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]; - // Logging to console - debug: false, + var depsNotFound = [], + i = bundleIds.length, + numWaiting = i, + fn, + bundleId, + r, + q; - // Auto play (if supported) - autoplay: false, + // define callback function + fn = function (bundleId, pathsNotFound) { + if (pathsNotFound.length) depsNotFound.push(bundleId); - // Only allow one media playing at once (vimeo only) - autopause: true, + numWaiting--; + if (!numWaiting) callbackFn(depsNotFound); + }; - // Default time to skip when rewind/fast forward - seekTime: 10, + // register callback + while (i--) { + bundleId = bundleIds[i]; - // Default volume - volume: 1, - muted: false, + // execute callback if in result cache + r = bundleResultCache[bundleId]; + if (r) { + fn(bundleId, r); + continue; + } - // Pass a custom duration - duration: null, + // add to callback queue + q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; + q.push(fn); + } +} - // 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, +/** + * 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; - // Clicking the currentTime inverts it's value to show time left rather than elapsed - toggleInvert: true, + var q = bundleCallbackQueue[bundleId]; - // Aspect ratio (for embeds) - ratio: '16:9', + // cache result + bundleResultCache[bundleId] = pathsNotFound; - // Click video container to play/pause - clickToPlay: true, + // exit if queue is empty + if (!q) return; - // Auto hide the controls - hideControls: true, + // empty callback queue + while (q.length) { + q[0](bundleId, pathsNotFound); + q.splice(0, 1); + } +} - // Revert to poster on finish (HTML5 - will cause reload) - showPosterOnEnd: false, - // Disable the standard context menu - disableContextMenu: true, +/** + * 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}; - // Sprite (for icons) - loadSprite: true, - iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.2.4/plyr.svg', + // success and error callbacks + if (depsNotFound.length) (args.error || devnull)(depsNotFound); + else (args.success || devnull)(args); +} - // Blank video (used to prevent errors on source change) - blankVideo: 'https://cdn.plyr.io/static/blank.mp4', - // Quality default - quality: { - default: 576, - options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240, 'default'] - }, +/** + * Load individual file. + * @param {string} path - The file path + * @param {Function} callbackFn - The callback function + */ +function loadFile(path, callbackFn, args, numTries) { + var doc = document, + async = args.async, + maxTries = (args.numRetries || 0) + 1, + beforeCallbackFn = args.before || devnull, + pathStripped = path.replace(/^(css|img)!/, ''), + isCss, + e; - // Set loops - loop: { - active: false - // start: null, - // end: null, - }, + numTries = numTries || 0; - // Speed default and options to display - speed: { - selected: 1, - options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] - }, + if (/(^css!|\.css$)/.test(path)) { + isCss = true; - // Keyboard shortcut settings - keyboard: { - focused: true, - global: false - }, + // css + e = doc.createElement('link'); + e.rel = 'stylesheet'; + e.href = pathStripped; //.replace(/^css!/, ''); // remove "css!" prefix + } else if (/(^img!|\.(png|gif|jpg|svg)$)/.test(path)) { + // image + e = doc.createElement('img'); + e.src = pathStripped; + } else { + // javascript + e = doc.createElement('script'); + e.src = path; + e.async = async === undefined ? true : async; + } - // Display tooltips - tooltips: { - controls: false, - seek: true - }, + e.onload = e.onerror = e.onbeforeload = function (ev) { + var result = ev.type[0]; - // Captions settings - captions: { - active: false, - language: (navigator.language || navigator.userLanguage).split('-')[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'; + } + } - // Fullscreen settings - fullscreen: { - enabled: true, // Allow fullscreen? - fallback: true, // Fallback for vintage browsers - iosNative: false // Use the native fullscreen in iOS (disables custom controls) - }, + // handle retries in case of load failure + if (result == 'e') { + // increment counter + numTries += 1; - // Local storage - storage: { - enabled: true, - key: 'plyr' - }, + // exit function and try again + if (numTries < maxTries) { + return loadFile(path, callbackFn, args, numTries); + } + } - // Default controls - controls: ['play-large', - // 'restart', - // 'rewind', - 'play', - // 'fast-forward', - 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'], - settings: ['captions', 'quality', 'speed'], + // execute callback + callbackFn(path, result, ev.defaultPrevented); + }; - // 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', - normal: 'Normal', - quality: 'Quality', - loop: 'Loop', - start: 'Start', - end: 'End', - all: 'All', - reset: 'Reset', - disabled: 'Disabled', - enabled: 'Enabled', - advertisement: 'Ad' - }, + // add to document (unless callback returns `false`) + if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); +} - // 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 - }, +/** + * 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]; - // 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', + var numWaiting = paths.length, + x = numWaiting, + pathsNotFound = [], + fn, + i; - // Custom events - 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', + // define callback function + fn = function(path, result, defaultPrevented) { + // handle error + if (result == 'e') pathsNotFound.push(path); - // YouTube - 'statechange', 'qualitychange', 'qualityrequested', + // 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; + } - // Ads - 'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'], + numWaiting--; + if (!numWaiting) callbackFn(pathsNotFound); + }; - // 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' - } - }, + // load scripts + for (i=0; i < x; i++) loadFile(paths[i], fn, args); +} - // 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' - } - }, +/** + * 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; - // API keys - keys: { - google: null - }, + // bundleId (if string) + if (arg1 && arg1.trim) bundleId = arg1; - // Advertisements plugin - // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio - ads: { - enabled: false, - publisherId: '' + // 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; } -}; + } -var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + // load scripts + loadFiles(paths, function (pathsNotFound) { + // execute callbacks + executeCallbacks(args, pathsNotFound); -function createCommonjsModule(fn, module) { - return module = { exports: {} }, fn(module, module.exports), module.exports; + // publish bundle load event + publish(bundleId, pathsNotFound); + }, args); } -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 + * Execute callbacks when dependencies have been satisfied. + * @param {(string|string[])} deps - List of bundle ids + * @param {Object} args - success/error arguments */ +loadjs.ready = function ready(deps, args) { + // subscribe to bundle load event + subscribe(deps, function (depsNotFound) { + // execute callbacks + executeCallbacks(args, depsNotFound); + }); -var devnull = function() {}, - bundleIdCache = {}, - bundleResultCache = {}, - bundleCallbackQueue = {}; - - -/** - * Subscribe to bundle load event. - * @param {string[]} bundleIds - Bundle ids - * @param {Function} callbackFn - The callback function - */ -function subscribe(bundleIds, callbackFn) { - // listify - bundleIds = bundleIds.push ? bundleIds : [bundleIds]; - - var depsNotFound = [], - i = bundleIds.length, - numWaiting = i, - fn, - bundleId, - r, - q; - - // define callback function - fn = function (bundleId, pathsNotFound) { - if (pathsNotFound.length) depsNotFound.push(bundleId); - - numWaiting--; - if (!numWaiting) callbackFn(depsNotFound); - }; - - // register callback - while (i--) { - bundleId = bundleIds[i]; - - // execute callback if in result cache - r = bundleResultCache[bundleId]; - if (r) { - fn(bundleId, r); - continue; - } - - // add to callback queue - q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; - q.push(fn); - } -} - - -/** - * Publish bundle load event. - * @param {string} bundleId - Bundle id - * @param {string[]} pathsNotFound - List of files not found - */ -function publish(bundleId, pathsNotFound) { - // exit if id isn't defined - if (!bundleId) return; - - var q = bundleCallbackQueue[bundleId]; - - // cache result - bundleResultCache[bundleId] = pathsNotFound; - - // exit if queue is empty - if (!q) return; - - // empty callback queue - while (q.length) { - q[0](bundleId, pathsNotFound); - q.splice(0, 1); - } -} - - -/** - * Execute callbacks. - * @param {Object or Function} args - The callback args - * @param {string[]} depsNotFound - List of dependencies not found - */ -function executeCallbacks(args, depsNotFound) { - // accept function as argument - if (args.call) args = {success: args}; - - // success and error callbacks - if (depsNotFound.length) (args.error || devnull)(depsNotFound); - else (args.success || devnull)(args); -} - - -/** - * Load individual file. - * @param {string} path - The file path - * @param {Function} callbackFn - The callback function - */ -function loadFile(path, callbackFn, args, numTries) { - var doc = document, - async = args.async, - maxTries = (args.numRetries || 0) + 1, - beforeCallbackFn = args.before || devnull, - pathStripped = path.replace(/^(css|img)!/, ''), - isCss, - e; - - numTries = numTries || 0; - - if (/(^css!|\.css$)/.test(path)) { - isCss = true; - - // css - e = doc.createElement('link'); - e.rel = 'stylesheet'; - e.href = pathStripped; //.replace(/^css!/, ''); // remove "css!" prefix - } else if (/(^img!|\.(png|gif|jpg|svg)$)/.test(path)) { - // image - e = doc.createElement('img'); - e.src = pathStripped; - } else { - // javascript - e = doc.createElement('script'); - e.src = path; - e.async = async === undefined ? true : async; - } - - e.onload = e.onerror = e.onbeforeload = function (ev) { - var result = ev.type[0]; - - // Note: The following code isolates IE using `hideFocus` and treats empty - // stylesheets as failures to get around lack of onerror support - if (isCss && 'hideFocus' in e) { - try { - if (!e.sheet.cssText.length) result = 'e'; - } catch (x) { - // sheets objects created from load errors don't allow access to - // `cssText` - result = 'e'; - } - } - - // handle retries in case of load failure - if (result == 'e') { - // increment counter - numTries += 1; - - // exit function and try again - if (numTries < maxTries) { - return loadFile(path, callbackFn, args, numTries); - } - } - - // execute callback - callbackFn(path, result, ev.defaultPrevented); - }; - - // add to document (unless callback returns `false`) - if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); -} - - -/** - * Load multiple files. - * @param {string[]} paths - The file paths - * @param {Function} callbackFn - The callback function - */ -function loadFiles(paths, callbackFn, args) { - // listify paths - paths = paths.push ? paths : [paths]; - - var numWaiting = paths.length, - x = numWaiting, - pathsNotFound = [], - fn, - i; - - // define callback function - fn = function(path, result, defaultPrevented) { - // handle error - if (result == 'e') pathsNotFound.push(path); - - // handle beforeload event. If defaultPrevented then that means the load - // will be blocked (ex. Ghostery/ABP on Safari) - if (result == 'b') { - if (defaultPrevented) pathsNotFound.push(path); - else return; - } - - numWaiting--; - if (!numWaiting) callbackFn(pathsNotFound); - }; - - // load scripts - for (i=0; i < x; i++) loadFile(paths[i], fn, args); -} - - -/** - * Initiate script load and register bundle. - * @param {(string|string[])} paths - The file paths - * @param {(string|Function)} [arg1] - The bundleId or success callback - * @param {Function} [arg2] - The success or error callback - * @param {Function} [arg3] - The error callback - */ -function loadjs(paths, arg1, arg2) { - var bundleId, - args; - - // bundleId (if string) - if (arg1 && arg1.trim) bundleId = arg1; - - // args (default is {}) - args = (bundleId ? arg2 : arg1) || {}; - - // throw error if bundle is already defined - if (bundleId) { - if (bundleId in bundleIdCache) { - throw "LoadJS"; - } else { - bundleIdCache[bundleId] = true; - } - } - - // load scripts - loadFiles(paths, function (pathsNotFound) { - // execute callbacks - executeCallbacks(args, pathsNotFound); - - // publish bundle load event - publish(bundleId, pathsNotFound); - }, args); -} - - -/** - * Execute callbacks when dependencies have been satisfied. - * @param {(string|string[])} deps - List of bundle ids - * @param {Object} args - success/error arguments - */ -loadjs.ready = function ready(deps, args) { - // subscribe to bundle load event - subscribe(deps, function (depsNotFound) { - // execute callbacks - executeCallbacks(args, depsNotFound); - }); - - return loadjs; -}; + return loadjs; +}; /** @@ -636,6 +304,21 @@ return loadjs; })); }); +// ========================================================================== +// Plyr supported types and providers +// ========================================================================== + +var providers = { + html5: 'html5', + youtube: 'youtube', + vimeo: 'vimeo' +}; + +var types = { + audio: 'audio', + video: 'video' +}; + var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -988,14 +671,14 @@ var utils = { }, - // Remove an element + // Remove element(s) removeElement: function removeElement(element) { - if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { + if (utils.is.nodeList(element) || utils.is.array(element)) { + Array.from(element).forEach(utils.removeElement); return; } - if (utils.is.nodeList(element) || utils.is.array(element)) { - Array.from(element).forEach(utils.removeElement); + if (!utils.is.element(element) || !utils.is.element(element.parentNode)) { return; } @@ -1167,61 +850,6 @@ var utils = { }, - // Find the UI controls and store references in custom controls - // TODO: Allow settings menus with custom controls - findElements: function findElements() { - try { - this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper); - - // Buttons - this.elements.buttons = { - play: utils.getElements.call(this, this.config.selectors.buttons.play), - pause: utils.getElement.call(this, this.config.selectors.buttons.pause), - restart: utils.getElement.call(this, this.config.selectors.buttons.restart), - rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind), - fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward), - mute: utils.getElement.call(this, this.config.selectors.buttons.mute), - pip: utils.getElement.call(this, this.config.selectors.buttons.pip), - airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay), - settings: utils.getElement.call(this, this.config.selectors.buttons.settings), - captions: utils.getElement.call(this, this.config.selectors.buttons.captions), - fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen) - }; - - // Progress - this.elements.progress = utils.getElement.call(this, this.config.selectors.progress); - - // Inputs - this.elements.inputs = { - seek: utils.getElement.call(this, this.config.selectors.inputs.seek), - volume: utils.getElement.call(this, this.config.selectors.inputs.volume) - }; - - // Display - this.elements.display = { - buffer: utils.getElement.call(this, this.config.selectors.display.buffer), - currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime), - duration: utils.getElement.call(this, this.config.selectors.display.duration) - }; - - // Seek tooltip - if (utils.is.element(this.elements.progress)) { - this.elements.display.seekTooltip = this.elements.progress.querySelector('.' + this.config.classNames.tooltip); - } - - return true; - } catch (error) { - // Log it - this.debug.warn('It looks like there is a problem with your custom controls HTML', error); - - // Restore native video controls - this.toggleNativeControls(true); - - return false; - } - }, - - // Get the focused element getFocusElement: function getFocusElement() { var focused = document.activeElement; @@ -1395,6 +1023,22 @@ var utils = { }, + // Format string + format: function format(input) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + if (utils.is.empty(input)) { + return input; + } + + return input.toString().replace(/{(\d+)}/g, function (match, i) { + return utils.is.string(args[i]) ? args[i] : ''; + }); + }, + + // Get percentage getPercentage: function getPercentage(current, max) { if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { @@ -1508,8 +1152,8 @@ var utils = { 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]; + for (var _len2 = arguments.length, sources = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + sources[_key2 - 1] = arguments[_key2]; } if (!sources.length) { @@ -1860,280 +1504,158 @@ var support = { reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches }; -// ========================================================================== -// Console wrapper // ========================================================================== -var noop = function noop() {}; +var html5 = { + getSources: function getSources() { + if (!this.isHTML5) { + return null; + } -var Console = function () { - function Console() { - var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - classCallCheck(this, Console); + return this.media.querySelectorAll('source'); + }, - this.enabled = window.console && enabled; - if (this.enabled) { - this.log('Debugging enabled'); + // Get quality levels + getQualityOptions: function getQualityOptions() { + if (!this.isHTML5) { + return null; } - } - 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; + // Get sources + var sources = html5.getSources.call(this); + + if (utils.is.empty(sources)) { + return null; } - }, { - key: 'warn', - get: function get$$1() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; + + // Get with size attribute + var sizes = Array.from(sources).filter(function (source) { + return !utils.is.empty(source.getAttribute('size')); + }); + + // If none, bail + if (utils.is.empty(sizes)) { + return null; } - }, { - key: 'error', - get: function get$$1() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; + + // Reduce to unique list + return utils.dedupe(sizes.map(function (source) { + return Number(source.getAttribute('size')); + })); + }, + extend: function extend() { + if (!this.isHTML5) { + return; } - }]); - return Console; -}(); -// ========================================================================== + var player = this; -var browser = utils.getBrowser(); + // Quality + Object.defineProperty(player.media, 'quality', { + get: function get() { + // Get sources + var sources = html5.getSources.call(player); -function onChange() { - if (!this.enabled) { - return; - } + if (utils.is.empty(sources)) { + return null; + } - // Update toggle button - var button = this.player.elements.buttons.fullscreen; - if (utils.is.element(button)) { - utils.toggleState(button, this.active); - } + var matches = Array.from(sources).filter(function (source) { + return source.getAttribute('src') === player.source; + }); - // Trigger an event - utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); + if (utils.is.empty(matches)) { + return null; + } - // Trap focus in container - if (!browser.isIos) { - utils.trapFocus.call(this.player, this.target, this.active); - } -} + return Number(matches[0].getAttribute('size')); + }, + set: function set(input) { + // Get sources + var sources = html5.getSources.call(player); -function toggleFallback() { - var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + if (utils.is.empty(sources)) { + return; + } - // 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); - } + // Get matches for requested size + var matches = Array.from(sources).filter(function (source) { + return Number(source.getAttribute('size')) === input; + }); - // Toggle scroll - document.body.style.overflow = toggle ? 'hidden' : ''; + // No matches for requested size + if (utils.is.empty(matches)) { + return; + } - // Toggle class hook - utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); + // Get supported sources + var supported = matches.filter(function (source) { + return support.mime.call(player, source.getAttribute('type')); + }); - // Toggle button and fire events - onChange.call(this); -} + // No supported sources + if (utils.is.empty(supported)) { + return; + } -var Fullscreen = function () { - function Fullscreen(player) { - var _this = this; + // Trigger change event + utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { + quality: input + }); - classCallCheck(this, Fullscreen); + // Get current state + var currentTime = player.currentTime, + playing = player.playing; - // Keep reference to parent - this.player = player; + // Set new source - // Get prefix - this.prefix = Fullscreen.prefix; - this.property = Fullscreen.property; + player.media.src = supported[0].getAttribute('src'); - // Scroll position - this.scrollPosition = { x: 0, y: 0 }; + // Load new source + player.media.load(); - // 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); - }); + // Resume playing + if (playing) { + player.play(); + } - // Fullscreen toggle on double click - utils.on(this.player.elements.container, 'dblclick', function (event) { - // Ignore double click in controls - if (utils.is.element(_this.player.elements.controls) && _this.player.elements.controls.contains(event.target)) { - return; - } + // Restore time + player.currentTime = currentTime; - _this.toggle(); + // Trigger change event + utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { + quality: input + }); + } }); + }, - // 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 + 'Request' + this.property](); - } - } - - // Bail from fullscreen - - }, { - key: 'exit', - value: function exit() { - if (!this.enabled) { - return; - } - - // iOS native fullscreen - if (browser.isIos && this.player.config.fullscreen.iosNative) { - this.target.webkitExitFullscreen(); - this.player.play(); - } else if (!Fullscreen.native) { - toggleFallback.call(this, false); - } else if (!this.prefix) { - (document.cancelFullScreen || document.exitFullscreen).call(document); - } else if (!utils.is.empty(this.prefix)) { - var action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; - document['' + this.prefix + action + this.property](); - } - } - - // Toggle state - - }, { - key: 'toggle', - value: function toggle() { - if (!this.active) { - this.enter(); - } else { - this.exit(); - } - } - }, { - key: 'enabled', - - - // Determine if fullscreen is enabled - get: function get$$1() { - return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo; - } - - // Get active state - - }, { - key: 'active', - get: function get$$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.property + 'Element']; - - return element === this.target; - } - - // Get target element - - }, { - key: 'target', - get: function get$$1() { - return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; - } - }], [{ - key: 'native', - get: function get$$1() { - return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); + // Cancel current network requests + // See https://github.com/sampotts/plyr/issues/174 + cancelRequests: function cancelRequests() { + if (!this.isHTML5) { + return; } - // Get the prefix for handlers - - }, { - key: 'prefix', - get: function get$$1() { - // No prefix - if (utils.is.function(document.exitFullscreen)) { - return ''; - } - - // Check for fullscreen support by vendor prefix - var value = ''; - var prefixes = ['webkit', 'moz', 'ms']; + // Remove child sources + utils.removeElement(html5.getSources()); - prefixes.some(function (pre) { - if (utils.is.function(document[pre + 'ExitFullscreen']) || utils.is.function(document[pre + 'CancelFullScreen'])) { - value = pre; - return true; - } + // Set blank video src attribute + // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error + // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection + this.media.setAttribute('src', this.config.blankVideo); - return false; - }); + // Load the new empty source + // This will cancel existing requests + // See https://github.com/sampotts/plyr/issues/174 + this.media.load(); - return value; - } - }, { - key: 'property', - get: function get$$1() { - return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; - } - }]); - return Fullscreen; -}(); + // Debugging + this.debug.log('Cancelled network requests'); + } +}; // ========================================================================== @@ -2167,513 +1689,275 @@ var i18n = { // ========================================================================== -var captions = { - // Setup captions - setup: function setup() { - // Requires UI support - if (!this.supported.ui) { - return; - } +// Sniff out the browser +var browser = utils.getBrowser(); - // Set default language if not set - var stored = this.storage.get('language'); +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); + }, - if (!utils.is.empty(stored)) { - this.captions.language = stored; - } - if (utils.is.empty(this.captions.language)) { - this.captions.language = this.config.captions.language.toLowerCase(); + // 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'); } + }, - // 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; - } - } + // Setup the UI + build: function build() { + var _this = this; - // 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); - } + // 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 the container - if (!utils.is.element(this.elements.captions)) { - this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); + // Inject custom controls if not present + if (!utils.is.element(this.elements.controls)) { + // Inject custom controls + controls.inject.call(this); - utils.insertAfter(this.elements.captions, this.elements.wrapper); + // Re-attach control listeners + this.listeners.controls(); } - // Set the class hook - utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); + // Remove native controls + ui.toggleNativeControls.call(this); - // Get tracks - var tracks = captions.getTracks.call(this); + // Captions + captions.setup.call(this); - // If no caption file exists, hide container for caption text - if (utils.is.empty(tracks)) { - return; - } + // Reset volume + this.volume = null; - // Get browser info - var browser = utils.getBrowser(); + // Reset mute state + this.muted = null; - // 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'); + // Reset speed + this.speed = null; - Array.from(elements).forEach(function (track) { - var src = track.getAttribute('src'); - var href = utils.parseUrl(src); + // Reset loop state + this.loop = null; - 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); - }); - } - }); - } + // Reset quality setting + this.quality = null; - // Set language - captions.setLanguage.call(this); + // Reset volume display + ui.updateVolume.call(this); - // Enable UI - captions.show.call(this); + // Reset time display + ui.timeUpdate.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); - } + // Update the UI + ui.checkPlaying.call(this); + + // Check for picture-in-picture support + utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); + + // Check for airplay support + utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); + + // Add iOS class + utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); + + // Add touch class + utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); + + // Ready for API calls + this.ready = true; + + // Ready event at end of execution stack + setTimeout(function () { + utils.dispatchEvent.call(_this, _this.media, 'ready'); + }, 0); + + // Set the title + ui.setTitle.call(this); + + // Set the poster image + ui.setPoster.call(this); }, - // Set the captions language - setLanguage: function setLanguage() { - var _this = this; + // Setup aria attribute for play and iframe title + setTitle: function setTitle() { + // Find the current text + var label = i18n.get('play', this.config); - // 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); - }); + // 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; - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line - track.mode = 'hidden'; + // 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); }); + } - // Get current track - var currentTrack = captions.getCurrentTrack.call(this); + // Set iframe title + // https://github.com/sampotts/plyr/issues/124 + if (this.isEmbed) { + var iframe = utils.getElement.call(this, 'iframe'); - // 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); - } + if (!utils.is.element(iframe)) { + return; } - } else if (this.isVimeo && this.captions.active) { - this.embed.enableTextTrack(this.language); + + // Default to media type + var title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; + var format = i18n.get('frameTitle', this.config); + + iframe.setAttribute('title', format.replace('{title}', title)); } }, - // Get the tracks - getTracks: function getTracks() { - // Return empty array at least - if (utils.is.nullOrUndefined(this.media)) { - return []; + // Set the poster image + setPoster: function setPoster() { + if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) { + return; } - // Only get accepted kinds - return Array.from(this.media.textTracks || []).filter(function (track) { - return ['captions', 'subtitles'].includes(track.kind); - }); + // Set the inline style + var posters = this.poster.split(','); + this.elements.poster.style.backgroundImage = posters.map(function (p) { + return 'url(\'' + p + '\')'; + }).join(','); }, - // Get the current track for the current language - getCurrentTrack: function getCurrentTrack() { - var _this2 = this; - - var tracks = captions.getTracks.call(this); + // 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.paused, this.paused); + utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); - if (!tracks.length) { - return null; - } + // Set ARIA state + utils.toggleState(this.elements.buttons.play, this.playing); - // Get track based on current language - var track = tracks.find(function (track) { - return track.language.toLowerCase() === _this2.language; - }); + // Toggle controls + this.toggleControls(!this.playing); + }, - // Get the with default attribute - if (!track) { - track = utils.getElement.call(this, 'track[default]'); - } - // Get the first track - if (!track) { - var _tracks = slicedToArray(tracks, 1); + // Check if media is loading + checkLoading: function checkLoading(event) { + var _this2 = this; - track = _tracks[0]; - } + this.loading = ['stalled', 'waiting'].includes(event.type); - return track; - }, + // 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); - // Get UI label for track - getLabel: function getLabel(track) { - var currentTrack = track; + // Show controls if loading, hide if done + _this2.toggleControls(_this2.loading); + }, this.loading ? 250 : 0); + }, - if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { - currentTrack = captions.getCurrentTrack.call(this); - } - if (utils.is.track(currentTrack)) { - if (!utils.is.empty(currentTrack.label)) { - return currentTrack.label; - } + // Check if media failed to load + checkFailed: function checkFailed() { + var _this3 = this; - if (!utils.is.empty(currentTrack.language)) { - return track.language.toUpperCase(); - } + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState + this.failed = this.media.networkState === 3; - return i18n.get('enabled', this.config); + if (this.failed) { + utils.toggleClass(this.elements.container, this.config.classNames.loading, false); + utils.toggleClass(this.elements.container, this.config.classNames.error, true); } - return i18n.get('disabled', this.config); - }, + // 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); - // 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; + // Show controls if loading, hide if done + _this3.toggleControls(_this3.loading); + }, this.loading ? 250 : 0); + }, - var active = activeCues.length && activeCues[0]; - var currentTrack = captions.getCurrentTrack.call(this); - // Only display current track - if (track !== currentTrack) { + // Update volume UI and storage + updateVolume: function updateVolume() { + if (!this.supported.ui) { 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); + // Update range + if (utils.is.element(this.elements.inputs.volume)) { + ui.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume); } - utils.dispatchEvent.call(this, this.media, 'cuechange'); + // Update mute state + if (utils.is.element(this.elements.buttons.mute)) { + utils.toggleState(this.elements.buttons.mute, this.muted || this.volume === 0); + } }, - // Set the current caption - setText: function setText(input) { - // Requires UI - if (!this.supported.ui) { + // 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; } - if (utils.is.element(this.elements.captions)) { - var content = utils.createElement('span'); + // eslint-disable-next-line + target.value = value; - // Empty the container - utils.emptyElement(this.elements.captions); + // Webkit range fill + controls.updateRangeFill.call(this, target); + }, - // 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 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; - // 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() { - // 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); - } - } -}; - -// ========================================================================== - -var ui = { - addStyleHook: function addStyleHook() { - utils.toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); - utils.toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); - }, - - - // Toggle native HTML5 media controls - toggleNativeControls: function toggleNativeControls() { - var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - - if (toggle && this.isHTML5) { - this.media.setAttribute('controls', ''); - } else { - this.media.removeAttribute('controls'); - } - }, - - - // Setup the UI - build: function build() { - var _this = this; - - // Re-attach media element listeners - // TODO: Use event bubbling? - this.listeners.media(); - - // Don't setup interface if no support - if (!this.supported.ui) { - this.debug.warn('Basic support only for ' + this.provider + ' ' + this.type); - - // Restore native controls - ui.toggleNativeControls.call(this, true); - - // Bail - return; - } - - // Inject custom controls if not present - if (!utils.is.element(this.elements.controls)) { - // Inject custom controls - controls.inject.call(this); - - // Re-attach control listeners - this.listeners.controls(); - } - - // Remove native controls - ui.toggleNativeControls.call(this); - - // Captions - captions.setup.call(this); - - // Reset volume - this.volume = null; - - // Reset mute state - this.muted = null; - - // Reset speed - this.speed = null; - - // Reset loop state - this.loop = null; - - // Reset quality setting - this.quality = null; - - // Reset volume display - ui.updateVolume.call(this); - - // Reset time display - ui.timeUpdate.call(this); - - // Update the UI - ui.checkPlaying.call(this); - - // 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 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; + } } }, @@ -2777,159 +2061,6 @@ var ui = { // ========================================================================== -var html5 = { - getSources: function getSources() { - if (!this.isHTML5) { - return null; - } - - return this.media.querySelectorAll('source'); - }, - - - // Get quality levels - getQualityOptions: function getQualityOptions() { - if (!this.isHTML5) { - return null; - } - - // Get sources - var sources = html5.getSources.call(this); - - if (utils.is.empty(sources)) { - return null; - } - - // Get with size attribute - var sizes = Array.from(sources).filter(function (source) { - return !utils.is.empty(source.getAttribute('size')); - }); - - // If none, bail - if (utils.is.empty(sizes)) { - return null; - } - - // Reduce to unique list - return utils.dedupe(sizes.map(function (source) { - return Number(source.getAttribute('size')); - })); - }, - extend: function extend() { - if (!this.isHTML5) { - return; - } - - var player = this; - - // Quality - Object.defineProperty(player.media, 'quality', { - get: function get() { - // Get sources - var sources = html5.getSources.call(player); - - if (utils.is.empty(sources)) { - return null; - } - - var matches = Array.from(sources).filter(function (source) { - return source.getAttribute('src') === player.source; - }); - - if (utils.is.empty(matches)) { - return null; - } - - return Number(matches[0].getAttribute('size')); - }, - set: function set(input) { - // Get sources - var sources = html5.getSources.call(player); - - if (utils.is.empty(sources)) { - return; - } - - // Get matches for requested size - var matches = Array.from(sources).filter(function (source) { - return Number(source.getAttribute('size')) === input; - }); - - // No matches for requested size - if (utils.is.empty(matches)) { - return; - } - - // Get supported sources - var supported = matches.filter(function (source) { - return support.mime.call(player, source.getAttribute('type')); - }); - - // No supported sources - if (utils.is.empty(supported)) { - return; - } - - // Trigger change event - utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { - quality: input - }); - - // Get current state - var currentTime = player.currentTime, - playing = player.playing; - - // Set new source - - player.media.src = supported[0].getAttribute('src'); - - // Load new source - player.media.load(); - - // Resume playing - if (playing) { - player.play(); - } - - // Restore time - player.currentTime = currentTime; - - // Trigger change event - utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { - quality: input - }); - } - }); - }, - - - // Cancel current network requests - // See https://github.com/sampotts/plyr/issues/174 - cancelRequests: function cancelRequests() { - if (!this.isHTML5) { - return; - } - - // Remove child sources - utils.removeElement(html5.getSources()); - - // Set blank video src attribute - // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error - // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection - this.media.setAttribute('src', this.config.blankVideo); - - // Load the new empty source - // This will cancel existing requests - // See https://github.com/sampotts/plyr/issues/174 - this.media.load(); - - // Debugging - this.debug.log('Cancelled network requests'); - } -}; - -// ========================================================================== - // Sniff out the browser var browser$1 = utils.getBrowser(); @@ -2959,18 +2090,76 @@ var controls = { // Get icon URL getIconUrl: function getIconUrl() { + var url = new URL(this.config.iconUrl, window.location); + var cors = url.host !== window.location.host || browser$1.isIE && !window.svg4everybody; + return { url: this.config.iconUrl, - absolute: this.config.iconUrl.indexOf('http') === 0 || browser$1.isIE && !window.svg4everybody + cors: cors }; }, + // Find the UI controls and store references in custom controls + // TODO: Allow settings menus with custom controls + findElements: function findElements() { + try { + this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper); + + // Buttons + this.elements.buttons = { + play: utils.getElements.call(this, this.config.selectors.buttons.play), + pause: utils.getElement.call(this, this.config.selectors.buttons.pause), + restart: utils.getElement.call(this, this.config.selectors.buttons.restart), + rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind), + fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward), + mute: utils.getElement.call(this, this.config.selectors.buttons.mute), + pip: utils.getElement.call(this, this.config.selectors.buttons.pip), + airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay), + settings: utils.getElement.call(this, this.config.selectors.buttons.settings), + captions: utils.getElement.call(this, this.config.selectors.buttons.captions), + fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen) + }; + + // Progress + this.elements.progress = utils.getElement.call(this, this.config.selectors.progress); + + // Inputs + this.elements.inputs = { + seek: utils.getElement.call(this, this.config.selectors.inputs.seek), + volume: utils.getElement.call(this, this.config.selectors.inputs.volume) + }; + + // Display + this.elements.display = { + buffer: utils.getElement.call(this, this.config.selectors.display.buffer), + currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime), + duration: utils.getElement.call(this, this.config.selectors.display.duration) + }; + + // Seek tooltip + if (utils.is.element(this.elements.progress)) { + this.elements.display.seekTooltip = this.elements.progress.querySelector('.' + this.config.classNames.tooltip); + } + + return true; + } catch (error) { + // Log it + this.debug.warn('It looks like there is a problem with your custom controls HTML', error); + + // Restore native video controls + this.toggleNativeControls(true); + + return false; + } + }, + + // Create icon createIcon: function createIcon(type, attributes) { var namespace = 'http://www.w3.org/2000/svg'; var iconUrl = controls.getIconUrl.call(this); - var iconPath = (!iconUrl.absolute ? iconUrl.url : '') + '#' + this.config.iconPrefix; + var iconPath = (!iconUrl.cors ? iconUrl.url : '') + '#' + this.config.iconPrefix; // Create var icon = document.createElementNS(namespace, 'svg'); @@ -3742,12 +2931,11 @@ var controls = { // Toggle Menu - showTab: function showTab(event) { + showTab: function showTab() { + var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; var menu = this.elements.settings.menu; - var tab = event.target; - var show = tab.getAttribute('aria-expanded') === 'false'; - var pane = document.getElementById(tab.getAttribute('aria-controls')); + var pane = document.getElementById(target); // Nothing to show, bail if (!utils.is.element(pane)) { @@ -3807,8 +2995,12 @@ var controls = { current.setAttribute('tabindex', -1); // Set attributes on target - utils.toggleHidden(pane, !show); - tab.setAttribute('aria-expanded', show); + utils.toggleHidden(pane, false); + + var tabs = utils.getElements.call(this, '[aria-controls="' + target + '"]'); + Array.from(tabs).forEach(function (tab) { + tab.setAttribute('aria-expanded', true); + }); pane.removeAttribute('tabindex'); // Focus the first item @@ -3977,212 +3169,1082 @@ var controls = { 'aria-expanded': false }), i18n.get(type, _this5.config)); - var value = utils.createElement('span', { - class: _this5.config.classNames.menu.value - }); + var value = utils.createElement('span', { + class: _this5.config.classNames.menu.value + }); + + // Speed contains HTML entities + value.innerHTML = data[type]; + + button.appendChild(value); + tab.appendChild(button); + tabs.appendChild(tab); + + _this5.elements.settings.tabs[type] = tab; + }); + + home.appendChild(tabs); + inner.appendChild(home); + + // Build the panes + this.config.settings.forEach(function (type) { + var pane = utils.createElement('div', { + id: 'plyr-settings-' + data.id + '-' + type, + hidden: '', + 'aria-labelled-by': 'plyr-settings-' + data.id + '-' + type + '-tab', + role: 'tabpanel', + tabindex: -1 + }); + + var back = utils.createElement('button', { + type: 'button', + class: _this5.config.classNames.control + ' ' + _this5.config.classNames.control + '--back', + 'aria-haspopup': true, + 'aria-controls': 'plyr-settings-' + data.id + '-home', + 'aria-expanded': false + }, i18n.get(type, _this5.config)); + + pane.appendChild(back); + + var options = utils.createElement('ul'); + + pane.appendChild(options); + inner.appendChild(pane); + + _this5.elements.settings.panes[type] = pane; + }); + + form.appendChild(inner); + menu.appendChild(form); + container.appendChild(menu); + + this.elements.settings.form = form; + this.elements.settings.menu = menu; + } + + // Picture in picture button + if (this.config.controls.includes('pip') && support.pip) { + container.appendChild(controls.createButton.call(this, 'pip')); + } + + // Airplay button + if (this.config.controls.includes('airplay') && support.airplay) { + container.appendChild(controls.createButton.call(this, 'airplay')); + } + + // Toggle fullscreen button + if (this.config.controls.includes('fullscreen')) { + container.appendChild(controls.createButton.call(this, 'fullscreen')); + } + + // Larger overlaid play button + if (this.config.controls.includes('play-large')) { + this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); + } + + this.elements.controls = container; + + if (this.isHTML5) { + controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); + } + + controls.setSpeedMenu.call(this); + + return container; + }, + + + // Insert controls + inject: function inject() { + var _this6 = this; + + // Sprite + if (this.config.loadSprite) { + var icon = controls.getIconUrl.call(this); + + // Only load external sprite using AJAX + if (icon.cors) { + utils.loadSprite(icon.url, 'sprite-plyr'); + } + } + + // Create a unique ID + this.id = Math.floor(Math.random() * 10000); + + // Null by default + var container = null; + this.elements.controls = null; + + // Set template properties + var props = { + id: this.id, + seektime: this.config.seekTime, + title: this.config.title + }; + var update = true; + + if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) { + // String or HTMLElement passed as the option + container = this.config.controls; + } else if (utils.is.function(this.config.controls)) { + // A custom function to build controls + // The function can return a HTMLElement or String + container = this.config.controls.call(this, props); + } else { + // Create controls + container = controls.create.call(this, { + id: this.id, + seektime: this.config.seekTime, + speed: this.speed, + quality: this.quality, + captions: captions.getLabel.call(this) + // TODO: Looping + // loop: 'None', + }); + update = false; + } + + // Replace props with their value + var replace = function replace(input) { + var result = input; + + Object.entries(props).forEach(function (_ref) { + var _ref2 = slicedToArray(_ref, 2), + key = _ref2[0], + value = _ref2[1]; + + result = utils.replaceAll(result, '{' + key + '}', value); + }); + + return result; + }; + + // Update markup + if (update) { + if (utils.is.string(this.config.controls)) { + container = replace(container); + } else if (utils.is.element(container)) { + container.innerHTML = replace(container.innerHTML); + } + } + + // Controls container + var target = void 0; + + // Inject to custom location + if (utils.is.string(this.config.selectors.controls.container)) { + target = document.querySelector(this.config.selectors.controls.container); + } + + // Inject into the container by default + if (!utils.is.element(target)) { + target = this.elements.container; + } + + // Inject controls HTML + if (utils.is.element(container)) { + target.appendChild(container); + } else if (container) { + target.insertAdjacentHTML('beforeend', container); + } + + // Find the elements if need be + if (!utils.is.element(this.elements.controls)) { + controls.findElements.call(this); + } + + // Edge sometimes doesn't finish the paint so force a redraw + if (window.navigator.userAgent.includes('Edge')) { + utils.repaint(target); + } + + // Setup tooltips + if (this.config.tooltips.controls) { + var labels = utils.getElements.call(this, [this.config.selectors.controls.wrapper, ' ', this.config.selectors.labels, ' .', this.config.classNames.hidden].join('')); + + Array.from(labels).forEach(function (label) { + utils.toggleClass(label, _this6.config.classNames.hidden, false); + utils.toggleClass(label, _this6.config.classNames.tooltip, true); + label.setAttribute('role', 'tooltip'); + }); + } + } +}; + +// ========================================================================== + +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; + + var tracks = captions.getTracks.call(this); + + if (!tracks.length) { + return null; + } + + // Get track based on current language + var track = tracks.find(function (track) { + return track.language.toLowerCase() === _this2.language; + }); + + // Get the with default attribute + if (!track) { + track = utils.getElement.call(this, 'track[default]'); + } + + // Get the first track + if (!track) { + var _tracks = slicedToArray(tracks, 1); + + track = _tracks[0]; + } + + return track; + }, + + + // Get UI label for track + getLabel: function getLabel(track) { + var currentTrack = track; + + if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { + currentTrack = captions.getCurrentTrack.call(this); + } + + if (utils.is.track(currentTrack)) { + if (!utils.is.empty(currentTrack.label)) { + return currentTrack.label; + } + + if (!utils.is.empty(currentTrack.language)) { + return track.language.toUpperCase(); + } + + return i18n.get('enabled', this.config); + } + + return i18n.get('disabled', this.config); + }, + + + // 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() { + // 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); + } + } +}; + +// ========================================================================== +// 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; +}(); + +// ========================================================================== +// Plyr default config +// ========================================================================== + +var defaults$1 = { + // 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, + + // Reset to start when playback ended + resetOnEnd: false, + + // Disable the standard context menu + disableContextMenu: true, + + // Sprite (for icons) + loadSprite: true, + iconPrefix: 'plyr', + iconUrl: 'https://cdn.plyr.io/3.3.0/plyr.svg', + + // Blank video (used to prevent errors on source change) + blankVideo: 'https://cdn.plyr.io/static/blank.mp4', + + // Quality default + quality: { + default: 576, + options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240, '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: (navigator.language || navigator.userLanguage).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', + normal: 'Normal', + quality: 'Quality', + loop: 'Loop', + start: 'Start', + end: 'End', + all: 'All', + reset: 'Reset', + disabled: 'Disabled', + enabled: 'Enabled', + advertisement: 'Ad' + }, + + // URLs + urls: { + vimeo: { + sdk: 'https://player.vimeo.com/api/player.js', + iframe: 'https://player.vimeo.com/video/{0}?{1}', + api: 'https://vimeo.com/api/v2/video/{0}.json' + }, + youtube: { + sdk: 'https://www.youtube.com/iframe_api', + api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', + poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg' + }, + googleIMA: { + sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js' + } + }, + + // Custom control listeners + listeners: { + seek: null, + play: null, + pause: null, + restart: null, + rewind: null, + fastForward: null, + mute: null, + volume: null, + captions: null, + fullscreen: null, + pip: null, + airplay: null, + speed: null, + quality: null, + loop: null, + language: null + }, + + // Events to watch and bubble + events: [ + // Events to watch on HTML5 media elements and bubble + // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events + 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', + + // Custom events + 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', + + // YouTube + 'statechange', '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', + poster: 'plyr__poster', + ads: 'plyr__ads', + control: 'plyr__control', + type: 'plyr--{0}', + provider: 'plyr--{0}', + playing: 'plyr--playing', + paused: 'plyr--paused', + stopped: 'plyr--stopped', + 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 browser$2 = 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$2.isIos) { + utils.trapFocus.call(this.player, this.target, this.active); + } +} - // Speed contains HTML entities - value.innerHTML = data[type]; +function toggleFallback() { + var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; - button.appendChild(value); - tab.appendChild(button); - tabs.appendChild(tab); + // 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); + } - _this5.elements.settings.tabs[type] = tab; - }); + // Toggle scroll + document.body.style.overflow = toggle ? 'hidden' : ''; - home.appendChild(tabs); - inner.appendChild(home); + // Toggle class hook + utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); - // Build the panes - this.config.settings.forEach(function (type) { - var pane = utils.createElement('div', { - id: 'plyr-settings-' + data.id + '-' + type, - hidden: '', - 'aria-labelled-by': 'plyr-settings-' + data.id + '-' + type + '-tab', - role: 'tabpanel', - tabindex: -1 - }); + // Toggle button and fire events + onChange.call(this); +} - var back = utils.createElement('button', { - type: 'button', - class: _this5.config.classNames.control + ' ' + _this5.config.classNames.control + '--back', - 'aria-haspopup': true, - 'aria-controls': 'plyr-settings-' + data.id + '-home', - 'aria-expanded': false - }, i18n.get(type, _this5.config)); +var Fullscreen = function () { + function Fullscreen(player) { + var _this = this; - pane.appendChild(back); + classCallCheck(this, Fullscreen); - var options = utils.createElement('ul'); + // Keep reference to parent + this.player = player; - pane.appendChild(options); - inner.appendChild(pane); + // Get prefix + this.prefix = Fullscreen.prefix; + this.property = Fullscreen.property; - _this5.elements.settings.panes[type] = pane; - }); + // Scroll position + this.scrollPosition = { x: 0, y: 0 }; - form.appendChild(inner); - menu.appendChild(form); - container.appendChild(menu); + // 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); + }); - this.elements.settings.form = form; - this.elements.settings.menu = menu; - } + // Fullscreen toggle on double click + utils.on(this.player.elements.container, 'dblclick', function (event) { + // Ignore double click in controls + if (utils.is.element(_this.player.elements.controls) && _this.player.elements.controls.contains(event.target)) { + return; + } - // Picture in picture button - if (this.config.controls.includes('pip') && support.pip) { - container.appendChild(controls.createButton.call(this, 'pip')); - } + _this.toggle(); + }); - // Airplay button - if (this.config.controls.includes('airplay') && support.airplay) { - container.appendChild(controls.createButton.call(this, 'airplay')); - } + // Update the UI + this.update(); + } - // Toggle fullscreen button - if (this.config.controls.includes('fullscreen')) { - container.appendChild(controls.createButton.call(this, 'fullscreen')); - } + // Determine if native supported - // Larger overlaid play button - if (this.config.controls.includes('play-large')) { - this.elements.container.appendChild(controls.createButton.call(this, 'play-large')); - } - this.elements.controls = container; + createClass(Fullscreen, [{ + key: 'update', - if (this.isHTML5) { - controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); + + // 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); } - controls.setSpeedMenu.call(this); + // Make an element fullscreen - return container; - }, + }, { + key: 'enter', + value: function enter() { + if (!this.enabled) { + return; + } + // iOS native fullscreen doesn't need the request step + if (browser$2.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.property](); + } + } - // Insert controls - inject: function inject() { - var _this6 = this; + // Bail from fullscreen - // Sprite - if (this.config.loadSprite) { - var icon = controls.getIconUrl.call(this); + }, { + key: 'exit', + value: function exit() { + if (!this.enabled) { + return; + } - // Only load external sprite using AJAX - if (icon.absolute) { - utils.loadSprite(icon.url, 'sprite-plyr'); + // iOS native fullscreen + if (browser$2.isIos && this.player.config.fullscreen.iosNative) { + this.target.webkitExitFullscreen(); + this.player.play(); + } else if (!Fullscreen.native) { + toggleFallback.call(this, false); + } else if (!this.prefix) { + (document.cancelFullScreen || document.exitFullscreen).call(document); + } else if (!utils.is.empty(this.prefix)) { + var action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; + document['' + this.prefix + action + this.property](); } } - // Create a unique ID - this.id = Math.floor(Math.random() * 10000); + // Toggle state - // Null by default - var container = null; - this.elements.controls = null; + }, { + key: 'toggle', + value: function toggle() { + if (!this.active) { + this.enter(); + } else { + this.exit(); + } + } + }, { + key: 'enabled', - // Set template properties - var props = { - id: this.id, - seektime: this.config.seekTime, - title: this.config.title - }; - var update = true; - if (utils.is.string(this.config.controls) || utils.is.element(this.config.controls)) { - // String or HTMLElement passed as the option - container = this.config.controls; - } else if (utils.is.function(this.config.controls)) { - // A custom function to build controls - // The function can return a HTMLElement or String - container = this.config.controls.call(this, props); - } else { - // Create controls - container = controls.create.call(this, { - id: this.id, - seektime: this.config.seekTime, - speed: this.speed, - quality: this.quality, - captions: captions.getLabel.call(this) - // TODO: Looping - // loop: 'None', - }); - update = false; + // Determine if fullscreen is enabled + get: function get$$1() { + return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo; } - // Replace props with their value - var replace = function replace(input) { - var result = input; + // Get active state - Object.entries(props).forEach(function (_ref) { - var _ref2 = slicedToArray(_ref, 2), - key = _ref2[0], - value = _ref2[1]; + }, { + key: 'active', + get: function get$$1() { + if (!this.enabled) { + return false; + } - result = utils.replaceAll(result, '{' + key + '}', value); - }); + // Fallback using classname + if (!Fullscreen.native) { + return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); + } - return result; - }; + var element = !this.prefix ? document.fullscreenElement : document['' + this.prefix + this.property + 'Element']; - // Update markup - if (update) { - if (utils.is.string(this.config.controls)) { - container = replace(container); - } else if (utils.is.element(container)) { - container.innerHTML = replace(container.innerHTML); - } + return element === this.target; } - // Controls container - var target = void 0; + // Get target element - // Inject to custom location - if (utils.is.string(this.config.selectors.controls.container)) { - target = document.querySelector(this.config.selectors.controls.container); + }, { + key: 'target', + get: function get$$1() { + return browser$2.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container; } - - // Inject into the container by default - if (!utils.is.element(target)) { - target = this.elements.container; + }], [{ + key: 'native', + get: function get$$1() { + return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); } - // Inject controls HTML - if (utils.is.element(container)) { - target.appendChild(container); - } else if (container) { - target.insertAdjacentHTML('beforeend', container); - } + // Get the prefix for handlers - // Find the elements if need be - if (!utils.is.element(this.elements.controls)) { - utils.findElements.call(this); - } + }, { + key: 'prefix', + get: function get$$1() { + // No prefix + if (utils.is.function(document.exitFullscreen)) { + return ''; + } - // Edge sometimes doesn't finish the paint so force a redraw - if (window.navigator.userAgent.includes('Edge')) { - utils.repaint(target); - } + // Check for fullscreen support by vendor prefix + var value = ''; + var prefixes = ['webkit', 'moz', 'ms']; - // Setup tooltips - if (this.config.tooltips.controls) { - var labels = utils.getElements.call(this, [this.config.selectors.controls.wrapper, ' ', this.config.selectors.labels, ' .', this.config.classNames.hidden].join('')); + prefixes.some(function (pre) { + if (utils.is.function(document[pre + 'ExitFullscreen']) || utils.is.function(document[pre + 'CancelFullScreen'])) { + value = pre; + return true; + } - Array.from(labels).forEach(function (label) { - utils.toggleClass(label, _this6.config.classNames.hidden, false); - utils.toggleClass(label, _this6.config.classNames.tooltip, true); - label.setAttribute('role', 'tooltip'); + return false; }); + + return value; } - } -}; + }, { + key: 'property', + get: function get$$1() { + return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; + } + }]); + return Fullscreen; +}(); // ========================================================================== // Sniff out the browser -var browser$2 = utils.getBrowser(); +var browser$3 = utils.getBrowser(); var Listeners = function () { function Listeners(player) { @@ -4449,12 +4511,9 @@ var Listeners = function () { // Handle the media finishing utils.on(this.player.media, 'ended', function () { // Show poster on end - if (_this3.player.isHTML5 && _this3.player.isVideo && _this3.player.config.showPosterOnEnd) { + if (_this3.player.isHTML5 && _this3.player.isVideo && _this3.player.config.resetOnEnd) { // Restart _this3.player.restart(); - - // Re-load media - _this3.player.media.load(); } }); @@ -4469,7 +4528,7 @@ var Listeners = function () { }); // Handle play/pause - utils.on(this.player.media, 'playing play pause ended emptied', function (event) { + utils.on(this.player.media, 'playing play pause ended emptied timeupdate', function (event) { return ui.checkPlaying.call(_this3.player, event); }); @@ -4601,7 +4660,7 @@ var Listeners = function () { var _this4 = this; // IE doesn't support input event, so we fallback to change - var inputEvent = browser$2.isIE ? 'change' : 'input'; + var inputEvent = browser$3.isIE ? 'change' : 'input'; // Run default and custom handlers var proxy = function proxy(event, defaultHandler, customHandlerKey) { @@ -4674,21 +4733,31 @@ var Listeners = function () { on(this.player.elements.settings.form, 'click', function (event) { event.stopPropagation(); + // Go back to home tab on click + var showHomeTab = function showHomeTab() { + var id = 'plyr-settings-' + _this4.player.id + '-home'; + controls.showTab.call(_this4.player, id); + }; + // Settings menu items - use event delegation as items are added/removed if (utils.matches(event.target, _this4.player.config.selectors.inputs.language)) { proxy(event, function () { _this4.player.language = event.target.value; + showHomeTab(); }, 'language'); } else if (utils.matches(event.target, _this4.player.config.selectors.inputs.quality)) { proxy(event, function () { _this4.player.quality = event.target.value; + showHomeTab(); }, 'quality'); } else if (utils.matches(event.target, _this4.player.config.selectors.inputs.speed)) { proxy(event, function () { _this4.player.speed = parseFloat(event.target.value); + showHomeTab(); }, 'speed'); } else { - controls.showTab.call(_this4.player, event); + var tab = event.target; + controls.showTab.call(_this4.player, tab.getAttribute('aria-controls')); } }); @@ -4717,7 +4786,7 @@ var Listeners = function () { }, 'volume'); // Polyfill for lower fill in
to hide the standard controls and UI + setAspectRatio: function setAspectRatio(input) { + var ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); + var padding = 100 / ratio[0] * ratio[1]; + this.elements.wrapper.style.paddingBottom = padding + '%'; - // We assume the adContainer is the video container of the plyr element - // that will house the ads - this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); + if (this.supported.ui) { + var height = 240; + var offset = (height - padding) / (height / 50); - // Request video ads to be pre-loaded - this.requestAds(); + this.media.style.transform = 'translateY(-' + offset + '%)'; } + }, - /** - * Request advertisements - */ - }, { - key: 'requestAds', - value: function requestAds() { - var _this4 = this; + // API Ready + ready: function ready() { + var _this2 = this; - var container = this.player.elements.container; + var player = this; + // Get Vimeo params for the iframe + var options = { + loop: player.config.loop.active, + autoplay: player.autoplay, + byline: false, + portrait: false, + title: false, + speed: true, + transparent: 0, + gesture: 'media', + playsinline: !this.config.fullscreen.iosNative + }; + var params = utils.buildUrlParams(options); - try { - // Create ads loader - this.loader = new google.ima.AdsLoader(this.elements.displayContainer); + // Get the source URL or ID + var source = player.media.getAttribute('src'); - // Listen and respond to ads loaded and error events - this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) { - return _this4.onAdsManagerLoaded(event); - }, false); - this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) { - return _this4.onAdError(error); - }, false); + // Get from
if needed + if (utils.is.empty(source)) { + source = player.media.getAttribute(player.config.attributes.embed.id); + } - // Request video ads - var request = new google.ima.AdsRequest(); - request.adTagUrl = this.tagUrl; + var id = utils.parseVimeoId(source); - // 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; + // Build an iframe + var iframe = utils.createElement('iframe'); + var src = utils.format(player.config.urls.vimeo.iframe, id, params); + iframe.setAttribute('src', src); + iframe.setAttribute('allowfullscreen', ''); + iframe.setAttribute('allowtransparency', ''); + iframe.setAttribute('allow', 'autoplay'); - // We only overlay ads as we only support video. - request.forceNonLinearFullSlot = false; + // Inject the package + var wrapper = utils.createElement('div'); + wrapper.appendChild(iframe); + player.media = utils.replaceElement(wrapper, player.media); - this.loader.requestAds(request); - } catch (e) { - this.onAdError(e); + // Get poster image + utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(function (response) { + if (utils.is.empty(response)) { + return; } - } - /** - * Update the ad countdown - * @param {boolean} start - */ + // Get the URL for thumbnail + var url = new URL(response[0].thumbnail_large); - }, { - key: 'pollCountdown', - value: function pollCountdown() { - var _this5 = this; + // Get original image + url.pathname = url.pathname.split('_')[0] + '.jpg'; - var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + // Set attribute + player.media.setAttribute('poster', url.href); - if (!start) { - clearInterval(this.countdownTimer); - this.elements.container.removeAttribute('data-badge-text'); - return; - } + // Update + ui.setPoster.call(player); + }); - var update = function update() { - var time = utils.formatTime(Math.max(_this5.manager.getRemainingTime(), 0)); - var label = i18n.get('advertisement', _this5.player.config) + ' - ' + time; - _this5.elements.container.setAttribute('data-badge-text', label); - }; + // Setup instance + // https://github.com/vimeo/player.js + player.embed = new window.Vimeo.Player(iframe); - this.countdownTimer = setInterval(update, 100); + player.media.paused = true; + player.media.currentTime = 0; + + // Disable native text track rendering + if (player.supported.ui) { + player.embed.disableTextTrack(); } - /** - * This method is called whenever the ads are ready inside the AdDisplayContainer - * @param {Event} adsManagerLoadedEvent - */ + // Create a faux HTML5 API using the Vimeo API + player.media.play = function () { + player.embed.play().then(function () { + player.media.paused = false; + }); + }; - }, { - key: 'onAdsManagerLoaded', - value: function onAdsManagerLoaded(event) { - var _this6 = this; + player.media.pause = function () { + player.embed.pause().then(function () { + player.media.paused = true; + }); + }; - // Get the ads manager - var settings = new google.ima.AdsRenderingSettings(); + player.media.stop = function () { + player.pause(); + player.currentTime = 0; + }; - // Tell the SDK to save and restore content video state on our behalf - settings.restoreCustomPlaybackStateOnAdBreakComplete = true; - settings.enablePreloading = true; + // Seeking + var currentTime = player.media.currentTime; - // The SDK is polling currentTime on the contentPlayback. And needs a duration - // so it can determine when to start the mid- and post-roll - this.manager = event.getAdsManager(this.player, settings); + Object.defineProperty(player.media, 'currentTime', { + get: function get() { + return currentTime; + }, + set: function set(time) { + // Get current paused state + // Vimeo will automatically play on seek + var paused = player.media.paused; + + // Set seeking flag + + player.media.seeking = true; - // Get the cue points for any mid-rolls by filtering out the pre- and post-roll - this.cuePoints = this.manager.getCuePoints(); + // Trigger seeking + utils.dispatchEvent.call(player, player.media, 'seeking'); - // Add advertisement cue's within the time line if available - if (!utils.is.empty(this.cuePoints)) { - this.cuePoints.forEach(function (cuePoint) { - if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < _this6.player.duration) { - var seekElement = _this6.player.elements.progress; + // Seek after events + player.embed.setCurrentTime(time).catch(function () { + // Do nothing + }); - if (utils.is.element(seekElement)) { - var cuePercentage = 100 / _this6.player.duration * cuePoint; - var cue = utils.createElement('span', { - class: _this6.player.config.classNames.cues - }); + // Restore pause state + if (paused) { + player.pause(); + } + } + }); - cue.style.left = cuePercentage.toString() + '%'; - seekElement.appendChild(cue); - } + // Playback speed + var speed = player.config.speed.selected; + Object.defineProperty(player.media, 'playbackRate', { + get: function get() { + return speed; + }, + set: function set(input) { + player.embed.setPlaybackRate(input).then(function () { + speed = input; + utils.dispatchEvent.call(player, player.media, 'ratechange'); + }).catch(function (error) { + // Hide menu item (and menu if empty) + if (error.name === 'Error') { + controls.setSpeedMenu.call(player, []); } }); } + }); - // 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 _this6.onAdError(error); - }); + // Volume + var volume = player.config.volume; - // Advertisement regular events - Object.keys(google.ima.AdEvent.Type).forEach(function (type) { - _this6.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) { - return _this6.onAdEvent(event); + Object.defineProperty(player.media, 'volume', { + get: function get() { + return volume; + }, + set: function set(input) { + player.embed.setVolume(input).then(function () { + volume = input; + utils.dispatchEvent.call(player, player.media, 'volumechange'); }); - }); + } + }); - // Resolve our adsManager - this.trigger('loaded'); - } + // Muted + var muted = player.config.muted; - /** - * 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 - */ + Object.defineProperty(player.media, 'muted', { + get: function get() { + return muted; + }, + set: function set(input) { + var toggle = utils.is.boolean(input) ? input : false; - }, { - key: 'onAdEvent', - value: function onAdEvent(event) { - var _this7 = this; + player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () { + muted = toggle; + utils.dispatchEvent.call(player, player.media, 'volumechange'); + }); + } + }); - var container = this.player.elements.container; + // Loop + var loop = player.config.loop; - // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED) - // don't have ad object associated + Object.defineProperty(player.media, 'loop', { + get: function get() { + return loop; + }, + set: function set(input) { + var toggle = utils.is.boolean(input) ? input : player.config.loop.active; - var ad = event.getAd(); + player.embed.setLoop(toggle).then(function () { + loop = toggle; + }); + } + }); - // Proxy event - var dispatchEvent = function dispatchEvent(type) { - var event = 'ads' + type.replace(/_/g, '').toLowerCase(); - utils.dispatchEvent.call(_this7.player, _this7.player.media, event); - }; + // Source + var currentSrc = void 0; + player.embed.getVideoUrl().then(function (value) { + currentSrc = value; + }).catch(function (error) { + _this2.debug.warn(error); + }); - switch (event.type) { - case google.ima.AdEvent.Type.LOADED: - // This is the first event sent for an ad - it is possible to determine whether the - // ad is a video ad or an overlay - this.trigger('loaded'); + Object.defineProperty(player.media, 'currentSrc', { + get: function get() { + return currentSrc; + } + }); - // Bubble event - dispatchEvent(event.type); + // Ended + Object.defineProperty(player.media, 'ended', { + get: function get() { + return player.currentTime === player.duration; + } + }); - // Start countdown - this.pollCountdown(true); + // Set aspect ratio based on video size + Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) { + var ratio = utils.getAspectRatio(dimensions[0], dimensions[1]); + vimeo.setAspectRatio.call(_this2, ratio); + }); - if (!ad.isLinear()) { - // Position AdDisplayContainer correctly for overlay - ad.width = container.offsetWidth; - ad.height = container.offsetHeight; - } + // Set autopause + player.embed.setAutopause(player.config.autopause).then(function (state) { + player.config.autopause = state; + }); - // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex()); - // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset()); - break; + // Get title + player.embed.getVideoTitle().then(function (title) { + player.config.title = title; + ui.setTitle.call(_this2); + }); - 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 + // Get current time + player.embed.getCurrentTime().then(function (value) { + currentTime = value; + utils.dispatchEvent.call(player, player.media, 'timeupdate'); + }); - // Fire event - dispatchEvent(event.type); + // Get duration + player.embed.getDuration().then(function (value) { + player.media.duration = value; + utils.dispatchEvent.call(player, player.media, 'durationchange'); + }); - // 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', }, ], - // }; + // Get captions + player.embed.getTextTracks().then(function (tracks) { + player.media.textTracks = tracks; + captions.setup.call(player); + }); - // 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 + player.embed.on('cuechange', function (data) { + var cue = null; - this.loadAds(); - break; + if (data.cues.length) { + cue = utils.stripHTML(data.cues[0].text); + } - 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 + captions.setText.call(player, cue); + }); - dispatchEvent(event.type); + player.embed.on('loaded', function () { + if (utils.is.element(player.embed.element) && player.supported.ui) { + var frame = player.embed.element; - this.pauseContent(); + // Fix keyboard focus issues + // https://github.com/sampotts/plyr/issues/317 + frame.setAttribute('tabindex', -1); + } + }); - break; + player.embed.on('play', function () { + // Only fire play if paused before + if (player.media.paused) { + utils.dispatchEvent.call(player, player.media, 'play'); + } + player.media.paused = false; + utils.dispatchEvent.call(player, player.media, 'playing'); + }); - 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 + player.embed.on('pause', function () { + player.media.paused = true; + utils.dispatchEvent.call(player, player.media, 'pause'); + }); - dispatchEvent(event.type); + player.embed.on('timeupdate', function (data) { + player.media.seeking = false; + currentTime = data.seconds; + utils.dispatchEvent.call(player, player.media, 'timeupdate'); + }); - this.pollCountdown(); + player.embed.on('progress', function (data) { + player.media.buffered = data.percent; + utils.dispatchEvent.call(player, player.media, 'progress'); - this.resumeContent(); + // Check all loaded + if (parseInt(data.percent, 10) === 1) { + utils.dispatchEvent.call(player, player.media, 'canplaythrough'); + } - break; + // Get duration as if we do it before load, it gives an incorrect value + // https://github.com/sampotts/plyr/issues/891 + player.embed.getDuration().then(function (value) { + if (value !== player.media.duration) { + player.media.duration = value; + utils.dispatchEvent.call(player, player.media, 'durationchange'); + } + }); + }); - case google.ima.AdEvent.Type.STARTED: - case google.ima.AdEvent.Type.MIDPOINT: - case google.ima.AdEvent.Type.COMPLETE: - case google.ima.AdEvent.Type.IMPRESSION: - case google.ima.AdEvent.Type.CLICK: - dispatchEvent(event.type); - break; + player.embed.on('seeked', function () { + player.media.seeking = false; + utils.dispatchEvent.call(player, player.media, 'seeked'); + utils.dispatchEvent.call(player, player.media, 'play'); + }); - default: - break; - } - } + player.embed.on('ended', function () { + player.media.paused = true; + utils.dispatchEvent.call(player, player.media, 'ended'); + }); - /** - * Any ad error handling comes through here - * @param {Event} event - */ + player.embed.on('error', function (detail) { + player.media.error = detail; + utils.dispatchEvent.call(player, player.media, 'error'); + }); - }, { - key: 'onAdError', - value: function onAdError(event) { - this.cancel(); - this.player.debug.warn('Ads error', event); - } + // Rebuild UI + setTimeout(function () { + return ui.build.call(player); + }, 0); + } +}; - /** - * Setup hooks for Plyr and window events. This ensures - * the mid- and post-roll launch at the correct time. And - * resize the advertisement when the player resizes - */ +// ========================================================================== - }, { - key: 'listeners', - value: function listeners() { - var _this8 = this; +// Standardise YouTube quality unit +function mapQualityUnit(input) { + switch (input) { + case 'hd2160': + return 2160; - var container = this.player.elements.container; + case 2160: + return 'hd2160'; - var time = void 0; + case 'hd1440': + return 1440; - // Add listeners to the required events - this.player.on('ended', function () { - _this8.loader.contentComplete(); - }); + case 1440: + return 'hd1440'; - this.player.on('seeking', function () { - time = _this8.player.currentTime; - return time; - }); + case 'hd1080': + return 1080; - this.player.on('seeked', function () { - var seekedTime = _this8.player.currentTime; + case 1080: + return 'hd1080'; - if (utils.is.empty(_this8.cuePoints)) { - return; - } + case 'hd720': + return 720; - _this8.cuePoints.forEach(function (cuePoint, index) { - if (time < cuePoint && cuePoint < seekedTime) { - _this8.manager.discardAdBreak(); - _this8.cuePoints.splice(index, 1); - } - }); - }); + case 720: + return 'hd720'; - // Listen to the resizing of the window. And resize ad accordingly - // TODO: eventually implement ResizeObserver - window.addEventListener('resize', function () { - if (_this8.manager) { - _this8.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); - } - }); - } + case 'large': + return 480; - /** - * Initialize the adsManager and start playing advertisements - */ + case 480: + return 'large'; - }, { - key: 'play', - value: function play() { - var _this9 = this; + case 'medium': + return 360; - var container = this.player.elements.container; + case 360: + return 'medium'; + case 'small': + return 240; - if (!this.managerPromise) { - this.resumeContent(); - } + case 240: + return 'small'; - // Play the requested advertisement whenever the adsManager is ready - this.managerPromise.then(function () { - // Initialize the container. Must be done via a user action on mobile devices - _this9.elements.displayContainer.initialize(); + default: + return 'default'; + } +} - try { - if (!_this9.initialized) { - // Initialize the ads manager. Ad rules playlist will start at this time - _this9.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); +function mapQualityUnits(levels) { + if (utils.is.empty(levels)) { + return levels; + } - // Call play to start showing the ad. Single video and overlay ads will - // start at this time; the call will be ignored for ad rules - _this9.manager.start(); - } + return utils.dedupe(levels.map(function (level) { + return mapQualityUnit(level); + })); +} - _this9.initialized = true; - } catch (adError) { - // An error may be thrown if there was a problem with the - // VAST response - _this9.onAdError(adError); - } - }).catch(function () {}); - } +var youtube = { + setup: function setup() { + var _this = this; - /** - * Resume our video - */ + // Add embed class for responsive + utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); - }, { - key: 'resumeContent', - value: function resumeContent() { - // Hide the advertisement container - this.elements.container.style.zIndex = ''; + // Set aspect ratio + youtube.setAspectRatio.call(this); - // Ad is stopped - this.playing = false; + // Setup API + if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { + youtube.ready.call(this); + } else { + // Load the API + utils.loadScript(this.config.urls.youtube.sdk).catch(function (error) { + _this.debug.warn('YouTube API failed to load', error); + }); - // Play our video - if (this.player.currentTime < this.player.duration) { - this.player.play(); - } - } + // Setup callback for the API + // YouTube has it's own system of course... + window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; - /** - * Pause our video - */ + // Add to queue + window.onYouTubeReadyCallbacks.push(function () { + youtube.ready.call(_this); + }); - }, { - key: 'pauseContent', - value: function pauseContent() { - // Show the advertisement container - this.elements.container.style.zIndex = 3; + // Set callback to process queue + window.onYouTubeIframeAPIReady = function () { + window.onYouTubeReadyCallbacks.forEach(function (callback) { + callback(); + }); + }; + } + }, - // Ad is playing. - this.playing = true; - // Pause our video. - this.player.pause(); - } + // Get the media title + getTitle: function getTitle(videoId) { + var _this2 = this; - /** - * Destroy the adsManager so we can grab new ads after this. If we don't then we're not - * allowed to call new ads based on google policies, as they interpret this as an accidental - * video requests. https://developers.google.com/interactive- - * media-ads/docs/sdks/android/faq#8 - */ + // Try via undocumented API method first + // This method disappears now and then though... + // https://github.com/sampotts/plyr/issues/709 + if (utils.is.function(this.embed.getVideoData)) { + var _embed$getVideoData = this.embed.getVideoData(), + title = _embed$getVideoData.title; - }, { - key: 'cancel', - value: function cancel() { - // Pause our video - if (this.initialized) { - this.resumeContent(); + if (utils.is.empty(title)) { + this.config.title = title; + ui.setTitle.call(this); + return; } + } - // Tell our instance that we're done for now - this.trigger('error'); + // Or via Google API + var key = this.config.keys.google; + if (utils.is.string(key) && !utils.is.empty(key)) { + var url = utils.format(this.config.urls.youtube.api, videoId, key); - // Re-create our adsManager - this.loadAds(); + utils.fetch(url).then(function (result) { + if (utils.is.object(result)) { + _this2.config.title = result.items[0].snippet.title; + ui.setTitle.call(_this2); + } + }).catch(function () {}); } + }, - /** - * Re-create our adsManager - */ - }, { - key: 'loadAds', - value: function loadAds() { - var _this10 = this; + // Set aspect ratio + setAspectRatio: function setAspectRatio() { + var ratio = this.config.ratio.split(':'); + this.elements.wrapper.style.paddingBottom = 100 / ratio[0] * ratio[1] + '%'; + }, - // Tell our adsManager to go bye bye - this.managerPromise.then(function () { - // Destroy our adsManager - if (_this10.manager) { - _this10.manager.destroy(); - } - // Re-set our adsManager promises - _this10.managerPromise = new Promise(function (resolve) { - _this10.on('loaded', resolve); - _this10.player.debug.log(_this10.manager); - }); + // API ready + ready: function ready() { + var player = this; - // Now request some new advertisements - _this10.requestAds(); - }).catch(function () {}); + // Ignore already setup (race condition) + var currentId = player.media.getAttribute('id'); + if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { + return; } - /** - * Handles callbacks after an ad event was invoked - * @param {string} event - Event type - */ - - }, { - key: 'trigger', - value: function trigger(event) { - var _this11 = this; + // Get the source URL or ID + var source = player.media.getAttribute('src'); - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } + // Get from
if needed + if (utils.is.empty(source)) { + source = player.media.getAttribute(this.config.attributes.embed.id); + } - var handlers = this.events[event]; + // Replace the