aboutsummaryrefslogtreecommitdiffstats
path: root/dist/plyr.js
diff options
context:
space:
mode:
Diffstat (limited to 'dist/plyr.js')
-rw-r--r--dist/plyr.js12674
1 files changed, 6131 insertions, 6543 deletions
diff --git a/dist/plyr.js b/dist/plyr.js
index 1766f17e..4cc8a6c1 100644
--- a/dist/plyr.js
+++ b/dist/plyr.js
@@ -1,350 +1,71 @@
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());
+ 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, []);
-};
-
-
-/**
- * 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;
-};
+ return obj;
+ }
-// export
-return loadjs;
-
-}));
-});
+ 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;
@@ -361,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;
}
@@ -370,2239 +91,2007 @@ 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 Storage = function () {
- function Storage(player) {
- classCallCheck(this, Storage);
+ function _nonIterableRest() {
+ throw new TypeError("Invalid attempt to destructure non-iterable instance");
+ }
- this.enabled = player.config.storage.enabled;
- this.key = player.config.storage.key;
- }
+ // ==========================================================================
+ // Type checking utils
+ // ==========================================================================
+ var getConstructor = function getConstructor(input) {
+ return input !== null && typeof input !== 'undefined' ? input.constructor : null;
+ };
- // Check for actual support (see if we can use it)
+ var instanceOf = function instanceOf(input, constructor) {
+ return Boolean(input && constructor && input instanceof constructor);
+ };
+ var isNullOrUndefined = function isNullOrUndefined(input) {
+ return input === null || typeof input === 'undefined';
+ };
- createClass(Storage, [{
- key: 'get',
- value: function get$$1(key) {
- if (!Storage.supported) {
- return null;
- }
+ var isObject = function isObject(input) {
+ return getConstructor(input) === Object;
+ };
- var store = window.localStorage.getItem(this.key);
+ var isNumber = function isNumber(input) {
+ return getConstructor(input) === Number && !Number.isNaN(input);
+ };
- if (utils.is.empty(store)) {
- return null;
- }
+ var isString = function isString(input) {
+ return getConstructor(input) === String;
+ };
- var json = JSON.parse(store);
+ var isBoolean = function isBoolean(input) {
+ return getConstructor(input) === Boolean;
+ };
- return utils.is.string(key) && key.length ? json[key] : json;
- }
- }, {
- key: 'set',
- value: function set$$1(object) {
- // Bail if we don't have localStorage support or it's disabled
- if (!Storage.supported || !this.enabled) {
- return;
- }
+ var isFunction = function isFunction(input) {
+ return getConstructor(input) === Function;
+ };
- // Can only store objectst
- if (!utils.is.object(object)) {
- return;
- }
+ var isArray = function isArray(input) {
+ return Array.isArray(input);
+ };
- // Get current storage
- var storage = this.get();
+ var isWeakMap = function isWeakMap(input) {
+ return instanceOf(input, WeakMap);
+ };
- // Default to empty object
- if (utils.is.empty(storage)) {
- storage = {};
- }
+ var isNodeList = function isNodeList(input) {
+ return instanceOf(input, NodeList);
+ };
- // Update the working copy of the values
- utils.extend(storage, object);
+ var isElement = function isElement(input) {
+ return instanceOf(input, Element);
+ };
- // Update storage
- window.localStorage.setItem(this.key, JSON.stringify(storage));
- }
- }], [{
- key: 'supported',
- get: function get$$1() {
- try {
- if (!('localStorage' in window)) {
- return false;
- }
+ var isTextNode = function isTextNode(input) {
+ return getConstructor(input) === Text;
+ };
- var test = '___test';
+ var isEvent = function isEvent(input) {
+ return instanceOf(input, Event);
+ };
- // 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 isKeyboardEvent = function isKeyboardEvent(input) {
+ return instanceOf(input, KeyboardEvent);
+ };
- return true;
- } catch (e) {
- return false;
- }
- }
- }]);
- return Storage;
-}();
+ var isCue = function isCue(input) {
+ return instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);
+ };
-// ==========================================================================
-// Plyr supported types and providers
-// ==========================================================================
+ var isTrack = function isTrack(input) {
+ return instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);
+ };
-var providers = {
- html5: 'html5',
- youtube: 'youtube',
- vimeo: 'vimeo'
-};
+ var isEmpty = function isEmpty(input) {
+ return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;
+ };
-var types = {
- audio: 'audio',
- video: 'video'
-};
+ var isUrl = function isUrl(input) {
+ // Accept a URL object
+ if (instanceOf(input, window.URL)) {
+ return true;
+ } // Must be string from here
-// ==========================================================================
-var utils = {
- // Check variable types
- is: {
- object: function object(input) {
- return this.getConstructor(input) === Object;
- },
- number: function number(input) {
- return this.getConstructor(input) === Number && !Number.isNaN(input);
- },
- string: function string(input) {
- return this.getConstructor(input) === String;
- },
- boolean: function boolean(input) {
- return this.getConstructor(input) === Boolean;
- },
- function: function _function(input) {
- return this.getConstructor(input) === Function;
- },
- array: function array(input) {
- return !this.nullOrUndefined(input) && Array.isArray(input);
- },
- weakMap: function weakMap(input) {
- return this.instanceof(input, WeakMap);
- },
- nodeList: function nodeList(input) {
- return this.instanceof(input, NodeList);
- },
- element: function element(input) {
- return this.instanceof(input, Element);
- },
- textNode: function textNode(input) {
- return this.getConstructor(input) === Text;
- },
- event: function event(input) {
- return this.instanceof(input, Event);
- },
- cue: function cue(input) {
- return this.instanceof(input, TextTrackCue) || this.instanceof(input, VTTCue);
- },
- track: function track(input) {
- return this.instanceof(input, TextTrack) || !this.nullOrUndefined(input) && this.string(input.kind);
- },
- url: function url(input) {
- return !this.nullOrUndefined(input) && /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(input);
- },
- nullOrUndefined: function nullOrUndefined(input) {
- return input === null || typeof input === 'undefined';
- },
- empty: function empty(input) {
- return this.nullOrUndefined(input) || (this.string(input) || this.array(input) || this.nodeList(input)) && !input.length || this.object(input) && !Object.keys(input).length;
- },
- instanceof: function _instanceof$$1(input, constructor) {
- return Boolean(input && constructor && input instanceof constructor);
- },
- getConstructor: function getConstructor(input) {
- return !this.nullOrUndefined(input) ? input.constructor : null;
- }
- },
+ if (!isString(input)) {
+ return false;
+ } // Add the protocol if required
- // 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)
- };
- },
+ var string = input;
- // 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';
+ if (!input.startsWith('http://') || !input.startsWith('https://')) {
+ string = "http://".concat(input);
+ }
- return new Promise(function (resolve, reject) {
- try {
- var request = new XMLHttpRequest();
+ try {
+ return !isEmpty(new URL(string).hostname);
+ } catch (e) {
+ return false;
+ }
+ };
- // Check for CORS support
- if (!('withCredentials' in request)) {
- return;
- }
+ 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
+ };
- request.addEventListener('load', function () {
- if (responseType === 'text') {
- try {
- resolve(JSON.parse(request.responseText));
- } catch (e) {
- resolve(request.responseText);
- }
- } else {
- resolve(request.response);
- }
- });
+ // ==========================================================================
+ // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
+ // https://www.youtube.com/watch?v=NPM6172J22g
- request.addEventListener('error', function () {
- throw new Error(request.statusText);
- });
+ var supportsPassiveListeners = function () {
+ // Test via a getter in the options object to see if the passive property is accessed
+ var supported = false;
- request.open('GET', url, true);
+ 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
+ }
- // Set the required response type
- request.responseType = responseType;
+ return supported;
+ }(); // Toggle event listener
- request.send();
- } catch (e) {
- reject(e);
- }
- });
- },
+ function toggleListener(element, event, callback) {
+ var _this = this;
- // 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.
- loadImage: function loadImage(src) {
- var minWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
+ 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;
- 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 });
- });
- },
+ // Bail if no element, event, or callback
+ if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {
+ return;
+ } // Allow multiple events
- // Load an external script
- loadScript: function loadScript(url) {
- return new Promise(function (resolve, reject) {
- loadjs_umd(url, {
- success: resolve,
- error: reject
- });
- });
- },
+ 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
- // Load an external SVG sprite
- loadSprite: function loadSprite(url, id) {
- if (!utils.is.string(url)) {
- return;
- }
+ 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
- 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]);
- }
+ events.forEach(function (type) {
+ if (_this && _this.eventListeners && toggle) {
+ // Cache event listener
+ _this.eventListeners.push({
+ element: element,
+ type: type,
+ callback: callback,
+ options: options
+ });
+ }
- // Only load once if ID set
- if (!hasId || !exists()) {
- var useStorage = Storage.supported;
+ 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 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;
+
+ 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];
+ }
- // Create container
- var container = document.createElement('div');
- utils.toggleHidden(container, true);
+ callback.apply(this, args);
+ }
- if (hasId) {
- container.setAttribute('id', id);
- }
+ 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
- // Check in cache
- if (useStorage) {
- var cached = window.localStorage.getItem(prefix + id);
- isCached = cached !== null;
+ function ready() {
+ var _this2 = this;
- if (isCached) {
- var data = JSON.parse(cached);
- injectSprite.call(container, data.content);
- return;
- }
- }
+ return new Promise(function (resolve) {
+ return _this2.ready ? setTimeout(resolve, 0) : on.call(_this2, _this2.elements.container, 'ready', resolve);
+ }).then(function () {});
+ }
- // Get the sprite
- utils.fetch(url).then(function (result) {
- if (utils.is.empty(result)) {
- return;
- }
+ 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).
- if (useStorage) {
- window.localStorage.setItem(prefix + id, JSON.stringify({
- content: result
- }));
- }
+ Array.from(targets).reverse().forEach(function (element, index) {
+ var child = index > 0 ? wrapper.cloneNode(true) : wrapper; // Cache the current parent and sibling.
- injectSprite.call(container, result);
- }).catch(function () {});
- }
- },
+ 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.
- // Generate a random ID
- generateId: function generateId(prefix) {
- return prefix + '-' + Math.floor(Math.random() * 10000);
- },
+ if (sibling) {
+ parent.insertBefore(child, sibling);
+ } else {
+ parent.appendChild(child);
+ }
+ });
+ } // Set attributes
+ 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"
- // 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;
+ Object.entries(attributes).filter(function (_ref) {
+ var _ref2 = _slicedToArray(_ref, 2),
+ value = _ref2[1];
- // Cache the current parent and sibling.
- var parent = element.parentNode;
- var sibling = element.nextSibling;
+ return !is.nullOrUndefined(value);
+ }).forEach(function (_ref3) {
+ var _ref4 = _slicedToArray(_ref3, 2),
+ key = _ref4[0],
+ value = _ref4[1];
- // Wrap the element (is automatically removed from its current
- // parent).
- child.appendChild(element);
+ return element.setAttribute(key, value);
+ });
+ } // Create a DocumentFragment
- // 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);
- }
- });
- },
+ function createElement(type, attributes, text) {
+ // Create a new <element>
+ var element = document.createElement(type); // Set all passed attributes
+ if (is.object(attributes)) {
+ setAttributes(element, attributes);
+ } // Add text node
- // 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);
- }
+ if (is.string(text)) {
+ element.innerText = text;
+ } // Return built element
- // Add text node
- if (utils.is.string(text)) {
- element.innerText = text;
- }
- // Return built element
- return element;
- },
+ return element;
+ } // Inaert an element after another
+ function insertAfter(element, target) {
+ if (!is.element(element) || !is.element(target)) {
+ return;
+ }
- // Inaert an element after another
- insertAfter: function insertAfter(element, target) {
- target.parentNode.insertBefore(element, target.nextSibling);
- },
+ target.parentNode.insertBefore(element, target.nextSibling);
+ } // Insert a DocumentFragment
+ function insertElement(type, parent, attributes, text) {
+ if (!is.element(parent)) {
+ return;
+ }
- // Insert a DocumentFragment
- insertElement: function insertElement(type, parent, attributes, text) {
- // Inject the new <element>
- parent.appendChild(utils.createElement(type, attributes, text));
- },
+ parent.appendChild(createElement(type, attributes, text));
+ } // Remove element(s)
+ function removeElement(element) {
+ if (is.nodeList(element) || is.array(element)) {
+ Array.from(element).forEach(removeElement);
+ return;
+ }
- // Remove element(s)
- removeElement: function removeElement(element) {
- if (utils.is.nodeList(element) || utils.is.array(element)) {
- Array.from(element).forEach(utils.removeElement);
- return;
- }
+ if (!is.element(element) || !is.element(element.parentNode)) {
+ return;
+ }
- if (!utils.is.element(element) || !utils.is.element(element.parentNode)) {
- return;
- }
+ element.parentNode.removeChild(element);
+ } // Remove all child elements
- element.parentNode.removeChild(element);
- },
+ function emptyElement(element) {
+ if (!is.element(element)) {
+ return;
+ }
+ var length = element.childNodes.length;
- // Remove all child elements
- emptyElement: function emptyElement(element) {
- var length = element.childNodes.length;
+ while (length > 0) {
+ element.removeChild(element.lastChild);
+ length -= 1;
+ }
+ } // Replace element
+ function replaceElement(newChild, oldChild) {
+ if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) {
+ return null;
+ }
- while (length > 0) {
- element.removeChild(element.lastChild);
- length -= 1;
- }
- },
+ 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
- // Replace element
- replaceElement: function replaceElement(newChild, oldChild) {
- if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) {
- return null;
- }
+ var parts = stripped.split('=');
+ var key = parts[0];
+ var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; // Get the first character
- oldChild.parentNode.replaceChild(newChild, oldChild);
+ var start = selector.charAt(0);
- return newChild;
- },
+ switch (start) {
+ case '.':
+ // Add to existing classname
+ if (is.object(existing) && is.string(existing.class)) {
+ existing.class += " ".concat(className);
+ }
+ attributes.class = className;
+ break;
- // Set attributes
- setAttributes: function setAttributes(element, attributes) {
- if (!utils.is.element(element) || utils.is.empty(attributes)) {
- return;
- }
+ case '#':
+ // ID selector
+ attributes.id = selector.replace('#', '');
+ break;
- Object.entries(attributes).forEach(function (_ref) {
- var _ref2 = slicedToArray(_ref, 2),
- key = _ref2[0],
- value = _ref2[1];
+ case '[':
+ // Attribute selector
+ attributes[key] = value;
+ break;
- element.setAttribute(key, value);
- });
- },
+ default:
+ break;
+ }
+ });
+ return attributes;
+ } // Toggle hidden
+ function toggleHidden(element, hidden) {
+ if (!is.element(element)) {
+ return;
+ }
- // 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' }
+ var hide = hidden;
- if (!utils.is.string(sel) || utils.is.empty(sel)) {
- return {};
- }
+ if (!is.boolean(hide)) {
+ hide = !element.hidden;
+ }
- var attributes = {};
- var existing = existingAttributes;
+ if (hide) {
+ element.setAttribute('hidden', '');
+ } else {
+ element.removeAttribute('hidden');
+ }
+ } // Mirror Element.classList.toggle, with IE compatibility for "force" argument
- sel.split(',').forEach(function (s) {
- // Remove whitespace
- var selector = s.trim();
- var className = selector.replace('.', '');
- var stripped = selector.replace(/[[\]]/g, '');
+ function toggleClass(element, className, force) {
+ if (is.nodeList(element)) {
+ return Array.from(element).map(function (e) {
+ return toggleClass(e, className, force);
+ });
+ }
- // Get the parts and value
- var parts = stripped.split('=');
- var key = parts[0];
- var value = parts.length > 1 ? parts[1].replace(/["']/g, '') : '';
+ if (is.element(element)) {
+ var method = 'toggle';
- // Get the first character
- var start = selector.charAt(0);
+ if (typeof force !== 'undefined') {
+ method = force ? 'add' : 'remove';
+ }
- switch (start) {
- case '.':
- // Add to existing classname
- if (utils.is.object(existing) && utils.is.string(existing.class)) {
- existing.class += ' ' + className;
- }
+ element.classList[method](className);
+ return element.classList.contains(className);
+ }
- attributes.class = className;
- break;
+ return false;
+ } // Has class name
- case '#':
- // ID selector
- attributes.id = selector.replace('#', '');
- break;
+ function hasClass(element, className) {
+ return is.element(element) && element.classList.contains(className);
+ } // Element matches selector
- case '[':
- // Attribute selector
- attributes[key] = value;
+ function matches(element, selector) {
- break;
+ function match() {
+ return Array.from(document.querySelectorAll(selector)).includes(this);
+ }
- default:
- break;
- }
- });
+ var matches = match;
+ return matches.call(element, selector);
+ } // Find all elements
- return attributes;
- },
+ function getElements(selector) {
+ return this.elements.container.querySelectorAll(selector);
+ } // Find a single element
+ function getElement(selector) {
+ return this.elements.container.querySelector(selector);
+ } // Trap focus inside container
- // Toggle hidden
- toggleHidden: function toggleHidden(element, hidden) {
- if (!utils.is.element(element)) {
- return;
- }
+ 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 hide = hidden;
+ if (!is.element(element)) {
+ return;
+ }
- if (!utils.is.boolean(hide)) {
- hide = !element.hasAttribute('hidden');
- }
+ var focusable = getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
+ var first = focusable[0];
+ var last = focusable[focusable.length - 1];
- if (hide) {
- element.setAttribute('hidden', '');
- } else {
- element.removeAttribute('hidden');
- }
- },
+ 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
- // Mirror Element.classList.toggle, with IE compatibility for "force" argument
- toggleClass: function toggleClass(element, className, force) {
- if (utils.is.element(element)) {
- var method = 'toggle';
- if (typeof force !== 'undefined') {
- method = force ? 'add' : 'remove';
- }
+ var focused = document.activeElement;
- element.classList[method](className);
- return element.classList.contains(className);
- }
+ 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();
+ }
+ };
- return null;
- },
+ toggleListener.call(this, this.elements.container, 'keydown', trap, toggle, false);
+ } // Set focus and tab focus class
+ function setFocus() {
+ var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+ var tabFocus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
- // Has class name
- hasClass: function hasClass(element, className) {
- return utils.is.element(element) && element.classList.contains(className);
- },
+ if (!is.element(element)) {
+ return;
+ } // Set regular focus
- // Element matches selector
- matches: function matches(element, selector) {
- var prototype = { Element: Element };
+ element.focus({
+ preventScroll: true
+ }); // If we want to mimic keyboard focus via tab
- function match() {
- return Array.from(document.querySelectorAll(selector)).includes(this);
- }
+ if (tabFocus) {
+ toggleClass(element, this.config.classNames.tabFocus);
+ }
+ }
- var matches = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;
+ // ==========================================================================
+ 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
- return matches.call(element, selector);
- },
+ function repaint(element) {
+ setTimeout(function () {
+ try {
+ toggleHidden(element, true);
+ element.offsetHeight; // eslint-disable-line
+ toggleHidden(element, false);
+ } catch (e) {// Do nothing
+ }
+ }, 0);
+ }
- // Find all elements
- getElements: function getElements(selector) {
- return this.elements.container.querySelectorAll(selector);
- },
+ // ==========================================================================
+ // Browser sniffing
+ // Unfortunately, due to mixed support, UA sniffing is required
+ // ==========================================================================
+ var browser = {
+ isIE:
+ /* @cc_on!@ */
+ !!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)
+ };
+ 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
- // Find a single element
- getElement: function getElement(selector) {
- return this.elements.container.querySelector(selector);
+ 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 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
- // Get the focused element
- getFocusElement: function getFocusElement() {
- var focused = document.activeElement;
+ if (is.function(createElement('video').webkitSetPresentationMode)) {
+ return true;
+ } // Chrome
+ // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
- if (!focused || focused === document.body) {
- focused = null;
- } else {
- focused = document.querySelector(':focus');
- }
-
- return focused;
- },
+ if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {
+ return true;
+ }
- // 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;
+ return false;
+ }(),
+ // Airplay support
+ // Safari only currently
+ 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(inputType) {
+ var _inputType$split = inputType.split('/'),
+ _inputType$split2 = _slicedToArray(_inputType$split, 1),
+ mediaType = _inputType$split2[0];
- if (!utils.is.element(element)) {
- return;
- }
+ var type = inputType; // Verify we're using HTML5 and there's no media type mismatch
- var focusable = utils.getElements.call(this, 'button:not(:disabled), input:not(:disabled), [tabindex]');
- var first = focusable[0];
- var last = focusable[focusable.length - 1];
+ if (!this.isHTML5 || mediaType !== this.type) {
+ return false;
+ } // Add codec if required
- var trap = function trap(event) {
- // Bail if not tab key or not fullscreen
- if (event.key !== 'Tab' || event.keyCode !== 9) {
- return;
- }
- // Get the current focused element
- var focused = utils.getFocusElement();
-
- if (focused === last && !event.shiftKey) {
- // Move focus to first element that can be tabbed if Shift isn't used
- first.focus();
- event.preventDefault();
- } else if (focused === first && event.shiftKey) {
- // Move focus to last element that can be tabbed if Shift is used
- last.focus();
- event.preventDefault();
- }
- };
+ if (Object.keys(defaultCodecs).includes(type)) {
+ type += "; codecs=\"".concat(defaultCodecs[inputType], "\"");
+ }
- if (toggle) {
- utils.on(this.elements.container, 'keydown', trap, false);
- } else {
- utils.off(this.elements.container, 'keydown', trap, false);
- }
+ try {
+ return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));
+ } catch (e) {
+ return false;
+ }
},
+ // Check for textTracks support
+ textTracks: 'textTracks' in document.createElement('video'),
+ // <input type="range"> Sliders
+ rangeInput: function () {
+ var range = document.createElement('input');
+ range.type = 'range';
+ return range.type === 'range';
+ }(),
+ // Touch
+ // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
+ touch: 'ontouchstart' in document.documentElement,
+ // Detect transitions support
+ transitions: transitionEndEvent !== false,
+ // Reduced motion iOS & MacOS setting
+ // https://webkit.org/blog/7551/responsive-design-for-motion/
+ reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
+ };
+ // ==========================================================================
+ var html5 = {
+ getSources: function getSources() {
+ var _this = this;
- // Toggle event listener
- toggleListener: function toggleListener(elements, event, callback) {
- var toggle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
- var passive = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
- var capture = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false;
-
- // Bail if no elemetns, event, or callback
- if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
- return;
- }
-
- // If a nodelist is passed, call itself on each node
- if (utils.is.nodeList(elements) || utils.is.array(elements)) {
- // Create listener for each node
- Array.from(elements).forEach(function (element) {
- if (element instanceof Node) {
- utils.toggleListener.call(null, element, event, callback, toggle, passive, capture);
- }
- });
-
- return;
- }
-
- // Allow multiple events
- var events = event.split(' ');
-
- // Build options
- // Default to just the capture boolean for browsers with no passive listener support
- var options = capture;
-
- // If passive events listeners are supported
- if (support.passiveListeners) {
- options = {
- // Whether the listener can be passive (i.e. default never prevented)
- passive: passive,
- // Whether the listener is a capturing listener or not
- capture: capture
- };
- }
-
- // If a single node is passed, bind the event listener
- events.forEach(function (type) {
- elements[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);
- });
- },
-
+ if (!this.isHTML5) {
+ return [];
+ }
- // 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;
+ var sources = Array.from(this.media.querySelectorAll('source')); // Filter out unsupported sources
- utils.toggleListener(element, events, callback, true, passive, capture);
+ return sources.filter(function (source) {
+ return support.mime.call(_this, source.getAttribute('type'));
+ });
},
-
-
- // Unbind event handler
- off: function off(element) {
- var events = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var callback = arguments[2];
- var passive = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
- var capture = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
-
- utils.toggleListener(element, events, callback, false, passive, capture);
+ // 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
- // Trigger event
- dispatchEvent: function dispatchEvent(element) {
- var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var bubbles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
- var detail = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
-
- // Bail if no element
- if (!utils.is.element(element) || utils.is.empty(type)) {
- return;
- }
-
- // Create and dispatch the event
- var event = new CustomEvent(type, {
- bubbles: bubbles,
- detail: Object.assign({}, detail, {
- plyr: this
- })
- });
-
- // Dispatch the event
- element.dispatchEvent(event);
- },
+ 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
+ return source && Number(source.getAttribute('size'));
+ },
+ set: function set(input) {
+ // Get sources
+ var sources = html5.getSources.call(player); // Get first match for requested size
- // 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 source = sources.find(function (source) {
+ return Number(source.getAttribute('size')) === input;
+ }); // No matching source found
- // Bail if no target
- if (!utils.is.element(element)) {
+ if (!source) {
return;
- }
-
- // Get state
- var pressed = element.getAttribute('aria-pressed') === 'true';
- var state = utils.is.boolean(input) ? input : !pressed;
+ } // Get current state
- // Set the attribute on target
- element.setAttribute('aria-pressed', state);
- },
-
-
- // Format string
- format: function format(input) {
- for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
- args[_key - 1] = arguments[_key];
- }
-
- if (utils.is.empty(input)) {
- return input;
- }
-
- return input.toString().replace(/{(\d+)}/g, function (match, i) {
- return utils.is.string(args[i]) ? args[i] : '';
- });
- },
+ var _player$media = player.media,
+ currentTime = _player$media.currentTime,
+ paused = _player$media.paused,
+ preload = _player$media.preload,
+ readyState = _player$media.readyState; // Set new source
- // Get percentage
- getPercentage: function getPercentage(current, max) {
- if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
- return 0;
- }
+ player.media.src = source.getAttribute('src'); // Prevent loading if preload="none" and the current source isn't loaded (#1044)
- return (current / max * 100).toFixed(2);
- },
+ if (preload !== 'none' || readyState) {
+ // Restore time
+ player.once('loadedmetadata', function () {
+ player.currentTime = currentTime; // Resume playing
+ if (!paused) {
+ player.play();
+ }
+ }); // Load new source
- // 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);
- },
+ player.media.load();
+ } // Trigger change event
- // 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);
+ triggerEvent.call(player, player.media, 'qualitychange', false, {
+ quality: input
+ });
}
-
- // Format time component to add leading zero
- var format = function format(value) {
- return ('0' + value).slice(-2);
- };
-
- // Breakdown to hours, mins, secs
- var hours = this.getHours(time);
- var mins = this.getMinutes(time);
- var secs = this.getSeconds(time);
-
- // Do we need to display hours?
- if (displayHours || hours > 0) {
- hours = hours + ':';
- } else {
- hours = '';
- }
-
- // Render
- return '' + (inverted ? '-' : '') + hours + format(mins) + ':' + format(secs);
- },
-
-
- // Replace all occurances of a string in a string
- replaceAll: function replaceAll() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
- var find = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
- var replace = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
-
- return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
+ });
},
+ // Cancel current network requests
+ // See https://github.com/sampotts/plyr/issues/174
+ cancelRequests: function cancelRequests() {
+ if (!this.isHTML5) {
+ return;
+ } // Remove child sources
- // Convert to title case
- toTitleCase: function toTitleCase() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ 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
- return input.toString().replace(/\w\S*/g, function (text) {
- return text.charAt(0).toUpperCase() + text.substr(1).toLowerCase();
- });
- },
+ 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
- // Convert string to pascalCase
- toPascalCase: function toPascalCase() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ this.debug.log('Cancelled network requests');
+ }
+ };
- var string = input.toString();
+ // ==========================================================================
- // Convert kebab case
- string = utils.replaceAll(string, '-', ' ');
+ function dedupe(array) {
+ if (!is.array(array)) {
+ return array;
+ }
- // Convert snake case
- string = utils.replaceAll(string, '_', ' ');
+ return array.filter(function (item, index) {
+ return array.indexOf(item) === index;
+ });
+ } // Get the closest value in an array
- // Convert to title case
- string = utils.toTitleCase(string);
+ function closest(array, value) {
+ if (!is.array(array) || !array.length) {
+ return null;
+ }
- // Convert to pascal case
- return utils.replaceAll(string, ' ', '');
- },
+ return array.reduce(function (prev, curr) {
+ return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
+ });
+ }
+ function cloneDeep(object) {
+ return JSON.parse(JSON.stringify(object));
+ } // Get a nested value in an object
- // Convert string to pascalCase
- toCamelCase: function toCamelCase() {
- var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ function getDeep(object, path) {
+ return path.split('.').reduce(function (obj, key) {
+ return obj && obj[key];
+ }, object);
+ } // Deep extend destination object with N more objects
- var string = input.toString();
+ function extend() {
+ var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
- // Convert to pascal case
- string = utils.toPascalCase(string);
+ for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ sources[_key - 1] = arguments[_key];
+ }
- // Convert first character to lowercase
- return string.charAt(0).toLowerCase() + string.slice(1);
- },
+ if (!sources.length) {
+ return target;
+ }
+ var source = sources.shift();
- // Deep extend destination object with N more objects
- extend: function extend() {
- var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+ if (!is.object(source)) {
+ return target;
+ }
- for (var _len2 = arguments.length, sources = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
- sources[_key2 - 1] = arguments[_key2];
+ Object.keys(source).forEach(function (key) {
+ if (is.object(source[key])) {
+ if (!Object.keys(target).includes(key)) {
+ Object.assign(target, _defineProperty({}, key, {}));
}
- if (!sources.length) {
- return target;
- }
+ extend(target[key], source[key]);
+ } else {
+ Object.assign(target, _defineProperty({}, key, source[key]));
+ }
+ });
+ return extend.apply(void 0, [target].concat(sources));
+ }
- var source = sources.shift();
+ // ==========================================================================
- if (!utils.is.object(source)) {
- return target;
- }
+ function generateId(prefix) {
+ return "".concat(prefix, "-").concat(Math.floor(Math.random() * 10000));
+ } // Format string
- Object.keys(source).forEach(function (key) {
- if (utils.is.object(source[key])) {
- if (!Object.keys(target).includes(key)) {
- Object.assign(target, defineProperty({}, key, {}));
- }
+ 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];
+ }
- utils.extend(target[key], source[key]);
- } else {
- Object.assign(target, defineProperty({}, key, source[key]));
- }
- });
+ if (is.empty(input)) {
+ return input;
+ }
- return utils.extend.apply(utils, [target].concat(toConsumableArray(sources)));
- },
+ return input.toString().replace(/{(\d+)}/g, function (match, i) {
+ return args[i].toString();
+ });
+ } // Get percentage
+ function getPercentage(current, max) {
+ if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
+ return 0;
+ }
- // Remove duplicates in an array
- dedupe: function dedupe(array) {
- if (!utils.is.array(array)) {
- return array;
- }
+ return (current / max * 100).toFixed(2);
+ } // Replace all occurances of a string in a string
- return array.filter(function (item, index) {
- return array.indexOf(item) === index;
- });
- },
+ 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
+ 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
- // Clone nested objects
- cloneDeep: function cloneDeep(object) {
- return JSON.parse(JSON.stringify(object));
- },
+ function toPascalCase() {
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var string = input.toString(); // Convert kebab case
+ string = replaceAll(string, '-', ' '); // Convert snake case
- // Get the closest value in an array
- closest: function closest(array, value) {
- if (!utils.is.array(array) || !array.length) {
- return null;
- }
+ string = replaceAll(string, '_', ' '); // Convert to title case
- return array.reduce(function (prev, curr) {
- return Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev;
- });
- },
+ string = toTitleCase(string); // Convert to pascal case
+ return replaceAll(string, ' ', '');
+ } // Convert string to pascalCase
- // Get the provider for a given URL
- getProviderByUrl: function getProviderByUrl(url) {
- // YouTube
- if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) {
- return providers.youtube;
- }
+ function toCamelCase() {
+ var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
+ var string = input.toString(); // Convert to pascal case
- // Vimeo
- if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) {
- return providers.vimeo;
- }
+ string = toPascalCase(string); // Convert first character to lowercase
- return null;
- },
+ return string.charAt(0).toLowerCase() + string.slice(1);
+ } // Remove HTML from a string
+ 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
- // Parse YouTube ID from URL
- parseYouTubeId: function parseYouTubeId(url) {
- if (utils.is.empty(url)) {
- return null;
- }
+ function getHTML(element) {
+ var wrapper = document.createElement('div');
+ wrapper.appendChild(element);
+ return wrapper.innerHTML;
+ }
- var regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
- return url.match(regex) ? RegExp.$2 : url;
- },
+ 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] : {};
+ if (is.empty(key) || is.empty(config)) {
+ return '';
+ }
- // Parse Vimeo ID from URL
- parseVimeoId: function parseVimeoId(url) {
- if (utils.is.empty(url)) {
- return null;
- }
+ var string = getDeep(config.i18n, key);
- if (utils.is.number(Number(url))) {
- return url;
+ if (is.empty(string)) {
+ if (Object.keys(resources).includes(key)) {
+ return resources[key];
}
- var regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
- return url.match(regex) ? RegExp.$2 : url;
- },
-
-
- // Convert a URL to a location object
- parseUrl: function parseUrl(url) {
- var parser = document.createElement('a');
- parser.href = url;
- return parser;
- },
+ return '';
+ }
+ 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;
+ }
+ };
- // Get URL query parameters
- getUrlParams: function getUrlParams(input) {
- var search = input;
+ var Storage =
+ /*#__PURE__*/
+ function () {
+ function Storage(player) {
+ _classCallCheck(this, Storage);
- // Parse URL if needed
- if (input.startsWith('http://') || input.startsWith('https://')) {
- var _parseUrl = this.parseUrl(input);
+ this.enabled = player.config.storage.enabled;
+ this.key = player.config.storage.key;
+ } // Check for actual support (see if we can use it)
- search = _parseUrl.search;
- }
- if (this.is.empty(search)) {
- return null;
+ _createClass(Storage, [{
+ key: "get",
+ value: function get(key) {
+ if (!Storage.supported || !this.enabled) {
+ return null;
}
- var hashes = search.slice(search.indexOf('?') + 1).split('&');
-
- return hashes.reduce(function (params, hash) {
- var _hash$split = hash.split('='),
- _hash$split2 = slicedToArray(_hash$split, 2),
- key = _hash$split2[0],
- val = _hash$split2[1];
+ var store = window.localStorage.getItem(this.key);
- return Object.assign(params, defineProperty({}, key, decodeURIComponent(val)));
- }, {});
- },
-
-
- // Convert object to URL parameters
- buildUrlParams: function buildUrlParams(input) {
- if (!utils.is.object(input)) {
- return '';
+ if (is.empty(store)) {
+ return null;
}
- return Object.keys(input).map(function (key) {
- return encodeURIComponent(key) + '=' + encodeURIComponent(input[key]);
- }).join('&');
- },
-
-
- // Remove HTML from a string
- stripHTML: function stripHTML(source) {
- var fragment = document.createDocumentFragment();
- var element = document.createElement('div');
- fragment.appendChild(element);
- element.innerHTML = source;
- return fragment.firstChild.innerText;
- },
-
-
- // Get aspect ratio for dimensions
- getAspectRatio: function getAspectRatio(width, height) {
- var getRatio = function getRatio(w, h) {
- return h === 0 ? w : getRatio(h, w % h);
- };
- var ratio = getRatio(width, height);
- return width / ratio + ':' + height / ratio;
- },
-
-
- // Get the transition end event
- get transitionEndEvent() {
- var element = document.createElement('span');
-
- var events = {
- WebkitTransition: 'webkitTransitionEnd',
- MozTransition: 'transitionend',
- OTransition: 'oTransitionEnd otransitionend',
- transition: 'transitionend'
- };
-
- var type = Object.keys(events).find(function (event) {
- return element.style[event] !== undefined;
- });
-
- return utils.is.string(type) ? events[type] : false;
- },
-
- // Force repaint of element
- repaint: function repaint(element) {
- setTimeout(function () {
- utils.toggleHidden(element, true);
- element.offsetHeight; // eslint-disable-line
- utils.toggleHidden(element, false);
- }, 0);
- }
-};
-
-// ==========================================================================
-
-// Check for feature support
-var support = {
- // Basic support
- audio: 'canPlayType' in document.createElement('audio'),
- video: 'canPlayType' in document.createElement('video'),
-
- // Check for support
- // Basic functionality vs full UI
- check: function check(type, provider, playsinline) {
- var api = false;
- var ui = false;
- var browser = utils.getBrowser();
- var canPlayInline = browser.isIPhone && playsinline && support.playsinline;
-
- switch (provider + ':' + type) {
- case 'html5:video':
- api = support.video;
- ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline);
- break;
-
- case 'html5:audio':
- api = support.audio;
- ui = api && support.rangeInput;
- break;
-
- case 'youtube:video':
- case 'vimeo:video':
- api = true;
- ui = support.rangeInput && (!browser.isIPhone || canPlayInline);
- break;
+ 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
- default:
- api = support.audio && support.video;
- ui = api && support.rangeInput;
- }
- return {
- api: api,
- ui: ui
- };
- },
+ if (!is.object(object)) {
+ return;
+ } // Get current storage
- // Picture-in-picture support
- // Safari only currently
- pip: function () {
- var browser = utils.getBrowser();
- return !browser.isIPhone && utils.is.function(utils.createElement('video').webkitSetPresentationMode);
- }(),
-
- // Airplay support
- // Safari only currently
- airplay: utils.is.function(window.WebKitPlaybackTargetAvailabilityEvent),
+ var storage = this.get(); // Default to empty object
- // Inline playback support
- // https://webkit.org/blog/6784/new-video-policies-for-ios/
- playsinline: 'playsInline' in document.createElement('video'),
+ if (is.empty(storage)) {
+ storage = {};
+ } // Update the working copy of the values
- // 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;
+ extend(storage, object); // Update storage
+ window.localStorage.setItem(this.key, JSON.stringify(storage));
+ }
+ }], [{
+ key: "supported",
+ get: function get() {
try {
- // Bail if no checking function
- if (!this.isHTML5 || !utils.is.function(media.canPlayType)) {
- return false;
- }
-
- // Check directly if codecs specified
- if (type.includes('codecs=')) {
- return media.canPlayType(type).replace(/no/, '');
- }
-
- // Type specific checks
- if (this.isVideo) {
- switch (type) {
- case 'video/webm':
- return media.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/no/, '');
-
- case 'video/mp4':
- return media.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '');
-
- case 'video/ogg':
- return media.canPlayType('video/ogg; codecs="theora"').replace(/no/, '');
-
- default:
- return false;
- }
- } else if (this.isAudio) {
- switch (type) {
- case 'audio/mpeg':
- return media.canPlayType('audio/mpeg;').replace(/no/, '');
-
- case 'audio/ogg':
- return media.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
-
- case 'audio/wav':
- return media.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
-
- default:
- return false;
- }
- }
- } catch (e) {
+ if (!('localStorage' in window)) {
return false;
- }
+ }
- // If we got this far, we're stuffed
- return false;
- },
-
-
- // Check for textTracks support
- textTracks: 'textTracks' in document.createElement('video'),
+ 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
- // 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);
- window.removeEventListener('test', null, options);
+ window.localStorage.setItem(test, test);
+ window.localStorage.removeItem(test);
+ return true;
} catch (e) {
- // Do nothing
- }
-
- return supported;
- }(),
-
- // <input type="range"> Sliders
- rangeInput: function () {
- var range = document.createElement('input');
- range.type = 'range';
- return range.type === 'range';
- }(),
-
- // Touch
- // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
- touch: 'ontouchstart' in document.documentElement,
-
- // Detect transitions support
- transitions: utils.transitionEndEvent !== false,
-
- // Reduced motion iOS & MacOS setting
- // https://webkit.org/blog/7551/responsive-design-for-motion/
- reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches
-};
-
-// ==========================================================================
-
-var html5 = {
- getSources: function getSources() {
- if (!this.isHTML5) {
- return null;
- }
-
- return this.media.querySelectorAll('source');
- },
-
-
- // Get quality levels
- getQualityOptions: function getQualityOptions() {
- if (!this.isHTML5) {
- return null;
+ return false;
}
+ }
+ }]);
- // Get sources
- var sources = html5.getSources.call(this);
+ 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 (utils.is.empty(sources)) {
- return null;
+ if (!('withCredentials' in request)) {
+ return;
}
- // Get <source> with size attribute
- var sizes = Array.from(sources).filter(function (source) {
- return !utils.is.empty(source.getAttribute('size'));
+ 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
- // If none, bail
- if (utils.is.empty(sizes)) {
- return null;
- }
-
- // Reduce to unique list
- return utils.dedupe(sizes.map(function (source) {
- return Number(source.getAttribute('size'));
- }));
- },
- extend: function extend() {
- if (!this.isHTML5) {
- return;
- }
-
- var player = this;
-
- // Quality
- Object.defineProperty(player.media, 'quality', {
- get: function get() {
- // Get sources
- var sources = html5.getSources.call(player);
-
- if (utils.is.empty(sources)) {
- return null;
- }
-
- var matches = Array.from(sources).filter(function (source) {
- return source.getAttribute('src') === player.source;
- });
-
- if (utils.is.empty(matches)) {
- return null;
- }
-
- return Number(matches[0].getAttribute('size'));
- },
- set: function set(input) {
- // Get sources
- var sources = html5.getSources.call(player);
+ request.responseType = responseType;
+ request.send();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ }
- if (utils.is.empty(sources)) {
- return;
- }
+ // ==========================================================================
- // Get matches for requested size
- var matches = Array.from(sources).filter(function (source) {
- return Number(source.getAttribute('size')) === input;
- });
+ function loadSprite(url, id) {
+ if (!is.string(url)) {
+ return;
+ }
- // No matches for requested size
- if (utils.is.empty(matches)) {
- return;
- }
+ var prefix = 'cache';
+ var hasId = is.string(id);
+ var isCached = false;
- // Get supported sources
- var supported = matches.filter(function (source) {
- return support.mime.call(player, source.getAttribute('type'));
- });
+ var exists = function exists() {
+ return document.getElementById(id) !== null;
+ };
- // No supported sources
- if (utils.is.empty(supported)) {
- return;
- }
+ var update = function update(container, data) {
+ container.innerHTML = data; // Check again incase of race condition
- // Trigger change event
- utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
- quality: input
- });
+ if (hasId && exists()) {
+ return;
+ } // Inject the SVG to the body
- // Get current state
- var currentTime = player.currentTime,
- playing = player.playing;
- // Set new source
+ document.body.insertAdjacentElement('afterbegin', container);
+ }; // Only load once if ID set
- player.media.src = supported[0].getAttribute('src');
- // Load new source
- player.media.load();
+ if (!hasId || !exists()) {
+ var useStorage = Storage.supported; // Create container
- // Resume playing
- if (playing) {
- player.play();
- }
+ var container = document.createElement('div');
+ container.setAttribute('hidden', '');
- // Restore time
- player.currentTime = currentTime;
+ if (hasId) {
+ container.setAttribute('id', id);
+ } // Check in cache
- // Trigger change event
- utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
- quality: input
- });
- }
- });
- },
+ if (useStorage) {
+ var cached = window.localStorage.getItem("".concat(prefix, "-").concat(id));
+ isCached = cached !== null;
- // Cancel current network requests
- // See https://github.com/sampotts/plyr/issues/174
- cancelRequests: function cancelRequests() {
- if (!this.isHTML5) {
- return;
+ if (isCached) {
+ var data = JSON.parse(cached);
+ update(container, data.content);
}
+ } // Get the sprite
- // 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);
+ fetch(url).then(function (result) {
+ if (is.empty(result)) {
+ return;
+ }
- // Load the new empty source
- // This will cancel existing requests
- // See https://github.com/sampotts/plyr/issues/174
- this.media.load();
+ if (useStorage) {
+ window.localStorage.setItem("".concat(prefix, "-").concat(id), JSON.stringify({
+ content: result
+ }));
+ }
- // Debugging
- this.debug.log('Cancelled network requests');
+ update(container, result);
+ }).catch(function () {});
}
-};
+ }
-// ==========================================================================
+ // ==========================================================================
-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] : {};
+ var getHours = function getHours(value) {
+ return parseInt(value / 60 / 60 % 60, 10);
+ };
+ var getMinutes = function getMinutes(value) {
+ return parseInt(value / 60 % 60, 10);
+ };
+ var getSeconds = function getSeconds(value) {
+ return parseInt(value % 60, 10);
+ }; // Format time to UI friendly string
- if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
- return '';
- }
+ 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;
- var string = config.i18n[key];
+ // Bail if the value isn't a number
+ if (!is.number(time)) {
+ return formatTime(null, displayHours, inverted);
+ } // Format time component to add leading zero
- 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];
+ var format = function format(value) {
+ return "0".concat(value).slice(-2);
+ }; // Breakdown to hours, mins, secs
- string = utils.replaceAll(string, key, value);
- });
- return string;
- }
-};
+ var hours = getHours(time);
+ var mins = getMinutes(time);
+ var secs = getSeconds(time); // Do we need to display hours?
-// ==========================================================================
+ if (displayHours || hours > 0) {
+ hours = "".concat(hours, ":");
+ } else {
+ hours = '';
+ } // Render
-// Sniff out the browser
-var browser = utils.getBrowser();
-var controls = {
+ 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
- };
+ 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 and store references in custom controls
- // TODO: Allow settings menus with custom controls
+ // Find the UI controls
findElements: function findElements() {
- try {
- this.elements.controls = utils.getElement.call(this, this.config.selectors.controls.wrapper);
-
- // Buttons
- this.elements.buttons = {
- play: utils.getElements.call(this, this.config.selectors.buttons.play),
- pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
- restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
- rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
- fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
- mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
- pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
- airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
- settings: utils.getElement.call(this, this.config.selectors.buttons.settings),
- captions: utils.getElement.call(this, this.config.selectors.buttons.captions),
- fullscreen: utils.getElement.call(this, this.config.selectors.buttons.fullscreen)
- };
-
- // Progress
- this.elements.progress = utils.getElement.call(this, this.config.selectors.progress);
-
- // Inputs
- this.elements.inputs = {
- seek: utils.getElement.call(this, this.config.selectors.inputs.seek),
- volume: utils.getElement.call(this, this.config.selectors.inputs.volume)
- };
-
- // Display
- this.elements.display = {
- buffer: utils.getElement.call(this, this.config.selectors.display.buffer),
- currentTime: utils.getElement.call(this, this.config.selectors.display.currentTime),
- duration: utils.getElement.call(this, this.config.selectors.display.duration)
- };
-
- // Seek tooltip
- if (utils.is.element(this.elements.progress)) {
- this.elements.display.seekTooltip = this.elements.progress.querySelector('.' + this.config.classNames.tooltip);
- }
-
- return true;
- } catch (error) {
- // Log it
- this.debug.warn('It looks like there is a problem with your custom controls HTML', error);
-
- // Restore native video controls
- this.toggleNativeControls(true);
-
- return false;
- }
+ 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 = (!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'
- }));
+ 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>
- // Create the <use> to reference sprite
- var use = document.createElementNS(namespace, 'use');
- var path = iconPath + '-' + type;
+ var icon = document.createElementNS(namespace, 'svg');
+ setAttributes(icon, extend(attributes, {
+ role: 'presentation',
+ focusable: 'false'
+ })); // Create the <use> to reference sprite
- // 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);
- }
+ 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
- // Add <use> to <svg>
- icon.appendChild(use);
+ 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)
- return icon;
- },
+ 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(type, attr) {
- var text = i18n.get(type, this.config);
- var attributes = Object.assign({}, attr);
-
- switch (type) {
- case 'pip':
- text = 'PIP';
- break;
-
- case 'airplay':
- text = 'AirPlay';
- break;
-
- default:
- break;
- }
-
- if ('class' in attributes) {
- attributes.class += ' ' + this.config.classNames.hidden;
- } else {
- attributes.class = this.config.classNames.hidden;
- }
-
- return utils.createElement('span', attributes, text);
+ 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 (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));
+ if (is.empty(text)) {
+ return null;
+ }
- return badge;
+ 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 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';
- }
+ 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;
- 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;
- }
-
- // Large play button
- switch (buttonType) {
- case 'play':
- toggle = true;
- label = 'play';
- labelPressed = 'pause';
- icon = 'play';
- iconPressed = 'pause';
- break;
+ default:
+ if (is.empty(props.label)) {
+ props.label = type;
+ }
- case 'mute':
- toggle = true;
- label = 'mute';
- labelPressed = 'unmute';
- icon = 'volume';
- iconPressed = 'muted';
- break;
+ if (is.empty(props.icon)) {
+ props.icon = buttonType;
+ }
- 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;
+ var button = createElement(props.element); // Setup toggle icon and labels
- case 'play-large':
- attributes.class += ' ' + this.config.classNames.control + '--overlaid';
- type = 'play';
- label = 'play';
- icon = 'play';
- break;
+ 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
- default:
- label = type;
- icon = buttonType;
- }
+ 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
- // 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' }));
- // Label/Tooltip
- button.appendChild(controls.createLabel.call(this, labelPressed, { class: 'label--pressed' }));
- button.appendChild(controls.createLabel.call(this, label, { class: 'label--not-pressed' }));
+ extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
+ setAttributes(button, attributes); // We have multiple play buttons
- // Add aria attributes
- attributes['aria-pressed'] = false;
- } else {
- button.appendChild(controls.createIcon.call(this, icon));
- button.appendChild(controls.createLabel.call(this, label));
+ if (type === 'play') {
+ if (!is.array(this.elements.buttons[type])) {
+ this.elements.buttons[type] = [];
}
- // Merge attributes
- utils.extend(attributes, utils.getAttributesFromSelector(this.config.selectors.buttons[type], attributes));
-
- utils.setAttributes(button, attributes);
-
- // We have multiple play buttons
- if (type === 'play') {
- if (!utils.is.array(this.elements.buttons[type])) {
- this.elements.buttons[type] = [];
- }
-
- this.elements.buttons[type].push(button);
- } else {
- this.elements.buttons[type] = button;
- }
+ this.elements.buttons[type].push(button);
+ } else {
+ this.elements.buttons[type] = button;
+ }
- return button;
+ return button;
},
-
-
// 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);
-
- return {
- label: label,
- input: input
- };
+ // 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 = utils.createElement('progress', utils.extend(utils.getAttributesFromSelector(this.config.selectors.display[type]), {
- min: 0,
- max: 100,
- value: 0,
- role: 'presentation',
- 'aria-hidden': true
- }, 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());
+ }
- // Create the label inside
- if (type !== 'volume') {
- progress.appendChild(utils.createElement('span', null, '0'));
+ 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;
- var suffix = '';
- switch (type) {
- case 'played':
- suffix = i18n.get('played', this.config);
- break;
+ // 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
- case 'buffer':
- suffix = i18n.get('buffered', this.config);
- break;
- default:
- break;
- }
+ event.preventDefault();
+ event.stopPropagation(); // We're just here to prevent the keydown bubbling
- progress.innerText = '% ' + suffix.toLowerCase();
+ if (event.type === 'keydown') {
+ return;
}
- this.elements.display[type] = progress;
+ var isRadioButton = matches(menuItem, '[role="menuitemradio"]'); // Show the respective menu
- return progress;
- },
+ if (!isRadioButton && [32, 39].includes(event.which)) {
+ controls.showMenuPanel.call(_this, type, true);
+ } else {
+ var target;
+ if (event.which !== 32) {
+ if (event.which === 40 || isRadioButton && event.which === 39) {
+ target = menuItem.nextElementSibling;
- // Create time display
- createTime: function createTime(type) {
- var attributes = utils.getAttributesFromSelector(this.config.selectors.display[type]);
+ if (!is.element(target)) {
+ target = menuItem.parentNode.firstElementChild;
+ }
+ } else {
+ target = menuItem.previousElementSibling;
+
+ if (!is.element(target)) {
+ target = menuItem.parentNode.lastElementChild;
+ }
+ }
- var container = utils.createElement('div', utils.extend(attributes, {
- class: 'plyr__time ' + attributes.class,
- 'aria-label': i18n.get(type, this.config)
- }), '00:00');
+ 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
- // Reference for updates
- this.elements.display[type] = container;
+ on(menuItem, 'keyup', function (event) {
+ if (event.which !== 13) {
+ return;
+ }
- return container;
+ 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);
+ }
+ menuItem.appendChild(flex); // Replicate radio button behaviour
- // 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;
+ 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');
+ });
+ }
- var item = utils.createElement('li');
+ menuItem.setAttribute('aria-checked', checked ? 'true' : 'false');
+ }
+ });
+ this.listeners.bind(menuItem, 'click keyup', function (event) {
+ if (is.keyboardEvent(event) && event.which !== 32) {
+ return;
+ }
- var label = utils.createElement('label', {
- class: this.config.classNames.control
- });
+ event.preventDefault();
+ event.stopPropagation();
+ menuItem.checked = true;
- 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'
- }));
+ switch (type) {
+ case 'language':
+ _this2.currentTrack = Number(value);
+ break;
- var faux = utils.createElement('span', { hidden: '' });
+ case 'quality':
+ _this2.quality = value;
+ break;
- label.appendChild(radio);
- label.appendChild(faux);
- label.insertAdjacentHTML('beforeend', title);
+ case 'speed':
+ _this2.speed = parseFloat(value);
+ break;
- if (utils.is.element(badge)) {
- label.appendChild(badge);
+ default:
+ break;
}
- item.appendChild(label);
- list.appendChild(item);
+ 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
+ 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;
+ var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+ var time = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+ var inverted = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
- // Bail if there's no element to display or the value isn't a number
- if (!utils.is.element(target) || !utils.is.number(time)) {
- return;
- }
+ // 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
- // Always display hours if duration is over an hour
- var forceHours = utils.getHours(this.duration) > 0;
- // eslint-disable-next-line no-param-reassign
- target.innerText = utils.formatTime(time, forceHours, inverted);
+ target.innerText = controls.formatTime(time, inverted);
},
-
-
// Update volume UI and storage
updateVolume: function updateVolume() {
- if (!this.supported.ui) {
- return;
- }
+ if (!this.supported.ui) {
+ return;
+ } // Update range
- // Update range
- if (utils.is.element(this.elements.inputs.volume)) {
- controls.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);
- }
- },
+ if (is.element(this.elements.inputs.volume)) {
+ controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);
+ } // Update mute state
+ 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;
-
- if (!utils.is.element(target)) {
- return;
- }
+ var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
- // eslint-disable-next-line
- target.value = value;
+ if (!is.element(target)) {
+ return;
+ } // eslint-disable-next-line
- // Webkit range fill
- controls.updateRangeFill.call(this, target);
- },
+ target.value = value; // Webkit range fill
+ controls.updateRangeFill.call(this, target);
+ },
// Update <progress> elements
updateProgress: function updateProgress(event) {
- var _this = this;
+ var _this3 = this;
- if (!this.supported.ui || !utils.is.event(event)) {
- return;
- }
+ if (!this.supported.ui || !is.event(event)) {
+ return;
+ }
- var value = 0;
+ var value = 0;
- var setProgress = function setProgress(target, input) {
- var value = utils.is.number(input) ? input : 0;
- var progress = utils.is.element(target) ? target : _this.elements.display.buffer;
+ 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
- // Update value and label
- if (utils.is.element(progress)) {
- progress.value = value;
+ if (is.element(progress)) {
+ progress.value = value; // Update text label inside
- // Update text label inside
- var label = progress.getElementsByTagName('span')[0];
- if (utils.is.element(label)) {
- label.childNodes[0].nodeValue = value;
- }
- }
- };
+ var label = progress.getElementsByTagName('span')[0];
- if (event) {
- switch (event.type) {
- // Video playing
- case 'timeupdate':
- case 'seeking':
- case 'seeked':
- value = utils.getPercentage(this.currentTime, this.duration);
+ if (is.element(label)) {
+ label.childNodes[0].nodeValue = value;
+ }
+ }
+ };
- // 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);
- }
+ 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
- break;
+ if (event.type === 'timeupdate') {
+ controls.setRange.call(this, this.elements.inputs.seek, value);
+ }
- // Check buffer status
- case 'playing':
- case 'progress':
- setProgress(this.elements.display.buffer, this.buffered * 100);
+ break;
+ // Check buffer status
- break;
+ case 'playing':
+ case 'progress':
+ setProgress(this.elements.display.buffer, this.buffered * 100);
+ break;
- default:
- break;
- }
+ default:
+ break;
}
+ }
},
-
-
// Webkit polyfill for lower fill range
updateRangeFill: function updateRangeFill(target) {
- // Get range from event if event passed
- var range = utils.is.event(target) ? target.target : target;
-
- // Needs to be a valid <input type='range'>
- if (!utils.is.element(range) || range.getAttribute('type') !== 'range') {
- return;
- }
+ // Get range from event if event passed
+ var range = is.event(target) ? target.target : target; // Needs to be a valid <input type='range'>
- // Set aria value for https://github.com/sampotts/plyr/issues/905
+ 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
- // WebKit only
- if (!browser.isWebkit) {
- return;
- }
- // Set CSS custom property
- range.style.setProperty('--value', range.value / range.max * 100 + '%');
- },
+ if (!browser.isWebkit) {
+ return;
+ } // Set CSS custom property
+ range.style.setProperty('--value', "".concat(range.value / range.max * 100, "%"));
+ },
// Update hover tooltip for seeking
updateSeekTooltip: function updateSeekTooltip(event) {
- var _this2 = this;
+ var _this4 = this;
- // 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;
- }
+ // 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
- // Calculate percentage
- var percent = 0;
- var clientRect = this.elements.progress.getBoundingClientRect();
- var visible = this.config.classNames.tooltip + '--visible';
- var toggle = function toggle(_toggle) {
- utils.toggleClass(_this2.elements.display.seekTooltip, visible, _toggle);
- };
+ var percent = 0;
+ var clientRect = this.elements.progress.getBoundingClientRect();
+ var visible = "".concat(this.config.classNames.tooltip, "--visible");
- // Hide on touch
- if (this.touch) {
- toggle(false);
- return;
- }
+ var toggle = function toggle(_toggle) {
+ toggleClass(_this4.elements.display.seekTooltip, visible, _toggle);
+ }; // Hide on touch
- // 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;
- }
- // Set bounds
- if (percent < 0) {
- percent = 0;
- } else if (percent > 100) {
- percent = 100;
- }
+ if (this.touch) {
+ toggle(false);
+ return;
+ } // Determine percentage, if already visible
- // Display the time a click would seek to
- controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent);
- // Set position
- this.elements.display.seekTooltip.style.left = percent + '%';
+ 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
- // 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');
- }
- },
+ if (percent < 0) {
+ percent = 0;
+ } else if (percent > 100) {
+ percent = 100;
+ } // Display the time a click would seek to
- // Handle time change event
- timeUpdate: function timeUpdate(event) {
- // Only invert if only one time element is displayed and used for both duration and currentTime
- var invert = !utils.is.element(this.elements.display.duration) && this.config.invertTime;
- // Duration
- controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);
+ controls.updateTimeDisplay.call(this, this.elements.display.seekTooltip, this.duration / 100 * percent); // Set position
- // Ignore updates while seeking
- if (event && event.type === 'timeupdate' && this.media.seeking) {
- return;
- }
+ 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
- // Playing progress
- controls.updateProgress.call(this, event);
+ 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
+
+ controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); // Ignore updates while seeking
+
+ if (event && event.type === 'timeupdate' && this.media.seeking) {
+ return;
+ } // Playing progress
+ 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;
- }
+ // 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
- // If there's a spot to display duration
- var hasDuration = utils.is.element(this.elements.display.duration);
- // If there's only one time display, display duration there
- if (!hasDuration && this.config.displayDuration && this.paused) {
- controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);
- }
+ if (this.duration >= Math.pow(2, 32)) {
+ toggleHidden(this.elements.display.currentTime, true);
+ toggleHidden(this.elements.progress, true);
+ return;
+ } // Update ARIA values
- // If there's a duration element, update content
- if (hasDuration) {
- controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
- }
- // Update the tooltip (if visible)
- controls.updateSeekTooltip.call(this);
- },
+ if (is.element(this.elements.inputs.seek)) {
+ this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);
+ } // If there's a spot to display duration
- // Hide/show a tab
- toggleTab: function toggleTab(setting, toggle) {
- utils.toggleHidden(this.elements.settings.tabs[setting], !toggle);
- },
+ var hasDuration = is.element(this.elements.display.duration); // If there's only one time display, display duration there
+ 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 the quality menu
- // TODO: Vimeo support
- setQualityMenu: function setQualityMenu(options) {
- var _this3 = this;
- // Menu required
- if (!utils.is.element(this.elements.settings.panes.quality)) {
- return;
- }
+ if (hasDuration) {
+ controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);
+ } // Update the tooltip (if visible)
- 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 _this3.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);
+ controls.updateSeekTooltip.call(this);
+ },
+ // Hide/show a tab
+ 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;
- // Check if we need to toggle the parent
- controls.checkMenu.call(this);
+ if (setting === 'captions') {
+ value = this.currentTrack;
+ } else {
+ value = !is.empty(input) ? input : this[setting]; // Get default
- // If we're hiding, nothing more to do
- if (!toggle) {
- return;
- }
+ if (is.empty(value)) {
+ value = this.config[setting].default;
+ } // Unsupported value
- // Empty the menu
- utils.emptyElement(list);
- // Get the badge HTML for HD, 4K etc
- var getBadge = function getBadge(quality) {
- var label = '';
+ 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
- switch (quality) {
- case 2160:
- label = '4K';
- break;
- case 1440:
- case 1080:
- case 720:
- label = 'HD';
- break;
+ 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
- case 576:
- case 480:
- label = 'SD';
- break;
- default:
- break;
- }
+ if (!is.element(list)) {
+ list = pane && pane.querySelector('[role="menu"]');
+ } // If there's no list it means it's not been rendered...
- if (!label.length) {
- return null;
- }
- return controls.createBadge.call(_this3, label);
- };
+ if (!is.element(list)) {
+ return;
+ } // Update the label
- // Sort options by the config and then render options
- this.options.quality.sort(function (a, b) {
- var sorting = _this3.config.quality.options;
- return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
- }).forEach(function (quality) {
- var label = controls.getLabel.call(_this3, 'quality', quality);
- controls.createMenuItem.call(_this3, quality, list, type, label, getBadge(quality));
- });
- controls.updateSetting.call(this, type, list);
- },
+ 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
+ var target = list && list.querySelector("[value=\"".concat(value, "\"]"));
+ 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,
@@ -2617,5551 +2106,5650 @@ 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 _this4 = this;
+ var _this6 = this;
- // TODO: Captions or language? Currently it's mixed
- var type = 'captions';
- var list = this.elements.settings.panes.captions.querySelector('ul');
-
- // 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(_this4, 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(_this4, track.language, list, 'language', track.label, track.language !== 'enabled' ? controls.createBadge.call(_this4, track.language.toUpperCase()) : null, track.language.toLowerCase() === _this4.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 _this5 = 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 _this5.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(_this5, 'speed', speed);
- controls.createMenuItem.call(_this5, 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 _this6 = 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
+
+ if (this.config.controls.includes('mute')) {
+ volume.appendChild(controls.createButton.call(this, 'mute'));
+ } // Volume range control
- container.appendChild(volume);
- }
- // 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(_this6.config.selectors.buttons.settings), {
- type: 'button',
- class: _this6.config.classNames.control + ' ' + _this6.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, _this6.config));
-
- var value = utils.createElement('span', {
- class: _this6.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);
- _this6.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: _this6.config.classNames.control + ' ' + _this6.config.classNames.control + '--back',
- 'aria-haspopup': true,
- 'aria-controls': 'plyr-settings-' + data.id + '-home',
- 'aria-expanded': false
- }, i18n.get(type, _this6.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);
- _this6.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 _this7 = 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, _this7.config.classNames.hidden, false);
- utils.toggleClass(label, _this7.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 redraw
- 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 (window.navigator.userAgent.includes('Edge')) {
+ 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);
+ });
+ }
+ }
+ };
+
+ /**
+ * Parse a string to a URL object
+ * @param {string} input - the URL to be parsed
+ * @param {boolean} safe - failsafe parsing
+ */
- // Get browser info
- var browser = utils.getBrowser();
+ function parseUrl(input) {
+ var safe = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
+ var url = input;
- // 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');
+ if (safe) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ url = parser.href;
+ }
- Array.from(elements).forEach(function (track) {
- var src = track.getAttribute('src');
- var href = utils.parseUrl(src);
+ try {
+ return new URL(url);
+ } catch (e) {
+ return null;
+ }
+ } // Convert object to URLSearchParams
- 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);
- });
- }
- });
- }
+ function buildUrlParams(input) {
+ var params = new URLSearchParams();
+
+ if (is.object(input)) {
+ Object.entries(input).forEach(function (_ref) {
+ var _ref2 = _slicedToArray(_ref, 2),
+ key = _ref2[0],
+ value = _ref2[1];
- // Set language
- captions.setLanguage.call(this);
+ params.set(key, value);
+ });
+ }
- // Enable UI
- captions.show.call(this);
+ return params;
+ }
- // 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);
+ var captions = {
+ // Setup captions
+ setup: function setup() {
+ // Requires UI support
+ if (!this.supported.ui) {
+ return;
+ } // Only Vimeo and HTML5 video supported at this point
+
+
+ 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!)
+
+
+ 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);
- // Turn off native caption rendering to avoid double captions
- // eslint-disable-next-line
- track.mode = 'hidden';
+ 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.innerText = 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');
+ var language = input.toLowerCase();
+ this.captions.language = language; // Set currentTrack
- // Otherwise fall back to the default config
- if (!utils.is.boolean(active)) {
- active = this.config.captions.active;
- } else {
- this.captions.active = active;
+ 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
+
+ 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
-// ==========================================================================
-// Plyr default config
-// ==========================================================================
-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.7/plyr.svg',
-
+ iconUrl: 'https://cdn.plyr.io/3.4.7/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 for vintage browsers
+ 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'
- },
- 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',
- posterEnabled: 'plyr__poster-enabled',
- ads: 'plyr__ads',
- control: 'plyr__control',
- 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',
- 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'
},
-
// 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: ''
}
-};
+ };
-// ==========================================================================
+ // ==========================================================================
+ // Plyr states
+ // ==========================================================================
+ var pip = {
+ active: 'picture-in-picture',
+ inactive: 'inline'
+ };
-var browser$1 = 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|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.call(this.player, 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
- // Trap focus in container
- if (!browser$1.isIos) {
- utils.trapFocus.call(this.player, this.target, this.active);
+
+ var button = this.player.elements.buttons.fullscreen;
+
+ if (is.element(button)) {
+ button.pressed = this.active;
+ } // Trigger an event
+
+
+ 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
+ }; // Register event listeners
+ // Handle event (incase user presses escape etc)
- // Make an element fullscreen
+ 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
- }, {
- key: 'enter',
- value: function enter() {
- if (!this.enabled) {
- return;
- }
-
- // iOS native fullscreen doesn't need the request step
- if (browser$1.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$1.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) {
+ this.player.debug.log("".concat(Fullscreen.native ? 'Native' : 'Fallback', " fullscreen enabled"));
+ } else {
+ this.player.debug.log('Fullscreen not supported and fallback disabled');
+ } // Add styling hook to show button
+
+
+ toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.enabled);
+ } // Make an element fullscreen
}, {
- key: 'toggle',
- value: function toggle() {
- if (!this.active) {
- this.enter();
- } else {
- this.exit();
- }
- }
- }, {
- key: 'enabled',
+ key: "enter",
+ value: function enter() {
+ if (!this.enabled) {
+ return;
+ } // iOS native fullscreen doesn't need the request step
- // 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;
+ if (browser.isIos && this.player.config.fullscreen.iosNative) {
+ this.target.webkitEnterFullscreen();
+ } else if (!Fullscreen.native) {
+ 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 active state
+ }, {
+ key: "exit",
+ value: function exit() {
+ if (!this.enabled) {
+ return;
+ } // iOS native fullscreen
+
+
+ if (browser.isIos && this.player.config.fullscreen.iosNative) {
+ this.target.webkitExitFullscreen();
+ this.player.play();
+ } else if (!Fullscreen.native) {
+ toggleFallback.call(this, false);
+ } else if (!this.prefix) {
+ (document.cancelFullScreen || document.exitFullscreen).call(document);
+ } else if (!is.empty(this.prefix)) {
+ var action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
+ document["".concat(this.prefix).concat(action).concat(this.property)]();
+ }
+ } // Toggle state
}, {
- key: 'active',
- get: function get$$1() {
- if (!this.enabled) {
- return false;
- }
+ key: "toggle",
+ value: function toggle() {
+ if (!this.active) {
+ this.enter();
+ } else {
+ this.exit();
+ }
+ }
+ }, {
+ 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
- // Fallback using classname
- if (!Fullscreen.native) {
- return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
- }
+ }, {
+ key: "active",
+ get: function get() {
+ if (!this.enabled) {
+ return false;
+ } // Fallback using classname
- var element = !this.prefix ? document.fullscreenElement : document['' + this.prefix + this.property + 'Element'];
- return element === this.target;
+ if (!Fullscreen.native) {
+ return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
}
- // Get target element
+ var element = !this.prefix ? document.fullscreenElement : document["".concat(this.prefix).concat(this.property, "Element")];
+ return element === this.target;
+ } // Get target element
}, {
- key: 'target',
- get: function get$$1() {
- return browser$1.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.container;
- }
+ 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$$1() {
- return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
- }
-
- // Get the prefix for handlers
+ key: "native",
+ get: function get() {
+ return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
+ } // Get the prefix for handlers
}, {
- key: 'prefix',
- get: function get$$1() {
- // No prefix
- if (utils.is.function(document.exitFullscreen)) {
- return '';
- }
-
- // Check for fullscreen support by vendor prefix
- var value = '';
- var prefixes = ['webkit', 'moz', 'ms'];
-
- prefixes.some(function (pre) {
- if (utils.is.function(document[pre + 'ExitFullscreen']) || utils.is.function(document[pre + 'CancelFullScreen'])) {
- value = pre;
- return true;
- }
-
- return false;
- });
+ 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 value;
- }
+ return false;
+ });
+ return value;
+ }
}, {
- key: 'property',
- get: function get$$1() {
- return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
- }
+ key: "property",
+ get: function get() {
+ return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
+ }
}]);
- return Fullscreen;
-}();
-
-// ==========================================================================
-// Sniff out the browser
-var browser$2 = utils.getBrowser();
+ 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 = {
+ // ==========================================================================
+ 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);
+ 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;
+ var toggle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
- if (toggle && this.isHTML5) {
- this.media.setAttribute('controls', '');
- } else {
- this.media.removeAttribute('controls');
- }
+ if (toggle && this.isHTML5) {
+ this.media.setAttribute('controls', '');
+ } else {
+ this.media.removeAttribute('controls');
+ }
},
-
-
// Setup the UI
build: function build() {
- var _this = this;
+ var _this = this;
- // Re-attach media element listeners
- // TODO: Use event bubbling?
- this.listeners.media();
+ // Re-attach media element listeners
+ // TODO: Use event bubbling?
+ this.listeners.media(); // Don't setup interface if no support
- // Don't setup interface if no support
- if (!this.supported.ui) {
- this.debug.warn('Basic support only for ' + this.provider + ' ' + this.type);
+ if (!this.supported.ui) {
+ this.debug.warn("Basic support only for ".concat(this.provider, " ").concat(this.type)); // Restore native controls
- // Restore native controls
- ui.toggleNativeControls.call(this, true);
+ ui.toggleNativeControls.call(this, true); // Bail
- // Bail
- return;
- }
+ return;
+ } // Inject custom controls if not present
- // Inject custom controls if not present
- if (!utils.is.element(this.elements.controls)) {
- // Inject custom controls
- controls.inject.call(this);
- // Re-attach control listeners
- this.listeners.controls();
- }
+ if (!is.element(this.elements.controls)) {
+ // Inject custom controls
+ controls.inject.call(this); // Re-attach control listeners
- // Remove native controls
- ui.toggleNativeControls.call(this);
+ this.listeners.controls();
+ } // Remove native controls
- // Captions
+
+ ui.toggleNativeControls.call(this); // Setup captions for HTML5
+
+ if (this.isHTML5) {
captions.setup.call(this);
+ } // Reset volume
- // Reset volume
- this.volume = null;
- // Reset mute state
- this.muted = null;
+ this.volume = null; // Reset mute state
- // Reset speed
- this.speed = null;
+ this.muted = null; // Reset speed
- // Reset loop state
- this.loop = null;
+ this.speed = null; // Reset loop state
- // Reset quality setting
- this.quality = null;
+ this.loop = null; // Reset quality setting
- // Reset volume display
- controls.updateVolume.call(this);
+ this.quality = null; // Reset volume display
- // Reset time display
- controls.timeUpdate.call(this);
+ controls.updateVolume.call(this); // Reset time display
- // Update the UI
- ui.checkPlaying.call(this);
+ controls.timeUpdate.call(this); // Update the UI
- // Check for picture-in-picture support
- utils.toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);
+ ui.checkPlaying.call(this); // Check for picture-in-picture support
- // Check for airplay support
- utils.toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);
+ toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); // Check for airplay support
- // Add iOS class
- utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser$2.isIos);
+ toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); // Add iOS class
- // Add touch class
- utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
+ toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); // Add touch class
- // Ready for API calls
- this.ready = true;
+ toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); // Ready for API calls
- // Ready event at end of execution stack
- setTimeout(function () {
- utils.dispatchEvent.call(_this, _this.media, 'ready');
- }, 0);
+ this.ready = true; // Ready event at end of execution stack
- // Set the title
- ui.setTitle.call(this);
+ setTimeout(function () {
+ triggerEvent.call(_this, _this.media, 'ready');
+ }, 0); // Set the title
- // Assure the poster image is set, if the property was added before the element was created
- if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
- ui.setPoster.call(this, this.poster);
- }
- },
+ 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);
+ // Find the current text
+ var label = i18n.get('play', this.config); // If there's a media title set, use that for the label
- // 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;
+ if (is.string(this.config.title) && !is.empty(this.config.title)) {
+ label += ", ".concat(this.config.title);
+ } // If there's a play button, set label
- // Set container label
- this.elements.container.setAttribute('aria-label', this.config.title);
- }
- // If there's a play button, set label
- if (utils.is.nodeList(this.elements.buttons.play)) {
- Array.from(this.elements.buttons.play).forEach(function (button) {
- button.setAttribute('aria-label', label);
- });
- }
+ Array.from(this.elements.buttons.play || []).forEach(function (button) {
+ button.setAttribute('aria-label', label);
+ }); // Set iframe title
+ // https://github.com/sampotts/plyr/issues/124
- // Set iframe title
- // https://github.com/sampotts/plyr/issues/124
- if (this.isEmbed) {
- var iframe = utils.getElement.call(this, 'iframe');
+ if (this.isEmbed) {
+ var iframe = getElement.call(this, 'iframe');
- if (!utils.is.element(iframe)) {
- return;
- }
+ if (!is.element(iframe)) {
+ return;
+ } // Default to media type
- // Default to media type
- var title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
- var format = i18n.get('frameTitle', this.config);
- iframe.setAttribute('title', format.replace('{title}', title));
- }
+ 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) {
- utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, 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 _this2 = this;
- // Set property regardless of validity
- this.media.setAttribute('poster', poster);
+ var passive = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
- // Bail if element is missing
- if (!utils.is.element(this.elements.poster)) {
- return Promise.reject();
- }
+ // 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
- // Load the image, and set poster if successful
- var loadPromise = utils.loadImage(poster).then(function () {
- _this2.elements.poster.style.backgroundImage = 'url(\'' + poster + '\')';
- Object.assign(_this2.elements.poster.style, {
- backgroundImage: 'url(\'' + 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;
- });
- // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video)
- loadPromise.catch(function () {
- return ui.togglePoster.call(_this2, false);
- });
+ this.media.setAttribute('poster', poster); // Wait until ui is ready
- // Return the promise so the caller can use it as well
- return loadPromise;
- },
+ 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) {
- // 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);
+ var _this3 = this;
- // Set ARIA state
- utils.toggleState(this.elements.buttons.play, this.playing);
+ // 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
- // Only update controls on non timeupdate events
- if (utils.is.event(event) && event.type === 'timeupdate') {
- return;
- }
+ Array.from(this.elements.buttons.play || []).forEach(function (target) {
+ target.pressed = _this3.playing;
+ }); // Only update controls on non timeupdate events
- // Toggle controls
- ui.toggleControls.call(this);
- },
+ if (is.event(event) && event.type === 'timeupdate') {
+ return;
+ } // Toggle controls
+ ui.toggleControls.call(this);
+ },
// Check if media is loading
checkLoading: function checkLoading(event) {
- var _this3 = this;
+ var _this4 = this;
- this.loading = ['stalled', 'waiting'].includes(event.type);
+ this.loading = ['stalled', 'waiting'].includes(event.type); // Clear timer
- // Clear timer
- clearTimeout(this.timers.loading);
+ clearTimeout(this.timers.loading); // Timer to prevent flicker when seeking
- // Timer to prevent flicker when seeking
- this.timers.loading = setTimeout(function () {
- // Update progress bar loading class state
- utils.toggleClass(_this3.elements.container, _this3.config.classNames.loading, _this3.loading);
+ this.timers.loading = setTimeout(function () {
+ // Update progress bar loading class state
+ toggleClass(_this4.elements.container, _this4.config.classNames.loading, _this4.loading); // Update controls visibility
- // Update controls visibility
- ui.toggleControls.call(_this3);
- }, this.loading ? 250 : 0);
+ 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;
+ 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
- if (controls$$1 && this.config.hideControls) {
- // Show controls if force, loading, paused, or button interaction, otherwise hide
- this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover));
- }
+ this.toggleControls(Boolean(force || this.loading || this.paused || controls$$1.pressed || controls$$1.hover || recentTouchSeek));
+ }
}
-};
-
-// ==========================================================================
-
-// Sniff out the browser
-var browser$3 = utils.getBrowser();
+ };
-var Listeners = function () {
+ var Listeners =
+ /*#__PURE__*/
+ function () {
function Listeners(player) {
- classCallCheck(this, Listeners);
+ _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; */
- this.player = player;
- this.lastKey = null;
+ default:
+ break;
+ } // Escape is handle natively when in full screen
+ // So we only need to worry about non native
- this.handleKey = this.handleKey.bind(this);
- this.toggleMenu = this.toggleMenu.bind(this);
- this.firstTouch = this.firstTouch.bind(this);
- }
- // Handle key presses
+ if (!player.fullscreen.enabled && player.fullscreen.active && code === 27) {
+ player.fullscreen.toggle();
+ } // Store last code for next cycle
- createClass(Listeners, [{
- key: 'handleKey',
- value: function handleKey(event) {
- var _this = this;
+ this.lastKey = code;
+ } else {
+ this.lastKey = null;
+ }
+ } // Toggle menu
- var code = event.keyCode ? event.keyCode : event.which;
- var pressed = event.type === 'keydown';
- var repeat = pressed && code === this.lastKey;
+ }, {
+ key: "toggleMenu",
+ value: function toggleMenu(event) {
+ controls.toggleMenu.call(this.player, event);
+ } // Device is touch enabled
- // Bail if a modifier key is set
- if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
- return;
- }
+ }, {
+ key: "firstTouch",
+ value: function firstTouch() {
+ var player = this.player;
+ var elements = player.elements;
+ player.touch = true; // Add touch class
- // If the event is bubbled from the media element
- // Firefox doesn't get the keycode for whatever reason
- if (!utils.is.number(code)) {
- return;
- }
+ 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
- // 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 (event.type === 'keydown' && event.which !== 9) {
+ return;
+ } // Store reference to event timeStamp
- // 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;
- }
- // If the code is found prevent default (e.g. prevent scrolling for arrows)
- if (preventDefault.includes(code)) {
- event.preventDefault();
- event.stopPropagation();
- }
+ if (event.type === 'keydown') {
+ this.lastKeyDown = event.timeStamp;
+ } // Remove current classes
- 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;
- }
- // 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();
- }
+ 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
- // Store last code for next cycle
- this.lastKey = code;
- } else {
- this.lastKey = null;
- }
- }
- // Toggle menu
+ var wasKeyDown = event.timeStamp - this.lastKeyDown <= 20; // Ignore focus events if a key was pressed prior
- }, {
- key: 'toggleMenu',
- value: function toggleMenu(event) {
- controls.toggleMenu.call(this.player, event);
- }
+ if (event.type === 'focus' && !wasKeyDown) {
+ return;
+ } // Remove all current
- // Device is touch enabled
- }, {
- key: 'firstTouch',
- value: function firstTouch() {
- this.player.touch = true;
+ removeCurrent(); // Delay the adding of classname until the focus has changed
+ // This event fires before the focusin event
- // Add touch class
- utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
+ this.focusTimer = setTimeout(function () {
+ var focused = document.activeElement; // Ignore if current focus element isn't inside the player
- // Clean up
- utils.off(document.body, 'touchstart', this.firstTouch);
- }
+ if (!elements.container.contains(focused)) {
+ return;
+ }
- // Global window & document listeners
+ 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
- // Container listeners
+ once.call(player, document.body, 'touchstart', this.firstTouch); // Tab focus detection
+
+ 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 elements = player.elements; // 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 (!player.config.keyboard.global && player.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 on mouse events and entering fullscreen
- utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', function (event) {
- var controls$$1 = _this2.player.elements.controls;
- // Remove button states for fullscreen
+ var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
+ var delay = 0;
- if (event.type === 'enterfullscreen') {
- controls$$1.pressed = false;
- controls$$1.hover = false;
- }
-
- // Show, then hide after a timeout unless another control event occurs
- var show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);
+ if (show) {
+ ui.toggleControls.call(player, true); // Use longer timeout for touch devices
- var delay = 0;
+ delay = player.touch ? 3000 : 2000;
+ } // Clear timer
- if (show) {
- ui.toggleControls.call(_this2.player, true);
- // Use longer timeout for touch devices
- delay = _this2.player.touch ? 3000 : 2000;
- }
- // Clear timer
- clearTimeout(_this2.player.timers.controls);
- // Timer to prevent flicker when seeking
- _this2.player.timers.controls = setTimeout(function () {
- return ui.toggleControls.call(_this2.player, false);
- }, delay);
- });
- }
+ clearTimeout(player.timers.controls); // Set new timer to prevent flicker when seeking
- // Listen for media events
+ player.timers.controls = setTimeout(function () {
+ return ui.toggleControls.call(player, false);
+ }, delay);
+ });
+ } // Listen for media events
}, {
- key: 'media',
- value: function media() {
- var _this3 = this;
+ key: "media",
+ value: function media() {
+ 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
- // Time change on media
- utils.on(this.player.media, 'timeupdate seeking seeked', function (event) {
- return controls.timeUpdate.call(_this3.player, event);
- });
- // Display duration
- utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', function (event) {
- return controls.durationUpdate.call(_this3.player, event);
+ 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
- // 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);
- });
+ 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)
- // 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();
- }
- });
+ if (!is.element(wrapper)) {
+ return;
+ } // On click play, pause or restart
- // Check for buffer progress
- utils.on(this.player.media, 'progress playing seeking seeked', function (event) {
- return controls.updateProgress.call(_this3.player, event);
- });
- // Handle volume changes
- utils.on(this.player.media, 'volumechange', function (event) {
- return controls.updateVolume.call(_this3.player, event);
- });
+ on.call(player, elements.container, 'click', function (event) {
+ var targets = [elements.container, wrapper]; // Ignore if click if not container or in video wrapper
- // Handle play/pause
- utils.on(this.player.media, 'playing play pause ended emptied timeupdate', function (event) {
- return ui.checkPlaying.call(_this3.player, event);
- });
+ if (!targets.includes(event.target) && !wrapper.contains(event.target)) {
+ return;
+ } // Touch devices will just show controls (if hidden)
- // Loading state
- utils.on(this.player.media, 'waiting canplay seeked playing', function (event) {
- return ui.checkLoading.call(_this3.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
- utils.on(this.player.media, 'playing', function () {
- if (!_this3.player.ads) {
- return;
- }
+ if (player.touch && player.config.hideControls) {
+ return;
+ }
- // 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();
- });
- }
- });
+ if (player.ended) {
+ player.restart();
+ player.play();
+ } else {
+ player.togglePlay();
+ }
+ });
+ } // Disable right click
- // 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);
- // Bail if there's no wrapper (this should never happen)
- if (!utils.is.element(wrapper)) {
- return;
- }
+ if (player.supported.ui && player.config.disableContextMenu) {
+ on.call(player, elements.wrapper, 'contextmenu', function (event) {
+ event.preventDefault();
+ }, false);
+ } // Volume change
- // 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();
- }
- });
- }
- // Disable right click
- if (this.player.supported.ui && this.player.config.disableContextMenu) {
- utils.on(this.player.elements.wrapper, 'contextmenu', function (event) {
- event.preventDefault();
- }, false);
- }
+ on.call(player, player.media, 'volumechange', function () {
+ // Save to storage
+ player.storage.set({
+ volume: player.volume,
+ muted: player.muted
+ });
+ }); // Speed change
- // Volume change
- utils.on(this.player.media, 'volumechange', function () {
- // Save to storage
- _this3.player.storage.set({ volume: _this3.player.volume, muted: _this3.player.muted });
- });
+ on.call(player, player.media, 'ratechange', function () {
+ // Update UI
+ controls.updateSetting.call(player, 'speed'); // Save to storage
- // Speed change
- utils.on(this.player.media, 'ratechange', function () {
- // Update UI
- controls.updateSetting.call(_this3.player, 'speed');
- // Save to storage
- _this3.player.storage.set({ speed: _this3.player.speed });
- });
+ player.storage.set({
+ speed: player.speed
+ });
+ }); // Quality change
- // 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, 'qualitychange', function (event) {
+ // Update UI
+ controls.updateSetting.call(player, 'quality', null, event.detail.quality);
+ }); // Update download link when ready and if quality changes
- // 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, 'ready qualitychange', function () {
+ controls.setDownloadLink.call(player);
+ }); // Proxy events to container
+ // Bubble up key events for Edge
- // Caption language change
- utils.on(this.player.media, 'languagechange', function () {
- // Update UI
- controls.updateSetting.call(_this3.player, 'captions');
+ 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
- // Save to storage
- _this3.player.storage.set({ language: _this3.player.language });
- });
+ if (event.type === 'error') {
+ detail = player.media.error;
+ }
- // Captions toggle
- utils.on(this.player.media, 'captionsenabled captionsdisabled', function () {
- // Update UI
- controls.updateSetting.call(_this3.player, 'captions');
+ triggerEvent.call(player, elements.container, event.type, true, detail);
+ });
+ } // Run default and custom handlers
- // Save to storage
- _this3.player.storage.set({ captions: _this3.player.captions.active });
- });
+ }, {
+ 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
- // 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 = {};
+ if (hasCustomHandler) {
+ returned = customHandler.call(player, event);
+ } // Only call default handler if not prevented in custom handler
- // Get error details from media
- if (event.type === 'error') {
- detail = _this3.player.media.error;
- }
- utils.dispatchEvent.call(_this3.player, _this3.player.elements.container, event.type, true, detail);
- });
+ 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 _this = 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 _this.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 _this2 = 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) {
+ _this2.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
- // Pause
- on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
+ this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind'); // Rewind
- // Rewind
- on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
+ this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward'); // Mute toggle
- // Rewind
- on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
+ this.bind(elements.buttons.mute, 'click', function () {
+ player.muted = !player.muted;
+ }, 'mute'); // Captions toggle
- // Mute toggle
- on(this.player.elements.buttons.mute, 'click', function () {
- _this4.player.muted = !_this4.player.muted;
- }, 'mute');
+ this.bind(elements.buttons.captions, 'click', function () {
+ return player.toggleCaptions();
+ }); // Download
- // Captions toggle
- on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
+ this.bind(elements.buttons.download, 'click', function () {
+ triggerEvent.call(player, player.media, 'download');
+ }, 'download'); // Fullscreen toggle
- // Fullscreen toggle
- on(this.player.elements.buttons.fullscreen, 'click', function () {
- _this4.player.fullscreen.toggle();
- }, 'fullscreen');
+ this.bind(elements.buttons.fullscreen, 'click', function () {
+ player.fullscreen.toggle();
+ }, 'fullscreen'); // Picture-in-Picture
- // Picture-in-Picture
- on(this.player.elements.buttons.pip, 'click', function () {
- _this4.player.pip = 'toggle';
- }, 'pip');
+ this.bind(elements.buttons.pip, 'click', function () {
+ player.pip = 'toggle';
+ }, 'pip'); // Airplay
- // Airplay
- on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
+ this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); // Settings menu - click toggle
- // Settings menu
- on(this.player.elements.buttons.settings, 'click', function (event) {
- controls.toggleMenu.call(_this4.player, event);
- });
+ this.bind(elements.buttons.settings, 'click', function (event) {
+ // Prevent the document click listener closing the menu
+ event.stopPropagation();
- // 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'));
- }
- });
+ 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
- // Set range input alternative "value", which matches the tooltip time (#954)
- on(this.player.elements.inputs.seek, 'mousedown mousemove', function (event) {
- var clientRect = _this4.player.elements.progress.getBoundingClientRect();
- var percent = 100 / clientRect.width * (event.pageX - clientRect.left);
- event.currentTarget.setAttribute('seek-value', percent);
- });
+ this.bind(elements.buttons.settings, 'keyup', function (event) {
+ var code = event.which; // We only care about space and return
- // Pause while seeking
- on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', function (event) {
- var seek = event.currentTarget;
+ if (![13, 32].includes(code)) {
+ return;
+ } // Because return triggers a click anyway, all we need to do is set focus
- // Was playing before?
- var play = seek.hasAttribute('play-on-seeked');
- // Done seeking
- var done = ['mouseup', 'touchend', 'keyup'].includes(event.type);
+ if (code === 13) {
+ controls.focusFirstMenuItem.call(player, null, true);
- // If we're done seeking and it was playing, resume playback
- if (play && done) {
- seek.removeAttribute('play-on-seeked');
- _this4.player.play();
- } else if (!done && _this4.player.playing) {
- seek.setAttribute('play-on-seeked', '');
- _this4.player.pause();
- }
- });
+ return;
+ } // Prevent scroll
- // Seek
- on(this.player.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');
+ event.preventDefault(); // Prevent playing video (Firefox)
- if (utils.is.empty(seekTo)) {
- seekTo = seek.value;
- }
+ event.stopPropagation(); // Toggle menu
- seek.removeAttribute('seek-value');
+ controls.toggleMenu.call(player, event);
+ }, null, false // Can't be passive as we're preventing default
+ ); // Escape closes menu
- _this4.player.currentTime = seekTo / seek.max * _this4.player.duration;
- }, 'seek');
+ 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)
- // 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;
- }
+ 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
- _this4.player.config.invertTime = !_this4.player.config.invertTime;
+ 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';
- controls.timeUpdate.call(_this4.player);
- });
- }
+ if (is.keyboardEvent(event) && code !== 39 && code !== 37) {
+ return;
+ } // Record seek time so we can prevent hiding controls for a few seconds after seek
- // Volume
- on(this.player.elements.inputs.volume, inputEvent, function (event) {
- _this4.player.volume = event.target.value;
- }, 'volume');
- // 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);
- });
- }
+ player.lastSeekTime = Date.now(); // Was playing before?
- // Seek tooltip
- on(this.player.elements.progress, 'mouseenter mouseleave mousemove', function (event) {
- return controls.updateSeekTooltip.call(_this4.player, event);
- });
+ var play = seek.hasAttribute(attribute); // Done seeking
- // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)
- on(this.player.elements.controls, 'mouseenter mouseleave', function (event) {
- _this4.player.elements.controls.hover = !_this4.player.touch && event.type === 'mouseenter';
- });
+ var done = ['mouseup', 'touchend', 'keyup'].includes(event.type); // If we're done seeking and it was playing, resume playback
- // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)
- on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', function (event) {
- _this4.player.elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);
+ 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 _this2.bind(input, inputEvent, function (event) {
+ return repaint(event.target);
});
+ });
+ } // Seek
- // Focus in/out on controls
- on(this.player.elements.controls, 'focusin focusout', function (event) {
- var _player = _this4.player,
- config = _player.config,
- elements = _player.elements,
- timers = _player.timers;
- // Skip transition to prevent focus from scrolling the parent element
+ 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)
- utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin');
+ var seekTo = seek.getAttribute('seek-value');
- // Toggle
- ui.toggleControls.call(_this4.player, event.type === 'focusin');
+ if (is.empty(seekTo)) {
+ seekTo = seek.value;
+ }
- // If focusin, hide again after delay
- if (event.type === 'focusin') {
- // Restore transition
- setTimeout(function () {
- utils.toggleClass(elements.controls, config.classNames.noTransition, false);
- }, 0);
+ seek.removeAttribute('seek-value');
+ player.currentTime = seekTo / seek.max * player.duration;
+ }, 'seek'); // Seek tooltip
- // Delay a little more for keyboard users
- var delay = _this4.touch ? 3000 : 4000;
+ this.bind(elements.progress, 'mouseenter mouseleave mousemove', function (event) {
+ return controls.updateSeekTooltip.call(player, event);
+ }); // Polyfill for lower fill in <input type="range"> for webkit
- // Clear timer
- clearTimeout(timers.controls);
- // Hide
- timers.controls = setTimeout(function () {
- return ui.toggleControls.call(_this4.player, false);
- }, delay);
- }
+ if (browser.isWebkit) {
+ Array.from(getElements.call(player, 'input[type="range"]')).forEach(function (element) {
+ _this2.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
- // 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;
- }
- }
- // 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;
- }
- }
+ 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;
+ }
- // 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);
- }
+ player.config.invertTime = !player.config.invertTime;
- // Reset on destroy
+ controls.timeUpdate.call(player);
+ });
+ } // Volume
- }, {
- key: 'clear',
- value: function clear() {
- this.global(false);
- }
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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
+
+ toggleClass(elements.controls, config.classNames.noTransition, true); // Toggle
+
+ ui.toggleControls.call(player, true); // Restore transition
+
+ setTimeout(function () {
+ toggleClass(elements.controls, config.classNames.noTransition, false);
+ }, 0); // Delay a little more for mouse users
+
+ var delay = _this2.touch ? 3000 : 4000; // Clear timer
+
+ clearTimeout(timers.controls); // Hide again after delay
+
+ timers.controls = setTimeout(function () {
+ return ui.toggleControls.call(player, false);
+ }, delay);
+ }); // Mouse wheel for volume
+
+ 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
+
+ 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)
+
+
+ var direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); // Change the volume by 2%
+
+ player.increaseVolume(direction / 50); // Don't break page scrolling at max and min
+
+ var volume = player.media.volume;
+
+ if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {
+ event.preventDefault();
+ }
+ }, 'volume', false);
+ }
}]);
+
return Listeners;
-}();
+ }();
-// ==========================================================================
+ var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
-// Set playback state and trigger change (only on actual change)
-function assurePlaybackState(play) {
- if (this.media.paused === play) {
- this.media.paused = !play;
- utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ function createCommonjsModule(fn, module) {
+ return module = { exports: {} }, fn(module, module.exports), module.exports;
+ }
+
+ 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;
+ }
-var vimeo = {
- setup: function setup() {
- var _this = this;
+ // add to callback queue
+ q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];
+ q.push(fn);
+ }
+ }
- // Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
- // Set intial ratio
- vimeo.setAspectRatio.call(this);
+ /**
+ * 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;
- // 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 q = bundleCallbackQueue[bundleId];
+ // cache result
+ bundleResultCache[bundleId] = pathsNotFound;
- // 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 + '%';
+ // exit if queue is empty
+ if (!q) return;
- if (this.supported.ui) {
- var height = 240;
- var offset = (height - padding) / (height / 50);
+ // empty callback queue
+ while (q.length) {
+ q[0](bundleId, pathsNotFound);
+ q.splice(0, 1);
+ }
+ }
- this.media.style.transform = 'translateY(-' + offset + '%)';
- }
- },
+ /**
+ * 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};
- // API Ready
- ready: function ready() {
- var _this2 = this;
+ // success and error callbacks
+ if (depsNotFound.length) (args.error || devnull)(depsNotFound);
+ else (args.success || devnull)(args);
+ }
- var player = this;
-
- // Get Vimeo params for the iframe
- var options = {
- loop: player.config.loop.active,
- autoplay: player.autoplay,
- // muted: player.muted,
- byline: false,
- portrait: false,
- title: false,
- speed: true,
- transparent: 0,
- gesture: 'media',
- playsinline: !this.config.fullscreen.iosNative
- };
- var params = utils.buildUrlParams(options);
- // Get the source URL or ID
- var source = player.media.getAttribute('src');
+ /**
+ * 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];
- // Get from <div> if needed
- if (utils.is.empty(source)) {
- source = player.media.getAttribute(player.config.attributes.embed.id);
+ // 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';
}
+ }
- 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;
- }
+ // handle retries in case of load failure
+ if (result == 'e') {
+ // increment counter
+ numTries += 1;
- // Get the URL for thumbnail
- var url = new URL(response[0].thumbnail_large);
+ // exit function and try again
+ if (numTries < maxTries) {
+ return loadFile(path, callbackFn, args, numTries);
+ }
+ }
- // Get original image
- url.pathname = url.pathname.split('_')[0] + '.jpg';
+ // execute callback
+ callbackFn(path, result, ev.defaultPrevented);
+ };
- // Set and show poster
- ui.setPoster.call(player, url.href);
- });
+ // add to document (unless callback returns `false`)
+ if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);
+ }
- // Setup instance
- // https://github.com/vimeo/player.js
- player.embed = new window.Vimeo.Player(iframe, {
- autopause: player.config.autopause,
- muted: player.muted
- });
- player.media.paused = true;
- player.media.currentTime = 0;
+ /**
+ * 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;
+ }
- // Disable native text track rendering
- if (player.supported.ui) {
- player.embed.disableTextTrack();
- }
+ numWaiting--;
+ if (!numWaiting) callbackFn(pathsNotFound);
+ };
- // Create a faux HTML5 API using the Vimeo API
- player.media.play = function () {
- assurePlaybackState.call(player, true);
- return player.embed.play();
- };
+ // load scripts
+ for (i=0; i < x; i++) loadFile(paths[i], fn, args);
+ }
- player.media.pause = function () {
- assurePlaybackState.call(player, false);
- return player.embed.pause();
- };
- player.media.stop = function () {
- player.pause();
- player.currentTime = 0;
- };
+ /**
+ * 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;
+ }
+ }
- // Seeking
- var currentTime = player.media.currentTime;
+ // load scripts
+ loadFiles(paths, function (pathsNotFound) {
+ // execute callbacks
+ executeCallbacks(args, pathsNotFound);
- 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;
-
- // Set seeking state and trigger event
-
- media.seeking = true;
- utils.dispatchEvent.call(player, media, 'seeking');
-
- // If paused, mute until seek is complete
- Promise.resolve(paused && embed.setVolume(0))
- // Seek
- .then(function () {
- return embed.setCurrentTime(time);
- })
- // Restore paused
- .then(function () {
- return paused && embed.pause();
- })
- // Restore volume
- .then(function () {
- return paused && embed.setVolume(volume);
- }).catch(function () {
- // Do nothing
- });
- }
- });
+ // publish bundle load event
+ publish(bundleId, pathsNotFound);
+ }, args);
+ }
- // Playback speed
- var speed = player.config.speed.selected;
- Object.defineProperty(player.media, 'playbackRate', {
- get: function get() {
- return speed;
- },
- set: function set(input) {
- player.embed.setPlaybackRate(input).then(function () {
- speed = input;
- utils.dispatchEvent.call(player, player.media, 'ratechange');
- }).catch(function (error) {
- // Hide menu item (and menu if empty)
- if (error.name === 'Error') {
- controls.setSpeedMenu.call(player, []);
- }
- });
- }
- });
- // Volume
- var volume = player.config.volume;
+ /**
+ * 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, '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');
- });
- }
- });
+ return loadjs;
+ };
- // Muted
- var muted = player.config.muted;
- Object.defineProperty(player.media, 'muted', {
- get: function get() {
- return muted;
- },
- set: function set(input) {
- var toggle = utils.is.boolean(input) ? input : false;
+ /**
+ * Manually satisfy bundle dependencies.
+ * @param {string} bundleId - The bundle id
+ */
+ loadjs.done = function done(bundleId) {
+ publish(bundleId, []);
+ };
- 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;
+ /**
+ * Reset loadjs dependencies statuses
+ */
+ loadjs.reset = function reset() {
+ bundleIdCache = {};
+ bundleResultCache = {};
+ bundleCallbackQueue = {};
+ };
- 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;
- });
- }
- });
+ /**
+ * Determine if bundle has already been defined
+ * @param String} bundleId - The bundle id
+ */
+ loadjs.isDefined = function isDefined(bundleId) {
+ return bundleId in bundleIdCache;
+ };
- // 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;
- }
- });
+ // export
+ return loadjs;
- // 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);
- });
+ // ==========================================================================
+ function loadScript(url) {
+ return new Promise(function (resolve, reject) {
+ loadjs_umd(url, {
+ success: resolve,
+ error: reject
+ });
+ });
+ }
- // Set autopause
- player.embed.setAutopause(player.config.autopause).then(function (state) {
- player.config.autopause = state;
- });
+ function parseId(url) {
+ if (is.empty(url)) {
+ return null;
+ }
- // Get title
- player.embed.getVideoTitle().then(function (title) {
- player.config.title = title;
- ui.setTitle.call(_this2);
- });
+ if (is.number(Number(url))) {
+ return url;
+ }
- // Get current time
- player.embed.getCurrentTime().then(function (value) {
- currentTime = value;
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
- });
+ var regex = /^.*(vimeo.com\/|video\/)(\d+).*/;
+ return url.match(regex) ? RegExp.$2 : url;
+ } // Get aspect ratio for dimensions
- // Get duration
- player.embed.getDuration().then(function (value) {
- player.media.duration = value;
- utils.dispatchEvent.call(player, player.media, 'durationchange');
- });
- // Get captions
- player.embed.getTextTracks().then(function (tracks) {
- player.media.textTracks = tracks;
- captions.setup.call(player);
- });
+ function getAspectRatio(width, height) {
+ var getRatio = function getRatio(w, h) {
+ return h === 0 ? w : getRatio(h, w % h);
+ };
- player.embed.on('cuechange', function (data) {
- var cue = null;
+ var ratio = getRatio(width, height);
+ return "".concat(width / ratio, ":").concat(height / ratio);
+ } // Set playback state and trigger change (only on actual change)
- if (data.cues.length) {
- cue = utils.stripHTML(data.cues[0].text);
- }
- captions.setText.call(player, cue);
- });
+ function assurePlaybackState(play) {
+ if (play && !this.embed.hasPlayed) {
+ this.embed.hasPlayed = true;
+ }
- player.embed.on('loaded', function () {
- if (utils.is.element(player.embed.element) && player.supported.ui) {
- var frame = player.embed.element;
+ if (this.media.paused === play) {
+ this.media.paused = !play;
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
+ }
+ }
- // Fix keyboard focus issues
- // https://github.com/sampotts/plyr/issues/317
- frame.setAttribute('tabindex', -1);
- }
- });
+ var vimeo = {
+ setup: function setup() {
+ var _this = this;
- player.embed.on('play', function () {
- assurePlaybackState.call(player, true);
- utils.dispatchEvent.call(player, player.media, 'playing');
- });
+ // Add embed class for responsive
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set intial ratio
- player.embed.on('pause', function () {
- assurePlaybackState.call(player, false);
- });
+ vimeo.setAspectRatio.call(this); // Load the API if not already
- player.embed.on('timeupdate', function (data) {
- player.media.seeking = false;
- currentTime = data.seconds;
- utils.dispatchEvent.call(player, player.media, 'timeupdate');
+ 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);
+ }
+ },
+ // Set aspect ratio
+ // For Vimeo we have an extra 300% height <div> to hide the standard controls and UI
+ setAspectRatio: function setAspectRatio(input) {
+ var _split$map = (is.string(input) ? input : this.config.ratio).split(':').map(Number),
+ _split$map2 = _slicedToArray(_split$map, 2),
+ x = _split$map2[0],
+ y = _split$map2[1];
+
+ var padding = 100 / x * y;
+ vimeo.padding = padding;
+ this.elements.wrapper.style.paddingBottom = "".concat(padding, "%");
+
+ if (this.supported.ui) {
+ var height = 240;
+ var offset = (height - padding) / (height / 50);
+ this.media.style.transform = "translateY(-".concat(offset, "%)");
+ }
+ },
+ // API Ready
+ ready: function ready$$1() {
+ var _this2 = this;
+
+ var player = this; // Get Vimeo params for the iframe
+
+ var options = {
+ loop: player.config.loop.active,
+ autoplay: player.autoplay,
+ // muted: player.muted,
+ byline: false,
+ portrait: false,
+ title: false,
+ speed: true,
+ transparent: 0,
+ gesture: 'media',
+ playsinline: !this.config.fullscreen.iosNative
+ };
+ var params = buildUrlParams(options); // 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(player.config.attributes.embed.id);
+ }
- player.embed.on('progress', function (data) {
- player.media.buffered = data.percent;
- utils.dispatchEvent.call(player, player.media, 'progress');
+ var id = parseId(source); // Build an iframe
- // Check all loaded
- if (parseInt(data.percent, 10) === 1) {
- utils.dispatchEvent.call(player, player.media, 'canplaythrough');
- }
+ 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
- // 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');
- }
- });
- });
+ var poster = player.poster; // Inject the package
- player.embed.on('seeked', function () {
- player.media.seeking = false;
- utils.dispatchEvent.call(player, player.media, 'seeked');
- });
+ var wrapper = createElement('div', {
+ poster: poster,
+ class: player.config.classNames.embedContainer
+ });
+ wrapper.appendChild(iframe);
+ player.media = replaceElement(wrapper, player.media); // Get poster image
- player.embed.on('ended', function () {
- player.media.paused = true;
- utils.dispatchEvent.call(player, player.media, 'ended');
- });
+ fetch(format(player.config.urls.vimeo.api, id), 'json').then(function (response) {
+ if (is.empty(response)) {
+ return;
+ } // Get the URL for thumbnail
- player.embed.on('error', function (detail) {
- player.media.error = detail;
- utils.dispatchEvent.call(player, player.media, 'error');
- });
- // Rebuild UI
- setTimeout(function () {
- return ui.build.call(player);
- }, 0);
- }
-};
+ var url = new URL(response[0].thumbnail_large); // Get original image
-// ==========================================================================
+ url.pathname = "".concat(url.pathname.split('_')[0], ".jpg"); // Set and show poster
-// Standardise YouTube quality unit
-function mapQualityUnit(input) {
- switch (input) {
- case 'hd2160':
- return 2160;
+ ui.setPoster.call(player, url.href).catch(function () {});
+ }); // Setup instance
+ // https://github.com/vimeo/player.js
- case 2160:
- return 'hd2160';
+ 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 'hd1440':
- return 1440;
+ if (player.supported.ui) {
+ player.embed.disableTextTrack();
+ } // Create a faux HTML5 API using the Vimeo API
- case 1440:
- return 'hd1440';
- case 'hd1080':
- return 1080;
+ player.media.play = function () {
+ assurePlaybackState.call(player, true);
+ return player.embed.play();
+ };
- case 1080:
- return 'hd1080';
+ player.media.pause = function () {
+ assurePlaybackState.call(player, false);
+ return player.embed.pause();
+ };
- case 'hd720':
- return 720;
+ player.media.stop = function () {
+ player.pause();
+ player.currentTime = 0;
+ }; // Seeking
- case 720:
- return 'hd720';
- case 'large':
- return 480;
+ 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) {
+ vimeo.ratio = getAspectRatio(dimensions[0], dimensions[1]);
+ vimeo.setAspectRatio.call(_this2, vimeo.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');
+ }
+ });
+
+ 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 480:
- return 'large';
- case 'medium':
- return 360;
+ 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');
+ }); // Set height/width on fullscreen
- case 360:
- return 'medium';
+ player.on('enterfullscreen exitfullscreen', function (event) {
+ var target = player.fullscreen.target; // Ignore for iOS native
- case 'small':
- return 240;
+ if (target !== player.elements.container) {
+ return;
+ }
- case 240:
- return 'small';
+ var toggle = event.type === 'enterfullscreen';
- default:
- return 'default';
+ var _vimeo$ratio$split$ma = vimeo.ratio.split(':').map(Number),
+ _vimeo$ratio$split$ma2 = _slicedToArray(_vimeo$ratio$split$ma, 2),
+ x = _vimeo$ratio$split$ma2[0],
+ y = _vimeo$ratio$split$ma2[1];
+
+ var dimension = x > y ? 'width' : 'height';
+ target.style[dimension] = toggle ? "".concat(vimeo.padding, "%") : null;
+ }); // 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)
+
+
+ function assurePlaybackState$1(play) {
+ if (play && !this.embed.hasPlayed) {
+ this.embed.hasPlayed = true;
+ }
-// Set playback state and trigger change (only on actual change)
-function assurePlaybackState$1(play) {
if (this.media.paused === play) {
- this.media.paused = !play;
- utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause');
+ this.media.paused = !play;
+ triggerEvent.call(this, this.media, play ? 'play' : 'pause');
}
-}
+ }
-var youtube = {
+ var youtube = {
setup: function setup() {
- var _this = this;
+ var _this = this;
- // Add embed class for responsive
- utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true);
+ // Add embed class for responsive
+ toggleClass(this.elements.wrapper, this.config.classNames.embed, true); // Set aspect ratio
- // Set aspect ratio
- youtube.setAspectRatio.call(this);
+ youtube.setAspectRatio.call(this); // Setup API
- // 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);
- });
+ 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...
- // Setup callback for the API
- // YouTube has it's own system of course...
- window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || [];
+ window.onYouTubeReadyCallbacks = window.onYouTubeReadyCallbacks || []; // Add to queue
- // Add to queue
- window.onYouTubeReadyCallbacks.push(function () {
- youtube.ready.call(_this);
- });
+ window.onYouTubeReadyCallbacks.push(function () {
+ youtube.ready.call(_this);
+ }); // Set callback to process queue
- // Set callback to process queue
- window.onYouTubeIframeAPIReady = function () {
- window.onYouTubeReadyCallbacks.forEach(function (callback) {
- callback();
- });
- };
- }
+ 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;
- // 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 () {});
+ if (is.empty(title)) {
+ this.config.title = title;
+ ui.setTitle.call(this);
+ return;
}
- },
+ } // Or via Google API
+
+ 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 () {});
+ }
+ },
// Set aspect ratio
setAspectRatio: function setAspectRatio() {
- var ratio = this.config.ratio.split(':');
- this.elements.wrapper.style.paddingBottom = 100 / ratio[0] * ratio[1] + '%';
+ var ratio = this.config.ratio.split(':');
+ this.elements.wrapper.style.paddingBottom = "".concat(100 / ratio[0] * ratio[1], "%");
},
-
-
// API ready
- ready: function ready() {
- var player = this;
+ ready: function ready$$1() {
+ var player = this; // Ignore already setup (race condition)
- // Ignore already setup (race condition)
- var currentId = player.media.getAttribute('id');
- if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) {
- return;
- }
+ var currentId = player.media.getAttribute('id');
+
+ 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 () {}); // 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
+ hl: player.config.hl,
+ // iframe interface language
+ 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) {
+ // 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();
+ };
- // Get the source URL or ID
- var source = player.media.getAttribute('src');
+ player.media.pause = function () {
+ assurePlaybackState$1.call(player, false);
+ instance.pauseVideo();
+ };
- // Get from <div> if needed
- if (utils.is.empty(source)) {
- source = player.media.getAttribute(this.config.attributes.embed.id);
- }
+ player.media.stop = function () {
+ instance.stopVideo();
+ };
- // 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);
+ 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) {
+ // 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
+
+
+ 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
- // Set poster image
- var posterSrc = function posterSrc(format) {
- return 'https://img.youtube.com/vi/' + videoId + '/' + format + 'default.jpg';
- };
+ player.options.speed = instance.getAvailablePlaybackRates(); // Set the tabindex to avoid focus entering iframe
- // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
- utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
- .catch(function () {
- return utils.loadImage(posterSrc('sd'), 121);
- }) // 480p padded 4:3
- .catch(function () {
- return utils.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';
+ if (player.supported.ui) {
+ player.media.setAttribute('tabindex', -1);
}
- });
- // 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
- };
-
- // 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 () {
- assurePlaybackState$1.call(player, true);
- instance.playVideo();
- };
-
- player.media.pause = function () {
- assurePlaybackState$1.call(player, false);
- instance.pauseVideo();
- };
-
- 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) {
- // If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).
- if (player.paused) {
- player.embed.mute();
- }
-
- // Set seeking state and trigger event
- player.media.seeking = true;
- utils.dispatchEvent.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);
- }
- });
-
- // 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);
-
- var seeked = player.media.seeking && [1, 2].includes(event.data);
-
- if (seeked) {
- // Unset seeking and fire seeked event
- player.media.seeking = false;
- utils.dispatchEvent.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
- 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:
- 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 {
- utils.dispatchEvent.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.media.pause();
- } else {
- assurePlaybackState$1.call(player, true);
-
- 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:
- // 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;
- }
-
- 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);
- }
+ wrap(this.media, this.elements.wrapper); // Faux poster container
- if (this.isEmbed) {
- switch (this.provider) {
- case 'youtube':
- youtube.setup.call(this);
- break;
-
- 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;
-
- classCallCheck(this, Ads);
+ var _this = this;
+
+ _classCallCheck(this, Ads);
+
+ 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);
+ });
+ this.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);
- });
+ _createClass(Ads, [{
+ key: "load",
- this.load();
- }
+ /**
+ * Load the IMA SDK
+ */
+ value: function load() {
+ var _this2 = this;
- 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();
- }
- }
+ 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
- */
+ }
+ /**
+ * Get the ads instance ready
+ */
}, {
- key: 'ready',
- value: function ready() {
- var _this3 = this;
+ 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()');
+ // 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
- // Clear the safety timer
- this.managerPromise.then(function () {
- _this3.clearSafetyTimer('onAdsManagerLoaded()');
- });
+ this.managerPromise.then(function () {
+ _this3.clearSafetyTimer('onAdsManagerLoaded()');
+ }); // Set listeners on the Plyr instance
- // Set listeners on the Plyr instance
- this.listeners();
+ this.listeners(); // Setup the IMA SDK
- // Setup the IMA SDK
- this.setupIMA();
- }
+ this.setupIMA();
+ } // Build the default 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
- // Build the default tag URL
+ google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED); // Set language
+
+ google.ima.settings.setLocale(this.player.config.ads.language); // We assume the adContainer is the video container of the plyr element
+ // that will house the ads
+
+ this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container); // Request video ads to be pre-loaded
+
+ this.requestAds();
+ }
+ /**
+ * Request advertisements
+ */
}, {
- key: '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);
+ key: "requestAds",
+ value: function requestAds() {
+ var _this4 = this;
- // So we can run VPAID2
- google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);
+ var container = this.player.elements.container;
- // Set language
- google.ima.settings.setLocale(this.player.config.ads.language);
+ try {
+ // Create ads loader
+ this.loader = new google.ima.AdsLoader(this.elements.displayContainer); // Listen and respond to ads loaded and error events
- // 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);
+ 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
- // Request video ads to be pre-loaded
- this.requestAds();
- }
+ 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 advertisements
- */
+ 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: 'requestAds',
- value: function requestAds() {
- var _this4 = this;
+ request.forceNonLinearFullSlot = false; // Mute based on current state
- var container = this.player.elements.container;
+ request.setAdWillPlayMuted(!this.player.muted);
+ this.loader.requestAds(request);
+ } catch (e) {
+ this.onAdError(e);
+ }
+ }
+ /**
+ * Update the ad countdown
+ * @param {boolean} start
+ */
+ }, {
+ key: "pollCountdown",
+ value: function pollCountdown() {
+ var _this5 = this;
- 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 start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+
+ if (!start) {
+ clearInterval(this.countdownTimer);
+ this.elements.container.removeAttribute('data-badge-text');
+ return;
}
- /**
- * Update the ad countdown
- * @param {boolean} start
- */
+ 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: 'pollCountdown',
- value: function pollCountdown() {
- var _this5 = this;
+ key: "onAdsManagerLoaded",
+ value: function onAdsManagerLoaded(event) {
+ var _this6 = this;
- var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+ // Load could occur after a source change (race condition)
+ if (!this.enabled) {
+ return;
+ } // Get the ads manager
- if (!start) {
- clearInterval(this.countdownTimer);
- this.elements.container.removeAttribute('data-badge-text');
- return;
- }
- 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);
- };
+ var settings = new google.ima.AdsRenderingSettings(); // Tell the SDK to save and restore content video state on our behalf
- this.countdownTimer = setInterval(update, 100);
- }
+ 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 method is called whenever the ads are ready inside the AdDisplayContainer
- * @param {Event} adsManagerLoadedEvent
- */
+ this.manager = event.getAdsManager(this.player, settings); // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
- }, {
- 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);
- }
- }
+ this.cuePoints = this.manager.getCuePoints(); // Add advertisement cue's within the time line if available
+
+ 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 (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
- // Get skippable state
- // TODO: Skip button
- // this.player.debug.warn(this.manager.getAdSkippableState());
- // Set volume to match player
- this.manager.setVolume(this.player.volume);
+ this.manager.setVolume(this.player.volume); // Add listeners to the required events
+ // Advertisement error events
- // Add listeners to the required events
- // Advertisement error events
- this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
- return _this6.onAdError(error);
- });
+ this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, function (error) {
+ return _this6.onAdError(error);
+ }); // Advertisement regular events
- // 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);
- });
- });
-
- // Resolve our adsManager
- this.trigger('loaded');
- }
+ 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 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
- */
+ 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: 'onAdEvent',
- value: function onAdEvent(event) {
- var _this7 = this;
+ key: "onAdEvent",
+ value: function onAdEvent(event) {
+ var _this7 = this;
- var container = this.player.elements.container;
+ 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
- // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)
- // don't have ad object associated
+ var ad = event.getAd(); // Proxy event
- var ad = event.getAd();
+ var dispatchEvent = function dispatchEvent(type) {
+ var event = "ads".concat(type.replace(/_/g, '').toLowerCase());
+ triggerEvent.call(_this7.player, _this7.player.media, event);
+ };
- // Proxy event
- var dispatchEvent = function dispatchEvent(type) {
- var event = 'ads' + type.replace(/_/g, '').toLowerCase();
- utils.dispatchEvent.call(_this7.player, _this7.player.media, 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
- 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;
- }
+ 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;
}
-
- /**
- * Any ad error handling comes through here
- * @param {Event} event
- */
+ }
+ /**
+ * Any ad error handling comes through here
+ * @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: "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: 'listeners',
- value: function listeners() {
- var _this8 = 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
- var time = void 0;
+ this.player.on('ended', function () {
+ _this8.loader.contentComplete();
+ });
+ this.player.on('seeking', function () {
+ time = _this8.player.currentTime;
+ return time;
+ });
+ this.player.on('seeked', function () {
+ var seekedTime = _this8.player.currentTime;
- // Add listeners to the required events
- this.player.on('ended', function () {
- _this8.loader.contentComplete();
- });
+ if (is.empty(_this8.cuePoints)) {
+ return;
+ }
- this.player.on('seeking', function () {
- time = _this8.player.currentTime;
- return time;
- });
+ _this8.cuePoints.forEach(function (cuePoint, index) {
+ if (time < cuePoint && cuePoint < seekedTime) {
+ _this8.manager.discardAdBreak();
- this.player.on('seeked', function () {
- var seekedTime = _this8.player.currentTime;
+ _this8.cuePoints.splice(index, 1);
+ }
+ });
+ }); // Listen to the resizing of the window. And resize ad accordingly
+ // TODO: eventually implement ResizeObserver
- if (utils.is.empty(_this8.cuePoints)) {
- return;
- }
+ 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
+ */
- _this8.cuePoints.forEach(function (cuePoint, index) {
- if (time < cuePoint && cuePoint < seekedTime) {
- _this8.manager.discardAdBreak();
- _this8.cuePoints.splice(index, 1);
- }
- });
- });
+ }, {
+ key: "play",
+ value: function play() {
+ var _this9 = this;
- // 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);
- }
- });
- }
+ var container = this.player.elements.container;
- /**
- * Initialize the adsManager and start playing advertisements
- */
+ if (!this.managerPromise) {
+ this.resumeContent();
+ } // Play the requested advertisement whenever the adsManager is ready
- }, {
- key: 'play',
- value: function play() {
- var _this9 = this;
- var container = this.player.elements.container;
+ 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
- if (!this.managerPromise) {
- this.resumeContent();
- }
- // 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 () {});
- }
+ _this9.manager.start();
+ }
- /**
- * Resume our video
- */
+ _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
+ */
}, {
- key: 'resumeContent',
- value: function resumeContent() {
- // Hide the advertisement container
- this.elements.container.style.zIndex = '';
+ key: "resumeContent",
+ value: function resumeContent() {
+ // Hide the advertisement container
+ this.elements.container.style.zIndex = ''; // Ad is stopped
- // Ad is stopped
- this.playing = false;
+ this.playing = false; // Play our video
- // Play our video
- if (this.player.currentTime < this.player.duration) {
- this.player.play();
- }
+ if (this.player.currentTime < this.player.duration) {
+ this.player.play();
}
-
- /**
- * Pause our video
- */
+ }
+ /**
+ * Pause our video
+ */
}, {
- key: 'pauseContent',
- value: function pauseContent() {
- // Show the advertisement container
- this.elements.container.style.zIndex = 3;
+ key: "pauseContent",
+ value: function pauseContent() {
+ // Show the advertisement container
+ this.elements.container.style.zIndex = 3; // Ad is playing.
- // Ad is playing.
- this.playing = true;
+ this.playing = true; // Pause our video.
- // 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
- */
+ this.player.pause();
+ }
+ /**
+ * Destroy the adsManager so we can grab new ads after this. If we don't then we're not
+ * allowed to call new ads based on google policies, as they interpret this as an accidental
+ * video requests. https://developers.google.com/interactive-
+ * media-ads/docs/sdks/android/faq#8
+ */
}, {
- key: 'cancel',
- value: function cancel() {
- // Pause our video
- if (this.initialized) {
- this.resumeContent();
- }
+ key: "cancel",
+ value: function cancel() {
+ // Pause our video
+ if (this.initialized) {
+ this.resumeContent();
+ } // Tell our instance that we're done for now
- // Tell our instance that we're done for now
- this.trigger('error');
- // Re-create our adsManager
- this.loadAds();
- }
+ this.trigger('error'); // Re-create our adsManager
- /**
- * Re-create our adsManager
- */
+ this.loadAds();
+ }
+ /**
+ * Re-create our adsManager
+ */
}, {
- 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();
- }
+ key: "loadAds",
+ value: function loadAds() {
+ var _this10 = this;
- // Re-set our adsManager promises
- _this10.managerPromise = new Promise(function (resolve) {
- _this10.on('loaded', resolve);
- _this10.player.debug.log(_this10.manager);
- });
+ // 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
- // Now request some new advertisements
- _this10.requestAds();
- }).catch(function () {});
- }
- /**
- * Handles callbacks after an ad event was invoked
- * @param {string} event - Event type
- */
+ _this10.managerPromise = new Promise(function (resolve) {
+ _this10.on('loaded', resolve);
- }, {
- key: 'trigger',
- value: function trigger(event) {
- var _this11 = this;
+ _this10.player.debug.log(_this10.manager);
+ }); // Now request some new advertisements
- for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
- args[_key - 1] = arguments[_key];
- }
+ _this10.requestAds();
+ }).catch(function () {});
+ }
+ /**
+ * Handles callbacks after an ad event was invoked
+ * @param {string} event - Event type
+ */
- var handlers = this.events[event];
+ }, {
+ key: "trigger",
+ value: function trigger(event) {
+ var _this11 = this;
- if (utils.is.array(handlers)) {
- handlers.forEach(function (handler) {
- if (utils.is.function(handler)) {
- handler.apply(_this11, args);
- }
- });
- }
+ for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+ args[_key - 1] = arguments[_key];
}
- /**
- * Add event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
- * @return {Ads}
- */
+ var handlers = this.events[event];
- }, {
- key: 'on',
- value: function on(event, callback) {
- if (!utils.is.array(this.events[event])) {
- this.events[event] = [];
+ 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}
+ */
- this.events[event].push(callback);
-
- return this;
+ }, {
+ key: "on",
+ value: function on$$1(event, callback) {
+ if (!is.array(this.events[event])) {
+ this.events[event] = [];
}
- /**
- * 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.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: 'startSafetyTimer',
- value: function startSafetyTimer(time, from) {
- var _this12 = this;
-
- this.player.debug.log('Safety timer invoked from: ' + from);
+ key: "startSafetyTimer",
+ value: function startSafetyTimer(time, from) {
+ var _this12 = this;
- this.safetyTimer = setTimeout(function () {
- _this12.cancel();
- _this12.clearSafetyTimer('startSafetyTimer()');
- }, time);
- }
+ this.player.debug.log("Safety timer invoked from: ".concat(from));
+ this.safetyTimer = setTimeout(function () {
+ _this12.cancel();
- /**
- * Clear our safety timer(s)
- * @param {string} from
- */
+ _this12.clearSafetyTimer('startSafetyTimer()');
+ }, time);
+ }
+ /**
+ * Clear our safety timer(s)
+ * @param {string} from
+ */
}, {
- key: 'clearSafetyTimer',
- value: function clearSafetyTimer(from) {
- if (!utils.is.nullOrUndefined(this.safetyTimer)) {
- this.player.debug.log('Safety timer cleared from: ' + from);
-
- clearTimeout(this.safetyTimer);
- this.safetyTimer = null;
- }
+ 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$$1() {
- return this.player.isVideo && this.player.config.ads.enabled && !utils.is.empty(this.publisherId);
- }
+ key: "enabled",
+ get: function get() {
+ return this.player.isHTML5 && this.player.isVideo && this.player.config.ads.enabled && !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
- };
-
- var base = 'https://go.aniview.com/api/adserver6/vast/';
-
- return base + '?' + utils.buildUrlParams(params);
- }
+ key: "tagUrl",
+ get: function get() {
+ 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));
+ }
}]);
- return Ads;
-}();
-// ==========================================================================
+ return Ads;
+ }();
-var source = {
+ var source = {
// Add elements to HTML5 media (source, tracks, etc)
insertElements: function insertElements(type, attributes) {
- var _this = this;
+ var _this = this;
- 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);
- });
- }
+ 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;
+ var _this2 = this;
- if (!utils.is.object(input) || !('sources' in input) || !input.sources.length) {
- this.debug.warn('Invalid source format');
- return;
- }
+ if (!getDeep(input, 'sources.length')) {
+ this.debug.warn('Invalid source format');
+ return;
+ } // Cancel current network requests
- // Cancel current network requests
- html5.cancelRequests.call(this);
- // Destroy instance and re-setup
- this.destroy.call(this, function () {
- // Reset quality options
- _this2.options.quality = [];
+ html5.cancelRequests.call(this); // Destroy instance and re-setup
- // Remove elements
- utils.removeElement(_this2.media);
- _this2.media = null;
+ this.destroy.call(this, function () {
+ // Reset quality options
+ _this2.options.quality = []; // Remove elements
- // Reset class name
- if (utils.is.element(_this2.elements.container)) {
- _this2.elements.container.removeAttribute('class');
- }
+ removeElement(_this2.media);
+ _this2.media = null; // Reset class name
- // 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 (is.element(_this2.elements.container)) {
+ _this2.elements.container.removeAttribute('class');
+ } // Set the type and provider
- // Check for support
- _this2.supported = support.check(_this2.type, _this2.provider, _this2.config.playsinline);
- // Create new markup
- switch (_this2.provider + ':' + _this2.type) {
- case 'html5:video':
- _this2.media = utils.createElement('video');
- break;
+ var sources = input.sources,
+ type = input.type;
- case 'html5:audio':
- _this2.media = utils.createElement('audio');
- break;
+ var _sources = _slicedToArray(sources, 1),
+ _sources$ = _sources[0],
+ _sources$$provider = _sources$.provider,
+ provider = _sources$$provider === void 0 ? providers.html5 : _sources$$provider,
+ src = _sources$.src;
- case 'youtube:video':
- case 'vimeo:video':
- _this2.media = utils.createElement('div', {
- src: input.sources[0].src
- });
- break;
+ 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
- default:
- break;
- }
+ _this2.elements.container.appendChild(_this2.media); // Autoplay the new source?
- // Inject the new element
- _this2.elements.container.appendChild(_this2.media);
- // Autoplay the new source?
- if (utils.is.boolean(input.autoplay)) {
- _this2.config.autoplay = input.autoplay;
- }
+ if (is.boolean(input.autoplay)) {
+ _this2.config.autoplay = input.autoplay;
+ } // Set attributes for audio and video
- // 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', '');
- }
- }
- // Restore class hook
- ui.addStyleHook.call(_this2);
+ if (_this2.isHTML5) {
+ if (_this2.config.crossorigin) {
+ _this2.media.setAttribute('crossorigin', '');
+ }
- // Set new sources for html5
- if (_this2.isHTML5) {
- source.insertElements.call(_this2, 'source', input.sources);
- }
+ if (_this2.config.autoplay) {
+ _this2.media.setAttribute('autoplay', '');
+ }
- // Set video title
- _this2.config.title = input.title;
+ if (!is.empty(input.poster)) {
+ _this2.poster = input.poster;
+ }
- // Set up from scratch
- media.setup.call(_this2);
+ if (_this2.config.loop.active) {
+ _this2.media.setAttribute('loop', '');
+ }
- // HTML5 stuff
- if (_this2.isHTML5) {
- // Setup captions
- if ('tracks' in input) {
- source.insertElements.call(_this2, 'track', input.tracks);
- }
+ if (_this2.config.muted) {
+ _this2.media.setAttribute('muted', '');
+ }
- // Load HTML5 sources
- _this2.media.load();
- }
+ if (_this2.config.playsinline) {
+ _this2.media.setAttribute('playsinline', '');
+ }
+ } // Restore class hook
- // 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);
- }
- // Update the fullscreen support
- _this2.fullscreen.update();
- }, true);
- }
-};
+ ui.addStyleHook.call(_this2); // Set new sources for html5
+
+ if (_this2.isHTML5) {
+ source.insertElements.call(_this2, 'source', sources);
+ } // Set video title
+
-// ==========================================================================
+ _this2.config.title = input.title; // Set up from scratch
-// Private properties
-// TODO: Use a WeakMap for private globals
-// const globals = new WeakMap();
+ media.setup.call(_this2); // HTML5 stuff
+
+ 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
+
+
+ if (_this2.isHTML5 || _this2.isEmbed && !_this2.supported.ui) {
+ // Setup interface
+ ui.build.call(_this2);
+ }
-// Plyr instance
+ if (_this2.isHTML5) {
+ // Load HTML5 sources
+ _this2.media.load();
+ } // Update the fullscreen support
-var Plyr = function () {
+
+ _this2.fullscreen.update();
+ }, true);
+ }
+ };
+
+ // TODO: Use a WeakMap for private globals
+ // const globals = new WeakMap();
+ // Plyr instance
+
+ var Plyr =
+ /*#__PURE__*/
+ function () {
function Plyr(target, options) {
- var _this = this;
+ var _this = this;
- classCallCheck(this, Plyr);
+ _classCallCheck(this, Plyr);
- this.timers = {};
+ this.timers = {}; // State
- // State
- this.ready = false;
- this.loading = false;
- this.failed = false;
+ this.ready = false;
+ this.loading = false;
+ this.failed = false; // Touch device
- // Touch device
- this.touch = support.touch;
+ this.touch = support.touch; // Set the media element
- // Set the media element
- this.media = target;
+ this.media = target; // String selector passed
- // String selector passed
- if (utils.is.string(this.media)) {
- this.media = document.querySelectorAll(this.media);
- }
+ if (is.string(this.media)) {
+ this.media = document.querySelectorAll(this.media);
+ } // jQuery, NodeList or Array passed, use first element
- // 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 (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
+
+
+ this.config = extend({}, defaults, Plyr.defaults, options || {}, function () {
+ try {
+ return JSON.parse(_this.media.getAttribute('data-plyr-config'));
+ } catch (e) {
+ return {};
}
+ }()); // Elements cache
- // Set config
- this.config = utils.extend({}, defaults$1, Plyr.defaults, 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
- };
+ 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
- // Captions
- this.captions = {
- active: null,
- currentTrack: null
- };
+ this.options = {
+ speed: [],
+ quality: []
+ }; // Debugging
+ // TODO: move to globals
- // Fullscreen
- this.fullscreen = {
- active: false
- };
+ this.debug = new Console(this.config.debug); // Log config options and support
- // Options
- this.options = {
- speed: [],
- quality: [],
- captions: []
- };
+ this.debug.log('Config', this.config);
+ this.debug.log('Support', support); // We need an element to setup
- // Debugging
- // TODO: move to globals
- this.debug = new Console(this.config.debug);
+ 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
- // Log config options and support
- this.debug.log('Config', this.config);
- this.debug.log('Support', support);
- // 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 (this.media.plyr) {
+ this.debug.warn('Target already setup');
+ return;
+ } // Bail if not enabled
- // Bail if the element is initialized
- if (this.media.plyr) {
- this.debug.warn('Target already setup');
- return;
- }
- // Bail if not enabled
- if (!this.config.enabled) {
- this.debug.error('Setup failed: disabled by config');
- return;
- }
+ 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
- // 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;
- }
- // Cache original element state for .destroy()
- var clone = this.media.cloneNode(true);
- clone.autoplay = false;
- this.elements.original = clone;
+ if (!support.check().api) {
+ this.debug.error('Setup failed: no support');
+ return;
+ } // Cache original element state for .destroy()
- // Set media type based on tag or data attribute
- // Supported: video, audio, vimeo, youtube
- var type = this.media.tagName.toLowerCase();
- // Embed properties
- var iframe = null;
- var url = null;
- var params = null;
+ 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
- // 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 type = this.media.tagName.toLowerCase(); // Embed properties
- // Remove attribute
- this.media.removeAttribute(this.config.attributes.embed.provider);
- }
+ var iframe = null;
+ var url = null; // Different setup based on type
- // Unsupported or missing provider
- if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) {
- this.debug.error('Setup failed: Invalid provider');
- return;
- }
+ switch (type) {
+ case 'div':
+ // Find the frame
+ iframe = this.media.querySelector('iframe'); // <iframe> type
- // Audio will come later for external providers
- this.type = types.video;
+ if (is.element(iframe)) {
+ // Detect provider
+ url = parseUrl(iframe.getAttribute('src'));
+ this.provider = getProviderByUrl(url.toString()); // Rework elements
- break;
+ this.elements.container = this.media;
+ this.media = iframe; // Reset classname
- case 'video':
- case 'audio':
- this.type = type;
- this.provider = providers.html5;
+ this.elements.container.className = ''; // Get attributes from URL and set config
- // 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;
- }
+ if (url.search.length) {
+ var truthy = ['1', 'true'];
- break;
+ if (truthy.includes(url.searchParams.get('autoplay'))) {
+ this.config.autoplay = true;
+ }
- default:
- this.debug.error('Setup failed: unsupported type');
- return;
- }
+ 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
+
+
+ if (this.isYouTube) {
+ this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));
+ this.config.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
- // Check for support again but with type
- this.supported = support.check(this.type, this.provider, this.config.playsinline);
+ this.media.removeAttribute(this.config.attributes.embed.provider);
+ } // Unsupported or missing provider
- // If no support for even API, bail
- if (!this.supported.api) {
- this.debug.error('Setup failed: no support');
+
+ 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
- // Create listeners
- this.listeners = new Listeners(this);
- // Setup local storage for user settings
- this.storage = new Storage(this);
+ this.type = types.video;
+ break;
- // Store reference
- this.media.plyr = this;
+ case 'video':
+ case 'audio':
+ this.type = type;
+ this.provider = providers.html5; // Get config from attributes
- // Wrap media
- if (!utils.is.element(this.elements.container)) {
- this.elements.container = utils.createElement('div');
- utils.wrap(this.media, this.elements.container);
- }
+ if (this.media.hasAttribute('crossorigin')) {
+ this.config.crossorigin = true;
+ }
- // Allow focus to be captured
- this.elements.container.setAttribute('tabindex', 0);
+ if (this.media.hasAttribute('autoplay')) {
+ this.config.autoplay = true;
+ }
- // Add style hook
- ui.addStyleHook.call(this);
+ if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {
+ this.config.playsinline = true;
+ }
- // Setup media
- media.setup.call(this);
+ if (this.media.hasAttribute('muted')) {
+ this.config.muted = true;
+ }
- // 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 (this.media.hasAttribute('loop')) {
+ this.config.loop.active = true;
+ }
- // 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);
- }
+ break;
- // Container listeners
- this.listeners.container();
+ default:
+ this.debug.error('Setup failed: unsupported type');
+ return;
+ } // Check for support again but with type
- // Global listeners
- this.listeners.global();
- // Setup fullscreen
- this.fullscreen = new Fullscreen(this);
+ this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail
- // Setup ads if provided
- this.ads = new Ads(this);
+ if (!this.supported.api) {
+ this.debug.error('Setup failed: no support');
+ return;
+ }
- // Autoplay if required
- if (this.config.autoplay) {
- this.play();
- }
- }
+ this.eventListeners = []; // Create listeners
- // ---------------------------------------
- // API
- // ---------------------------------------
+ this.listeners = new Listeners(this); // Setup local storage for user settings
- /**
- * Types and provider helpers
- */
+ this.storage = new Storage(this); // Store reference
+ this.media.plyr = this; // Wrap media
- createClass(Plyr, [{
- key: 'play',
+ if (!is.element(this.elements.container)) {
+ this.elements.container = createElement('div');
+ wrap(this.media, this.elements.container);
+ } // Add style hook
- /**
- * Play the media, or play the advertisement (if they are not blocked)
- */
- value: function play() {
- if (!utils.is.function(this.media.play)) {
- return null;
- }
+ ui.addStyleHook.call(this); // Setup media
- // Return the promise (for HTML5)
- return this.media.play();
- }
+ media.setup.call(this); // Listen for events if debugging
- /**
- * Pause the media
- */
+ 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
- }, {
- key: 'pause',
- value: function pause() {
- if (!this.playing || !utils.is.function(this.media.pause)) {
- return;
- }
- this.media.pause();
- }
+ if (this.isHTML5 || this.isEmbed && !this.supported.ui) {
+ ui.build.call(this);
+ } // Container listeners
- /**
- * Get playing state
- */
- }, {
- key: 'togglePlay',
+ this.listeners.container(); // Global listeners
+ this.listeners.global(); // Setup fullscreen
- /**
- * 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;
+ this.fullscreen = new Fullscreen(this); // Setup ads if provided
- if (toggle) {
- this.play();
- } else {
- this.pause();
- }
- }
+ if (this.config.ads.enabled) {
+ this.ads = new Ads(this);
+ } // Autoplay if required
- /**
- * Stop playback
- */
- }, {
- key: 'stop',
- value: function stop() {
- if (this.isHTML5) {
- this.pause();
- this.restart();
- } else if (utils.is.function(this.media.stop)) {
- this.media.stop();
- }
- }
+ 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
- /**
- * Restart playback
- */
- }, {
- key: 'restart',
- value: function restart() {
- this.currentTime = 0;
- }
+ this.lastSeekTime = 0;
+ } // ---------------------------------------
+ // API
+ // ---------------------------------------
- /**
- * Rewind
- * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
- */
+ /**
+ * Types and provider helpers
+ */
- }, {
- 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
- */
+ _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)
+
+
+ return this.media.play();
+ }
+ /**
+ * Pause the media
+ */
}, {
- key: 'forward',
- value: function forward(seekTime) {
- this.currentTime = this.currentTime + (utils.is.number(seekTime) ? seekTime : this.config.seekTime);
+ key: "pause",
+ value: function pause() {
+ if (!this.playing || !is.function(this.media.pause)) {
+ return;
}
- /**
- * Seek to a time
- * @param {number} input - where to seek to in seconds. Defaults to 0 (the start)
- */
+ this.media.pause();
+ }
+ /**
+ * Get playing state
+ */
}, {
- key: 'increaseVolume',
+ 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;
- /**
- * 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);
+ if (toggle) {
+ this.play();
+ } else {
+ this.pause();
}
-
- /**
- * Decrease volume
- * @param {boolean} step - How much to decrease by (between 0 and 1)
- */
+ }
+ /**
+ * Stop playback
+ */
}, {
- key: 'decreaseVolume',
- value: function decreaseVolume(step) {
- var volume = this.media.muted ? 0 : this.volume;
- this.volume = volume - (utils.is.number(step) ? step : 1);
+ key: "stop",
+ value: function stop() {
+ if (this.isHTML5) {
+ this.pause();
+ this.restart();
+ } else if (is.function(this.media.stop)) {
+ this.media.stop();
}
-
- /**
- * Set muted state
- * @param {boolean} mute
- */
+ }
+ /**
+ * Restart playback
+ */
}, {
- key: 'toggleCaptions',
-
+ key: "restart",
+ value: function restart() {
+ this.currentTime = 0;
+ }
+ /**
+ * Rewind
+ * @param {number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime
+ */
- /**
- * Toggle captions
- * @param {boolean} input - Whether to enable captions
- */
- value: function toggleCaptions(input) {
- // If there's no full support
- if (!this.supported.ui) {
- return;
- }
+ }, {
+ 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
+ */
- // 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);
+ }, {
+ 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)
+ */
- // Nothing to change...
- if (this.captions.active === show) {
- return;
- }
+ }, {
+ 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)
+ */
- // Set global
- this.captions.active = show;
+ }, {
+ key: "decreaseVolume",
+ value: function decreaseVolume(step) {
+ this.increaseVolume(-step);
+ }
+ /**
+ * Set muted state
+ * @param {boolean} mute
+ */
- // Toggle state
- utils.toggleState(this.elements.buttons.captions, this.captions.active);
+ }, {
+ 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
+ */
- // Add class hook
- utils.toggleClass(this.elements.container, this.config.classNames.captions.active, this.captions.active);
+ }, {
+ key: "airplay",
- // Trigger an event
- utils.dispatchEvent.call(this, this.media, this.captions.active ? 'captionsenabled' : 'captionsdisabled');
+ /**
+ * Trigger the airplay dialog
+ * TODO: update player with state, support, enabled
+ */
+ value: function airplay() {
+ // Show dialog if supported
+ if (support.airplay) {
+ this.media.webkitShowPlaybackTargetPicker();
}
-
- /**
- * Set the captions language
- * @param {string} - Two character ISO language code (e.g. EN, FR, PT, etc)
- */
+ }
+ /**
+ * Toggle the player controls
+ * @param {boolean} [toggle] - Whether to show the controls
+ */
}, {
- key: 'airplay',
+ 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
- /**
- * Trigger the airplay dialog
- * TODO: update player with state, support, enabled
- */
- value: function airplay() {
- // Show dialog if supported
- if (support.airplay) {
- this.media.webkitShowPlaybackTargetPicker();
- }
- }
+ var hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force); // Close menu
- /**
- * Toggle the player controls
- * @param {boolean} [toggle] - Whether to show the controls
- */
+ if (hiding && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {
+ controls.toggleMenu.call(this, false);
+ } // Trigger event on change
- }, {
- 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 = utils.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 = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
-
- // Close menu
- if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
- controls.toggleMenu.call(this, false);
- }
- // Trigger event on change
- if (hiding !== isHidden) {
- var eventName = hiding ? 'controlshidden' : 'controlsshown';
- utils.dispatchEvent.call(this, this.media, eventName);
- }
- return !hiding;
- }
- return false;
- }
- /**
- * Add event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
- */
+ if (hiding !== isHidden) {
+ var eventName = hiding ? 'controlshidden' : 'controlsshown';
+ triggerEvent.call(this, this.media, eventName);
+ }
- }, {
- key: 'on',
- value: function on(event, callback) {
- utils.on(this.elements.container, event, callback);
+ return !hiding;
}
- /**
- * Remove event listeners
- * @param {string} event - Event type
- * @param {function} callback - Callback for when event occurs
- */
+ return false;
+ }
+ /**
+ * Add event listeners
+ * @param {string} event - Event type
+ * @param {function} callback - Callback for when event occurs
+ */
}, {
- key: 'off',
- value: function off(event, callback) {
- utils.off(this.elements.container, event, callback);
- }
+ 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
+ */
- /**
- * 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)
- */
+ }, {
+ 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
+ */
}, {
- key: 'destroy',
- value: function destroy(callback) {
- var _this2 = this;
+ 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)
+ */
- var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
+ }, {
+ key: "destroy",
+ value: function destroy(callback) {
+ var _this2 = this;
- if (!this.ready) {
- return;
- }
+ var soft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
- var done = function done() {
- // Reset overflow (incase destroyed while in fullscreen)
- document.body.style.overflow = '';
-
- // GC for embed
- _this2.embed = null;
-
- // If it's a soft destroy, make minimal changes
- if (soft) {
- if (Object.keys(_this2.elements).length) {
- // Remove elements
- utils.removeElement(_this2.elements.buttons.play);
- utils.removeElement(_this2.elements.captions);
- utils.removeElement(_this2.elements.controls);
- utils.removeElement(_this2.elements.wrapper);
-
- // Clear for GC
- _this2.elements.buttons.play = null;
- _this2.elements.captions = null;
- _this2.elements.controls = null;
- _this2.elements.wrapper = null;
- }
-
- // Callback
- if (utils.is.function(callback)) {
- callback();
- }
- } else {
- // Unbind listeners
- _this2.listeners.clear();
+ if (!this.ready) {
+ return;
+ }
- // Replace the container with the original element provided
- utils.replaceElement(_this2.elements.original, _this2.elements.container);
+ var done = function done() {
+ // Reset overflow (incase destroyed while in fullscreen)
+ document.body.style.overflow = ''; // GC for embed
- // Event
- utils.dispatchEvent.call(_this2, _this2.elements.original, 'destroyed', true);
+ _this2.embed = null; // If it's a soft destroy, make minimal changes
- // Callback
- if (utils.is.function(callback)) {
- callback.call(_this2.elements.original);
- }
+ 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
- // Reset state
- _this2.ready = false;
+ _this2.elements.buttons.play = null;
+ _this2.elements.captions = null;
+ _this2.elements.controls = null;
+ _this2.elements.wrapper = null;
+ } // Callback
- // Clear for garbage collection
- setTimeout(function () {
- _this2.elements = null;
- _this2.media = null;
- }, 200);
- }
- };
- // Stop playback
- this.stop();
+ if (is.function(callback)) {
+ callback();
+ }
+ } else {
+ // Unbind listeners
+ unbindListeners.call(_this2); // Replace the container with the original element provided
- // Type specific stuff
- switch (this.provider + ':' + this.type) {
- case 'html5:video':
- case 'html5:audio':
- // Clear timeout
- clearTimeout(this.timers.loading);
+ replaceElement(_this2.elements.original, _this2.elements.container); // Event
- // Restore native video controls
- ui.toggleNativeControls.call(this, true);
+ triggerEvent.call(_this2, _this2.elements.original, 'destroyed', true); // Callback
- // Clean up
- done();
+ if (is.function(callback)) {
+ callback.call(_this2.elements.original);
+ } // Reset state
- break;
- case 'youtube:video':
- // Clear timers
- clearInterval(this.timers.buffering);
- clearInterval(this.timers.playing);
+ _this2.ready = false; // Clear for garbage collection
- // Destroy YouTube API
- if (this.embed !== null && utils.is.function(this.embed.destroy)) {
- this.embed.destroy();
- }
+ setTimeout(function () {
+ _this2.elements = null;
+ _this2.media = null;
+ }, 200);
+ }
+ }; // Stop playback
- // Clean up
- done();
- break;
+ this.stop(); // Provider specific stuff
- 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.isHTML5) {
+ // Clear timeout
+ clearTimeout(this.timers.loading); // Restore native video controls
- // Vimeo does not always return
- setTimeout(done, 200);
+ ui.toggleNativeControls.call(this, true); // Clean up
- break;
+ done();
+ } else if (this.isYouTube) {
+ // Clear timers
+ clearInterval(this.timers.buffering);
+ clearInterval(this.timers.playing); // Destroy YouTube API
- default:
- break;
- }
- }
+ if (this.embed !== null && is.function(this.embed.destroy)) {
+ this.embed.destroy();
+ } // Clean up
- /**
- * Check for support for a mime type (HTML5 only)
- * @param {string} type - Mime type
- */
- }, {
- key: 'supports',
- value: function supports(type) {
- return support.mime.call(this, type);
+ 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
+
+
+ setTimeout(done, 200);
}
+ }
+ /**
+ * Check for support for a mime type (HTML5 only)
+ * @param {string} type - Mime 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
- */
+ }, {
+ 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
+ */
}, {
- key: 'isHTML5',
- get: function get$$1() {
- return Boolean(this.provider === providers.html5);
- }
+ key: "isHTML5",
+ get: function get() {
+ return Boolean(this.provider === providers.html5);
+ }
}, {
- key: 'isEmbed',
- get: function get$$1() {
- return Boolean(this.isYouTube || this.isVimeo);
- }
+ key: "isEmbed",
+ get: function get() {
+ return Boolean(this.isYouTube || this.isVimeo);
+ }
}, {
- key: 'isYouTube',
- get: function get$$1() {
- return Boolean(this.provider === providers.youtube);
- }
+ key: "isYouTube",
+ get: function get() {
+ return Boolean(this.provider === providers.youtube);
+ }
}, {
- key: 'isVimeo',
- get: function get$$1() {
- return Boolean(this.provider === providers.vimeo);
- }
+ key: "isVimeo",
+ get: function get() {
+ return Boolean(this.provider === providers.vimeo);
+ }
}, {
- key: 'isVideo',
- get: function get$$1() {
- return Boolean(this.type === types.video);
- }
+ key: "isVideo",
+ get: function get() {
+ return Boolean(this.type === types.video);
+ }
}, {
- key: 'isAudio',
- get: function get$$1() {
- return Boolean(this.type === types.audio);
- }
+ key: "isAudio",
+ get: function get() {
+ return Boolean(this.type === types.audio);
+ }
}, {
- key: 'playing',
- get: function get$$1() {
- return Boolean(this.ready && !this.paused && !this.ended);
- }
-
- /**
- * Get paused state
- */
+ key: "playing",
+ get: function get() {
+ return Boolean(this.ready && !this.paused && !this.ended);
+ }
+ /**
+ * Get paused state
+ */
}, {
- key: 'paused',
- get: function get$$1() {
- return Boolean(this.media.paused);
- }
-
- /**
- * Get stopped state
- */
+ key: "paused",
+ get: function get() {
+ return Boolean(this.media.paused);
+ }
+ /**
+ * Get stopped state
+ */
}, {
- key: 'stopped',
- get: function get$$1() {
- return Boolean(this.paused && this.currentTime === 0);
- }
-
- /**
- * Get ended state
- */
+ key: "stopped",
+ get: function get() {
+ return Boolean(this.paused && this.currentTime === 0);
+ }
+ /**
+ * Get ended state
+ */
}, {
- key: 'ended',
- get: function get$$1() {
- return Boolean(this.media.ended);
- }
+ key: "ended",
+ get: function get() {
+ return Boolean(this.media.ended);
+ }
}, {
- key: 'currentTime',
- set: function set$$1(input) {
- var targetTime = 0;
+ key: "currentTime",
+ set: function set(input) {
+ // Bail if media duration isn't available yet
+ if (!this.duration) {
+ return;
+ } // Validate input
- if (utils.is.number(input)) {
- targetTime = input;
- }
-
- // Normalise targetTime
- if (targetTime < 0) {
- targetTime = 0;
- } else if (targetTime > this.duration) {
- targetTime = this.duration;
- }
- // Set
- this.media.currentTime = targetTime;
+ var inputIsValid = is.number(input) && input > 0; // Set
- // Logging
- this.debug.log('Seeking to ' + this.currentTime + ' seconds');
- }
+ this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0; // Logging
- /**
- * Get current time
- */
- ,
- get: function get$$1() {
- return Number(this.media.currentTime);
- }
-
- /**
- * Get buffered
- */
+ this.debug.log("Seeking to ".concat(this.currentTime, " seconds"));
+ }
+ /**
+ * Get current time
+ */
+ ,
+ get: function get() {
+ return Number(this.media.currentTime);
+ }
+ /**
+ * Get buffered
+ */
}, {
- key: 'buffered',
- get: function get$$1() {
- var buffered = this.media.buffered;
+ key: "buffered",
+ get: function get() {
+ var buffered = this.media.buffered; // YouTube / Vimeo return a float between 0-1
- // YouTube / Vimeo return a float between 0-1
+ if (is.number(buffered)) {
+ return buffered;
+ } // HTML5
+ // TODO: Handle buffered chunks of the media
+ // (i.e. seek to another section buffers only that section)
- 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;
- }
- return 0;
+ if (buffered && buffered.length && this.duration > 0) {
+ return buffered.end(0) / this.duration;
}
- /**
- * Get seeking status
- */
+ return 0;
+ }
+ /**
+ * Get seeking status
+ */
+
+ }, {
+ key: "seeking",
+ get: function get() {
+ return Boolean(this.media.seeking);
+ }
+ /**
+ * Get the duration of the current media
+ */
}, {
- key: 'seeking',
- get: function get$$1() {
- return Boolean(this.media.seeking);
- }
+ 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
- /**
- * Get the duration of the current media
- */
+ var realDuration = (this.media || {}).duration;
+ var duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration; // If config duration is funky, use regular duration
+
+ 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
+ */
}, {
- key: 'duration',
- get: function get$$1() {
- // Faux duration set via config
- var fauxDuration = parseFloat(this.config.duration);
+ key: "volume",
+ set: function set(value) {
+ var volume = value;
+ var max = 1;
+ var min = 0;
- // True duration
- var realDuration = this.media ? Number(this.media.duration) : 0;
+ if (is.string(volume)) {
+ volume = Number(volume);
+ } // Load volume from storage if no value specified
- // 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
- */
+ if (!is.number(volume)) {
+ volume = this.storage.get('volume');
+ } // Use config if all else fails
- }, {
- key: 'volume',
- set: function set$$1(value) {
- var volume = value;
- var max = 1;
- var min = 0;
-
- if (utils.is.string(volume)) {
- volume = Number(volume);
- }
- // Load volume from storage if no value specified
- if (!utils.is.number(volume)) {
- volume = this.storage.get('volume');
- }
+ if (!is.number(volume)) {
+ volume = this.config.volume;
+ } // Maximum is volumeMax
- // Use config if all else fails
- if (!utils.is.number(volume)) {
- volume = this.config.volume;
- }
- // Maximum is volumeMax
- if (volume > max) {
- volume = max;
- }
- // Minimum is volumeMin
- if (volume < min) {
- volume = min;
- }
+ if (volume > max) {
+ volume = max;
+ } // Minimum is volumeMin
- // Update config
- this.config.volume = volume;
- // Set the player volume
- this.media.volume = volume;
+ if (volume < min) {
+ volume = min;
+ } // Update config
- // 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);
+ this.config.volume = volume; // Set the player volume
+
+ this.media.volume = volume; // If muted, and we're increasing volume manually, reset muted state
+
+ 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$$1(mute) {
- var toggle = mute;
+ key: "muted",
+ set: function set(mute) {
+ var toggle = mute; // Load muted state from storage
- // Load muted state from storage
- if (!utils.is.boolean(toggle)) {
- toggle = this.storage.get('muted');
- }
+ if (!is.boolean(toggle)) {
+ toggle = this.storage.get('muted');
+ } // Use config if all else fails
- // Use config if all else fails
- if (!utils.is.boolean(toggle)) {
- toggle = this.config.muted;
- }
- // Update config
- this.config.muted = toggle;
+ if (!is.boolean(toggle)) {
+ toggle = this.config.muted;
+ } // Update config
- // Set mute on the player
- this.media.muted = toggle;
- }
- /**
- * Get current muted state
- */
- ,
- get: function get$$1() {
- return Boolean(this.media.muted);
- }
+ this.config.muted = toggle; // Set mute on the player
- /**
- * Check if the media has audio
- */
+ this.media.muted = toggle;
+ }
+ /**
+ * Get current muted state
+ */
+ ,
+ get: function get() {
+ return Boolean(this.media.muted);
+ }
+ /**
+ * Check if the media has audio
+ */
}, {
- key: 'hasAudio',
- get: function get$$1() {
- // Assume yes for all non HTML5 (as we can't tell...)
- if (!this.isHTML5) {
- return true;
- }
-
- if (this.isAudio) {
- return true;
- }
-
- // Get audio tracks
- return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);
+ key: "hasAudio",
+ get: function get() {
+ // Assume yes for all non HTML5 (as we can't tell...)
+ if (!this.isHTML5) {
+ return true;
}
- /**
- * Set playback speed
- * @param {number} speed - the speed of playback (0.5-2.0)
- */
+ if (this.isAudio) {
+ return true;
+ } // Get audio tracks
- }, {
- key: 'speed',
- set: function set$$1(input) {
- var speed = null;
- if (utils.is.number(input)) {
- speed = input;
- }
+ 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)
+ */
- if (!utils.is.number(speed)) {
- speed = this.storage.get('speed');
- }
+ }, {
+ key: "speed",
+ set: function set(input) {
+ var speed = null;
- if (!utils.is.number(speed)) {
- speed = this.config.speed.selected;
- }
+ if (is.number(input)) {
+ speed = input;
+ }
- // Set min/max
- if (speed < 0.1) {
- speed = 0.1;
- }
- if (speed > 2.0) {
- speed = 2.0;
- }
+ if (!is.number(speed)) {
+ speed = this.storage.get('speed');
+ }
- if (!this.config.speed.options.includes(speed)) {
- this.debug.warn('Unsupported speed (' + speed + ')');
- return;
- }
+ if (!is.number(speed)) {
+ speed = this.config.speed.selected;
+ } // Set min/max
- // Update config
- this.config.speed.selected = speed;
- // Set media speed
- this.media.playbackRate = speed;
+ if (speed < 0.1) {
+ speed = 0.1;
}
- /**
- * Get current playback speed
- */
- ,
- get: function get$$1() {
- return Number(this.media.playbackRate);
+ if (speed > 2.0) {
+ speed = 2.0;
}
- /**
- * Set playback quality
- * Currently HTML5 & YouTube only
- * @param {number} input - Quality level
- */
+ if (!this.config.speed.options.includes(speed)) {
+ this.debug.warn("Unsupported speed (".concat(speed, ")"));
+ return;
+ } // Update config
- }, {
- key: 'quality',
- set: function set$$1(input) {
- var quality = null;
- if (!utils.is.empty(input)) {
- quality = Number(input);
- }
+ this.config.speed.selected = speed; // Set media speed
- if (!utils.is.number(quality) || quality === 0) {
- quality = this.storage.get('quality');
- }
+ 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
+ */
- if (!utils.is.number(quality)) {
- quality = this.config.quality.selected;
- }
+ }, {
+ key: "quality",
+ set: function set(input) {
+ var config = this.config.quality;
+ var options = this.options.quality;
- if (!utils.is.number(quality)) {
- quality = this.config.quality.default;
- }
+ if (!options.length) {
+ return;
+ }
- if (!this.options.quality.length) {
- return;
- }
+ var quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);
+ var updateStorage = true;
- 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;
- }
+ 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
- // Update config
- this.config.quality.selected = quality;
+ updateStorage = false;
+ } // Update config
- // Set quality
- this.media.quality = quality;
- }
- /**
- * Get current quality level
- */
- ,
- get: function get$$1() {
- return this.media.quality;
- }
+ config.selected = quality; // Set 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.media.quality = quality; // Save to storage
+
+ 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
+ */
}, {
- 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:
+ 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;
- break;
- } */
- }
-
- /**
- * Get current loop state
- */
- ,
- get: function get$$1() {
- return Boolean(this.media.loop);
- }
-
- /**
- * Set new media source
- * @param {object} input - The new source object (see docs)
- */
+ } 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)
+ */
}, {
- key: 'source',
- set: function set$$1(input) {
- source.change.call(this, input);
- }
-
- /**
- * Get current source
- */
- ,
- get: function get$$1() {
- return this.media.currentSrc;
- }
-
- /**
- * Set the poster image for a video
- * @param {input} - the URL for the new poster image
- */
+ 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)
+ */
}, {
- key: 'poster',
- set: function set$$1(input) {
- if (!this.isVideo) {
- this.debug.warn('Poster can only be set for video');
- return;
- }
+ 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
+ */
- ui.setPoster.call(this, input);
+ }, {
+ key: "poster",
+ set: function set(input) {
+ if (!this.isVideo) {
+ this.debug.warn('Poster can only be set for video');
+ return;
}
- /**
- * Get the current poster image
- */
- ,
- get: function get$$1() {
- if (!this.isVideo) {
- return null;
- }
-
- return this.media.getAttribute('poster');
+ ui.setPoster.call(this, input, false).catch(function () {});
+ }
+ /**
+ * Get the current poster image
+ */
+ ,
+ get: function get() {
+ if (!this.isVideo) {
+ return null;
}
- /**
- * Set the autoplay state
- * @param {boolean} input - Whether to autoplay or not
- */
+ return this.media.getAttribute('poster');
+ }
+ /**
+ * Set the autoplay state
+ * @param {boolean} input - Whether to autoplay or not
+ */
}, {
- key: 'autoplay',
- set: function set$$1(input) {
- var toggle = utils.is.boolean(input) ? input : this.config.autoplay;
- this.config.autoplay = toggle;
- }
-
- /**
- * Get the current autoplay state
- */
- ,
- get: function get$$1() {
- return Boolean(this.config.autoplay);
- }
+ 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: '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;
- }
-
- // Normalize
- var language = input.toLowerCase();
-
- // Check for support
- if (!this.options.captions.includes(language)) {
- this.debug.warn('Unsupported language option: ' + language);
- return;
- }
-
- // Ensure captions are enabled
- this.toggleCaptions(true);
+ 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)
+ */
- // Enabled only
- if (language === 'enabled') {
- 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
+ */
- // If nothing to change, bail
- if (this.language === language) {
- return;
- }
+ }, {
+ key: "pip",
+ set: function set(input) {
+ // Bail if no support
+ if (!support.pip) {
+ return;
+ } // Toggle based on current state if not passed
- // Update config
- this.captions.language = language;
- // Clear caption
- captions.setText.call(this, null);
+ var toggle = is.boolean(input) ? input : !this.pip; // Toggle based on current state
+ // Safari
- // Update captions
- captions.setLanguage.call(this);
+ if (is.function(this.media.webkitSetPresentationMode)) {
+ this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);
+ } // Chrome
- // Trigger an event
- utils.dispatchEvent.call(this, this.media, 'languagechange');
- }
- /**
- * Get the current captions language
- */
- ,
- get: function get$$1() {
- return this.captions.language;
+ 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
- /**
- * Toggle picture-in-picture playback on WebKit/MacOS
- * TODO: update player with state, support, enabled
- * TODO: detect outside changes
- */
-
- }, {
- key: 'pip',
- set: function set$$1(input) {
- var states = {
- pip: 'picture-in-picture',
- inline: 'inline'
- };
-
- // Bail if no support
- if (!support.pip) {
- return;
- }
-
- // Toggle based on current state if not passed
- var toggle = utils.is.boolean(input) ? input : this.pip === states.inline;
- // Toggle based on current state
- this.media.webkitSetPresentationMode(toggle ? states.pip : states.inline);
- }
+ if (!is.empty(this.media.webkitPresentationMode)) {
+ return this.media.webkitPresentationMode === pip.active;
+ } // Chrome
- /**
- * Get the current picture-in-picture state
- */
- ,
- get: function get$$1() {
- if (!support.pip) {
- return null;
- }
- return this.media.webkitPresentationMode;
- }
+ 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: "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: 'loadSprite',
- value: function loadSprite(url, id) {
- return utils.loadSprite(url, id);
- }
-
- /**
- * Setup multiple instances
- * @param {*} selector
- * @param {object} options
- */
+ key: "loadSprite",
+ value: function loadSprite$$1(url, id) {
+ return loadSprite(url, id);
+ }
+ /**
+ * Setup multiple instances
+ * @param {*} selector
+ * @param {object} options
+ */
}, {
- 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: "setup",
+ value: function setup(selector) {
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+ var targets = null;
- if (utils.is.empty(targets)) {
- return null;
- }
+ 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);
+ }
- return targets.map(function (t) {
- return new Plyr(t, options);
- });
+ if (is.empty(targets)) {
+ return null;
}
+
+ return targets.map(function (t) {
+ return new Plyr(t, options);
+ });
+ }
}]);
+
return Plyr;
-}();
+ }();
-Plyr.defaults = utils.cloneDeep(defaults$1);
+ Plyr.defaults = cloneDeep(defaults);
-return Plyr;
+ return Plyr;
})));