aboutsummaryrefslogtreecommitdiffstats
path: root/dist/plyr.js
diff options
context:
space:
mode:
Diffstat (limited to 'dist/plyr.js')
-rw-r--r--dist/plyr.js13512
1 files changed, 7012 insertions, 6500 deletions
diff --git a/dist/plyr.js b/dist/plyr.js
index 3e20b389..031ccb1e 100644
--- a/dist/plyr.js
+++ b/dist/plyr.js
@@ -1,365 +1,71 @@
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define('Plyr', factory) :
- (global.Plyr = factory());
+typeof navigator === "object" && (function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define('Plyr', factory) :
+ (global.Plyr = factory());
}(this, (function () { 'use strict';
-var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
-
-function createCommonjsModule(fn, module) {
- return module = { exports: {} }, fn(module, module.exports), module.exports;
-}
-
-var loadjs_umd = createCommonjsModule(function (module, exports) {
-(function(root, factory) {
- if (typeof undefined === 'function' && undefined.amd) {
- undefined([], factory);
- } else {
- module.exports = factory();
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
}
-}(commonjsGlobal, function() {
-/**
- * Global dependencies.
- * @global {Object} document - DOM
- */
-
-var devnull = function() {},
- bundleIdCache = {},
- bundleResultCache = {},
- bundleCallbackQueue = {};
-
-/**
- * Subscribe to bundle load event.
- * @param {string[]} bundleIds - Bundle ids
- * @param {Function} callbackFn - The callback function
- */
-function subscribe(bundleIds, callbackFn) {
- // listify
- bundleIds = bundleIds.push ? bundleIds : [bundleIds];
-
- var depsNotFound = [],
- i = bundleIds.length,
- numWaiting = i,
- fn,
- bundleId,
- r,
- q;
-
- // define callback function
- fn = function (bundleId, pathsNotFound) {
- if (pathsNotFound.length) depsNotFound.push(bundleId);
-
- numWaiting--;
- if (!numWaiting) callbackFn(depsNotFound);
- };
-
- // register callback
- while (i--) {
- bundleId = bundleIds[i];
-
- // execute callback if in result cache
- r = bundleResultCache[bundleId];
- if (r) {
- fn(bundleId, r);
- continue;
+ function _defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
}
-
- // 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;
+ function _createClass(Constructor, protoProps, staticProps) {
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) _defineProperties(Constructor, staticProps);
+ return Constructor;
}
- 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";
+ function _defineProperty(obj, key, value) {
+ if (key in obj) {
+ Object.defineProperty(obj, key, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: true
+ });
} else {
- bundleIdCache[bundleId] = true;
+ obj[key] = value;
}
- }
-
- // load scripts
- loadFiles(paths, function (pathsNotFound) {
- // execute callbacks
- executeCallbacks(args, pathsNotFound);
-
- // publish bundle load event
- publish(bundleId, pathsNotFound);
- }, args);
-}
-
-
-/**
- * Execute callbacks when dependencies have been satisfied.
- * @param {(string|string[])} deps - List of bundle ids
- * @param {Object} args - success/error arguments
- */
-loadjs.ready = function ready(deps, args) {
- // subscribe to bundle load event
- subscribe(deps, function (depsNotFound) {
- // execute callbacks
- executeCallbacks(args, depsNotFound);
- });
-
- return loadjs;
-};
-
-
-/**
- * Manually satisfy bundle dependencies.
- * @param {string} bundleId - The bundle id
- */
-loadjs.done = function done(bundleId) {
- publish(bundleId, []);
-};
+ return obj;
+ }
-/**
- * Reset loadjs dependencies statuses
- */
-loadjs.reset = function reset() {
- bundleIdCache = {};
- bundleResultCache = {};
- bundleCallbackQueue = {};
-};
-
-
-/**
- * Determine if bundle has already been defined
- * @param String} bundleId - The bundle id
- */
-loadjs.isDefined = function isDefined(bundleId) {
- return bundleId in bundleIdCache;
-};
-
-
-// export
-return loadjs;
-
-}));
-});
-
-// ==========================================================================
-// Plyr supported types and providers
-// ==========================================================================
-
-var providers = {
- html5: 'html5',
- youtube: 'youtube',
- vimeo: 'vimeo'
-};
-
-var types = {
- audio: 'audio',
- video: 'video'
-};
+ function _slicedToArray(arr, i) {
+ return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
+ }
-var classCallCheck = function (instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
+ function _toConsumableArray(arr) {
+ return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
}
-};
-var createClass = function () {
- function defineProperties(target, props) {
- for (var i = 0; i < props.length; i++) {
- var descriptor = props[i];
- descriptor.enumerable = descriptor.enumerable || false;
- descriptor.configurable = true;
- if ("value" in descriptor) descriptor.writable = true;
- Object.defineProperty(target, descriptor.key, descriptor);
+ function _arrayWithoutHoles(arr) {
+ if (Array.isArray(arr)) {
+ for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
+
+ return arr2;
}
}
- return function (Constructor, protoProps, staticProps) {
- if (protoProps) defineProperties(Constructor.prototype, protoProps);
- if (staticProps) defineProperties(Constructor, staticProps);
- return Constructor;
- };
-}();
-
-var defineProperty = function (obj, key, value) {
- if (key in obj) {
- Object.defineProperty(obj, key, {
- value: value,
- enumerable: true,
- configurable: true,
- writable: true
- });
- } else {
- obj[key] = value;
+ function _arrayWithHoles(arr) {
+ if (Array.isArray(arr)) return arr;
}
- return obj;
-};
+ function _iterableToArray(iter) {
+ if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
+ }
-var slicedToArray = function () {
- function sliceIterator(arr, i) {
+ function _iterableToArrayLimit(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
@@ -376,7 +82,7 @@ var slicedToArray = function () {
_e = err;
} finally {
try {
- if (!_n && _i["return"]) _i["return"]();
+ if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
@@ -385,2342 +91,2020 @@ var slicedToArray = function () {
return _arr;
}
- return function (arr, i) {
- if (Array.isArray(arr)) {
- return arr;
- } else if (Symbol.iterator in Object(arr)) {
- return sliceIterator(arr, i);
- } else {
- throw new TypeError("Invalid attempt to destructure non-iterable instance");
- }
- };
-}();
-
-var toConsumableArray = function (arr) {
- if (Array.isArray(arr)) {
- for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
-
- return arr2;
- } else {
- return Array.from(arr);
+ function _nonIterableSpread() {
+ throw new TypeError("Invalid attempt to spread non-iterable instance");
}
-};
-
-// ==========================================================================
-
-var utils = {
- // Check variable types
- is: {
- plyr: function plyr(input) {
- return this.instanceof(input, window.Plyr);
- },
- object: function object(input) {
- return this.getConstructor(input) === Object;
- },
- number: function number(input) {
- return this.getConstructor(input) === Number && !Number.isNaN(input);
- },
- string: function string(input) {
- return this.getConstructor(input) === String;
- },
- boolean: function boolean(input) {
- return this.getConstructor(input) === Boolean;
- },
- function: function _function(input) {
- return this.getConstructor(input) === Function;
- },
- array: function array(input) {
- return !this.nullOrUndefined(input) && Array.isArray(input);
- },
- weakMap: function weakMap(input) {
- return this.instanceof(input, window.WeakMap);
- },
- nodeList: function nodeList(input) {
- return this.instanceof(input, window.NodeList);
- },
- element: function element(input) {
- return this.instanceof(input, window.Element);
- },
- textNode: function textNode(input) {
- return this.getConstructor(input) === Text;
- },
- event: function event(input) {
- return this.instanceof(input, window.Event);
- },
- cue: function cue(input) {
- return this.instanceof(input, window.TextTrackCue) || this.instanceof(input, window.VTTCue);
- },
- track: function track(input) {
- return this.instanceof(input, TextTrack) || !this.nullOrUndefined(input) && this.string(input.kind);
- },
- url: function url(input) {
- return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
- },
- nullOrUndefined: function nullOrUndefined(input) {
- return input === null || typeof input === 'undefined';
- },
- empty: function empty(input) {
- return this.nullOrUndefined(input) || (this.string(input) || this.array(input) || this.nodeList(input)) && !input.length || this.object(input) && !Object.keys(input).length;
- },
- instanceof: function _instanceof$$1(input, constructor) {
- return Boolean(input && constructor && input instanceof constructor);
- },
- getConstructor: function getConstructor(input) {
- return !this.nullOrUndefined(input) ? input.constructor : null;
- }
- },
-
- // Unfortunately, due to mixed support, UA sniffing is required
- getBrowser: function getBrowser() {
- return {
- isIE: /* @cc_on!@ */false || !!document.documentMode,
- isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
- isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
- isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform)
- };
- },
-
-
- // Fetch wrapper
- // Using XHR to avoid issues with older browsers
- fetch: function fetch(url) {
- var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text';
-
- return new Promise(function (resolve, reject) {
- try {
- var request = new XMLHttpRequest();
-
- // Check for CORS support
- if (!('withCredentials' in request)) {
- return;
- }
-
- request.addEventListener('load', function () {
- if (responseType === 'text') {
- try {
- resolve(JSON.parse(request.responseText));
- } catch (e) {
- resolve(request.responseText);
- }
- } else {
- resolve(request.response);
- }
- });
-
- request.addEventListener('error', function () {
- throw new Error(request.statusText);
- });
-
- request.open('GET', url, true);
-
- // Set the required response type
- request.responseType = responseType;
-
- request.send();
- } catch (e) {
- reject(e);
- }
- });
- },
-
-
- // Load an external script
- loadScript: function loadScript(url) {
- return new Promise(function (resolve, reject) {
- loadjs_umd(url, {
- success: resolve,
- error: reject
- });
- });
- },
-
-
- // Load an external SVG sprite
- loadSprite: function loadSprite(url, id) {
- if (!utils.is.string(url)) {
- return;
- }
-
- var prefix = 'cache-';
- var hasId = utils.is.string(id);
- var isCached = false;
-
- var exists = function exists() {
- return document.querySelectorAll('#' + id).length;
- };
-
- function injectSprite(data) {
- // Check again incase of race condition
- if (hasId && exists()) {
- return;
- }
-
- // Inject content
- this.innerHTML = data;
-
- // Inject the SVG to the body
- document.body.insertBefore(this, document.body.childNodes[0]);
- }
-
- // Only load once if ID set
- if (!hasId || !exists()) {
- // Create container
- var container = document.createElement('div');
- utils.toggleHidden(container, true);
-
- if (hasId) {
- container.setAttribute('id', id);
- }
-
- // Check in cache
- if (support.storage) {
- var cached = window.localStorage.getItem(prefix + id);
- isCached = cached !== null;
-
- if (isCached) {
- var data = JSON.parse(cached);
- injectSprite.call(container, data.content);
- return;
- }
- }
-
- // Get the sprite
- utils.fetch(url).then(function (result) {
- if (utils.is.empty(result)) {
- return;
- }
-
- if (support.storage) {
- window.localStorage.setItem(prefix + id, JSON.stringify({
- content: result
- }));
- }
-
- injectSprite.call(container, result);
- }).catch(function () {});
- }
- },
+ function _nonIterableRest() {
+ throw new TypeError("Invalid attempt to destructure non-iterable instance");
+ }
- // Generate a random ID
- generateId: function generateId(prefix) {
- return prefix + '-' + Math.floor(Math.random() * 10000);
- },
-
-
- // Wrap an element
- wrap: function wrap(elements, wrapper) {
- // Convert `elements` to an array, if necessary.
- var targets = elements.length ? elements : [elements];
-
- // Loops backwards to prevent having to clone the wrapper on the
- // first element (see `child` below).
- Array.from(targets).reverse().forEach(function (element, index) {
- var child = index > 0 ? wrapper.cloneNode(true) : wrapper;
-
- // Cache the current parent and sibling.
- var parent = element.parentNode;
- var sibling = element.nextSibling;
-
- // Wrap the element (is automatically removed from its current
- // parent).
- child.appendChild(element);
-
- // If the element had a sibling, insert the wrapper before
- // the sibling to maintain the HTML structure; otherwise, just
- // append it to the parent.
- if (sibling) {
- parent.insertBefore(child, sibling);
- } else {
- parent.appendChild(child);
- }
- });
- },
-
-
- // Create a DocumentFragment
- createElement: function createElement(type, attributes, text) {
- // Create a new <element>
- var element = document.createElement(type);
-
- // Set all passed attributes
- if (utils.is.object(attributes)) {
- utils.setAttributes(element, attributes);
- }
-
- // Add text node
- if (utils.is.string(text)) {
- element.textContent = text;
- }
-
- // Return built element
- return element;
- },
-
-
- // Inaert an element after another
- insertAfter: function insertAfter(element, target) {
- target.parentNode.insertBefore(element, target.nextSibling);
- },
-
-
- // Insert a DocumentFragment
- insertElement: function insertElement(type, parent, attributes, text) {
- // Inject the new <element>
- parent.appendChild(utils.createElement(type, attributes, text));
- },
-
-
- // Remove element(s)
- removeElement: function removeElement(element) {
- if (utils.is.nodeList(element) || utils.is.array(element)) {
- Array.from(element).forEach(utils.removeElement);
- return;
- }
-
- if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
- return;
- }
-
- element.parentNode.removeChild(element);
- },
-
-
- // Remove all child elements
- emptyElement: function emptyElement(element) {
- var length = element.childNodes.length;
-
-
- while (length > 0) {
- element.removeChild(element.lastChild);
- length -= 1;
- }
- },
-
-
- // Replace element
- replaceElement: function replaceElement(newChild, oldChild) {
- if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
- return null;
- }
-
- oldChild.parentNode.replaceChild(newChild, oldChild);
-
- return newChild;
- },
-
-
- // Set attributes
- setAttributes: function setAttributes(element, attributes) {
- if (!utils.is.element(element) || utils.is.empty(attributes)) {
- return;
- }
-
- Object.entries(attributes).forEach(function (_ref) {
- var _ref2 = slicedToArray(_ref, 2),
- key = _ref2[0],
- value = _ref2[1];
-
- element.setAttribute(key, value);
- });
- },
-
-
- // Get an attribute object from a string selector
- getAttributesFromSelector: function getAttributesFromSelector(sel, existingAttributes) {
- // For example:
- // '.test' to { class: 'test' }
- // '#test' to { id: 'test' }
- // '[data-test="test"]' to { 'data-test': 'test' }
-
- if (!utils.is.string(sel) || utils.is.empty(sel)) {
- return {};
- }
-
- var attributes = {};
- var existing = existingAttributes;
-
- sel.split(',').forEach(function (s) {
- // Remove whitespace
- var selector = s.trim();
- var className = selector.replace('.', '');
- var stripped = selector.replace(/[[\]]/g, '');
-
- // Get the parts and value
- var parts = stripped.split('=');
- var key = parts[0];
- var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
-
- // Get the first character
- var start = selector.charAt(0);
-
- switch (start) {
- case '.':
- // Add to existing classname
- if (utils.is.object(existing) && utils.is.string(existing.class)) {
- existing.class += ' ' + className;
- }
-
- attributes.class = className;
- break;
-
- case '#':
- // ID selector
- attributes.id = selector.replace('#', '');
- break;
-
- case '[':
- // Attribute selector
- attributes[key] = value;
-
- break;
-
- default:
- break;
- }
- });
-
- return attributes;
- },
-
-
- // Toggle hidden
- toggleHidden: function toggleHidden(element, hidden) {
- if (!utils.is.element(element)) {
- return;
- }
-
- var hide = hidden;
-
- if (!utils.is.boolean(hide)) {
- hide = !element.hasAttribute('hidden');
- }
-
- if (hide) {
- element.setAttribute('hidden', '');
- } else {
- element.removeAttribute('hidden');
- }
- },
-
-
- // Toggle class on an element
- toggleClass: function toggleClass(element, className, toggle) {
- if (utils.is.element(element)) {
- var contains = element.classList.contains(className);
-
- element.classList[toggle ? 'add' : 'remove'](className);
-
- return toggle && !contains || !toggle && contains;
- }
-
- return null;
- },
-
-
- // Has class name
- hasClass: function hasClass(element, className) {
- return utils.is.element(element) && element.classList.contains(className);
- },
-
-
- // Element matches selector
- matches: function matches(element, selector) {
- var prototype = { Element: Element };
-
- function match() {
- return Array.from(document.querySelectorAll(selector)).includes(this);
- }
-
- var matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
-
- return matches.call(element, selector);
- },
-
-
- // Find all elements
- getElements: function getElements(selector) {
- return this.elements.container.querySelectorAll(selector);
- },
-
-
- // Find a single element
- getElement: function getElement(selector) {
- return this.elements.container.querySelector(selector);
- },
-
+ // ==========================================================================
+ // Type checking utils
+ // ==========================================================================
+ var getConstructor = function getConstructor(input) {
+ return input !== null && typeof input !== 'undefined' ? input.constructor : null;
+ };
- // Get the focused element
- getFocusElement: function getFocusElement() {
- var focused = document.activeElement;
+ var instanceOf = function instanceOf(input, constructor) {
+ return Boolean(input && constructor && input instanceof constructor);
+ };
- if (!focused || focused === document.body) {
- focused = null;
- } else {
- focused = document.querySelector(':focus');
- }
+ var isNullOrUndefined = function isNullOrUndefined(input) {
+ return input === null || typeof input === 'undefined';
+ };
- return focused;
- },
+ var isObject = function isObject(input) {
+ return getConstructor(input) === Object;
+ };
+ var isNumber = function isNumber(input) {
+ return getConstructor(input) === Number && !Number.isNaN(input);
+ };
- // Trap focus inside container
- trapFocus: function trapFocus() {
- var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
- var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var isString = function isString(input) {
+ return getConstructor(input) === String;
+ };
- if (!utils.is.element(element)) {
- return;
- }
+ var isBoolean = function isBoolean(input) {
+ return getConstructor(input) === Boolean;
+ };
- var focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
- var first = focusable[0];
- var last = focusable[focusable.length - 1];
+ var isFunction = function isFunction(input) {
+ return getConstructor(input) === Function;
+ };
- var trap = function trap(event) {
- // Bail if not tab key or not fullscreen
- if (event.key !== 'Tab' || event.keyCode !== 9) {
- return;
- }
+ var isArray = function isArray(input) {
+ return Array.isArray(input);
+ };
- // Get the current focused element
- var focused = utils.getFocusElement();
-
- if (focused === last && !event.shiftKey) {
- // Move focus to first element that can be tabbed if Shift isn't used
- first.focus();
- event.preventDefault();
- } else if (focused === first && event.shiftKey) {
- // Move focus to last element that can be tabbed if Shift is used
- last.focus();
- event.preventDefault();
- }
- };
+ var isWeakMap = function isWeakMap(input) {
+ return instanceOf(input, WeakMap);
+ };
- if (toggle) {
- utils.on(this.elements.container, 'keydown', trap, false);
- } else {
- utils.off(this.elements.container, 'keydown', trap, false);
- }
- },
+ var isNodeList = function isNodeList(input) {
+ return instanceOf(input, NodeList);
+ };
+ var isElement = function isElement(input) {
+ return instanceOf(input, Element);
+ };
- // Toggle event listener
- toggleListener: function toggleListener(elements, event, callback) {
- var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
- var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
- var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
+ var isTextNode = function isTextNode(input) {
+ return getConstructor(input) === Text;
+ };
- // Bail if no elemetns, event, or callback
- if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
- return;
- }
+ var isEvent = function isEvent(input) {
+ return instanceOf(input, Event);
+ };
- // If a nodelist is passed, call itself on each node
- if (utils.is.nodeList(elements) || utils.is.array(elements)) {
- // Create listener for each node
- Array.from(elements).forEach(function (element) {
- if (element instanceof Node) {
- utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
- }
- });
+ var isKeyboardEvent = function isKeyboardEvent(input) {
+ return instanceOf(input, KeyboardEvent);
+ };
- return;
- }
+ var isCue = function isCue(input) {
+ return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
+ };
- // Allow multiple events
- var events = event.split(' ');
+ var isTrack = function isTrack(input) {
+ return instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);
+ };
- // Build options
- // Default to just the capture boolean for browsers with no passive listener support
- var options = capture;
+ var isEmpty = function isEmpty(input) {
+ return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;
+ };
- // If passive events listeners are supported
- if (support.passiveListeners) {
- options = {
- // Whether the listener can be passive (i.e. default never prevented)
- passive: passive,
- // Whether the listener is a capturing listener or not
- capture: capture
- };
- }
+ var isUrl = function isUrl(input) {
+ // Accept a URL object
+ if (instanceOf(input, window.URL)) {
+ return true;
+ } // Must be string from here
- // If a single node is passed, bind the event listener
- events.forEach(function (type) {
- elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
- });
- },
+ if (!isString(input)) {
+ return false;
+ } // Add the protocol if required
- // Bind event handler
- on: function on(element) {
- var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var callback = arguments[2];
- var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
- var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
- utils.toggleListener(element, events, callback, true, passive, capture);
- },
+ var string = input;
+ if (!input.startsWith('http://') || !input.startsWith('https://')) {
+ string = "http://".concat(input);
+ }
- // Unbind event handler
- off: function off(element) {
- var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var callback = arguments[2];
- var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
- var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+ try {
+ return !isEmpty(new URL(string).hostname);
+ } catch (e) {
+ return false;
+ }
+ };
- utils.toggleListener(element, events, callback, false, passive, capture);
- },
+ var is = {
+ nullOrUndefined: isNullOrUndefined,
+ object: isObject,
+ number: isNumber,
+ string: isString,
+ boolean: isBoolean,
+ function: isFunction,
+ array: isArray,
+ weakMap: isWeakMap,
+ nodeList: isNodeList,
+ element: isElement,
+ textNode: isTextNode,
+ event: isEvent,
+ keyboardEvent: isKeyboardEvent,
+ cue: isCue,
+ track: isTrack,
+ url: isUrl,
+ empty: isEmpty
+ };
+ // ==========================================================================
+ // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
+ // https://www.youtube.com/watch?v=NPM6172J22g
- // Trigger event
- dispatchEvent: function dispatchEvent(element) {
- var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var bubbles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
- var detail = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
+ var supportsPassiveListeners = function () {
+ // Test via a getter in the options object to see if the passive property is accessed
+ var supported = false;
- // Bail if no element
- if (!utils.is.element(element) || utils.is.empty(type)) {
- return;
- }
+ try {
+ var options = Object.defineProperty({}, 'passive', {
+ get: function get() {
+ supported = true;
+ return null;
+ }
+ });
+ window.addEventListener('test', null, options);
+ window.removeEventListener('test', null, options);
+ } catch (e) {// Do nothing
+ }
- // Create and dispatch the event
- var event = new CustomEvent(type, {
- bubbles: bubbles,
- detail: Object.assign({}, detail, {
- plyr: utils.is.plyr(this) ? this : null
- })
- });
+ return supported;
+ }(); // Toggle event listener
- // Dispatch the event
- element.dispatchEvent(event);
- },
+ function toggleListener(element, event, callback) {
+ var _this = this;
- // Toggle aria-pressed state on a toggle button
- // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
- toggleState: function toggleState(element, input) {
- // If multiple elements passed
- if (utils.is.array(element) || utils.is.nodeList(element)) {
- Array.from(element).forEach(function (target) {
- return utils.toggleState(target, input);
- });
- return;
- }
+ var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
+ var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
+ var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
- // Bail if no target
- if (!utils.is.element(element)) {
- return;
- }
+ // Bail if no element, event, or callback
+ if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
+ return;
+ } // Allow multiple events
- // Get state
- var pressed = element.getAttribute('aria-pressed') === 'true';
- var state = utils.is.boolean(input) ? input : !pressed;
- // Set the attribute on target
- element.setAttribute('aria-pressed', state);
- },
+ var events = event.split(' '); // Build options
+ // Default to just the capture boolean for browsers with no passive listener support
+ var options = capture; // If passive events listeners are supported
- // 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 (supportsPassiveListeners) {
+ options = {
+ // Whether the listener can be passive (i.e. default never prevented)
+ passive: passive,
+ // Whether the listener is a capturing listener or not
+ capture: capture
+ };
+ } // If a single node is passed, bind the event listener
- if (utils.is.empty(input)) {
- return input;
- }
- return input.toString().replace(/{(\d+)}/g, function (match, i) {
- return utils.is.string(args[i]) ? args[i] : '';
+ events.forEach(function (type) {
+ if (_this && _this.eventListeners && toggle) {
+ // Cache event listener
+ _this.eventListeners.push({
+ element: element,
+ type: type,
+ callback: callback,
+ options: options
});
- },
-
-
- // Get percentage
- getPercentage: function getPercentage(current, max) {
- if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
- return 0;
- }
-
- return (current / max * 100).toFixed(2);
- },
-
-
- // Time helpers
- getHours: function getHours(value) {
- return parseInt(value / 60 / 60 % 60, 10);
- },
- getMinutes: function getMinutes(value) {
- return parseInt(value / 60 % 60, 10);
- },
- getSeconds: function getSeconds(value) {
- return parseInt(value % 60, 10);
- },
-
-
- // Format time to UI friendly string
- formatTime: function formatTime() {
- var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
- var displayHours = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
- var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ }
- // Bail if the value isn't a number
- if (!utils.is.number(time)) {
- return this.formatTime(null, displayHours, inverted);
- }
+ element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
+ });
+ } // Bind event handler
+
+ function on(element) {
+ var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+ var callback = arguments.length > 2 ? arguments[2] : undefined;
+ var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
+ var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+ toggleListener.call(this, element, events, callback, true, passive, capture);
+ } // Unbind event handler
+
+ function off(element) {
+ var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+ var callback = arguments.length > 2 ? arguments[2] : undefined;
+ var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
+ var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+ toggleListener.call(this, element, events, callback, false, passive, capture);
+ } // Bind once-only event handler
+
+ function once(element) {
+ var _this2 = this;
+
+ var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+ var callback = arguments.length > 2 ? arguments[2] : undefined;
+ var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
+ var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+
+ var onceCallback = function onceCallback() {
+ off(element, events, onceCallback, passive, capture);
+
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
+ args[_key] = arguments[_key];
+ }
- // Format time component to add leading zero
- var format = function format(value) {
- return ('0' + value).slice(-2);
- };
+ callback.apply(_this2, args);
+ };
+
+ toggleListener.call(this, element, events, onceCallback, true, passive, capture);
+ } // Trigger event
+
+ function triggerEvent(element) {
+ var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+ var bubbles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var detail = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
+
+ // Bail if no element
+ if (!is.element(element) || is.empty(type)) {
+ return;
+ } // Create and dispatch the event
+
+
+ var event = new CustomEvent(type, {
+ bubbles: bubbles,
+ detail: Object.assign({}, detail, {
+ plyr: this
+ })
+ }); // Dispatch the event
+
+ element.dispatchEvent(event);
+ } // Unbind all cached event listeners
+
+ function unbindListeners() {
+ if (this && this.eventListeners) {
+ this.eventListeners.forEach(function (item) {
+ var element = item.element,
+ type = item.type,
+ callback = item.callback,
+ options = item.options;
+ element.removeEventListener(type, callback, options);
+ });
+ this.eventListeners = [];
+ }
+ } // Run method when / if player is ready
- // Breakdown to hours, mins, secs
- var hours = this.getHours(time);
- var mins = this.getMinutes(time);
- var secs = this.getSeconds(time);
+ function ready() {
+ var _this3 = this;
- // Do we need to display hours?
- if (displayHours || hours > 0) {
- hours = hours + ':';
- } else {
- hours = '';
- }
+ return new Promise(function (resolve) {
+ return _this3.ready ? setTimeout(resolve, 0) : on.call(_this3, _this3.elements.container, 'ready', resolve);
+ }).then(function () {});
+ }
- // Render
- return '' + (inverted ? '-' : '') + hours + format(mins) + ':' + format(secs);
- },
+ function wrap(elements, wrapper) {
+ // Convert `elements` to an array, if necessary.
+ var targets = elements.length ? elements : [elements]; // Loops backwards to prevent having to clone the wrapper on the
+ // first element (see `child` below).
+ Array.from(targets).reverse().forEach(function (element, index) {
+ var child = index > 0 ? wrapper.cloneNode(true) : wrapper; // Cache the current parent and sibling.
- // Replace all occurances of a string in a string
- replaceAll: function replaceAll() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
- var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
+ var parent = element.parentNode;
+ var sibling = element.nextSibling; // Wrap the element (is automatically removed from its current
+ // parent).
- return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
- },
+ child.appendChild(element); // If the element had a sibling, insert the wrapper before
+ // the sibling to maintain the HTML structure; otherwise, just
+ // append it to the parent.
+ if (sibling) {
+ parent.insertBefore(child, sibling);
+ } else {
+ parent.appendChild(child);
+ }
+ });
+ } // Set attributes
- // Convert to title case
- toTitleCase: function toTitleCase() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ function setAttributes(element, attributes) {
+ if (!is.element(element) || is.empty(attributes)) {
+ return;
+ } // Assume null and undefined attributes should be left out,
+ // Setting them would otherwise convert them to "null" and "undefined"
- return input.toString().replace(/\w\S*/g, function (text) {
- return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
- });
- },
+ Object.entries(attributes).filter(function (_ref) {
+ var _ref2 = _slicedToArray(_ref, 2),
+ value = _ref2[1];
- // Convert string to pascalCase
- toPascalCase: function toPascalCase() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ return !is.nullOrUndefined(value);
+ }).forEach(function (_ref3) {
+ var _ref4 = _slicedToArray(_ref3, 2),
+ key = _ref4[0],
+ value = _ref4[1];
- var string = input.toString();
+ return element.setAttribute(key, value);
+ });
+ } // Create a DocumentFragment
- // Convert kebab case
- string = utils.replaceAll(string, '-', ' ');
+ function createElement(type, attributes, text) {
+ // Create a new <element>
+ var element = document.createElement(type); // Set all passed attributes
- // Convert snake case
- string = utils.replaceAll(string, '_', ' ');
+ if (is.object(attributes)) {
+ setAttributes(element, attributes);
+ } // Add text node
- // Convert to title case
- string = utils.toTitleCase(string);
- // Convert to pascal case
- return utils.replaceAll(string, ' ', '');
- },
+ if (is.string(text)) {
+ element.innerText = text;
+ } // Return built element
- // Convert string to pascalCase
- toCamelCase: function toCamelCase() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ return element;
+ } // Inaert an element after another
- var string = input.toString();
+ function insertAfter(element, target) {
+ if (!is.element(element) || !is.element(target)) {
+ return;
+ }
- // Convert to pascal case
- string = utils.toPascalCase(string);
+ target.parentNode.insertBefore(element, target.nextSibling);
+ } // Insert a DocumentFragment
- // Convert first character to lowercase
- return string.charAt(0).toLowerCase() + string.slice(1);
- },
+ function insertElement(type, parent, attributes, text) {
+ if (!is.element(parent)) {
+ return;
+ }
+ parent.appendChild(createElement(type, attributes, text));
+ } // Remove element(s)
- // Deep extend destination object with N more objects
- extend: function extend() {
- var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ function removeElement(element) {
+ if (is.nodeList(element) || is.array(element)) {
+ Array.from(element).forEach(removeElement);
+ return;
+ }
- for (var _len2 = arguments.length, sources = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
- sources[_key2 - 1] = arguments[_key2];
- }
+ if (!is.element(element) || !is.element(element.parentNode)) {
+ return;
+ }
- if (!sources.length) {
- return target;
- }
+ element.parentNode.removeChild(element);
+ } // Remove all child elements
- var source = sources.shift();
+ function emptyElement(element) {
+ if (!is.element(element)) {
+ return;
+ }
- if (!utils.is.object(source)) {
- return target;
- }
+ var length = element.childNodes.length;
- Object.keys(source).forEach(function (key) {
- if (utils.is.object(source[key])) {
- if (!Object.keys(target).includes(key)) {
- Object.assign(target, defineProperty({}, key, {}));
- }
+ while (length > 0) {
+ element.removeChild(element.lastChild);
+ length -= 1;
+ }
+ } // Replace element
- utils.extend(target[key], source[key]);
- } else {
- Object.assign(target, defineProperty({}, key, source[key]));
- }
- });
+ function replaceElement(newChild, oldChild) {
+ if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
+ return null;
+ }
- return utils.extend.apply(utils, [target].concat(toConsumableArray(sources)));
- },
+ oldChild.parentNode.replaceChild(newChild, oldChild);
+ return newChild;
+ } // Get an attribute object from a string selector
+
+ function getAttributesFromSelector(sel, existingAttributes) {
+ // For example:
+ // '.test' to { class: 'test' }
+ // '#test' to { id: 'test' }
+ // '[data-test="test"]' to { 'data-test': 'test' }
+ if (!is.string(sel) || is.empty(sel)) {
+ return {};
+ }
+ var attributes = {};
+ var existing = existingAttributes;
+ sel.split(',').forEach(function (s) {
+ // Remove whitespace
+ var selector = s.trim();
+ var className = selector.replace('.', '');
+ var stripped = selector.replace(/[[\]]/g, ''); // Get the parts and value
- // Remove duplicates in an array
- dedupe: function dedupe(array) {
- if (!utils.is.array(array)) {
- return array;
- }
+ var parts = stripped.split('=');
+ var key = parts[0];
+ var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; // Get the first character
- return array.filter(function (item, index) {
- return array.indexOf(item) === index;
- });
- },
+ var start = selector.charAt(0);
+ switch (start) {
+ case '.':
+ // Add to existing classname
+ if (is.object(existing) && is.string(existing.class)) {
+ existing.class += " ".concat(className);
+ }
- // Get the closest value in an array
- closest: function closest(array, value) {
- if (!utils.is.array(array) || !array.length) {
- return null;
- }
+ attributes.class = className;
+ break;
- return array.reduce(function (prev, curr) {
- return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
- });
- },
+ case '#':
+ // ID selector
+ attributes.id = selector.replace('#', '');
+ break;
+ case '[':
+ // Attribute selector
+ attributes[key] = value;
+ break;
- // Get the provider for a given URL
- getProviderByUrl: function getProviderByUrl(url) {
- // YouTube
- if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
- return providers.youtube;
- }
+ default:
+ break;
+ }
+ });
+ return attributes;
+ } // Toggle hidden
- // Vimeo
- if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
- return providers.vimeo;
- }
+ function toggleHidden(element, hidden) {
+ if (!is.element(element)) {
+ return;
+ }
- return null;
- },
+ var hide = hidden;
+ if (!is.boolean(hide)) {
+ hide = !element.hidden;
+ }
- // Parse YouTube ID from URL
- parseYouTubeId: function parseYouTubeId(url) {
- if (utils.is.empty(url)) {
- return null;
- }
+ if (hide) {
+ element.setAttribute('hidden', '');
+ } else {
+ element.removeAttribute('hidden');
+ }
+ } // Mirror Element.classList.toggle, with IE compatibility for "force" argument
- var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
- return url.match(regex) ? RegExp.$2 : url;
- },
+ function toggleClass(element, className, force) {
+ if (is.nodeList(element)) {
+ return Array.from(element).map(function (e) {
+ return toggleClass(e, className, force);
+ });
+ }
+ if (is.element(element)) {
+ var method = 'toggle';
- // Parse Vimeo ID from URL
- parseVimeoId: function parseVimeoId(url) {
- if (utils.is.empty(url)) {
- return null;
- }
+ if (typeof force !== 'undefined') {
+ method = force ? 'add' : 'remove';
+ }
- if (utils.is.number(Number(url))) {
- return url;
- }
+ element.classList[method](className);
+ return element.classList.contains(className);
+ }
- var regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
- return url.match(regex) ? RegExp.$2 : url;
- },
+ return false;
+ } // Has class name
+ function hasClass(element, className) {
+ return is.element(element) && element.classList.contains(className);
+ } // Element matches selector
- // Convert a URL to a location object
- parseUrl: function parseUrl(url) {
- var parser = document.createElement('a');
- parser.href = url;
- return parser;
- },
+ function matches(element, selector) {
+ function match() {
+ return Array.from(document.querySelectorAll(selector)).includes(this);
+ }
- // Get URL query parameters
- getUrlParams: function getUrlParams(input) {
- var search = input;
+ var matches = match;
+ return matches.call(element, selector);
+ } // Find all elements
- // Parse URL if needed
- if (input.startsWith('http://') || input.startsWith('https://')) {
- var _parseUrl = this.parseUrl(input);
+ function getElements(selector) {
+ return this.elements.container.querySelectorAll(selector);
+ } // Find a single element
- search = _parseUrl.search;
- }
+ function getElement(selector) {
+ return this.elements.container.querySelector(selector);
+ } // Trap focus inside container
- if (this.is.empty(search)) {
- return null;
- }
+ function trapFocus() {
+ var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+ var toggle = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
- var hashes = search.slice(search.indexOf('?') + 1).split('&');
+ if (!is.element(element)) {
+ return;
+ }
- return hashes.reduce(function (params, hash) {
- var _hash$split = hash.split('='),
- _hash$split2 = slicedToArray(_hash$split, 2),
- key = _hash$split2[0],
- val = _hash$split2[1];
+ var focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
+ var first = focusable[0];
+ var last = focusable[focusable.length - 1];
- return Object.assign(params, defineProperty({}, key, decodeURIComponent(val)));
- }, {});
- },
+ var trap = function trap(event) {
+ // Bail if not tab key or not fullscreen
+ if (event.key !== 'Tab' || event.keyCode !== 9) {
+ return;
+ } // Get the current focused element
- // Convert object to URL parameters
- buildUrlParams: function buildUrlParams(input) {
- if (!utils.is.object(input)) {
- return '';
- }
+ var focused = document.activeElement;
- return Object.keys(input).map(function (key) {
- return encodeURIComponent(key) + '=' + encodeURIComponent(input[key]);
- }).join('&');
- },
+ if (focused === last && !event.shiftKey) {
+ // Move focus to first element that can be tabbed if Shift isn't used
+ first.focus();
+ event.preventDefault();
+ } else if (focused === first && event.shiftKey) {
+ // Move focus to last element that can be tabbed if Shift is used
+ last.focus();
+ event.preventDefault();
+ }
+ };
+ toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
+ } // Set focus and tab focus class
- // Remove HTML from a string
- stripHTML: function stripHTML(source) {
- var fragment = document.createDocumentFragment();
- var element = document.createElement('div');
- fragment.appendChild(element);
- element.innerHTML = source;
- return fragment.firstChild.innerText;
- },
+ function setFocus() {
+ var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+ var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ if (!is.element(element)) {
+ return;
+ } // Set regular focus
- // Get aspect ratio for dimensions
- getAspectRatio: function getAspectRatio(width, height) {
- var getRatio = function getRatio(w, h) {
- return h === 0 ? w : getRatio(h, w % h);
- };
- var ratio = getRatio(width, height);
- return width / ratio + ':' + height / ratio;
- },
+ element.focus({
+ preventScroll: true
+ }); // If we want to mimic keyboard focus via tab
- // Get the transition end event
- get transitionEndEvent() {
- var element = document.createElement('span');
+ if (tabFocus) {
+ toggleClass(element, this.config.classNames.tabFocus);
+ }
+ }
- var events = {
- WebkitTransition: 'webkitTransitionEnd',
- MozTransition: 'transitionend',
- OTransition: 'oTransitionEnd otransitionend',
- transition: 'transitionend'
- };
+ // ==========================================================================
+ var transitionEndEvent = function () {
+ var element = document.createElement('span');
+ var events = {
+ WebkitTransition: 'webkitTransitionEnd',
+ MozTransition: 'transitionend',
+ OTransition: 'oTransitionEnd otransitionend',
+ transition: 'transitionend'
+ };
+ var type = Object.keys(events).find(function (event) {
+ return element.style[event] !== undefined;
+ });
+ return is.string(type) ? events[type] : false;
+ }(); // Force repaint of element
- var type = Object.keys(events).find(function (event) {
- return element.style[event] !== undefined;
- });
+ function repaint(element) {
+ setTimeout(function () {
+ try {
+ toggleHidden(element, true);
+ element.offsetHeight; // eslint-disable-line
- return utils.is.string(type) ? events[type] : false;
- },
+ toggleHidden(element, false);
+ } catch (e) {// Do nothing
+ }
+ }, 0);
+ }
- // Force repaint of element
- repaint: function repaint(element) {
- setTimeout(function () {
- utils.toggleHidden(element, true);
- element.offsetHeight; // eslint-disable-line
- utils.toggleHidden(element, false);
- }, 0);
- }
-};
+ // ==========================================================================
+ // Browser sniffing
+ // Unfortunately, due to mixed support, UA sniffing is required
+ // ==========================================================================
+ var browser = {
+ isIE:
+ /* @cc_on!@ */
+ !!document.documentMode,
+ isEdge: window.navigator.userAgent.includes('Edge'),
+ isWebkit: 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent),
+ isIPhone: /(iPhone|iPod)/gi.test(navigator.platform),
+ isIos: /(iPad|iPhone|iPod)/gi.test(navigator.platform)
+ };
-// ==========================================================================
+ var defaultCodecs = {
+ 'audio/ogg': 'vorbis',
+ 'audio/wav': '1',
+ 'video/webm': 'vp8, vorbis',
+ 'video/mp4': 'avc1.42E01E, mp4a.40.2',
+ 'video/ogg': 'theora'
+ }; // Check for feature support
-// Check for feature support
-var support = {
+ var support = {
// Basic support
audio: 'canPlayType' in document.createElement('audio'),
video: 'canPlayType' in document.createElement('video'),
-
// Check for support
// Basic functionality vs full UI
check: function check(type, provider, playsinline) {
- var api = false;
- var ui = false;
- var browser = utils.getBrowser();
- var canPlayInline = browser.isIPhone && playsinline && support.playsinline;
-
- switch (provider + ':' + type) {
- case 'html5:video':
- api = support.video;
- ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
- break;
-
- case 'html5:audio':
- api = support.audio;
- ui = api && support.rangeInput;
- break;
+ var canPlayInline = browser.isIPhone && playsinline && support.playsinline;
+ var api = support[type] || provider !== 'html5';
+ var ui = api && support.rangeInput && (type !== 'video' || !browser.isIPhone || canPlayInline);
+ return {
+ api: api,
+ ui: ui
+ };
+ },
+ // Picture-in-picture support
+ // Safari & Chrome only currently
+ pip: function () {
+ if (browser.isIPhone) {
+ return false;
+ } // Safari
+ // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
- case 'youtube:video':
- case 'vimeo:video':
- api = true;
- ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
- break;
- default:
- api = support.audio && support.video;
- ui = api && support.rangeInput;
- }
+ if (is.function(createElement('video').webkitSetPresentationMode)) {
+ return true;
+ } // Chrome
+ // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
- return {
- api: api,
- ui: ui
- };
- },
+ if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
+ return true;
+ }
- // Picture-in-picture support
- // Safari only currently
- pip: function () {
- var browser = utils.getBrowser();
- return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
+ return false;
}(),
-
// Airplay support
// Safari only currently
- airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
-
+ airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),
// Inline playback support
// https://webkit.org/blog/6784/new-video-policies-for-ios/
playsinline: 'playsInline' in document.createElement('video'),
-
// Check for mime type support against a player instance
// Credits: http://diveintohtml5.info/everything.html
// Related: http://www.leanbackplayer.com/test/h5mt.html
- mime: function mime(type) {
- var media = this.media;
-
-
- try {
- // Bail if no checking function
- if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
- return false;
- }
-
- // Check directly if codecs specified
- if (type.includes('codecs=')) {
- return media.canPlayType(type).replace(/no/, '');
- }
-
- // Type specific checks
- if (this.isVideo) {
- switch (type) {
- case 'video/webm':
- return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
-
- case 'video/mp4':
- return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
+ mime: function mime(input) {
+ if (is.empty(input)) {
+ return false;
+ }
- case 'video/ogg':
- return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
+ var _input$split = input.split('/'),
+ _input$split2 = _slicedToArray(_input$split, 1),
+ mediaType = _input$split2[0];
- default:
- return false;
- }
- } else if (this.isAudio) {
- switch (type) {
- case 'audio/mpeg':
- return media.canPlayType('audio/mpeg;').replace(/no/, '');
+ var type = input; // Verify we're using HTML5 and there's no media type mismatch
- case 'audio/ogg':
- return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
+ if (!this.isHTML5 || mediaType !== this.type) {
+ return false;
+ } // Add codec if required
- case 'audio/wav':
- return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
- default:
- return false;
- }
- }
- } catch (e) {
- return false;
- }
+ if (Object.keys(defaultCodecs).includes(type)) {
+ type += "; codecs=\"".concat(defaultCodecs[input], "\"");
+ }
- // If we got this far, we're stuffed
+ try {
+ return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
+ } catch (e) {
return false;
+ }
},
-
-
// Check for textTracks support
textTracks: 'textTracks' in document.createElement('video'),
-
- // Check for passive event listener support
- // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
- // https://www.youtube.com/watch?v=NPM6172J22g
- passiveListeners: function () {
- // Test via a getter in the options object to see if the passive property is accessed
- var supported = false;
- try {
- var options = Object.defineProperty({}, 'passive', {
- get: function get() {
- supported = true;
- return null;
- }
- });
- window.addEventListener('test', null, options);
- } catch (e) {
- // Do nothing
- }
-
- return supported;
- }(),
-
// <input type="range"> Sliders
rangeInput: function () {
- var range = document.createElement('input');
- range.type = 'range';
- return range.type === 'range';
+ var range = document.createElement('input');
+ range.type = 'range';
+ return range.type === 'range';
}(),
-
// Touch
// NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement,
-
// Detect transitions support
- transitions: utils.transitionEndEvent !== false,
-
+ transitions: transitionEndEvent !== false,
// Reduced motion iOS & MacOS setting
// https://webkit.org/blog/7551/responsive-design-for-motion/
reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
-};
-
-// ==========================================================================
+ };
-var html5 = {
+ // ==========================================================================
+ var html5 = {
getSources: function getSources() {
- if (!this.isHTML5) {
- return null;
- }
-
- return this.media.querySelectorAll('source');
- },
+ var _this = this;
+ if (!this.isHTML5) {
+ return [];
+ }
- // 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;
- }
+ var sources = Array.from(this.media.querySelectorAll('source')); // Filter out unsupported sources (if type is specified)
- // Get <source> with size attribute
- var sizes = Array.from(sources).filter(function (source) {
- return !utils.is.empty(source.getAttribute('size'));
- });
+ return sources.filter(function (source) {
+ var type = source.getAttribute('type');
- // If none, bail
- if (utils.is.empty(sizes)) {
- return null;
+ if (is.empty(type)) {
+ return true;
}
- // Reduce to unique list
- return utils.dedupe(sizes.map(function (source) {
- return Number(source.getAttribute('size'));
- }));
+ return support.mime.call(_this, type);
+ });
+ },
+ // Get quality levels
+ getQualityOptions: function getQualityOptions() {
+ // Get sizes from <source> elements
+ return html5.getSources.call(this).map(function (source) {
+ return Number(source.getAttribute('size'));
+ }).filter(Boolean);
},
extend: function extend() {
- if (!this.isHTML5) {
- return;
- }
-
- var 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 (!this.isHTML5) {
+ return;
+ }
- if (utils.is.empty(sources)) {
- return;
- }
+ var player = this; // Quality
- // Get matches for requested size
- var matches = Array.from(sources).filter(function (source) {
- return Number(source.getAttribute('size')) === input;
- });
+ Object.defineProperty(player.media, 'quality', {
+ get: function get() {
+ // Get sources
+ var sources = html5.getSources.call(player);
+ var source = sources.find(function (source) {
+ return source.getAttribute('src') === player.source;
+ }); // Return size, if match is found
- // No matches for requested size
- if (utils.is.empty(matches)) {
- return;
- }
+ return source && Number(source.getAttribute('size'));
+ },
+ set: function set(input) {
+ // Get sources
+ var sources = html5.getSources.call(player); // Get first match for requested size
- // Get supported sources
- var supported = matches.filter(function (source) {
- return support.mime.call(player, source.getAttribute('type'));
- });
+ var source = sources.find(function (source) {
+ return Number(source.getAttribute('size')) === input;
+ }); // No matching source found
- // No supported sources
- if (utils.is.empty(supported)) {
- return;
- }
+ if (!source) {
+ return;
+ } // Get current state
- // Trigger change event
- utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
- quality: input
- });
- // Get current state
- var currentTime = player.currentTime,
- playing = player.playing;
+ var _player$media = player.media,
+ currentTime = _player$media.currentTime,
+ paused = _player$media.paused,
+ preload = _player$media.preload,
+ readyState = _player$media.readyState; // Set new source
- // Set new source
+ player.media.src = source.getAttribute('src'); // Prevent loading if preload="none" and the current source isn't loaded (#1044)
- player.media.src = supported[0].getAttribute('src');
+ if (preload !== 'none' || readyState) {
+ // Restore time
+ player.once('loadedmetadata', function () {
+ player.currentTime = currentTime; // Resume playing
- // Load new source
- player.media.load();
+ if (!paused) {
+ player.play();
+ }
+ }); // Load new source
- // Resume playing
- if (playing) {
- player.play();
- }
+ player.media.load();
+ } // Trigger change event
- // Restore time
- player.currentTime = currentTime;
- // Trigger change event
- utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
- quality: input
- });
- }
- });
+ triggerEvent.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');
- }
-};
+ if (!this.isHTML5) {
+ return;
+ } // Remove child sources
-// ==========================================================================
-var i18n = {
- get: function get$$1() {
- var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
- var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ removeElement(html5.getSources.call(this)); // Set blank video src attribute
+ // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error
+ // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection
- if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
- return '';
- }
+ 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
- var string = config.i18n[key];
-
- var replace = {
- '{seektime}': config.seekTime,
- '{title}': config.title
- };
+ this.media.load(); // Debugging
- Object.entries(replace).forEach(function (_ref) {
- var _ref2 = slicedToArray(_ref, 2),
- key = _ref2[0],
- value = _ref2[1];
+ this.debug.log('Cancelled network requests');
+ }
+ };
- string = utils.replaceAll(string, key, value);
- });
+ // ==========================================================================
- return string;
+ function dedupe(array) {
+ if (!is.array(array)) {
+ return array;
}
-};
-
-// ==========================================================================
-// Sniff out the browser
-var browser = utils.getBrowser();
+ return array.filter(function (item, index) {
+ return array.indexOf(item) === index;
+ });
+ } // Get the closest value in an array
-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);
- },
+ function closest(array, value) {
+ if (!is.array(array) || !array.length) {
+ return null;
+ }
+ return array.reduce(function (prev, curr) {
+ return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
+ });
+ }
- // Toggle native HTML5 media controls
- toggleNativeControls: function toggleNativeControls() {
- var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ function cloneDeep(object) {
+ return JSON.parse(JSON.stringify(object));
+ } // Get a nested value in an object
- if (toggle && this.isHTML5) {
- this.media.setAttribute('controls', '');
- } else {
- this.media.removeAttribute('controls');
- }
- },
+ function getDeep(object, path) {
+ return path.split('.').reduce(function (obj, key) {
+ return obj && obj[key];
+ }, object);
+ } // Deep extend destination object with N more objects
+ function extend() {
+ var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
- // Setup the UI
- build: function build() {
- var _this = this;
+ for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ sources[_key - 1] = arguments[_key];
+ }
- // Re-attach media element listeners
- // TODO: Use event bubbling?
- this.listeners.media();
+ if (!sources.length) {
+ return target;
+ }
- // Don't setup interface if no support
- if (!this.supported.ui) {
- this.debug.warn('Basic support only for ' + this.provider + ' ' + this.type);
+ var source = sources.shift();
- // Restore native controls
- ui.toggleNativeControls.call(this, true);
+ if (!is.object(source)) {
+ return target;
+ }
- // Bail
- return;
+ Object.keys(source).forEach(function (key) {
+ if (is.object(source[key])) {
+ if (!Object.keys(target).includes(key)) {
+ Object.assign(target, _defineProperty({}, key, {}));
}
- // Inject custom controls if not present
- if (!utils.is.element(this.elements.controls)) {
- // Inject custom controls
- controls.inject.call(this);
+ extend(target[key], source[key]);
+ } else {
+ Object.assign(target, _defineProperty({}, key, source[key]));
+ }
+ });
+ return extend.apply(void 0, [target].concat(sources));
+ }
- // Re-attach control listeners
- this.listeners.controls();
- }
+ // ==========================================================================
- // Remove native controls
- ui.toggleNativeControls.call(this);
+ function generateId(prefix) {
+ return "".concat(prefix, "-").concat(Math.floor(Math.random() * 10000));
+ } // Format string
- // Captions
- captions.setup.call(this);
+ function format(input) {
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
+ }
- // Reset volume
- this.volume = null;
+ if (is.empty(input)) {
+ return input;
+ }
- // Reset mute state
- this.muted = null;
+ return input.toString().replace(/{(\d+)}/g, function (match, i) {
+ return args[i].toString();
+ });
+ } // Get percentage
- // Reset speed
- this.speed = null;
+ function getPercentage(current, max) {
+ if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
+ return 0;
+ }
- // Reset loop state
- this.loop = null;
+ return (current / max * 100).toFixed(2);
+ } // Replace all occurances of a string in a string
- // Reset quality setting
- this.quality = null;
+ function replaceAll() {
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
+ var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
+ return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
+ } // Convert to title case
- // Reset volume display
- ui.updateVolume.call(this);
+ function toTitleCase() {
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ return input.toString().replace(/\w\S*/g, function (text) {
+ return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
+ });
+ } // Convert string to pascalCase
- // Reset time display
- ui.timeUpdate.call(this);
+ function toPascalCase() {
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var string = input.toString(); // Convert kebab case
- // Update the UI
- ui.checkPlaying.call(this);
+ string = replaceAll(string, '-', ' '); // Convert snake case
- // Check for picture-in-picture support
- utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
+ string = replaceAll(string, '_', ' '); // Convert to title case
- // Check for airplay support
- utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
+ string = toTitleCase(string); // Convert to pascal case
- // Add iOS class
- utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
+ return replaceAll(string, ' ', '');
+ } // Convert string to pascalCase
- // Add touch class
- utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
+ function toCamelCase() {
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var string = input.toString(); // Convert to pascal case
- // Ready for API calls
- this.ready = true;
+ string = toPascalCase(string); // Convert first character to lowercase
- // Ready event at end of execution stack
- setTimeout(function () {
- utils.dispatchEvent.call(_this, _this.media, 'ready');
- }, 0);
+ return string.charAt(0).toLowerCase() + string.slice(1);
+ } // Remove HTML from a string
- // Set the title
- ui.setTitle.call(this);
+ function stripHTML(source) {
+ var fragment = document.createDocumentFragment();
+ var element = document.createElement('div');
+ fragment.appendChild(element);
+ element.innerHTML = source;
+ return fragment.firstChild.innerText;
+ } // Like outerHTML, but also works for DocumentFragment
- // Set the poster image
- ui.setPoster.call(this);
- },
+ function getHTML(element) {
+ var wrapper = document.createElement('div');
+ wrapper.appendChild(element);
+ return wrapper.innerHTML;
+ }
+ var resources = {
+ pip: 'PIP',
+ airplay: 'AirPlay',
+ html5: 'HTML5',
+ vimeo: 'Vimeo',
+ youtube: 'YouTube'
+ };
+ var i18n = {
+ get: function get() {
+ var key = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- // Setup aria attribute for play and iframe title
- setTitle: function setTitle() {
- // Find the current text
- var label = i18n.get('play', this.config);
+ if (is.empty(key) || is.empty(config)) {
+ return '';
+ }
- // 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;
+ var string = getDeep(config.i18n, key);
- // Set container label
- this.elements.container.setAttribute('aria-label', this.config.title);
+ if (is.empty(string)) {
+ if (Object.keys(resources).includes(key)) {
+ return resources[key];
}
- // 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);
- });
- }
+ return '';
+ }
- // Set iframe title
- // https://github.com/sampotts/plyr/issues/124
- if (this.isEmbed) {
- var iframe = utils.getElement.call(this, 'iframe');
+ var replace = {
+ '{seektime}': config.seekTime,
+ '{title}': config.title
+ };
+ Object.entries(replace).forEach(function (_ref) {
+ var _ref2 = _slicedToArray(_ref, 2),
+ key = _ref2[0],
+ value = _ref2[1];
+
+ string = replaceAll(string, key, value);
+ });
+ return string;
+ }
+ };
- if (!utils.is.element(iframe)) {
- return;
- }
+ var Storage =
+ /*#__PURE__*/
+ function () {
+ function Storage(player) {
+ _classCallCheck(this, Storage);
- // Default to media type
- var title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
- var format = i18n.get('frameTitle', this.config);
+ this.enabled = player.config.storage.enabled;
+ this.key = player.config.storage.key;
+ } // Check for actual support (see if we can use it)
- iframe.setAttribute('title', format.replace('{title}', title));
+
+ _createClass(Storage, [{
+ key: "get",
+ value: function get(key) {
+ if (!Storage.supported || !this.enabled) {
+ return null;
}
- },
+ var store = window.localStorage.getItem(this.key);
- // Set the poster image
- setPoster: function setPoster() {
- if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) {
- return;
+ if (is.empty(store)) {
+ return null;
}
- // Set the inline style
- var posters = this.poster.split(',');
- this.elements.poster.style.backgroundImage = posters.map(function (p) {
- return 'url(\'' + p + '\')';
- }).join(',');
- },
+ var json = JSON.parse(store);
+ return is.string(key) && key.length ? json[key] : json;
+ }
+ }, {
+ key: "set",
+ value: function set(object) {
+ // Bail if we don't have localStorage support or it's disabled
+ if (!Storage.supported || !this.enabled) {
+ return;
+ } // Can only store objectst
- // Check playing state
- checkPlaying: function checkPlaying(event) {
- // Class hooks
- utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
- utils.toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
- utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);
+ if (!is.object(object)) {
+ return;
+ } // Get current storage
- // Set ARIA state
- utils.toggleState(this.elements.buttons.play, this.playing);
- // Only update controls on non timeupdate events
- if (utils.is.event(event) && event.type === 'timeupdate') {
- return;
- }
+ var storage = this.get(); // Default to empty object
- // Toggle controls
- this.toggleControls(!this.playing);
- },
+ if (is.empty(storage)) {
+ storage = {};
+ } // Update the working copy of the values
- // Check if media is loading
- checkLoading: function checkLoading(event) {
- var _this2 = this;
+ extend(storage, object); // Update storage
- this.loading = ['stalled', 'waiting'].includes(event.type);
+ window.localStorage.setItem(this.key, JSON.stringify(storage));
+ }
+ }], [{
+ key: "supported",
+ get: function get() {
+ try {
+ if (!('localStorage' in window)) {
+ return false;
+ }
- // Clear timer
- clearTimeout(this.timers.loading);
+ var test = '___test'; // Try to use it (it might be disabled, e.g. user is in private mode)
+ // see: https://github.com/sampotts/plyr/issues/131
- // 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);
+ window.localStorage.setItem(test, test);
+ window.localStorage.removeItem(test);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+ }]);
- // Show controls if loading, hide if done
- _this2.toggleControls(_this2.loading);
- }, this.loading ? 250 : 0);
- },
+ return Storage;
+ }();
+
+ // ==========================================================================
+ // Fetch wrapper
+ // Using XHR to avoid issues with older browsers
+ // ==========================================================================
+ function fetch(url) {
+ var responseType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'text';
+ return new Promise(function (resolve, reject) {
+ try {
+ var request = new XMLHttpRequest(); // Check for CORS support
+ if (!('withCredentials' in request)) {
+ return;
+ }
- // Check if media failed to load
- checkFailed: function checkFailed() {
- var _this3 = this;
+ request.addEventListener('load', function () {
+ if (responseType === 'text') {
+ try {
+ resolve(JSON.parse(request.responseText));
+ } catch (e) {
+ resolve(request.responseText);
+ }
+ } else {
+ resolve(request.response);
+ }
+ });
+ request.addEventListener('error', function () {
+ throw new Error(request.status);
+ });
+ request.open('GET', url, true); // Set the required response type
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
- this.failed = this.media.networkState === 3;
+ request.responseType = responseType;
+ request.send();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- 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);
+ function loadSprite(url, id) {
+ if (!is.string(url)) {
+ return;
+ }
- // 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);
+ var prefix = 'cache';
+ var hasId = is.string(id);
+ var isCached = false;
- // Show controls if loading, hide if done
- _this3.toggleControls(_this3.loading);
- }, this.loading ? 250 : 0);
- },
+ var exists = function exists() {
+ return document.getElementById(id) !== null;
+ };
+ var update = function update(container, data) {
+ container.innerHTML = data; // Check again incase of race condition
- // Update volume UI and storage
- updateVolume: function updateVolume() {
- if (!this.supported.ui) {
- return;
- }
+ if (hasId && exists()) {
+ return;
+ } // Inject the SVG to the body
- // 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);
- }
- },
+ document.body.insertAdjacentElement('afterbegin', container);
+ }; // Only load once if ID set
- // Update seek value and lower fill
- setRange: function setRange(target) {
- var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+ if (!hasId || !exists()) {
+ var useStorage = Storage.supported; // Create container
- if (!utils.is.element(target)) {
- return;
- }
+ var container = document.createElement('div');
+ container.setAttribute('hidden', '');
- // eslint-disable-next-line
- target.value = value;
+ if (hasId) {
+ container.setAttribute('id', id);
+ } // Check in cache
- // Webkit range fill
- controls.updateRangeFill.call(this, target);
- },
+ if (useStorage) {
+ var cached = window.localStorage.getItem("".concat(prefix, "-").concat(id));
+ isCached = cached !== null;
- // Set <progress> 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;
+ if (isCached) {
+ var data = JSON.parse(cached);
+ update(container, data.content);
+ }
+ } // Get the sprite
- // 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;
- }
+ fetch(url).then(function (result) {
+ if (is.empty(result)) {
+ return;
}
- },
-
- // Update <progress> elements
- updateProgress: function updateProgress(event) {
- if (!this.supported.ui || !utils.is.event(event)) {
- return;
+ if (useStorage) {
+ window.localStorage.setItem("".concat(prefix, "-").concat(id), JSON.stringify({
+ content: result
+ }));
}
- var value = 0;
+ update(container, result);
+ }).catch(function () {});
+ }
+ }
- if (event) {
- switch (event.type) {
- // Video playing
- case 'timeupdate':
- case 'seeking':
- value = utils.getPercentage(this.currentTime, this.duration);
+ // ==========================================================================
- // Set seek range value only if it's a 'natural' time event
- if (event.type === 'timeupdate') {
- ui.setRange.call(this, this.elements.inputs.seek, value);
- }
+ var getHours = function getHours(value) {
+ return Math.trunc(value / 60 / 60 % 60, 10);
+ };
+ var getMinutes = function getMinutes(value) {
+ return Math.trunc(value / 60 % 60, 10);
+ };
+ var getSeconds = function getSeconds(value) {
+ return Math.trunc(value % 60, 10);
+ }; // Format time to UI friendly string
- break;
+ function formatTime() {
+ var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ var displayHours = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
- // Check buffer status
- case 'playing':
- case 'progress':
- ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
+ // Bail if the value isn't a number
+ if (!is.number(time)) {
+ return formatTime(null, displayHours, inverted);
+ } // Format time component to add leading zero
- break;
- default:
- break;
- }
- }
- },
+ var format = function format(value) {
+ return "0".concat(value).slice(-2);
+ }; // Breakdown to hours, mins, secs
- // Update the displayed time
- updateTimeDisplay: function updateTimeDisplay() {
- var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
- var time = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
- var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ var hours = getHours(time);
+ var mins = getMinutes(time);
+ var secs = getSeconds(time); // Do we need to display hours?
- // Bail if there's no element to display or the value isn't a number
- if (!utils.is.element(target) || !utils.is.number(time)) {
- return;
- }
+ if (displayHours || hours > 0) {
+ hours = "".concat(hours, ":");
+ } else {
+ hours = '';
+ } // Render
- // Always display hours if duration is over an hour
- var forceHours = utils.getHours(this.duration) > 0;
- // eslint-disable-next-line no-param-reassign
- target.textContent = utils.formatTime(time, forceHours, inverted);
+ return "".concat(inverted && time > 0 ? '-' : '').concat(hours).concat(format(mins), ":").concat(format(secs));
+ }
+
+ var controls = {
+ // Get icon URL
+ getIconUrl: function getIconUrl() {
+ var url = new URL(this.config.iconUrl, window.location);
+ var cors = url.host !== window.location.host || browser.isIE && !window.svg4everybody;
+ return {
+ url: this.config.iconUrl,
+ cors: cors
+ };
+ },
+ // Find the UI controls
+ findElements: function findElements() {
+ try {
+ this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); // Buttons
+
+ this.elements.buttons = {
+ play: getElements.call(this, this.config.selectors.buttons.play),
+ pause: getElement.call(this, this.config.selectors.buttons.pause),
+ restart: getElement.call(this, this.config.selectors.buttons.restart),
+ rewind: getElement.call(this, this.config.selectors.buttons.rewind),
+ fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),
+ mute: getElement.call(this, this.config.selectors.buttons.mute),
+ pip: getElement.call(this, this.config.selectors.buttons.pip),
+ airplay: getElement.call(this, this.config.selectors.buttons.airplay),
+ settings: getElement.call(this, this.config.selectors.buttons.settings),
+ captions: getElement.call(this, this.config.selectors.buttons.captions),
+ fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)
+ }; // Progress
+
+ this.elements.progress = getElement.call(this, this.config.selectors.progress); // Inputs
+
+ this.elements.inputs = {
+ seek: getElement.call(this, this.config.selectors.inputs.seek),
+ volume: getElement.call(this, this.config.selectors.inputs.volume)
+ }; // Display
+
+ this.elements.display = {
+ buffer: getElement.call(this, this.config.selectors.display.buffer),
+ currentTime: getElement.call(this, this.config.selectors.display.currentTime),
+ duration: getElement.call(this, this.config.selectors.display.duration)
+ }; // Seek tooltip
+
+ if (is.element(this.elements.progress)) {
+ this.elements.display.seekTooltip = this.elements.progress.querySelector(".".concat(this.config.classNames.tooltip));
+ }
+
+ return true;
+ } catch (error) {
+ // Log it
+ this.debug.warn('It looks like there is a problem with your custom controls HTML', error); // Restore native video controls
+
+ this.toggleNativeControls(true);
+ return false;
+ }
},
+ // Create <svg> icon
+ createIcon: function createIcon(type, attributes) {
+ var namespace = 'http://www.w3.org/2000/svg';
+ var iconUrl = controls.getIconUrl.call(this);
+ var iconPath = "".concat(!iconUrl.cors ? iconUrl.url : '', "#").concat(this.config.iconPrefix); // Create <svg>
+ var icon = document.createElementNS(namespace, 'svg');
+ setAttributes(icon, extend(attributes, {
+ role: 'presentation',
+ focusable: 'false'
+ })); // Create the <use> to reference sprite
- // Handle time change event
- timeUpdate: function timeUpdate(event) {
- // Only invert if only one time element is displayed and used for both duration and currentTime
- var invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
+ var use = document.createElementNS(namespace, 'use');
+ var path = "".concat(iconPath, "-").concat(type); // Set `href` attributes
+ // https://github.com/sampotts/plyr/issues/460
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
- // Duration
- ui.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
+ if ('href' in use) {
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
+ } // Always set the older attribute even though it's "deprecated" (it'll be around for ages)
- // Ignore updates while seeking
- if (event && event.type === 'timeupdate' && this.media.seeking) {
- return;
- }
- // Playing progress
- ui.updateProgress.call(this, event);
+ use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); // Add <use> to <svg>
+
+ icon.appendChild(use);
+ return icon;
+ },
+ // Create hidden text label
+ createLabel: function createLabel(key) {
+ var attr = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var text = i18n.get(key, this.config);
+ var attributes = Object.assign({}, attr, {
+ class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')
+ });
+ return createElement('span', attributes, text);
},
+ // Create a badge
+ createBadge: function createBadge(text) {
+ if (is.empty(text)) {
+ return null;
+ }
+ var badge = createElement('span', {
+ class: this.config.classNames.menu.value
+ });
+ badge.appendChild(createElement('span', {
+ class: this.config.classNames.menu.badge
+ }, text));
+ return badge;
+ },
+ // Create a <button>
+ createButton: function createButton(buttonType, attr) {
+ var attributes = Object.assign({}, attr);
+ var type = toCamelCase(buttonType);
+ var props = {
+ element: 'button',
+ toggle: false,
+ label: null,
+ icon: null,
+ labelPressed: null,
+ iconPressed: null
+ };
+ ['element', 'icon', 'label'].forEach(function (key) {
+ if (Object.keys(attributes).includes(key)) {
+ props[key] = attributes[key];
+ delete attributes[key];
+ }
+ }); // Default to 'button' type to prevent form submission
+
+ if (props.element === 'button' && !Object.keys(attributes).includes('type')) {
+ attributes.type = 'button';
+ } // Set class name
+
+
+ if (Object.keys(attributes).includes('class')) {
+ if (!attributes.class.includes(this.config.classNames.control)) {
+ attributes.class += " ".concat(this.config.classNames.control);
+ }
+ } else {
+ attributes.class = this.config.classNames.control;
+ } // Large play button
+
+
+ switch (buttonType) {
+ case 'play':
+ props.toggle = true;
+ props.label = 'play';
+ props.labelPressed = 'pause';
+ props.icon = 'play';
+ props.iconPressed = 'pause';
+ break;
+
+ case 'mute':
+ props.toggle = true;
+ props.label = 'mute';
+ props.labelPressed = 'unmute';
+ props.icon = 'volume';
+ props.iconPressed = 'muted';
+ break;
+
+ case 'captions':
+ props.toggle = true;
+ props.label = 'enableCaptions';
+ props.labelPressed = 'disableCaptions';
+ props.icon = 'captions-off';
+ props.iconPressed = 'captions-on';
+ break;
+
+ case 'fullscreen':
+ props.toggle = true;
+ props.label = 'enterFullscreen';
+ props.labelPressed = 'exitFullscreen';
+ props.icon = 'enter-fullscreen';
+ props.iconPressed = 'exit-fullscreen';
+ break;
+
+ case 'play-large':
+ attributes.class += " ".concat(this.config.classNames.control, "--overlaid");
+ type = 'play';
+ props.label = 'play';
+ props.icon = 'play';
+ break;
- // Show the duration on metadataloaded
- durationUpdate: function durationUpdate() {
- if (!this.supported.ui) {
- return;
- }
+ default:
+ if (is.empty(props.label)) {
+ props.label = type;
+ }
- // If there's a spot to display duration
- var hasDuration = utils.is.element(this.elements.display.duration);
+ if (is.empty(props.icon)) {
+ props.icon = buttonType;
+ }
- // If there's only one time display, display duration there
- if (!hasDuration && this.config.displayDuration && this.paused) {
- ui.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
- }
+ }
- // If there's a duration element, update content
- if (hasDuration) {
- ui.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
- }
+ var button = createElement(props.element); // Setup toggle icon and labels
- // Update the tooltip (if visible)
- controls.updateSeekTooltip.call(this);
- }
-};
+ if (props.toggle) {
+ // Icon
+ button.appendChild(controls.createIcon.call(this, props.iconPressed, {
+ class: 'icon--pressed'
+ }));
+ button.appendChild(controls.createIcon.call(this, props.icon, {
+ class: 'icon--not-pressed'
+ })); // Label/Tooltip
-// ==========================================================================
+ button.appendChild(controls.createLabel.call(this, props.labelPressed, {
+ class: 'label--pressed'
+ }));
+ button.appendChild(controls.createLabel.call(this, props.label, {
+ class: 'label--not-pressed'
+ }));
+ } else {
+ button.appendChild(controls.createIcon.call(this, props.icon));
+ button.appendChild(controls.createLabel.call(this, props.label));
+ } // Merge and set attributes
-// Sniff out the browser
-var browser$1 = utils.getBrowser();
-var controls = {
- // Webkit polyfill for lower fill range
- updateRangeFill: function updateRangeFill(target) {
- // Get range from event if event passed
- var range = utils.is.event(target) ? target.target : target;
+ extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
+ setAttributes(button, attributes); // We have multiple play buttons
- // Needs to be a valid <input type='range'>
- if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
- return;
+ if (type === 'play') {
+ if (!is.array(this.elements.buttons[type])) {
+ this.elements.buttons[type] = [];
}
- // Set aria value for https://github.com/sampotts/plyr/issues/905
- range.setAttribute('aria-valuenow', range.value);
+ this.elements.buttons[type].push(button);
+ } else {
+ this.elements.buttons[type] = button;
+ }
- // WebKit only
- if (!browser$1.isWebkit) {
- return;
- }
+ return button;
+ },
+ // Create an <input type='range'>
+ createRange: function createRange(type, attributes) {
+ // Seek input
+ var input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {
+ type: 'range',
+ min: 0,
+ max: 100,
+ step: 0.01,
+ value: 0,
+ autocomplete: 'off',
+ // A11y fixes for https://github.com/sampotts/plyr/issues/905
+ role: 'slider',
+ 'aria-label': i18n.get(type, this.config),
+ 'aria-valuemin': 0,
+ 'aria-valuemax': 100,
+ 'aria-valuenow': 0
+ }, attributes));
+ this.elements.inputs[type] = input; // Set the fill for webkit now
+
+ controls.updateRangeFill.call(this, input);
+ return input;
+ },
+ // Create a <progress>
+ createProgress: function createProgress(type, attributes) {
+ var progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {
+ min: 0,
+ max: 100,
+ value: 0,
+ role: 'presentation',
+ 'aria-hidden': true
+ }, attributes)); // Create the label inside
+
+ if (type !== 'volume') {
+ progress.appendChild(createElement('span', null, '0'));
+ var suffixKey = {
+ played: 'played',
+ buffer: 'buffered'
+ }[type];
+ var suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';
+ progress.innerText = "% ".concat(suffix.toLowerCase());
+ }
- // Set CSS custom property
- range.style.setProperty('--value', range.value / range.max * 100 + '%');
+ this.elements.display[type] = progress;
+ return progress;
+ },
+ // Create time display
+ createTime: function createTime(type) {
+ var attributes = getAttributesFromSelector(this.config.selectors.display[type]);
+ var container = createElement('div', extend(attributes, {
+ class: "".concat(this.config.classNames.display.time, " ").concat(attributes.class ? attributes.class : '').trim(),
+ 'aria-label': i18n.get(type, this.config)
+ }), '00:00'); // Reference for updates
+
+ this.elements.display[type] = container;
+ return container;
},
+ // Bind keyboard shortcuts for a menu item
+ // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
+ bindMenuItemShortcuts: function bindMenuItemShortcuts(menuItem, type) {
+ var _this = this;
+ // Navigate through menus via arrow keys and space
+ on(menuItem, 'keydown keyup', function (event) {
+ // We only care about space and ⬆️ ⬇️️ ➡️
+ if (![32, 38, 39, 40].includes(event.which)) {
+ return;
+ } // Prevent play / seek
- // Get icon URL
- getIconUrl: function getIconUrl() {
- var url = new URL(this.config.iconUrl, window.location);
- var cors = url.host !== window.location.host || browser$1.isIE && !window.svg4everybody;
- return {
- url: this.config.iconUrl,
- cors: cors
- };
- },
+ event.preventDefault();
+ event.stopPropagation(); // We're just here to prevent the keydown bubbling
+ if (event.type === 'keydown') {
+ return;
+ }
- // 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)
- };
+ var isRadioButton = matches(menuItem, '[role="menuitemradio"]'); // Show the respective menu
- // Progress
- this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
+ if (!isRadioButton && [32, 39].includes(event.which)) {
+ controls.showMenuPanel.call(_this, type, true);
+ } else {
+ var target;
- // Inputs
- this.elements.inputs = {
- seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
- volume: utils.getElement.call(this, this.config.selectors.inputs.volume)
- };
+ if (event.which !== 32) {
+ if (event.which === 40 || isRadioButton && event.which === 39) {
+ target = menuItem.nextElementSibling;
- // 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)
- };
+ if (!is.element(target)) {
+ target = menuItem.parentNode.firstElementChild;
+ }
+ } else {
+ target = menuItem.previousElementSibling;
- // Seek tooltip
- if (utils.is.element(this.elements.progress)) {
- this.elements.display.seekTooltip = this.elements.progress.querySelector('.' + this.config.classNames.tooltip);
+ if (!is.element(target)) {
+ target = menuItem.parentNode.lastElementChild;
+ }
}
- 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);
+ setFocus.call(_this, target, true);
+ }
+ }
+ }, false); // Enter will fire a `click` event but we still need to manage focus
+ // So we bind to keyup which fires after and set focus here
- return false;
+ on(menuItem, 'keyup', function (event) {
+ if (event.which !== 13) {
+ return;
}
- },
+ controls.focusFirstMenuItem.call(_this, null, true);
+ });
+ },
+ // Create a settings menu item
+ createMenuItem: function createMenuItem(_ref) {
+ var _this2 = this;
+
+ var value = _ref.value,
+ list = _ref.list,
+ type = _ref.type,
+ title = _ref.title,
+ _ref$badge = _ref.badge,
+ badge = _ref$badge === void 0 ? null : _ref$badge,
+ _ref$checked = _ref.checked,
+ checked = _ref$checked === void 0 ? false : _ref$checked;
+ var attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);
+ var menuItem = createElement('button', extend(attributes, {
+ type: 'button',
+ role: 'menuitemradio',
+ class: "".concat(this.config.classNames.control, " ").concat(attributes.class ? attributes.class : '').trim(),
+ 'aria-checked': checked,
+ value: value
+ }));
+ var flex = createElement('span'); // We have to set as HTML incase of special characters
+
+ flex.innerHTML = title;
+
+ if (is.element(badge)) {
+ flex.appendChild(badge);
+ }
- // Create <svg> icon
- createIcon: function createIcon(type, attributes) {
- var namespace = 'http://www.w3.org/2000/svg';
- var iconUrl = controls.getIconUrl.call(this);
- var iconPath = (!iconUrl.cors ? iconUrl.url : '') + '#' + this.config.iconPrefix;
-
- // Create <svg>
- var icon = document.createElementNS(namespace, 'svg');
- utils.setAttributes(icon, utils.extend(attributes, {
- role: 'presentation',
- focusable: 'false'
- }));
+ menuItem.appendChild(flex); // Replicate radio button behaviour
- // Create the <use> to reference sprite
- var use = document.createElementNS(namespace, 'use');
- var path = iconPath + '-' + type;
+ Object.defineProperty(menuItem, 'checked', {
+ enumerable: true,
+ get: function get() {
+ return menuItem.getAttribute('aria-checked') === 'true';
+ },
+ set: function set(checked) {
+ // Ensure exclusivity
+ if (checked) {
+ Array.from(menuItem.parentNode.children).filter(function (node) {
+ return matches(node, '[role="menuitemradio"]');
+ }).forEach(function (node) {
+ return node.setAttribute('aria-checked', 'false');
+ });
+ }
- // Set `href` attributes
- // https://github.com/sampotts/plyr/issues/460
- // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href
- if ('href' in use) {
- use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);
- } else {
- use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);
+ menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
+ }
+ });
+ this.listeners.bind(menuItem, 'click keyup', function (event) {
+ if (is.keyboardEvent(event) && event.which !== 32) {
+ return;
}
- // Add <use> to <svg>
- icon.appendChild(use);
-
- return icon;
- },
-
-
- // Create hidden text label
- createLabel: function createLabel(type, attr) {
- var text = i18n.get(type, this.config);
- var attributes = Object.assign({}, attr);
+ event.preventDefault();
+ event.stopPropagation();
+ menuItem.checked = true;
switch (type) {
- case 'pip':
- text = 'PIP';
- break;
+ case 'language':
+ _this2.currentTrack = Number(value);
+ break;
- case 'airplay':
- text = 'AirPlay';
- break;
+ case 'quality':
+ _this2.quality = value;
+ break;
- default:
- break;
- }
+ case 'speed':
+ _this2.speed = parseFloat(value);
+ break;
- if ('class' in attributes) {
- attributes.class += ' ' + this.config.classNames.hidden;
- } else {
- attributes.class = this.config.classNames.hidden;
+ default:
+ break;
}
- return utils.createElement('span', attributes, text);
+ controls.showMenuPanel.call(_this2, 'home', is.keyboardEvent(event));
+ }, type, false);
+ controls.bindMenuItemShortcuts.call(this, menuItem, type);
+ list.appendChild(menuItem);
},
+ // Format a time for display
+ formatTime: function formatTime$$1() {
+ var time = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ var inverted = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ // Bail if the value isn't a number
+ if (!is.number(time)) {
+ return time;
+ } // Always display hours if duration is over an hour
- // Create a badge
- createBadge: function createBadge(text) {
- if (utils.is.empty(text)) {
- return null;
- }
-
- var badge = utils.createElement('span', {
- class: this.config.classNames.menu.value
- });
-
- badge.appendChild(utils.createElement('span', {
- class: this.config.classNames.menu.badge
- }, text));
- return badge;
+ var forceHours = getHours(this.duration) > 0;
+ return formatTime(time, forceHours, inverted);
},
+ // Update the displayed time
+ updateTimeDisplay: function updateTimeDisplay() {
+ var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+ var time = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+ var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+ // Bail if there's no element to display or the value isn't a number
+ if (!is.element(target) || !is.number(time)) {
+ return;
+ } // eslint-disable-next-line no-param-reassign
- // Create a <button>
- createButton: function createButton(buttonType, attr) {
- var button = utils.createElement('button');
- var attributes = Object.assign({}, attr);
- var type = utils.toCamelCase(buttonType);
-
- var toggle = false;
- var label = void 0;
- var icon = void 0;
- var labelPressed = void 0;
- var iconPressed = void 0;
-
- if (!('type' in attributes)) {
- attributes.type = 'button';
- }
- if ('class' in attributes) {
- if (attributes.class.includes(this.config.classNames.control)) {
- attributes.class += ' ' + this.config.classNames.control;
- }
- } else {
- attributes.class = this.config.classNames.control;
- }
+ target.innerText = controls.formatTime(time, inverted);
+ },
+ // Update volume UI and storage
+ updateVolume: function updateVolume() {
+ if (!this.supported.ui) {
+ return;
+ } // Update range
- // Large play button
- switch (buttonType) {
- case 'play':
- toggle = true;
- label = 'play';
- labelPressed = 'pause';
- icon = 'play';
- iconPressed = 'pause';
- break;
- case 'mute':
- toggle = true;
- label = 'mute';
- labelPressed = 'unmute';
- icon = 'volume';
- iconPressed = 'muted';
- break;
+ if (is.element(this.elements.inputs.volume)) {
+ controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
+ } // Update mute state
- case 'captions':
- toggle = true;
- label = 'enableCaptions';
- labelPressed = 'disableCaptions';
- icon = 'captions-off';
- iconPressed = 'captions-on';
- break;
- case 'fullscreen':
- toggle = true;
- label = 'enterFullscreen';
- labelPressed = 'exitFullscreen';
- icon = 'enter-fullscreen';
- iconPressed = 'exit-fullscreen';
- break;
+ if (is.element(this.elements.buttons.mute)) {
+ this.elements.buttons.mute.pressed = this.muted || this.volume === 0;
+ }
+ },
+ // Update seek value and lower fill
+ setRange: function setRange(target) {
+ var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
- case 'play-large':
- attributes.class += ' ' + this.config.classNames.control + '--overlaid';
- type = 'play';
- label = 'play';
- icon = 'play';
- break;
+ if (!is.element(target)) {
+ return;
+ } // eslint-disable-next-line
- default:
- label = type;
- icon = buttonType;
- }
- // Setup toggle icon and labels
- if (toggle) {
- // Icon
- button.appendChild(controls.createIcon.call(this, iconPressed, { class: 'icon--pressed' }));
- button.appendChild(controls.createIcon.call(this, icon, { class: 'icon--not-pressed' }));
+ target.value = value; // Webkit range fill
- // Label/Tooltip
- button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
- button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
+ controls.updateRangeFill.call(this, target);
+ },
+ // Update <progress> elements
+ updateProgress: function updateProgress(event) {
+ var _this3 = this;
- // Add aria attributes
- attributes['aria-pressed'] = false;
- } else {
- button.appendChild(controls.createIcon.call(this, icon));
- button.appendChild(controls.createLabel.call(this, label));
- }
+ if (!this.supported.ui || !is.event(event)) {
+ return;
+ }
- // Merge attributes
- utils.extend(attributes, utils.getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
+ var value = 0;
- utils.setAttributes(button, attributes);
+ var setProgress = function setProgress(target, input) {
+ var value = is.number(input) ? input : 0;
+ var progress = is.element(target) ? target : _this3.elements.display.buffer; // Update value and label
- // We have multiple play buttons
- if (type === 'play') {
- if (!utils.is.array(this.elements.buttons[type])) {
- this.elements.buttons[type] = [];
- }
+ if (is.element(progress)) {
+ progress.value = value; // Update text label inside
- this.elements.buttons[type].push(button);
- } else {
- this.elements.buttons[type] = button;
+ var label = progress.getElementsByTagName('span')[0];
+
+ if (is.element(label)) {
+ label.childNodes[0].nodeValue = value;
+ }
}
+ };
- return button;
- },
+ if (event) {
+ switch (event.type) {
+ // Video playing
+ case 'timeupdate':
+ case 'seeking':
+ case 'seeked':
+ value = getPercentage(this.currentTime, this.duration); // Set seek range value only if it's a 'natural' time event
+ if (event.type === 'timeupdate') {
+ controls.setRange.call(this, this.elements.inputs.seek, value);
+ }
- // Create an <input type='range'>
- createRange: function createRange(type, attributes) {
- // Seek label
- var label = utils.createElement('label', {
- for: attributes.id,
- id: attributes.id + '-label',
- class: this.config.classNames.hidden
- }, i18n.get(type, this.config));
-
- // Seek input
- var input = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs[type]), {
- type: 'range',
- min: 0,
- max: 100,
- step: 0.01,
- value: 0,
- autocomplete: 'off',
- // A11y fixes for https://github.com/sampotts/plyr/issues/905
- role: 'slider',
- 'aria-labelledby': attributes.id + '-label',
- 'aria-valuemin': 0,
- 'aria-valuemax': 100,
- 'aria-valuenow': 0
- }, attributes));
-
- this.elements.inputs[type] = input;
-
- // Set the fill for webkit now
- controls.updateRangeFill.call(this, input);
+ break;
+ // Check buffer status
- return {
- label: label,
- input: input
- };
+ case 'playing':
+ case 'progress':
+ setProgress(this.elements.display.buffer, this.buffered * 100);
+ break;
+
+ default:
+ break;
+ }
+ }
},
+ // Webkit polyfill for lower fill range
+ updateRangeFill: function updateRangeFill(target) {
+ // Get range from event if event passed
+ var range = is.event(target) ? target.target : target; // Needs to be a valid <input type='range'>
+ if (!is.element(range) || range.getAttribute('type') !== 'range') {
+ return;
+ } // Set aria values for https://github.com/sampotts/plyr/issues/905
+
+
+ if (matches(range, this.config.selectors.inputs.seek)) {
+ range.setAttribute('aria-valuenow', this.currentTime);
+ var currentTime = controls.formatTime(this.currentTime);
+ var duration = controls.formatTime(this.duration);
+ var format$$1 = i18n.get('seekLabel', this.config);
+ range.setAttribute('aria-valuetext', format$$1.replace('{currentTime}', currentTime).replace('{duration}', duration));
+ } else if (matches(range, this.config.selectors.inputs.volume)) {
+ var percent = range.value * 100;
+ range.setAttribute('aria-valuenow', percent);
+ range.setAttribute('aria-valuetext', "".concat(percent.toFixed(1), "%"));
+ } else {
+ range.setAttribute('aria-valuenow', range.value);
+ } // WebKit only
- // Create a <progress>
- createProgress: function createProgress(type, attributes) {
- var progress = utils.createElement('progress', utils.extend(utils.getAttributesFromSelector(this.config.selectors.display[type]), {
- min: 0,
- max: 100,
- value: 0,
- role: 'presentation',
- 'aria-hidden': true
- }, attributes));
- // Create the label inside
- if (type !== 'volume') {
- progress.appendChild(utils.createElement('span', null, '0'));
+ if (!browser.isWebkit) {
+ return;
+ } // Set CSS custom property
- var suffix = '';
- switch (type) {
- case 'played':
- suffix = i18n.get('played', this.config);
- break;
- case 'buffer':
- suffix = i18n.get('buffered', this.config);
- break;
+ range.style.setProperty('--value', "".concat(range.value / range.max * 100, "%"));
+ },
+ // Update hover tooltip for seeking
+ updateSeekTooltip: function updateSeekTooltip(event) {
+ var _this4 = this;
- default:
- break;
- }
+ // Bail if setting not true
+ if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {
+ return;
+ } // Calculate percentage
- progress.textContent = '% ' + suffix.toLowerCase();
- }
- this.elements.display[type] = progress;
+ var percent = 0;
+ var clientRect = this.elements.progress.getBoundingClientRect();
+ var visible = "".concat(this.config.classNames.tooltip, "--visible");
- return progress;
- },
+ var toggle = function toggle(_toggle) {
+ toggleClass(_this4.elements.display.seekTooltip, visible, _toggle);
+ }; // Hide on touch
- // Create time display
- createTime: function createTime(type) {
- var attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
+ if (this.touch) {
+ toggle(false);
+ return;
+ } // Determine percentage, if already visible
- var container = utils.createElement('div', utils.extend(attributes, {
- class: 'plyr__time ' + attributes.class,
- 'aria-label': i18n.get(type, this.config)
- }), '00:00');
- // Reference for updates
- this.elements.display[type] = container;
+ if (is.event(event)) {
+ percent = 100 / clientRect.width * (event.pageX - clientRect.left);
+ } else if (hasClass(this.elements.display.seekTooltip, visible)) {
+ percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
+ } else {
+ return;
+ } // Set bounds
- return container;
- },
+ if (percent < 0) {
+ percent = 0;
+ } else if (percent > 100) {
+ percent = 100;
+ } // Display the time a click would seek to
- // Create a settings menu item
- createMenuItem: function createMenuItem(value, list, type, title) {
- var badge = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
- var checked = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
- var item = utils.createElement('li');
+ controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position
- var label = utils.createElement('label', {
- class: this.config.classNames.control
- });
+ this.elements.display.seekTooltip.style.left = "".concat(percent, "%"); // Show/hide the tooltip
+ // If the event is a moues in/out and percentage is inside bounds
- var radio = utils.createElement('input', utils.extend(utils.getAttributesFromSelector(this.config.selectors.inputs[type]), {
- type: 'radio',
- name: 'plyr-' + type,
- value: value,
- checked: checked,
- class: 'plyr__sr-only'
- }));
+ if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
+ toggle(event.type === 'mouseenter');
+ }
+ },
+ // Handle time change event
+ timeUpdate: function timeUpdate(event) {
+ // Only invert if only one time element is displayed and used for both duration and currentTime
+ var invert = !is.element(this.elements.display.duration) && this.config.invertTime; // Duration
- var faux = utils.createElement('span', { hidden: '' });
+ controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); // Ignore updates while seeking
- label.appendChild(radio);
- label.appendChild(faux);
- label.insertAdjacentHTML('beforeend', title);
+ if (event && event.type === 'timeupdate' && this.media.seeking) {
+ return;
+ } // Playing progress
- if (utils.is.element(badge)) {
- label.appendChild(badge);
- }
- item.appendChild(label);
- list.appendChild(item);
+ controls.updateProgress.call(this, event);
},
+ // Show the duration on metadataloaded or durationchange events
+ durationUpdate: function durationUpdate() {
+ // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false
+ if (!this.supported.ui || !this.config.invertTime && this.currentTime) {
+ return;
+ } // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.
+ // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415
+ // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062
+ // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338
- // Update hover tooltip for seeking
- updateSeekTooltip: function updateSeekTooltip(event) {
- var _this = this;
+ if (this.duration >= Math.pow(2, 32)) {
+ toggleHidden(this.elements.display.currentTime, true);
+ toggleHidden(this.elements.progress, true);
+ return;
+ } // Update ARIA values
- // Bail if setting not true
- if (!this.config.tooltips.seek || !utils.is.element(this.elements.inputs.seek) || !utils.is.element(this.elements.display.seekTooltip) || this.duration === 0) {
- return;
- }
- // Calculate percentage
- var percent = 0;
- var clientRect = this.elements.inputs.seek.getBoundingClientRect();
- var visible = this.config.classNames.tooltip + '--visible';
+ if (is.element(this.elements.inputs.seek)) {
+ this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
+ } // If there's a spot to display duration
- var toggle = function toggle(_toggle) {
- utils.toggleClass(_this.elements.display.seekTooltip, visible, _toggle);
- };
- // Hide on touch
- if (this.touch) {
- toggle(false);
- return;
- }
+ var hasDuration = is.element(this.elements.display.duration); // If there's only one time display, display duration there
- // Determine percentage, if already visible
- if (utils.is.event(event)) {
- percent = 100 / clientRect.width * (event.pageX - clientRect.left);
- } else if (utils.hasClass(this.elements.display.seekTooltip, visible)) {
- percent = parseFloat(this.elements.display.seekTooltip.style.left, 10);
- } else {
- return;
- }
+ if (!hasDuration && this.config.displayDuration && this.paused) {
+ controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
+ } // If there's a duration element, update content
- // Set bounds
- if (percent < 0) {
- percent = 0;
- } else if (percent > 100) {
- percent = 100;
- }
- // Display the time a click would seek to
- ui.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
+ if (hasDuration) {
+ controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
+ } // Update the tooltip (if visible)
- // Set position
- this.elements.display.seekTooltip.style.left = percent + '%';
- // Show/hide the tooltip
- // If the event is a moues in/out and percentage is inside bounds
- if (utils.is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {
- toggle(event.type === 'mouseenter');
- }
+ controls.updateSeekTooltip.call(this);
},
-
-
// Hide/show a tab
- toggleTab: function toggleTab(setting, toggle) {
- utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
+ toggleMenuButton: function toggleMenuButton(setting, toggle) {
+ toggleHidden(this.elements.settings.buttons[setting], !toggle);
},
+ // Update the selected setting
+ updateSetting: function updateSetting(setting, container, input) {
+ var pane = this.elements.settings.panels[setting];
+ var value = null;
+ var list = container;
+ if (setting === 'captions') {
+ value = this.currentTrack;
+ } else {
+ value = !is.empty(input) ? input : this[setting]; // Get default
- // Set the quality menu
- // TODO: Vimeo support
- setQualityMenu: function setQualityMenu(options) {
- var _this2 = this;
+ if (is.empty(value)) {
+ value = this.config[setting].default;
+ } // Unsupported value
- // Menu required
- if (!utils.is.element(this.elements.settings.panes.quality)) {
- return;
- }
-
- var type = 'quality';
- var list = this.elements.settings.panes.quality.querySelector('ul');
-
- // Set options if passed and filter based on config
- if (utils.is.array(options)) {
- this.options.quality = options.filter(function (quality) {
- return _this2.config.quality.options.includes(quality);
- });
- }
- // Toggle the pane and tab
- var toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
- controls.toggleTab.call(this, type, toggle);
+ if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
+ this.debug.warn("Unsupported value of '".concat(value, "' for ").concat(setting));
+ return;
+ } // Disabled value
- // Check if we need to toggle the parent
- controls.checkMenu.call(this);
- // If we're hiding, nothing more to do
- if (!toggle) {
- return;
+ if (!this.config[setting].options.includes(value)) {
+ this.debug.warn("Disabled value of '".concat(value, "' for ").concat(setting));
+ return;
}
+ } // Get the list if we need to
- // Empty the menu
- utils.emptyElement(list);
-
- // Get the badge HTML for HD, 4K etc
- var getBadge = function getBadge(quality) {
- var label = '';
- switch (quality) {
- case 2160:
- label = '4K';
- break;
+ if (!is.element(list)) {
+ list = pane && pane.querySelector('[role="menu"]');
+ } // If there's no list it means it's not been rendered...
- case 1440:
- case 1080:
- case 720:
- label = 'HD';
- break;
- case 576:
- case 480:
- label = 'SD';
- break;
-
- default:
- break;
- }
+ if (!is.element(list)) {
+ return;
+ } // Update the label
- if (!label.length) {
- return null;
- }
- return controls.createBadge.call(_this2, label);
- };
+ var label = this.elements.settings.buttons[setting].querySelector(".".concat(this.config.classNames.menu.value));
+ label.innerHTML = controls.getLabel.call(this, setting, value); // Find the radio option and check it
- // Sort options by the config and then render options
- this.options.quality.sort(function (a, b) {
- var sorting = _this2.config.quality.options;
- return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
- }).forEach(function (quality) {
- var label = controls.getLabel.call(_this2, 'quality', quality);
- controls.createMenuItem.call(_this2, quality, list, type, label, getBadge(quality));
- });
+ var target = list && list.querySelector("[value=\"".concat(value, "\"]"));
- controls.updateSetting.call(this, type, list);
+ if (is.element(target)) {
+ target.checked = true;
+ }
},
-
-
// Translate a value into a nice label
- // TODO: Localisation
getLabel: function getLabel(setting, value) {
- switch (setting) {
- case 'speed':
- return value === 1 ? i18n.get('normal', this.config) : value + '&times;';
+ switch (setting) {
+ case 'speed':
+ return value === 1 ? i18n.get('normal', this.config) : "".concat(value, "&times;");
- case 'quality':
- if (utils.is.number(value)) {
- return value + 'p';
- }
+ case 'quality':
+ if (is.number(value)) {
+ var label = i18n.get("qualityLabel.".concat(value), this.config);
- return utils.toTitleCase(value);
+ if (!label.length) {
+ return "".concat(value, "p");
+ }
- case 'captions':
- return captions.getLabel.call(this);
+ return label;
+ }
- default:
- return null;
- }
- },
+ return toTitleCase(value);
+ case 'captions':
+ return captions.getLabel.call(this);
- // Update the selected setting
- updateSetting: function updateSetting(setting, container, input) {
- var pane = this.elements.settings.panes[setting];
- var value = null;
- var list = container;
-
- switch (setting) {
- case 'captions':
- if (this.captions.active) {
- if (this.options.captions.length > 2 || !this.options.captions.some(function (lang) {
- return lang === 'enabled';
- })) {
- value = this.captions.language;
- } else {
- value = 'enabled';
- }
- } else {
- value = '';
- }
+ default:
+ return null;
+ }
+ },
+ // Set the quality menu
+ setQualityMenu: function setQualityMenu(options) {
+ var _this5 = this;
- break;
+ // Menu required
+ if (!is.element(this.elements.settings.panels.quality)) {
+ return;
+ }
- default:
- value = !utils.is.empty(input) ? input : this[setting];
+ var type = 'quality';
+ var list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); // Set options if passed and filter based on uniqueness and config
- // Get default
- if (utils.is.empty(value)) {
- value = this.config[setting].default;
- }
+ if (is.array(options)) {
+ this.options.quality = dedupe(options).filter(function (quality) {
+ return _this5.config.quality.options.includes(quality);
+ });
+ } // Toggle the pane and tab
- // Unsupported value
- if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
- this.debug.warn('Unsupported value of \'' + value + '\' for ' + setting);
- return;
- }
- // Disabled value
- if (!this.config[setting].options.includes(value)) {
- this.debug.warn('Disabled value of \'' + value + '\' for ' + setting);
- return;
- }
+ var toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;
+ controls.toggleMenuButton.call(this, type, toggle); // Empty the menu
- break;
- }
+ emptyElement(list); // Check if we need to toggle the parent
- // Get the list if we need to
- if (!utils.is.element(list)) {
- list = pane && pane.querySelector('ul');
- }
+ controls.checkMenu.call(this); // If we're hiding, nothing more to do
- // If there's no list it means it's not been rendered...
- if (!utils.is.element(list)) {
- return;
- }
+ if (!toggle) {
+ return;
+ } // Get the badge HTML for HD, 4K etc
- // Update the label
- var label = this.elements.settings.tabs[setting].querySelector('.' + this.config.classNames.menu.value);
- label.innerHTML = controls.getLabel.call(this, setting, value);
- // Find the radio option and check it
- var target = list && list.querySelector('input[value="' + value + '"]');
+ var getBadge = function getBadge(quality) {
+ var label = i18n.get("qualityBadge.".concat(quality), _this5.config);
- if (utils.is.element(target)) {
- target.checked = true;
+ if (!label.length) {
+ return null;
}
- },
+
+ return controls.createBadge.call(_this5, label);
+ }; // Sort options by the config and then render options
+ this.options.quality.sort(function (a, b) {
+ var sorting = _this5.config.quality.options;
+ return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
+ }).forEach(function (quality) {
+ controls.createMenuItem.call(_this5, {
+ value: quality,
+ list: list,
+ type: type,
+ title: controls.getLabel.call(_this5, 'quality', quality),
+ badge: getBadge(quality)
+ });
+ });
+ controls.updateSetting.call(this, type, list);
+ },
// Set the looping options
+
/* setLoopMenu() {
// Menu required
- if (!utils.is.element(this.elements.settings.panes.loop)) {
+ if (!is.element(this.elements.settings.panels.loop)) {
return;
}
const options = ['start', 'end', 'all', 'reset'];
- const list = this.elements.settings.panes.loop.querySelector('ul');
+ const list = this.elements.settings.panels.loop.querySelector('[role="menu"]');
// Show the pane and tab
- utils.toggleHidden(this.elements.settings.tabs.loop, false);
- utils.toggleHidden(this.elements.settings.panes.loop, false);
+ toggleHidden(this.elements.settings.buttons.loop, false);
+ toggleHidden(this.elements.settings.panels.loop, false);
// Toggle the pane and tab
- const toggle = !utils.is.empty(this.loop.options);
- controls.toggleTab.call(this, 'loop', toggle);
+ const toggle = !is.empty(this.loop.options);
+ controls.toggleMenuButton.call(this, 'loop', toggle);
// Empty the menu
- utils.emptyElement(list);
+ emptyElement(list);
options.forEach(option => {
- const item = utils.createElement('li');
- const button = utils.createElement(
+ const item = createElement('li');
+ const button = createElement(
'button',
- utils.extend(utils.getAttributesFromSelector(this.config.selectors.buttons.loop), {
+ extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {
type: 'button',
class: this.config.classNames.control,
'data-plyr-loop-action': option,
@@ -2735,5355 +2119,6483 @@ var controls = {
list.appendChild(item);
});
}, */
-
// Get current selected caption language
// TODO: rework this to user the getter in the API?
-
// Set a list of available captions languages
setCaptionsMenu: function setCaptionsMenu() {
- var _this3 = this;
-
- // TODO: Captions or language? Currently it's mixed
- var type = 'captions';
- var list = this.elements.settings.panes.captions.querySelector('ul');
+ var _this6 = this;
- // Toggle the pane and tab
- var toggle = captions.getTracks.call(this).length;
- controls.toggleTab.call(this, type, toggle);
+ // Menu required
+ if (!is.element(this.elements.settings.panels.captions)) {
+ return;
+ } // TODO: Captions or language? Currently it's mixed
- // Empty the menu
- utils.emptyElement(list);
- // Check if we need to toggle the parent
- controls.checkMenu.call(this);
+ var type = 'captions';
+ var list = this.elements.settings.panels.captions.querySelector('[role="menu"]');
+ var tracks = captions.getTracks.call(this);
+ var toggle = Boolean(tracks.length); // Toggle the pane and tab
- // If there's no captions, bail
- if (!toggle) {
- return;
- }
+ controls.toggleMenuButton.call(this, type, toggle); // Empty the menu
- // Re-map the tracks into just the data we need
- var tracks = captions.getTracks.call(this).map(function (track) {
- return {
- language: !utils.is.empty(track.language) ? track.language : 'enabled',
- label: captions.getLabel.call(_this3, track)
- };
- });
+ emptyElement(list); // Check if we need to toggle the parent
- // Add the "Disabled" option to turn off captions
- tracks.unshift({
- language: '',
- label: i18n.get('disabled', this.config)
- });
+ controls.checkMenu.call(this); // If there's no captions, bail
- // Generate options
- tracks.forEach(function (track) {
- controls.createMenuItem.call(_this3, track.language, list, 'language', track.label, track.language !== 'enabled' ? controls.createBadge.call(_this3, track.language.toUpperCase()) : null, track.language.toLowerCase() === _this3.captions.language.toLowerCase());
- });
+ if (!toggle) {
+ return;
+ } // Generate options data
- // Store reference
- this.options.captions = tracks.map(function (track) {
- return track.language;
- });
- controls.updateSetting.call(this, type, list);
+ var options = tracks.map(function (track, value) {
+ return {
+ value: value,
+ checked: _this6.captions.toggled && _this6.currentTrack === value,
+ title: captions.getLabel.call(_this6, track),
+ badge: track.language && controls.createBadge.call(_this6, track.language.toUpperCase()),
+ list: list,
+ type: 'language'
+ };
+ }); // Add the "Disabled" option to turn off captions
+
+ options.unshift({
+ value: -1,
+ checked: !this.captions.toggled,
+ title: i18n.get('disabled', this.config),
+ list: list,
+ type: 'language'
+ }); // Generate options
+
+ options.forEach(controls.createMenuItem.bind(this));
+ controls.updateSetting.call(this, type, list);
},
-
-
// Set a list of available captions languages
setSpeedMenu: function setSpeedMenu(options) {
- var _this4 = this;
+ var _this7 = this;
- // Do nothing if not selected
- if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
- return;
- }
+ // Menu required
+ if (!is.element(this.elements.settings.panels.speed)) {
+ return;
+ }
- // Menu required
- if (!utils.is.element(this.elements.settings.panes.speed)) {
- return;
- }
+ var type = 'speed';
+ var list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); // Set the speed options
- var type = 'speed';
+ if (is.array(options)) {
+ this.options.speed = options;
+ } else if (this.isHTML5 || this.isVimeo) {
+ this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
+ } // Set options if passed and filter based on config
- // Set the speed options
- if (utils.is.array(options)) {
- this.options.speed = options;
- } else if (this.isHTML5 || this.isVimeo) {
- this.options.speed = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
- }
- // Set options if passed and filter based on config
- this.options.speed = this.options.speed.filter(function (speed) {
- return _this4.config.speed.options.includes(speed);
- });
+ this.options.speed = this.options.speed.filter(function (speed) {
+ return _this7.config.speed.options.includes(speed);
+ }); // Toggle the pane and tab
- // Toggle the pane and tab
- var toggle = !utils.is.empty(this.options.speed) && this.options.speed.length > 1;
- controls.toggleTab.call(this, type, toggle);
+ var toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;
+ controls.toggleMenuButton.call(this, type, toggle); // Empty the menu
- // Check if we need to toggle the parent
- controls.checkMenu.call(this);
+ emptyElement(list); // Check if we need to toggle the parent
- // If we're hiding, nothing more to do
- if (!toggle) {
- return;
- }
+ controls.checkMenu.call(this); // If we're hiding, nothing more to do
- // Get the list to populate
- var list = this.elements.settings.panes.speed.querySelector('ul');
+ if (!toggle) {
+ return;
+ } // Create items
- // Empty the menu
- utils.emptyElement(list);
- // Create items
- this.options.speed.forEach(function (speed) {
- var label = controls.getLabel.call(_this4, 'speed', speed);
- controls.createMenuItem.call(_this4, speed, list, type, label);
+ this.options.speed.forEach(function (speed) {
+ controls.createMenuItem.call(_this7, {
+ value: speed,
+ list: list,
+ type: type,
+ title: controls.getLabel.call(_this7, 'speed', speed)
});
-
- controls.updateSetting.call(this, type, list);
+ });
+ controls.updateSetting.call(this, type, list);
},
-
-
// Check if we need to hide/show the settings menu
checkMenu: function checkMenu() {
- var tabs = this.elements.settings.tabs;
-
- var visible = !utils.is.empty(tabs) && Object.values(tabs).some(function (tab) {
- return !tab.hidden;
- });
-
- utils.toggleHidden(this.elements.settings.menu, !visible);
+ var buttons = this.elements.settings.buttons;
+ var visible = !is.empty(buttons) && Object.values(buttons).some(function (button) {
+ return !button.hidden;
+ });
+ toggleHidden(this.elements.settings.menu, !visible);
},
+ // Focus the first menu item in a given (or visible) menu
+ focusFirstMenuItem: function focusFirstMenuItem(pane) {
+ var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ if (this.elements.settings.popup.hidden) {
+ return;
+ }
- // Show/hide menu
- toggleMenu: function toggleMenu(event) {
- var form = this.elements.settings.form;
-
- var button = this.elements.buttons.settings;
-
- // Menu and button are required
- if (!utils.is.element(form) || !utils.is.element(button)) {
- return;
- }
+ var target = pane;
- var show = utils.is.boolean(event) ? event : utils.is.element(form) && form.hasAttribute('hidden');
+ if (!is.element(target)) {
+ target = Object.values(this.elements.settings.panels).find(function (pane) {
+ return !pane.hidden;
+ });
+ }
- if (utils.is.event(event)) {
- var isMenuItem = utils.is.element(form) && form.contains(event.target);
- var isButton = event.target === this.elements.buttons.settings;
+ var firstItem = target.querySelector('[role^="menuitem"]');
+ setFocus.call(this, firstItem, tabFocus);
+ },
+ // Show/hide menu
+ toggleMenu: function toggleMenu(input) {
+ var popup = this.elements.settings.popup;
+ var button = this.elements.buttons.settings; // Menu and button are required
- // If the click was inside the form or if the click
- // wasn't the button or menu item and we're trying to
- // show the menu (a doc click shouldn't show the menu)
- if (isMenuItem || !isMenuItem && !isButton && show) {
- return;
- }
+ if (!is.element(popup) || !is.element(button)) {
+ return;
+ } // True toggle by default
- // Prevent the toggle being caught by the doc listener
- if (isButton) {
- event.stopPropagation();
- }
- }
- // Set form and button attributes
- if (utils.is.element(button)) {
- button.setAttribute('aria-expanded', show);
- }
+ var hidden = popup.hidden;
+ var show = hidden;
- if (utils.is.element(form)) {
- utils.toggleHidden(form, !show);
- utils.toggleClass(this.elements.container, this.config.classNames.menu.open, show);
+ if (is.boolean(input)) {
+ show = input;
+ } else if (is.keyboardEvent(input) && input.which === 27) {
+ show = false;
+ } else if (is.event(input)) {
+ var isMenuItem = popup.contains(input.target); // If the click was inside the menu or if the click
+ // wasn't the button or menu item and we're trying to
+ // show the menu (a doc click shouldn't show the menu)
- if (show) {
- form.removeAttribute('tabindex');
- } else {
- form.setAttribute('tabindex', -1);
- }
+ if (isMenuItem || !isMenuItem && input.target !== button && show) {
+ return;
}
- },
+ } // Set button attributes
- // Get the natural size of a tab
- getTabSize: function getTabSize(tab) {
- var clone = tab.cloneNode(true);
- clone.style.position = 'absolute';
- clone.style.opacity = 0;
- clone.removeAttribute('hidden');
+ button.setAttribute('aria-expanded', show); // Show the actual popup
- // Prevent input's being unchecked due to the name being identical
- Array.from(clone.querySelectorAll('input[name]')).forEach(function (input) {
- var name = input.getAttribute('name');
- input.setAttribute('name', name + '-clone');
- });
+ toggleHidden(popup, !show); // Add class hook
- // Append to parent so we get the "real" size
- tab.parentNode.appendChild(clone);
+ toggleClass(this.elements.container, this.config.classNames.menu.open, show); // Focus the first item if key interaction
- // Get the sizes before we remove
- var width = clone.scrollWidth;
- var height = clone.scrollHeight;
+ if (show && is.keyboardEvent(input)) {
+ controls.focusFirstMenuItem.call(this, null, true);
+ } else if (!show && !hidden) {
+ // If closing, re-focus the button
+ setFocus.call(this, button, is.keyboardEvent(input));
+ }
+ },
+ // Get the natural size of a menu panel
+ getMenuSize: function getMenuSize(tab) {
+ var clone = tab.cloneNode(true);
+ clone.style.position = 'absolute';
+ clone.style.opacity = 0;
+ clone.removeAttribute('hidden'); // Append to parent so we get the "real" size
+
+ tab.parentNode.appendChild(clone); // Get the sizes before we remove
+
+ var width = clone.scrollWidth;
+ var height = clone.scrollHeight; // Remove from the DOM
+
+ removeElement(clone);
+ return {
+ width: width,
+ height: height
+ };
+ },
+ // Show a panel in the menu
+ showMenuPanel: function showMenuPanel() {
+ var _this8 = this;
- // Remove from the DOM
- utils.removeElement(clone);
+ var type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var target = document.getElementById("plyr-settings-".concat(this.id, "-").concat(type)); // Nothing to show, bail
- return {
- width: width,
- height: height
- };
- },
+ if (!is.element(target)) {
+ return;
+ } // Hide all other panels
- // Toggle Menu
- showTab: function showTab() {
- var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
- var menu = this.elements.settings.menu;
+ var container = target.parentNode;
+ var current = Array.from(container.children).find(function (node) {
+ return !node.hidden;
+ }); // If we can do fancy animations, we'll animate the height/width
- var pane = document.getElementById(target);
+ if (support.transitions && !support.reducedMotion) {
+ // Set the current width as a base
+ container.style.width = "".concat(current.scrollWidth, "px");
+ container.style.height = "".concat(current.scrollHeight, "px"); // Get potential sizes
- // Nothing to show, bail
- if (!utils.is.element(pane)) {
- return;
- }
+ var size = controls.getMenuSize.call(this, target); // Restore auto height/width
- // Are we targetting a tab? If not, bail
- var isTab = pane.getAttribute('role') === 'tabpanel';
- if (!isTab) {
+ var restore = function restore(event) {
+ // We're only bothered about height and width on the container
+ if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {
return;
- }
+ } // Revert back to auto
- // Hide all other tabs
- // Get other tabs
- var current = menu.querySelector('[role="tabpanel"]:not([hidden])');
- var container = current.parentNode;
- // Set other toggles to be expanded false
- Array.from(menu.querySelectorAll('[aria-controls="' + current.getAttribute('id') + '"]')).forEach(function (toggle) {
- toggle.setAttribute('aria-expanded', false);
- });
+ container.style.width = '';
+ container.style.height = ''; // Only listen once
- // If we can do fancy animations, we'll animate the height/width
- if (support.transitions && !support.reducedMotion) {
- // Set the current width as a base
- container.style.width = current.scrollWidth + 'px';
- container.style.height = current.scrollHeight + 'px';
+ off.call(_this8, container, transitionEndEvent, restore);
+ }; // Listen for the transition finishing and restore auto height/width
- // Get potential sizes
- var size = controls.getTabSize.call(this, pane);
- // Restore auto height/width
- var restore = function restore(e) {
- // We're only bothered about height and width on the container
- if (e.target !== container || !['width', 'height'].includes(e.propertyName)) {
- return;
- }
+ on.call(this, container, transitionEndEvent, restore); // Set dimensions to target
- // Revert back to auto
- container.style.width = '';
- container.style.height = '';
+ container.style.width = "".concat(size.width, "px");
+ container.style.height = "".concat(size.height, "px");
+ } // Set attributes on current tab
- // Only listen once
- utils.off(container, utils.transitionEndEvent, restore);
- };
- // Listen for the transition finishing and restore auto height/width
- utils.on(container, utils.transitionEndEvent, restore);
+ toggleHidden(current, true); // Set attributes on target
- // Set dimensions to target
- container.style.width = size.width + 'px';
- container.style.height = size.height + 'px';
- }
+ toggleHidden(target, false); // Focus the first item
- // Set attributes on current tab
- utils.toggleHidden(current, true);
- current.setAttribute('tabindex', -1);
+ controls.focusFirstMenuItem.call(this, target, tabFocus);
+ },
+ // Set the download link
+ setDownloadLink: function setDownloadLink() {
+ var button = this.elements.buttons.download; // Bail if no button
- // Set attributes on target
- utils.toggleHidden(pane, false);
+ if (!is.element(button)) {
+ return;
+ } // Set download link
- 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
- pane.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]')[0].focus();
+ button.setAttribute('href', this.download);
},
-
-
// Build the default HTML
// TODO: Set order based on order in the config.controls array?
create: function create(data) {
- var _this5 = this;
+ var _this9 = this;
- // Do nothing if we want no controls
- if (utils.is.empty(this.config.controls)) {
- return null;
- }
+ // Create the container
+ var container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); // Restart button
- // Create the container
- var container = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.controls.wrapper));
+ if (this.config.controls.includes('restart')) {
+ container.appendChild(controls.createButton.call(this, 'restart'));
+ } // Rewind button
- // Restart button
- if (this.config.controls.includes('restart')) {
- container.appendChild(controls.createButton.call(this, 'restart'));
- }
- // Rewind button
- if (this.config.controls.includes('rewind')) {
- container.appendChild(controls.createButton.call(this, 'rewind'));
- }
+ if (this.config.controls.includes('rewind')) {
+ container.appendChild(controls.createButton.call(this, 'rewind'));
+ } // Play/Pause button
- // Play/Pause button
- if (this.config.controls.includes('play')) {
- container.appendChild(controls.createButton.call(this, 'play'));
- }
- // Fast forward button
- if (this.config.controls.includes('fast-forward')) {
- container.appendChild(controls.createButton.call(this, 'fast-forward'));
- }
+ if (this.config.controls.includes('play')) {
+ container.appendChild(controls.createButton.call(this, 'play'));
+ } // Fast forward button
- // Progress
- if (this.config.controls.includes('progress')) {
- var progress = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.progress));
- // Seek range slider
- var seek = controls.createRange.call(this, 'seek', {
- id: 'plyr-seek-' + data.id
- });
- progress.appendChild(seek.label);
- progress.appendChild(seek.input);
+ if (this.config.controls.includes('fast-forward')) {
+ container.appendChild(controls.createButton.call(this, 'fast-forward'));
+ } // Progress
- // Buffer progress
- progress.appendChild(controls.createProgress.call(this, 'buffer'));
- // TODO: Add loop display indicator
+ if (this.config.controls.includes('progress')) {
+ var progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); // Seek range slider
- // Seek tooltip
- if (this.config.tooltips.seek) {
- var tooltip = utils.createElement('span', {
- role: 'tooltip',
- class: this.config.classNames.tooltip
- }, '00:00');
+ progress.appendChild(controls.createRange.call(this, 'seek', {
+ id: "plyr-seek-".concat(data.id)
+ })); // Buffer progress
- progress.appendChild(tooltip);
- this.elements.display.seekTooltip = tooltip;
- }
+ progress.appendChild(controls.createProgress.call(this, 'buffer')); // TODO: Add loop display indicator
+ // Seek tooltip
- this.elements.progress = progress;
- container.appendChild(this.elements.progress);
+ if (this.config.tooltips.seek) {
+ var tooltip = createElement('span', {
+ class: this.config.classNames.tooltip
+ }, '00:00');
+ progress.appendChild(tooltip);
+ this.elements.display.seekTooltip = tooltip;
}
- // Media current time display
- if (this.config.controls.includes('current-time')) {
- container.appendChild(controls.createTime.call(this, 'currentTime'));
- }
+ this.elements.progress = progress;
+ container.appendChild(this.elements.progress);
+ } // Media current time display
- // Media duration display
- if (this.config.controls.includes('duration')) {
- container.appendChild(controls.createTime.call(this, 'duration'));
- }
- // Toggle mute button
- if (this.config.controls.includes('mute')) {
- container.appendChild(controls.createButton.call(this, 'mute'));
- }
+ if (this.config.controls.includes('current-time')) {
+ container.appendChild(controls.createTime.call(this, 'currentTime'));
+ } // Media duration display
- // Volume range control
- if (this.config.controls.includes('volume')) {
- var volume = utils.createElement('div', {
- class: 'plyr__volume'
- });
- // Set the attributes
- var attributes = {
- max: 1,
- step: 0.05,
- value: this.config.volume
- };
+ if (this.config.controls.includes('duration')) {
+ container.appendChild(controls.createTime.call(this, 'duration'));
+ } // Volume controls
- // Create the volume range slider
- var range = controls.createRange.call(this, 'volume', utils.extend(attributes, {
- id: 'plyr-volume-' + data.id
- }));
- volume.appendChild(range.label);
- volume.appendChild(range.input);
- this.elements.volume = volume;
+ if (this.config.controls.includes('mute') || this.config.controls.includes('volume')) {
+ var volume = createElement('div', {
+ class: 'plyr__volume'
+ }); // Toggle mute button
- container.appendChild(volume);
- }
+ if (this.config.controls.includes('mute')) {
+ volume.appendChild(controls.createButton.call(this, 'mute'));
+ } // Volume range control
- // Toggle captions button
- if (this.config.controls.includes('captions')) {
- container.appendChild(controls.createButton.call(this, 'captions'));
+
+ if (this.config.controls.includes('volume')) {
+ // Set the attributes
+ var attributes = {
+ max: 1,
+ step: 0.05,
+ value: this.config.volume
+ }; // Create the volume range slider
+
+ volume.appendChild(controls.createRange.call(this, 'volume', extend(attributes, {
+ id: "plyr-volume-".concat(data.id)
+ })));
+ this.elements.volume = volume;
}
- // Settings button / menu
- if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
- var menu = utils.createElement('div', {
- class: 'plyr__menu',
- hidden: ''
- });
+ container.appendChild(volume);
+ } // Toggle captions button
- menu.appendChild(controls.createButton.call(this, 'settings', {
- id: 'plyr-settings-toggle-' + data.id,
- 'aria-haspopup': true,
- 'aria-controls': 'plyr-settings-' + data.id,
- 'aria-expanded': false
- }));
-
- var form = utils.createElement('form', {
- class: 'plyr__menu__container',
- id: 'plyr-settings-' + data.id,
- hidden: '',
- 'aria-labelled-by': 'plyr-settings-toggle-' + data.id,
- role: 'tablist',
- tabindex: -1
- });
- var inner = utils.createElement('div');
+ if (this.config.controls.includes('captions')) {
+ container.appendChild(controls.createButton.call(this, 'captions'));
+ } // Settings button / menu
- var home = utils.createElement('div', {
- id: 'plyr-settings-' + data.id + '-home',
- 'aria-labelled-by': 'plyr-settings-toggle-' + data.id,
- role: 'tabpanel'
- });
- // Create the tab list
- var tabs = utils.createElement('ul', {
- role: 'tablist'
- });
+ if (this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
+ var control = createElement('div', {
+ class: 'plyr__menu',
+ hidden: ''
+ });
+ control.appendChild(controls.createButton.call(this, 'settings', {
+ 'aria-haspopup': true,
+ 'aria-controls': "plyr-settings-".concat(data.id),
+ 'aria-expanded': false
+ }));
+ var popup = createElement('div', {
+ class: 'plyr__menu__container',
+ id: "plyr-settings-".concat(data.id),
+ hidden: ''
+ });
+ var inner = createElement('div');
+ var home = createElement('div', {
+ id: "plyr-settings-".concat(data.id, "-home")
+ }); // Create the menu
- // Build the tabs
- this.config.settings.forEach(function (type) {
- var tab = utils.createElement('li', {
- role: 'tab',
- hidden: ''
- });
+ var menu = createElement('div', {
+ role: 'menu'
+ });
+ home.appendChild(menu);
+ inner.appendChild(home);
+ this.elements.settings.panels.home = home; // Build the menu items
+
+ this.config.settings.forEach(function (type) {
+ // TODO: bundle this with the createMenuItem helper and bindings
+ var menuItem = createElement('button', extend(getAttributesFromSelector(_this9.config.selectors.buttons.settings), {
+ type: 'button',
+ class: "".concat(_this9.config.classNames.control, " ").concat(_this9.config.classNames.control, "--forward"),
+ role: 'menuitem',
+ 'aria-haspopup': true,
+ hidden: ''
+ })); // Bind menu shortcuts for keyboard users
+
+ controls.bindMenuItemShortcuts.call(_this9, menuItem, type); // Show menu on click
+
+ on(menuItem, 'click', function () {
+ controls.showMenuPanel.call(_this9, type, false);
+ });
+ var flex = createElement('span', null, i18n.get(type, _this9.config));
+ var value = createElement('span', {
+ class: _this9.config.classNames.menu.value
+ }); // Speed contains HTML entities
+
+ value.innerHTML = data[type];
+ flex.appendChild(value);
+ menuItem.appendChild(flex);
+ menu.appendChild(menuItem); // Build the panes
+
+ var pane = createElement('div', {
+ id: "plyr-settings-".concat(data.id, "-").concat(type),
+ hidden: ''
+ }); // Back button
+
+ var backButton = createElement('button', {
+ type: 'button',
+ class: "".concat(_this9.config.classNames.control, " ").concat(_this9.config.classNames.control, "--back")
+ }); // Visible label
+
+ backButton.appendChild(createElement('span', {
+ 'aria-hidden': true
+ }, i18n.get(type, _this9.config))); // Screen reader label
- var button = utils.createElement('button', utils.extend(utils.getAttributesFromSelector(_this5.config.selectors.buttons.settings), {
- type: 'button',
- class: _this5.config.classNames.control + ' ' + _this5.config.classNames.control + '--forward',
- id: 'plyr-settings-' + data.id + '-' + type + '-tab',
- 'aria-haspopup': true,
- 'aria-controls': 'plyr-settings-' + data.id + '-' + type,
- 'aria-expanded': false
- }), i18n.get(type, _this5.config));
-
- var value = utils.createElement('span', {
- class: _this5.config.classNames.menu.value
- });
+ backButton.appendChild(createElement('span', {
+ class: _this9.config.classNames.hidden
+ }, i18n.get('menuBack', _this9.config))); // Go back via keyboard
- // Speed contains HTML entities
- value.innerHTML = data[type];
+ on(pane, 'keydown', function (event) {
+ // We only care about <-
+ if (event.which !== 37) {
+ return;
+ } // Prevent seek
- button.appendChild(value);
- tab.appendChild(button);
- tabs.appendChild(tab);
- _this5.elements.settings.tabs[type] = tab;
- });
+ event.preventDefault();
+ event.stopPropagation(); // Show the respective menu
- 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
- });
+ controls.showMenuPanel.call(_this9, 'home', true);
+ }, false); // Go back via button click
- 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));
+ on(backButton, 'click', function () {
+ controls.showMenuPanel.call(_this9, 'home', false);
+ }); // Add to pane
- pane.appendChild(back);
+ pane.appendChild(backButton); // Menu
- var options = utils.createElement('ul');
+ pane.appendChild(createElement('div', {
+ role: 'menu'
+ }));
+ inner.appendChild(pane);
+ _this9.elements.settings.buttons[type] = menuItem;
+ _this9.elements.settings.panels[type] = pane;
+ });
+ popup.appendChild(inner);
+ control.appendChild(popup);
+ container.appendChild(control);
+ this.elements.settings.popup = popup;
+ this.elements.settings.menu = control;
+ } // Picture in picture button
- pane.appendChild(options);
- inner.appendChild(pane);
- _this5.elements.settings.panes[type] = pane;
- });
+ if (this.config.controls.includes('pip') && support.pip) {
+ container.appendChild(controls.createButton.call(this, 'pip'));
+ } // Airplay button
- form.appendChild(inner);
- menu.appendChild(form);
- container.appendChild(menu);
- this.elements.settings.form = form;
- this.elements.settings.menu = menu;
- }
+ if (this.config.controls.includes('airplay') && support.airplay) {
+ container.appendChild(controls.createButton.call(this, 'airplay'));
+ } // Download button
- // 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'));
- }
+ if (this.config.controls.includes('download')) {
+ var _attributes = {
+ element: 'a',
+ href: this.download,
+ target: '_blank'
+ };
+ var download = this.config.urls.download;
- // Toggle fullscreen button
- if (this.config.controls.includes('fullscreen')) {
- container.appendChild(controls.createButton.call(this, 'fullscreen'));
+ if (!is.url(download) && this.isEmbed) {
+ extend(_attributes, {
+ icon: "logo-".concat(this.provider),
+ label: this.provider
+ });
}
- // Larger overlaid play button
- if (this.config.controls.includes('play-large')) {
- this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
- }
+ container.appendChild(controls.createButton.call(this, 'download', _attributes));
+ } // Toggle fullscreen button
- this.elements.controls = container;
- if (this.isHTML5) {
- controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
- }
+ if (this.config.controls.includes('fullscreen')) {
+ container.appendChild(controls.createButton.call(this, 'fullscreen'));
+ } // Larger overlaid play button
- controls.setSpeedMenu.call(this);
- return container;
- },
+ if (this.config.controls.includes('play-large')) {
+ this.elements.container.appendChild(controls.createButton.call(this, 'play-large'));
+ }
+ this.elements.controls = container; // Set available quality levels
+ 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;
+ var _this10 = this;
- // Sprite
- if (this.config.loadSprite) {
- var icon = controls.getIconUrl.call(this);
+ // Sprite
+ if (this.config.loadSprite) {
+ var icon = controls.getIconUrl.call(this); // Only load external sprite using AJAX
- // Only load external sprite using AJAX
- if (icon.cors) {
- utils.loadSprite(icon.url, 'sprite-plyr');
- }
+ if (icon.cors) {
+ loadSprite(icon.url, 'sprite-plyr');
}
+ } // Create a unique ID
- // Create a unique ID
- this.id = Math.floor(Math.random() * 10000);
- // Null by default
- var container = null;
- this.elements.controls = null;
+ this.id = Math.floor(Math.random() * 10000); // Null by default
- // 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;
- }
+ var container = null;
+ this.elements.controls = null; // Set template properties
- // Replace props with their value
- var replace = function replace(input) {
- var result = input;
+ var props = {
+ id: this.id,
+ seektime: this.config.seekTime,
+ title: this.config.title
+ };
+ var update = true; // If function, run it and use output
- Object.entries(props).forEach(function (_ref) {
- var _ref2 = slicedToArray(_ref, 2),
- key = _ref2[0],
- value = _ref2[1];
+ if (is.function(this.config.controls)) {
+ this.config.controls = this.config.controls.call(this, props);
+ } // Convert falsy controls to empty array (primarily for empty strings)
- result = utils.replaceAll(result, '{' + key + '}', value);
- });
- return result;
- };
+ if (!this.config.controls) {
+ this.config.controls = [];
+ }
- // 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);
- }
- }
+ if (is.element(this.config.controls) || is.string(this.config.controls)) {
+ // HTMLElement or Non-empty string passed as the option
+ container = this.config.controls;
+ } else {
+ // Create controls
+ container = controls.create.call(this, {
+ id: this.id,
+ seektime: this.config.seekTime,
+ speed: this.speed,
+ quality: this.quality,
+ captions: captions.getLabel.call(this) // TODO: Looping
+ // loop: 'None',
- // Controls container
- var target = void 0;
+ });
+ update = false;
+ } // Replace props with their value
- // 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;
- }
+ var replace = function replace(input) {
+ var result = input;
+ Object.entries(props).forEach(function (_ref2) {
+ var _ref3 = _slicedToArray(_ref2, 2),
+ key = _ref3[0],
+ value = _ref3[1];
- // Inject controls HTML
- if (utils.is.element(container)) {
- target.appendChild(container);
- } else if (container) {
- target.insertAdjacentHTML('beforeend', container);
- }
+ result = replaceAll(result, "{".concat(key, "}"), value);
+ });
+ return result;
+ }; // Update markup
- // 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);
+ if (update) {
+ if (is.string(this.config.controls)) {
+ container = replace(container);
+ } else if (is.element(container)) {
+ container.innerHTML = replace(container.innerHTML);
}
+ } // Controls container
- // 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 target; // Inject to custom location
-// ==========================================================================
+ if (is.string(this.config.selectors.controls.container)) {
+ target = document.querySelector(this.config.selectors.controls.container);
+ } // Inject into the container by default
-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 (!is.element(target)) {
+ target = this.elements.container;
+ } // Inject controls HTML (needs to be before captions, hence "afterbegin")
- if (!utils.is.empty(stored)) {
- this.captions.language = stored;
- }
- if (utils.is.empty(this.captions.language)) {
- this.captions.language = this.config.captions.language.toLowerCase();
- }
+ var insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';
+ target[insertMethod]('afterbegin', container); // Find the elements if need be
- // Set captions enabled state if not set
- if (!utils.is.boolean(this.captions.active)) {
- var active = this.storage.get('captions');
+ if (!is.element(this.elements.controls)) {
+ controls.findElements.call(this);
+ } // Add pressed property to buttons
- 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);
+ if (!is.empty(this.elements.buttons)) {
+ var addProperty = function addProperty(button) {
+ var className = _this10.config.classNames.controlPressed;
+ Object.defineProperty(button, 'pressed', {
+ enumerable: true,
+ get: function get() {
+ return hasClass(button, className);
+ },
+ set: function set() {
+ var pressed = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ toggleClass(button, className, pressed);
}
+ });
+ }; // Toggle classname when pressed property is set
- return;
- }
- // Inject the container
- if (!utils.is.element(this.elements.captions)) {
- this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
+ Object.values(this.elements.buttons).filter(Boolean).forEach(function (button) {
+ if (is.array(button) || is.nodeList(button)) {
+ Array.from(button).filter(Boolean).forEach(addProperty);
+ } else {
+ addProperty(button);
+ }
+ });
+ } // Edge sometimes doesn't finish the paint so force a repaint
- 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)));
+ if (browser.isEdge) {
+ repaint(target);
+ } // Setup tooltips
- // Get tracks
- var tracks = captions.getTracks.call(this);
- // If no caption file exists, hide container for caption text
- if (utils.is.empty(tracks)) {
- return;
- }
+ if (this.config.tooltips.controls) {
+ var _this$config = this.config,
+ classNames = _this$config.classNames,
+ selectors = _this$config.selectors;
+ var selector = "".concat(selectors.controls.wrapper, " ").concat(selectors.labels, " .").concat(classNames.hidden);
+ var labels = getElements.call(this, selector);
+ Array.from(labels).forEach(function (label) {
+ toggleClass(label, _this10.config.classNames.hidden, false);
+ toggleClass(label, _this10.config.classNames.tooltip, true);
+ });
+ }
+ }
+ };
- // Get browser info
- var browser = utils.getBrowser();
+ /**
+ * Parse a string to a URL object
+ * @param {string} input - the URL to be parsed
+ * @param {boolean} safe - failsafe parsing
+ */
- // 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');
+ function parseUrl(input) {
+ var safe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+ var url = input;
- Array.from(elements).forEach(function (track) {
- var src = track.getAttribute('src');
- var href = utils.parseUrl(src);
+ if (safe) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ url = parser.href;
+ }
- 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);
- });
- }
- });
- }
+ try {
+ return new URL(url);
+ } catch (e) {
+ return null;
+ }
+ } // Convert object to URLSearchParams
+
+ function buildUrlParams(input) {
+ var params = new URLSearchParams();
- // Set language
- captions.setLanguage.call(this);
+ if (is.object(input)) {
+ Object.entries(input).forEach(function (_ref) {
+ var _ref2 = _slicedToArray(_ref, 2),
+ key = _ref2[0],
+ value = _ref2[1];
- // Enable UI
- captions.show.call(this);
+ params.set(key, value);
+ });
+ }
+
+ return params;
+ }
+
+ var captions = {
+ // Setup captions
+ setup: function setup() {
+ // Requires UI support
+ if (!this.supported.ui) {
+ return;
+ } // Only Vimeo and HTML5 video supported at this point
- // 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);
+
+ if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {
+ // Clear menu and hide
+ if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {
+ controls.setCaptionsMenu.call(this);
}
- },
+ return;
+ } // Inject the container
- // 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);
- });
+ if (!is.element(this.elements.captions)) {
+ this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));
+ insertAfter(this.elements.captions, this.elements.wrapper);
+ } // Fix IE captions if CORS is used
+ // Fetch captions and inject as blobs instead (data URIs not supported!)
- // Turn off native caption rendering to avoid double captions
- // eslint-disable-next-line
- track.mode = 'hidden';
+
+ if (browser.isIE && window.URL) {
+ var elements = this.media.querySelectorAll('track');
+ Array.from(elements).forEach(function (track) {
+ var src = track.getAttribute('src');
+ var url = parseUrl(src);
+
+ if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {
+ fetch(src, 'blob').then(function (blob) {
+ track.setAttribute('src', window.URL.createObjectURL(blob));
+ }).catch(function () {
+ removeElement(track);
});
+ }
+ });
+ } // Get and set initial data
+ // The "preferred" options are not realized unless / until the wanted language has a match
+ // * languages: Array of user's browser languages.
+ // * language: The language preferred by user settings or config
+ // * active: The state preferred by user settings or config
+ // * toggled: The real captions state
- // 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);
- }
- },
+ var browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];
+ var languages = dedupe(browserLanguages.map(function (language) {
+ return language.split('-')[0];
+ }));
+ var language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); // Use first browser language when language is 'auto'
+ if (language === 'auto') {
+ var _languages = _slicedToArray(languages, 1);
- // Get the tracks
- getTracks: function getTracks() {
- // Return empty array at least
- if (utils.is.nullOrUndefined(this.media)) {
- return [];
- }
+ language = _languages[0];
+ }
- // Only get accepted kinds
- return Array.from(this.media.textTracks || []).filter(function (track) {
- return ['captions', 'subtitles'].includes(track.kind);
- });
+ var active = this.storage.get('captions');
+
+ if (!is.boolean(active)) {
+ active = this.config.captions.active;
+ }
+
+ Object.assign(this.captions, {
+ toggled: false,
+ active: active,
+ language: language,
+ languages: languages
+ }); // Watch changes to textTracks and update captions menu
+
+ if (this.isHTML5) {
+ var trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';
+ on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));
+ } // Update available languages in list next tick (the event must not be triggered before the listeners)
+
+
+ setTimeout(captions.update.bind(this), 0);
},
+ // Update available language options in settings based on tracks
+ update: function update() {
+ var _this = this;
+ var tracks = captions.getTracks.call(this, true); // Get the wanted language
- // Get the current track for the current language
- getCurrentTrack: function getCurrentTrack() {
- var _this2 = this;
+ var _this$captions = this.captions,
+ active = _this$captions.active,
+ language = _this$captions.language,
+ meta = _this$captions.meta,
+ currentTrackNode = _this$captions.currentTrackNode;
+ var languageExists = Boolean(tracks.find(function (track) {
+ return track.language === language;
+ })); // Handle tracks (add event listener and "pseudo"-default)
- var tracks = captions.getTracks.call(this);
+ if (this.isHTML5 && this.isVideo) {
+ tracks.filter(function (track) {
+ return !meta.get(track);
+ }).forEach(function (track) {
+ _this.debug.log('Track added', track); // Attempt to store if the original dom element was "default"
- if (!tracks.length) {
- return null;
- }
- // Get track based on current language
- var track = tracks.find(function (track) {
- return track.language.toLowerCase() === _this2.language;
+ meta.set(track, {
+ default: track.mode === 'showing'
+ }); // Turn off native caption rendering to avoid double captions
+
+ track.mode = 'hidden'; // Add event listener for cue changes
+
+ on.call(_this, track, 'cuechange', function () {
+ return captions.updateCues.call(_this);
+ });
});
+ } // Update language first time it matches, or if the previous matching track was removed
- // Get the <track> with default attribute
- if (!track) {
- track = utils.getElement.call(this, 'track[default]');
- }
- // Get the first track
- if (!track) {
- var _tracks = slicedToArray(tracks, 1);
+ if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {
+ captions.setLanguage.call(this, language);
+ captions.toggle.call(this, active && languageExists);
+ } // Enable or disable captions based on track length
- track = _tracks[0];
- }
- return track;
+ toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); // Update available languages in list
+
+ if ((this.config.controls || []).includes('settings') && this.config.settings.includes('captions')) {
+ controls.setCaptionsMenu.call(this);
+ }
},
+ // Toggle captions display
+ // Used internally for the toggleCaptions method, with the passive option forced to false
+ toggle: function toggle(input) {
+ var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+ // If there's no full support
+ if (!this.supported.ui) {
+ return;
+ }
- // Get UI label for track
- getLabel: function getLabel(track) {
- var currentTrack = track;
+ var toggled = this.captions.toggled; // Current state
- if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) {
- currentTrack = captions.getCurrentTrack.call(this);
- }
+ var activeClass = this.config.classNames.captions.active; // Get the next state
+ // If the method is called without parameter, toggle based on current value
- if (utils.is.track(currentTrack)) {
- if (!utils.is.empty(currentTrack.label)) {
- return currentTrack.label;
- }
+ var active = is.nullOrUndefined(input) ? !toggled : input; // Update state and trigger event
- if (!utils.is.empty(currentTrack.language)) {
- return track.language.toUpperCase();
- }
+ if (active !== toggled) {
+ // When passive, don't override user preferences
+ if (!passive) {
+ this.captions.active = active;
+ this.storage.set({
+ captions: active
+ });
+ } // Force language if the call isn't passive and there is no matching language to toggle to
- return i18n.get('enabled', this.config);
- }
- return i18n.get('disabled', this.config);
- },
+ if (!this.language && active && !passive) {
+ var tracks = captions.getTracks.call(this);
+ var track = captions.findTrack.call(this, [this.captions.language].concat(_toConsumableArray(this.captions.languages)), true); // Override user preferences to avoid switching languages if a matching track is added
+ this.captions.language = track.language; // Set caption, but don't store in localStorage as user preference
- // 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;
+ captions.set.call(this, tracks.indexOf(track));
+ return;
+ } // Toggle button if it's enabled
- var active = activeCues.length && activeCues[0];
- var currentTrack = captions.getCurrentTrack.call(this);
- // Only display current track
- if (track !== currentTrack) {
- return;
- }
+ if (this.elements.buttons.captions) {
+ this.elements.buttons.captions.pressed = active;
+ } // Add class hook
- // 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');
+ toggleClass(this.elements.container, activeClass, active);
+ this.captions.toggled = active; // Update settings menu
+
+ controls.updateSetting.call(this, 'captions'); // Trigger event (not used internally)
+
+ triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');
+ }
},
+ // Set captions by track index
+ // Used internally for the currentTrack setter with the passive option forced to false
+ set: function set(index) {
+ var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+ var tracks = captions.getTracks.call(this); // Disable captions if setting to -1
+
+ if (index === -1) {
+ captions.toggle.call(this, false, passive);
+ return;
+ }
+ if (!is.number(index)) {
+ this.debug.warn('Invalid caption argument', index);
+ return;
+ }
- // Set the current caption
- setText: function setText(input) {
- // Requires UI
- if (!this.supported.ui) {
- return;
- }
+ if (!(index in tracks)) {
+ this.debug.warn('Track not found', index);
+ return;
+ }
- if (utils.is.element(this.elements.captions)) {
- var content = utils.createElement('span');
+ if (this.captions.currentTrack !== index) {
+ this.captions.currentTrack = index;
+ var track = tracks[index];
- // Empty the container
- utils.emptyElement(this.elements.captions);
+ var _ref = track || {},
+ language = _ref.language; // Store reference to node for invalidation on remove
- // 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);
- }
+ this.captions.currentTrackNode = track; // Update settings menu
- // Set new caption text
- this.elements.captions.appendChild(content);
- } else {
- this.debug.warn('No captions element to render to');
- }
+ controls.updateSetting.call(this, 'captions'); // When passive, don't override user preferences
+
+ if (!passive) {
+ this.captions.language = language;
+ this.storage.set({
+ language: language
+ });
+ } // Handle Vimeo captions
+
+
+ if (this.isVimeo) {
+ this.embed.enableTextTrack(language);
+ } // Trigger event
+
+
+ triggerEvent.call(this, this.media, 'languagechange');
+ } // Show captions
+
+
+ captions.toggle.call(this, true, passive);
+
+ if (this.isHTML5 && this.isVideo) {
+ // If we change the active track while a cue is already displayed we need to update it
+ captions.updateCues.call(this);
+ }
},
+ // Set captions by language
+ // Used internally for the language setter with the passive option forced to false
+ setLanguage: function setLanguage(input) {
+ var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+ if (!is.string(input)) {
+ this.debug.warn('Invalid language argument', input);
+ return;
+ } // Normalize
- // 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;
+ var language = input.toLowerCase();
+ this.captions.language = language; // Set currentTrack
+
+ var tracks = captions.getTracks.call(this);
+ var track = captions.findTrack.call(this, [language]);
+ captions.set.call(this, tracks.indexOf(track), passive);
+ },
+ // Get current valid caption tracks
+ // If update is false it will also ignore tracks without metadata
+ // This is used to "freeze" the language options when captions.update is false
+ getTracks: function getTracks() {
+ var _this2 = this;
+
+ var update = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ // Handle media or textTracks missing or null
+ var tracks = Array.from((this.media || {}).textTracks || []); // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)
+ // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)
+
+ return tracks.filter(function (track) {
+ return !_this2.isHTML5 || update || _this2.captions.meta.has(track);
+ }).filter(function (track) {
+ return ['captions', 'subtitles'].includes(track.kind);
+ });
+ },
+ // Match tracks based on languages and get the first
+ findTrack: function findTrack(languages) {
+ var _this3 = this;
+
+ var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var tracks = captions.getTracks.call(this);
+
+ var sortIsDefault = function sortIsDefault(track) {
+ return Number((_this3.captions.meta.get(track) || {}).default);
+ };
+
+ var sorted = Array.from(tracks).sort(function (a, b) {
+ return sortIsDefault(b) - sortIsDefault(a);
+ });
+ var track;
+ languages.every(function (language) {
+ track = sorted.find(function (track) {
+ return track.language === language;
+ });
+ return !track; // Break iteration if there is a match
+ }); // If no match is found but is required, get first
+
+ return track || (force ? sorted[0] : undefined);
+ },
+ // Get the current track
+ getCurrentTrack: function getCurrentTrack() {
+ return captions.getTracks.call(this)[this.currentTrack];
+ },
+ // Get UI label for track
+ getLabel: function getLabel(track) {
+ var currentTrack = track;
+
+ if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {
+ currentTrack = captions.getCurrentTrack.call(this);
+ }
+
+ if (is.track(currentTrack)) {
+ if (!is.empty(currentTrack.label)) {
+ return currentTrack.label;
}
- if (active) {
- utils.toggleClass(this.elements.container, this.config.classNames.captions.active, true);
- utils.toggleState(this.elements.buttons.captions, true);
+ if (!is.empty(currentTrack.language)) {
+ return track.language.toUpperCase();
}
- }
-};
-// ==========================================================================
-// Console wrapper
-// ==========================================================================
+ return i18n.get('enabled', this.config);
+ }
-var noop = function noop() {};
+ return i18n.get('disabled', this.config);
+ },
+ // Update captions using current track's active cues
+ // Also optional array argument in case there isn't any track (ex: vimeo)
+ updateCues: function updateCues(input) {
+ // Requires UI
+ if (!this.supported.ui) {
+ return;
+ }
-var Console = function () {
- function Console() {
- var enabled = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
- classCallCheck(this, Console);
+ if (!is.element(this.elements.captions)) {
+ this.debug.warn('No captions element to render to');
+ return;
+ } // Only accept array or empty input
- this.enabled = window.console && enabled;
- if (this.enabled) {
- this.log('Debugging enabled');
- }
- }
+ if (!is.nullOrUndefined(input) && !Array.isArray(input)) {
+ this.debug.warn('updateCues: Invalid input', input);
+ return;
+ }
- 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;
-}();
+ var cues = input; // Get cues from track
-// ==========================================================================
-// Plyr default config
-// ==========================================================================
+ if (!cues) {
+ var track = captions.getCurrentTrack.call(this);
+ cues = Array.from((track || {}).activeCues || []).map(function (cue) {
+ return cue.getCueAsHTML();
+ }).map(getHTML);
+ } // Set new caption text
-var defaults$1 = {
+
+ var content = cues.map(function (cueText) {
+ return cueText.trim();
+ }).join('\n');
+ var changed = content !== this.elements.captions.innerHTML;
+
+ if (changed) {
+ // Empty the container and create a new child element
+ emptyElement(this.elements.captions);
+ var caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));
+ caption.innerHTML = content;
+ this.elements.captions.appendChild(caption); // Trigger event
+
+ triggerEvent.call(this, this.media, 'cuechange');
+ }
+ }
+ };
+
+ // ==========================================================================
+ // Plyr default config
+ // ==========================================================================
+ var defaults = {
// Disable
enabled: true,
-
// Custom media title
title: '',
-
// Logging to console
debug: false,
-
// Auto play (if supported)
autoplay: false,
-
// Only allow one media playing at once (vimeo only)
autopause: true,
-
+ // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present)
+ // TODO: Remove iosNative fullscreen option in favour of this (logic needs work)
+ playsinline: true,
// Default time to skip when rewind/fast forward
seekTime: 10,
-
// Default volume
volume: 1,
muted: false,
-
// Pass a custom duration
duration: null,
-
// Display the media duration on load in the current time position
// If you have opted to display both duration and currentTime, this is ignored
displayDuration: true,
-
// Invert the current time to be a countdown
invertTime: true,
-
// Clicking the currentTime inverts it's value to show time left rather than elapsed
toggleInvert: true,
-
// Aspect ratio (for embeds)
ratio: '16:9',
-
// Click video container to play/pause
clickToPlay: true,
-
// Auto hide the controls
hideControls: true,
-
// Reset to start when playback ended
resetOnEnd: false,
-
// Disable the standard context menu
disableContextMenu: true,
-
// Sprite (for icons)
loadSprite: true,
iconPrefix: 'plyr',
- iconUrl: 'https://cdn.plyr.io/3.3.6/plyr.svg',
-
+ iconUrl: 'https://cdn.plyr.io/3.4.8/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']
+ default: 576,
+ options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240]
},
-
// Set loops
loop: {
- active: false
- // start: null,
- // end: null,
- },
+ 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]
+ selected: 1,
+ options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
},
-
// Keyboard shortcut settings
keyboard: {
- focused: true,
- global: false
+ focused: true,
+ global: false
},
-
// Display tooltips
tooltips: {
- controls: false,
- seek: true
+ controls: false,
+ seek: true
},
-
// Captions settings
captions: {
- active: false,
- language: (navigator.language || navigator.userLanguage).split('-')[0]
+ active: false,
+ language: 'auto',
+ // Listen to new tracks added after Plyr is initialized.
+ // This is needed for streaming captions, but may result in unselectable options
+ update: false
},
-
// Fullscreen settings
fullscreen: {
- enabled: true, // Allow fullscreen?
- fallback: true, // Fallback for vintage browsers
- iosNative: false // Use the native fullscreen in iOS (disables custom controls)
- },
+ enabled: true,
+ // Allow fullscreen?
+ fallback: true,
+ // Fallback using full viewport/window
+ iosNative: false // Use the native fullscreen in iOS (disables custom controls)
+ },
// Local storage
storage: {
- enabled: true,
- key: 'plyr'
+ enabled: true,
+ key: 'plyr'
},
-
// Default controls
- controls: ['play-large',
- // 'restart',
+ controls: ['play-large', // 'restart',
// 'rewind',
- 'play',
- // 'fast-forward',
- 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
+ 'play', // 'fast-forward',
+ 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', // 'download',
+ 'fullscreen'],
settings: ['captions', 'quality', 'speed'],
-
// Localisation
i18n: {
- restart: 'Restart',
- rewind: 'Rewind {seektime}s',
- play: 'Play',
- pause: 'Pause',
- fastForward: 'Forward {seektime}s',
- 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'
+ restart: 'Restart',
+ rewind: 'Rewind {seektime}s',
+ play: 'Play',
+ pause: 'Pause',
+ fastForward: 'Forward {seektime}s',
+ seek: 'Seek',
+ seekLabel: '{currentTime} of {duration}',
+ played: 'Played',
+ buffered: 'Buffered',
+ currentTime: 'Current time',
+ duration: 'Duration',
+ volume: 'Volume',
+ mute: 'Mute',
+ unmute: 'Unmute',
+ enableCaptions: 'Enable captions',
+ disableCaptions: 'Disable captions',
+ download: 'Download',
+ enterFullscreen: 'Enter fullscreen',
+ exitFullscreen: 'Exit fullscreen',
+ frameTitle: 'Player for {title}',
+ captions: 'Captions',
+ settings: 'Settings',
+ menuBack: 'Go back to previous menu',
+ speed: 'Speed',
+ normal: 'Normal',
+ quality: 'Quality',
+ loop: 'Loop',
+ start: 'Start',
+ end: 'End',
+ all: 'All',
+ reset: 'Reset',
+ disabled: 'Disabled',
+ enabled: 'Enabled',
+ advertisement: 'Ad',
+ qualityBadge: {
+ 2160: '4K',
+ 1440: 'HD',
+ 1080: 'HD',
+ 720: 'HD',
+ 576: 'SD',
+ 480: 'SD'
+ }
},
-
// URLs
urls: {
- vimeo: {
- sdk: 'https://player.vimeo.com/api/player.js',
- iframe: 'https://player.vimeo.com/video/{0}?{1}',
- api: 'https://vimeo.com/api/v2/video/{0}.json'
- },
- youtube: {
- sdk: 'https://www.youtube.com/iframe_api',
- api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet',
- 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'
- }
+ download: null,
+ vimeo: {
+ sdk: 'https://player.vimeo.com/api/player.js',
+ iframe: 'https://player.vimeo.com/video/{0}?{1}',
+ api: 'https://vimeo.com/api/v2/video/{0}.json'
+ },
+ youtube: {
+ sdk: 'https://www.youtube.com/iframe_api',
+ api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet'
+ },
+ googleIMA: {
+ sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'
+ }
},
-
// Custom control listeners
listeners: {
- seek: null,
- play: null,
- pause: null,
- restart: null,
- rewind: null,
- fastForward: null,
- mute: null,
- volume: null,
- captions: null,
- fullscreen: null,
- pip: null,
- airplay: null,
- speed: null,
- quality: null,
- loop: null,
- language: null
+ seek: null,
+ play: null,
+ pause: null,
+ restart: null,
+ rewind: null,
+ fastForward: null,
+ mute: null,
+ volume: null,
+ captions: null,
+ download: 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
+ 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
+ 'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange', // Custom events
+ 'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready', // YouTube
+ 'statechange', // Quality
+ 'qualitychange', // Ads
'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],
-
// Selectors
// Change these to match your template if using custom HTML
selectors: {
- editable: 'input, textarea, select, [contenteditable]',
- container: '.plyr',
- controls: {
- container: null,
- wrapper: '.plyr__controls'
- },
- labels: '[data-plyr]',
- buttons: {
- play: '[data-plyr="play"]',
- pause: '[data-plyr="pause"]',
- restart: '[data-plyr="restart"]',
- rewind: '[data-plyr="rewind"]',
- fastForward: '[data-plyr="fast-forward"]',
- mute: '[data-plyr="mute"]',
- captions: '[data-plyr="captions"]',
- fullscreen: '[data-plyr="fullscreen"]',
- pip: '[data-plyr="pip"]',
- airplay: '[data-plyr="airplay"]',
- settings: '[data-plyr="settings"]',
- loop: '[data-plyr="loop"]'
- },
- inputs: {
- seek: '[data-plyr="seek"]',
- volume: '[data-plyr="volume"]',
- speed: '[data-plyr="speed"]',
- language: '[data-plyr="language"]',
- quality: '[data-plyr="quality"]'
- },
- display: {
- currentTime: '.plyr__time--current',
- duration: '.plyr__time--duration',
- buffer: '.plyr__progress--buffer',
- 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'
- }
+ 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"]',
+ download: '[data-plyr="download"]',
+ fullscreen: '[data-plyr="fullscreen"]',
+ pip: '[data-plyr="pip"]',
+ airplay: '[data-plyr="airplay"]',
+ settings: '[data-plyr="settings"]',
+ loop: '[data-plyr="loop"]'
+ },
+ inputs: {
+ seek: '[data-plyr="seek"]',
+ volume: '[data-plyr="volume"]',
+ speed: '[data-plyr="speed"]',
+ language: '[data-plyr="language"]',
+ quality: '[data-plyr="quality"]'
+ },
+ display: {
+ currentTime: '.plyr__time--current',
+ duration: '.plyr__time--duration',
+ buffer: '.plyr__progress__buffer',
+ loop: '.plyr__progress__loop',
+ // Used later
+ volume: '.plyr__volume--display'
+ },
+ progress: '.plyr__progress',
+ captions: '.plyr__captions',
+ caption: '.plyr__caption',
+ menu: {
+ quality: '.js-plyr__menu__list--quality'
+ }
},
-
// Class hooks added to the player in different states
classNames: {
- type: 'plyr--{0}',
- provider: 'plyr--{0}',
- video: 'plyr__video-wrapper',
- embed: 'plyr__video-embed',
- embedContainer: 'plyr__video-embed__container',
- poster: 'plyr__poster',
- ads: 'plyr__ads',
- control: 'plyr__control',
- 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'
+ type: 'plyr--{0}',
+ provider: 'plyr--{0}',
+ video: 'plyr__video-wrapper',
+ embed: 'plyr__video-embed',
+ embedContainer: 'plyr__video-embed__container',
+ poster: 'plyr__poster',
+ posterEnabled: 'plyr__poster-enabled',
+ ads: 'plyr__ads',
+ control: 'plyr__control',
+ controlPressed: 'plyr__control--pressed',
+ playing: 'plyr--playing',
+ paused: 'plyr--paused',
+ stopped: 'plyr--stopped',
+ loading: 'plyr--loading',
+ hover: 'plyr--hover',
+ tooltip: 'plyr__tooltip',
+ cues: 'plyr__cues',
+ hidden: 'plyr__sr-only',
+ hideControls: 'plyr--hide-controls',
+ isIos: 'plyr--is-ios',
+ isTouch: 'plyr--is-touch',
+ uiSupported: 'plyr--full-ui',
+ noTransition: 'plyr--no-transition',
+ display: {
+ time: 'plyr__time'
+ },
+ menu: {
+ value: 'plyr__menu__value',
+ badge: 'plyr__badge',
+ open: 'plyr--menu-open'
+ },
+ captions: {
+ enabled: 'plyr--captions-enabled',
+ active: 'plyr--captions-active'
+ },
+ fullscreen: {
+ enabled: 'plyr--fullscreen-enabled',
+ fallback: 'plyr--fullscreen-fallback'
+ },
+ pip: {
+ supported: 'plyr--pip-supported',
+ active: 'plyr--pip-active'
+ },
+ airplay: {
+ supported: 'plyr--airplay-supported',
+ active: 'plyr--airplay-active'
+ },
+ tabFocus: 'plyr__tab-focus',
+ previewThumbnails: {
+ // Tooltip thumbs
+ thumbContainer: 'plyr__preview-thumb',
+ thumbContainerShown: 'plyr__preview-thumb--is-shown',
+ imageContainer: 'plyr__preview-thumb__image-container',
+ timeContainer: 'plyr__preview-thumb__time-container',
+ // Scrubbing
+ scrubbingContainer: 'plyr__preview-scrubbing',
+ scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'
+ }
},
-
// Embed attributes
attributes: {
- embed: {
- provider: 'data-plyr-provider',
- id: 'data-plyr-embed-id'
- }
+ embed: {
+ provider: 'data-plyr-provider',
+ id: 'data-plyr-embed-id'
+ }
},
-
// API keys
keys: {
- google: null
+ google: null
},
-
// Advertisements plugin
// Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: {
- enabled: false,
- publisherId: ''
+ enabled: false,
+ publisherId: '',
+ tagUrl: ''
+ },
+ // Preview Thumbnails plugin
+ previewThumbnails: {
+ enabled: false,
+ src: ''
+ },
+ // Vimeo plugin
+ vimeo: {
+ byline: false,
+ portrait: false,
+ title: false,
+ speed: true,
+ transparent: false
+ },
+ // YouTube plugin
+ youtube: {
+ noCookie: false,
+ // Whether to use an alternative version of YouTube without cookies
+ rel: 0,
+ // No related vids
+ showinfo: 0,
+ // Hide info
+ iv_load_policy: 3,
+ // Hide annotations
+ modestbranding: 1 // Hide logos as much as possible (they still show one in the corner when paused)
+
}
-};
+ };
-// ==========================================================================
+ // ==========================================================================
+ // Plyr states
+ // ==========================================================================
+ var pip = {
+ active: 'picture-in-picture',
+ inactive: 'inline'
+ };
-var browser$2 = utils.getBrowser();
+ // ==========================================================================
+ // Plyr supported types and providers
+ // ==========================================================================
+ var providers = {
+ html5: 'html5',
+ youtube: 'youtube',
+ vimeo: 'vimeo'
+ };
+ var types = {
+ audio: 'audio',
+ video: 'video'
+ };
+ /**
+ * Get provider by URL
+ * @param {String} url
+ */
-function onChange() {
- if (!this.enabled) {
- return;
+ function getProviderByUrl(url) {
+ // YouTube
+ if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) {
+ return providers.youtube;
+ } // Vimeo
+
+
+ if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
+ return providers.vimeo;
}
- // Update toggle button
- var button = this.player.elements.buttons.fullscreen;
- if (utils.is.element(button)) {
- utils.toggleState(button, this.active);
+ return null;
+ }
+
+ // ==========================================================================
+ // Console wrapper
+ // ==========================================================================
+ var noop = function noop() {};
+
+ var Console =
+ /*#__PURE__*/
+ 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');
+ }
}
- // Trigger an event
- utils.dispatchEvent(this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
+ _createClass(Console, [{
+ key: "log",
+ get: function get() {
+ // eslint-disable-next-line no-console
+ return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;
+ }
+ }, {
+ key: "warn",
+ get: function get() {
+ // eslint-disable-next-line no-console
+ return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;
+ }
+ }, {
+ key: "error",
+ get: function get() {
+ // eslint-disable-next-line no-console
+ return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;
+ }
+ }]);
+
+ return Console;
+ }();
+
+ function onChange() {
+ if (!this.enabled) {
+ return;
+ } // Update toggle button
+
+
+ var button = this.player.elements.buttons.fullscreen;
+
+ if (is.element(button)) {
+ button.pressed = this.active;
+ } // Trigger an event
+
- // Trap focus in container
- if (!browser$2.isIos) {
- utils.trapFocus.call(this.player, this.target, this.active);
+ triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); // Trap focus in container
+
+ if (!browser.isIos) {
+ trapFocus.call(this.player, this.target, this.active);
}
-}
+ }
+
+ function toggleFallback() {
+ var _this = this;
-function toggleFallback() {
var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
// Store or restore scroll position
if (toggle) {
- this.scrollPosition = {
- x: window.scrollX || 0,
- y: window.scrollY || 0
- };
+ this.scrollPosition = {
+ x: window.scrollX || 0,
+ y: window.scrollY || 0
+ };
} else {
- window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
- }
+ window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);
+ } // Toggle scroll
- // Toggle scroll
- document.body.style.overflow = toggle ? 'hidden' : '';
- // Toggle class hook
- utils.toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);
+ document.body.style.overflow = toggle ? 'hidden' : ''; // Toggle class hook
- // Toggle button and fire events
- onChange.call(this);
-}
+ toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); // Force full viewport on iPhone X+
-var Fullscreen = function () {
- function Fullscreen(player) {
- var _this = this;
+ if (browser.isIos) {
+ var viewport = document.head.querySelector('meta[name="viewport"]');
+ var property = 'viewport-fit=cover'; // Inject the viewport meta if required
- classCallCheck(this, Fullscreen);
+ if (!viewport) {
+ viewport = document.createElement('meta');
+ viewport.setAttribute('name', 'viewport');
+ } // Check if the property already exists
- // Keep reference to parent
- this.player = player;
- // Get prefix
- this.prefix = Fullscreen.prefix;
- this.property = Fullscreen.property;
+ var hasProperty = is.string(viewport.content) && viewport.content.includes(property);
- // Scroll position
- this.scrollPosition = { x: 0, y: 0 };
+ if (toggle) {
+ this.cleanupViewport = !hasProperty;
- // 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);
- });
+ if (!hasProperty) {
+ viewport.content += ",".concat(property);
+ }
+ } else if (this.cleanupViewport) {
+ viewport.content = viewport.content.split(',').filter(function (part) {
+ return part.trim() !== property;
+ }).join(',');
+ } // Force a repaint as sometimes Safari doesn't want to fill the screen
- // 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;
- }
- _this.toggle();
- });
+ setTimeout(function () {
+ return repaint(_this.target);
+ }, 100);
+ } // Toggle button and fire events
- // Update the UI
- this.update();
- }
- // Determine if native supported
+ onChange.call(this);
+ }
+ var Fullscreen =
+ /*#__PURE__*/
+ function () {
+ function Fullscreen(player) {
+ var _this2 = this;
- createClass(Fullscreen, [{
- key: 'update',
+ _classCallCheck(this, Fullscreen);
+ // Keep reference to parent
+ this.player = player; // Get prefix
- // Update UI
- value: function update() {
- if (this.enabled) {
- this.player.debug.log((Fullscreen.native ? 'Native' : 'Fallback') + ' fullscreen enabled');
- } else {
- this.player.debug.log('Fullscreen not supported and fallback disabled');
- }
+ this.prefix = Fullscreen.prefix;
+ this.property = Fullscreen.property; // Scroll position
- // Add styling hook to show button
- utils.toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
- }
+ this.scrollPosition = {
+ x: 0,
+ y: 0
+ }; // Force the use of 'full window/browser' rather than fullscreen
- // Make an element fullscreen
+ this.forceFallback = player.config.fullscreen.fallback === 'force'; // Register event listeners
+ // Handle event (incase user presses escape etc)
- }, {
- key: 'enter',
- value: function enter() {
- if (!this.enabled) {
- return;
- }
+ on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : "".concat(this.prefix, "fullscreenchange"), function () {
+ // TODO: Filter for target??
+ onChange.call(_this2);
+ }); // Fullscreen toggle on double click
- // 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]();
- }
+ on.call(this.player, this.player.elements.container, 'dblclick', function (event) {
+ // Ignore double click in controls
+ if (is.element(_this2.player.elements.controls) && _this2.player.elements.controls.contains(event.target)) {
+ return;
}
- // Bail from fullscreen
+ _this2.toggle();
+ }); // Update the UI
- }, {
- key: 'exit',
- value: function exit() {
- if (!this.enabled) {
- return;
- }
+ this.update();
+ } // Determine if native supported
- // 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]();
- }
- }
- // Toggle state
+ _createClass(Fullscreen, [{
+ key: "update",
+ // Update UI
+ value: function update() {
+ if (this.enabled) {
+ var mode;
- }, {
- key: 'toggle',
- value: function toggle() {
- if (!this.active) {
- this.enter();
- } else {
- this.exit();
- }
- }
- }, {
- key: 'enabled',
+ if (this.forceFallback) {
+ mode = 'Fallback (forced)';
+ } else if (Fullscreen.native) {
+ mode = 'Native';
+ } else {
+ mode = 'Fallback';
+ }
+ this.player.debug.log("".concat(mode, " fullscreen enabled"));
+ } else {
+ this.player.debug.log('Fullscreen not supported and fallback disabled');
+ } // Add styling hook to show button
- // 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
+ toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
+ } // Make an element fullscreen
}, {
- 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);
- }
+ key: "enter",
+ value: function enter() {
+ if (!this.enabled) {
+ return;
+ } // iOS native fullscreen doesn't need the request step
- var element = !this.prefix ? document.fullscreenElement : document['' + this.prefix + this.property + 'Element'];
- return element === this.target;
+ if (browser.isIos && this.player.config.fullscreen.iosNative) {
+ this.target.webkitEnterFullscreen();
+ } else if (!Fullscreen.native || this.forceFallback) {
+ toggleFallback.call(this, true);
+ } else if (!this.prefix) {
+ this.target.requestFullscreen();
+ } else if (!is.empty(this.prefix)) {
+ this.target["".concat(this.prefix, "Request").concat(this.property)]();
}
+ } // Bail from fullscreen
- // Get target element
+ }, {
+ 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 || this.forceFallback) {
+ toggleFallback.call(this, false);
+ } else if (!this.prefix) {
+ (document.cancelFullScreen || document.exitFullscreen).call(document);
+ } else if (!is.empty(this.prefix)) {
+ var action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
+ document["".concat(this.prefix).concat(action).concat(this.property)]();
+ }
+ } // Toggle state
}, {
- key: 'target',
- get: function get$$1() {
- return browser$2.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);
+ key: "toggle",
+ value: function toggle() {
+ if (!this.active) {
+ this.enter();
+ } else {
+ this.exit();
}
+ }
+ }, {
+ key: "usingNative",
+ // If we're actually using native
+ get: function get() {
+ return Fullscreen.native && !this.forceFallback;
+ } // Get the prefix for handlers
- // Get the prefix for handlers
+ }, {
+ key: "enabled",
+ // Determine if fullscreen is enabled
+ get: function get() {
+ return (Fullscreen.native || this.player.config.fullscreen.fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo;
+ } // Get active state
}, {
- key: 'prefix',
- get: function get$$1() {
- // No prefix
- if (utils.is.function(document.exitFullscreen)) {
- return '';
- }
+ key: "active",
+ get: function get() {
+ if (!this.enabled) {
+ return false;
+ } // Fallback using classname
- // Check for fullscreen support by vendor prefix
- var value = '';
- var prefixes = ['webkit', 'moz', 'ms'];
- prefixes.some(function (pre) {
- if (utils.is.function(document[pre + 'ExitFullscreen']) || utils.is.function(document[pre + 'CancelFullScreen'])) {
- value = pre;
- return true;
- }
+ if (!Fullscreen.native || this.forceFallback) {
+ return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
+ }
- return false;
- });
+ var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
+ return element === this.target;
+ } // Get target element
- return value;
- }
}, {
- key: 'property',
- get: function get$$1() {
- return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
- }
+ key: "target",
+ get: function get() {
+ return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
+ }
+ }], [{
+ key: "native",
+ get: function get() {
+ return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
+ }
+ }, {
+ key: "prefix",
+ get: function get() {
+ // No prefix
+ if (is.function(document.exitFullscreen)) {
+ return '';
+ } // Check for fullscreen support by vendor prefix
+
+
+ var value = '';
+ var prefixes = ['webkit', 'moz', 'ms'];
+ prefixes.some(function (pre) {
+ if (is.function(document["".concat(pre, "ExitFullscreen")]) || is.function(document["".concat(pre, "CancelFullScreen")])) {
+ value = pre;
+ return true;
+ }
+
+ return false;
+ });
+ return value;
+ }
+ }, {
+ key: "property",
+ get: function get() {
+ return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
+ }
}]);
+
return Fullscreen;
-}();
+ }();
+
+ // ==========================================================================
+ // Load image avoiding xhr/fetch CORS issues
+ // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded
+ // By default it checks if it is at least 1px, but you can add a second argument to change this
+ // ==========================================================================
+ function loadImage(src) {
+ var minWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+ return new Promise(function (resolve, reject) {
+ var image = new Image();
+
+ var handler = function handler() {
+ delete image.onload;
+ delete image.onerror;
+ (image.naturalWidth >= minWidth ? resolve : reject)(image);
+ };
+
+ Object.assign(image, {
+ onload: handler,
+ onerror: handler,
+ src: src
+ });
+ });
+ }
+
+ // ==========================================================================
+ var ui = {
+ addStyleHook: function addStyleHook() {
+ toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
+ toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
+ },
+ // Toggle native HTML5 media controls
+ toggleNativeControls: function toggleNativeControls() {
+ var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ if (toggle && this.isHTML5) {
+ this.media.setAttribute('controls', '');
+ } else {
+ this.media.removeAttribute('controls');
+ }
+ },
+ // Setup the UI
+ build: function build() {
+ var _this = this;
-// ==========================================================================
+ // Re-attach media element listeners
+ // TODO: Use event bubbling?
+ this.listeners.media(); // Don't setup interface if no support
-// Sniff out the browser
-var browser$3 = utils.getBrowser();
+ if (!this.supported.ui) {
+ this.debug.warn("Basic support only for ".concat(this.provider, " ").concat(this.type)); // Restore native controls
-var Listeners = function () {
- function Listeners(player) {
- classCallCheck(this, Listeners);
+ ui.toggleNativeControls.call(this, true); // Bail
+
+ return;
+ } // Inject custom controls if not present
+
+
+ if (!is.element(this.elements.controls)) {
+ // Inject custom controls
+ controls.inject.call(this); // Re-attach control listeners
+
+ this.listeners.controls();
+ } // Remove native controls
+
+
+ ui.toggleNativeControls.call(this); // Setup captions for HTML5
+
+ if (this.isHTML5) {
+ 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
+
+ controls.updateVolume.call(this); // Reset time display
+
+ controls.timeUpdate.call(this); // Update the UI
+
+ ui.checkPlaying.call(this); // Check for picture-in-picture support
+
+ toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support
+
+ toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class
+
+ toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class
- this.player = player;
- this.lastKey = null;
+ toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls
- this.handleKey = this.handleKey.bind(this);
- this.toggleMenu = this.toggleMenu.bind(this);
- this.firstTouch = this.firstTouch.bind(this);
+ this.ready = true; // Ready event at end of execution stack
+
+ setTimeout(function () {
+ triggerEvent.call(_this, _this.media, 'ready');
+ }, 0); // Set the title
+
+ ui.setTitle.call(this); // Assure the poster image is set, if the property was added before the element was created
+
+ if (this.poster) {
+ ui.setPoster.call(this, this.poster, false).catch(function () {});
+ } // Manually set the duration if user has overridden it.
+ // The event listeners for it doesn't get called if preload is disabled (#701)
+
+
+ if (this.config.duration) {
+ controls.durationUpdate.call(this);
+ }
+ },
+ // 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 (is.string(this.config.title) && !is.empty(this.config.title)) {
+ label += ", ".concat(this.config.title);
+ } // If there's a play button, set label
+
+
+ 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 = getElement.call(this, 'iframe');
+
+ if (!is.element(iframe)) {
+ return;
+ } // Default to media type
+
+
+ var title = !is.empty(this.config.title) ? this.config.title : 'video';
+ var format = i18n.get('frameTitle', this.config);
+ iframe.setAttribute('title', format.replace('{title}', title));
+ }
+ },
+ // Toggle poster
+ togglePoster: function togglePoster(enable) {
+ toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
+ },
+ // Set the poster image (async)
+ // Used internally for the poster setter, with the passive option forced to false
+ setPoster: function setPoster(poster) {
+ var _this2 = this;
+
+ var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+
+ // Don't override if call is passive
+ if (passive && this.poster) {
+ return Promise.reject(new Error('Poster already set'));
+ } // Set property synchronously to respect the call order
+
+
+ this.media.setAttribute('poster', poster); // Wait until ui is ready
+
+ return ready.call(this) // Load image
+ .then(function () {
+ return loadImage(poster);
+ }).catch(function (err) {
+ // Hide poster on error unless it's been set by another call
+ if (poster === _this2.poster) {
+ ui.togglePoster.call(_this2, false);
+ } // Rethrow
+
+
+ throw err;
+ }).then(function () {
+ // Prevent race conditions
+ if (poster !== _this2.poster) {
+ throw new Error('setPoster cancelled by later call to setPoster');
+ }
+ }).then(function () {
+ Object.assign(_this2.elements.poster.style, {
+ backgroundImage: "url('".concat(poster, "')"),
+ // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
+ backgroundSize: ''
+ });
+ ui.togglePoster.call(_this2, true);
+ return poster;
+ });
+ },
+ // Check playing state
+ checkPlaying: function checkPlaying(event) {
+ var _this3 = this;
+
+ // Class hooks
+ toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
+ toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
+ toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); // Set state
+
+ Array.from(this.elements.buttons.play || []).forEach(function (target) {
+ target.pressed = _this3.playing;
+ }); // Only update controls on non timeupdate events
+
+ if (is.event(event) && event.type === 'timeupdate') {
+ return;
+ } // Toggle controls
+
+
+ ui.toggleControls.call(this);
+ },
+ // Check if media is loading
+ checkLoading: function checkLoading(event) {
+ var _this4 = 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 () {
+ // Update progress bar loading class state
+ toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading); // Update controls visibility
+
+ ui.toggleControls.call(_this4);
+ }, this.loading ? 250 : 0);
+ },
+ // Toggle controls based on state and `force` argument
+ toggleControls: function toggleControls(force) {
+ var controls$$1 = this.elements.controls;
+
+ if (controls$$1 && this.config.hideControls) {
+ // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
+ var recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
+
+ this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover || recentTouchSeek));
+ }
}
+ };
- // Handle key presses
+ /* function reduceAspectRatio(width, height) {
+ const getRatio = (w, h) => (h === 0 ? w : getRatio(h, w % h));
+ const ratio = getRatio(width, height);
+ return `${width / ratio}:${height / ratio}`;
+ } */
+ // Set aspect ratio for responsive container
+ function setAspectRatio(input) {
+ var ratio = input;
- createClass(Listeners, [{
- key: 'handleKey',
- value: function handleKey(event) {
- var _this = this;
+ if (!is.string(ratio) && !is.nullOrUndefined(this.embed)) {
+ ratio = this.embed.ratio;
+ }
- var code = event.keyCode ? event.keyCode : event.which;
- var pressed = event.type === 'keydown';
- var repeat = pressed && code === this.lastKey;
+ if (!is.string(ratio)) {
+ ratio = this.config.ratio;
+ }
- // Bail if a modifier key is set
- if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
- return;
- }
+ var _ratio$split$map = ratio.split(':').map(Number),
+ _ratio$split$map2 = _slicedToArray(_ratio$split$map, 2),
+ x = _ratio$split$map2[0],
+ y = _ratio$split$map2[1];
- // If the event is bubbled from the media element
- // Firefox doesn't get the keycode for whatever reason
- if (!utils.is.number(code)) {
- return;
- }
+ var padding = 100 / x * y;
+ this.elements.wrapper.style.paddingBottom = "".concat(padding, "%"); // For Vimeo we have an extra <div> to hide the standard controls and UI
- // Seek by the number keys
- var seekByKey = function seekByKey() {
- // Divide the max duration into 10th's and times by the number value
- _this.player.currentTime = _this.player.duration / 10 * (code - 48);
- };
+ if (this.isVimeo && this.supported.ui) {
+ var height = 240;
+ var offset = (height - padding) / (height / 50);
+ this.media.style.transform = "translateY(-".concat(offset, "%)");
+ }
- // Handle the key on keydown
- // Reset on keyup
- if (pressed) {
- // Which keycodes should we prevent default
- var preventDefault = [48, 49, 50, 51, 52, 53, 54, 56, 57, 32, 75, 38, 40, 77, 39, 37, 70, 67, 73, 76, 79];
-
- // Check focused element
- // and if the focused element is not editable (e.g. text input)
- // and any that accept key input http://webaim.org/techniques/keyboard/
- var focused = utils.getFocusElement();
- if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) {
- return;
- }
+ return {
+ padding: padding,
+ ratio: ratio
+ };
+ }
- // If the code is found prevent default (e.g. prevent scrolling for arrows)
- if (preventDefault.includes(code)) {
- event.preventDefault();
- event.stopPropagation();
- }
+ var Listeners =
+ /*#__PURE__*/
+ function () {
+ function Listeners(player) {
+ _classCallCheck(this, Listeners);
+
+ this.player = player;
+ this.lastKey = null;
+ this.focusTimer = null;
+ this.lastKeyDown = null;
+ this.handleKey = this.handleKey.bind(this);
+ this.toggleMenu = this.toggleMenu.bind(this);
+ this.setTabFocus = this.setTabFocus.bind(this);
+ this.firstTouch = this.firstTouch.bind(this);
+ } // Handle key presses
+
+
+ _createClass(Listeners, [{
+ key: "handleKey",
+ value: function handleKey(event) {
+ var player = this.player;
+ var elements = player.elements;
+ var code = event.keyCode ? event.keyCode : event.which;
+ var pressed = event.type === 'keydown';
+ var repeat = pressed && code === this.lastKey; // Bail if a modifier key is set
+
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
+ return;
+ } // If the event is bubbled from the media element
+ // Firefox doesn't get the keycode for whatever reason
+
+
+ if (!is.number(code)) {
+ return;
+ } // Seek by the number keys
+
+
+ var seekByKey = function seekByKey() {
+ // Divide the max duration into 10th's and times by the number value
+ player.currentTime = player.duration / 10 * (code - 48);
+ }; // Handle the key on keydown
+ // Reset on keyup
+
+
+ if (pressed) {
+ // Check focused element
+ // and if the focused element is not editable (e.g. text input)
+ // and any that accept key input http://webaim.org/techniques/keyboard/
+ var focused = document.activeElement;
+
+ if (is.element(focused)) {
+ var editable = player.config.selectors.editable;
+ var seek = elements.inputs.seek;
+
+ if (focused !== seek && matches(focused, editable)) {
+ return;
+ }
+
+ if (event.which === 32 && matches(focused, 'button, [role^="menuitem"]')) {
+ return;
+ }
+ } // Which keycodes should we prevent default
+
+
+ var preventDefault = [32, 37, 38, 39, 40, 48, 49, 50, 51, 52, 53, 54, 56, 57, 67, 70, 73, 75, 76, 77, 79]; // If the code is found prevent default (e.g. prevent scrolling for arrows)
+
+ if (preventDefault.includes(code)) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ switch (code) {
+ case 48:
+ case 49:
+ case 50:
+ case 51:
+ case 52:
+ case 53:
+ case 54:
+ case 55:
+ case 56:
+ case 57:
+ // 0-9
+ if (!repeat) {
+ seekByKey();
+ }
+
+ break;
+
+ case 32:
+ case 75:
+ // Space and K key
+ if (!repeat) {
+ player.togglePlay();
+ }
+
+ break;
+
+ case 38:
+ // Arrow up
+ player.increaseVolume(0.1);
+ break;
+
+ case 40:
+ // Arrow down
+ player.decreaseVolume(0.1);
+ break;
+
+ case 77:
+ // M key
+ if (!repeat) {
+ player.muted = !player.muted;
+ }
+
+ break;
+
+ case 39:
+ // Arrow forward
+ player.forward();
+ break;
+
+ case 37:
+ // Arrow back
+ player.rewind();
+ break;
+
+ case 70:
+ // F key
+ player.fullscreen.toggle();
+ break;
+
+ case 67:
+ // C key
+ if (!repeat) {
+ player.toggleCaptions();
+ }
+
+ break;
+
+ case 76:
+ // L key
+ player.loop = !player.loop;
+ break;
+
+ /* case 73:
+ this.setLoop('start');
+ break;
+ case 76:
+ this.setLoop();
+ break;
+ case 79:
+ this.setLoop('end');
+ break; */
- switch (code) {
- case 48:
- case 49:
- case 50:
- case 51:
- case 52:
- case 53:
- case 54:
- case 55:
- case 56:
- case 57:
- // 0-9
- if (!repeat) {
- seekByKey();
- }
- break;
-
- case 32:
- case 75:
- // Space and K key
- if (!repeat) {
- this.player.togglePlay();
- }
- break;
-
- case 38:
- // Arrow up
- this.player.increaseVolume(0.1);
- break;
-
- case 40:
- // Arrow down
- this.player.decreaseVolume(0.1);
- break;
-
- case 77:
- // M key
- if (!repeat) {
- this.player.muted = !this.player.muted;
- }
- break;
-
- case 39:
- // Arrow forward
- this.player.forward();
- break;
-
- case 37:
- // Arrow back
- this.player.rewind();
- break;
-
- case 70:
- // F key
- this.player.fullscreen.toggle();
- break;
-
- case 67:
- // C key
- if (!repeat) {
- this.player.toggleCaptions();
- }
- break;
-
- case 76:
- // L key
- this.player.loop = !this.player.loop;
- break;
-
- /* case 73:
- this.setLoop('start');
- break;
- case 76:
- this.setLoop();
- break;
- case 79:
- this.setLoop('end');
- break; */
-
- default:
- break;
- }
+ default:
+ break;
+ } // Escape is handle natively when in full screen
+ // So we only need to worry about non native
- // Escape is handle natively when in full screen
- // So we only need to worry about non native
- if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) {
- this.player.fullscreen.toggle();
- }
- // Store last code for next cycle
- this.lastKey = code;
- } else {
- this.lastKey = null;
- }
- }
+ if (code === 27 && !player.fullscreen.usingNative && player.fullscreen.active) {
+ player.fullscreen.toggle();
+ } // Store last code for next cycle
- // Toggle menu
- }, {
- key: 'toggleMenu',
- value: function toggleMenu(event) {
- controls.toggleMenu.call(this.player, event);
+ this.lastKey = code;
+ } else {
+ this.lastKey = null;
}
+ } // Toggle menu
- // Device is touch enabled
+ }, {
+ key: "toggleMenu",
+ value: function toggleMenu(event) {
+ controls.toggleMenu.call(this.player, event);
+ } // Device is touch enabled
}, {
- key: 'firstTouch',
- value: function firstTouch() {
- this.player.touch = true;
+ key: "firstTouch",
+ value: function firstTouch() {
+ var player = this.player;
+ var elements = player.elements;
+ player.touch = true; // Add touch class
- // Add touch class
- utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
+ toggleClass(elements.container, player.config.classNames.isTouch, true);
+ }
+ }, {
+ key: "setTabFocus",
+ value: function setTabFocus(event) {
+ var player = this.player;
+ var elements = player.elements;
+ clearTimeout(this.focusTimer); // Ignore any key other than tab
- // Clean up
- utils.off(document.body, 'touchstart', this.firstTouch);
- }
+ if (event.type === 'keydown' && event.which !== 9) {
+ return;
+ } // Store reference to event timeStamp
+
+
+ if (event.type === 'keydown') {
+ this.lastKeyDown = event.timeStamp;
+ } // Remove current classes
+
+
+ var removeCurrent = function removeCurrent() {
+ var className = player.config.classNames.tabFocus;
+ var current = getElements.call(player, ".".concat(className));
+ toggleClass(current, className, false);
+ }; // Determine if a key was pressed to trigger this event
+
+
+ var wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; // Ignore focus events if a key was pressed prior
- // Global window & document listeners
+ if (event.type === 'focus' && !wasKeyDown) {
+ return;
+ } // Remove all current
+
+
+ removeCurrent(); // Delay the adding of classname until the focus has changed
+ // This event fires before the focusin event
+
+ this.focusTimer = setTimeout(function () {
+ var focused = document.activeElement; // Ignore if current focus element isn't inside the player
+
+ if (!elements.container.contains(focused)) {
+ return;
+ }
+
+ toggleClass(document.activeElement, player.config.classNames.tabFocus, true);
+ }, 10);
+ } // Global window & document listeners
}, {
- key: 'global',
- value: function global() {
- var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+ key: "global",
+ value: function global() {
+ var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
+ var player = this.player; // Keyboard shortcuts
- // Keyboard shortcuts
- if (this.player.config.keyboard.global) {
- utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false);
- }
+ if (player.config.keyboard.global) {
+ toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);
+ } // Click anywhere closes menu
- // Click anywhere closes menu
- utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
- // Detect touch by events
- utils.on(document.body, 'touchstart', this.firstTouch);
- }
+ toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); // Detect touch by events
+
+ once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
- // Container listeners
+ toggleListener.call(player, document.body, 'keydown focus blur', this.setTabFocus, toggle, false, true);
+ } // Container listeners
}, {
- key: 'container',
- value: function container() {
- var _this2 = this;
+ key: "container",
+ value: function container() {
+ var player = this.player;
+ var config = player.config,
+ elements = player.elements,
+ timers = player.timers; // Keyboard shortcuts
- // Keyboard shortcuts
- if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
- utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false);
- }
+ if (!config.keyboard.global && config.keyboard.focused) {
+ on.call(player, elements.container, 'keydown keyup', this.handleKey, false);
+ } // Toggle controls on mouse events and entering fullscreen
- // Detect tab focus
- // Remove class on blur/focusout
- utils.on(this.player.elements.container, 'focusout', function (event) {
- utils.toggleClass(event.target, _this2.player.config.classNames.tabFocus, false);
- });
- // Add classname to tabbed elements
- utils.on(this.player.elements.container, 'keydown', function (event) {
- if (event.keyCode !== 9) {
- return;
- }
+ on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) {
+ var controls$$1 = elements.controls; // Remove button states for fullscreen
- // Delay the adding of classname until the focus has changed
- // This event fires before the focusin event
- setTimeout(function () {
- utils.toggleClass(utils.getFocusElement(), _this2.player.config.classNames.tabFocus, true);
- }, 0);
- });
+ if (controls$$1 && event.type === 'enterfullscreen') {
+ controls$$1.pressed = false;
+ controls$$1.hover = false;
+ } // Show, then hide after a timeout unless another control event occurs
- // Toggle controls visibility based on mouse movement
- if (this.player.config.hideControls) {
- // Toggle controls on mouse events and entering fullscreen
- utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', function (event) {
- _this2.player.toggleControls(event);
- });
- }
- }
- // Listen for media events
+ var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
+ var delay = 0;
- }, {
- key: 'media',
- value: function media() {
- var _this3 = this;
+ if (show) {
+ ui.toggleControls.call(player, true); // Use longer timeout for touch devices
- // Time change on media
- utils.on(this.player.media, 'timeupdate seeking', function (event) {
- return ui.timeUpdate.call(_this3.player, event);
- });
+ delay = player.touch ? 3000 : 2000;
+ } // Clear timer
- // Display duration
- utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', function (event) {
- return ui.durationUpdate.call(_this3.player, event);
- });
- // Check for audio tracks on load
- // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
- utils.on(this.player.media, 'loadeddata', function () {
- utils.toggleHidden(_this3.player.elements.volume, !_this3.player.hasAudio);
- utils.toggleHidden(_this3.player.elements.buttons.mute, !_this3.player.hasAudio);
- });
+ clearTimeout(timers.controls); // Set new timer to prevent flicker when seeking
- // 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.resetOnEnd) {
- // Restart
- _this3.player.restart();
- }
- });
+ timers.controls = setTimeout(function () {
+ return ui.toggleControls.call(player, false);
+ }, delay);
+ }); // Force edge to repaint on exit fullscreen
+ // TODO: Fix weird bug where Edge doesn't re-draw when exiting fullscreen
- // Check for buffer progress
- utils.on(this.player.media, 'progress playing', function (event) {
- return ui.updateProgress.call(_this3.player, event);
+ /* if (browser.isEdge) {
+ on.call(player, elements.container, 'exitfullscreen', () => {
+ setTimeout(() => repaint(elements.container), 100);
});
+ } */
+ // Set a gutter for Vimeo
- // Handle volume changes
- utils.on(this.player.media, 'volumechange', function (event) {
- return ui.updateVolume.call(_this3.player, event);
- });
+ var setGutter = function setGutter(ratio, padding, toggle) {
+ if (!player.isVimeo) {
+ return;
+ }
- // Handle play/pause
- utils.on(this.player.media, 'playing play pause ended emptied timeupdate', function (event) {
- return ui.checkPlaying.call(_this3.player, event);
- });
+ var target = player.elements.wrapper.firstChild;
- // Loading state
- utils.on(this.player.media, 'waiting canplay seeked playing', function (event) {
- return ui.checkLoading.call(_this3.player, event);
- });
+ var _ratio$split$map = ratio.split(':').map(Number),
+ _ratio$split$map2 = _slicedToArray(_ratio$split$map, 2),
+ height = _ratio$split$map2[1];
- // Check if media failed to load
- // utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
+ var _player$embed$ratio$s = player.embed.ratio.split(':').map(Number),
+ _player$embed$ratio$s2 = _slicedToArray(_player$embed$ratio$s, 2),
+ videoWidth = _player$embed$ratio$s2[0],
+ videoHeight = _player$embed$ratio$s2[1];
- // If autoplay, then load advertisement if required
- // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
- utils.on(this.player.media, 'playing', function () {
- if (!_this3.player.ads) {
- return;
- }
+ target.style.maxWidth = toggle ? "".concat(height / videoHeight * videoWidth, "px") : null;
+ target.style.margin = toggle ? '0 auto' : null;
+ }; // Resize on fullscreen change
- // If ads are enabled, wait for them first
- if (_this3.player.ads.enabled && !_this3.player.ads.initialized) {
- // Wait for manager response
- _this3.player.ads.managerPromise.then(function () {
- return _this3.player.ads.play();
- }).catch(function () {
- return _this3.player.play();
- });
- }
+
+ var setPlayerSize = function setPlayerSize(measure) {
+ // If we don't need to measure the viewport
+ if (!measure) {
+ return setAspectRatio.call(player);
+ }
+
+ var rect = elements.container.getBoundingClientRect();
+ var width = rect.width,
+ height = rect.height;
+ return setAspectRatio.call(player, "".concat(width, ":").concat(height));
+ };
+
+ var resized = function resized() {
+ window.clearTimeout(timers.resized);
+ timers.resized = window.setTimeout(setPlayerSize, 50);
+ };
+
+ on.call(player, elements.container, 'enterfullscreen exitfullscreen', function (event) {
+ var _player$fullscreen = player.fullscreen,
+ target = _player$fullscreen.target,
+ usingNative = _player$fullscreen.usingNative; // Ignore for iOS native
+
+ if (!player.isEmbed || target !== elements.container) {
+ return;
+ }
+
+ var isEnter = event.type === 'enterfullscreen'; // Set the player size when entering fullscreen to viewport size
+
+ var _setPlayerSize = setPlayerSize(isEnter),
+ padding = _setPlayerSize.padding,
+ ratio = _setPlayerSize.ratio; // Set Vimeo gutter
+
+
+ setGutter(ratio, padding, isEnter); // If not using native fullscreen, we need to check for resizes of viewport
+
+ if (!usingNative) {
+ if (isEnter) {
+ on.call(player, window, 'resize', resized);
+ } else {
+ off.call(player, window, 'resize', resized);
+ }
+ }
+ });
+ } // Listen for media events
+
+ }, {
+ key: "media",
+ value: function media() {
+ var _this = this;
+
+ var player = this.player;
+ var elements = player.elements; // Time change on media
+
+ on.call(player, player.media, 'timeupdate seeking seeked', function (event) {
+ return controls.timeUpdate.call(player, event);
+ }); // Display duration
+
+ on.call(player, player.media, 'durationchange loadeddata loadedmetadata', function (event) {
+ return controls.durationUpdate.call(player, event);
+ }); // Check for audio tracks on load
+ // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
+
+ on.call(player, player.media, 'canplay loadeddata', function () {
+ toggleHidden(elements.volume, !player.hasAudio);
+ toggleHidden(elements.buttons.mute, !player.hasAudio);
+ }); // Handle the media finishing
+
+ on.call(player, player.media, 'ended', function () {
+ // Show poster on end
+ if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {
+ // Restart
+ player.restart();
+ }
+ }); // Check for buffer progress
+
+ on.call(player, player.media, 'progress playing seeking seeked', function (event) {
+ return controls.updateProgress.call(player, event);
+ }); // Handle volume changes
+
+ on.call(player, player.media, 'volumechange', function (event) {
+ return controls.updateVolume.call(player, event);
+ }); // Handle play/pause
+
+ on.call(player, player.media, 'playing play pause ended emptied timeupdate', function (event) {
+ return ui.checkPlaying.call(player, event);
+ }); // Loading state
+
+ on.call(player, player.media, 'waiting canplay seeked playing', function (event) {
+ return ui.checkLoading.call(player, event);
+ }); // If autoplay, then load advertisement if required
+ // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
+
+ on.call(player, player.media, 'playing', function () {
+ if (!player.ads) {
+ return;
+ } // If ads are enabled, wait for them first
+
+
+ if (player.ads.enabled && !player.ads.initialized) {
+ // Wait for manager response
+ player.ads.managerPromise.then(function () {
+ return player.ads.play();
+ }).catch(function () {
+ return player.play();
});
+ }
+ }); // Click video
- // Click video
- if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
- // Re-fetch the wrapper
- var wrapper = utils.getElement.call(this.player, '.' + this.player.config.classNames.video);
+ if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {
+ // Re-fetch the wrapper
+ var wrapper = getElement.call(player, ".".concat(player.config.classNames.video)); // Bail if there's no wrapper (this should never happen)
- // Bail if there's no wrapper (this should never happen)
- if (!utils.is.element(wrapper)) {
- return;
- }
+ if (!is.element(wrapper)) {
+ return;
+ } // On click play, pause or restart
- // On click play, pause ore restart
- utils.on(wrapper, 'click', function () {
- // Touch devices will just show controls (if we're hiding controls)
- if (_this3.player.config.hideControls && _this3.player.touch && !_this3.player.paused) {
- return;
- }
-
- if (_this3.player.paused) {
- _this3.player.play();
- } else if (_this3.player.ended) {
- _this3.player.restart();
- _this3.player.play();
- } else {
- _this3.player.pause();
- }
- });
+
+ on.call(player, elements.container, 'click', function (event) {
+ var targets = [elements.container, wrapper]; // Ignore if click if not container or in video wrapper
+
+ if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
+ return;
+ } // Touch devices will just show controls (if hidden)
+
+
+ if (player.touch && player.config.hideControls) {
+ return;
}
- // Disable right click
- if (this.player.supported.ui && this.player.config.disableContextMenu) {
- utils.on(this.player.elements.wrapper, 'contextmenu', function (event) {
- event.preventDefault();
- }, false);
+ if (player.ended) {
+ _this.proxy(event, player.restart, 'restart');
+
+ _this.proxy(event, player.play, 'play');
+ } else {
+ _this.proxy(event, player.togglePlay, 'play');
}
+ });
+ } // Disable right click
- // Volume change
- utils.on(this.player.media, 'volumechange', function () {
- // Save to storage
- _this3.player.storage.set({ volume: _this3.player.volume, muted: _this3.player.muted });
- });
- // Speed change
- utils.on(this.player.media, 'ratechange', function () {
- // Update UI
- controls.updateSetting.call(_this3.player, 'speed');
+ if (player.supported.ui && player.config.disableContextMenu) {
+ on.call(player, elements.wrapper, 'contextmenu', function (event) {
+ event.preventDefault();
+ }, false);
+ } // Volume change
- // Save to storage
- _this3.player.storage.set({ speed: _this3.player.speed });
- });
- // Quality request
- utils.on(this.player.media, 'qualityrequested', function (event) {
- // Save to storage
- _this3.player.storage.set({ quality: event.detail.quality });
- });
+ on.call(player, player.media, 'volumechange', function () {
+ // Save to storage
+ player.storage.set({
+ volume: player.volume,
+ muted: player.muted
+ });
+ }); // Speed change
- // Quality change
- utils.on(this.player.media, 'qualitychange', function (event) {
- // Update UI
- controls.updateSetting.call(_this3.player, 'quality', null, event.detail.quality);
- });
+ on.call(player, player.media, 'ratechange', function () {
+ // Update UI
+ controls.updateSetting.call(player, 'speed'); // Save to storage
- // Caption language change
- utils.on(this.player.media, 'languagechange', function () {
- // Update UI
- controls.updateSetting.call(_this3.player, 'captions');
- // Save to storage
- _this3.player.storage.set({ language: _this3.player.language });
- });
+ player.storage.set({
+ speed: player.speed
+ });
+ }); // Quality change
- // Captions toggle
- utils.on(this.player.media, 'captionsenabled captionsdisabled', function () {
- // Update UI
- controls.updateSetting.call(_this3.player, 'captions');
+ on.call(player, player.media, 'qualitychange', function (event) {
+ // Update UI
+ controls.updateSetting.call(player, 'quality', null, event.detail.quality);
+ }); // Update download link when ready and if quality changes
- // Save to storage
- _this3.player.storage.set({ captions: _this3.player.captions.active });
- });
+ on.call(player, player.media, 'ready qualitychange', function () {
+ controls.setDownloadLink.call(player);
+ }); // Proxy events to container
+ // Bubble up key events for Edge
- // Proxy events to container
- // Bubble up key events for Edge
- utils.on(this.player.media, this.player.config.events.concat(['keyup', 'keydown']).join(' '), function (event) {
- var detail = {};
+ var proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');
+ on.call(player, player.media, proxyEvents, function (event) {
+ var _event$detail = event.detail,
+ detail = _event$detail === void 0 ? {} : _event$detail; // Get error details from media
- // Get error details from media
- if (event.type === 'error') {
- detail = _this3.player.media.error;
- }
+ if (event.type === 'error') {
+ detail = player.media.error;
+ }
- utils.dispatchEvent.call(_this3.player, _this3.player.elements.container, event.type, true, detail);
- });
+ triggerEvent.call(player, elements.container, event.type, true, detail);
+ });
+ } // Run default and custom handlers
+
+ }, {
+ key: "proxy",
+ value: function proxy(event, defaultHandler, customHandlerKey) {
+ var player = this.player;
+ var customHandler = player.config.listeners[customHandlerKey];
+ var hasCustomHandler = is.function(customHandler);
+ var returned = true; // Execute custom handler
+
+ if (hasCustomHandler) {
+ returned = customHandler.call(player, event);
+ } // Only call default handler if not prevented in custom handler
+
+
+ if (returned && is.function(defaultHandler)) {
+ defaultHandler.call(player, event);
}
+ } // Trigger custom and default handlers
- // Listen for control events
+ }, {
+ key: "bind",
+ value: function bind(element, type, defaultHandler, customHandlerKey) {
+ var _this2 = this;
+
+ var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
+ var player = this.player;
+ var customHandler = player.config.listeners[customHandlerKey];
+ var hasCustomHandler = is.function(customHandler);
+ on.call(player, element, type, function (event) {
+ return _this2.proxy(event, defaultHandler, customHandlerKey);
+ }, passive && !hasCustomHandler);
+ } // Listen for control events
}, {
- key: 'controls',
- value: function controls$$1() {
- var _this4 = this;
-
- // IE doesn't support input event, so we fallback to change
- var inputEvent = browser$3.isIE ? 'change' : 'input';
-
- // Run default and custom handlers
- var proxy = function proxy(event, defaultHandler, customHandlerKey) {
- var customHandler = _this4.player.config.listeners[customHandlerKey];
- var hasCustomHandler = utils.is.function(customHandler);
- var returned = true;
-
- // Execute custom handler
- if (hasCustomHandler) {
- returned = customHandler.call(_this4.player, event);
- }
+ key: "controls",
+ value: function controls$$1() {
+ var _this3 = this;
- // Only call default handler if not prevented in custom handler
- if (returned && utils.is.function(defaultHandler)) {
- defaultHandler.call(_this4.player, event);
- }
- };
+ var player = this.player;
+ var elements = player.elements; // IE doesn't support input event, so we fallback to change
- // Trigger custom and default handlers
- var on = function on(element, type, defaultHandler, customHandlerKey) {
- var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
+ var inputEvent = browser.isIE ? 'change' : 'input'; // Play/pause toggle
- var customHandler = _this4.player.config.listeners[customHandlerKey];
- var hasCustomHandler = utils.is.function(customHandler);
+ if (elements.buttons.play) {
+ Array.from(elements.buttons.play).forEach(function (button) {
+ _this3.bind(button, 'click', player.togglePlay, 'play');
+ });
+ } // Pause
- utils.on(element, type, function (event) {
- return proxy(event, defaultHandler, customHandlerKey);
- }, passive && !hasCustomHandler);
- };
- // Play/pause toggle
- on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
+ this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); // Rewind
+
+ this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind
+
+ this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle
- // Pause
- on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
+ this.bind(elements.buttons.mute, 'click', function () {
+ player.muted = !player.muted;
+ }, 'mute'); // Captions toggle
- // Rewind
- on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
+ this.bind(elements.buttons.captions, 'click', function () {
+ return player.toggleCaptions();
+ }); // Download
- // Rewind
- on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
+ this.bind(elements.buttons.download, 'click', function () {
+ triggerEvent.call(player, player.media, 'download');
+ }, 'download'); // Fullscreen toggle
- // Mute toggle
- on(this.player.elements.buttons.mute, 'click', function () {
- _this4.player.muted = !_this4.player.muted;
- }, 'mute');
+ this.bind(elements.buttons.fullscreen, 'click', function () {
+ player.fullscreen.toggle();
+ }, 'fullscreen'); // Picture-in-Picture
- // Captions toggle
- on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
+ this.bind(elements.buttons.pip, 'click', function () {
+ player.pip = 'toggle';
+ }, 'pip'); // Airplay
- // Fullscreen toggle
- on(this.player.elements.buttons.fullscreen, 'click', function () {
- _this4.player.fullscreen.toggle();
- }, 'fullscreen');
+ this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle
- // Picture-in-Picture
- on(this.player.elements.buttons.pip, 'click', function () {
- _this4.player.pip = 'toggle';
- }, 'pip');
+ this.bind(elements.buttons.settings, 'click', function (event) {
+ // Prevent the document click listener closing the menu
+ event.stopPropagation();
- // Airplay
- on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
+ controls.toggleMenu.call(player, event);
+ }); // Settings menu - keyboard toggle
+ // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
- // Settings menu
- on(this.player.elements.buttons.settings, 'click', function (event) {
- controls.toggleMenu.call(_this4.player, event);
+ this.bind(elements.buttons.settings, 'keyup', function (event) {
+ var code = event.which; // We only care about space and return
+
+ if (![13, 32].includes(code)) {
+ return;
+ } // Because return triggers a click anyway, all we need to do is set focus
+
+
+ if (code === 13) {
+ controls.focusFirstMenuItem.call(player, null, true);
+
+ return;
+ } // Prevent scroll
+
+
+ event.preventDefault(); // Prevent playing video (Firefox)
+
+ event.stopPropagation(); // Toggle menu
+
+ controls.toggleMenu.call(player, event);
+ }, null, false // Can't be passive as we're preventing default
+ ); // Escape closes menu
+
+ this.bind(elements.settings.menu, 'keydown', function (event) {
+ if (event.which === 27) {
+ controls.toggleMenu.call(player, event);
+ }
+ }); // Set range input alternative "value", which matches the tooltip time (#954)
+
+ this.bind(elements.inputs.seek, 'mousedown mousemove', function (event) {
+ var rect = elements.progress.getBoundingClientRect();
+ var percent = 100 / rect.width * (event.pageX - rect.left);
+ event.currentTarget.setAttribute('seek-value', percent);
+ }); // Pause while seeking
+
+ this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) {
+ var seek = event.currentTarget;
+ var code = event.keyCode ? event.keyCode : event.which;
+ var attribute = 'play-on-seeked';
+
+ if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
+ return;
+ } // Record seek time so we can prevent hiding controls for a few seconds after seek
+
+
+ player.lastSeekTime = Date.now(); // Was playing before?
+
+ var play = seek.hasAttribute(attribute); // Done seeking
+
+ var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
+
+ if (play && done) {
+ seek.removeAttribute(attribute);
+ player.play();
+ } else if (!done && player.playing) {
+ seek.setAttribute(attribute, '');
+ player.pause();
+ }
+ }); // Fix range inputs on iOS
+ // Super weird iOS bug where after you interact with an <input type="range">,
+ // it takes over further interactions on the page. This is a hack
+
+ if (browser.isIos) {
+ var inputs = getElements.call(player, 'input[type="range"]');
+ Array.from(inputs).forEach(function (input) {
+ return _this3.bind(input, inputEvent, function (event) {
+ return repaint(event.target);
});
+ });
+ } // Seek
- // Settings menu
- 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 {
- var tab = event.target;
- controls.showTab.call(_this4.player, tab.getAttribute('aria-controls'));
- }
+
+ this.bind(elements.inputs.seek, inputEvent, function (event) {
+ var seek = event.currentTarget; // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954)
+
+ var seekTo = seek.getAttribute('seek-value');
+
+ if (is.empty(seekTo)) {
+ seekTo = seek.value;
+ }
+
+ seek.removeAttribute('seek-value');
+ player.currentTime = seekTo / seek.max * player.duration;
+ }, 'seek'); // Seek tooltip
+
+ this.bind(elements.progress, 'mouseenter mouseleave mousemove', function (event) {
+ return controls.updateSeekTooltip.call(player, event);
+ }); // Preview thumbnails plugin
+ // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this
+
+ this.bind(elements.progress, 'mousemove touchmove', function (event) {
+ var previewThumbnails = player.previewThumbnails;
+
+ if (previewThumbnails && previewThumbnails.loaded) {
+ previewThumbnails.startMove(event);
+ }
+ }); // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering
+
+ this.bind(elements.progress, 'mouseleave click', function () {
+ var previewThumbnails = player.previewThumbnails;
+
+ if (previewThumbnails && previewThumbnails.loaded) {
+ previewThumbnails.endMove(false, true);
+ }
+ }); // Show scrubbing preview
+
+ this.bind(elements.progress, 'mousedown touchstart', function (event) {
+ var previewThumbnails = player.previewThumbnails;
+
+ if (previewThumbnails && previewThumbnails.loaded) {
+ previewThumbnails.startScrubbing(event);
+ }
+ });
+ this.bind(elements.progress, 'mouseup touchend', function (event) {
+ var previewThumbnails = player.previewThumbnails;
+
+ if (previewThumbnails && previewThumbnails.loaded) {
+ previewThumbnails.endScrubbing(event);
+ }
+ }); // Polyfill for lower fill in <input type="range"> for webkit
+
+ if (browser.isWebkit) {
+ Array.from(getElements.call(player, 'input[type="range"]')).forEach(function (element) {
+ _this3.bind(element, 'input', function (event) {
+ return controls.updateRangeFill.call(player, event.target);
});
+ });
+ } // Current time invert
+ // Only if one time element is used for both currentTime and duration
- // Seek
- on(this.player.elements.inputs.seek, inputEvent, function (event) {
- _this4.player.currentTime = event.target.value / event.target.max * _this4.player.duration;
- }, 'seek');
-
- // Current time invert
- // Only if one time element is used for both currentTime and duration
- if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) {
- on(this.player.elements.display.currentTime, 'click', function () {
- // Do nothing if we're at the start
- if (_this4.player.currentTime === 0) {
- return;
- }
-
- _this4.player.config.invertTime = !_this4.player.config.invertTime;
- ui.timeUpdate.call(_this4.player);
- });
+
+ if (player.config.toggleInvert && !is.element(elements.display.duration)) {
+ this.bind(elements.display.currentTime, 'click', function () {
+ // Do nothing if we're at the start
+ if (player.currentTime === 0) {
+ return;
}
- // Volume
- on(this.player.elements.inputs.volume, inputEvent, function (event) {
- _this4.player.volume = event.target.value;
- }, 'volume');
+ player.config.invertTime = !player.config.invertTime;
- // Polyfill for lower fill in <input type="range"> for webkit
- if (browser$3.isWebkit) {
- on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', function (event) {
- controls.updateRangeFill.call(_this4.player, event.target);
- });
- }
+ controls.timeUpdate.call(player);
+ });
+ } // Volume
- // Seek tooltip
- on(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) {
- return controls.updateSeekTooltip.call(_this4.player, event);
- });
- // Toggle controls visibility based on mouse movement
- if (this.player.config.hideControls) {
- // Watch for cursor over controls so they don't hide when trying to interact
- on(this.player.elements.controls, 'mouseenter mouseleave', function (event) {
- _this4.player.elements.controls.hover = !_this4.player.touch && event.type === 'mouseenter';
- });
+ this.bind(elements.inputs.volume, inputEvent, function (event) {
+ player.volume = event.target.value;
+ }, 'volume'); // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
- // Watch for cursor over controls so they don't hide when trying to interact
- on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
- _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
- });
+ this.bind(elements.controls, 'mouseenter mouseleave', function (event) {
+ elements.controls.hover = !player.touch && event.type === 'mouseenter';
+ }); // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
- // Focus in/out on controls
- on(this.player.elements.controls, 'focusin focusout', function (event) {
- _this4.player.toggleControls(event);
- });
- }
+ this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
+ elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
+ }); // Show controls when they receive focus (e.g., when using keyboard tab key)
- // Mouse wheel for volume
- on(this.player.elements.inputs.volume, 'wheel', function (event) {
- // Detect "natural" scroll - suppored on OS X Safari only
- // Other browsers on OS X will be inverted until support improves
- var inverted = event.webkitDirectionInvertedFromDevice;
- var step = 1 / 50;
- var direction = 0;
-
- // Scroll down (or up on natural) to decrease
- if (event.deltaY < 0 || event.deltaX > 0) {
- if (inverted) {
- _this4.player.decreaseVolume(step);
- direction = -1;
- } else {
- _this4.player.increaseVolume(step);
- direction = 1;
- }
- }
+ this.bind(elements.controls, 'focusin', function () {
+ var config = player.config,
+ elements = player.elements,
+ timers = player.timers; // Skip transition to prevent focus from scrolling the parent element
- // Scroll up (or down on natural) to increase
- if (event.deltaY > 0 || event.deltaX < 0) {
- if (inverted) {
- _this4.player.increaseVolume(step);
- direction = 1;
- } else {
- _this4.player.decreaseVolume(step);
- direction = -1;
- }
- }
+ toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
- // Don't break page scrolling at max and min
- if (direction === 1 && _this4.player.media.volume < 1 || direction === -1 && _this4.player.media.volume > 0) {
- event.preventDefault();
- }
- }, 'volume', false);
- }
+ ui.toggleControls.call(player, true); // Restore transition
- // Reset on destroy
+ setTimeout(function () {
+ toggleClass(elements.controls, config.classNames.noTransition, false);
+ }, 0); // Delay a little more for mouse users
- }, {
- key: 'clear',
- value: function clear() {
- this.global(false);
- }
- }]);
- return Listeners;
-}();
+ var delay = _this3.touch ? 3000 : 4000; // Clear timer
-// ==========================================================================
+ clearTimeout(timers.controls); // Hide again after delay
-var vimeo = {
- setup: function setup() {
- var _this = this;
+ timers.controls = setTimeout(function () {
+ return ui.toggleControls.call(player, false);
+ }, delay);
+ }); // Mouse wheel for volume
- // Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
+ this.bind(elements.inputs.volume, 'wheel', function (event) {
+ // Detect "natural" scroll - suppored on OS X Safari only
+ // Other browsers on OS X will be inverted until support improves
+ var inverted = event.webkitDirectionInvertedFromDevice; // Get delta from event. Invert if `inverted` is true
- // Set intial ratio
- vimeo.setAspectRatio.call(this);
+ var _map = [event.deltaX, -event.deltaY].map(function (value) {
+ return inverted ? -value : value;
+ }),
+ _map2 = _slicedToArray(_map, 2),
+ x = _map2[0],
+ y = _map2[1]; // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)
- // Load the API if not already
- if (!utils.is.object(window.Vimeo)) {
- utils.loadScript(this.config.urls.vimeo.sdk).then(function () {
- vimeo.ready.call(_this);
- }).catch(function (error) {
- _this.debug.warn('Vimeo API failed to load', error);
- });
- } else {
- vimeo.ready.call(this);
- }
- },
+ var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2%
- // Set aspect ratio
- // For Vimeo we have an extra 300% height <div> 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 + '%';
+ player.increaseVolume(direction / 50); // Don't break page scrolling at max and min
- if (this.supported.ui) {
- var height = 240;
- var offset = (height - padding) / (height / 50);
+ var volume = player.media.volume;
- this.media.style.transform = 'translateY(-' + offset + '%)';
- }
- },
+ if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {
+ event.preventDefault();
+ }
+ }, 'volume', false);
+ }
+ }]);
+ return Listeners;
+ }();
- // API Ready
- ready: function ready() {
- var _this2 = this;
+ var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
- var player = this;
-
- // Get Vimeo params for the iframe
- var options = {
- loop: player.config.loop.active,
- autoplay: player.autoplay,
- // muted: player.muted,
- byline: false,
- portrait: false,
- title: false,
- speed: true,
- transparent: 0,
- gesture: 'media',
- playsinline: !this.config.fullscreen.iosNative
- };
- var params = utils.buildUrlParams(options);
+ function createCommonjsModule(fn, module) {
+ return module = { exports: {} }, fn(module, module.exports), module.exports;
+ }
- // Get the source URL or ID
- var source = player.media.getAttribute('src');
+ var loadjs_umd = createCommonjsModule(function (module, exports) {
+ (function(root, factory) {
+ {
+ module.exports = factory();
+ }
+ }(commonjsGlobal, function() {
+ /**
+ * Global dependencies.
+ * @global {Object} document - DOM
+ */
+
+ var devnull = function() {},
+ bundleIdCache = {},
+ bundleResultCache = {},
+ bundleCallbackQueue = {};
+
+
+ /**
+ * Subscribe to bundle load event.
+ * @param {string[]} bundleIds - Bundle ids
+ * @param {Function} callbackFn - The callback function
+ */
+ function subscribe(bundleIds, callbackFn) {
+ // listify
+ bundleIds = bundleIds.push ? bundleIds : [bundleIds];
+
+ var depsNotFound = [],
+ i = bundleIds.length,
+ numWaiting = i,
+ fn,
+ bundleId,
+ r,
+ q;
+
+ // define callback function
+ fn = function (bundleId, pathsNotFound) {
+ if (pathsNotFound.length) depsNotFound.push(bundleId);
+
+ numWaiting--;
+ if (!numWaiting) callbackFn(depsNotFound);
+ };
+
+ // register callback
+ while (i--) {
+ bundleId = bundleIds[i];
+
+ // execute callback if in result cache
+ r = bundleResultCache[bundleId];
+ if (r) {
+ fn(bundleId, r);
+ continue;
+ }
- // Get from <div> if needed
- if (utils.is.empty(source)) {
- source = player.media.getAttribute(player.config.attributes.embed.id);
- }
+ // add to callback queue
+ q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
+ q.push(fn);
+ }
+ }
- var id = utils.parseVimeoId(source);
-
- // Build an iframe
- var iframe = utils.createElement('iframe');
- var src = utils.format(player.config.urls.vimeo.iframe, id, params);
- iframe.setAttribute('src', src);
- iframe.setAttribute('allowfullscreen', '');
- iframe.setAttribute('allowtransparency', '');
- iframe.setAttribute('allow', 'autoplay');
-
- // Inject the package
- var wrapper = utils.createElement('div', { class: player.config.classNames.embedContainer });
- wrapper.appendChild(iframe);
- player.media = utils.replaceElement(wrapper, player.media);
-
- // Get poster image
- utils.fetch(utils.format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
- if (utils.is.empty(response)) {
- return;
- }
- // Get the URL for thumbnail
- var url = new URL(response[0].thumbnail_large);
+ /**
+ * 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;
- // Get original image
- url.pathname = url.pathname.split('_')[0] + '.jpg';
+ var q = bundleCallbackQueue[bundleId];
- // Set attribute
- player.media.setAttribute('poster', url.href);
+ // cache result
+ bundleResultCache[bundleId] = pathsNotFound;
- // Update
- ui.setPoster.call(player);
- });
+ // exit if queue is empty
+ if (!q) return;
- // Setup instance
- // https://github.com/vimeo/player.js
- player.embed = new window.Vimeo.Player(iframe, {
- autopause: player.config.autopause,
- muted: player.muted
- });
+ // 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);
+ }
- player.media.paused = true;
- player.media.currentTime = 0;
- // Disable native text track rendering
- if (player.supported.ui) {
- player.embed.disableTextTrack();
+ /**
+ * 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` (unless error is Code:18 SecurityError)
+ if (x.code != 18) result = 'e';
}
+ }
- // Create a faux HTML5 API using the Vimeo API
- player.media.play = function () {
- player.embed.play().then(function () {
- player.media.paused = false;
- });
- };
+ // handle retries in case of load failure
+ if (result == 'e') {
+ // increment counter
+ numTries += 1;
- player.media.pause = function () {
- player.embed.pause().then(function () {
- player.media.paused = true;
- });
- };
+ // exit function and try again
+ if (numTries < maxTries) {
+ return loadFile(path, callbackFn, args, numTries);
+ }
+ }
- player.media.stop = function () {
- player.pause();
- player.currentTime = 0;
- };
+ // execute callback
+ callbackFn(path, result, ev.defaultPrevented);
+ };
- // Seeking
- var currentTime = player.media.currentTime;
+ // add to document (unless callback returns `false`)
+ if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
+ }
- 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
+ /**
+ * 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;
+ }
- player.media.seeking = true;
+ numWaiting--;
+ if (!numWaiting) callbackFn(pathsNotFound);
+ };
- // Trigger seeking
- utils.dispatchEvent.call(player, player.media, 'seeking');
+ // load scripts
+ for (i=0; i < x; i++) loadFile(paths[i], fn, args);
+ }
- // Seek after events
- player.embed.setCurrentTime(time).catch(function () {
- // Do nothing
- });
- // Restore pause state
- if (paused) {
- player.pause();
- }
- }
- });
+ /**
+ * 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;
+ }
+ }
- // 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, []);
- }
- });
- }
- });
+ // load scripts
+ loadFiles(paths, function (pathsNotFound) {
+ // execute callbacks
+ executeCallbacks(args, pathsNotFound);
- // Volume
- var volume = player.config.volume;
+ // publish bundle load event
+ publish(bundleId, pathsNotFound);
+ }, args);
+ }
- Object.defineProperty(player.media, 'volume', {
- get: function get() {
- return volume;
- },
- set: function set(input) {
- player.embed.setVolume(input).then(function () {
- volume = input;
- utils.dispatchEvent.call(player, player.media, 'volumechange');
- });
- }
- });
- // Muted
- var muted = player.config.muted;
+ /**
+ * 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);
+ });
- Object.defineProperty(player.media, 'muted', {
- get: function get() {
- return muted;
- },
- set: function set(input) {
- var toggle = utils.is.boolean(input) ? input : false;
+ return loadjs;
+ };
- player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () {
- muted = toggle;
- utils.dispatchEvent.call(player, player.media, 'volumechange');
- });
- }
- });
- // Loop
- var loop = player.config.loop;
+ /**
+ * Manually satisfy bundle dependencies.
+ * @param {string} bundleId - The bundle id
+ */
+ loadjs.done = function done(bundleId) {
+ publish(bundleId, []);
+ };
- Object.defineProperty(player.media, 'loop', {
- get: function get() {
- return loop;
- },
- set: function set(input) {
- var toggle = utils.is.boolean(input) ? input : player.config.loop.active;
- player.embed.setLoop(toggle).then(function () {
- loop = toggle;
- });
- }
- });
+ /**
+ * Reset loadjs dependencies statuses
+ */
+ loadjs.reset = function reset() {
+ bundleIdCache = {};
+ bundleResultCache = {};
+ bundleCallbackQueue = {};
+ };
- // Source
- var currentSrc = void 0;
- player.embed.getVideoUrl().then(function (value) {
- currentSrc = value;
- }).catch(function (error) {
- _this2.debug.warn(error);
- });
- Object.defineProperty(player.media, 'currentSrc', {
- get: function get() {
- return currentSrc;
- }
- });
+ /**
+ * Determine if bundle has already been defined
+ * @param String} bundleId - The bundle id
+ */
+ loadjs.isDefined = function isDefined(bundleId) {
+ return bundleId in bundleIdCache;
+ };
- // Ended
- Object.defineProperty(player.media, 'ended', {
- get: function get() {
- return player.currentTime === player.duration;
- }
- });
- // Set aspect ratio based on video size
- Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) {
- var ratio = utils.getAspectRatio(dimensions[0], dimensions[1]);
- vimeo.setAspectRatio.call(_this2, ratio);
- });
+ // export
+ return loadjs;
- // Set autopause
- player.embed.setAutopause(player.config.autopause).then(function (state) {
- player.config.autopause = state;
- });
+ }));
+ });
- // Get title
- player.embed.getVideoTitle().then(function (title) {
- player.config.title = title;
- ui.setTitle.call(_this2);
- });
+ // ==========================================================================
+ function loadScript(url) {
+ return new Promise(function (resolve, reject) {
+ loadjs_umd(url, {
+ success: resolve,
+ error: reject
+ });
+ });
+ }
- // Get current time
- player.embed.getCurrentTime().then(function (value) {
- currentTime = value;
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- });
+ function parseId(url) {
+ if (is.empty(url)) {
+ return null;
+ }
- // Get duration
- player.embed.getDuration().then(function (value) {
- player.media.duration = value;
- utils.dispatchEvent.call(player, player.media, 'durationchange');
- });
+ if (is.number(Number(url))) {
+ return url;
+ }
- // Get captions
- player.embed.getTextTracks().then(function (tracks) {
- player.media.textTracks = tracks;
- captions.setup.call(player);
- });
+ var regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
+ return url.match(regex) ? RegExp.$2 : url;
+ } // Set playback state and trigger change (only on actual change)
- player.embed.on('cuechange', function (data) {
- var cue = null;
- if (data.cues.length) {
- cue = utils.stripHTML(data.cues[0].text);
- }
+ function assurePlaybackState(play) {
+ if (play && !this.embed.hasPlayed) {
+ this.embed.hasPlayed = true;
+ }
- captions.setText.call(player, cue);
- });
+ if (this.media.paused === play) {
+ this.media.paused = !play;
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
+ }
+ }
- player.embed.on('loaded', function () {
- if (utils.is.element(player.embed.element) && player.supported.ui) {
- var frame = player.embed.element;
+ var vimeo = {
+ setup: function setup() {
+ var _this = this;
- // Fix keyboard focus issues
- // https://github.com/sampotts/plyr/issues/317
- frame.setAttribute('tabindex', -1);
- }
- });
+ // Add embed class for responsive
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set intial ratio
- 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');
- });
+ setAspectRatio.call(this); // Load the API if not already
- player.embed.on('pause', function () {
- player.media.paused = true;
- utils.dispatchEvent.call(player, player.media, 'pause');
+ if (!is.object(window.Vimeo)) {
+ loadScript(this.config.urls.vimeo.sdk).then(function () {
+ vimeo.ready.call(_this);
+ }).catch(function (error) {
+ _this.debug.warn('Vimeo API failed to load', error);
});
+ } else {
+ vimeo.ready.call(this);
+ }
+ },
+ // API Ready
+ ready: function ready$$1() {
+ var _this2 = this;
- player.embed.on('timeupdate', function (data) {
- player.media.seeking = false;
- currentTime = data.seconds;
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- });
+ var player = this;
+ var config = player.config.vimeo; // Get Vimeo params for the iframe
- player.embed.on('progress', function (data) {
- player.media.buffered = data.percent;
- utils.dispatchEvent.call(player, player.media, 'progress');
+ var params = buildUrlParams(extend({}, {
+ loop: player.config.loop.active,
+ autoplay: player.autoplay,
+ muted: player.muted,
+ gesture: 'media',
+ playsinline: !this.config.fullscreen.iosNative
+ }, config)); // Get the source URL or ID
- // Check all loaded
- if (parseInt(data.percent, 10) === 1) {
- utils.dispatchEvent.call(player, player.media, 'canplaythrough');
- }
+ var source = player.media.getAttribute('src'); // Get from <div> if needed
- // 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');
- }
- });
- });
+ if (is.empty(source)) {
+ source = player.media.getAttribute(player.config.attributes.embed.id);
+ }
- player.embed.on('seeked', function () {
- player.media.seeking = false;
- utils.dispatchEvent.call(player, player.media, 'seeked');
- utils.dispatchEvent.call(player, player.media, 'play');
- });
+ var id = parseId(source); // Build an iframe
- player.embed.on('ended', function () {
- player.media.paused = true;
- utils.dispatchEvent.call(player, player.media, 'ended');
- });
+ var iframe = createElement('iframe');
+ var src = format(player.config.urls.vimeo.iframe, id, params);
+ iframe.setAttribute('src', src);
+ iframe.setAttribute('allowfullscreen', '');
+ iframe.setAttribute('allowtransparency', '');
+ iframe.setAttribute('allow', 'autoplay'); // Get poster, if already set
- player.embed.on('error', function (detail) {
- player.media.error = detail;
- utils.dispatchEvent.call(player, player.media, 'error');
- });
+ var poster = player.poster; // Inject the package
- // Rebuild UI
- setTimeout(function () {
- return ui.build.call(player);
- }, 0);
- }
-};
+ var wrapper = createElement('div', {
+ poster: poster,
+ class: player.config.classNames.embedContainer
+ });
+ wrapper.appendChild(iframe);
+ player.media = replaceElement(wrapper, player.media); // Get poster image
-// ==========================================================================
+ fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
+ if (is.empty(response)) {
+ return;
+ } // Get the URL for thumbnail
-// Standardise YouTube quality unit
-function mapQualityUnit(input) {
- switch (input) {
- case 'hd2160':
- return 2160;
- case 2160:
- return 'hd2160';
+ var url = new URL(response[0].thumbnail_large); // Get original image
- case 'hd1440':
- return 1440;
+ url.pathname = "".concat(url.pathname.split('_')[0], ".jpg"); // Set and show poster
- case 1440:
- return 'hd1440';
+ ui.setPoster.call(player, url.href).catch(function () {});
+ }); // Setup instance
+ // https://github.com/vimeo/player.js
- case 'hd1080':
- return 1080;
+ player.embed = new window.Vimeo.Player(iframe, {
+ autopause: player.config.autopause,
+ muted: player.muted
+ });
+ player.media.paused = true;
+ player.media.currentTime = 0; // Disable native text track rendering
- case 1080:
- return 'hd1080';
+ if (player.supported.ui) {
+ player.embed.disableTextTrack();
+ } // Create a faux HTML5 API using the Vimeo API
- case 'hd720':
- return 720;
- case 720:
- return 'hd720';
+ player.media.play = function () {
+ assurePlaybackState.call(player, true);
+ return player.embed.play();
+ };
- case 'large':
- return 480;
+ player.media.pause = function () {
+ assurePlaybackState.call(player, false);
+ return player.embed.pause();
+ };
- case 480:
- return 'large';
+ player.media.stop = function () {
+ player.pause();
+ player.currentTime = 0;
+ }; // Seeking
- case 'medium':
- return 360;
- case 360:
- return 'medium';
+ var currentTime = player.media.currentTime;
+ Object.defineProperty(player.media, 'currentTime', {
+ get: function get() {
+ return currentTime;
+ },
+ set: function set(time) {
+ // Vimeo will automatically play on seek if the video hasn't been played before
+ // Get current paused state and volume etc
+ var embed = player.embed,
+ media = player.media,
+ paused = player.paused,
+ volume = player.volume;
+ var restorePause = paused && !embed.hasPlayed; // Set seeking state and trigger event
+
+ media.seeking = true;
+ triggerEvent.call(player, media, 'seeking'); // If paused, mute until seek is complete
+
+ Promise.resolve(restorePause && embed.setVolume(0)) // Seek
+ .then(function () {
+ return embed.setCurrentTime(time);
+ }) // Restore paused
+ .then(function () {
+ return restorePause && embed.pause();
+ }) // Restore volume
+ .then(function () {
+ return restorePause && embed.setVolume(volume);
+ }).catch(function () {// Do nothing
+ });
+ }
+ }); // Playback speed
+
+ var speed = player.config.speed.selected;
+ Object.defineProperty(player.media, 'playbackRate', {
+ get: function get() {
+ return speed;
+ },
+ set: function set(input) {
+ player.embed.setPlaybackRate(input).then(function () {
+ speed = input;
+ triggerEvent.call(player, player.media, 'ratechange');
+ }).catch(function (error) {
+ // Hide menu item (and menu if empty)
+ if (error.name === 'Error') {
+ controls.setSpeedMenu.call(player, []);
+ }
+ });
+ }
+ }); // Volume
+
+ var volume = player.config.volume;
+ Object.defineProperty(player.media, 'volume', {
+ get: function get() {
+ return volume;
+ },
+ set: function set(input) {
+ player.embed.setVolume(input).then(function () {
+ volume = input;
+ triggerEvent.call(player, player.media, 'volumechange');
+ });
+ }
+ }); // Muted
+
+ var muted = player.config.muted;
+ Object.defineProperty(player.media, 'muted', {
+ get: function get() {
+ return muted;
+ },
+ set: function set(input) {
+ var toggle = is.boolean(input) ? input : false;
+ player.embed.setVolume(toggle ? 0 : player.config.volume).then(function () {
+ muted = toggle;
+ triggerEvent.call(player, player.media, 'volumechange');
+ });
+ }
+ }); // Loop
+
+ var loop = player.config.loop;
+ Object.defineProperty(player.media, 'loop', {
+ get: function get() {
+ return loop;
+ },
+ set: function set(input) {
+ var toggle = is.boolean(input) ? input : player.config.loop.active;
+ player.embed.setLoop(toggle).then(function () {
+ loop = toggle;
+ });
+ }
+ }); // Source
+
+ var currentSrc;
+ player.embed.getVideoUrl().then(function (value) {
+ currentSrc = value;
+ controls.setDownloadLink.call(player);
+ }).catch(function (error) {
+ _this2.debug.warn(error);
+ });
+ Object.defineProperty(player.media, 'currentSrc', {
+ get: function get() {
+ return currentSrc;
+ }
+ }); // Ended
+
+ Object.defineProperty(player.media, 'ended', {
+ get: function get() {
+ return player.currentTime === player.duration;
+ }
+ }); // Set aspect ratio based on video size
+
+ Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(function (dimensions) {
+ var _dimensions = _slicedToArray(dimensions, 2),
+ width = _dimensions[0],
+ height = _dimensions[1];
+
+ player.embed.ratio = "".concat(width, ":").concat(height);
+ setAspectRatio.call(_this2, player.embed.ratio);
+ }); // Set autopause
+
+ player.embed.setAutopause(player.config.autopause).then(function (state) {
+ player.config.autopause = state;
+ }); // Get title
+
+ player.embed.getVideoTitle().then(function (title) {
+ player.config.title = title;
+ ui.setTitle.call(_this2);
+ }); // Get current time
+
+ player.embed.getCurrentTime().then(function (value) {
+ currentTime = value;
+ triggerEvent.call(player, player.media, 'timeupdate');
+ }); // Get duration
+
+ player.embed.getDuration().then(function (value) {
+ player.media.duration = value;
+ triggerEvent.call(player, player.media, 'durationchange');
+ }); // Get captions
+
+ player.embed.getTextTracks().then(function (tracks) {
+ player.media.textTracks = tracks;
+ captions.setup.call(player);
+ });
+ player.embed.on('cuechange', function (_ref) {
+ var _ref$cues = _ref.cues,
+ cues = _ref$cues === void 0 ? [] : _ref$cues;
+ var strippedCues = cues.map(function (cue) {
+ return stripHTML(cue.text);
+ });
+ captions.updateCues.call(player, strippedCues);
+ });
+ player.embed.on('loaded', function () {
+ // Assure state and events are updated on autoplay
+ player.embed.getPaused().then(function (paused) {
+ assurePlaybackState.call(player, !paused);
+
+ if (!paused) {
+ triggerEvent.call(player, player.media, 'playing');
+ }
+ });
- case 'small':
- return 240;
+ if (is.element(player.embed.element) && player.supported.ui) {
+ var frame = player.embed.element; // Fix keyboard focus issues
+ // https://github.com/sampotts/plyr/issues/317
+
+ frame.setAttribute('tabindex', -1);
+ }
+ });
+ player.embed.on('play', function () {
+ assurePlaybackState.call(player, true);
+ triggerEvent.call(player, player.media, 'playing');
+ });
+ player.embed.on('pause', function () {
+ assurePlaybackState.call(player, false);
+ });
+ player.embed.on('timeupdate', function (data) {
+ player.media.seeking = false;
+ currentTime = data.seconds;
+ triggerEvent.call(player, player.media, 'timeupdate');
+ });
+ player.embed.on('progress', function (data) {
+ player.media.buffered = data.percent;
+ triggerEvent.call(player, player.media, 'progress'); // Check all loaded
+
+ if (parseInt(data.percent, 10) === 1) {
+ triggerEvent.call(player, player.media, 'canplaythrough');
+ } // Get duration as if we do it before load, it gives an incorrect value
+ // https://github.com/sampotts/plyr/issues/891
- case 240:
- return 'small';
- default:
- return 'default';
+ player.embed.getDuration().then(function (value) {
+ if (value !== player.media.duration) {
+ player.media.duration = value;
+ triggerEvent.call(player, player.media, 'durationchange');
+ }
+ });
+ });
+ player.embed.on('seeked', function () {
+ player.media.seeking = false;
+ triggerEvent.call(player, player.media, 'seeked');
+ });
+ player.embed.on('ended', function () {
+ player.media.paused = true;
+ triggerEvent.call(player, player.media, 'ended');
+ });
+ player.embed.on('error', function (detail) {
+ player.media.error = detail;
+ triggerEvent.call(player, player.media, 'error');
+ }); // Rebuild UI
+
+ setTimeout(function () {
+ return ui.build.call(player);
+ }, 0);
}
-}
+ };
+
+ // ==========================================================================
-function mapQualityUnits(levels) {
- if (utils.is.empty(levels)) {
- return levels;
+ function parseId$1(url) {
+ if (is.empty(url)) {
+ return null;
}
- return utils.dedupe(levels.map(function (level) {
- return mapQualityUnit(level);
- }));
-}
+ var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
+ return url.match(regex) ? RegExp.$2 : url;
+ } // Set playback state and trigger change (only on actual change)
-var youtube = {
- setup: function setup() {
- var _this = this;
- // Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
+ function assurePlaybackState$1(play) {
+ if (play && !this.embed.hasPlayed) {
+ this.embed.hasPlayed = true;
+ }
+
+ if (this.media.paused === play) {
+ this.media.paused = !play;
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
+ }
+ }
- // Set aspect ratio
- youtube.setAspectRatio.call(this);
+ var youtube = {
+ setup: function setup() {
+ var _this = this;
- // Setup API
- if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) {
- youtube.ready.call(this);
- } else {
- // Load the API
- utils.loadScript(this.config.urls.youtube.sdk).catch(function (error) {
- _this.debug.warn('YouTube API failed to load', error);
- });
+ // Add embed class for responsive
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio
- // Setup callback for the API
- // YouTube has it's own system of course...
- window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
+ setAspectRatio.call(this); // Setup API
- // Add to queue
- window.onYouTubeReadyCallbacks.push(function () {
- youtube.ready.call(_this);
- });
+ if (is.object(window.YT) && is.function(window.YT.Player)) {
+ youtube.ready.call(this);
+ } else {
+ // Load the API
+ loadScript(this.config.urls.youtube.sdk).catch(function (error) {
+ _this.debug.warn('YouTube API failed to load', error);
+ }); // Setup callback for the API
+ // YouTube has it's own system of course...
- // Set callback to process queue
- window.onYouTubeIframeAPIReady = function () {
- window.onYouTubeReadyCallbacks.forEach(function (callback) {
- callback();
- });
- };
- }
- },
+ window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; // Add to queue
+ window.onYouTubeReadyCallbacks.push(function () {
+ youtube.ready.call(_this);
+ }); // Set callback to process queue
+ window.onYouTubeIframeAPIReady = function () {
+ window.onYouTubeReadyCallbacks.forEach(function (callback) {
+ callback();
+ });
+ };
+ }
+ },
// Get the media title
getTitle: function getTitle(videoId) {
- var _this2 = this;
+ var _this2 = this;
- // Try via undocumented API method first
- // This method disappears now and then though...
- // https://github.com/sampotts/plyr/issues/709
- if (utils.is.function(this.embed.getVideoData)) {
- var _embed$getVideoData = this.embed.getVideoData(),
- title = _embed$getVideoData.title;
-
- if (utils.is.empty(title)) {
- this.config.title = title;
- ui.setTitle.call(this);
- return;
- }
+ // Try via undocumented API method first
+ // This method disappears now and then though...
+ // https://github.com/sampotts/plyr/issues/709
+ if (is.function(this.embed.getVideoData)) {
+ var _this$embed$getVideoD = this.embed.getVideoData(),
+ title = _this$embed$getVideoD.title;
+
+ if (is.empty(title)) {
+ this.config.title = title;
+ ui.setTitle.call(this);
+ return;
}
+ } // Or via Google API
- // Or via Google API
- var key = this.config.keys.google;
- if (utils.is.string(key) && !utils.is.empty(key)) {
- var url = utils.format(this.config.urls.youtube.api, videoId, key);
- utils.fetch(url).then(function (result) {
- if (utils.is.object(result)) {
- _this2.config.title = result.items[0].snippet.title;
- ui.setTitle.call(_this2);
- }
- }).catch(function () {});
- }
+ var key = this.config.keys.google;
+
+ if (is.string(key) && !is.empty(key)) {
+ var url = format(this.config.urls.youtube.api, videoId, key);
+ fetch(url).then(function (result) {
+ if (is.object(result)) {
+ _this2.config.title = result.items[0].snippet.title;
+ ui.setTitle.call(_this2);
+ }
+ }).catch(function () {});
+ }
},
+ // API ready
+ ready: function ready$$1() {
+ var player = this; // Ignore already setup (race condition)
+ var currentId = player.media.getAttribute('id');
- // Set aspect ratio
- setAspectRatio: function setAspectRatio() {
- var ratio = this.config.ratio.split(':');
- this.elements.wrapper.style.paddingBottom = 100 / ratio[0] * ratio[1] + '%';
- },
+ if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
+ return;
+ } // Get the source URL or ID
+
+
+ var source = player.media.getAttribute('src'); // Get from <div> if needed
+
+ if (is.empty(source)) {
+ source = player.media.getAttribute(this.config.attributes.embed.id);
+ } // Replace the <iframe> with a <div> due to YouTube API issues
+
+
+ var videoId = parseId$1(source);
+ var id = generateId(player.provider); // Get poster, if already set
+
+ var poster = player.poster; // Replace media element
+
+ var container = createElement('div', {
+ id: id,
+ poster: poster
+ });
+ player.media = replaceElement(container, player.media); // Id to poster wrapper
+
+ var posterSrc = function posterSrc(format$$1) {
+ return "https://img.youtube.com/vi/".concat(videoId, "/").concat(format$$1, "default.jpg");
+ }; // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
+
+
+ loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
+ .catch(function () {
+ return loadImage(posterSrc('sd'), 121);
+ }) // 480p padded 4:3
+ .catch(function () {
+ return loadImage(posterSrc('hq'));
+ }) // 360p padded 4:3. Always exists
+ .then(function (image) {
+ return ui.setPoster.call(player, image.src);
+ }).then(function (posterSrc) {
+ // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
+ if (!posterSrc.includes('maxres')) {
+ player.elements.poster.style.backgroundSize = 'cover';
+ }
+ }).catch(function () {});
+ var config = player.config.youtube; // Setup instance
+ // https://developers.google.com/youtube/iframe_api_reference
+
+ player.embed = new window.YT.Player(id, {
+ videoId: videoId,
+ host: config.noCookie ? 'https://www.youtube-nocookie.com' : undefined,
+ playerVars: extend({}, {
+ autoplay: player.config.autoplay ? 1 : 0,
+ // Autoplay
+ hl: player.config.hl,
+ // iframe interface language
+ controls: player.supported.ui ? 0 : 1,
+ // Only show controls if not fully supported
+ disablekb: 1,
+ // Disable keyboard as we handle it
+ playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
+ // Allow iOS inline playback
+ // Captions are flaky on YouTube
+ cc_load_policy: player.captions.active ? 1 : 0,
+ cc_lang_pref: player.config.captions.language,
+ // Tracking for stats
+ widget_referrer: window ? window.location.href : null
+ }, config),
+ events: {
+ onError: function onError(event) {
+ // YouTube may fire onError twice, so only handle it once
+ if (!player.media.error) {
+ var code = event.data; // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
+
+ var message = {
+ 2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',
+ 5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',
+ 100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',
+ 101: 'The owner of the requested video does not allow it to be played in embedded players.',
+ 150: 'The owner of the requested video does not allow it to be played in embedded players.'
+ }[code] || 'An unknown error occured';
+ player.media.error = {
+ code: code,
+ message: message
+ };
+ triggerEvent.call(player, player.media, 'error');
+ }
+ },
+ onPlaybackRateChange: function onPlaybackRateChange(event) {
+ // Get the instance
+ var instance = event.target; // Get current speed
+
+ player.media.playbackRate = instance.getPlaybackRate();
+ triggerEvent.call(player, player.media, 'ratechange');
+ },
+ onReady: function onReady(event) {
+ // Bail if onReady has already been called. See issue #1108
+ if (is.function(player.media.play)) {
+ return;
+ } // Get the instance
+
+
+ var instance = event.target; // Get the title
+
+ youtube.getTitle.call(player, videoId); // Create a faux HTML5 API using the YouTube API
+
+ player.media.play = function () {
+ assurePlaybackState$1.call(player, true);
+ instance.playVideo();
+ };
+ player.media.pause = function () {
+ assurePlaybackState$1.call(player, false);
+ instance.pauseVideo();
+ };
- // API ready
- ready: function ready() {
- var player = this;
+ player.media.stop = function () {
+ instance.stopVideo();
+ };
- // Ignore already setup (race condition)
- var currentId = player.media.getAttribute('id');
- if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
- return;
- }
+ player.media.duration = instance.getDuration();
+ player.media.paused = true; // Seeking
- // Get the source URL or ID
- var source = player.media.getAttribute('src');
+ player.media.currentTime = 0;
+ Object.defineProperty(player.media, 'currentTime', {
+ get: function get() {
+ return Number(instance.getCurrentTime());
+ },
+ set: function set(time) {
+ // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
+ if (player.paused && !player.embed.hasPlayed) {
+ player.embed.mute();
+ } // Set seeking state and trigger event
- // Get from <div> if needed
- if (utils.is.empty(source)) {
- source = player.media.getAttribute(this.config.attributes.embed.id);
- }
- // Replace the <iframe> with a <div> due to YouTube API issues
- var videoId = utils.parseYouTubeId(source);
- var id = utils.generateId(player.provider);
- var container = utils.createElement('div', { id: id });
- player.media = utils.replaceElement(container, player.media);
-
- // Set poster image
- player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId));
-
- // Setup instance
- // https://developers.google.com/youtube/iframe_api_reference
- player.embed = new window.YT.Player(id, {
- videoId: videoId,
- playerVars: {
- autoplay: player.config.autoplay ? 1 : 0, // Autoplay
- controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
- rel: 0, // No related vids
- showinfo: 0, // Hide info
- iv_load_policy: 3, // Hide annotations
- modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
- disablekb: 1, // Disable keyboard as we handle it
- playsinline: 1, // Allow iOS inline playback
-
- // Tracking for stats
- // origin: window ? `${window.location.protocol}//${window.location.host}` : null,
- widget_referrer: window ? window.location.href : null,
-
- // Captions are flaky on YouTube
- cc_load_policy: player.captions.active ? 1 : 0,
- cc_lang_pref: player.config.captions.language
- },
- events: {
- onError: function onError(event) {
- // If we've already fired an error, don't do it again
- // YouTube fires onError twice
- if (utils.is.object(player.media.error)) {
- return;
- }
-
- var detail = {
- code: event.data
- };
+ player.media.seeking = true;
+ triggerEvent.call(player, player.media, 'seeking'); // Seek after events sent
+
+ instance.seekTo(time);
+ }
+ }); // Playback speed
+
+ Object.defineProperty(player.media, 'playbackRate', {
+ get: function get() {
+ return instance.getPlaybackRate();
+ },
+ set: function set(input) {
+ instance.setPlaybackRate(input);
+ }
+ }); // Volume
+
+ var volume = player.config.volume;
+ Object.defineProperty(player.media, 'volume', {
+ get: function get() {
+ return volume;
+ },
+ set: function set(input) {
+ volume = input;
+ instance.setVolume(volume * 100);
+ triggerEvent.call(player, player.media, 'volumechange');
+ }
+ }); // Muted
+
+ var muted = player.config.muted;
+ Object.defineProperty(player.media, 'muted', {
+ get: function get() {
+ return muted;
+ },
+ set: function set(input) {
+ var toggle = is.boolean(input) ? input : muted;
+ muted = toggle;
+ instance[toggle ? 'mute' : 'unMute']();
+ triggerEvent.call(player, player.media, 'volumechange');
+ }
+ }); // Source
+
+ Object.defineProperty(player.media, 'currentSrc', {
+ get: function get() {
+ return instance.getVideoUrl();
+ }
+ }); // Ended
+
+ Object.defineProperty(player.media, 'ended', {
+ get: function get() {
+ return player.currentTime === player.duration;
+ }
+ }); // Get available speeds
- // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError
- switch (event.data) {
- case 2:
- detail.message = 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.';
- break;
-
- case 5:
- detail.message = 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';
- break;
-
- case 100:
- detail.message = 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.';
- break;
-
- case 101:
- case 150:
- detail.message = 'The owner of the requested video does not allow it to be played in embedded players.';
- break;
-
- default:
- detail.message = 'An unknown error occured';
- break;
- }
-
- player.media.error = detail;
-
- utils.dispatchEvent.call(player, player.media, 'error');
- },
- onPlaybackQualityChange: function onPlaybackQualityChange() {
- utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
- quality: player.media.quality
- });
- },
- onPlaybackRateChange: function onPlaybackRateChange(event) {
- // Get the instance
- var instance = event.target;
-
- // Get current speed
- player.media.playbackRate = instance.getPlaybackRate();
-
- utils.dispatchEvent.call(player, player.media, 'ratechange');
- },
- onReady: function onReady(event) {
- // Get the instance
- var instance = event.target;
-
- // Get the title
- youtube.getTitle.call(player, videoId);
-
- // Create a faux HTML5 API using the YouTube API
- player.media.play = function () {
- instance.playVideo();
- };
+ player.options.speed = instance.getAvailablePlaybackRates(); // Set the tabindex to avoid focus entering iframe
- player.media.pause = function () {
- instance.pauseVideo();
- };
+ if (player.supported.ui) {
+ player.media.setAttribute('tabindex', -1);
+ }
- player.media.stop = function () {
- instance.stopVideo();
- };
+ triggerEvent.call(player, player.media, 'timeupdate');
+ triggerEvent.call(player, player.media, 'durationchange'); // Reset timer
+
+ clearInterval(player.timers.buffering); // Setup buffering
+
+ player.timers.buffering = setInterval(function () {
+ // Get loaded % from YouTube
+ player.media.buffered = instance.getVideoLoadedFraction(); // Trigger progress only when we actually buffer something
+
+ if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
+ triggerEvent.call(player, player.media, 'progress');
+ } // Set last buffer point
+
+
+ player.media.lastBuffered = player.media.buffered; // Bail if we're at 100%
+
+ if (player.media.buffered === 1) {
+ clearInterval(player.timers.buffering); // Trigger event
+
+ triggerEvent.call(player, player.media, 'canplaythrough');
+ }
+ }, 200); // Rebuild UI
+
+ setTimeout(function () {
+ return ui.build.call(player);
+ }, 50);
+ },
+ onStateChange: function onStateChange(event) {
+ // Get the instance
+ var instance = event.target; // Reset timer
+
+ clearInterval(player.timers.playing);
+ var seeked = player.media.seeking && [1, 2].includes(event.data);
+ if (seeked) {
+ // Unset seeking and fire seeked event
+ player.media.seeking = false;
+ triggerEvent.call(player, player.media, 'seeked');
+ } // Handle events
+ // -1 Unstarted
+ // 0 Ended
+ // 1 Playing
+ // 2 Paused
+ // 3 Buffering
+ // 5 Video cued
+
+
+ switch (event.data) {
+ case -1:
+ // Update scrubber
+ triggerEvent.call(player, player.media, 'timeupdate'); // Get loaded % from YouTube
+
+ player.media.buffered = instance.getVideoLoadedFraction();
+ triggerEvent.call(player, player.media, 'progress');
+ break;
+
+ case 0:
+ assurePlaybackState$1.call(player, false); // YouTube doesn't support loop for a single video, so mimick it.
+
+ if (player.media.loop) {
+ // YouTube needs a call to `stopVideo` before playing again
+ instance.stopVideo();
+ instance.playVideo();
+ } else {
+ triggerEvent.call(player, player.media, 'ended');
+ }
+
+ break;
+
+ case 1:
+ // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
+ if (player.media.paused && !player.embed.hasPlayed) {
+ player.media.pause();
+ } else {
+ assurePlaybackState$1.call(player, true);
+ triggerEvent.call(player, player.media, 'playing'); // Poll to get playback progress
+
+ player.timers.playing = setInterval(function () {
+ triggerEvent.call(player, player.media, 'timeupdate');
+ }, 50); // Check duration again due to YouTube bug
+ // https://github.com/sampotts/plyr/issues/374
+ // https://code.google.com/p/gdata-issues/issues/detail?id=8690
+
+ if (player.media.duration !== instance.getDuration()) {
player.media.duration = instance.getDuration();
- player.media.paused = true;
-
- // Seeking
- player.media.currentTime = 0;
- Object.defineProperty(player.media, 'currentTime', {
- get: function get() {
- return Number(instance.getCurrentTime());
- },
- set: function set(time) {
- // Vimeo will automatically play on seek
- var paused = player.media.paused;
-
- // Set seeking flag
-
- player.media.seeking = true;
-
- // Trigger seeking
- utils.dispatchEvent.call(player, player.media, 'seeking');
-
- // Seek after events sent
- instance.seekTo(time);
-
- // Restore pause state
- if (paused) {
- player.pause();
- }
- }
- });
-
- // Playback speed
- Object.defineProperty(player.media, 'playbackRate', {
- get: function get() {
- return instance.getPlaybackRate();
- },
- set: function set(input) {
- instance.setPlaybackRate(input);
- }
- });
-
- // Quality
- Object.defineProperty(player.media, 'quality', {
- get: function get() {
- return mapQualityUnit(instance.getPlaybackQuality());
- },
- set: function set(input) {
- var quality = input;
-
- // Set via API
- instance.setPlaybackQuality(mapQualityUnit(quality));
-
- // Trigger request event
- utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
- quality: quality
- });
- }
- });
-
- // Volume
- var volume = player.config.volume;
-
- Object.defineProperty(player.media, 'volume', {
- get: function get() {
- return volume;
- },
- set: function set(input) {
- volume = input;
- instance.setVolume(volume * 100);
- utils.dispatchEvent.call(player, player.media, 'volumechange');
- }
- });
-
- // Muted
- var muted = player.config.muted;
-
- Object.defineProperty(player.media, 'muted', {
- get: function get() {
- return muted;
- },
- set: function set(input) {
- var toggle = utils.is.boolean(input) ? input : muted;
- muted = toggle;
- instance[toggle ? 'mute' : 'unMute']();
- utils.dispatchEvent.call(player, player.media, 'volumechange');
- }
- });
-
- // Source
- Object.defineProperty(player.media, 'currentSrc', {
- get: function get() {
- return instance.getVideoUrl();
- }
- });
-
- // Ended
- Object.defineProperty(player.media, 'ended', {
- get: function get() {
- return player.currentTime === player.duration;
- }
- });
-
- // Get available speeds
- player.options.speed = instance.getAvailablePlaybackRates();
-
- // Set the tabindex to avoid focus entering iframe
- if (player.supported.ui) {
- player.media.setAttribute('tabindex', -1);
- }
-
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- utils.dispatchEvent.call(player, player.media, 'durationchange');
-
- // Reset timer
- clearInterval(player.timers.buffering);
-
- // Setup buffering
- player.timers.buffering = setInterval(function () {
- // Get loaded % from YouTube
- player.media.buffered = instance.getVideoLoadedFraction();
-
- // Trigger progress only when we actually buffer something
- if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {
- utils.dispatchEvent.call(player, player.media, 'progress');
- }
-
- // Set last buffer point
- player.media.lastBuffered = player.media.buffered;
-
- // Bail if we're at 100%
- if (player.media.buffered === 1) {
- clearInterval(player.timers.buffering);
-
- // Trigger event
- utils.dispatchEvent.call(player, player.media, 'canplaythrough');
- }
- }, 200);
-
- // Rebuild UI
- setTimeout(function () {
- return ui.build.call(player);
- }, 50);
- },
- onStateChange: function onStateChange(event) {
- // Get the instance
- var instance = event.target;
-
- // Reset timer
- clearInterval(player.timers.playing);
-
- // Handle events
- // -1 Unstarted
- // 0 Ended
- // 1 Playing
- // 2 Paused
- // 3 Buffering
- // 5 Video cued
- switch (event.data) {
- case -1:
- // Update scrubber
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
-
- // Get loaded % from YouTube
- player.media.buffered = instance.getVideoLoadedFraction();
- utils.dispatchEvent.call(player, player.media, 'progress');
-
- break;
-
- case 0:
- player.media.paused = true;
-
- // YouTube doesn't support loop for a single video, so mimick it.
- if (player.media.loop) {
- // YouTube needs a call to `stopVideo` before playing again
- instance.stopVideo();
- instance.playVideo();
- } else {
- utils.dispatchEvent.call(player, player.media, 'ended');
- }
-
- break;
-
- case 1:
- // If we were seeking, fire seeked event
- if (player.media.seeking) {
- utils.dispatchEvent.call(player, player.media, 'seeked');
- }
- player.media.seeking = false;
-
- // 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');
-
- // Poll to get playback progress
- player.timers.playing = setInterval(function () {
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- }, 50);
-
- // Check duration again due to YouTube bug
- // https://github.com/sampotts/plyr/issues/374
- // https://code.google.com/p/gdata-issues/issues/detail?id=8690
- if (player.media.duration !== instance.getDuration()) {
- player.media.duration = instance.getDuration();
- utils.dispatchEvent.call(player, player.media, 'durationchange');
- }
-
- // Get quality
- controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
-
- break;
-
- case 2:
- player.media.paused = true;
-
- utils.dispatchEvent.call(player, player.media, 'pause');
-
- break;
-
- default:
- break;
- }
-
- utils.dispatchEvent.call(player, player.elements.container, 'statechange', false, {
- code: event.data
- });
+ triggerEvent.call(player, player.media, 'durationchange');
+ }
}
+
+ break;
+
+ case 2:
+ // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)
+ if (!player.muted) {
+ player.embed.unMute();
+ }
+
+ assurePlaybackState$1.call(player, false);
+ break;
+
+ default:
+ break;
}
- });
- }
-};
-// ==========================================================================
+ triggerEvent.call(player, player.elements.container, 'statechange', false, {
+ code: event.data
+ });
+ }
+ }
+ });
+ }
+ };
-var media = {
+ // ==========================================================================
+ var media = {
// Setup media
setup: function setup() {
- // If there's no media, bail
- if (!this.media) {
- this.debug.warn('No media element found!');
- return;
- }
+ // If there's no media, bail
+ if (!this.media) {
+ this.debug.warn('No media element found!');
+ return;
+ } // Add type class
- // Add type class
- utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);
- // Add provider class
- utils.toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);
+ toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true); // Add provider class
- // Add video class for embeds
- // This will require changes if audio embeds are added
- if (this.isEmbed) {
- utils.toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
- }
+ toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true); // Add video class for embeds
+ // This will require changes if audio embeds are added
- // Inject the player wrapper
- if (this.isVideo) {
- // Create the wrapper div
- this.elements.wrapper = utils.createElement('div', {
- class: this.config.classNames.video
- });
+ if (this.isEmbed) {
+ toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);
+ } // Inject the player wrapper
- // Wrap the video in a container
- utils.wrap(this.media, this.elements.wrapper);
- // Faux poster container
- this.elements.poster = utils.createElement('div', {
- class: this.config.classNames.poster
- });
+ if (this.isVideo) {
+ // Create the wrapper div
+ this.elements.wrapper = createElement('div', {
+ class: this.config.classNames.video
+ }); // Wrap the video in a container
- this.elements.wrapper.appendChild(this.elements.poster);
- }
-
- if (this.isEmbed) {
- switch (this.provider) {
- case 'youtube':
- youtube.setup.call(this);
- break;
+ wrap(this.media, this.elements.wrapper); // Faux poster container
- case 'vimeo':
- vimeo.setup.call(this);
- break;
+ this.elements.poster = createElement('div', {
+ class: this.config.classNames.poster
+ });
+ this.elements.wrapper.appendChild(this.elements.poster);
+ }
- default:
- break;
- }
- } else if (this.isHTML5) {
- html5.extend.call(this);
- }
+ if (this.isHTML5) {
+ html5.extend.call(this);
+ } else if (this.isYouTube) {
+ youtube.setup.call(this);
+ } else if (this.isVimeo) {
+ vimeo.setup.call(this);
+ }
}
-};
-
-// ==========================================================================
+ };
-var Ads = function () {
+ var Ads =
+ /*#__PURE__*/
+ function () {
/**
* Ads constructor.
* @param {object} player
* @return {Ads}
*/
function Ads(player) {
- var _this = this;
+ var _this = this;
+
+ _classCallCheck(this, Ads);
+
+ this.player = player;
+ this.config = player.config.ads;
+ this.playing = false;
+ this.initialized = false;
+ this.elements = {
+ container: null,
+ displayContainer: null
+ };
+ this.manager = null;
+ this.loader = null;
+ this.cuePoints = null;
+ this.events = {};
+ this.safetyTimer = null;
+ this.countdownTimer = null; // Setup a promise to resolve when the IMA manager is ready
+
+ this.managerPromise = new Promise(function (resolve, reject) {
+ // The ad is loaded and ready
+ _this.on('loaded', resolve); // Ads failed
+
+
+ _this.on('error', reject);
+ });
+ this.load();
+ }
- classCallCheck(this, Ads);
+ _createClass(Ads, [{
+ key: "load",
- this.player = player;
- this.publisherId = player.config.ads.publisherId;
- this.playing = false;
- this.initialized = false;
- this.elements = {
- container: null,
- displayContainer: null
- };
- this.manager = null;
- this.loader = null;
- this.cuePoints = null;
- this.events = {};
- this.safetyTimer = null;
- this.countdownTimer = null;
-
- // Setup a promise to resolve when the IMA manager is ready
- this.managerPromise = new Promise(function (resolve, reject) {
- // The ad is loaded and ready
- _this.on('loaded', resolve);
-
- // Ads failed
- _this.on('error', reject);
+ /**
+ * Load the IMA SDK
+ */
+ value: function load() {
+ var _this2 = this;
+
+ if (this.enabled) {
+ // Check if the Google IMA3 SDK is loaded or load it ourselves
+ if (!is.object(window.google) || !is.object(window.google.ima)) {
+ loadScript(this.player.config.urls.googleIMA.sdk).then(function () {
+ _this2.ready();
+ }).catch(function () {
+ // Script failed to load or is blocked
+ _this2.trigger('error', new Error('Google IMA SDK failed to load'));
+ });
+ } else {
+ this.ready();
+ }
+ }
+ }
+ /**
+ * Get the ads instance ready
+ */
+
+ }, {
+ key: "ready",
+ value: function ready$$1() {
+ var _this3 = this;
+
+ // Start ticking our safety timer. If the whole advertisement
+ // thing doesn't resolve within our set time; we bail
+ this.startSafetyTimer(12000, 'ready()'); // Clear the safety timer
+
+ this.managerPromise.then(function () {
+ _this3.clearSafetyTimer('onAdsManagerLoaded()');
+ }); // Set listeners on the Plyr instance
+
+ this.listeners(); // Setup the IMA SDK
+
+ this.setupIMA();
+ } // Build the tag URL
+
+ }, {
+ key: "setupIMA",
+
+ /**
+ * In order for the SDK to display ads for our video, we need to tell it where to put them,
+ * so here we define our ad container. This div is set up to render on top of the video player.
+ * Using the code below, we tell the SDK to render ads within that div. We also provide a
+ * handle to the content video player - the SDK will poll the current time of our player to
+ * properly place mid-rolls. After we create the ad display container, we initialize it. On
+ * mobile devices, this initialization is done as the result of a user action.
+ */
+ value: function setupIMA() {
+ // Create the container for our advertisements
+ this.elements.container = createElement('div', {
+ class: this.player.config.classNames.ads
});
+ this.player.elements.container.appendChild(this.elements.container); // So we can run VPAID2
- this.load();
- }
+ google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); // Set language
- createClass(Ads, [{
- key: 'load',
-
-
- /**
- * Load the IMA SDK
- */
- value: function load() {
- var _this2 = this;
-
- if (this.enabled) {
- // Check if the Google IMA3 SDK is loaded or load it ourselves
- if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
- utils.loadScript(this.player.config.urls.googleIMA.sdk).then(function () {
- _this2.ready();
- }).catch(function () {
- // Script failed to load or is blocked
- _this2.trigger('error', new Error('Google IMA SDK failed to load'));
- });
- } else {
- this.ready();
- }
- }
- }
+ google.ima.settings.setLocale(this.player.config.ads.language); // We assume the adContainer is the video container of the plyr element
+ // that will house the ads
+
+ this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); // Request video ads to be pre-loaded
- /**
- * Get the ads instance ready
- */
+ this.requestAds();
+ }
+ /**
+ * Request advertisements
+ */
}, {
- key: 'ready',
- value: function ready() {
- var _this3 = this;
+ key: "requestAds",
+ value: function requestAds() {
+ var _this4 = this;
- // Start ticking our safety timer. If the whole advertisement
- // thing doesn't resolve within our set time; we bail
- this.startSafetyTimer(12000, 'ready()');
+ var container = this.player.elements.container;
- // Clear the safety timer
- this.managerPromise.then(function () {
- _this3.clearSafetyTimer('onAdsManagerLoaded()');
- });
+ try {
+ // Create ads loader
+ this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
- // Set listeners on the Plyr instance
- this.listeners();
+ this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
+ return _this4.onAdsManagerLoaded(event);
+ }, false);
+ this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
+ return _this4.onAdError(error);
+ }, false); // Request video ads
- // Setup the IMA SDK
- this.setupIMA();
- }
+ var request = new google.ima.AdsRequest();
+ request.adTagUrl = this.tagUrl; // Specify the linear and nonlinear slot sizes. This helps the SDK
+ // to select the correct creative if multiple are returned
- // Build the default tag URL
+ request.linearAdSlotWidth = container.offsetWidth;
+ request.linearAdSlotHeight = container.offsetHeight;
+ request.nonLinearAdSlotWidth = container.offsetWidth;
+ request.nonLinearAdSlotHeight = container.offsetHeight; // We only overlay ads as we only support video.
- }, {
- key: 'setupIMA',
-
-
- /**
- * In order for the SDK to display ads for our video, we need to tell it where to put them,
- * so here we define our ad container. This div is set up to render on top of the video player.
- * Using the code below, we tell the SDK to render ads within that div. We also provide a
- * handle to the content video player - the SDK will poll the current time of our player to
- * properly place mid-rolls. After we create the ad display container, we initialize it. On
- * mobile devices, this initialization is done as the result of a user action.
- */
- value: function setupIMA() {
- // Create the container for our advertisements
- this.elements.container = utils.createElement('div', {
- class: this.player.config.classNames.ads
- });
- this.player.elements.container.appendChild(this.elements.container);
+ request.forceNonLinearFullSlot = false; // Mute based on current state
- // So we can run VPAID2
- google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);
+ request.setAdWillPlayMuted(!this.player.muted);
+ this.loader.requestAds(request);
+ } catch (e) {
+ this.onAdError(e);
+ }
+ }
+ /**
+ * Update the ad countdown
+ * @param {boolean} start
+ */
- // Set language
- google.ima.settings.setLocale(this.player.config.ads.language);
+ }, {
+ key: "pollCountdown",
+ value: function pollCountdown() {
+ var _this5 = this;
- // 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);
+ var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
- // Request video ads to be pre-loaded
- this.requestAds();
+ if (!start) {
+ clearInterval(this.countdownTimer);
+ this.elements.container.removeAttribute('data-badge-text');
+ return;
}
- /**
- * Request advertisements
- */
+ var update = function update() {
+ var time = formatTime(Math.max(_this5.manager.getRemainingTime(), 0));
+ var label = "".concat(i18n.get('advertisement', _this5.player.config), " - ").concat(time);
+
+ _this5.elements.container.setAttribute('data-badge-text', label);
+ };
+
+ this.countdownTimer = setInterval(update, 100);
+ }
+ /**
+ * This method is called whenever the ads are ready inside the AdDisplayContainer
+ * @param {Event} adsManagerLoadedEvent
+ */
}, {
- key: 'requestAds',
- value: function requestAds() {
- var _this4 = this;
+ key: "onAdsManagerLoaded",
+ value: function onAdsManagerLoaded(event) {
+ var _this6 = this;
- var container = this.player.elements.container;
+ // Load could occur after a source change (race condition)
+ if (!this.enabled) {
+ return;
+ } // Get the ads manager
- try {
- // Create ads loader
- this.loader = new google.ima.AdsLoader(this.elements.displayContainer);
-
- // Listen and respond to ads loaded and error events
- this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, function (event) {
- return _this4.onAdsManagerLoaded(event);
- }, false);
- this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
- return _this4.onAdError(error);
- }, false);
-
- // Request video ads
- var request = new google.ima.AdsRequest();
- request.adTagUrl = this.tagUrl;
-
- // Specify the linear and nonlinear slot sizes. This helps the SDK
- // to select the correct creative if multiple are returned
- request.linearAdSlotWidth = container.offsetWidth;
- request.linearAdSlotHeight = container.offsetHeight;
- request.nonLinearAdSlotWidth = container.offsetWidth;
- request.nonLinearAdSlotHeight = container.offsetHeight;
-
- // We only overlay ads as we only support video.
- request.forceNonLinearFullSlot = false;
-
- // Mute based on current state
- request.setAdWillPlayMuted(!this.player.muted);
-
- this.loader.requestAds(request);
- } catch (e) {
- this.onAdError(e);
- }
- }
+ var settings = new google.ima.AdsRenderingSettings(); // Tell the SDK to save and restore content video state on our behalf
- /**
- * Update the ad countdown
- * @param {boolean} start
- */
+ settings.restoreCustomPlaybackStateOnAdBreakComplete = true;
+ settings.enablePreloading = true; // The SDK is polling currentTime on the contentPlayback. And needs a duration
+ // so it can determine when to start the mid- and post-roll
- }, {
- key: 'pollCountdown',
- value: function pollCountdown() {
- var _this5 = this;
+ this.manager = event.getAdsManager(this.player, settings); // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
+
+ this.cuePoints = this.manager.getCuePoints(); // Add advertisement cue's within the time line if available
- var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ if (!is.empty(this.cuePoints)) {
+ this.cuePoints.forEach(function (cuePoint) {
+ if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < _this6.player.duration) {
+ var seekElement = _this6.player.elements.progress;
- if (!start) {
- clearInterval(this.countdownTimer);
- this.elements.container.removeAttribute('data-badge-text');
- return;
+ if (is.element(seekElement)) {
+ var cuePercentage = 100 / _this6.player.duration * cuePoint;
+ var cue = createElement('span', {
+ class: _this6.player.config.classNames.cues
+ });
+ cue.style.left = "".concat(cuePercentage.toString(), "%");
+ seekElement.appendChild(cue);
+ }
}
+ });
+ } // Set volume to match player
- 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);
- };
- this.countdownTimer = setInterval(update, 100);
- }
+ 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);
+ }); // Advertisement regular events
- /**
- * This method is called whenever the ads are ready inside the AdDisplayContainer
- * @param {Event} adsManagerLoadedEvent
- */
+ Object.keys(google.ima.AdEvent.Type).forEach(function (type) {
+ _this6.manager.addEventListener(google.ima.AdEvent.Type[type], function (event) {
+ return _this6.onAdEvent(event);
+ });
+ }); // Resolve our adsManager
+
+ this.trigger('loaded');
+ }
+ /**
+ * This is where all the event handling takes place. Retrieve the ad from the event. Some
+ * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated
+ * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type
+ * @param {Event} event
+ */
}, {
- key: 'onAdsManagerLoaded',
- value: function onAdsManagerLoaded(event) {
- var _this6 = this;
-
- // Get the ads manager
- var settings = new google.ima.AdsRenderingSettings();
-
- // Tell the SDK to save and restore content video state on our behalf
- settings.restoreCustomPlaybackStateOnAdBreakComplete = true;
- settings.enablePreloading = true;
-
- // The SDK is polling currentTime on the contentPlayback. And needs a duration
- // so it can determine when to start the mid- and post-roll
- this.manager = event.getAdsManager(this.player, settings);
-
- // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
- this.cuePoints = this.manager.getCuePoints();
-
- // Add advertisement cue's within the time line if available
- if (!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;
-
- if (utils.is.element(seekElement)) {
- var cuePercentage = 100 / _this6.player.duration * cuePoint;
- var cue = utils.createElement('span', {
- class: _this6.player.config.classNames.cues
- });
-
- cue.style.left = cuePercentage.toString() + '%';
- seekElement.appendChild(cue);
- }
- }
- });
- }
+ key: "onAdEvent",
+ value: function onAdEvent(event) {
+ var _this7 = this;
- // Get skippable state
- // TODO: Skip button
- // this.player.debug.warn(this.manager.getAdSkippableState());
+ var container = this.player.elements.container; // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
+ // don't have ad object associated
- // Set volume to match player
- this.manager.setVolume(this.player.volume);
+ var ad = event.getAd();
+ var adData = event.getAdData(); // Proxy event
- // Add listeners to the required events
- // Advertisement error events
- this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
- return _this6.onAdError(error);
- });
+ var dispatchEvent = function dispatchEvent(type) {
+ var event = "ads".concat(type.replace(/_/g, '').toLowerCase());
+ triggerEvent.call(_this7.player, _this7.player.media, event);
+ };
- // 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);
- });
- });
+ switch (event.type) {
+ case google.ima.AdEvent.Type.LOADED:
+ // This is the first event sent for an ad - it is possible to determine whether the
+ // ad is a video ad or an overlay
+ this.trigger('loaded'); // Bubble event
+
+ dispatchEvent(event.type); // Start countdown
+
+ this.pollCountdown(true);
+
+ if (!ad.isLinear()) {
+ // Position AdDisplayContainer correctly for overlay
+ ad.width = container.offsetWidth;
+ ad.height = container.offsetHeight;
+ } // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
+ // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
+
+
+ break;
+
+ case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
+ // All ads for the current videos are done. We can now request new advertisements
+ // in case the video is re-played
+ // Fire event
+ dispatchEvent(event.type); // TODO: Example for what happens when a next video in a playlist would be loaded.
+ // So here we load a new video when all ads are done.
+ // Then we load new ads within a new adsManager. When the video
+ // Is started - after - the ads are loaded, then we get ads.
+ // You can also easily test cancelling and reloading by running
+ // player.ads.cancel() and player.ads.play from the console I guess.
+ // this.player.source = {
+ // type: 'video',
+ // title: 'View From A Blue Moon',
+ // sources: [{
+ // src:
+ // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:
+ // 'video/mp4', }], poster:
+ // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:
+ // [ { kind: 'captions', label: 'English', srclang: 'en', src:
+ // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
+ // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:
+ // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],
+ // };
+ // TODO: So there is still this thing where a video should only be allowed to start
+ // playing when the IMA SDK is ready or has failed
- // Resolve our adsManager
- this.trigger('loaded');
+ this.loadAds();
+ break;
+
+ case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
+ // This event indicates the ad has started - the video player can adjust the UI,
+ // for example display a pause button and remaining time. Fired when content should
+ // be paused. This usually happens right before an ad is about to cover the content
+ dispatchEvent(event.type);
+ this.pauseContent();
+ break;
+
+ case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
+ // This event indicates the ad has finished - the video player can perform
+ // appropriate UI actions, such as removing the timer for remaining time detection.
+ // Fired when content should be resumed. This usually happens when an ad finishes
+ // or collapses
+ dispatchEvent(event.type);
+ this.pollCountdown();
+ this.resumeContent();
+ break;
+
+ case google.ima.AdEvent.Type.STARTED:
+ case google.ima.AdEvent.Type.MIDPOINT:
+ case google.ima.AdEvent.Type.COMPLETE:
+ case google.ima.AdEvent.Type.IMPRESSION:
+ case google.ima.AdEvent.Type.CLICK:
+ dispatchEvent(event.type);
+ break;
+
+ case google.ima.AdEvent.Type.LOG:
+ if (adData.adError) {
+ this.player.debug.warn("Non-fatal ad error: ".concat(adData.adError.getMessage()));
+ }
+
+ break;
+
+ default:
+ break;
}
+ }
+ /**
+ * Any ad error handling comes through here
+ * @param {Event} event
+ */
- /**
- * This is where all the event handling takes place. Retrieve the ad from the event. Some
- * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated
- * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type
- * @param {Event} event
- */
+ }, {
+ key: "onAdError",
+ value: function onAdError(event) {
+ this.cancel();
+ this.player.debug.warn('Ads error', event);
+ }
+ /**
+ * Setup hooks for Plyr and window events. This ensures
+ * the mid- and post-roll launch at the correct time. And
+ * resize the advertisement when the player resizes
+ */
}, {
- key: 'onAdEvent',
- value: function onAdEvent(event) {
- var _this7 = this;
+ key: "listeners",
+ value: function listeners() {
+ var _this8 = this;
- var container = this.player.elements.container;
+ var container = this.player.elements.container;
+ var time; // Add listeners to the required events
- // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
- // don't have ad object associated
+ this.player.on('ended', function () {
+ _this8.loader.contentComplete();
+ });
+ this.player.on('timeupdate', function () {
+ time = _this8.player.currentTime;
+ });
+ this.player.on('seeked', function () {
+ var seekedTime = _this8.player.currentTime;
- var ad = event.getAd();
+ if (is.empty(_this8.cuePoints)) {
+ return;
+ }
- // Proxy event
- var dispatchEvent = function dispatchEvent(type) {
- var event = 'ads' + type.replace(/_/g, '').toLowerCase();
- utils.dispatchEvent.call(_this7.player, _this7.player.media, event);
- };
+ _this8.cuePoints.forEach(function (cuePoint, index) {
+ if (time < cuePoint && cuePoint < seekedTime) {
+ _this8.manager.discardAdBreak();
- switch (event.type) {
- case google.ima.AdEvent.Type.LOADED:
- // This is the first event sent for an ad - it is possible to determine whether the
- // ad is a video ad or an overlay
- this.trigger('loaded');
-
- // Bubble event
- dispatchEvent(event.type);
-
- // Start countdown
- this.pollCountdown(true);
-
- if (!ad.isLinear()) {
- // Position AdDisplayContainer correctly for overlay
- ad.width = container.offsetWidth;
- ad.height = container.offsetHeight;
- }
-
- // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());
- // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());
- break;
-
- case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
- // All ads for the current videos are done. We can now request new advertisements
- // in case the video is re-played
-
- // Fire event
- dispatchEvent(event.type);
-
- // TODO: Example for what happens when a next video in a playlist would be loaded.
- // So here we load a new video when all ads are done.
- // Then we load new ads within a new adsManager. When the video
- // Is started - after - the ads are loaded, then we get ads.
- // You can also easily test cancelling and reloading by running
- // player.ads.cancel() and player.ads.play from the console I guess.
- // this.player.source = {
- // type: 'video',
- // title: 'View From A Blue Moon',
- // sources: [{
- // src:
- // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:
- // 'video/mp4', }], poster:
- // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:
- // [ { kind: 'captions', label: 'English', srclang: 'en', src:
- // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
- // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:
- // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],
- // };
-
- // TODO: So there is still this thing where a video should only be allowed to start
- // playing when the IMA SDK is ready or has failed
-
- this.loadAds();
- break;
-
- case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:
- // This event indicates the ad has started - the video player can adjust the UI,
- // for example display a pause button and remaining time. Fired when content should
- // be paused. This usually happens right before an ad is about to cover the content
-
- dispatchEvent(event.type);
-
- this.pauseContent();
-
- break;
-
- case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:
- // This event indicates the ad has finished - the video player can perform
- // appropriate UI actions, such as removing the timer for remaining time detection.
- // Fired when content should be resumed. This usually happens when an ad finishes
- // or collapses
-
- dispatchEvent(event.type);
-
- this.pollCountdown();
-
- this.resumeContent();
-
- break;
-
- case google.ima.AdEvent.Type.STARTED:
- case google.ima.AdEvent.Type.MIDPOINT:
- case google.ima.AdEvent.Type.COMPLETE:
- case google.ima.AdEvent.Type.IMPRESSION:
- case google.ima.AdEvent.Type.CLICK:
- dispatchEvent(event.type);
- break;
-
- default:
- break;
+ _this8.cuePoints.splice(index, 1);
}
- }
+ });
+ }); // Listen to the resizing of the window. And resize ad accordingly
+ // TODO: eventually implement ResizeObserver
- /**
- * Any ad error handling comes through here
- * @param {Event} event
- */
+ window.addEventListener('resize', function () {
+ if (_this8.manager) {
+ _this8.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
+ }
+ });
+ }
+ /**
+ * Initialize the adsManager and start playing advertisements
+ */
}, {
- key: 'onAdError',
- value: function onAdError(event) {
- this.cancel();
- this.player.debug.warn('Ads error', event);
- }
+ key: "play",
+ value: function play() {
+ var _this9 = this;
- /**
- * 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
- */
+ var container = this.player.elements.container;
- }, {
- key: 'listeners',
- value: function listeners() {
- var _this8 = this;
+ if (!this.managerPromise) {
+ this.resumeContent();
+ } // Play the requested advertisement whenever the adsManager is ready
- var container = this.player.elements.container;
- var time = void 0;
+ this.managerPromise.then(function () {
+ // Initialize the container. Must be done via a user action on mobile devices
+ _this9.elements.displayContainer.initialize();
- // Add listeners to the required events
- this.player.on('ended', function () {
- _this8.loader.contentComplete();
- });
+ try {
+ if (!_this9.initialized) {
+ // Initialize the ads manager. Ad rules playlist will start at this time
+ _this9.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will
+ // start at this time; the call will be ignored for ad rules
- this.player.on('seeking', function () {
- time = _this8.player.currentTime;
- return time;
- });
- this.player.on('seeked', function () {
- var seekedTime = _this8.player.currentTime;
+ _this9.manager.start();
+ }
- if (utils.is.empty(_this8.cuePoints)) {
- return;
- }
+ _this9.initialized = true;
+ } catch (adError) {
+ // An error may be thrown if there was a problem with the
+ // VAST response
+ _this9.onAdError(adError);
+ }
+ }).catch(function () {});
+ }
+ /**
+ * Resume our video
+ */
- _this8.cuePoints.forEach(function (cuePoint, index) {
- if (time < cuePoint && cuePoint < seekedTime) {
- _this8.manager.discardAdBreak();
- _this8.cuePoints.splice(index, 1);
- }
- });
- });
+ }, {
+ key: "resumeContent",
+ value: function resumeContent() {
+ // Hide the advertisement container
+ this.elements.container.style.zIndex = ''; // Ad is stopped
- // 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);
- }
- });
- }
+ this.playing = false; // Play our video
- /**
- * Initialize the adsManager and start playing advertisements
- */
+ if (this.player.currentTime < this.player.duration) {
+ this.player.play();
+ }
+ }
+ /**
+ * Pause our video
+ */
}, {
- key: 'play',
- value: function play() {
- var _this9 = this;
+ key: "pauseContent",
+ value: function pauseContent() {
+ // Show the advertisement container
+ this.elements.container.style.zIndex = 3; // Ad is playing.
- var container = this.player.elements.container;
+ this.playing = true; // Pause our video.
+ this.player.pause();
+ }
+ /**
+ * Destroy the adsManager so we can grab new ads after this. If we don't then we're not
+ * allowed to call new ads based on google policies, as they interpret this as an accidental
+ * video requests. https://developers.google.com/interactive-
+ * media-ads/docs/sdks/android/faq#8
+ */
- if (!this.managerPromise) {
- this.resumeContent();
- }
+ }, {
+ key: "cancel",
+ value: function cancel() {
+ // Pause our video
+ if (this.initialized) {
+ this.resumeContent();
+ } // Tell our instance that we're done for now
- // Play the requested advertisement whenever the adsManager is ready
- this.managerPromise.then(function () {
- // Initialize the container. Must be done via a user action on mobile devices
- _this9.elements.displayContainer.initialize();
-
- try {
- if (!_this9.initialized) {
- // Initialize the ads manager. Ad rules playlist will start at this time
- _this9.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
-
- // Call play to start showing the ad. Single video and overlay ads will
- // start at this time; the call will be ignored for ad rules
- _this9.manager.start();
- }
-
- _this9.initialized = true;
- } catch (adError) {
- // An error may be thrown if there was a problem with the
- // VAST response
- _this9.onAdError(adError);
- }
- }).catch(function () {});
- }
- /**
- * Resume our video
- */
+ this.trigger('error'); // Re-create our adsManager
+
+ this.loadAds();
+ }
+ /**
+ * Re-create our adsManager
+ */
}, {
- key: 'resumeContent',
- value: function resumeContent() {
- // Hide the advertisement container
- this.elements.container.style.zIndex = '';
+ key: "loadAds",
+ value: function loadAds() {
+ var _this10 = this;
- // Ad is stopped
- this.playing = false;
+ // 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
- // Play our video
- if (this.player.currentTime < this.player.duration) {
- this.player.play();
- }
- }
- /**
- * Pause our video
- */
+ _this10.managerPromise = new Promise(function (resolve) {
+ _this10.on('loaded', resolve);
- }, {
- key: 'pauseContent',
- value: function pauseContent() {
- // Show the advertisement container
- this.elements.container.style.zIndex = 3;
+ _this10.player.debug.log(_this10.manager);
+ }); // Now request some new advertisements
+
+ _this10.requestAds();
+ }).catch(function () {});
+ }
+ /**
+ * Handles callbacks after an ad event was invoked
+ * @param {string} event - Event type
+ */
- // Ad is playing.
- this.playing = true;
+ }, {
+ key: "trigger",
+ value: function trigger(event) {
+ var _this11 = this;
- // Pause our video.
- this.player.pause();
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
}
- /**
- * 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
- */
+ var handlers = this.events[event];
- }, {
- key: 'cancel',
- value: function cancel() {
- // Pause our video
- if (this.initialized) {
- this.resumeContent();
+ if (is.array(handlers)) {
+ handlers.forEach(function (handler) {
+ if (is.function(handler)) {
+ handler.apply(_this11, args);
}
+ });
+ }
+ }
+ /**
+ * Add event listeners
+ * @param {string} event - Event type
+ * @param {function} callback - Callback for when event occurs
+ * @return {Ads}
+ */
- // Tell our instance that we're done for now
- this.trigger('error');
-
- // Re-create our adsManager
- this.loadAds();
+ }, {
+ key: "on",
+ value: function on$$1(event, callback) {
+ if (!is.array(this.events[event])) {
+ this.events[event] = [];
}
- /**
- * Re-create our adsManager
- */
+ this.events[event].push(callback);
+ return this;
+ }
+ /**
+ * Setup a safety timer for when the ad network doesn't respond for whatever reason.
+ * The advertisement has 12 seconds to get its things together. We stop this timer when the
+ * advertisement is playing, or when a user action is required to start, then we clear the
+ * timer on ad ready
+ * @param {number} time
+ * @param {string} from
+ */
}, {
- key: 'loadAds',
- value: function loadAds() {
- var _this10 = this;
-
- // Tell our adsManager to go bye bye
- this.managerPromise.then(function () {
- // Destroy our adsManager
- if (_this10.manager) {
- _this10.manager.destroy();
- }
-
- // Re-set our adsManager promises
- _this10.managerPromise = new Promise(function (resolve) {
- _this10.on('loaded', resolve);
- _this10.player.debug.log(_this10.manager);
- });
+ key: "startSafetyTimer",
+ value: function startSafetyTimer(time, from) {
+ var _this12 = this;
- // Now request some new advertisements
- _this10.requestAds();
- }).catch(function () {});
- }
+ this.player.debug.log("Safety timer invoked from: ".concat(from));
+ this.safetyTimer = setTimeout(function () {
+ _this12.cancel();
- /**
- * Handles callbacks after an ad event was invoked
- * @param {string} event - Event type
- */
+ _this12.clearSafetyTimer('startSafetyTimer()');
+ }, time);
+ }
+ /**
+ * Clear our safety timer(s)
+ * @param {string} from
+ */
}, {
- key: 'trigger',
- value: function trigger(event) {
- var _this11 = this;
+ key: "clearSafetyTimer",
+ value: function clearSafetyTimer(from) {
+ if (!is.nullOrUndefined(this.safetyTimer)) {
+ this.player.debug.log("Safety timer cleared from: ".concat(from));
+ clearTimeout(this.safetyTimer);
+ this.safetyTimer = null;
+ }
+ }
+ }, {
+ key: "enabled",
+ get: function get() {
+ var config = this.config;
+ return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));
+ }
+ }, {
+ key: "tagUrl",
+ get: function get() {
+ var config = this.config;
+
+ if (is.url(config.tagUrl)) {
+ return config.tagUrl;
+ }
+
+ var params = {
+ AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
+ AV_CHANNELID: '5a0458dc28a06145e4519d21',
+ AV_URL: window.location.hostname,
+ cb: Date.now(),
+ AV_WIDTH: 640,
+ AV_HEIGHT: 480,
+ AV_CDIM2: this.publisherId
+ };
+ var base = 'https://go.aniview.com/api/adserver6/vast/';
+ return "".concat(base, "?").concat(buildUrlParams(params));
+ }
+ }]);
- for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
- args[_key - 1] = arguments[_key];
- }
+ return Ads;
+ }();
+
+ var parseVtt = function parseVtt(vttDataString) {
+ var processedList = [];
+ var frames = vttDataString.split(/\r\n\r\n|\n\n|\r\r/);
+ frames.forEach(function (frame) {
+ var result = {};
+ var lines = frame.split(/\r\n|\n|\r/);
+ lines.forEach(function (line) {
+ if (!is.number(result.startTime)) {
+ // The line with start and end times on it is the first line of interest
+ var matchTimes = line.match(/([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT
+
+ if (matchTimes) {
+ result.startTime = Number(matchTimes[1]) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number("0.".concat(matchTimes[4]));
+ result.endTime = Number(matchTimes[6]) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number("0.".concat(matchTimes[9]));
+ }
+ } else if (!is.empty(line.trim()) && is.empty(result.text)) {
+ // If we already have the startTime, then we're definitely up to the text line(s)
+ var lineSplit = line.trim().split('#xywh=');
+
+ var _lineSplit = _slicedToArray(lineSplit, 1);
+
+ result.text = _lineSplit[0];
+
+ // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image
+ if (lineSplit[1]) {
+ var _lineSplit$1$split = lineSplit[1].split(',');
+
+ var _lineSplit$1$split2 = _slicedToArray(_lineSplit$1$split, 4);
+
+ result.x = _lineSplit$1$split2[0];
+ result.y = _lineSplit$1$split2[1];
+ result.w = _lineSplit$1$split2[2];
+ result.h = _lineSplit$1$split2[3];
+ }
+ }
+ });
+
+ if (result.text) {
+ processedList.push(result);
+ }
+ });
+ return processedList;
+ };
+ /**
+ * Preview thumbnails for seek hover and scrubbing
+ * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar
+ * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed
+ *
+ * Notes:
+ * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole
+ * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
+ * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered
+ */
+
+
+ var PreviewThumbnails =
+ /*#__PURE__*/
+ function () {
+ /**
+ * PreviewThumbnails constructor.
+ * @param {Plyr} player
+ * @return {PreviewThumbnails}
+ */
+ function PreviewThumbnails(player) {
+ _classCallCheck(this, PreviewThumbnails);
+
+ this.player = player;
+ this.thumbnails = [];
+ this.loaded = false;
+ this.lastMouseMoveTime = Date.now();
+ this.mouseDown = false;
+ this.loadedImages = [];
+ this.elements = {
+ thumb: {},
+ scrubbing: {}
+ };
+ this.load();
+ }
- var handlers = this.events[event];
+ _createClass(PreviewThumbnails, [{
+ key: "load",
+ value: function load() {
+ var _this = this;
- if (utils.is.array(handlers)) {
- handlers.forEach(function (handler) {
- if (utils.is.function(handler)) {
- handler.apply(_this11, args);
- }
- });
- }
+ // Togglethe regular seek tooltip
+ if (this.player.elements.display.seekTooltip) {
+ this.player.elements.display.seekTooltip.hidden = this.enabled;
}
- /**
- * Add event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
- * @return {Ads}
- */
+ if (!this.enabled) {
+ return;
+ }
- }, {
- key: 'on',
- value: function on(event, callback) {
- if (!utils.is.array(this.events[event])) {
- this.events[event] = [];
- }
+ this.getThumbnails().then(function () {
+ // Render DOM elements
+ _this.render(); // Check to see if thumb container size was specified manually in CSS
- this.events[event].push(callback);
- return this;
- }
+ _this.determineContainerAutoSizing();
- /**
- * Setup a safety timer for when the ad network doesn't respond for whatever reason.
- * The advertisement has 12 seconds to get its things together. We stop this timer when the
- * advertisement is playing, or when a user action is required to start, then we clear the
- * timer on ad ready
- * @param {number} time
- * @param {string} from
- */
+ _this.loaded = true;
+ });
+ } // Download VTT files and parse them
}, {
- key: 'startSafetyTimer',
- value: function startSafetyTimer(time, from) {
- var _this12 = this;
+ key: "getThumbnails",
+ value: function getThumbnails() {
+ var _this2 = this;
- this.player.debug.log('Safety timer invoked from: ' + from);
+ return new Promise(function (resolve) {
+ var src = _this2.player.config.previewThumbnails.src;
- this.safetyTimer = setTimeout(function () {
- _this12.cancel();
- _this12.clearSafetyTimer('startSafetyTimer()');
- }, time);
- }
+ if (is.empty(src)) {
+ throw new Error('Missing previewThumbnails.src config attribute');
+ } // If string, convert into single-element list
- /**
- * Clear our safety timer(s)
- * @param {string} from
- */
- }, {
- key: 'clearSafetyTimer',
- value: function clearSafetyTimer(from) {
- if (!utils.is.nullOrUndefined(this.safetyTimer)) {
- this.player.debug.log('Safety timer cleared from: ' + from);
+ var urls = is.string(src) ? [src] : src; // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails
+
+ var promises = urls.map(function (u) {
+ return _this2.getThumbnail(u);
+ });
+ Promise.all(promises).then(function () {
+ // Sort smallest to biggest (e.g., [120p, 480p, 1080p])
+ _this2.thumbnails.sort(function (x, y) {
+ return x.height - y.height;
+ });
+
+ _this2.player.debug.log('Preview thumbnails', _this2.thumbnails);
+
+ resolve();
+ });
+ });
+ } // Process individual VTT file
- clearTimeout(this.safetyTimer);
- this.safetyTimer = null;
- }
- }
- }, {
- key: 'enabled',
- get: function get$$1() {
- return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
- }
}, {
- key: 'tagUrl',
- get: function get$$1() {
- var params = {
- AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
- AV_CHANNELID: '5a0458dc28a06145e4519d21',
- AV_URL: location.hostname,
- cb: Date.now(),
- AV_WIDTH: 640,
- AV_HEIGHT: 480,
- AV_CDIM2: this.publisherId
- };
+ key: "getThumbnail",
+ value: function getThumbnail(url) {
+ var _this3 = this;
- var base = 'https://go.aniview.com/api/adserver6/vast/';
+ return new Promise(function (resolve) {
+ fetch(url).then(function (response) {
+ var thumbnail = {
+ frames: parseVtt(response),
+ height: null,
+ urlPrefix: ''
+ }; // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file
+ // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank
- return base + '?' + utils.buildUrlParams(params);
- }
- }]);
- return Ads;
-}();
+ if (!thumbnail.frames[0].text.startsWith('/')) {
+ thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);
+ } // Download the first frame, so that we can determine/set the height of this thumbnailsDef
-// ==========================================================================
-var source = {
- // Add elements to HTML5 media (source, tracks, etc)
- insertElements: function insertElements(type, attributes) {
- var _this = this;
+ var tempImage = new Image();
- if (utils.is.string(attributes)) {
- utils.insertElement(type, this.media, {
- src: attributes
- });
- } else if (utils.is.array(attributes)) {
- attributes.forEach(function (attribute) {
- utils.insertElement(type, _this.media, attribute);
- });
- }
- },
+ tempImage.onload = function () {
+ thumbnail.height = tempImage.naturalHeight;
+ thumbnail.width = tempImage.naturalWidth;
+ _this3.thumbnails.push(thumbnail);
- // Update source
- // Sources are not checked for support so be careful
- change: function change(input) {
- var _this2 = this;
+ resolve();
+ };
- if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) {
- this.debug.warn('Invalid source format');
- return;
+ tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;
+ });
+ });
+ }
+ }, {
+ key: "startMove",
+ value: function startMove(event) {
+ if (!this.loaded) {
+ return;
}
- // Cancel current network requests
- html5.cancelRequests.call(this);
+ if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) {
+ return;
+ } // Wait until media has a duration
- // Destroy instance and re-setup
- this.destroy.call(this, function () {
- // Reset quality options
- _this2.options.quality = [];
- // Remove elements
- utils.removeElement(_this2.media);
- _this2.media = null;
+ if (!this.player.media.duration) {
+ return;
+ }
- // Reset class name
- if (utils.is.element(_this2.elements.container)) {
- _this2.elements.container.removeAttribute('class');
- }
+ if (event.type === 'touchmove') {
+ // Calculate seek hover position as approx video seconds
+ this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);
+ } else {
+ // Calculate seek hover position as approx video seconds
+ var clientRect = this.player.elements.progress.getBoundingClientRect();
+ var percentage = 100 / clientRect.width * (event.pageX - clientRect.left);
+ this.seekTime = this.player.media.duration * (percentage / 100);
- // Set the type and provider
- _this2.type = input.type;
- _this2.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5;
+ if (this.seekTime < 0) {
+ // The mousemove fires for 10+px out to the left
+ this.seekTime = 0;
+ }
- // Check for support
- _this2.supported = support.check(_this2.type, _this2.provider, _this2.config.playsinline);
+ if (this.seekTime > this.player.media.duration - 1) {
+ // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video
+ this.seekTime = this.player.media.duration - 1;
+ }
- // Create new markup
- switch (_this2.provider + ':' + _this2.type) {
- case 'html5:video':
- _this2.media = utils.createElement('video');
- break;
+ this.mousePosX = event.pageX; // Set time text inside image container
- case 'html5:audio':
- _this2.media = utils.createElement('audio');
- break;
+ this.elements.thumb.time.innerText = formatTime(this.seekTime);
+ } // Download and show image
- case 'youtube:video':
- case 'vimeo:video':
- _this2.media = utils.createElement('div', {
- src: input.sources[0].src
- });
- break;
- default:
- break;
- }
+ this.showImageAtCurrentTime();
+ }
+ }, {
+ key: "endMove",
+ value: function endMove() {
+ this.toggleThumbContainer(false, true);
+ }
+ }, {
+ key: "startScrubbing",
+ value: function startScrubbing(event) {
+ // Only act on left mouse button (0), or touch device (event.button is false)
+ if (event.button === false || event.button === 0) {
+ this.mouseDown = true; // Wait until media has a duration
- // Inject the new element
- _this2.elements.container.appendChild(_this2.media);
+ if (this.player.media.duration) {
+ this.toggleScrubbingContainer(true);
+ this.toggleThumbContainer(false, true); // Download and show image
- // Autoplay the new source?
- if (utils.is.boolean(input.autoplay)) {
- _this2.config.autoplay = input.autoplay;
- }
+ this.showImageAtCurrentTime();
+ }
+ }
+ }
+ }, {
+ key: "finishScrubbing",
+ value: function finishScrubbing() {
+ var _this4 = this;
- // Set attributes for audio and video
- if (_this2.isHTML5) {
- if (_this2.config.crossorigin) {
- _this2.media.setAttribute('crossorigin', '');
- }
- if (_this2.config.autoplay) {
- _this2.media.setAttribute('autoplay', '');
- }
- if (!utils.is.empty(input.poster)) {
- _this2.poster = input.poster;
- }
- if (_this2.config.loop.active) {
- _this2.media.setAttribute('loop', '');
- }
- if (_this2.config.muted) {
- _this2.media.setAttribute('muted', '');
- }
- if (_this2.config.playsinline) {
- _this2.media.setAttribute('playsinline', '');
- }
+ this.mouseDown = false; // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview
+
+ if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {
+ // The video was already seeked/loaded at the chosen time - hide immediately
+ this.toggleScrubbingContainer(false);
+ } else {
+ // The video hasn't seeked yet. Wait for that
+ once.call(this.player, this.player.media, 'timeupdate', function () {
+ // Re-check mousedown - we might have already started scrubbing again
+ if (!_this4.mouseDown) {
+ _this4.toggleScrubbingContainer(false);
}
+ });
+ }
+ }
+ /**
+ * Setup hooks for Plyr and window events
+ */
- // Restore class hook
- ui.addStyleHook.call(_this2);
+ }, {
+ key: "listeners",
+ value: function listeners() {
+ var _this5 = this;
- // Set new sources for html5
- if (_this2.isHTML5) {
- source.insertElements.call(_this2, 'source', input.sources);
- }
+ // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering
+ this.player.on('play', function () {
+ _this5.toggleThumbContainer(false, true);
+ });
+ this.player.on('seeked', function () {
+ _this5.toggleThumbContainer(false);
+ });
+ this.player.on('timeupdate', function () {
+ _this5.lastTime = _this5.player.media.currentTime;
+ });
+ }
+ /**
+ * Create HTML elements for image containers
+ */
- // Set video title
- _this2.config.title = input.title;
+ }, {
+ key: "render",
+ value: function render() {
+ // Create HTML element: plyr__preview-thumbnail-container
+ this.elements.thumb.container = createElement('div', {
+ class: this.player.config.classNames.previewThumbnails.thumbContainer
+ }); // Wrapper for the image for styling
+
+ this.elements.thumb.imageContainer = createElement('div', {
+ class: this.player.config.classNames.previewThumbnails.imageContainer
+ });
+ this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer); // Create HTML element, parent+span: time text (e.g., 01:32:00)
- // Set up from scratch
- media.setup.call(_this2);
+ var timeContainer = createElement('div', {
+ class: this.player.config.classNames.previewThumbnails.timeContainer
+ });
+ this.elements.thumb.time = createElement('span', {}, '00:00');
+ timeContainer.appendChild(this.elements.thumb.time);
+ this.elements.thumb.container.appendChild(timeContainer); // Inject the whole thumb
- // HTML5 stuff
- if (_this2.isHTML5) {
- // Setup captions
- if ('tracks' in input) {
- source.insertElements.call(_this2, 'track', input.tracks);
- }
+ this.player.elements.progress.appendChild(this.elements.thumb.container); // Create HTML element: plyr__preview-scrubbing-container
- // Load HTML5 sources
- _this2.media.load();
- }
+ this.elements.scrubbing.container = createElement('div', {
+ class: this.player.config.classNames.previewThumbnails.scrubbingContainer
+ });
+ this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);
+ }
+ }, {
+ key: "showImageAtCurrentTime",
+ value: function showImageAtCurrentTime() {
+ var _this6 = this;
- // If HTML5 or embed but not fully supported, setupInterface and call ready now
- if (_this2.isHTML5 || _this2.isEmbed && !_this2.supported.ui) {
- // Setup interface
- ui.build.call(_this2);
- }
+ if (this.mouseDown) {
+ this.setScrubbingContainerSize();
+ } else {
+ this.toggleThumbContainer(true);
+ this.setThumbContainerSizeAndPos();
+ } // Find the desired thumbnail index
+ // TODO: Handle a video longer than the thumbs where thumbNum is null
- // Update the fullscreen support
- _this2.fullscreen.update();
- }, true);
- }
-};
-// ==========================================================================
+ var thumbNum = this.thumbnails[0].frames.findIndex(function (frame) {
+ return _this6.seekTime >= frame.startTime && _this6.seekTime <= frame.endTime;
+ });
+ var hasThumb = thumbNum >= 0;
+ var qualityIndex = 0;
+ this.toggleThumbContainer(hasThumb); // No matching thumb found
-var Storage = function () {
- function Storage(player) {
- classCallCheck(this, Storage);
+ if (!hasThumb) {
+ return;
+ } // Check to see if we've already downloaded higher quality versions of this image
- this.enabled = player.config.storage.enabled;
- this.key = player.config.storage.key;
- }
- // Check for actual support (see if we can use it)
+ this.thumbnails.forEach(function (thumbnail, index) {
+ if (_this6.loadedImages.includes(thumbnail.frames[thumbNum].text)) {
+ qualityIndex = index;
+ }
+ }); // Only proceed if either thumbnum or thumbfilename has changed
+ if (thumbNum !== this.showingThumb) {
+ this.showingThumb = thumbNum;
+ this.loadImage(qualityIndex);
+ }
+ } // Show the image that's currently specified in this.showingThumb
- createClass(Storage, [{
- key: 'get',
- value: function get$$1(key) {
- if (!Storage.supported) {
- return null;
- }
+ }, {
+ key: "loadImage",
+ value: function loadImage() {
+ var _this7 = this;
+
+ var qualityIndex = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ var thumbNum = this.showingThumb;
+ var thumbnail = this.thumbnails[qualityIndex];
+ var urlPrefix = thumbnail.urlPrefix;
+ var frame = thumbnail.frames[thumbNum];
+ var thumbFilename = thumbnail.frames[thumbNum].text;
+ var thumbUrl = urlPrefix + thumbFilename;
+
+ if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {
+ // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one
+ // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort
+ if (this.loadingImage && this.usingSprites) {
+ this.loadingImage.onload = null;
+ } // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image
+ // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background
+ // images causes a flicker. Putting a new image over the top does not
+
+
+ var previewImage = new Image();
+ previewImage.src = thumbUrl;
+ previewImage.dataset.index = thumbNum;
+ previewImage.dataset.filename = thumbFilename;
+ this.showingThumbFilename = thumbFilename;
+ this.player.debug.log("Loading image: ".concat(thumbUrl)); // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...
+
+ previewImage.onload = function () {
+ return _this7.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);
+ };
+
+ this.loadingImage = previewImage;
+ this.removeOldImages(previewImage);
+ } else {
+ // Update the existing image
+ this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);
+ this.currentImageElement.dataset.index = thumbNum;
+ this.removeOldImages(this.currentImageElement);
+ }
+ }
+ }, {
+ key: "showImage",
+ value: function showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename) {
+ var newImage = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;
+ this.player.debug.log("Showing thumb: ".concat(thumbFilename, ". num: ").concat(thumbNum, ". qual: ").concat(qualityIndex, ". newimg: ").concat(newImage));
+ this.setImageSizeAndOffset(previewImage, frame);
- var store = window.localStorage.getItem(this.key);
+ if (newImage) {
+ this.currentImageContainer.appendChild(previewImage);
+ this.currentImageElement = previewImage;
- if (utils.is.empty(store)) {
- return null;
- }
+ if (!this.loadedImages.includes(thumbFilename)) {
+ this.loadedImages.push(thumbFilename);
+ }
+ } // Preload images before and after the current one
+ // Show higher quality of the same frame
+ // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading
- var json = JSON.parse(store);
- return utils.is.string(key) && key.length ? json[key] : json;
- }
+ this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));
+ } // Remove all preview images that aren't the designated current image
+
}, {
- key: 'set',
- value: function set$$1(object) {
- // Bail if we don't have localStorage support or it's disabled
- if (!Storage.supported || !this.enabled) {
- return;
- }
+ key: "removeOldImages",
+ value: function removeOldImages(currentImage) {
+ var _this8 = this;
- // Can only store objectst
- if (!utils.is.object(object)) {
- return;
- }
+ // Get a list of all images, convert it from a DOM list to an array
+ Array.from(this.currentImageContainer.children).forEach(function (image) {
+ if (image.tagName.toLowerCase() !== 'img') {
+ return;
+ }
- // Get current storage
- var storage = this.get();
+ var removeDelay = _this8.usingSprites ? 500 : 1000;
- // Default to empty object
- if (utils.is.empty(storage)) {
- storage = {};
- }
+ if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {
+ // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients
+ // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function
+ image.dataset.deleting = true; // This has to be set before the timeout - to prevent issues switching between hover and scrub
- // Update the working copy of the values
- utils.extend(storage, object);
+ var currentImageContainer = _this8.currentImageContainer;
+ setTimeout(function () {
+ currentImageContainer.removeChild(image);
- // Update storage
- window.localStorage.setItem(this.key, JSON.stringify(storage));
- }
- }], [{
- key: 'supported',
- get: function get$$1() {
- try {
- if (!('localStorage' in window)) {
- return false;
- }
+ _this8.player.debug.log("Removing thumb: ".concat(image.dataset.filename));
+ }, removeDelay);
+ }
+ });
+ } // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame
+ // This will only preload the lowest quality
- var test = '___test';
+ }, {
+ key: "preloadNearby",
+ value: function preloadNearby(thumbNum) {
+ var _this9 = this;
- // Try to use it (it might be disabled, e.g. user is in private mode)
- // see: https://github.com/sampotts/plyr/issues/131
- window.localStorage.setItem(test, test);
- window.localStorage.removeItem(test);
+ var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+ return new Promise(function (resolve) {
+ setTimeout(function () {
+ var oldThumbFilename = _this9.thumbnails[0].frames[thumbNum].text;
- return true;
- } catch (e) {
- return false;
- }
- }
- }]);
- return Storage;
-}();
+ if (_this9.showingThumbFilename === oldThumbFilename) {
+ // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away
+ var thumbnailsClone;
-// ==========================================================================
+ if (forward) {
+ thumbnailsClone = _this9.thumbnails[0].frames.slice(thumbNum);
+ } else {
+ thumbnailsClone = _this9.thumbnails[0].frames.slice(0, thumbNum).reverse();
+ }
-// Private properties
-// TODO: Use a WeakMap for private globals
-// const globals = new WeakMap();
+ var foundOne = false;
+ thumbnailsClone.forEach(function (frame) {
+ var newThumbFilename = frame.text;
-// Plyr instance
+ if (newThumbFilename !== oldThumbFilename) {
+ // Found one with a different filename. Make sure it hasn't already been loaded on this page visit
+ if (!_this9.loadedImages.includes(newThumbFilename)) {
+ foundOne = true;
-var Plyr = function () {
- function Plyr(target, options) {
- var _this = this;
+ _this9.player.debug.log("Preloading thumb filename: ".concat(newThumbFilename));
- classCallCheck(this, Plyr);
+ var urlPrefix = _this9.thumbnails[0].urlPrefix;
+ var thumbURL = urlPrefix + newThumbFilename;
+ var previewImage = new Image();
+ previewImage.src = thumbURL;
- this.timers = {};
+ previewImage.onload = function () {
+ _this9.player.debug.log("Preloaded thumb filename: ".concat(newThumbFilename));
- // State
- this.ready = false;
- this.loading = false;
- this.failed = false;
+ if (!_this9.loadedImages.includes(newThumbFilename)) _this9.loadedImages.push(newThumbFilename); // We don't resolve until the thumb is loaded
- // Touch device
- this.touch = support.touch;
+ resolve();
+ };
+ }
+ }
+ }); // If there are none to preload then we want to resolve immediately
- // Set the media element
- this.media = target;
+ if (!foundOne) {
+ resolve();
+ }
+ }
+ }, 300);
+ });
+ } // If user has been hovering current image for half a second, look for a higher quality one
- // String selector passed
- if (utils.is.string(this.media)) {
- this.media = document.querySelectorAll(this.media);
+ }, {
+ key: "getHigherQuality",
+ value: function getHigherQuality(currentQualityIndex, previewImage, frame, thumbFilename) {
+ var _this10 = this;
+
+ if (currentQualityIndex < this.thumbnails.length - 1) {
+ // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container
+ var previewImageHeight = previewImage.naturalHeight;
+
+ if (this.usingSprites) {
+ previewImageHeight = frame.h;
+ }
+
+ if (previewImageHeight < this.thumbContainerHeight) {
+ // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while
+ setTimeout(function () {
+ // Make sure the mouse hasn't already moved on and started hovering at another image
+ if (_this10.showingThumbFilename === thumbFilename) {
+ _this10.player.debug.log("Showing higher quality thumb for: ".concat(thumbFilename));
+
+ _this10.loadImage(currentQualityIndex + 1);
+ }
+ }, 300);
+ }
}
+ }
+ }, {
+ key: "toggleThumbContainer",
+ value: function toggleThumbContainer() {
+ var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var clearShowing = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ var className = this.player.config.classNames.previewThumbnails.thumbContainerShown;
+ this.elements.thumb.container.classList.toggle(className, toggle);
- // jQuery, NodeList or Array passed, use first element
- if (window.jQuery && this.media instanceof jQuery || utils.is.nodeList(this.media) || utils.is.array(this.media)) {
- // eslint-disable-next-line
- this.media = this.media[0];
+ if (!toggle && clearShowing) {
+ this.showingThumb = null;
+ this.showingThumbFilename = null;
}
+ }
+ }, {
+ key: "toggleScrubbingContainer",
+ value: function toggleScrubbingContainer() {
+ var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ var className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;
+ this.elements.scrubbing.container.classList.toggle(className, toggle);
- // Set config
- this.config = utils.extend({}, defaults$1, options || {}, function () {
- try {
- return JSON.parse(_this.media.getAttribute('data-plyr-config'));
- } catch (e) {
- return {};
- }
- }());
-
- // Elements cache
- this.elements = {
- container: null,
- buttons: {},
- display: {},
- progress: {},
- inputs: {},
- settings: {
- menu: null,
- panes: {},
- tabs: {}
- },
- captions: null
- };
+ if (!toggle) {
+ this.showingThumb = null;
+ this.showingThumbFilename = null;
+ }
+ }
+ }, {
+ key: "determineContainerAutoSizing",
+ value: function determineContainerAutoSizing() {
+ if (this.elements.thumb.imageContainer.clientHeight > 20) {
+ // This will prevent auto sizing in this.setThumbContainerSizeAndPos()
+ this.sizeSpecifiedInCSS = true;
+ }
+ } // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS
- // Captions
- this.captions = {
- active: null,
- currentTrack: null
- };
+ }, {
+ key: "setThumbContainerSizeAndPos",
+ value: function setThumbContainerSizeAndPos() {
+ if (!this.sizeSpecifiedInCSS) {
+ var thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);
+ this.elements.thumb.imageContainer.style.height = "".concat(this.thumbContainerHeight, "px");
+ this.elements.thumb.imageContainer.style.width = "".concat(thumbWidth, "px");
+ }
- // Fullscreen
- this.fullscreen = {
- active: false
- };
+ this.setThumbContainerPos();
+ }
+ }, {
+ key: "setThumbContainerPos",
+ value: function setThumbContainerPos() {
+ var seekbarRect = this.player.elements.progress.getBoundingClientRect();
+ var plyrRect = this.player.elements.container.getBoundingClientRect();
+ var container = this.elements.thumb.container; // Find the lowest and highest desired left-position, so we don't slide out the side of the video container
- // Options
- this.options = {
- speed: [],
- quality: [],
- captions: []
- };
+ var minVal = plyrRect.left - seekbarRect.left + 10;
+ var maxVal = plyrRect.right - seekbarRect.left - container.clientWidth - 10; // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth
- // Debugging
- // TODO: move to globals
- this.debug = new Console(this.config.debug);
+ var previewPos = this.mousePosX - seekbarRect.left - container.clientWidth / 2;
- // Log config options and support
- this.debug.log('Config', this.config);
- this.debug.log('Support', support);
+ if (previewPos < minVal) {
+ previewPos = minVal;
+ }
- // We need an element to setup
- if (utils.is.nullOrUndefined(this.media) || !utils.is.element(this.media)) {
- this.debug.error('Setup failed: no suitable element passed');
- return;
+ if (previewPos > maxVal) {
+ previewPos = maxVal;
}
- // Bail if the element is initialized
- if (this.media.plyr) {
- this.debug.warn('Target already setup');
- return;
+ container.style.left = "".concat(previewPos, "px");
+ } // Can't use 100% width, in case the video is a different aspect ratio to the video container
+
+ }, {
+ key: "setScrubbingContainerSize",
+ value: function setScrubbingContainerSize() {
+ this.elements.scrubbing.container.style.width = "".concat(this.player.media.clientWidth, "px"); // Can't use media.clientHeight - html5 video goes big and does black bars above and below
+
+ this.elements.scrubbing.container.style.height = "".concat(this.player.media.clientWidth / this.thumbAspectRatio, "px");
+ } // Sprites need to be offset to the correct location
+
+ }, {
+ key: "setImageSizeAndOffset",
+ value: function setImageSizeAndOffset(previewImage, frame) {
+ if (!this.usingSprites) {
+ return;
+ } // Find difference between height and preview container height
+
+
+ var multiplier = this.thumbContainerHeight / frame.h;
+ previewImage.style.height = "".concat(Math.floor(previewImage.naturalHeight * multiplier), "px");
+ previewImage.style.width = "".concat(Math.floor(previewImage.naturalWidth * multiplier), "px");
+ previewImage.style.left = "-".concat(frame.x * multiplier, "px");
+ previewImage.style.top = "-".concat(frame.y * multiplier, "px");
+ }
+ }, {
+ key: "enabled",
+ get: function get() {
+ return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;
+ }
+ }, {
+ key: "currentImageContainer",
+ get: function get() {
+ if (this.mouseDown) {
+ return this.elements.scrubbing.container;
}
- // Bail if not enabled
- if (!this.config.enabled) {
- this.debug.error('Setup failed: disabled by config');
- return;
+ return this.elements.thumb.imageContainer;
+ }
+ }, {
+ key: "usingSprites",
+ get: function get() {
+ return Object.keys(this.thumbnails[0].frames[0]).includes('w');
+ }
+ }, {
+ key: "thumbAspectRatio",
+ get: function get() {
+ if (this.usingSprites) {
+ return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;
}
- // Bail if disabled or no basic support
- // You may want to disable certain UAs etc
- if (!support.check().api) {
- this.debug.error('Setup failed: no support');
- return;
+ return this.thumbnails[0].width / this.thumbnails[0].height;
+ }
+ }, {
+ key: "thumbContainerHeight",
+ get: function get() {
+ if (this.mouseDown) {
+ // Can't use media.clientHeight - HTML5 video goes big and does black bars above and below
+ return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio);
}
- // Cache original element state for .destroy()
- var clone = this.media.cloneNode(true);
- clone.autoplay = false;
- this.elements.original = clone;
+ return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);
+ }
+ }, {
+ key: "currentImageElement",
+ get: function get() {
+ if (this.mouseDown) {
+ return this.currentScrubbingImageElement;
+ }
- // Set media type based on tag or data attribute
- // Supported: video, audio, vimeo, youtube
- var type = this.media.tagName.toLowerCase();
+ return this.currentThumbnailImageElement;
+ },
+ set: function set(element) {
+ if (this.mouseDown) {
+ this.currentScrubbingImageElement = element;
+ } else {
+ this.currentThumbnailImageElement = element;
+ }
+ }
+ }]);
- // Embed properties
- var iframe = null;
- var url = null;
- var params = null;
+ return PreviewThumbnails;
+ }();
- // Different setup based on type
- switch (type) {
- case 'div':
- // Find the frame
- iframe = this.media.querySelector('iframe');
-
- // <iframe> type
- if (utils.is.element(iframe)) {
- // Detect provider
- url = iframe.getAttribute('src');
- this.provider = utils.getProviderByUrl(url);
-
- // Rework elements
- this.elements.container = this.media;
- this.media = iframe;
-
- // Reset classname
- this.elements.container.className = '';
-
- // Get attributes from URL and set config
- params = utils.getUrlParams(url);
- if (!utils.is.empty(params)) {
- var truthy = ['1', 'true'];
-
- if (truthy.includes(params.autoplay)) {
- this.config.autoplay = true;
- }
- if (truthy.includes(params.loop)) {
- this.config.loop.active = true;
- }
-
- // TODO: replace fullscreen.iosNative with this playsinline config option
- // YouTube requires the playsinline in the URL
- if (this.isYouTube) {
- this.config.playsinline = truthy.includes(params.playsinline);
- } else {
- this.config.playsinline = true;
- }
- }
- } else {
- // <div> with attributes
- this.provider = this.media.getAttribute(this.config.attributes.embed.provider);
+ var source = {
+ // Add elements to HTML5 media (source, tracks, etc)
+ insertElements: function insertElements(type, attributes) {
+ var _this = this;
- // Remove attribute
- this.media.removeAttribute(this.config.attributes.embed.provider);
- }
+ if (is.string(attributes)) {
+ insertElement(type, this.media, {
+ src: attributes
+ });
+ } else if (is.array(attributes)) {
+ attributes.forEach(function (attribute) {
+ insertElement(type, _this.media, attribute);
+ });
+ }
+ },
+ // Update source
+ // Sources are not checked for support so be careful
+ change: function change(input) {
+ var _this2 = this;
- // Unsupported or missing provider
- if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
- this.debug.error('Setup failed: Invalid provider');
- return;
- }
+ if (!getDeep(input, 'sources.length')) {
+ this.debug.warn('Invalid source format');
+ return;
+ } // Cancel current network requests
- // Audio will come later for external providers
- this.type = types.video;
- break;
+ html5.cancelRequests.call(this); // Destroy instance and re-setup
- case 'video':
- case 'audio':
- this.type = type;
- this.provider = providers.html5;
+ this.destroy.call(this, function () {
+ // Reset quality options
+ _this2.options.quality = []; // Remove elements
- // Get config from attributes
- if (this.media.hasAttribute('crossorigin')) {
- this.config.crossorigin = true;
- }
- if (this.media.hasAttribute('autoplay')) {
- this.config.autoplay = true;
- }
- if (this.media.hasAttribute('playsinline')) {
- this.config.playsinline = true;
- }
- if (this.media.hasAttribute('muted')) {
- this.config.muted = true;
- }
- if (this.media.hasAttribute('loop')) {
- this.config.loop.active = true;
- }
+ removeElement(_this2.media);
+ _this2.media = null; // Reset class name
- break;
+ if (is.element(_this2.elements.container)) {
+ _this2.elements.container.removeAttribute('class');
+ } // Set the type and provider
- default:
- this.debug.error('Setup failed: unsupported type');
- return;
- }
- // Check for support again but with type
- this.supported = support.check(this.type, this.provider, this.config.playsinline);
+ var sources = input.sources,
+ type = input.type;
- // If no support for even API, bail
- if (!this.supported.api) {
- this.debug.error('Setup failed: no support');
- return;
- }
+ var _sources = _slicedToArray(sources, 1),
+ _sources$ = _sources[0],
+ _sources$$provider = _sources$.provider,
+ provider = _sources$$provider === void 0 ? providers.html5 : _sources$$provider,
+ src = _sources$.src;
- // Create listeners
- this.listeners = new Listeners(this);
+ var tagName = provider === 'html5' ? type : 'div';
+ var attributes = provider === 'html5' ? {} : {
+ src: src
+ };
+ Object.assign(_this2, {
+ provider: provider,
+ type: type,
+ // Check for support
+ supported: support.check(type, provider, _this2.config.playsinline),
+ // Create new element
+ media: createElement(tagName, attributes)
+ }); // Inject the new element
- // Setup local storage for user settings
- this.storage = new Storage(this);
+ _this2.elements.container.appendChild(_this2.media); // Autoplay the new source?
- // Store reference
- this.media.plyr = this;
- // Wrap media
- if (!utils.is.element(this.elements.container)) {
- this.elements.container = utils.createElement('div');
- utils.wrap(this.media, this.elements.container);
- }
+ if (is.boolean(input.autoplay)) {
+ _this2.config.autoplay = input.autoplay;
+ } // Set attributes for audio and video
- // Allow focus to be captured
- this.elements.container.setAttribute('tabindex', 0);
- // Add style hook
- ui.addStyleHook.call(this);
+ if (_this2.isHTML5) {
+ if (_this2.config.crossorigin) {
+ _this2.media.setAttribute('crossorigin', '');
+ }
- // Setup media
- media.setup.call(this);
+ if (_this2.config.autoplay) {
+ _this2.media.setAttribute('autoplay', '');
+ }
- // Listen for events if debugging
- if (this.config.debug) {
- utils.on(this.elements.container, this.config.events.join(' '), function (event) {
- _this.debug.log('event: ' + event.type);
- });
- }
+ if (!is.empty(input.poster)) {
+ _this2.poster = input.poster;
+ }
- // Setup interface
- // If embed but not fully supported, build interface now to avoid flash of controls
- if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
- ui.build.call(this);
- }
+ if (_this2.config.loop.active) {
+ _this2.media.setAttribute('loop', '');
+ }
- // Container listeners
- this.listeners.container();
+ if (_this2.config.muted) {
+ _this2.media.setAttribute('muted', '');
+ }
- // Global listeners
- this.listeners.global();
+ if (_this2.config.playsinline) {
+ _this2.media.setAttribute('playsinline', '');
+ }
+ } // Restore class hook
- // Setup fullscreen
- this.fullscreen = new Fullscreen(this);
- // Setup ads if provided
- this.ads = new Ads(this);
+ ui.addStyleHook.call(_this2); // Set new sources for html5
- // Autoplay if required
- if (this.config.autoplay) {
- this.play();
- }
- }
+ if (_this2.isHTML5) {
+ source.insertElements.call(_this2, 'source', sources);
+ } // Set video title
- // ---------------------------------------
- // API
- // ---------------------------------------
- /**
- * Types and provider helpers
- */
+ _this2.config.title = input.title; // Set up from scratch
+ media.setup.call(_this2); // HTML5 stuff
- createClass(Plyr, [{
- key: 'play',
+ if (_this2.isHTML5) {
+ // Setup captions
+ if (Object.keys(input).includes('tracks')) {
+ source.insertElements.call(_this2, 'track', input.tracks);
+ }
+ } // If HTML5 or embed but not fully supported, setupInterface and call ready now
- /**
- * Play the media, or play the advertisement (if they are not blocked)
- */
- value: function play() {
- if (!utils.is.function(this.media.play)) {
- return null;
- }
+ if (_this2.isHTML5 || _this2.isEmbed && !_this2.supported.ui) {
+ // Setup interface
+ ui.build.call(_this2);
+ } // Load HTML5 sources
- // Return the promise (for HTML5)
- return this.media.play();
- }
- /**
- * Pause the media
- */
+ if (_this2.isHTML5) {
+ _this2.media.load();
+ } // Reload thumbnails
- }, {
- key: 'pause',
- value: function pause() {
- if (!this.playing || !utils.is.function(this.media.pause)) {
- return;
- }
- this.media.pause();
- }
+ if (_this2.previewThumbnails) {
+ _this2.previewThumbnails.load();
+ } // Update the fullscreen support
- /**
- * Get playing state
- */
- }, {
- key: 'togglePlay',
+ _this2.fullscreen.update();
+ }, true);
+ }
+ };
+ // TODO: Use a WeakMap for private globals
+ // const globals = new WeakMap();
+ // Plyr instance
- /**
- * Toggle playback based on current status
- * @param {boolean} input
- */
- value: function togglePlay(input) {
- // Toggle based on current state if nothing passed
- var toggle = utils.is.boolean(input) ? input : !this.playing;
+ var Plyr =
+ /*#__PURE__*/
+ function () {
+ function Plyr(target, options) {
+ var _this = this;
- if (toggle) {
- this.play();
- } else {
- this.pause();
- }
- }
+ _classCallCheck(this, Plyr);
- /**
- * Stop playback
- */
+ this.timers = {}; // State
- }, {
- key: 'stop',
- value: function stop() {
- if (this.isHTML5) {
- this.pause();
- this.restart();
- } else if (utils.is.function(this.media.stop)) {
- this.media.stop();
- }
- }
+ this.ready = false;
+ this.loading = false;
+ this.failed = false; // Touch device
- /**
- * Restart playback
- */
+ this.touch = support.touch; // Set the media element
- }, {
- key: 'restart',
- value: function restart() {
- this.currentTime = 0;
- }
+ this.media = target; // String selector passed
- /**
- * Rewind
- * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
- */
+ if (is.string(this.media)) {
+ this.media = document.querySelectorAll(this.media);
+ } // jQuery, NodeList or Array passed, use first element
- }, {
- key: 'rewind',
- value: function rewind(seekTime) {
- this.currentTime = this.currentTime - (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
- }
- /**
- * Fast forward
- * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
- */
+ if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {
+ // eslint-disable-next-line
+ this.media = this.media[0];
+ } // Set config
- }, {
- key: 'forward',
- value: function forward(seekTime) {
- this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
+
+ this.config = extend({}, defaults, Plyr.defaults, options || {}, function () {
+ try {
+ return JSON.parse(_this.media.getAttribute('data-plyr-config'));
+ } catch (e) {
+ return {};
}
+ }()); // Elements cache
- /**
- * Seek to a time
- * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
- */
+ this.elements = {
+ container: null,
+ captions: null,
+ buttons: {},
+ display: {},
+ progress: {},
+ inputs: {},
+ settings: {
+ popup: null,
+ menu: null,
+ panels: {},
+ buttons: {}
+ }
+ }; // Captions
+
+ this.captions = {
+ active: null,
+ currentTrack: -1,
+ meta: new WeakMap()
+ }; // Fullscreen
+
+ this.fullscreen = {
+ active: false
+ }; // Options
- }, {
- key: 'increaseVolume',
+ this.options = {
+ speed: [],
+ quality: []
+ }; // Debugging
+ // TODO: move to globals
+ this.debug = new Console(this.config.debug); // Log config options and support
- /**
- * Increase volume
- * @param {boolean} step - How much to decrease by (between 0 and 1)
- */
- value: function increaseVolume(step) {
- var volume = this.media.muted ? 0 : this.volume;
- this.volume = volume + (utils.is.number(step) ? step : 1);
- }
+ this.debug.log('Config', this.config);
+ this.debug.log('Support', support); // We need an element to setup
- /**
- * Decrease volume
- * @param {boolean} step - How much to decrease by (between 0 and 1)
- */
+ if (is.nullOrUndefined(this.media) || !is.element(this.media)) {
+ this.debug.error('Setup failed: no suitable element passed');
+ return;
+ } // Bail if the element is initialized
- }, {
- key: 'decreaseVolume',
- value: function decreaseVolume(step) {
- var volume = this.media.muted ? 0 : this.volume;
- this.volume = volume - (utils.is.number(step) ? step : 1);
- }
- /**
- * Set muted state
- * @param {boolean} mute
- */
+ if (this.media.plyr) {
+ this.debug.warn('Target already setup');
+ return;
+ } // Bail if not enabled
- }, {
- key: 'toggleCaptions',
+ if (!this.config.enabled) {
+ this.debug.error('Setup failed: disabled by config');
+ return;
+ } // Bail if disabled or no basic support
+ // You may want to disable certain UAs etc
- /**
- * Toggle captions
- * @param {boolean} input - Whether to enable captions
- */
- value: function toggleCaptions(input) {
- // If there's no full support
- if (!this.supported.ui) {
- return;
- }
- // If the method is called without parameter, toggle based on current value
- var show = utils.is.boolean(input) ? input : !this.elements.container.classList.contains(this.config.classNames.captions.active);
+ if (!support.check().api) {
+ this.debug.error('Setup failed: no support');
+ return;
+ } // Cache original element state for .destroy()
- // Nothing to change...
- if (this.captions.active === show) {
- return;
- }
- // Set global
- this.captions.active = show;
+ var clone = this.media.cloneNode(true);
+ clone.autoplay = false;
+ this.elements.original = clone; // Set media type based on tag or data attribute
+ // Supported: video, audio, vimeo, youtube
- // Toggle state
- utils.toggleState(this.elements.buttons.captions, this.captions.active);
+ var type = this.media.tagName.toLowerCase(); // Embed properties
- // Add class hook
- utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active);
+ var iframe = null;
+ var url = null; // Different setup based on type
- // Trigger an event
- utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
- }
+ switch (type) {
+ case 'div':
+ // Find the frame
+ iframe = this.media.querySelector('iframe'); // <iframe> type
- /**
- * Set the captions language
- * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
- */
+ if (is.element(iframe)) {
+ // Detect provider
+ url = parseUrl(iframe.getAttribute('src'));
+ this.provider = getProviderByUrl(url.toString()); // Rework elements
- }, {
- key: 'airplay',
+ this.elements.container = this.media;
+ this.media = iframe; // Reset classname
+ this.elements.container.className = ''; // Get attributes from URL and set config
- /**
- * Trigger the airplay dialog
- * TODO: update player with state, support, enabled
- */
- value: function airplay() {
- // Show dialog if supported
- if (support.airplay) {
- this.media.webkitShowPlaybackTargetPicker();
- }
- }
+ if (url.search.length) {
+ var truthy = ['1', 'true'];
- /**
- * Toggle the player controls
- * @param {boolean} toggle - Whether to show the controls
- */
+ if (truthy.includes(url.searchParams.get('autoplay'))) {
+ this.config.autoplay = true;
+ }
- }, {
- key: 'toggleControls',
- value: function toggleControls(toggle) {
- var _this2 = this;
+ if (truthy.includes(url.searchParams.get('loop'))) {
+ this.config.loop.active = true;
+ } // TODO: replace fullscreen.iosNative with this playsinline config option
+ // YouTube requires the playsinline in the URL
- // We need controls of course...
- if (!utils.is.element(this.elements.controls)) {
- return;
- }
- // Don't hide if no UI support or it's audio
- if (!this.supported.ui || this.isAudio) {
- return;
+ if (this.isYouTube) {
+ this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
+ this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?
+ } else {
+ this.config.playsinline = true;
+ }
}
+ } else {
+ // <div> with attributes
+ this.provider = this.media.getAttribute(this.config.attributes.embed.provider); // Remove attribute
- var delay = 0;
- var show = toggle;
- var isEnterFullscreen = false;
+ this.media.removeAttribute(this.config.attributes.embed.provider);
+ } // Unsupported or missing provider
- // Get toggle state if not set
- if (!utils.is.boolean(toggle)) {
- if (utils.is.event(toggle)) {
- // Is the enter fullscreen event
- isEnterFullscreen = toggle.type === 'enterfullscreen';
- // Events that show the controls
- var showEvents = ['touchstart', 'touchmove', 'mouseenter', 'mousemove', 'focusin'];
+ if (is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
+ this.debug.error('Setup failed: Invalid provider');
+ return;
+ } // Audio will come later for external providers
- // Events that delay hiding
- var delayEvents = ['touchmove', 'touchend', 'mousemove'];
- // Whether to show controls
- show = showEvents.includes(toggle.type);
+ this.type = types.video;
+ break;
- // Delay hiding on move events
- if (delayEvents.includes(toggle.type)) {
- delay = 2000;
- }
+ case 'video':
+ case 'audio':
+ this.type = type;
+ this.provider = providers.html5; // Get config from attributes
- // Delay a little more for keyboard users
- if (!this.touch && toggle.type === 'focusin') {
- delay = 3000;
- utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
- }
- } else {
- show = utils.hasClass(this.elements.container, this.config.classNames.hideControls);
- }
- }
+ if (this.media.hasAttribute('crossorigin')) {
+ this.config.crossorigin = true;
+ }
- // Clear timer on every call
- clearTimeout(this.timers.controls);
+ if (this.media.hasAttribute('autoplay')) {
+ this.config.autoplay = true;
+ }
- // If the mouse is not over the controls, set a timeout to hide them
- if (show || this.paused || this.loading) {
- // Check if controls toggled
- var toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false);
+ if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
+ this.config.playsinline = true;
+ }
- // Trigger event
- if (toggled) {
- utils.dispatchEvent.call(this, this.media, 'controlsshown');
- }
+ if (this.media.hasAttribute('muted')) {
+ this.config.muted = true;
+ }
- // Always show controls when paused or if touch
- if (this.paused || this.loading) {
- return;
- }
+ if (this.media.hasAttribute('loop')) {
+ this.config.loop.active = true;
+ }
- // Delay for hiding on touch
- if (this.touch) {
- delay = 3000;
- }
- }
+ break;
- // If toggle is false or if we're playing (regardless of toggle),
- // then set the timer to hide the controls
- if (!show || this.playing) {
- this.timers.controls = setTimeout(function () {
- // We need controls of course...
- if (!utils.is.element(_this2.elements.controls)) {
- return;
- }
-
- // If the mouse is over the controls (and not entering fullscreen), bail
- if ((_this2.elements.controls.pressed || _this2.elements.controls.hover) && !isEnterFullscreen) {
- return;
- }
-
- // Restore transition behaviour
- if (!utils.hasClass(_this2.elements.container, _this2.config.classNames.hideControls)) {
- utils.toggleClass(_this2.elements.controls, _this2.config.classNames.noTransition, false);
- }
-
- // Set hideControls class
- var toggled = utils.toggleClass(_this2.elements.container, _this2.config.classNames.hideControls, _this2.config.hideControls);
-
- // Trigger event and close menu
- if (toggled) {
- utils.dispatchEvent.call(_this2, _this2.media, 'controlshidden');
-
- if (_this2.config.controls.includes('settings') && !utils.is.empty(_this2.config.settings)) {
- controls.toggleMenu.call(_this2, false);
- }
- }
- }, delay);
- }
- }
+ default:
+ this.debug.error('Setup failed: unsupported type');
+ return;
+ } // Check for support again but with type
- /**
- * Add event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
- */
- }, {
- key: 'on',
- value: function on(event, callback) {
- utils.on(this.elements.container, event, callback);
- }
+ this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail
- /**
- * Remove event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
- */
+ if (!this.supported.api) {
+ this.debug.error('Setup failed: no support');
+ return;
+ }
- }, {
- key: 'off',
- value: function off(event, callback) {
- utils.off(this.elements.container, event, callback);
- }
+ this.eventListeners = []; // Create listeners
- /**
- * Destroy an instance
- * Event listeners are removed when elements are removed
- * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
- * @param {function} callback - Callback for when destroy is complete
- * @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
- */
+ this.listeners = new Listeners(this); // Setup local storage for user settings
- }, {
- key: 'destroy',
- value: function destroy(callback) {
- var _this3 = this;
+ this.storage = new Storage(this); // Store reference
- var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ this.media.plyr = this; // Wrap media
- if (!this.ready) {
- return;
- }
+ if (!is.element(this.elements.container)) {
+ this.elements.container = createElement('div', {
+ tabindex: 0
+ });
+ wrap(this.media, this.elements.container);
+ } // Add style hook
- var done = function done() {
- // Reset overflow (incase destroyed while in fullscreen)
- document.body.style.overflow = '';
-
- // GC for embed
- _this3.embed = null;
-
- // If it's a soft destroy, make minimal changes
- if (soft) {
- if (Object.keys(_this3.elements).length) {
- // Remove elements
- utils.removeElement(_this3.elements.buttons.play);
- utils.removeElement(_this3.elements.captions);
- utils.removeElement(_this3.elements.controls);
- utils.removeElement(_this3.elements.wrapper);
-
- // Clear for GC
- _this3.elements.buttons.play = null;
- _this3.elements.captions = null;
- _this3.elements.controls = null;
- _this3.elements.wrapper = null;
- }
-
- // Callback
- if (utils.is.function(callback)) {
- callback();
- }
- } else {
- // Unbind listeners
- _this3.listeners.clear();
- // Replace the container with the original element provided
- utils.replaceElement(_this3.elements.original, _this3.elements.container);
+ ui.addStyleHook.call(this); // Setup media
- // Event
- utils.dispatchEvent.call(_this3, _this3.elements.original, 'destroyed', true);
+ media.setup.call(this); // Listen for events if debugging
- // Callback
- if (utils.is.function(callback)) {
- callback.call(_this3.elements.original);
- }
+ if (this.config.debug) {
+ on.call(this, this.elements.container, this.config.events.join(' '), function (event) {
+ _this.debug.log("event: ".concat(event.type));
+ });
+ } // Setup interface
+ // If embed but not fully supported, build interface now to avoid flash of controls
- // Reset state
- _this3.ready = false;
- // Clear for garbage collection
- setTimeout(function () {
- _this3.elements = null;
- _this3.media = null;
- }, 200);
- }
- };
+ if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
+ ui.build.call(this);
+ } // Container listeners
- // Stop playback
- this.stop();
- // Type specific stuff
- switch (this.provider + ':' + this.type) {
- case 'html5:video':
- case 'html5:audio':
- // Clear timeout
- clearTimeout(this.timers.loading);
+ this.listeners.container(); // Global listeners
- // Restore native video controls
- ui.toggleNativeControls.call(this, true);
+ this.listeners.global(); // Setup fullscreen
- // Clean up
- done();
+ this.fullscreen = new Fullscreen(this); // Setup ads if provided
- break;
+ if (this.config.ads.enabled) {
+ this.ads = new Ads(this);
+ } // Autoplay if required
- case 'youtube:video':
- // Clear timers
- clearInterval(this.timers.buffering);
- clearInterval(this.timers.playing);
- // Destroy YouTube API
- if (this.embed !== null && utils.is.function(this.embed.destroy)) {
- this.embed.destroy();
- }
+ if (this.config.autoplay) {
+ this.play();
+ } // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
- // Clean up
- done();
- break;
+ this.lastSeekTime = 0; // Setup preview thumbnails if enabled
- case 'vimeo:video':
- // Destroy Vimeo API
- // then clean up (wait, to prevent postmessage errors)
- if (this.embed !== null) {
- this.embed.unload().then(done);
- }
+ if (this.config.previewThumbnails.enabled) {
+ this.previewThumbnails = new PreviewThumbnails(this);
+ }
+ } // ---------------------------------------
+ // API
+ // ---------------------------------------
- // Vimeo does not always return
- setTimeout(done, 200);
+ /**
+ * Types and provider helpers
+ */
- break;
- default:
- break;
- }
- }
+ _createClass(Plyr, [{
+ key: "play",
+
+ /**
+ * Play the media, or play the advertisement (if they are not blocked)
+ */
+ value: function play() {
+ if (!is.function(this.media.play)) {
+ return null;
+ } // Return the promise (for HTML5)
- /**
- * Check for support for a mime type (HTML5 only)
- * @param {string} type - Mime type
- */
+
+ return this.media.play();
+ }
+ /**
+ * Pause the media
+ */
}, {
- key: 'supports',
- value: function supports(type) {
- return support.mime.call(this, type);
+ key: "pause",
+ value: function pause() {
+ if (!this.playing || !is.function(this.media.pause)) {
+ return;
}
- /**
- * Check for support
- * @param {string} type - Player type (audio/video)
- * @param {string} provider - Provider (html5/youtube/vimeo)
- * @param {bool} inline - Where player has `playsinline` sttribute
- */
+ this.media.pause();
+ }
+ /**
+ * Get playing state
+ */
}, {
- key: 'isHTML5',
- get: function get$$1() {
- return Boolean(this.provider === providers.html5);
+ key: "togglePlay",
+
+ /**
+ * Toggle playback based on current status
+ * @param {boolean} input
+ */
+ value: function togglePlay(input) {
+ // Toggle based on current state if nothing passed
+ var toggle = is.boolean(input) ? input : !this.playing;
+
+ if (toggle) {
+ this.play();
+ } else {
+ this.pause();
}
+ }
+ /**
+ * Stop playback
+ */
+
}, {
- key: 'isEmbed',
- get: function get$$1() {
- return Boolean(this.isYouTube || this.isVimeo);
+ key: "stop",
+ value: function stop() {
+ if (this.isHTML5) {
+ this.pause();
+ this.restart();
+ } else if (is.function(this.media.stop)) {
+ this.media.stop();
}
+ }
+ /**
+ * Restart playback
+ */
+
}, {
- key: 'isYouTube',
- get: function get$$1() {
- return Boolean(this.provider === providers.youtube);
- }
+ key: "restart",
+ value: function restart() {
+ this.currentTime = 0;
+ }
+ /**
+ * Rewind
+ * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
+ */
+
}, {
- key: 'isVimeo',
- get: function get$$1() {
- return Boolean(this.provider === providers.vimeo);
- }
+ key: "rewind",
+ value: function rewind(seekTime) {
+ this.currentTime = this.currentTime - (is.number(seekTime) ? seekTime : this.config.seekTime);
+ }
+ /**
+ * Fast forward
+ * @param {number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime
+ */
+
}, {
- key: 'isVideo',
- get: function get$$1() {
- return Boolean(this.type === types.video);
- }
+ key: "forward",
+ value: function forward(seekTime) {
+ this.currentTime = this.currentTime + (is.number(seekTime) ? seekTime : this.config.seekTime);
+ }
+ /**
+ * Seek to a time
+ * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
+ */
+
}, {
- key: 'isAudio',
- get: function get$$1() {
- return Boolean(this.type === types.audio);
- }
+ key: "increaseVolume",
+
+ /**
+ * Increase volume
+ * @param {boolean} step - How much to decrease by (between 0 and 1)
+ */
+ value: function increaseVolume(step) {
+ var volume = this.media.muted ? 0 : this.volume;
+ this.volume = volume + (is.number(step) ? step : 0);
+ }
+ /**
+ * Decrease volume
+ * @param {boolean} step - How much to decrease by (between 0 and 1)
+ */
+
}, {
- key: 'playing',
- get: function get$$1() {
- return Boolean(this.ready && !this.paused && !this.ended);
- }
+ key: "decreaseVolume",
+ value: function decreaseVolume(step) {
+ this.increaseVolume(-step);
+ }
+ /**
+ * Set muted state
+ * @param {boolean} mute
+ */
- /**
- * Get paused state
- */
+ }, {
+ key: "toggleCaptions",
+
+ /**
+ * Toggle captions
+ * @param {boolean} input - Whether to enable captions
+ */
+ value: function toggleCaptions(input) {
+ captions.toggle.call(this, input, false);
+ }
+ /**
+ * Set the caption track by index
+ * @param {number} - Caption index
+ */
}, {
- key: 'paused',
- get: function get$$1() {
- return Boolean(this.media.paused);
- }
+ key: "airplay",
- /**
- * Get stopped state
- */
+ /**
+ * Trigger the airplay dialog
+ * TODO: update player with state, support, enabled
+ */
+ value: function airplay() {
+ // Show dialog if supported
+ if (support.airplay) {
+ this.media.webkitShowPlaybackTargetPicker();
+ }
+ }
+ /**
+ * Toggle the player controls
+ * @param {boolean} [toggle] - Whether to show the controls
+ */
}, {
- key: 'stopped',
- get: function get$$1() {
- return Boolean(this.paused && this.currentTime === 0);
+ key: "toggleControls",
+ value: function toggleControls(toggle) {
+ // Don't toggle if missing UI support or if it's audio
+ if (this.supported.ui && !this.isAudio) {
+ // Get state before change
+ var isHidden = hasClass(this.elements.container, this.config.classNames.hideControls); // Negate the argument if not undefined since adding the class to hides the controls
+
+ var force = typeof toggle === 'undefined' ? undefined : !toggle; // Apply and get updated state
+
+ var hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); // Close menu
+
+ if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
+ controls.toggleMenu.call(this, false);
+ } // Trigger event on change
+
+
+ if (hiding !== isHidden) {
+ var eventName = hiding ? 'controlshidden' : 'controlsshown';
+ triggerEvent.call(this, this.media, eventName);
+ }
+
+ return !hiding;
}
- /**
- * Get ended state
- */
+ return false;
+ }
+ /**
+ * Add event listeners
+ * @param {string} event - Event type
+ * @param {function} callback - Callback for when event occurs
+ */
}, {
- key: 'ended',
- get: function get$$1() {
- return Boolean(this.media.ended);
- }
+ key: "on",
+ value: function on$$1(event, callback) {
+ on.call(this, this.elements.container, event, callback);
+ }
+ /**
+ * Add event listeners once
+ * @param {string} event - Event type
+ * @param {function} callback - Callback for when event occurs
+ */
+
}, {
- key: 'currentTime',
- set: function set$$1(input) {
- var targetTime = 0;
+ key: "once",
+ value: function once$$1(event, callback) {
+ once.call(this, this.elements.container, event, callback);
+ }
+ /**
+ * Remove event listeners
+ * @param {string} event - Event type
+ * @param {function} callback - Callback for when event occurs
+ */
- if (utils.is.number(input)) {
- targetTime = input;
- }
+ }, {
+ key: "off",
+ value: function off$$1(event, callback) {
+ off(this.elements.container, event, callback);
+ }
+ /**
+ * Destroy an instance
+ * Event listeners are removed when elements are removed
+ * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory
+ * @param {function} callback - Callback for when destroy is complete
+ * @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
+ */
- // Normalise targetTime
- if (targetTime < 0) {
- targetTime = 0;
- } else if (targetTime > this.duration) {
- targetTime = this.duration;
- }
+ }, {
+ key: "destroy",
+ value: function destroy(callback) {
+ var _this2 = this;
- // Set
- this.media.currentTime = targetTime;
+ var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
- // Logging
- this.debug.log('Seeking to ' + this.currentTime + ' seconds');
+ if (!this.ready) {
+ return;
}
- /**
- * Get current time
- */
- ,
- get: function get$$1() {
- return Number(this.media.currentTime);
- }
+ var done = function done() {
+ // Reset overflow (incase destroyed while in fullscreen)
+ document.body.style.overflow = ''; // GC for embed
- /**
- * Get buffered
- */
+ _this2.embed = null; // If it's a soft destroy, make minimal changes
- }, {
- key: 'buffered',
- get: function get$$1() {
- var buffered = this.media.buffered;
+ if (soft) {
+ if (Object.keys(_this2.elements).length) {
+ // Remove elements
+ removeElement(_this2.elements.buttons.play);
+ removeElement(_this2.elements.captions);
+ removeElement(_this2.elements.controls);
+ removeElement(_this2.elements.wrapper); // Clear for GC
- // YouTube / Vimeo return a float between 0-1
+ _this2.elements.buttons.play = null;
+ _this2.elements.captions = null;
+ _this2.elements.controls = null;
+ _this2.elements.wrapper = null;
+ } // Callback
- if (utils.is.number(buffered)) {
- return buffered;
- }
- // HTML5
- // TODO: Handle buffered chunks of the media
- // (i.e. seek to another section buffers only that section)
- if (buffered && buffered.length && this.duration > 0) {
- return buffered.end(0) / this.duration;
+ if (is.function(callback)) {
+ callback();
}
+ } else {
+ // Unbind listeners
+ unbindListeners.call(_this2); // Replace the container with the original element provided
- return 0;
- }
+ replaceElement(_this2.elements.original, _this2.elements.container); // Event
- /**
- * Get seeking status
- */
+ triggerEvent.call(_this2, _this2.elements.original, 'destroyed', true); // Callback
- }, {
- key: 'seeking',
- get: function get$$1() {
- return Boolean(this.media.seeking);
- }
+ if (is.function(callback)) {
+ callback.call(_this2.elements.original);
+ } // Reset state
- /**
- * Get the duration of the current media
- */
- }, {
- key: 'duration',
- get: function get$$1() {
- // Faux duration set via config
- var fauxDuration = parseFloat(this.config.duration);
+ _this2.ready = false; // Clear for garbage collection
- // True duration
- var realDuration = this.media ? Number(this.media.duration) : 0;
+ setTimeout(function () {
+ _this2.elements = null;
+ _this2.media = null;
+ }, 200);
+ }
+ }; // Stop playback
- // If custom duration is funky, use regular duration
- return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
- }
- /**
- * Set the player volume
- * @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
- */
+ this.stop(); // Provider specific stuff
- }, {
- key: 'volume',
- set: function set$$1(value) {
- var volume = value;
- var max = 1;
- var min = 0;
-
- if (utils.is.string(volume)) {
- volume = Number(volume);
- }
+ if (this.isHTML5) {
+ // Clear timeout
+ clearTimeout(this.timers.loading); // Restore native video controls
- // Load volume from storage if no value specified
- if (!utils.is.number(volume)) {
- volume = this.storage.get('volume');
- }
+ ui.toggleNativeControls.call(this, true); // Clean up
- // Use config if all else fails
- if (!utils.is.number(volume)) {
- volume = this.config.volume;
- }
+ done();
+ } else if (this.isYouTube) {
+ // Clear timers
+ clearInterval(this.timers.buffering);
+ clearInterval(this.timers.playing); // Destroy YouTube API
- // Maximum is volumeMax
- if (volume > max) {
- volume = max;
- }
- // Minimum is volumeMin
- if (volume < min) {
- volume = min;
- }
+ if (this.embed !== null && is.function(this.embed.destroy)) {
+ this.embed.destroy();
+ } // Clean up
- // Update config
- this.config.volume = volume;
- // Set the player volume
- this.media.volume = volume;
+ done();
+ } else if (this.isVimeo) {
+ // Destroy Vimeo API
+ // then clean up (wait, to prevent postmessage errors)
+ if (this.embed !== null) {
+ this.embed.unload().then(done);
+ } // Vimeo does not always return
- // If muted, and we're increasing volume manually, reset muted state
- if (!utils.is.empty(value) && this.muted && volume > 0) {
- this.muted = false;
- }
- }
- /**
- * Get the current player volume
- */
- ,
- get: function get$$1() {
- return Number(this.media.volume);
+ setTimeout(done, 200);
}
+ }
+ /**
+ * Check for support for a mime type (HTML5 only)
+ * @param {string} type - Mime type
+ */
+
}, {
- key: 'muted',
- set: function set$$1(mute) {
- var toggle = mute;
+ key: "supports",
+ value: function supports(type) {
+ return support.mime.call(this, type);
+ }
+ /**
+ * Check for support
+ * @param {string} type - Player type (audio/video)
+ * @param {string} provider - Provider (html5/youtube/vimeo)
+ * @param {bool} inline - Where player has `playsinline` sttribute
+ */
- // Load muted state from storage
- if (!utils.is.boolean(toggle)) {
- toggle = this.storage.get('muted');
- }
+ }, {
+ key: "isHTML5",
+ get: function get() {
+ return Boolean(this.provider === providers.html5);
+ }
+ }, {
+ key: "isEmbed",
+ get: function get() {
+ return Boolean(this.isYouTube || this.isVimeo);
+ }
+ }, {
+ key: "isYouTube",
+ get: function get() {
+ return Boolean(this.provider === providers.youtube);
+ }
+ }, {
+ key: "isVimeo",
+ get: function get() {
+ return Boolean(this.provider === providers.vimeo);
+ }
+ }, {
+ key: "isVideo",
+ get: function get() {
+ return Boolean(this.type === types.video);
+ }
+ }, {
+ key: "isAudio",
+ get: function get() {
+ return Boolean(this.type === types.audio);
+ }
+ }, {
+ key: "playing",
+ get: function get() {
+ return Boolean(this.ready && !this.paused && !this.ended);
+ }
+ /**
+ * Get paused state
+ */
- // Use config if all else fails
- if (!utils.is.boolean(toggle)) {
- toggle = this.config.muted;
- }
+ }, {
+ key: "paused",
+ get: function get() {
+ return Boolean(this.media.paused);
+ }
+ /**
+ * Get stopped state
+ */
- // Update config
- this.config.muted = toggle;
+ }, {
+ key: "stopped",
+ get: function get() {
+ return Boolean(this.paused && this.currentTime === 0);
+ }
+ /**
+ * Get ended state
+ */
- // Set mute on the player
- this.media.muted = toggle;
- }
+ }, {
+ key: "ended",
+ get: function get() {
+ return Boolean(this.media.ended);
+ }
+ }, {
+ key: "currentTime",
+ set: function set(input) {
+ // Bail if media duration isn't available yet
+ if (!this.duration) {
+ return;
+ } // Validate input
- /**
- * Get current muted state
- */
- ,
- get: function get$$1() {
- return Boolean(this.media.muted);
- }
- /**
- * Check if the media has audio
- */
+ var inputIsValid = is.number(input) && input > 0; // Set
+
+ this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging
+
+ this.debug.log("Seeking to ".concat(this.currentTime, " seconds"));
+ }
+ /**
+ * Get current time
+ */
+ ,
+ get: function get() {
+ return Number(this.media.currentTime);
+ }
+ /**
+ * Get buffered
+ */
}, {
- key: 'hasAudio',
- get: function get$$1() {
- // Assume yes for all non HTML5 (as we can't tell...)
- if (!this.isHTML5) {
- return true;
- }
+ key: "buffered",
+ get: function get() {
+ var buffered = this.media.buffered; // YouTube / Vimeo return a float between 0-1
- if (this.isAudio) {
- return true;
- }
+ if (is.number(buffered)) {
+ return buffered;
+ } // HTML5
+ // TODO: Handle buffered chunks of the media
+ // (i.e. seek to another section buffers only that section)
- // Get audio tracks
- return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
+
+ if (buffered && buffered.length && this.duration > 0) {
+ return buffered.end(0) / this.duration;
}
- /**
- * Set playback speed
- * @param {number} speed - the speed of playback (0.5-2.0)
- */
+ return 0;
+ }
+ /**
+ * Get seeking status
+ */
}, {
- key: 'speed',
- set: function set$$1(input) {
- var speed = null;
-
- if (utils.is.number(input)) {
- speed = input;
- }
+ key: "seeking",
+ get: function get() {
+ return Boolean(this.media.seeking);
+ }
+ /**
+ * Get the duration of the current media
+ */
- if (!utils.is.number(speed)) {
- speed = this.storage.get('speed');
- }
+ }, {
+ key: "duration",
+ get: function get() {
+ // Faux duration set via config
+ var fauxDuration = parseFloat(this.config.duration); // Media duration can be NaN or Infinity before the media has loaded
- if (!utils.is.number(speed)) {
- speed = this.config.speed.selected;
- }
+ var realDuration = (this.media || {}).duration;
+ var duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; // If config duration is funky, use regular duration
- // Set min/max
- if (speed < 0.1) {
- speed = 0.1;
- }
- if (speed > 2.0) {
- speed = 2.0;
- }
+ return fauxDuration || duration;
+ }
+ /**
+ * Set the player volume
+ * @param {number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage
+ */
- if (!this.config.speed.options.includes(speed)) {
- this.debug.warn('Unsupported speed (' + speed + ')');
- return;
- }
+ }, {
+ key: "volume",
+ set: function set(value) {
+ var volume = value;
+ var max = 1;
+ var min = 0;
- // Update config
- this.config.speed.selected = speed;
+ if (is.string(volume)) {
+ volume = Number(volume);
+ } // Load volume from storage if no value specified
- // Set media speed
- this.media.playbackRate = speed;
- }
- /**
- * Get current playback speed
- */
- ,
- get: function get$$1() {
- return Number(this.media.playbackRate);
- }
+ if (!is.number(volume)) {
+ volume = this.storage.get('volume');
+ } // Use config if all else fails
- /**
- * Set playback quality
- * Currently HTML5 & YouTube only
- * @param {number} input - Quality level
- */
- }, {
- key: 'quality',
- set: function set$$1(input) {
- var quality = null;
+ if (!is.number(volume)) {
+ volume = this.config.volume;
+ } // Maximum is volumeMax
- if (!utils.is.empty(input)) {
- quality = Number(input);
- }
- if (!utils.is.number(quality) || quality === 0) {
- quality = this.storage.get('quality');
- }
+ if (volume > max) {
+ volume = max;
+ } // Minimum is volumeMin
- if (!utils.is.number(quality)) {
- quality = this.config.quality.selected;
- }
- if (!utils.is.number(quality)) {
- quality = this.config.quality.default;
- }
+ if (volume < min) {
+ volume = min;
+ } // Update config
- if (!this.options.quality.length) {
- return;
- }
- if (!this.options.quality.includes(quality)) {
- var closest = utils.closest(this.options.quality, quality);
- this.debug.warn('Unsupported quality option: ' + quality + ', using ' + closest + ' instead');
- quality = closest;
- }
+ this.config.volume = volume; // Set the player volume
- // Update config
- this.config.quality.selected = quality;
+ this.media.volume = volume; // If muted, and we're increasing volume manually, reset muted state
- // Set quality
- this.media.quality = quality;
+ if (!is.empty(value) && this.muted && volume > 0) {
+ this.muted = false;
}
+ }
+ /**
+ * Get the current player volume
+ */
+ ,
+ get: function get() {
+ return Number(this.media.volume);
+ }
+ }, {
+ key: "muted",
+ set: function set(mute) {
+ var toggle = mute; // Load muted state from storage
+
+ if (!is.boolean(toggle)) {
+ toggle = this.storage.get('muted');
+ } // Use config if all else fails
+
+
+ if (!is.boolean(toggle)) {
+ toggle = this.config.muted;
+ } // Update config
- /**
- * Get current quality level
- */
- ,
- get: function get$$1() {
- return this.media.quality;
- }
- /**
- * Toggle loop
- * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
- * @param {boolean} input - Whether to loop or not
- */
+ this.config.muted = toggle; // Set mute on the player
+
+ this.media.muted = toggle;
+ }
+ /**
+ * Get current muted state
+ */
+ ,
+ get: function get() {
+ return Boolean(this.media.muted);
+ }
+ /**
+ * Check if the media has audio
+ */
}, {
- key: 'loop',
- set: function set$$1(input) {
- var toggle = utils.is.boolean(input) ? input : this.config.loop.active;
- this.config.loop.active = toggle;
- this.media.loop = toggle;
-
- // Set default to be a true toggle
- /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
- switch (type) {
- case 'start':
- if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
- this.config.loop.end = null;
- }
- this.config.loop.start = this.currentTime;
- // this.config.loop.indicator.start = this.elements.display.played.value;
- break;
- case 'end':
- if (this.config.loop.start >= this.currentTime) {
- return this;
- }
- this.config.loop.end = this.currentTime;
- // this.config.loop.indicator.end = this.elements.display.played.value;
- break;
- case 'all':
- this.config.loop.start = 0;
- this.config.loop.end = this.duration - 2;
- this.config.loop.indicator.start = 0;
- this.config.loop.indicator.end = 100;
- break;
- case 'toggle':
- if (this.config.loop.active) {
- this.config.loop.start = 0;
- this.config.loop.end = null;
- } else {
- this.config.loop.start = 0;
- this.config.loop.end = this.duration - 2;
- }
- break;
- default:
- this.config.loop.start = 0;
- this.config.loop.end = null;
- break;
- } */
+ key: "hasAudio",
+ get: function get() {
+ // Assume yes for all non HTML5 (as we can't tell...)
+ if (!this.isHTML5) {
+ return true;
}
- /**
- * Get current loop state
- */
- ,
- get: function get$$1() {
- return Boolean(this.media.loop);
- }
+ if (this.isAudio) {
+ return true;
+ } // Get audio tracks
- /**
- * Set new media source
- * @param {object} input - The new source object (see docs)
- */
+
+ return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
+ }
+ /**
+ * Set playback speed
+ * @param {number} speed - the speed of playback (0.5-2.0)
+ */
}, {
- key: 'source',
- set: function set$$1(input) {
- source.change.call(this, input);
+ key: "speed",
+ set: function set(input) {
+ var speed = null;
+
+ if (is.number(input)) {
+ speed = input;
}
- /**
- * Get current source
- */
- ,
- get: function get$$1() {
- return this.media.currentSrc;
+ if (!is.number(speed)) {
+ speed = this.storage.get('speed');
}
- /**
- * Set the poster image for a video
- * @param {input} - the URL for the new poster image
- */
+ if (!is.number(speed)) {
+ speed = this.config.speed.selected;
+ } // Set min/max
- }, {
- key: 'poster',
- set: function set$$1(input) {
- if (!this.isVideo) {
- this.debug.warn('Poster can only be set for video');
- return;
- }
- if (utils.is.string(input)) {
- this.media.setAttribute('poster', input);
- ui.setPoster.call(this);
- }
+ if (speed < 0.1) {
+ speed = 0.1;
}
- /**
- * Get the current poster image
- */
- ,
- get: function get$$1() {
- if (!this.isVideo) {
- return null;
- }
-
- return this.media.getAttribute('poster');
+ if (speed > 2.0) {
+ speed = 2.0;
}
- /**
- * Set the autoplay state
- * @param {boolean} input - Whether to autoplay or not
- */
+ if (!this.config.speed.options.includes(speed)) {
+ this.debug.warn("Unsupported speed (".concat(speed, ")"));
+ return;
+ } // Update config
+
+
+ this.config.speed.selected = speed; // Set media speed
+
+ this.media.playbackRate = speed;
+ }
+ /**
+ * Get current playback speed
+ */
+ ,
+ get: function get() {
+ return Number(this.media.playbackRate);
+ }
+ /**
+ * Set playback quality
+ * Currently HTML5 & YouTube only
+ * @param {number} input - Quality level
+ */
}, {
- key: 'autoplay',
- set: function set$$1(input) {
- var toggle = utils.is.boolean(input) ? input : this.config.autoplay;
- this.config.autoplay = toggle;
- }
+ key: "quality",
+ set: function set(input) {
+ var config = this.config.quality;
+ var options = this.options.quality;
- /**
- * Get the current autoplay state
- */
- ,
- get: function get$$1() {
- return Boolean(this.config.autoplay);
+ if (!options.length) {
+ return;
}
- }, {
- key: 'language',
- set: function set$$1(input) {
- // Nothing specified
- if (!utils.is.string(input)) {
- return;
- }
- // If empty string is passed, assume disable captions
- if (utils.is.empty(input)) {
- this.toggleCaptions(false);
- return;
- }
+ var quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);
+ var updateStorage = true;
- // Normalize
- var language = input.toLowerCase();
+ if (!options.includes(quality)) {
+ var value = closest(options, quality);
+ this.debug.warn("Unsupported quality option: ".concat(quality, ", using ").concat(value, " instead"));
+ quality = value; // Don't update storage if quality is not supported
- // Check for support
- if (!this.options.captions.includes(language)) {
- this.debug.warn('Unsupported language option: ' + language);
- return;
- }
+ updateStorage = false;
+ } // Update config
- // Ensure captions are enabled
- this.toggleCaptions(true);
- // Enabled only
- if (language === 'enabled') {
- return;
- }
+ config.selected = quality; // Set quality
- // If nothing to change, bail
- if (this.language === language) {
- return;
- }
+ this.media.quality = quality; // Save to storage
- // Update config
- this.captions.language = language;
+ if (updateStorage) {
+ this.storage.set({
+ quality: quality
+ });
+ }
+ }
+ /**
+ * Get current quality level
+ */
+ ,
+ get: function get() {
+ return this.media.quality;
+ }
+ /**
+ * Toggle loop
+ * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config
+ * @param {boolean} input - Whether to loop or not
+ */
- // Clear caption
- captions.setText.call(this, null);
+ }, {
+ key: "loop",
+ set: function set(input) {
+ var toggle = is.boolean(input) ? input : this.config.loop.active;
+ this.config.loop.active = toggle;
+ this.media.loop = toggle; // Set default to be a true toggle
+
+ /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';
+ switch (type) {
+ case 'start':
+ if (this.config.loop.end && this.config.loop.end <= this.currentTime) {
+ this.config.loop.end = null;
+ }
+ this.config.loop.start = this.currentTime;
+ // this.config.loop.indicator.start = this.elements.display.played.value;
+ break;
+ case 'end':
+ if (this.config.loop.start >= this.currentTime) {
+ return this;
+ }
+ this.config.loop.end = this.currentTime;
+ // this.config.loop.indicator.end = this.elements.display.played.value;
+ break;
+ case 'all':
+ this.config.loop.start = 0;
+ this.config.loop.end = this.duration - 2;
+ this.config.loop.indicator.start = 0;
+ this.config.loop.indicator.end = 100;
+ break;
+ case 'toggle':
+ if (this.config.loop.active) {
+ this.config.loop.start = 0;
+ this.config.loop.end = null;
+ } else {
+ this.config.loop.start = 0;
+ this.config.loop.end = this.duration - 2;
+ }
+ break;
+ default:
+ this.config.loop.start = 0;
+ this.config.loop.end = null;
+ break;
+ } */
+ }
+ /**
+ * Get current loop state
+ */
+ ,
+ get: function get() {
+ return Boolean(this.media.loop);
+ }
+ /**
+ * Set new media source
+ * @param {object} input - The new source object (see docs)
+ */
- // Update captions
- captions.setLanguage.call(this);
+ }, {
+ key: "source",
+ set: function set(input) {
+ source.change.call(this, input);
+ }
+ /**
+ * Get current source
+ */
+ ,
+ get: function get() {
+ return this.media.currentSrc;
+ }
+ /**
+ * Get a download URL (either source or custom)
+ */
- // Trigger an event
- utils.dispatchEvent.call(this, this.media, 'languagechange');
+ }, {
+ key: "download",
+ get: function get() {
+ var download = this.config.urls.download;
+ return is.url(download) ? download : this.source;
+ }
+ /**
+ * Set the poster image for a video
+ * @param {input} - the URL for the new poster image
+ */
+
+ }, {
+ key: "poster",
+ set: function set(input) {
+ if (!this.isVideo) {
+ this.debug.warn('Poster can only be set for video');
+ return;
}
- /**
- * Get the current captions language
- */
- ,
- get: function get$$1() {
- return this.captions.language;
+ ui.setPoster.call(this, input, false).catch(function () {});
+ }
+ /**
+ * Get the current poster image
+ */
+ ,
+ get: function get() {
+ if (!this.isVideo) {
+ return null;
}
- /**
- * Toggle picture-in-picture playback on WebKit/MacOS
- * TODO: update player with state, support, enabled
- * TODO: detect outside changes
- */
+ return this.media.getAttribute('poster');
+ }
+ /**
+ * Set the autoplay state
+ * @param {boolean} input - Whether to autoplay or not
+ */
}, {
- key: 'pip',
- set: function set$$1(input) {
- var states = {
- pip: 'picture-in-picture',
- inline: 'inline'
- };
+ key: "autoplay",
+ set: function set(input) {
+ var toggle = is.boolean(input) ? input : this.config.autoplay;
+ this.config.autoplay = toggle;
+ }
+ /**
+ * Get the current autoplay state
+ */
+ ,
+ get: function get() {
+ return Boolean(this.config.autoplay);
+ }
+ }, {
+ key: "currentTrack",
+ set: function set(input) {
+ captions.set.call(this, input, false);
+ }
+ /**
+ * Get the current caption track index (-1 if disabled)
+ */
+ ,
+ get: function get() {
+ var _this$captions = this.captions,
+ toggled = _this$captions.toggled,
+ currentTrack = _this$captions.currentTrack;
+ return toggled ? currentTrack : -1;
+ }
+ /**
+ * Set the wanted language for captions
+ * Since tracks can be added later it won't update the actual caption track until there is a matching track
+ * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
+ */
- // Bail if no support
- if (!support.pip) {
- return;
- }
+ }, {
+ key: "language",
+ set: function set(input) {
+ captions.setLanguage.call(this, input, false);
+ }
+ /**
+ * Get the current track's language
+ */
+ ,
+ get: function get() {
+ return (captions.getCurrentTrack.call(this) || {}).language;
+ }
+ /**
+ * Toggle picture-in-picture playback on WebKit/MacOS
+ * TODO: update player with state, support, enabled
+ * TODO: detect outside changes
+ */
- // Toggle based on current state if not passed
- var toggle = utils.is.boolean(input) ? input : this.pip === states.inline;
+ }, {
+ key: "pip",
+ set: function set(input) {
+ // Bail if no support
+ if (!support.pip) {
+ return;
+ } // Toggle based on current state if not passed
- // Toggle based on current state
- this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
- }
- /**
- * Get the current picture-in-picture state
- */
- ,
- get: function get$$1() {
- if (!support.pip) {
- return null;
- }
+ var toggle = is.boolean(input) ? input : !this.pip; // Toggle based on current state
+ // Safari
- return this.media.webkitPresentationMode;
- }
- }], [{
- key: 'supported',
- value: function supported(type, provider, inline) {
- return support.check(type, provider, inline);
- }
+ if (is.function(this.media.webkitSetPresentationMode)) {
+ this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
+ } // Chrome
- /**
- * Load an SVG sprite into the page
- * @param {string} url - URL for the SVG sprite
- * @param {string} [id] - Unique ID
- */
- }, {
- key: 'loadSprite',
- value: function loadSprite(url, id) {
- return utils.loadSprite(url, id);
+ if (is.function(this.media.requestPictureInPicture)) {
+ if (!this.pip && toggle) {
+ this.media.requestPictureInPicture();
+ } else if (this.pip && !toggle) {
+ document.exitPictureInPicture();
+ }
}
+ }
+ /**
+ * Get the current picture-in-picture state
+ */
+ ,
+ get: function get() {
+ if (!support.pip) {
+ return null;
+ } // Safari
+
- /**
- * Setup multiple instances
- * @param {*} selector
- * @param {object} options
- */
+ if (!is.empty(this.media.webkitPresentationMode)) {
+ return this.media.webkitPresentationMode === pip.active;
+ } // Chrome
+
+
+ return this.media === document.pictureInPictureElement;
+ }
+ }], [{
+ key: "supported",
+ value: function supported(type, provider, inline) {
+ return support.check(type, provider, inline);
+ }
+ /**
+ * Load an SVG sprite into the page
+ * @param {string} url - URL for the SVG sprite
+ * @param {string} [id] - Unique ID
+ */
}, {
- key: 'setup',
- value: function setup(selector) {
- var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
-
- var targets = null;
-
- if (utils.is.string(selector)) {
- targets = Array.from(document.querySelectorAll(selector));
- } else if (utils.is.nodeList(selector)) {
- targets = Array.from(selector);
- } else if (utils.is.array(selector)) {
- targets = selector.filter(function (i) {
- return utils.is.element(i);
- });
- }
+ key: "loadSprite",
+ value: function loadSprite$$1(url, id) {
+ return loadSprite(url, id);
+ }
+ /**
+ * Setup multiple instances
+ * @param {*} selector
+ * @param {object} options
+ */
- if (utils.is.empty(targets)) {
- return null;
- }
+ }, {
+ key: "setup",
+ value: function setup(selector) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var targets = null;
- return targets.map(function (t) {
- return new Plyr(t, options);
- });
+ if (is.string(selector)) {
+ targets = Array.from(document.querySelectorAll(selector));
+ } else if (is.nodeList(selector)) {
+ targets = Array.from(selector);
+ } else if (is.array(selector)) {
+ targets = selector.filter(is.element);
+ }
+
+ if (is.empty(targets)) {
+ return null;
}
+
+ return targets.map(function (t) {
+ return new Plyr(t, options);
+ });
+ }
}]);
+
return Plyr;
-}();
+ }();
-return Plyr;
+ Plyr.defaults = cloneDeep(defaults);
-})));
+ return Plyr;
-//# sourceMappingURL=plyr.js.map
+})));