/**
* @license
* Video.js 8.17.4
* Copyright Brightcove, Inc.
* Available under Apache License Version 2.0
*
*
* Includes vtt.js
* Available under Apache License Version 2.0
*
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
})(this, (function () { 'use strict';
var version = "8.17.4";
/**
* An Object that contains lifecycle hooks as keys which point to an array
* of functions that are run when a lifecycle is triggered
*
* @private
*/
const hooks_ = {};
/**
* Get a list of hooks for a specific lifecycle
*
* @param {string} type
* the lifecycle to get hooks from
*
* @param {Function|Function[]} [fn]
* Optionally add a hook (or hooks) to the lifecycle that your are getting.
*
* @return {Array}
* an array of hooks, or an empty array if there are none.
*/
const hooks = function (type, fn) {
hooks_[type] = hooks_[type] || [];
if (fn) {
hooks_[type] = hooks_[type].concat(fn);
}
return hooks_[type];
};
/**
* Add a function hook to a specific videojs lifecycle.
*
* @param {string} type
* the lifecycle to hook the function to.
*
* @param {Function|Function[]}
* The function or array of functions to attach.
*/
const hook = function (type, fn) {
hooks(type, fn);
};
/**
* Remove a hook from a specific videojs lifecycle.
*
* @param {string} type
* the lifecycle that the function hooked to
*
* @param {Function} fn
* The hooked function to remove
*
* @return {boolean}
* The function that was removed or undef
*/
const removeHook = function (type, fn) {
const index = hooks(type).indexOf(fn);
if (index <= -1) {
return false;
}
hooks_[type] = hooks_[type].slice();
hooks_[type].splice(index, 1);
return true;
};
/**
* Add a function hook that will only run once to a specific videojs lifecycle.
*
* @param {string} type
* the lifecycle to hook the function to.
*
* @param {Function|Function[]}
* The function or array of functions to attach.
*/
const hookOnce = function (type, fn) {
hooks(type, [].concat(fn).map(original => {
const wrapper = (...args) => {
removeHook(type, wrapper);
return original(...args);
};
return wrapper;
}));
};
/**
* @file fullscreen-api.js
* @module fullscreen-api
*/
/**
* Store the browser-specific methods for the fullscreen API.
*
* @type {Object}
* @see [Specification]{@link https://fullscreen.spec.whatwg.org}
* @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
*/
const FullscreenApi = {
prefixed: true
};
// browser API methods
const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
// WebKit
['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
const specApi = apiMap[0];
let browserApi;
// determine the supported set of functions
for (let i = 0; i < apiMap.length; i++) {
// check for exitFullscreen function
if (apiMap[i][1] in document) {
browserApi = apiMap[i];
break;
}
}
// map the browser API names to the spec API names
if (browserApi) {
for (let i = 0; i < browserApi.length; i++) {
FullscreenApi[specApi[i]] = browserApi[i];
}
FullscreenApi.prefixed = browserApi[0] !== specApi[0];
}
/**
* @file create-logger.js
* @module create-logger
*/
// This is the private tracking variable for the logging history.
let history = [];
/**
* Log messages to the console and history based on the type of message
*
* @private
* @param {string} name
* The name of the console method to use.
*
* @param {Object} log
* The arguments to be passed to the matching console method.
*
* @param {string} [styles]
* styles for name
*/
const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
const lvl = log.levels[level];
const lvlRegExp = new RegExp(`^(${lvl})$`);
let resultName = name;
if (type !== 'log') {
// Add the type to the front of the message when it's not "log".
args.unshift(type.toUpperCase() + ':');
}
if (styles) {
resultName = `%c${name}`;
args.unshift(styles);
}
// Add console prefix after adding to history.
args.unshift(resultName + ':');
// Add a clone of the args at this point to history.
if (history) {
history.push([].concat(args));
// only store 1000 history entries
const splice = history.length - 1000;
history.splice(0, splice > 0 ? splice : 0);
}
// If there's no console then don't try to output messages, but they will
// still be stored in history.
if (!window.console) {
return;
}
// Was setting these once outside of this function, but containing them
// in the function makes it easier to test cases where console doesn't exist
// when the module is executed.
let fn = window.console[type];
if (!fn && type === 'debug') {
// Certain browsers don't have support for console.debug. For those, we
// should default to the closest comparable log.
fn = window.console.info || window.console.log;
}
// Bail out if there's no console or if this type is not allowed by the
// current logging level.
if (!fn || !lvl || !lvlRegExp.test(type)) {
return;
}
fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
};
function createLogger$1(name, delimiter = ':', styles = '') {
// This is the private tracking variable for logging level.
let level = 'info';
// the curried logByType bound to the specific log and history
let logByType;
/**
* Logs plain debug messages. Similar to `console.log`.
*
* Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
* of our JSDoc template, we cannot properly document this as both a function
* and a namespace, so its function signature is documented here.
*
* #### Arguments
* ##### *args
* *[]
*
* Any combination of values that could be passed to `console.log()`.
*
* #### Return Value
*
* `undefined`
*
* @namespace
* @param {...*} args
* One or more messages or objects that should be logged.
*/
function log(...args) {
logByType('log', level, args);
}
// This is the logByType helper that the logging methods below use
logByType = LogByTypeFactory(name, log, styles);
/**
* Create a new subLogger which chains the old name to the new name.
*
* For example, doing `mylogger = videojs.log.createLogger('player')` and then using that logger will log the following:
* ```js
* mylogger('foo');
* // > VIDEOJS: player: foo
* ```
*
* @param {string} subName
* The name to add call the new logger
* @param {string} [subDelimiter]
* Optional delimiter
* @param {string} [subStyles]
* Optional styles
* @return {Object}
*/
log.createLogger = (subName, subDelimiter, subStyles) => {
const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
const resultStyles = subStyles !== undefined ? subStyles : styles;
const resultName = `${name} ${resultDelimiter} ${subName}`;
return createLogger$1(resultName, resultDelimiter, resultStyles);
};
/**
* Create a new logger.
*
* @param {string} newName
* The name for the new logger
* @param {string} [newDelimiter]
* Optional delimiter
* @param {string} [newStyles]
* Optional styles
* @return {Object}
*/
log.createNewLogger = (newName, newDelimiter, newStyles) => {
return createLogger$1(newName, newDelimiter, newStyles);
};
/**
* Enumeration of available logging levels, where the keys are the level names
* and the values are `|`-separated strings containing logging methods allowed
* in that logging level. These strings are used to create a regular expression
* matching the function name being called.
*
* Levels provided by Video.js are:
*
* - `off`: Matches no calls. Any value that can be cast to `false` will have
* this effect. The most restrictive.
* - `all`: Matches only Video.js-provided functions (`debug`, `log`,
* `log.warn`, and `log.error`).
* - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
* - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
* - `warn`: Matches `log.warn` and `log.error` calls.
* - `error`: Matches only `log.error` calls.
*
* @type {Object}
*/
log.levels = {
all: 'debug|log|warn|error',
off: '',
debug: 'debug|log|warn|error',
info: 'log|warn|error',
warn: 'warn|error',
error: 'error',
DEFAULT: level
};
/**
* Get or set the current logging level.
*
* If a string matching a key from {@link module:log.levels} is provided, acts
* as a setter.
*
* @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
* Pass a valid level to set a new logging level.
*
* @return {string}
* The current logging level.
*/
log.level = lvl => {
if (typeof lvl === 'string') {
if (!log.levels.hasOwnProperty(lvl)) {
throw new Error(`"${lvl}" in not a valid log level`);
}
level = lvl;
}
return level;
};
/**
* Returns an array containing everything that has been logged to the history.
*
* This array is a shallow clone of the internal history record. However, its
* contents are _not_ cloned; so, mutating objects inside this array will
* mutate them in history.
*
* @return {Array}
*/
log.history = () => history ? [].concat(history) : [];
/**
* Allows you to filter the history by the given logger name
*
* @param {string} fname
* The name to filter by
*
* @return {Array}
* The filtered list to return
*/
log.history.filter = fname => {
return (history || []).filter(historyItem => {
// if the first item in each historyItem includes `fname`, then it's a match
return new RegExp(`.*${fname}.*`).test(historyItem[0]);
});
};
/**
* Clears the internal history tracking, but does not prevent further history
* tracking.
*/
log.history.clear = () => {
if (history) {
history.length = 0;
}
};
/**
* Disable history tracking if it is currently enabled.
*/
log.history.disable = () => {
if (history !== null) {
history.length = 0;
history = null;
}
};
/**
* Enable history tracking if it is currently disabled.
*/
log.history.enable = () => {
if (history === null) {
history = [];
}
};
/**
* Logs error messages. Similar to `console.error`.
*
* @param {...*} args
* One or more messages or objects that should be logged as an error
*/
log.error = (...args) => logByType('error', level, args);
/**
* Logs warning messages. Similar to `console.warn`.
*
* @param {...*} args
* One or more messages or objects that should be logged as a warning.
*/
log.warn = (...args) => logByType('warn', level, args);
/**
* Logs debug messages. Similar to `console.debug`, but may also act as a comparable
* log if `console.debug` is not available
*
* @param {...*} args
* One or more messages or objects that should be logged as debug.
*/
log.debug = (...args) => logByType('debug', level, args);
return log;
}
/**
* @file log.js
* @module log
*/
const log = createLogger$1('VIDEOJS');
const createLogger = log.createLogger;
/**
* @file obj.js
* @module obj
*/
/**
* @callback obj:EachCallback
*
* @param {*} value
* The current key for the object that is being iterated over.
*
* @param {string} key
* The current key-value for object that is being iterated over
*/
/**
* @callback obj:ReduceCallback
*
* @param {*} accum
* The value that is accumulating over the reduce loop.
*
* @param {*} value
* The current key for the object that is being iterated over.
*
* @param {string} key
* The current key-value for object that is being iterated over
*
* @return {*}
* The new accumulated value.
*/
const toString$1 = Object.prototype.toString;
/**
* Get the keys of an Object
*
* @param {Object}
* The Object to get the keys from
*
* @return {string[]}
* An array of the keys from the object. Returns an empty array if the
* object passed in was invalid or had no keys.
*
* @private
*/
const keys = function (object) {
return isObject(object) ? Object.keys(object) : [];
};
/**
* Array-like iteration for objects.
*
* @param {Object} object
* The object to iterate over
*
* @param {obj:EachCallback} fn
* The callback function which is called for each key in the object.
*/
function each(object, fn) {
keys(object).forEach(key => fn(object[key], key));
}
/**
* Array-like reduce for objects.
*
* @param {Object} object
* The Object that you want to reduce.
*
* @param {Function} fn
* A callback function which is called for each key in the object. It
* receives the accumulated value and the per-iteration value and key
* as arguments.
*
* @param {*} [initial = 0]
* Starting value
*
* @return {*}
* The final accumulated value.
*/
function reduce(object, fn, initial = 0) {
return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
}
/**
* Returns whether a value is an object of any kind - including DOM nodes,
* arrays, regular expressions, etc. Not functions, though.
*
* This avoids the gotcha where using `typeof` on a `null` value
* results in `'object'`.
*
* @param {Object} value
* @return {boolean}
*/
function isObject(value) {
return !!value && typeof value === 'object';
}
/**
* Returns whether an object appears to be a "plain" object - that is, a
* direct instance of `Object`.
*
* @param {Object} value
* @return {boolean}
*/
function isPlain(value) {
return isObject(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object;
}
/**
* Merge two objects recursively.
*
* Performs a deep merge like
* {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
* plain objects (not arrays, elements, or anything else).
*
* Non-plain object values will be copied directly from the right-most
* argument.
*
* @param {Object[]} sources
* One or more objects to merge into a new object.
*
* @return {Object}
* A new object that is the merged result of all sources.
*/
function merge(...sources) {
const result = {};
sources.forEach(source => {
if (!source) {
return;
}
each(source, (value, key) => {
if (!isPlain(value)) {
result[key] = value;
return;
}
if (!isPlain(result[key])) {
result[key] = {};
}
result[key] = merge(result[key], value);
});
});
return result;
}
/**
* Returns an array of values for a given object
*
* @param {Object} source - target object
* @return {Array} - object values
*/
function values(source = {}) {
const result = [];
for (const key in source) {
if (source.hasOwnProperty(key)) {
const value = source[key];
result.push(value);
}
}
return result;
}
/**
* Object.defineProperty but "lazy", which means that the value is only set after
* it is retrieved the first time, rather than being set right away.
*
* @param {Object} obj the object to set the property on
* @param {string} key the key for the property to set
* @param {Function} getValue the function used to get the value when it is needed.
* @param {boolean} setter whether a setter should be allowed or not
*/
function defineLazyProperty(obj, key, getValue, setter = true) {
const set = value => Object.defineProperty(obj, key, {
value,
enumerable: true,
writable: true
});
const options = {
configurable: true,
enumerable: true,
get() {
const value = getValue();
set(value);
return value;
}
};
if (setter) {
options.set = set;
}
return Object.defineProperty(obj, key, options);
}
var Obj = /*#__PURE__*/Object.freeze({
__proto__: null,
each: each,
reduce: reduce,
isObject: isObject,
isPlain: isPlain,
merge: merge,
values: values,
defineLazyProperty: defineLazyProperty
});
/**
* @file browser.js
* @module browser
*/
/**
* Whether or not this device is an iPod.
*
* @static
* @type {Boolean}
*/
let IS_IPOD = false;
/**
* The detected iOS version - or `null`.
*
* @static
* @type {string|null}
*/
let IOS_VERSION = null;
/**
* Whether or not this is an Android device.
*
* @static
* @type {Boolean}
*/
let IS_ANDROID = false;
/**
* The detected Android version - or `null` if not Android or indeterminable.
*
* @static
* @type {number|string|null}
*/
let ANDROID_VERSION;
/**
* Whether or not this is Mozilla Firefox.
*
* @static
* @type {Boolean}
*/
let IS_FIREFOX = false;
/**
* Whether or not this is Microsoft Edge.
*
* @static
* @type {Boolean}
*/
let IS_EDGE = false;
/**
* Whether or not this is any Chromium Browser
*
* @static
* @type {Boolean}
*/
let IS_CHROMIUM = false;
/**
* Whether or not this is any Chromium browser that is not Edge.
*
* This will also be `true` for Chrome on iOS, which will have different support
* as it is actually Safari under the hood.
*
* Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
* IS_CHROMIUM should be used instead.
* "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
*
* @static
* @deprecated
* @type {Boolean}
*/
let IS_CHROME = false;
/**
* The detected Chromium version - or `null`.
*
* @static
* @type {number|null}
*/
let CHROMIUM_VERSION = null;
/**
* The detected Google Chrome version - or `null`.
* This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
* Deprecated, use CHROMIUM_VERSION instead.
*
* @static
* @deprecated
* @type {number|null}
*/
let CHROME_VERSION = null;
/**
* Whether or not this is a Chromecast receiver application.
*
* @static
* @type {Boolean}
*/
const IS_CHROMECAST_RECEIVER = Boolean(window.cast && window.cast.framework && window.cast.framework.CastReceiverContext);
/**
* The detected Internet Explorer version - or `null`.
*
* @static
* @deprecated
* @type {number|null}
*/
let IE_VERSION = null;
/**
* Whether or not this is desktop Safari.
*
* @static
* @type {Boolean}
*/
let IS_SAFARI = false;
/**
* Whether or not this is a Windows machine.
*
* @static
* @type {Boolean}
*/
let IS_WINDOWS = false;
/**
* Whether or not this device is an iPad.
*
* @static
* @type {Boolean}
*/
let IS_IPAD = false;
/**
* Whether or not this device is an iPhone.
*
* @static
* @type {Boolean}
*/
// The Facebook app's UIWebView identifies as both an iPhone and iPad, so
// to identify iPhones, we need to exclude iPads.
// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
let IS_IPHONE = false;
/**
* Whether or not this is a Tizen device.
*
* @static
* @type {Boolean}
*/
let IS_TIZEN = false;
/**
* Whether or not this is a WebOS device.
*
* @static
* @type {Boolean}
*/
let IS_WEBOS = false;
/**
* Whether or not this is a Smart TV (Tizen or WebOS) device.
*
* @static
* @type {Boolean}
*/
let IS_SMART_TV = false;
/**
* Whether or not this device is touch-enabled.
*
* @static
* @const
* @type {Boolean}
*/
const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch));
const UAD = window.navigator && window.navigator.userAgentData;
if (UAD && UAD.platform && UAD.brands) {
// If userAgentData is present, use it instead of userAgent to avoid warnings
// Currently only implemented on Chromium
// userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
IS_ANDROID = UAD.platform === 'Android';
IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
IS_CHROME = !IS_EDGE && IS_CHROMIUM;
CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
IS_WINDOWS = UAD.platform === 'Windows';
}
// If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
// or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
// the checks need to be made agiainst the regular userAgent string.
if (!IS_CHROMIUM) {
const USER_AGENT = window.navigator && window.navigator.userAgent || '';
IS_IPOD = /iPod/i.test(USER_AGENT);
IOS_VERSION = function () {
const match = USER_AGENT.match(/OS (\d+)_/i);
if (match && match[1]) {
return match[1];
}
return null;
}();
IS_ANDROID = /Android/i.test(USER_AGENT);
ANDROID_VERSION = function () {
// This matches Android Major.Minor.Patch versions
// ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
if (!match) {
return null;
}
const major = match[1] && parseFloat(match[1]);
const minor = match[2] && parseFloat(match[2]);
if (major && minor) {
return parseFloat(match[1] + '.' + match[2]);
} else if (major) {
return major;
}
return null;
}();
IS_FIREFOX = /Firefox/i.test(USER_AGENT);
IS_EDGE = /Edg/i.test(USER_AGENT);
IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
IS_CHROME = !IS_EDGE && IS_CHROMIUM;
CHROMIUM_VERSION = CHROME_VERSION = function () {
const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
if (match && match[2]) {
return parseFloat(match[2]);
}
return null;
}();
IE_VERSION = function () {
const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
let version = result && parseFloat(result[1]);
if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
// IE 11 has a different user agent string than other IE versions
version = 11.0;
}
return version;
}();
IS_TIZEN = /Tizen/i.test(USER_AGENT);
IS_WEBOS = /Web0S/i.test(USER_AGENT);
IS_SMART_TV = IS_TIZEN || IS_WEBOS;
IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV;
IS_WINDOWS = /Windows/i.test(USER_AGENT);
IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
}
/**
* Whether or not this is an iOS device.
*
* @static
* @const
* @type {Boolean}
*/
const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
/**
* Whether or not this is any flavor of Safari - including iOS.
*
* @static
* @const
* @type {Boolean}
*/
const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
var browser = /*#__PURE__*/Object.freeze({
__proto__: null,
get IS_IPOD () { return IS_IPOD; },
get IOS_VERSION () { return IOS_VERSION; },
get IS_ANDROID () { return IS_ANDROID; },
get ANDROID_VERSION () { return ANDROID_VERSION; },
get IS_FIREFOX () { return IS_FIREFOX; },
get IS_EDGE () { return IS_EDGE; },
get IS_CHROMIUM () { return IS_CHROMIUM; },
get IS_CHROME () { return IS_CHROME; },
get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
get CHROME_VERSION () { return CHROME_VERSION; },
IS_CHROMECAST_RECEIVER: IS_CHROMECAST_RECEIVER,
get IE_VERSION () { return IE_VERSION; },
get IS_SAFARI () { return IS_SAFARI; },
get IS_WINDOWS () { return IS_WINDOWS; },
get IS_IPAD () { return IS_IPAD; },
get IS_IPHONE () { return IS_IPHONE; },
get IS_TIZEN () { return IS_TIZEN; },
get IS_WEBOS () { return IS_WEBOS; },
get IS_SMART_TV () { return IS_SMART_TV; },
TOUCH_ENABLED: TOUCH_ENABLED,
IS_IOS: IS_IOS,
IS_ANY_SAFARI: IS_ANY_SAFARI
});
/**
* @file dom.js
* @module dom
*/
/**
* Detect if a value is a string with any non-whitespace characters.
*
* @private
* @param {string} str
* The string to check
*
* @return {boolean}
* Will be `true` if the string is non-blank, `false` otherwise.
*
*/
function isNonBlankString(str) {
// we use str.trim as it will trim any whitespace characters
// from the front or back of non-whitespace characters. aka
// Any string that contains non-whitespace characters will
// still contain them after `trim` but whitespace only strings
// will have a length of 0, failing this check.
return typeof str === 'string' && Boolean(str.trim());
}
/**
* Throws an error if the passed string has whitespace. This is used by
* class methods to be relatively consistent with the classList API.
*
* @private
* @param {string} str
* The string to check for whitespace.
*
* @throws {Error}
* Throws an error if there is whitespace in the string.
*/
function throwIfWhitespace(str) {
// str.indexOf instead of regex because str.indexOf is faster performance wise.
if (str.indexOf(' ') >= 0) {
throw new Error('class has illegal whitespace characters');
}
}
/**
* Whether the current DOM interface appears to be real (i.e. not simulated).
*
* @return {boolean}
* Will be `true` if the DOM appears to be real, `false` otherwise.
*/
function isReal() {
// Both document and window will never be undefined thanks to `global`.
return document === window.document;
}
/**
* Determines, via duck typing, whether or not a value is a DOM element.
*
* @param {*} value
* The value to check.
*
* @return {boolean}
* Will be `true` if the value is a DOM element, `false` otherwise.
*/
function isEl(value) {
return isObject(value) && value.nodeType === 1;
}
/**
* Determines if the current DOM is embedded in an iframe.
*
* @return {boolean}
* Will be `true` if the DOM is embedded in an iframe, `false`
* otherwise.
*/
function isInFrame() {
// We need a try/catch here because Safari will throw errors when attempting
// to get either `parent` or `self`
try {
return window.parent !== window.self;
} catch (x) {
return true;
}
}
/**
* Creates functions to query the DOM using a given method.
*
* @private
* @param {string} method
* The method to create the query with.
*
* @return {Function}
* The query method
*/
function createQuerier(method) {
return function (selector, context) {
if (!isNonBlankString(selector)) {
return document[method](null);
}
if (isNonBlankString(context)) {
context = document.querySelector(context);
}
const ctx = isEl(context) ? context : document;
return ctx[method] && ctx[method](selector);
};
}
/**
* Creates an element and applies properties, attributes, and inserts content.
*
* @param {string} [tagName='div']
* Name of tag to be created.
*
* @param {Object} [properties={}]
* Element properties to be applied.
*
* @param {Object} [attributes={}]
* Element attributes to be applied.
*
* @param {ContentDescriptor} [content]
* A content descriptor object.
*
* @return {Element}
* The element that was created.
*/
function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
const el = document.createElement(tagName);
Object.getOwnPropertyNames(properties).forEach(function (propName) {
const val = properties[propName];
// Handle textContent since it's not supported everywhere and we have a
// method for it.
if (propName === 'textContent') {
textContent(el, val);
} else if (el[propName] !== val || propName === 'tabIndex') {
el[propName] = val;
}
});
Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
el.setAttribute(attrName, attributes[attrName]);
});
if (content) {
appendContent(el, content);
}
return el;
}
/**
* Injects text into an element, replacing any existing contents entirely.
*
* @param {HTMLElement} el
* The element to add text content into
*
* @param {string} text
* The text content to add.
*
* @return {Element}
* The element with added text content.
*/
function textContent(el, text) {
if (typeof el.textContent === 'undefined') {
el.innerText = text;
} else {
el.textContent = text;
}
return el;
}
/**
* Insert an element as the first child node of another
*
* @param {Element} child
* Element to insert
*
* @param {Element} parent
* Element to insert child into
*/
function prependTo(child, parent) {
if (parent.firstChild) {
parent.insertBefore(child, parent.firstChild);
} else {
parent.appendChild(child);
}
}
/**
* Check if an element has a class name.
*
* @param {Element} element
* Element to check
*
* @param {string} classToCheck
* Class name to check for
*
* @return {boolean}
* Will be `true` if the element has a class, `false` otherwise.
*
* @throws {Error}
* Throws an error if `classToCheck` has white space.
*/
function hasClass(element, classToCheck) {
throwIfWhitespace(classToCheck);
return element.classList.contains(classToCheck);
}
/**
* Add a class name to an element.
*
* @param {Element} element
* Element to add class name to.
*
* @param {...string} classesToAdd
* One or more class name to add.
*
* @return {Element}
* The DOM element with the added class name.
*/
function addClass(element, ...classesToAdd) {
element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
return element;
}
/**
* Remove a class name from an element.
*
* @param {Element} element
* Element to remove a class name from.
*
* @param {...string} classesToRemove
* One or more class name to remove.
*
* @return {Element}
* The DOM element with class name removed.
*/
function removeClass(element, ...classesToRemove) {
// Protect in case the player gets disposed
if (!element) {
log.warn("removeClass was called with an element that doesn't exist");
return null;
}
element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
return element;
}
/**
* The callback definition for toggleClass.
*
* @callback PredicateCallback
* @param {Element} element
* The DOM element of the Component.
*
* @param {string} classToToggle
* The `className` that wants to be toggled
*
* @return {boolean|undefined}
* If `true` is returned, the `classToToggle` will be added to the
* `element`, but not removed. If `false`, the `classToToggle` will be removed from
* the `element`, but not added. If `undefined`, the callback will be ignored.
*
*/
/**
* Adds or removes a class name to/from an element depending on an optional
* condition or the presence/absence of the class name.
*
* @param {Element} element
* The element to toggle a class name on.
*
* @param {string} classToToggle
* The class that should be toggled.
*
* @param {boolean|PredicateCallback} [predicate]
* See the return value for {@link module:dom~PredicateCallback}
*
* @return {Element}
* The element with a class that has been toggled.
*/
function toggleClass(element, classToToggle, predicate) {
if (typeof predicate === 'function') {
predicate = predicate(element, classToToggle);
}
if (typeof predicate !== 'boolean') {
predicate = undefined;
}
classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
return element;
}
/**
* Apply attributes to an HTML element.
*
* @param {Element} el
* Element to add attributes to.
*
* @param {Object} [attributes]
* Attributes to be applied.
*/
function setAttributes(el, attributes) {
Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
const attrValue = attributes[attrName];
if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
el.removeAttribute(attrName);
} else {
el.setAttribute(attrName, attrValue === true ? '' : attrValue);
}
});
}
/**
* Get an element's attribute values, as defined on the HTML tag.
*
* Attributes are not the same as properties. They're defined on the tag
* or with setAttribute.
*
* @param {Element} tag
* Element from which to get tag attributes.
*
* @return {Object}
* All attributes of the element. Boolean attributes will be `true` or
* `false`, others will be strings.
*/
function getAttributes(tag) {
const obj = {};
// known boolean attributes
// we can check for matching boolean properties, but not all browsers
// and not all tags know about these attributes, so, we still want to check them manually
const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
if (tag && tag.attributes && tag.attributes.length > 0) {
const attrs = tag.attributes;
for (let i = attrs.length - 1; i >= 0; i--) {
const attrName = attrs[i].name;
/** @type {boolean|string} */
let attrVal = attrs[i].value;
// check for known booleans
// the matching element property will return a value for typeof
if (knownBooleans.includes(attrName)) {
// the value of an included boolean attribute is typically an empty
// string ('') which would equal false if we just check for a false value.
// we also don't want support bad code like autoplay='false'
attrVal = attrVal !== null ? true : false;
}
obj[attrName] = attrVal;
}
}
return obj;
}
/**
* Get the value of an element's attribute.
*
* @param {Element} el
* A DOM element.
*
* @param {string} attribute
* Attribute to get the value of.
*
* @return {string}
* The value of the attribute.
*/
function getAttribute(el, attribute) {
return el.getAttribute(attribute);
}
/**
* Set the value of an element's attribute.
*
* @param {Element} el
* A DOM element.
*
* @param {string} attribute
* Attribute to set.
*
* @param {string} value
* Value to set the attribute to.
*/
function setAttribute(el, attribute, value) {
el.setAttribute(attribute, value);
}
/**
* Remove an element's attribute.
*
* @param {Element} el
* A DOM element.
*
* @param {string} attribute
* Attribute to remove.
*/
function removeAttribute(el, attribute) {
el.removeAttribute(attribute);
}
/**
* Attempt to block the ability to select text.
*/
function blockTextSelection() {
document.body.focus();
document.onselectstart = function () {
return false;
};
}
/**
* Turn off text selection blocking.
*/
function unblockTextSelection() {
document.onselectstart = function () {
return true;
};
}
/**
* Identical to the native `getBoundingClientRect` function, but ensures that
* the method is supported at all (it is in all browsers we claim to support)
* and that the element is in the DOM before continuing.
*
* This wrapper function also shims properties which are not provided by some
* older browsers (namely, IE8).
*
* Additionally, some browsers do not support adding properties to a
* `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
* properties (except `x` and `y` which are not widely supported). This helps
* avoid implementations where keys are non-enumerable.
*
* @param {Element} el
* Element whose `ClientRect` we want to calculate.
*
* @return {Object|undefined}
* Always returns a plain object - or `undefined` if it cannot.
*/
function getBoundingClientRect(el) {
if (el && el.getBoundingClientRect && el.parentNode) {
const rect = el.getBoundingClientRect();
const result = {};
['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
if (rect[k] !== undefined) {
result[k] = rect[k];
}
});
if (!result.height) {
result.height = parseFloat(computedStyle(el, 'height'));
}
if (!result.width) {
result.width = parseFloat(computedStyle(el, 'width'));
}
return result;
}
}
/**
* Represents the position of a DOM element on the page.
*
* @typedef {Object} module:dom~Position
*
* @property {number} left
* Pixels to the left.
*
* @property {number} top
* Pixels from the top.
*/
/**
* Get the position of an element in the DOM.
*
* Uses `getBoundingClientRect` technique from John Resig.
*
* @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
*
* @param {Element} el
* Element from which to get offset.
*
* @return {module:dom~Position}
* The position of the element that was passed in.
*/
function findPosition(el) {
if (!el || el && !el.offsetParent) {
return {
left: 0,
top: 0,
width: 0,
height: 0
};
}
const width = el.offsetWidth;
const height = el.offsetHeight;
let left = 0;
let top = 0;
while (el.offsetParent && el !== document[FullscreenApi.fullscreenElement]) {
left += el.offsetLeft;
top += el.offsetTop;
el = el.offsetParent;
}
return {
left,
top,
width,
height
};
}
/**
* Represents x and y coordinates for a DOM element or mouse pointer.
*
* @typedef {Object} module:dom~Coordinates
*
* @property {number} x
* x coordinate in pixels
*
* @property {number} y
* y coordinate in pixels
*/
/**
* Get the pointer position within an element.
*
* The base on the coordinates are the bottom left of the element.
*
* @param {Element} el
* Element on which to get the pointer position on.
*
* @param {Event} event
* Event object.
*
* @return {module:dom~Coordinates}
* A coordinates object corresponding to the mouse position.
*
*/
function getPointerPosition(el, event) {
const translated = {
x: 0,
y: 0
};
if (IS_IOS) {
let item = el;
while (item && item.nodeName.toLowerCase() !== 'html') {
const transform = computedStyle(item, 'transform');
if (/^matrix/.test(transform)) {
const values = transform.slice(7, -1).split(/,\s/).map(Number);
translated.x += values[4];
translated.y += values[5];
} else if (/^matrix3d/.test(transform)) {
const values = transform.slice(9, -1).split(/,\s/).map(Number);
translated.x += values[12];
translated.y += values[13];
}
if (item.assignedSlot && item.assignedSlot.parentElement && window.WebKitCSSMatrix) {
const transformValue = window.getComputedStyle(item.assignedSlot.parentElement).transform;
const matrix = new window.WebKitCSSMatrix(transformValue);
translated.x += matrix.m41;
translated.y += matrix.m42;
}
item = item.parentNode || item.host;
}
}
const position = {};
const boxTarget = findPosition(event.target);
const box = findPosition(el);
const boxW = box.width;
const boxH = box.height;
let offsetY = event.offsetY - (box.top - boxTarget.top);
let offsetX = event.offsetX - (box.left - boxTarget.left);
if (event.changedTouches) {
offsetX = event.changedTouches[0].pageX - box.left;
offsetY = event.changedTouches[0].pageY + box.top;
if (IS_IOS) {
offsetX -= translated.x;
offsetY -= translated.y;
}
}
position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
position.x = Math.max(0, Math.min(1, offsetX / boxW));
return position;
}
/**
* Determines, via duck typing, whether or not a value is a text node.
*
* @param {*} value
* Check if this value is a text node.
*
* @return {boolean}
* Will be `true` if the value is a text node, `false` otherwise.
*/
function isTextNode(value) {
return isObject(value) && value.nodeType === 3;
}
/**
* Empties the contents of an element.
*
* @param {Element} el
* The element to empty children from
*
* @return {Element}
* The element with no children
*/
function emptyEl(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
return el;
}
/**
* This is a mixed value that describes content to be injected into the DOM
* via some method. It can be of the following types:
*
* Type | Description
* -----------|-------------
* `string` | The value will be normalized into a text node.
* `Element` | The value will be accepted as-is.
* `Text` | A TextNode. The value will be accepted as-is.
* `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
* `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
*
* @typedef {string|Element|Text|Array|Function} ContentDescriptor
*/
/**
* Normalizes content for eventual insertion into the DOM.
*
* This allows a wide range of content definition methods, but helps protect
* from falling into the trap of simply writing to `innerHTML`, which could
* be an XSS concern.
*
* The content for an element can be passed in multiple types and
* combinations, whose behavior is as follows:
*
* @param {ContentDescriptor} content
* A content descriptor value.
*
* @return {Array}
* All of the content that was passed in, normalized to an array of
* elements or text nodes.
*/
function normalizeContent(content) {
// First, invoke content if it is a function. If it produces an array,
// that needs to happen before normalization.
if (typeof content === 'function') {
content = content();
}
// Next up, normalize to an array, so one or many items can be normalized,
// filtered, and returned.
return (Array.isArray(content) ? content : [content]).map(value => {
// First, invoke value if it is a function to produce a new value,
// which will be subsequently normalized to a Node of some kind.
if (typeof value === 'function') {
value = value();
}
if (isEl(value) || isTextNode(value)) {
return value;
}
if (typeof value === 'string' && /\S/.test(value)) {
return document.createTextNode(value);
}
}).filter(value => value);
}
/**
* Normalizes and appends content to an element.
*
* @param {Element} el
* Element to append normalized content to.
*
* @param {ContentDescriptor} content
* A content descriptor value.
*
* @return {Element}
* The element with appended normalized content.
*/
function appendContent(el, content) {
normalizeContent(content).forEach(node => el.appendChild(node));
return el;
}
/**
* Normalizes and inserts content into an element; this is identical to
* `appendContent()`, except it empties the element first.
*
* @param {Element} el
* Element to insert normalized content into.
*
* @param {ContentDescriptor} content
* A content descriptor value.
*
* @return {Element}
* The element with inserted normalized content.
*/
function insertContent(el, content) {
return appendContent(emptyEl(el), content);
}
/**
* Check if an event was a single left click.
*
* @param {MouseEvent} event
* Event object.
*
* @return {boolean}
* Will be `true` if a single left click, `false` otherwise.
*/
function isSingleLeftClick(event) {
// Note: if you create something draggable, be sure to
// call it on both `mousedown` and `mousemove` event,
// otherwise `mousedown` should be enough for a button
if (event.button === undefined && event.buttons === undefined) {
// Why do we need `buttons` ?
// Because, middle mouse sometimes have this:
// e.button === 0 and e.buttons === 4
// Furthermore, we want to prevent combination click, something like
// HOLD middlemouse then left click, that would be
// e.button === 0, e.buttons === 5
// just `button` is not gonna work
// Alright, then what this block does ?
// this is for chrome `simulate mobile devices`
// I want to support this as well
return true;
}
if (event.button === 0 && event.buttons === undefined) {
// Touch screen, sometimes on some specific device, `buttons`
// doesn't have anything (safari on ios, blackberry...)
return true;
}
// `mouseup` event on a single left click has
// `button` and `buttons` equal to 0
if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
return true;
}
// MacOS Sonoma trackpad when "tap to click enabled"
if (event.type === 'mousedown' && event.button === 0 && event.buttons === 0) {
return true;
}
if (event.button !== 0 || event.buttons !== 1) {
// This is the reason we have those if else block above
// if any special case we can catch and let it slide
// we do it above, when get to here, this definitely
// is-not-left-click
return false;
}
return true;
}
/**
* Finds a single DOM element matching `selector` within the optional
* `context` of another DOM element (defaulting to `document`).
*
* @param {string} selector
* A valid CSS selector, which will be passed to `querySelector`.
*
* @param {Element|String} [context=document]
* A DOM element within which to query. Can also be a selector
* string in which case the first matching element will be used
* as context. If missing (or no element matches selector), falls
* back to `document`.
*
* @return {Element|null}
* The element that was found or null.
*/
const $ = createQuerier('querySelector');
/**
* Finds a all DOM elements matching `selector` within the optional
* `context` of another DOM element (defaulting to `document`).
*
* @param {string} selector
* A valid CSS selector, which will be passed to `querySelectorAll`.
*
* @param {Element|String} [context=document]
* A DOM element within which to query. Can also be a selector
* string in which case the first matching element will be used
* as context. If missing (or no element matches selector), falls
* back to `document`.
*
* @return {NodeList}
* A element list of elements that were found. Will be empty if none
* were found.
*
*/
const $$ = createQuerier('querySelectorAll');
/**
* A safe getComputedStyle.
*
* This is needed because in Firefox, if the player is loaded in an iframe with
* `display:none`, then `getComputedStyle` returns `null`, so, we do a
* null-check to make sure that the player doesn't break in these cases.
*
* @param {Element} el
* The element you want the computed style of
*
* @param {string} prop
* The property name you want
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
*/
function computedStyle(el, prop) {
if (!el || !prop) {
return '';
}
if (typeof window.getComputedStyle === 'function') {
let computedStyleValue;
try {
computedStyleValue = window.getComputedStyle(el);
} catch (e) {
return '';
}
return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
}
return '';
}
/**
* Copy document style sheets to another window.
*
* @param {Window} win
* The window element you want to copy the document style sheets to.
*
*/
function copyStyleSheetsToWindow(win) {
[...document.styleSheets].forEach(styleSheet => {
try {
const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
const style = document.createElement('style');
style.textContent = cssRules;
win.document.head.appendChild(style);
} catch (e) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = styleSheet.type;
// For older Safari this has to be the string; on other browsers setting the MediaList works
link.media = styleSheet.media.mediaText;
link.href = styleSheet.href;
win.document.head.appendChild(link);
}
});
}
var Dom = /*#__PURE__*/Object.freeze({
__proto__: null,
isReal: isReal,
isEl: isEl,
isInFrame: isInFrame,
createEl: createEl,
textContent: textContent,
prependTo: prependTo,
hasClass: hasClass,
addClass: addClass,
removeClass: removeClass,
toggleClass: toggleClass,
setAttributes: setAttributes,
getAttributes: getAttributes,
getAttribute: getAttribute,
setAttribute: setAttribute,
removeAttribute: removeAttribute,
blockTextSelection: blockTextSelection,
unblockTextSelection: unblockTextSelection,
getBoundingClientRect: getBoundingClientRect,
findPosition: findPosition,
getPointerPosition: getPointerPosition,
isTextNode: isTextNode,
emptyEl: emptyEl,
normalizeContent: normalizeContent,
appendContent: appendContent,
insertContent: insertContent,
isSingleLeftClick: isSingleLeftClick,
$: $,
$$: $$,
computedStyle: computedStyle,
copyStyleSheetsToWindow: copyStyleSheetsToWindow
});
/**
* @file setup.js - Functions for setting up a player without
* user interaction based on the data-setup `attribute` of the video tag.
*
* @module setup
*/
let _windowLoaded = false;
let videojs$1;
/**
* Set up any tags that have a data-setup `attribute` when the player is started.
*/
const autoSetup = function () {
if (videojs$1.options.autoSetup === false) {
return;
}
const vids = Array.prototype.slice.call(document.getElementsByTagName('video'));
const audios = Array.prototype.slice.call(document.getElementsByTagName('audio'));
const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js'));
const mediaEls = vids.concat(audios, divs);
// Check if any media elements exist
if (mediaEls && mediaEls.length > 0) {
for (let i = 0, e = mediaEls.length; i < e; i++) {
const mediaEl = mediaEls[i];
// Check if element exists, has getAttribute func.
if (mediaEl && mediaEl.getAttribute) {
// Make sure this player hasn't already been set up.
if (mediaEl.player === undefined) {
const options = mediaEl.getAttribute('data-setup');
// Check if data-setup attr exists.
// We only auto-setup if they've added the data-setup attr.
if (options !== null) {
// Create new video.js instance.
videojs$1(mediaEl);
}
}
// If getAttribute isn't defined, we need to wait for the DOM.
} else {
autoSetupTimeout(1);
break;
}
}
// No videos were found, so keep looping unless page is finished loading.
} else if (!_windowLoaded) {
autoSetupTimeout(1);
}
};
/**
* Wait until the page is loaded before running autoSetup. This will be called in
* autoSetup if `hasLoaded` returns false.
*
* @param {number} wait
* How long to wait in ms
*
* @param {module:videojs} [vjs]
* The videojs library function
*/
function autoSetupTimeout(wait, vjs) {
// Protect against breakage in non-browser environments
if (!isReal()) {
return;
}
if (vjs) {
videojs$1 = vjs;
}
window.setTimeout(autoSetup, wait);
}
/**
* Used to set the internal tracking of window loaded state to true.
*
* @private
*/
function setWindowLoaded() {
_windowLoaded = true;
window.removeEventListener('load', setWindowLoaded);
}
if (isReal()) {
if (document.readyState === 'complete') {
setWindowLoaded();
} else {
/**
* Listen for the load event on window, and set _windowLoaded to true.
*
* We use a standard event listener here to avoid incrementing the GUID
* before any players are created.
*
* @listens load
*/
window.addEventListener('load', setWindowLoaded);
}
}
/**
* @file stylesheet.js
* @module stylesheet
*/
/**
* Create a DOM style element given a className for it.
*
* @param {string} className
* The className to add to the created style element.
*
* @return {Element}
* The element that was created.
*/
const createStyleElement = function (className) {
const style = document.createElement('style');
style.className = className;
return style;
};
/**
* Add text to a DOM element.
*
* @param {Element} el
* The Element to add text content to.
*
* @param {string} content
* The text to add to the element.
*/
const setTextContent = function (el, content) {
if (el.styleSheet) {
el.styleSheet.cssText = content;
} else {
el.textContent = content;
}
};
/**
* @file dom-data.js
* @module dom-data
*/
/**
* Element Data Store.
*
* Allows for binding data to an element without putting it directly on the
* element. Ex. Event listeners are stored here.
* (also from jsninja.com, slightly modified and updated for closure compiler)
*
* @type {Object}
* @private
*/
var DomData = new WeakMap();
/**
* @file guid.js
* @module guid
*/
// Default value for GUIDs. This allows us to reset the GUID counter in tests.
//
// The initial GUID is 3 because some users have come to rely on the first
// default player ID ending up as `vjs_video_3`.
//
// See: https://github.com/videojs/video.js/pull/6216
const _initialGuid = 3;
/**
* Unique ID for an element or function
*
* @type {Number}
*/
let _guid = _initialGuid;
/**
* Get a unique auto-incrementing ID by number that has not been returned before.
*
* @return {number}
* A new unique ID.
*/
function newGUID() {
return _guid++;
}
/**
* @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
* (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
* This should work very similarly to jQuery's events, however it's based off the book version which isn't as
* robust as jquery's, so there's probably some differences.
*
* @file events.js
* @module events
*/
/**
* Clean up the listener cache and dispatchers
*
* @param {Element|Object} elem
* Element to clean up
*
* @param {string} type
* Type of event to clean up
*/
function _cleanUpEvents(elem, type) {
if (!DomData.has(elem)) {
return;
}
const data = DomData.get(elem);
// Remove the events of a particular type if there are none left
if (data.handlers[type].length === 0) {
delete data.handlers[type];
// data.handlers[type] = null;
// Setting to null was causing an error with data.handlers
// Remove the meta-handler from the element
if (elem.removeEventListener) {
elem.removeEventListener(type, data.dispatcher, false);
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, data.dispatcher);
}
}
// Remove the events object if there are no types left
if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
delete data.handlers;
delete data.dispatcher;
delete data.disabled;
}
// Finally remove the element data if there is no data left
if (Object.getOwnPropertyNames(data).length === 0) {
DomData.delete(elem);
}
}
/**
* Loops through an array of event types and calls the requested method for each type.
*
* @param {Function} fn
* The event method we want to use.
*
* @param {Element|Object} elem
* Element or object to bind listeners to
*
* @param {string[]} types
* Type of event to bind to.
*
* @param {Function} callback
* Event listener.
*/
function _handleMultipleEvents(fn, elem, types, callback) {
types.forEach(function (type) {
// Call the event method for each one of the types
fn(elem, type, callback);
});
}
/**
* Fix a native event to have standard property values
*
* @param {Object} event
* Event object to fix.
*
* @return {Object}
* Fixed event object.
*/
function fixEvent(event) {
if (event.fixed_) {
return event;
}
function returnTrue() {
return true;
}
function returnFalse() {
return false;
}
// Test if fixing up is needed
// Used to check if !event.stopPropagation instead of isPropagationStopped
// But native events return true for stopPropagation, but don't have
// other expected methods like isPropagationStopped. Seems to be a problem
// with the Javascript Ninja code. So we're just overriding all events now.
if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
const old = event || window.event;
event = {};
// Clone the old object so that we can modify the values event = {};
// IE8 Doesn't like when you mess with native event properties
// Firefox returns false for event.hasOwnProperty('type') and other props
// which makes copying more difficult.
// TODO: Probably best to create an allowlist of event props
const deprecatedProps = ['layerX', 'layerY', 'keyLocation', 'path', 'webkitMovementX', 'webkitMovementY', 'mozPressure', 'mozInputSource'];
for (const key in old) {
// Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
// Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
// and webkitMovementX/Y
// Lighthouse complains if Event.path is copied
if (!deprecatedProps.includes(key)) {
// Chrome 32+ warns if you try to copy deprecated returnValue, but
// we still want to if preventDefault isn't supported (IE8).
if (!(key === 'returnValue' && old.preventDefault)) {
event[key] = old[key];
}
}
}
// The event occurred on this element
if (!event.target) {
event.target = event.srcElement || document;
}
// Handle which other element the event is related to
if (!event.relatedTarget) {
event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
}
// Stop the default browser action
event.preventDefault = function () {
if (old.preventDefault) {
old.preventDefault();
}
event.returnValue = false;
old.returnValue = false;
event.defaultPrevented = true;
};
event.defaultPrevented = false;
// Stop the event from bubbling
event.stopPropagation = function () {
if (old.stopPropagation) {
old.stopPropagation();
}
event.cancelBubble = true;
old.cancelBubble = true;
event.isPropagationStopped = returnTrue;
};
event.isPropagationStopped = returnFalse;
// Stop the event from bubbling and executing other handlers
event.stopImmediatePropagation = function () {
if (old.stopImmediatePropagation) {
old.stopImmediatePropagation();
}
event.isImmediatePropagationStopped = returnTrue;
event.stopPropagation();
};
event.isImmediatePropagationStopped = returnFalse;
// Handle mouse position
if (event.clientX !== null && event.clientX !== undefined) {
const doc = document.documentElement;
const body = document.body;
event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
}
// Handle key presses
event.which = event.charCode || event.keyCode;
// Fix button for mouse clicks:
// 0 == left; 1 == middle; 2 == right
if (event.button !== null && event.button !== undefined) {
// The following is disabled because it does not pass videojs-standard
// and... yikes.
/* eslint-disable */
event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
/* eslint-enable */
}
}
event.fixed_ = true;
// Returns fixed-up instance
return event;
}
/**
* Whether passive event listeners are supported
*/
let _supportsPassive;
const supportsPassive = function () {
if (typeof _supportsPassive !== 'boolean') {
_supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
_supportsPassive = true;
}
});
window.addEventListener('test', null, opts);
window.removeEventListener('test', null, opts);
} catch (e) {
// disregard
}
}
return _supportsPassive;
};
/**
* Touch events Chrome expects to be passive
*/
const passiveEvents = ['touchstart', 'touchmove'];
/**
* Add an event listener to element
* It stores the handler function in a separate cache object
* and adds a generic handler to the element's event,
* along with a unique id (guid) to the element.
*
* @param {Element|Object} elem
* Element or object to bind listeners to
*
* @param {string|string[]} type
* Type of event to bind to.
*
* @param {Function} fn
* Event listener.
*/
function on(elem, type, fn) {
if (Array.isArray(type)) {
return _handleMultipleEvents(on, elem, type, fn);
}
if (!DomData.has(elem)) {
DomData.set(elem, {});
}
const data = DomData.get(elem);
// We need a place to store all our handler data
if (!data.handlers) {
data.handlers = {};
}
if (!data.handlers[type]) {
data.handlers[type] = [];
}
if (!fn.guid) {
fn.guid = newGUID();
}
data.handlers[type].push(fn);
if (!data.dispatcher) {
data.disabled = false;
data.dispatcher = function (event, hash) {
if (data.disabled) {
return;
}
event = fixEvent(event);
const handlers = data.handlers[event.type];
if (handlers) {
// Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
const handlersCopy = handlers.slice(0);
for (let m = 0, n = handlersCopy.length; m < n; m++) {
if (event.isImmediatePropagationStopped()) {
break;
} else {
try {
handlersCopy[m].call(elem, event, hash);
} catch (e) {
log.error(e);
}
}
}
}
};
}
if (data.handlers[type].length === 1) {
if (elem.addEventListener) {
let options = false;
if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
options = {
passive: true
};
}
elem.addEventListener(type, data.dispatcher, options);
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, data.dispatcher);
}
}
}
/**
* Removes event listeners from an element
*
* @param {Element|Object} elem
* Object to remove listeners from.
*
* @param {string|string[]} [type]
* Type of listener to remove. Don't include to remove all events from element.
*
* @param {Function} [fn]
* Specific listener to remove. Don't include to remove listeners for an event
* type.
*/
function off(elem, type, fn) {
// Don't want to add a cache object through getElData if not needed
if (!DomData.has(elem)) {
return;
}
const data = DomData.get(elem);
// If no events exist, nothing to unbind
if (!data.handlers) {
return;
}
if (Array.isArray(type)) {
return _handleMultipleEvents(off, elem, type, fn);
}
// Utility function
const removeType = function (el, t) {
data.handlers[t] = [];
_cleanUpEvents(el, t);
};
// Are we removing all bound events?
if (type === undefined) {
for (const t in data.handlers) {
if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
removeType(elem, t);
}
}
return;
}
const handlers = data.handlers[type];
// If no handlers exist, nothing to unbind
if (!handlers) {
return;
}
// If no listener was provided, remove all listeners for type
if (!fn) {
removeType(elem, type);
return;
}
// We're only removing a single handler
if (fn.guid) {
for (let n = 0; n < handlers.length; n++) {
if (handlers[n].guid === fn.guid) {
handlers.splice(n--, 1);
}
}
}
_cleanUpEvents(elem, type);
}
/**
* Trigger an event for an element
*
* @param {Element|Object} elem
* Element to trigger an event on
*
* @param {EventTarget~Event|string} event
* A string (the type) or an event object with a type attribute
*
* @param {Object} [hash]
* data hash to pass along with the event
*
* @return {boolean|undefined}
* Returns the opposite of `defaultPrevented` if default was
* prevented. Otherwise, returns `undefined`
*/
function trigger(elem, event, hash) {
// Fetches element data and a reference to the parent (for bubbling).
// Don't want to add a data object to cache for every parent,
// so checking hasElData first.
const elemData = DomData.has(elem) ? DomData.get(elem) : {};
const parent = elem.parentNode || elem.ownerDocument;
// type = event.type || event,
// handler;
// If an event name was passed as a string, creates an event out of it
if (typeof event === 'string') {
event = {
type: event,
target: elem
};
} else if (!event.target) {
event.target = elem;
}
// Normalizes the event properties.
event = fixEvent(event);
// If the passed element has a dispatcher, executes the established handlers.
if (elemData.dispatcher) {
elemData.dispatcher.call(elem, event, hash);
}
// Unless explicitly stopped or the event does not bubble (e.g. media events)
// recursively calls this function to bubble the event up the DOM.
if (parent && !event.isPropagationStopped() && event.bubbles === true) {
trigger.call(null, parent, event, hash);
// If at the top of the DOM, triggers the default action unless disabled.
} else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
if (!DomData.has(event.target)) {
DomData.set(event.target, {});
}
const targetData = DomData.get(event.target);
// Checks if the target has a default action for this event.
if (event.target[event.type]) {
// Temporarily disables event dispatching on the target as we have already executed the handler.
targetData.disabled = true;
// Executes the default action.
if (typeof event.target[event.type] === 'function') {
event.target[event.type]();
}
// Re-enables event dispatching.
targetData.disabled = false;
}
}
// Inform the triggerer if the default was prevented by returning false
return !event.defaultPrevented;
}
/**
* Trigger a listener only once for an event.
*
* @param {Element|Object} elem
* Element or object to bind to.
*
* @param {string|string[]} type
* Name/type of event
*
* @param {Event~EventListener} fn
* Event listener function
*/
function one(elem, type, fn) {
if (Array.isArray(type)) {
return _handleMultipleEvents(one, elem, type, fn);
}
const func = function () {
off(elem, type, func);
fn.apply(this, arguments);
};
// copy the guid to the new function so it can removed using the original function's ID
func.guid = fn.guid = fn.guid || newGUID();
on(elem, type, func);
}
/**
* Trigger a listener only once and then turn if off for all
* configured events
*
* @param {Element|Object} elem
* Element or object to bind to.
*
* @param {string|string[]} type
* Name/type of event
*
* @param {Event~EventListener} fn
* Event listener function
*/
function any(elem, type, fn) {
const func = function () {
off(elem, type, func);
fn.apply(this, arguments);
};
// copy the guid to the new function so it can removed using the original function's ID
func.guid = fn.guid = fn.guid || newGUID();
// multiple ons, but one off for everything
on(elem, type, func);
}
var Events = /*#__PURE__*/Object.freeze({
__proto__: null,
fixEvent: fixEvent,
on: on,
off: off,
trigger: trigger,
one: one,
any: any
});
/**
* @file fn.js
* @module fn
*/
const UPDATE_REFRESH_INTERVAL = 30;
/**
* A private, internal-only function for changing the context of a function.
*
* It also stores a unique id on the function so it can be easily removed from
* events.
*
* @private
* @function
* @param {*} context
* The object to bind as scope.
*
* @param {Function} fn
* The function to be bound to a scope.
*
* @param {number} [uid]
* An optional unique ID for the function to be set
*
* @return {Function}
* The new function that will be bound into the context given
*/
const bind_ = function (context, fn, uid) {
// Make sure the function has a unique ID
if (!fn.guid) {
fn.guid = newGUID();
}
// Create the new function that changes the context
const bound = fn.bind(context);
// Allow for the ability to individualize this function
// Needed in the case where multiple objects might share the same prototype
// IF both items add an event listener with the same function, then you try to remove just one
// it will remove both because they both have the same guid.
// when using this, you need to use the bind method when you remove the listener as well.
// currently used in text tracks
bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
return bound;
};
/**
* Wraps the given function, `fn`, with a new function that only invokes `fn`
* at most once per every `wait` milliseconds.
*
* @function
* @param {Function} fn
* The function to be throttled.
*
* @param {number} wait
* The number of milliseconds by which to throttle.
*
* @return {Function}
*/
const throttle = function (fn, wait) {
let last = window.performance.now();
const throttled = function (...args) {
const now = window.performance.now();
if (now - last >= wait) {
fn(...args);
last = now;
}
};
return throttled;
};
/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked.
*
* Inspired by lodash and underscore implementations.
*
* @function
* @param {Function} func
* The function to wrap with debounce behavior.
*
* @param {number} wait
* The number of milliseconds to wait after the last invocation.
*
* @param {boolean} [immediate]
* Whether or not to invoke the function immediately upon creation.
*
* @param {Object} [context=window]
* The "context" in which the debounced function should debounce. For
* example, if this function should be tied to a Video.js player,
* the player can be passed here. Alternatively, defaults to the
* global `window` object.
*
* @return {Function}
* A debounced function.
*/
const debounce = function (func, wait, immediate, context = window) {
let timeout;
const cancel = () => {
context.clearTimeout(timeout);
timeout = null;
};
/* eslint-disable consistent-this */
const debounced = function () {
const self = this;
const args = arguments;
let later = function () {
timeout = null;
later = null;
if (!immediate) {
func.apply(self, args);
}
};
if (!timeout && immediate) {
func.apply(self, args);
}
context.clearTimeout(timeout);
timeout = context.setTimeout(later, wait);
};
/* eslint-enable consistent-this */
debounced.cancel = cancel;
return debounced;
};
var Fn = /*#__PURE__*/Object.freeze({
__proto__: null,
UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
bind_: bind_,
throttle: throttle,
debounce: debounce
});
/**
* @file src/js/event-target.js
*/
let EVENT_MAP;
/**
* `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
* adds shorthand functions that wrap around lengthy functions. For example:
* the `on` function is a wrapper around `addEventListener`.
*
* @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
* @class EventTarget
*/
class EventTarget {
/**
* Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
* function that will get called when an event with a certain name gets triggered.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to call with `EventTarget`s
*/
on(type, fn) {
// Remove the addEventListener alias before calling Events.on
// so we don't get into an infinite type loop
const ael = this.addEventListener;
this.addEventListener = () => {};
on(this, type, fn);
this.addEventListener = ael;
}
/**
* Removes an `event listener` for a specific event from an instance of `EventTarget`.
* This makes it so that the `event listener` will no longer get called when the
* named event happens.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to remove.
*/
off(type, fn) {
off(this, type, fn);
}
/**
* This function will add an `event listener` that gets triggered only once. After the
* first trigger it will get removed. This is like adding an `event listener`
* with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to be called once for each event name.
*/
one(type, fn) {
// Remove the addEventListener aliasing Events.on
// so we don't get into an infinite type loop
const ael = this.addEventListener;
this.addEventListener = () => {};
one(this, type, fn);
this.addEventListener = ael;
}
/**
* This function will add an `event listener` that gets triggered only once and is
* removed from all events. This is like adding an array of `event listener`s
* with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
* first time it is triggered.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to be called once for each event name.
*/
any(type, fn) {
// Remove the addEventListener aliasing Events.on
// so we don't get into an infinite type loop
const ael = this.addEventListener;
this.addEventListener = () => {};
any(this, type, fn);
this.addEventListener = ael;
}
/**
* This function causes an event to happen. This will then cause any `event listeners`
* that are waiting for that event, to get called. If there are no `event listeners`
* for an event then nothing will happen.
*
* If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
* Trigger will also call the `on` + `uppercaseEventName` function.
*
* Example:
* 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
* `onClick` if it exists.
*
* @param {string|EventTarget~Event|Object} event
* The name of the event, an `Event`, or an object with a key of type set to
* an event name.
*/
trigger(event) {
const type = event.type || event;
// deprecation
// In a future version we should default target to `this`
// similar to how we default the target to `elem` in
// `Events.trigger`. Right now the default `target` will be
// `document` due to the `Event.fixEvent` call.
if (typeof event === 'string') {
event = {
type
};
}
event = fixEvent(event);
if (this.allowedEvents_[type] && this['on' + type]) {
this['on' + type](event);
}
trigger(this, event);
}
queueTrigger(event) {
// only set up EVENT_MAP if it'll be used
if (!EVENT_MAP) {
EVENT_MAP = new Map();
}
const type = event.type || event;
let map = EVENT_MAP.get(this);
if (!map) {
map = new Map();
EVENT_MAP.set(this, map);
}
const oldTimeout = map.get(type);
map.delete(type);
window.clearTimeout(oldTimeout);
const timeout = window.setTimeout(() => {
map.delete(type);
// if we cleared out all timeouts for the current target, delete its map
if (map.size === 0) {
map = null;
EVENT_MAP.delete(this);
}
this.trigger(event);
}, 0);
map.set(type, timeout);
}
}
/**
* A Custom DOM event.
*
* @typedef {CustomEvent} Event
* @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
*/
/**
* All event listeners should follow the following format.
*
* @callback EventListener
* @this {EventTarget}
*
* @param {Event} event
* the event that triggered this function
*
* @param {Object} [hash]
* hash of data sent during the event
*/
/**
* An object containing event names as keys and booleans as values.
*
* > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
* will have extra functionality. See that function for more information.
*
* @property EventTarget.prototype.allowedEvents_
* @protected
*/
EventTarget.prototype.allowedEvents_ = {};
/**
* An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
* the standard DOM API.
*
* @function
* @see {@link EventTarget#on}
*/
EventTarget.prototype.addEventListener = EventTarget.prototype.on;
/**
* An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
* the standard DOM API.
*
* @function
* @see {@link EventTarget#off}
*/
EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
/**
* An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
* the standard DOM API.
*
* @function
* @see {@link EventTarget#trigger}
*/
EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
/**
* @file mixins/evented.js
* @module evented
*/
const objName = obj => {
if (typeof obj.name === 'function') {
return obj.name();
}
if (typeof obj.name === 'string') {
return obj.name;
}
if (obj.name_) {
return obj.name_;
}
if (obj.constructor && obj.constructor.name) {
return obj.constructor.name;
}
return typeof obj;
};
/**
* Returns whether or not an object has had the evented mixin applied.
*
* @param {Object} object
* An object to test.
*
* @return {boolean}
* Whether or not the object appears to be evented.
*/
const isEvented = object => object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
/**
* Adds a callback to run after the evented mixin applied.
*
* @param {Object} target
* An object to Add
* @param {Function} callback
* The callback to run.
*/
const addEventedCallback = (target, callback) => {
if (isEvented(target)) {
callback();
} else {
if (!target.eventedCallbacks) {
target.eventedCallbacks = [];
}
target.eventedCallbacks.push(callback);
}
};
/**
* Whether a value is a valid event type - non-empty string or array.
*
* @private
* @param {string|Array} type
* The type value to test.
*
* @return {boolean}
* Whether or not the type is a valid event type.
*/
const isValidEventType = type =>
// The regex here verifies that the `type` contains at least one non-
// whitespace character.
typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
/**
* Validates a value to determine if it is a valid event target. Throws if not.
*
* @private
* @throws {Error}
* If the target does not appear to be a valid event target.
*
* @param {Object} target
* The object to test.
*
* @param {Object} obj
* The evented object we are validating for
*
* @param {string} fnName
* The name of the evented mixin function that called this.
*/
const validateTarget = (target, obj, fnName) => {
if (!target || !target.nodeName && !isEvented(target)) {
throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
}
};
/**
* Validates a value to determine if it is a valid event target. Throws if not.
*
* @private
* @throws {Error}
* If the type does not appear to be a valid event type.
*
* @param {string|Array} type
* The type to test.
*
* @param {Object} obj
* The evented object we are validating for
*
* @param {string} fnName
* The name of the evented mixin function that called this.
*/
const validateEventType = (type, obj, fnName) => {
if (!isValidEventType(type)) {
throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
}
};
/**
* Validates a value to determine if it is a valid listener. Throws if not.
*
* @private
* @throws {Error}
* If the listener is not a function.
*
* @param {Function} listener
* The listener to test.
*
* @param {Object} obj
* The evented object we are validating for
*
* @param {string} fnName
* The name of the evented mixin function that called this.
*/
const validateListener = (listener, obj, fnName) => {
if (typeof listener !== 'function') {
throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
}
};
/**
* Takes an array of arguments given to `on()` or `one()`, validates them, and
* normalizes them into an object.
*
* @private
* @param {Object} self
* The evented object on which `on()` or `one()` was called. This
* object will be bound as the `this` value for the listener.
*
* @param {Array} args
* An array of arguments passed to `on()` or `one()`.
*
* @param {string} fnName
* The name of the evented mixin function that called this.
*
* @return {Object}
* An object containing useful values for `on()` or `one()` calls.
*/
const normalizeListenArgs = (self, args, fnName) => {
// If the number of arguments is less than 3, the target is always the
// evented object itself.
const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
let target;
let type;
let listener;
if (isTargetingSelf) {
target = self.eventBusEl_;
// Deal with cases where we got 3 arguments, but we are still listening to
// the evented object itself.
if (args.length >= 3) {
args.shift();
}
[type, listener] = args;
} else {
// This was `[target, type, listener] = args;` but this block needs more than
// one statement to produce minified output compatible with Chrome 53.
// See https://github.com/videojs/video.js/pull/8810
target = args[0];
type = args[1];
listener = args[2];
}
validateTarget(target, self, fnName);
validateEventType(type, self, fnName);
validateListener(listener, self, fnName);
listener = bind_(self, listener);
return {
isTargetingSelf,
target,
type,
listener
};
};
/**
* Adds the listener to the event type(s) on the target, normalizing for
* the type of target.
*
* @private
* @param {Element|Object} target
* A DOM node or evented object.
*
* @param {string} method
* The event binding method to use ("on" or "one").
*
* @param {string|Array} type
* One or more event type(s).
*
* @param {Function} listener
* A listener function.
*/
const listen = (target, method, type, listener) => {
validateTarget(target, target, method);
if (target.nodeName) {
Events[method](target, type, listener);
} else {
target[method](type, listener);
}
};
/**
* Contains methods that provide event capabilities to an object which is passed
* to {@link module:evented|evented}.
*
* @mixin EventedMixin
*/
const EventedMixin = {
/**
* Add a listener to an event (or events) on this object or another evented
* object.
*
* @param {string|Array|Element|Object} targetOrType
* If this is a string or array, it represents the event type(s)
* that will trigger the listener.
*
* Another evented object can be passed here instead, which will
* cause the listener to listen for events on _that_ object.
*
* In either case, the listener's `this` value will be bound to
* this object.
*
* @param {string|Array|Function} typeOrListener
* If the first argument was a string or array, this should be the
* listener function. Otherwise, this is a string or array of event
* type(s).
*
* @param {Function} [listener]
* If the first argument was another evented object, this will be
* the listener function.
*/
on(...args) {
const {
isTargetingSelf,
target,
type,
listener
} = normalizeListenArgs(this, args, 'on');
listen(target, 'on', type, listener);
// If this object is listening to another evented object.
if (!isTargetingSelf) {
// If this object is disposed, remove the listener.
const removeListenerOnDispose = () => this.off(target, type, listener);
// Use the same function ID as the listener so we can remove it later it
// using the ID of the original listener.
removeListenerOnDispose.guid = listener.guid;
// Add a listener to the target's dispose event as well. This ensures
// that if the target is disposed BEFORE this object, we remove the
// removal listener that was just added. Otherwise, we create a memory leak.
const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
// Use the same function ID as the listener so we can remove it later
// it using the ID of the original listener.
removeRemoverOnTargetDispose.guid = listener.guid;
listen(this, 'on', 'dispose', removeListenerOnDispose);
listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
}
},
/**
* Add a listener to an event (or events) on this object or another evented
* object. The listener will be called once per event and then removed.
*
* @param {string|Array|Element|Object} targetOrType
* If this is a string or array, it represents the event type(s)
* that will trigger the listener.
*
* Another evented object can be passed here instead, which will
* cause the listener to listen for events on _that_ object.
*
* In either case, the listener's `this` value will be bound to
* this object.
*
* @param {string|Array|Function} typeOrListener
* If the first argument was a string or array, this should be the
* listener function. Otherwise, this is a string or array of event
* type(s).
*
* @param {Function} [listener]
* If the first argument was another evented object, this will be
* the listener function.
*/
one(...args) {
const {
isTargetingSelf,
target,
type,
listener
} = normalizeListenArgs(this, args, 'one');
// Targeting this evented object.
if (isTargetingSelf) {
listen(target, 'one', type, listener);
// Targeting another evented object.
} else {
// TODO: This wrapper is incorrect! It should only
// remove the wrapper for the event type that called it.
// Instead all listeners are removed on the first trigger!
// see https://github.com/videojs/video.js/issues/5962
const wrapper = (...largs) => {
this.off(target, type, wrapper);
listener.apply(null, largs);
};
// Use the same function ID as the listener so we can remove it later
// it using the ID of the original listener.
wrapper.guid = listener.guid;
listen(target, 'one', type, wrapper);
}
},
/**
* Add a listener to an event (or events) on this object or another evented
* object. The listener will only be called once for the first event that is triggered
* then removed.
*
* @param {string|Array|Element|Object} targetOrType
* If this is a string or array, it represents the event type(s)
* that will trigger the listener.
*
* Another evented object can be passed here instead, which will
* cause the listener to listen for events on _that_ object.
*
* In either case, the listener's `this` value will be bound to
* this object.
*
* @param {string|Array|Function} typeOrListener
* If the first argument was a string or array, this should be the
* listener function. Otherwise, this is a string or array of event
* type(s).
*
* @param {Function} [listener]
* If the first argument was another evented object, this will be
* the listener function.
*/
any(...args) {
const {
isTargetingSelf,
target,
type,
listener
} = normalizeListenArgs(this, args, 'any');
// Targeting this evented object.
if (isTargetingSelf) {
listen(target, 'any', type, listener);
// Targeting another evented object.
} else {
const wrapper = (...largs) => {
this.off(target, type, wrapper);
listener.apply(null, largs);
};
// Use the same function ID as the listener so we can remove it later
// it using the ID of the original listener.
wrapper.guid = listener.guid;
listen(target, 'any', type, wrapper);
}
},
/**
* Removes listener(s) from event(s) on an evented object.
*
* @param {string|Array|Element|Object} [targetOrType]
* If this is a string or array, it represents the event type(s).
*
* Another evented object can be passed here instead, in which case
* ALL 3 arguments are _required_.
*
* @param {string|Array|Function} [typeOrListener]
* If the first argument was a string or array, this may be the
* listener function. Otherwise, this is a string or array of event
* type(s).
*
* @param {Function} [listener]
* If the first argument was another evented object, this will be
* the listener function; otherwise, _all_ listeners bound to the
* event type(s) will be removed.
*/
off(targetOrType, typeOrListener, listener) {
// Targeting this evented object.
if (!targetOrType || isValidEventType(targetOrType)) {
off(this.eventBusEl_, targetOrType, typeOrListener);
// Targeting another evented object.
} else {
const target = targetOrType;
const type = typeOrListener;
// Fail fast and in a meaningful way!
validateTarget(target, this, 'off');
validateEventType(type, this, 'off');
validateListener(listener, this, 'off');
// Ensure there's at least a guid, even if the function hasn't been used
listener = bind_(this, listener);
// Remove the dispose listener on this evented object, which was given
// the same guid as the event listener in on().
this.off('dispose', listener);
if (target.nodeName) {
off(target, type, listener);
off(target, 'dispose', listener);
} else if (isEvented(target)) {
target.off(type, listener);
target.off('dispose', listener);
}
}
},
/**
* Fire an event on this evented object, causing its listeners to be called.
*
* @param {string|Object} event
* An event type or an object with a type property.
*
* @param {Object} [hash]
* An additional object to pass along to listeners.
*
* @return {boolean}
* Whether or not the default behavior was prevented.
*/
trigger(event, hash) {
validateTarget(this.eventBusEl_, this, 'trigger');
const type = event && typeof event !== 'string' ? event.type : event;
if (!isValidEventType(type)) {
throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
}
return trigger(this.eventBusEl_, event, hash);
}
};
/**
* Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
*
* @param {Object} target
* The object to which to add event methods.
*
* @param {Object} [options={}]
* Options for customizing the mixin behavior.
*
* @param {string} [options.eventBusKey]
* By default, adds a `eventBusEl_` DOM element to the target object,
* which is used as an event bus. If the target object already has a
* DOM element that should be used, pass its key here.
*
* @return {Object}
* The target object.
*/
function evented(target, options = {}) {
const {
eventBusKey
} = options;
// Set or create the eventBusEl_.
if (eventBusKey) {
if (!target[eventBusKey].nodeName) {
throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
}
target.eventBusEl_ = target[eventBusKey];
} else {
target.eventBusEl_ = createEl('span', {
className: 'vjs-event-bus'
});
}
Object.assign(target, EventedMixin);
if (target.eventedCallbacks) {
target.eventedCallbacks.forEach(callback => {
callback();
});
}
// When any evented object is disposed, it removes all its listeners.
target.on('dispose', () => {
target.off();
[target, target.el_, target.eventBusEl_].forEach(function (val) {
if (val && DomData.has(val)) {
DomData.delete(val);
}
});
window.setTimeout(() => {
target.eventBusEl_ = null;
}, 0);
});
return target;
}
/**
* @file mixins/stateful.js
* @module stateful
*/
/**
* Contains methods that provide statefulness to an object which is passed
* to {@link module:stateful}.
*
* @mixin StatefulMixin
*/
const StatefulMixin = {
/**
* A hash containing arbitrary keys and values representing the state of
* the object.
*
* @type {Object}
*/
state: {},
/**
* Set the state of an object by mutating its
* {@link module:stateful~StatefulMixin.state|state} object in place.
*
* @fires module:stateful~StatefulMixin#statechanged
* @param {Object|Function} stateUpdates
* A new set of properties to shallow-merge into the plugin state.
* Can be a plain object or a function returning a plain object.
*
* @return {Object|undefined}
* An object containing changes that occurred. If no changes
* occurred, returns `undefined`.
*/
setState(stateUpdates) {
// Support providing the `stateUpdates` state as a function.
if (typeof stateUpdates === 'function') {
stateUpdates = stateUpdates();
}
let changes;
each(stateUpdates, (value, key) => {
// Record the change if the value is different from what's in the
// current state.
if (this.state[key] !== value) {
changes = changes || {};
changes[key] = {
from: this.state[key],
to: value
};
}
this.state[key] = value;
});
// Only trigger "statechange" if there were changes AND we have a trigger
// function. This allows us to not require that the target object be an
// evented object.
if (changes && isEvented(this)) {
/**
* An event triggered on an object that is both
* {@link module:stateful|stateful} and {@link module:evented|evented}
* indicating that its state has changed.
*
* @event module:stateful~StatefulMixin#statechanged
* @type {Object}
* @property {Object} changes
* A hash containing the properties that were changed and
* the values they were changed `from` and `to`.
*/
this.trigger({
changes,
type: 'statechanged'
});
}
return changes;
}
};
/**
* Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
* object.
*
* If the target object is {@link module:evented|evented} and has a
* `handleStateChanged` method, that method will be automatically bound to the
* `statechanged` event on itself.
*
* @param {Object} target
* The object to be made stateful.
*
* @param {Object} [defaultState]
* A default set of properties to populate the newly-stateful object's
* `state` property.
*
* @return {Object}
* Returns the `target`.
*/
function stateful(target, defaultState) {
Object.assign(target, StatefulMixin);
// This happens after the mixing-in because we need to replace the `state`
// added in that step.
target.state = Object.assign({}, target.state, defaultState);
// Auto-bind the `handleStateChanged` method of the target object if it exists.
if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
target.on('statechanged', target.handleStateChanged);
}
return target;
}
/**
* @file str.js
* @module to-lower-case
*/
/**
* Lowercase the first letter of a string.
*
* @param {string} string
* String to be lowercased
*
* @return {string}
* The string with a lowercased first letter
*/
const toLowerCase = function (string) {
if (typeof string !== 'string') {
return string;
}
return string.replace(/./, w => w.toLowerCase());
};
/**
* Uppercase the first letter of a string.
*
* @param {string} string
* String to be uppercased
*
* @return {string}
* The string with an uppercased first letter
*/
const toTitleCase = function (string) {
if (typeof string !== 'string') {
return string;
}
return string.replace(/./, w => w.toUpperCase());
};
/**
* Compares the TitleCase versions of the two strings for equality.
*
* @param {string} str1
* The first string to compare
*
* @param {string} str2
* The second string to compare
*
* @return {boolean}
* Whether the TitleCase versions of the strings are equal
*/
const titleCaseEquals = function (str1, str2) {
return toTitleCase(str1) === toTitleCase(str2);
};
var Str = /*#__PURE__*/Object.freeze({
__proto__: null,
toLowerCase: toLowerCase,
toTitleCase: toTitleCase,
titleCaseEquals: titleCaseEquals
});
/**
* Player Component - Base class for all UI objects
*
* @file component.js
*/
/** @import Player from './player' */
/**
* A callback to be called if and when the component is ready.
* `this` will be the Component instance.
*
* @callback ReadyCallback
* @returns {void}
*/
/**
* Base class for all UI Components.
* Components are UI objects which represent both a javascript object and an element
* in the DOM. They can be children of other components, and can have
* children themselves.
*
* Components can also use methods from {@link EventTarget}
*/
class Component {
/**
* Creates an instance of this class.
*
* @param {Player} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of component options.
*
* @param {Object[]} [options.children]
* An array of children objects to initialize this component with. Children objects have
* a name property that will be used if more than one component of the same type needs to be
* added.
*
* @param {string} [options.className]
* A class or space separated list of classes to add the component
*
* @param {ReadyCallback} [ready]
* Function that gets called when the `Component` is ready.
*/
constructor(player, options, ready) {
// The component might be the player itself and we can't pass `this` to super
if (!player && this.play) {
this.player_ = player = this; // eslint-disable-line
} else {
this.player_ = player;
}
this.isDisposed_ = false;
// Hold the reference to the parent component via `addChild` method
this.parentComponent_ = null;
// Make a copy of prototype.options_ to protect against overriding defaults
this.options_ = merge({}, this.options_);
// Updated options with supplied options
options = this.options_ = merge(this.options_, options);
// Get ID from options or options element if one is supplied
this.id_ = options.id || options.el && options.el.id;
// If there was no ID from the options, generate one
if (!this.id_) {
// Don't require the player ID function in the case of mock players
const id = player && player.id && player.id() || 'no_player';
this.id_ = `${id}_component_${newGUID()}`;
}
this.name_ = options.name || null;
// Create element if one wasn't provided in options
if (options.el) {
this.el_ = options.el;
} else if (options.createEl !== false) {
this.el_ = this.createEl();
}
if (options.className && this.el_) {
options.className.split(' ').forEach(c => this.addClass(c));
}
// Remove the placeholder event methods. If the component is evented, the
// real methods are added next
['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
this[fn] = undefined;
});
// if evented is anything except false, we want to mixin in evented
if (options.evented !== false) {
// Make this an evented object and use `el_`, if available, as its event bus
evented(this, {
eventBusKey: this.el_ ? 'el_' : null
});
this.handleLanguagechange = this.handleLanguagechange.bind(this);
this.on(this.player_, 'languagechange', this.handleLanguagechange);
}
stateful(this, this.constructor.defaultState);
this.children_ = [];
this.childIndex_ = {};
this.childNameIndex_ = {};
this.setTimeoutIds_ = new Set();
this.setIntervalIds_ = new Set();
this.rafIds_ = new Set();
this.namedRafs_ = new Map();
this.clearingTimersOnDispose_ = false;
// Add any child components in options
if (options.initChildren !== false) {
this.initChildren();
}
// Don't want to trigger ready here or it will go before init is actually
// finished for all children that run this constructor
this.ready(ready);
if (options.reportTouchActivity !== false) {
this.enableTouchActivity();
}
}
// `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
// They are replaced or removed in the constructor
/**
* Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
* function that will get called when an event with a certain name gets triggered.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to call with `EventTarget`s
*/
/**
* Removes an `event listener` for a specific event from an instance of `EventTarget`.
* This makes it so that the `event listener` will no longer get called when the
* named event happens.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} [fn]
* The function to remove. If not specified, all listeners managed by Video.js will be removed.
*/
/**
* This function will add an `event listener` that gets triggered only once. After the
* first trigger it will get removed. This is like adding an `event listener`
* with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to be called once for each event name.
*/
/**
* This function will add an `event listener` that gets triggered only once and is
* removed from all events. This is like adding an array of `event listener`s
* with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
* first time it is triggered.
*
* @param {string|string[]} type
* An event name or an array of event names.
*
* @param {Function} fn
* The function to be called once for each event name.
*/
/**
* This function causes an event to happen. This will then cause any `event listeners`
* that are waiting for that event, to get called. If there are no `event listeners`
* for an event then nothing will happen.
*
* If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
* Trigger will also call the `on` + `uppercaseEventName` function.
*
* Example:
* 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
* `onClick` if it exists.
*
* @param {string|Event|Object} event
* The name of the event, an `Event`, or an object with a key of type set to
* an event name.
*
* @param {Object} [hash]
* Optionally extra argument to pass through to an event listener
*/
/**
* Dispose of the `Component` and all child components.
*
* @fires Component#dispose
*
* @param {Object} options
* @param {Element} options.originalEl element with which to replace player element
*/
dispose(options = {}) {
// Bail out if the component has already been disposed.
if (this.isDisposed_) {
return;
}
if (this.readyQueue_) {
this.readyQueue_.length = 0;
}
/**
* Triggered when a `Component` is disposed.
*
* @event Component#dispose
* @type {Event}
*
* @property {boolean} [bubbles=false]
* set to false so that the dispose event does not
* bubble up
*/
this.trigger({
type: 'dispose',
bubbles: false
});
this.isDisposed_ = true;
// Dispose all children.
if (this.children_) {
for (let i = this.children_.length - 1; i >= 0; i--) {
if (this.children_[i].dispose) {
this.children_[i].dispose();
}
}
}
// Delete child references
this.children_ = null;
this.childIndex_ = null;
this.childNameIndex_ = null;
this.parentComponent_ = null;
if (this.el_) {
// Remove element from DOM
if (this.el_.parentNode) {
if (options.restoreEl) {
this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
} else {
this.el_.parentNode.removeChild(this.el_);
}
}
this.el_ = null;
}
// remove reference to the player after disposing of the element
this.player_ = null;
}
/**
* Determine whether or not this component has been disposed.
*
* @return {boolean}
* If the component has been disposed, will be `true`. Otherwise, `false`.
*/
isDisposed() {
return Boolean(this.isDisposed_);
}
/**
* Return the {@link Player} that the `Component` has attached to.
*
* @return {Player}
* The player that this `Component` has attached to.
*/
player() {
return this.player_;
}
/**
* Deep merge of options objects with new options.
* > Note: When both `obj` and `options` contain properties whose values are objects.
* The two properties get merged using {@link module:obj.merge}
*
* @param {Object} obj
* The object that contains new options.
*
* @return {Object}
* A new object of `this.options_` and `obj` merged together.
*/
options(obj) {
if (!obj) {
return this.options_;
}
this.options_ = merge(this.options_, obj);
return this.options_;
}
/**
* Get the `Component`s DOM element
*
* @return {Element}
* The DOM element for this `Component`.
*/
el() {
return this.el_;
}
/**
* Create the `Component`s DOM element.
*
* @param {string} [tagName]
* Element's DOM node type. e.g. 'div'
*
* @param {Object} [properties]
* An object of properties that should be set.
*
* @param {Object} [attributes]
* An object of attributes that should be set.
*
* @return {Element}
* The element that gets created.
*/
createEl(tagName, properties, attributes) {
return createEl(tagName, properties, attributes);
}
/**
* Localize a string given the string in english.
*
* If tokens are provided, it'll try and run a simple token replacement on the provided string.
* The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
*
* If a `defaultValue` is provided, it'll use that over `string`,
* if a value isn't found in provided language files.
* This is useful if you want to have a descriptive key for token replacement
* but have a succinct localized string and not require `en.json` to be included.
*
* Currently, it is used for the progress bar timing.
* ```js
* {
* "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
* }
* ```
* It is then used like so:
* ```js
* this.localize('progress bar timing: currentTime={1} duration{2}',
* [this.player_.currentTime(), this.player_.duration()],
* '{1} of {2}');
* ```
*
* Which outputs something like: `01:23 of 24:56`.
*
*
* @param {string} string
* The string to localize and the key to lookup in the language files.
* @param {string[]} [tokens]
* If the current item has token replacements, provide the tokens here.
* @param {string} [defaultValue]
* Defaults to `string`. Can be a default value to use for token replacement
* if the lookup key is needed to be separate.
*
* @return {string}
* The localized string or if no localization exists the english string.
*/
localize(string, tokens, defaultValue = string) {
const code = this.player_.language && this.player_.language();
const languages = this.player_.languages && this.player_.languages();
const language = languages && languages[code];
const primaryCode = code && code.split('-')[0];
const primaryLang = languages && languages[primaryCode];
let localizedString = defaultValue;
if (language && language[string]) {
localizedString = language[string];
} else if (primaryLang && primaryLang[string]) {
localizedString = primaryLang[string];
}
if (tokens) {
localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
const value = tokens[index - 1];
let ret = value;
if (typeof value === 'undefined') {
ret = match;
}
return ret;
});
}
return localizedString;
}
/**
* Handles language change for the player in components. Should be overridden by sub-components.
*
* @abstract
*/
handleLanguagechange() {}
/**
* Return the `Component`s DOM element. This is where children get inserted.
* This will usually be the the same as the element returned in {@link Component#el}.
*
* @return {Element}
* The content element for this `Component`.
*/
contentEl() {
return this.contentEl_ || this.el_;
}
/**
* Get this `Component`s ID
*
* @return {string}
* The id of this `Component`
*/
id() {
return this.id_;
}
/**
* Get the `Component`s name. The name gets used to reference the `Component`
* and is set during registration.
*
* @return {string}
* The name of this `Component`.
*/
name() {
return this.name_;
}
/**
* Get an array of all child components
*
* @return {Array}
* The children
*/
children() {
return this.children_;
}
/**
* Returns the child `Component` with the given `id`.
*
* @param {string} id
* The id of the child `Component` to get.
*
* @return {Component|undefined}
* The child `Component` with the given `id` or undefined.
*/
getChildById(id) {
return this.childIndex_[id];
}
/**
* Returns the child `Component` with the given `name`.
*
* @param {string} name
* The name of the child `Component` to get.
*
* @return {Component|undefined}
* The child `Component` with the given `name` or undefined.
*/
getChild(name) {
if (!name) {
return;
}
return this.childNameIndex_[name];
}
/**
* Returns the descendant `Component` following the givent
* descendant `names`. For instance ['foo', 'bar', 'baz'] would
* try to get 'foo' on the current component, 'bar' on the 'foo'
* component and 'baz' on the 'bar' component and return undefined
* if any of those don't exist.
*
* @param {...string[]|...string} names
* The name of the child `Component` to get.
*
* @return {Component|undefined}
* The descendant `Component` following the given descendant
* `names` or undefined.
*/
getDescendant(...names) {
// flatten array argument into the main array
names = names.reduce((acc, n) => acc.concat(n), []);
let currentChild = this;
for (let i = 0; i < names.length; i++) {
currentChild = currentChild.getChild(names[i]);
if (!currentChild || !currentChild.getChild) {
return;
}
}
return currentChild;
}
/**
* Adds an SVG icon element to another element or component.
*
* @param {string} iconName
* The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
*
* @param {Element} [el=this.el()]
* Element to set the title on. Defaults to the current Component's element.
*
* @return {Element}
* The newly created icon element.
*/
setIcon(iconName, el = this.el()) {
// TODO: In v9 of video.js, we will want to remove font icons entirely.
// This means this check, as well as the others throughout the code, and
// the unecessary CSS for font icons, will need to be removed.
// See https://github.com/videojs/video.js/pull/8260 as to which components
// need updating.
if (!this.player_.options_.experimentalSvgIcons) {
return;
}
const xmlnsURL = 'http://www.w3.org/2000/svg';
// The below creates an element in the format of:
//
const iconContainer = createEl('span', {
className: 'vjs-icon-placeholder vjs-svg-icon'
}, {
'aria-hidden': 'true'
});
const svgEl = document.createElementNS(xmlnsURL, 'svg');
svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
const useEl = document.createElementNS(xmlnsURL, 'use');
svgEl.appendChild(useEl);
useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
iconContainer.appendChild(svgEl);
// Replace a pre-existing icon if one exists.
if (this.iconIsSet_) {
el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
} else {
el.appendChild(iconContainer);
}
this.iconIsSet_ = true;
return iconContainer;
}
/**
* Add a child `Component` inside the current `Component`.
*
* @param {string|Component} child
* The name or instance of a child to add.
*
* @param {Object} [options={}]
* The key/value store of options that will get passed to children of
* the child.
*
* @param {number} [index=this.children_.length]
* The index to attempt to add a child into.
*
*
* @return {Component}
* The `Component` that gets added as a child. When using a string the
* `Component` will get created by this process.
*/
addChild(child, options = {}, index = this.children_.length) {
let component;
let componentName;
// If child is a string, create component with options
if (typeof child === 'string') {
componentName = toTitleCase(child);
const componentClassName = options.componentClass || componentName;
// Set name through options
options.name = componentName;
// Create a new object & element for this controls set
// If there's no .player_, this is a player
const ComponentClass = Component.getComponent(componentClassName);
if (!ComponentClass) {
throw new Error(`Component ${componentClassName} does not exist`);
}
// data stored directly on the videojs object may be
// misidentified as a component to retain
// backwards-compatibility with 4.x. check to make sure the
// component class can be instantiated.
if (typeof ComponentClass !== 'function') {
return null;
}
component = new ComponentClass(this.player_ || this, options);
// child is a component instance
} else {
component = child;
}
if (component.parentComponent_) {
component.parentComponent_.removeChild(component);
}
this.children_.splice(index, 0, component);
component.parentComponent_ = this;
if (typeof component.id === 'function') {
this.childIndex_[component.id()] = component;
}
// If a name wasn't used to create the component, check if we can use the
// name function of the component
componentName = componentName || component.name && toTitleCase(component.name());
if (componentName) {
this.childNameIndex_[componentName] = component;
this.childNameIndex_[toLowerCase(componentName)] = component;
}
// Add the UI object's element to the container div (box)
// Having an element is not required
if (typeof component.el === 'function' && component.el()) {
// If inserting before a component, insert before that component's element
let refNode = null;
if (this.children_[index + 1]) {
// Most children are components, but the video tech is an HTML element
if (this.children_[index + 1].el_) {
refNode = this.children_[index + 1].el_;
} else if (isEl(this.children_[index + 1])) {
refNode = this.children_[index + 1];
}
}
this.contentEl().insertBefore(component.el(), refNode);
}
// Return so it can stored on parent object if desired.
return component;
}
/**
* Remove a child `Component` from this `Component`s list of children. Also removes
* the child `Component`s element from this `Component`s element.
*
* @param {Component} component
* The child `Component` to remove.
*/
removeChild(component) {
if (typeof component === 'string') {
component = this.getChild(component);
}
if (!component || !this.children_) {
return;
}
let childFound = false;
for (let i = this.children_.length - 1; i >= 0; i--) {
if (this.children_[i] === component) {
childFound = true;
this.children_.splice(i, 1);
break;
}
}
if (!childFound) {
return;
}
component.parentComponent_ = null;
this.childIndex_[component.id()] = null;
this.childNameIndex_[toTitleCase(component.name())] = null;
this.childNameIndex_[toLowerCase(component.name())] = null;
const compEl = component.el();
if (compEl && compEl.parentNode === this.contentEl()) {
this.contentEl().removeChild(component.el());
}
}
/**
* Add and initialize default child `Component`s based upon options.
*/
initChildren() {
const children = this.options_.children;
if (children) {
// `this` is `parent`
const parentOptions = this.options_;
const handleAdd = child => {
const name = child.name;
let opts = child.opts;
// Allow options for children to be set at the parent options
// e.g. videojs(id, { controlBar: false });
// instead of videojs(id, { children: { controlBar: false });
if (parentOptions[name] !== undefined) {
opts = parentOptions[name];
}
// Allow for disabling default components
// e.g. options['children']['posterImage'] = false
if (opts === false) {
return;
}
// Allow options to be passed as a simple boolean if no configuration
// is necessary.
if (opts === true) {
opts = {};
}
// We also want to pass the original player options
// to each component as well so they don't need to
// reach back into the player for options later.
opts.playerOptions = this.options_.playerOptions;
// Create and add the child component.
// Add a direct reference to the child by name on the parent instance.
// If two of the same component are used, different names should be supplied
// for each
const newChild = this.addChild(name, opts);
if (newChild) {
this[name] = newChild;
}
};
// Allow for an array of children details to passed in the options
let workingChildren;
const Tech = Component.getComponent('Tech');
if (Array.isArray(children)) {
workingChildren = children;
} else {
workingChildren = Object.keys(children);
}
workingChildren
// children that are in this.options_ but also in workingChildren would
// give us extra children we do not want. So, we want to filter them out.
.concat(Object.keys(this.options_).filter(function (child) {
return !workingChildren.some(function (wchild) {
if (typeof wchild === 'string') {
return child === wchild;
}
return child === wchild.name;
});
})).map(child => {
let name;
let opts;
if (typeof child === 'string') {
name = child;
opts = children[name] || this.options_[name] || {};
} else {
name = child.name;
opts = child;
}
return {
name,
opts
};
}).filter(child => {
// we have to make sure that child.name isn't in the techOrder since
// techs are registered as Components but can't aren't compatible
// See https://github.com/videojs/video.js/issues/2772
const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name));
return c && !Tech.isTech(c);
}).forEach(handleAdd);
}
}
/**
* Builds the default DOM class name. Should be overridden by sub-components.
*
* @return {string}
* The DOM class name for this object.
*
* @abstract
*/
buildCSSClass() {
// Child classes can include a function that does:
// return 'CLASS NAME' + this._super();
return '';
}
/**
* Bind a listener to the component's ready state.
* Different from event listeners in that if the ready event has already happened
* it will trigger the function immediately.
*
* @param {ReadyCallback} fn
* Function that gets called when the `Component` is ready.
*/
ready(fn, sync = false) {
if (!fn) {
return;
}
if (!this.isReady_) {
this.readyQueue_ = this.readyQueue_ || [];
this.readyQueue_.push(fn);
return;
}
if (sync) {
fn.call(this);
} else {
// Call the function asynchronously by default for consistency
this.setTimeout(fn, 1);
}
}
/**
* Trigger all the ready listeners for this `Component`.
*
* @fires Component#ready
*/
triggerReady() {
this.isReady_ = true;
// Ensure ready is triggered asynchronously
this.setTimeout(function () {
const readyQueue = this.readyQueue_;
// Reset Ready Queue
this.readyQueue_ = [];
if (readyQueue && readyQueue.length > 0) {
readyQueue.forEach(function (fn) {
fn.call(this);
}, this);
}
// Allow for using event listeners also
/**
* Triggered when a `Component` is ready.
*
* @event Component#ready
* @type {Event}
*/
this.trigger('ready');
}, 1);
}
/**
* Find a single DOM element matching a `selector`. This can be within the `Component`s
* `contentEl()` or another custom context.
*
* @param {string} selector
* A valid CSS selector, which will be passed to `querySelector`.
*
* @param {Element|string} [context=this.contentEl()]
* A DOM element within which to query. Can also be a selector string in
* which case the first matching element will get used as context. If
* missing `this.contentEl()` gets used. If `this.contentEl()` returns
* nothing it falls back to `document`.
*
* @return {Element|null}
* the dom element that was found, or null
*
* @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
*/
$(selector, context) {
return $(selector, context || this.contentEl());
}
/**
* Finds all DOM element matching a `selector`. This can be within the `Component`s
* `contentEl()` or another custom context.
*
* @param {string} selector
* A valid CSS selector, which will be passed to `querySelectorAll`.
*
* @param {Element|string} [context=this.contentEl()]
* A DOM element within which to query. Can also be a selector string in
* which case the first matching element will get used as context. If
* missing `this.contentEl()` gets used. If `this.contentEl()` returns
* nothing it falls back to `document`.
*
* @return {NodeList}
* a list of dom elements that were found
*
* @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
*/
$$(selector, context) {
return $$(selector, context || this.contentEl());
}
/**
* Check if a component's element has a CSS class name.
*
* @param {string} classToCheck
* CSS class name to check.
*
* @return {boolean}
* - True if the `Component` has the class.
* - False if the `Component` does not have the class`
*/
hasClass(classToCheck) {
return hasClass(this.el_, classToCheck);
}
/**
* Add a CSS class name to the `Component`s element.
*
* @param {...string} classesToAdd
* One or more CSS class name to add.
*/
addClass(...classesToAdd) {
addClass(this.el_, ...classesToAdd);
}
/**
* Remove a CSS class name from the `Component`s element.
*
* @param {...string} classesToRemove
* One or more CSS class name to remove.
*/
removeClass(...classesToRemove) {
removeClass(this.el_, ...classesToRemove);
}
/**
* Add or remove a CSS class name from the component's element.
* - `classToToggle` gets added when {@link Component#hasClass} would return false.
* - `classToToggle` gets removed when {@link Component#hasClass} would return true.
*
* @param {string} classToToggle
* The class to add or remove. Passed to DOMTokenList's toggle()
*
* @param {boolean|Dom.PredicateCallback} [predicate]
* A boolean or function that returns a boolean. Passed to DOMTokenList's toggle().
*/
toggleClass(classToToggle, predicate) {
toggleClass(this.el_, classToToggle, predicate);
}
/**
* Show the `Component`s element if it is hidden by removing the
* 'vjs-hidden' class name from it.
*/
show() {
this.removeClass('vjs-hidden');
}
/**
* Hide the `Component`s element if it is currently showing by adding the
* 'vjs-hidden` class name to it.
*/
hide() {
this.addClass('vjs-hidden');
}
/**
* Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
* class name to it. Used during fadeIn/fadeOut.
*
* @private
*/
lockShowing() {
this.addClass('vjs-lock-showing');
}
/**
* Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
* class name from it. Used during fadeIn/fadeOut.
*
* @private
*/
unlockShowing() {
this.removeClass('vjs-lock-showing');
}
/**
* Get the value of an attribute on the `Component`s element.
*
* @param {string} attribute
* Name of the attribute to get the value from.
*
* @return {string|null}
* - The value of the attribute that was asked for.
* - Can be an empty string on some browsers if the attribute does not exist
* or has no value
* - Most browsers will return null if the attribute does not exist or has
* no value.
*
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
*/
getAttribute(attribute) {
return getAttribute(this.el_, attribute);
}
/**
* Set the value of an attribute on the `Component`'s element
*
* @param {string} attribute
* Name of the attribute to set.
*
* @param {string} value
* Value to set the attribute to.
*
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
*/
setAttribute(attribute, value) {
setAttribute(this.el_, attribute, value);
}
/**
* Remove an attribute from the `Component`s element.
*
* @param {string} attribute
* Name of the attribute to remove.
*
* @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
*/
removeAttribute(attribute) {
removeAttribute(this.el_, attribute);
}
/**
* Get or set the width of the component based upon the CSS styles.
* See {@link Component#dimension} for more detailed information.
*
* @param {number|string} [num]
* The width that you want to set postfixed with '%', 'px' or nothing.
*
* @param {boolean} [skipListeners]
* Skip the componentresize event trigger
*
* @return {number|undefined}
* The width when getting, zero if there is no width
*/
width(num, skipListeners) {
return this.dimension('width', num, skipListeners);
}
/**
* Get or set the height of the component based upon the CSS styles.
* See {@link Component#dimension} for more detailed information.
*
* @param {number|string} [num]
* The height that you want to set postfixed with '%', 'px' or nothing.
*
* @param {boolean} [skipListeners]
* Skip the componentresize event trigger
*
* @return {number|undefined}
* The height when getting, zero if there is no height
*/
height(num, skipListeners) {
return this.dimension('height', num, skipListeners);
}
/**
* Set both the width and height of the `Component` element at the same time.
*
* @param {number|string} width
* Width to set the `Component`s element to.
*
* @param {number|string} height
* Height to set the `Component`s element to.
*/
dimensions(width, height) {
// Skip componentresize listeners on width for optimization
this.width(width, true);
this.height(height);
}
/**
* Get or set width or height of the `Component` element. This is the shared code
* for the {@link Component#width} and {@link Component#height}.
*
* Things to know:
* - If the width or height in an number this will return the number postfixed with 'px'.
* - If the width/height is a percent this will return the percent postfixed with '%'
* - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
* defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
* See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
* for more information
* - If you want the computed style of the component, use {@link Component#currentWidth}
* and {@link {Component#currentHeight}
*
* @fires Component#componentresize
*
* @param {string} widthOrHeight
8 'width' or 'height'
*
* @param {number|string} [num]
8 New dimension
*
* @param {boolean} [skipListeners]
* Skip componentresize event trigger
*
* @return {number|undefined}
* The dimension when getting or 0 if unset
*/
dimension(widthOrHeight, num, skipListeners) {
if (num !== undefined) {
// Set to zero if null or literally NaN (NaN !== NaN)
if (num === null || num !== num) {
num = 0;
}
// Check if using css width/height (% or px) and adjust
if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
this.el_.style[widthOrHeight] = num;
} else if (num === 'auto') {
this.el_.style[widthOrHeight] = '';
} else {
this.el_.style[widthOrHeight] = num + 'px';
}
// skipListeners allows us to avoid triggering the resize event when setting both width and height
if (!skipListeners) {
/**
* Triggered when a component is resized.
*
* @event Component#componentresize
* @type {Event}
*/
this.trigger('componentresize');
}
return;
}
// Not setting a value, so getting it
// Make sure element exists
if (!this.el_) {
return 0;
}
// Get dimension value from style
const val = this.el_.style[widthOrHeight];
const pxIndex = val.indexOf('px');
if (pxIndex !== -1) {
// Return the pixel value with no 'px'
return parseInt(val.slice(0, pxIndex), 10);
}
// No px so using % or no style was set, so falling back to offsetWidth/height
// If component has display:none, offset will return 0
// TODO: handle display:none and no dimension style using px
return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
}
/**
* Get the computed width or the height of the component's element.
*
* Uses `window.getComputedStyle`.
*
* @param {string} widthOrHeight
* A string containing 'width' or 'height'. Whichever one you want to get.
*
* @return {number}
* The dimension that gets asked for or 0 if nothing was set
* for that dimension.
*/
currentDimension(widthOrHeight) {
let computedWidthOrHeight = 0;
if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
throw new Error('currentDimension only accepts width or height value');
}
computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
// remove 'px' from variable and parse as integer
computedWidthOrHeight = parseFloat(computedWidthOrHeight);
// if the computed value is still 0, it's possible that the browser is lying
// and we want to check the offset values.
// This code also runs wherever getComputedStyle doesn't exist.
if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
const rule = `offset${toTitleCase(widthOrHeight)}`;
computedWidthOrHeight = this.el_[rule];
}
return computedWidthOrHeight;
}
/**
* An object that contains width and height values of the `Component`s
* computed style. Uses `window.getComputedStyle`.
*
* @typedef {Object} Component~DimensionObject
*
* @property {number} width
* The width of the `Component`s computed style.
*
* @property {number} height
* The height of the `Component`s computed style.
*/
/**
* Get an object that contains computed width and height values of the
* component's element.
*
* Uses `window.getComputedStyle`.
*
* @return {Component~DimensionObject}
* The computed dimensions of the component's element.
*/
currentDimensions() {
return {
width: this.currentDimension('width'),
height: this.currentDimension('height')
};
}
/**
* Get the computed width of the component's element.
*
* Uses `window.getComputedStyle`.
*
* @return {number}
* The computed width of the component's element.
*/
currentWidth() {
return this.currentDimension('width');
}
/**
* Get the computed height of the component's element.
*
* Uses `window.getComputedStyle`.
*
* @return {number}
* The computed height of the component's element.
*/
currentHeight() {
return this.currentDimension('height');
}
/**
* Retrieves the position and size information of the component's element.
*
* @return {Object} An object with `boundingClientRect` and `center` properties.
* - `boundingClientRect`: An object with properties `x`, `y`, `width`,
* `height`, `top`, `right`, `bottom`, and `left`, representing
* the bounding rectangle of the element.
* - `center`: An object with properties `x` and `y`, representing
* the center point of the element. `width` and `height` are set to 0.
*/
getPositions() {
const rect = this.el_.getBoundingClientRect();
// Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
const boundingClientRect = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left
};
// Calculating the center position
const center = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
width: 0,
height: 0,
top: rect.top + rect.height / 2,
right: rect.left + rect.width / 2,
bottom: rect.top + rect.height / 2,
left: rect.left + rect.width / 2
};
return {
boundingClientRect,
center
};
}
/**
* Set the focus to this component
*/
focus() {
this.el_.focus();
}
/**
* Remove the focus from this component
*/
blur() {
this.el_.blur();
}
/**
* When this Component receives a `keydown` event which it does not process,
* it passes the event to the Player for handling.
*
* @param {KeyboardEvent} event
* The `keydown` event that caused this function to be called.
*/
handleKeyDown(event) {
if (this.player_) {
// We only stop propagation here because we want unhandled events to fall
// back to the browser. Exclude Tab for focus trapping, exclude also when spatialNavigation is enabled.
if (event.key !== 'Tab' && !(this.player_.options_.playerOptions.spatialNavigation && this.player_.options_.playerOptions.spatialNavigation.enabled)) {
event.stopPropagation();
}
this.player_.handleKeyDown(event);
}
}
/**
* Many components used to have a `handleKeyPress` method, which was poorly
* named because it listened to a `keydown` event. This method name now
* delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
* will not see their method calls stop working.
*
* @param {KeyboardEvent} event
* The event that caused this function to be called.
*/
handleKeyPress(event) {
this.handleKeyDown(event);
}
/**
* Emit a 'tap' events when touch event support gets detected. This gets used to
* support toggling the controls through a tap on the video. They get enabled
* because every sub-component would have extra overhead otherwise.
*
* @protected
* @fires Component#tap
* @listens Component#touchstart
* @listens Component#touchmove
* @listens Component#touchleave
* @listens Component#touchcancel
* @listens Component#touchend
*/
emitTapEvents() {
// Track the start time so we can determine how long the touch lasted
let touchStart = 0;
let firstTouch = null;
// Maximum movement allowed during a touch event to still be considered a tap
// Other popular libs use anywhere from 2 (hammer.js) to 15,
// so 10 seems like a nice, round number.
const tapMovementThreshold = 10;
// The maximum length a touch can be while still being considered a tap
const touchTimeThreshold = 200;
let couldBeTap;
this.on('touchstart', function (event) {
// If more than one finger, don't consider treating this as a click
if (event.touches.length === 1) {
// Copy pageX/pageY from the object
firstTouch = {
pageX: event.touches[0].pageX,
pageY: event.touches[0].pageY
};
// Record start time so we can detect a tap vs. "touch and hold"
touchStart = window.performance.now();
// Reset couldBeTap tracking
couldBeTap = true;
}
});
this.on('touchmove', function (event) {
// If more than one finger, don't consider treating this as a click
if (event.touches.length > 1) {
couldBeTap = false;
} else if (firstTouch) {
// Some devices will throw touchmoves for all but the slightest of taps.
// So, if we moved only a small distance, this could still be a tap
const xdiff = event.touches[0].pageX - firstTouch.pageX;
const ydiff = event.touches[0].pageY - firstTouch.pageY;
const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
if (touchDistance > tapMovementThreshold) {
couldBeTap = false;
}
}
});
const noTap = function () {
couldBeTap = false;
};
// TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
this.on('touchleave', noTap);
this.on('touchcancel', noTap);
// When the touch ends, measure how long it took and trigger the appropriate
// event
this.on('touchend', function (event) {
firstTouch = null;
// Proceed only if the touchmove/leave/cancel event didn't happen
if (couldBeTap === true) {
// Measure how long the touch lasted
const touchTime = window.performance.now() - touchStart;
// Make sure the touch was less than the threshold to be considered a tap
if (touchTime < touchTimeThreshold) {
// Don't let browser turn this into a click
event.preventDefault();
/**
* Triggered when a `Component` is tapped.
*
* @event Component#tap
* @type {MouseEvent}
*/
this.trigger('tap');
// It may be good to copy the touchend event object and change the
// type to tap, if the other event properties aren't exact after
// Events.fixEvent runs (e.g. event.target)
}
}
});
}
/**
* This function reports user activity whenever touch events happen. This can get
* turned off by any sub-components that wants touch events to act another way.
*
* Report user touch activity when touch events occur. User activity gets used to
* determine when controls should show/hide. It is simple when it comes to mouse
* events, because any mouse event should show the controls. So we capture mouse
* events that bubble up to the player and report activity when that happens.
* With touch events it isn't as easy as `touchstart` and `touchend` toggle player
* controls. So touch events can't help us at the player level either.
*
* User activity gets checked asynchronously. So what could happen is a tap event
* on the video turns the controls off. Then the `touchend` event bubbles up to
* the player. Which, if it reported user activity, would turn the controls right
* back on. We also don't want to completely block touch events from bubbling up.
* Furthermore a `touchmove` event and anything other than a tap, should not turn
* controls back on.
*
* @listens Component#touchstart
* @listens Component#touchmove
* @listens Component#touchend
* @listens Component#touchcancel
*/
enableTouchActivity() {
// Don't continue if the root player doesn't support reporting user activity
if (!this.player() || !this.player().reportUserActivity) {
return;
}
// listener for reporting that the user is active
const report = bind_(this.player(), this.player().reportUserActivity);
let touchHolding;
this.on('touchstart', function () {
report();
// For as long as the they are touching the device or have their mouse down,
// we consider them active even if they're not moving their finger or mouse.
// So we want to continue to update that they are active
this.clearInterval(touchHolding);
// report at the same interval as activityCheck
touchHolding = this.setInterval(report, 250);
});
const touchEnd = function (event) {
report();
// stop the interval that maintains activity if the touch is holding
this.clearInterval(touchHolding);
};
this.on('touchmove', report);
this.on('touchend', touchEnd);
this.on('touchcancel', touchEnd);
}
/**
* A callback that has no parameters and is bound into `Component`s context.
*
* @callback Component~GenericCallback
* @this Component
*/
/**
* Creates a function that runs after an `x` millisecond timeout. This function is a
* wrapper around `window.setTimeout`. There are a few reasons to use this one
* instead though:
* 1. It gets cleared via {@link Component#clearTimeout} when
* {@link Component#dispose} gets called.
* 2. The function callback will gets turned into a {@link Component~GenericCallback}
*
* > Note: You can't use `window.clearTimeout` on the id returned by this function. This
* will cause its dispose listener not to get cleaned up! Please use
* {@link Component#clearTimeout} or {@link Component#dispose} instead.
*
* @param {Component~GenericCallback} fn
* The function that will be run after `timeout`.
*
* @param {number} timeout
* Timeout in milliseconds to delay before executing the specified function.
*
* @return {number}
* Returns a timeout ID that gets used to identify the timeout. It can also
* get used in {@link Component#clearTimeout} to clear the timeout that
* was set.
*
* @listens Component#dispose
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
*/
setTimeout(fn, timeout) {
// declare as variables so they are properly available in timeout function
// eslint-disable-next-line
var timeoutId;
fn = bind_(this, fn);
this.clearTimersOnDispose_();
timeoutId = window.setTimeout(() => {
if (this.setTimeoutIds_.has(timeoutId)) {
this.setTimeoutIds_.delete(timeoutId);
}
fn();
}, timeout);
this.setTimeoutIds_.add(timeoutId);
return timeoutId;
}
/**
* Clears a timeout that gets created via `window.setTimeout` or
* {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
* use this function instead of `window.clearTimout`. If you don't your dispose
* listener will not get cleaned up until {@link Component#dispose}!
*
* @param {number} timeoutId
* The id of the timeout to clear. The return value of
* {@link Component#setTimeout} or `window.setTimeout`.
*
* @return {number}
* Returns the timeout id that was cleared.
*
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
*/
clearTimeout(timeoutId) {
if (this.setTimeoutIds_.has(timeoutId)) {
this.setTimeoutIds_.delete(timeoutId);
window.clearTimeout(timeoutId);
}
return timeoutId;
}
/**
* Creates a function that gets run every `x` milliseconds. This function is a wrapper
* around `window.setInterval`. There are a few reasons to use this one instead though.
* 1. It gets cleared via {@link Component#clearInterval} when
* {@link Component#dispose} gets called.
* 2. The function callback will be a {@link Component~GenericCallback}
*
* @param {Component~GenericCallback} fn
* The function to run every `x` seconds.
*
* @param {number} interval
* Execute the specified function every `x` milliseconds.
*
* @return {number}
* Returns an id that can be used to identify the interval. It can also be be used in
* {@link Component#clearInterval} to clear the interval.
*
* @listens Component#dispose
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
*/
setInterval(fn, interval) {
fn = bind_(this, fn);
this.clearTimersOnDispose_();
const intervalId = window.setInterval(fn, interval);
this.setIntervalIds_.add(intervalId);
return intervalId;
}
/**
* Clears an interval that gets created via `window.setInterval` or
* {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
* use this function instead of `window.clearInterval`. If you don't your dispose
* listener will not get cleaned up until {@link Component#dispose}!
*
* @param {number} intervalId
* The id of the interval to clear. The return value of
* {@link Component#setInterval} or `window.setInterval`.
*
* @return {number}
* Returns the interval id that was cleared.
*
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
*/
clearInterval(intervalId) {
if (this.setIntervalIds_.has(intervalId)) {
this.setIntervalIds_.delete(intervalId);
window.clearInterval(intervalId);
}
return intervalId;
}
/**
* Queues up a callback to be passed to requestAnimationFrame (rAF), but
* with a few extra bonuses:
*
* - Supports browsers that do not support rAF by falling back to
* {@link Component#setTimeout}.
*
* - The callback is turned into a {@link Component~GenericCallback} (i.e.
* bound to the component).
*
* - Automatic cancellation of the rAF callback is handled if the component
* is disposed before it is called.
*
* @param {Component~GenericCallback} fn
* A function that will be bound to this component and executed just
* before the browser's next repaint.
*
* @return {number}
* Returns an rAF ID that gets used to identify the timeout. It can
* also be used in {@link Component#cancelAnimationFrame} to cancel
* the animation frame callback.
*
* @listens Component#dispose
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
*/
requestAnimationFrame(fn) {
this.clearTimersOnDispose_();
// declare as variables so they are properly available in rAF function
// eslint-disable-next-line
var id;
fn = bind_(this, fn);
id = window.requestAnimationFrame(() => {
if (this.rafIds_.has(id)) {
this.rafIds_.delete(id);
}
fn();
});
this.rafIds_.add(id);
return id;
}
/**
* Request an animation frame, but only one named animation
* frame will be queued. Another will never be added until
* the previous one finishes.
*
* @param {string} name
* The name to give this requestAnimationFrame
*
* @param {Component~GenericCallback} fn
* A function that will be bound to this component and executed just
* before the browser's next repaint.
*/
requestNamedAnimationFrame(name, fn) {
if (this.namedRafs_.has(name)) {
this.cancelNamedAnimationFrame(name);
}
this.clearTimersOnDispose_();
fn = bind_(this, fn);
const id = this.requestAnimationFrame(() => {
fn();
if (this.namedRafs_.has(name)) {
this.namedRafs_.delete(name);
}
});
this.namedRafs_.set(name, id);
return name;
}
/**
* Cancels a current named animation frame if it exists.
*
* @param {string} name
* The name of the requestAnimationFrame to cancel.
*/
cancelNamedAnimationFrame(name) {
if (!this.namedRafs_.has(name)) {
return;
}
this.cancelAnimationFrame(this.namedRafs_.get(name));
this.namedRafs_.delete(name);
}
/**
* Cancels a queued callback passed to {@link Component#requestAnimationFrame}
* (rAF).
*
* If you queue an rAF callback via {@link Component#requestAnimationFrame},
* use this function instead of `window.cancelAnimationFrame`. If you don't,
* your dispose listener will not get cleaned up until {@link Component#dispose}!
*
* @param {number} id
* The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
*
* @return {number}
* Returns the rAF ID that was cleared.
*
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
*/
cancelAnimationFrame(id) {
if (this.rafIds_.has(id)) {
this.rafIds_.delete(id);
window.cancelAnimationFrame(id);
}
return id;
}
/**
* A function to setup `requestAnimationFrame`, `setTimeout`,
* and `setInterval`, clearing on dispose.
*
* > Previously each timer added and removed dispose listeners on it's own.
* For better performance it was decided to batch them all, and use `Set`s
* to track outstanding timer ids.
*
* @private
*/
clearTimersOnDispose_() {
if (this.clearingTimersOnDispose_) {
return;
}
this.clearingTimersOnDispose_ = true;
this.one('dispose', () => {
[['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
// for a `Set` key will actually be the value again
// so forEach((val, val) =>` but for maps we want to use
// the key.
this[idName].forEach((val, key) => this[cancelName](key));
});
this.clearingTimersOnDispose_ = false;
});
}
/**
* Decide whether an element is actually disabled or not.
*
* @function isActuallyDisabled
* @param element {Node}
* @return {boolean}
*
* @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
*/
getIsDisabled() {
return Boolean(this.el_.disabled);
}
/**
* Decide whether the element is expressly inert or not.
*
* @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
* @function isExpresslyInert
* @param element {Node}
* @return {boolean}
*/
getIsExpresslyInert() {
return this.el_.inert && !this.el_.ownerDocument.documentElement.inert;
}
/**
* Determine whether or not this component can be considered as focusable component.
*
* @param {HTMLElement} el - The HTML element representing the component.
* @return {boolean}
* If the component can be focused, will be `true`. Otherwise, `false`.
*/
getIsFocusable(el) {
const element = el || this.el_;
return element.tabIndex >= 0 && !(this.getIsDisabled() || this.getIsExpresslyInert());
}
/**
* Determine whether or not this component is currently visible/enabled/etc...
*
* @param {HTMLElement} el - The HTML element representing the component.
* @return {boolean}
* If the component can is currently visible & enabled, will be `true`. Otherwise, `false`.
*/
getIsAvailableToBeFocused(el) {
/**
* Decide the style property of this element is specified whether it's visible or not.
*
* @function isVisibleStyleProperty
* @param element {CSSStyleDeclaration}
* @return {boolean}
*/
function isVisibleStyleProperty(element) {
const elementStyle = window.getComputedStyle(element, null);
const thisVisibility = elementStyle.getPropertyValue('visibility');
const thisDisplay = elementStyle.getPropertyValue('display');
const invisibleStyle = ['hidden', 'collapse'];
return thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility);
}
/**
* Decide whether the element is being rendered or not.
* 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
* 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
* 3. If width and height of an element are explicitly set to 0, it is not being rendered.
* 4. If a parent element is hidden, an element itself is not being rendered.
* (CSS visibility property and display property are inherited.)
*
* @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
* @function isBeingRendered
* @param element {Node}
* @return {boolean}
*/
function isBeingRendered(element) {
if (!isVisibleStyleProperty(element.parentElement)) {
return false;
}
if (!isVisibleStyleProperty(element) || element.style.opacity === '0' || window.getComputedStyle(element).height === '0px' || window.getComputedStyle(element).width === '0px') {
return false;
}
return true;
}
/**
* Determine if the element is visible for the user or not.
* 1. If an element sum of its offsetWidth, offsetHeight, height and width is less than 1 is not visible.
* 2. If elementCenter.x is less than is not visible.
* 3. If elementCenter.x is more than the document's width is not visible.
* 4. If elementCenter.y is less than 0 is not visible.
* 5. If elementCenter.y is the document's height is not visible.
*
* @function isVisible
* @param element {Node}
* @return {boolean}
*/
function isVisible(element) {
if (element.offsetWidth + element.offsetHeight + element.getBoundingClientRect().height + element.getBoundingClientRect().width === 0) {
return false;
}
// Define elementCenter object with props of x and y
// x: Left position relative to the viewport plus element's width (no margin) divided between 2.
// y: Top position relative to the viewport plus element's height (no margin) divided between 2.
const elementCenter = {
x: element.getBoundingClientRect().left + element.offsetWidth / 2,
y: element.getBoundingClientRect().top + element.offsetHeight / 2
};
if (elementCenter.x < 0) {
return false;
}
if (elementCenter.x > (document.documentElement.clientWidth || window.innerWidth)) {
return false;
}
if (elementCenter.y < 0) {
return false;
}
if (elementCenter.y > (document.documentElement.clientHeight || window.innerHeight)) {
return false;
}
let pointContainer = document.elementFromPoint(elementCenter.x, elementCenter.y);
while (pointContainer) {
if (pointContainer === element) {
return true;
}
if (pointContainer.parentNode) {
pointContainer = pointContainer.parentNode;
} else {
return false;
}
}
}
// If no DOM element was passed as argument use this component's element.
if (!el) {
el = this.el();
}
// If element is visible, is being rendered & either does not have a parent element or its tabIndex is not negative.
if (isVisible(el) && isBeingRendered(el) && (!el.parentElement || el.tabIndex >= 0)) {
return true;
}
return false;
}
/**
* Register a `Component` with `videojs` given the name and the component.
*
* > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
* should be registered using {@link Tech.registerTech} or
* {@link videojs:videojs.registerTech}.
*
* > NOTE: This function can also be seen on videojs as
* {@link videojs:videojs.registerComponent}.
*
* @param {string} name
* The name of the `Component` to register.
*
* @param {Component} ComponentToRegister
* The `Component` class to register.
*
* @return {Component}
* The `Component` that was registered.
*/
static registerComponent(name, ComponentToRegister) {
if (typeof name !== 'string' || !name) {
throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
}
const Tech = Component.getComponent('Tech');
// We need to make sure this check is only done if Tech has been registered.
const isTech = Tech && Tech.isTech(ComponentToRegister);
const isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
if (isTech || !isComp) {
let reason;
if (isTech) {
reason = 'techs must be registered using Tech.registerTech()';
} else {
reason = 'must be a Component subclass';
}
throw new Error(`Illegal component, "${name}"; ${reason}.`);
}
name = toTitleCase(name);
if (!Component.components_) {
Component.components_ = {};
}
const Player = Component.getComponent('Player');
if (name === 'Player' && Player && Player.players) {
const players = Player.players;
const playerNames = Object.keys(players);
// If we have players that were disposed, then their name will still be
// in Players.players. So, we must loop through and verify that the value
// for each item is not null. This allows registration of the Player component
// after all players have been disposed or before any were created.
if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
throw new Error('Can not register Player component after player has been created.');
}
}
Component.components_[name] = ComponentToRegister;
Component.components_[toLowerCase(name)] = ComponentToRegister;
return ComponentToRegister;
}
/**
* Get a `Component` based on the name it was registered with.
*
* @param {string} name
* The Name of the component to get.
*
* @return {typeof Component}
* The `Component` that got registered under the given name.
*/
static getComponent(name) {
if (!name || !Component.components_) {
return;
}
return Component.components_[name];
}
}
Component.registerComponent('Component', Component);
/**
* @file time.js
* @module time
*/
/**
* Returns the time for the specified index at the start or end
* of a TimeRange object.
*
* @typedef {Function} TimeRangeIndex
*
* @param {number} [index=0]
* The range number to return the time for.
*
* @return {number}
* The time offset at the specified index.
*
* @deprecated The index argument must be provided.
* In the future, leaving it out will throw an error.
*/
/**
* An object that contains ranges of time, which mimics {@link TimeRanges}.
*
* @typedef {Object} TimeRange
*
* @property {number} length
* The number of time ranges represented by this object.
*
* @property {module:time~TimeRangeIndex} start
* Returns the time offset at which a specified time range begins.
*
* @property {module:time~TimeRangeIndex} end
* Returns the time offset at which a specified time range ends.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
*/
/**
* Check if any of the time ranges are over the maximum index.
*
* @private
* @param {string} fnName
* The function name to use for logging
*
* @param {number} index
* The index to check
*
* @param {number} maxIndex
* The maximum possible index
*
* @throws {Error} if the timeRanges provided are over the maxIndex
*/
function rangeCheck(fnName, index, maxIndex) {
if (typeof index !== 'number' || index < 0 || index > maxIndex) {
throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
}
}
/**
* Get the time for the specified index at the start or end
* of a TimeRange object.
*
* @private
* @param {string} fnName
* The function name to use for logging
*
* @param {string} valueIndex
* The property that should be used to get the time. should be
* 'start' or 'end'
*
* @param {Array} ranges
* An array of time ranges
*
* @param {Array} [rangeIndex=0]
* The index to start the search at
*
* @return {number}
* The time that offset at the specified index.
*
* @deprecated rangeIndex must be set to a value, in the future this will throw an error.
* @throws {Error} if rangeIndex is more than the length of ranges
*/
function getRange(fnName, valueIndex, ranges, rangeIndex) {
rangeCheck(fnName, rangeIndex, ranges.length - 1);
return ranges[rangeIndex][valueIndex];
}
/**
* Create a time range object given ranges of time.
*
* @private
* @param {Array} [ranges]
* An array of time ranges.
*
* @return {TimeRange}
*/
function createTimeRangesObj(ranges) {
let timeRangesObj;
if (ranges === undefined || ranges.length === 0) {
timeRangesObj = {
length: 0,
start() {
throw new Error('This TimeRanges object is empty');
},
end() {
throw new Error('This TimeRanges object is empty');
}
};
} else {
timeRangesObj = {
length: ranges.length,
start: getRange.bind(null, 'start', 0, ranges),
end: getRange.bind(null, 'end', 1, ranges)
};
}
if (window.Symbol && window.Symbol.iterator) {
timeRangesObj[window.Symbol.iterator] = () => (ranges || []).values();
}
return timeRangesObj;
}
/**
* Create a `TimeRange` object which mimics an
* {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
*
* @param {number|Array[]} start
* The start of a single range (a number) or an array of ranges (an
* array of arrays of two numbers each).
*
* @param {number} end
* The end of a single range. Cannot be used with the array form of
* the `start` argument.
*
* @return {TimeRange}
*/
function createTimeRanges(start, end) {
if (Array.isArray(start)) {
return createTimeRangesObj(start);
} else if (start === undefined || end === undefined) {
return createTimeRangesObj();
}
return createTimeRangesObj([[start, end]]);
}
/**
* Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
* seconds) will force a number of leading zeros to cover the length of the
* guide.
*
* @private
* @param {number} seconds
* Number of seconds to be turned into a string
*
* @param {number} guide
* Number (in seconds) to model the string after
*
* @return {string}
* Time formatted as H:MM:SS or M:SS
*/
const defaultImplementation = function (seconds, guide) {
seconds = seconds < 0 ? 0 : seconds;
let s = Math.floor(seconds % 60);
let m = Math.floor(seconds / 60 % 60);
let h = Math.floor(seconds / 3600);
const gm = Math.floor(guide / 60 % 60);
const gh = Math.floor(guide / 3600);
// handle invalid times
if (isNaN(seconds) || seconds === Infinity) {
// '-' is false for all relational operators (e.g. <, >=) so this setting
// will add the minimum number of fields specified by the guide
h = m = s = '-';
}
// Check if we need to show hours
h = h > 0 || gh > 0 ? h + ':' : '';
// If hours are showing, we may need to add a leading zero.
// Always show at least one digit of minutes.
m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
// Check if leading zero is need for seconds
s = s < 10 ? '0' + s : s;
return h + m + s;
};
// Internal pointer to the current implementation.
let implementation = defaultImplementation;
/**
* Replaces the default formatTime implementation with a custom implementation.
*
* @param {Function} customImplementation
* A function which will be used in place of the default formatTime
* implementation. Will receive the current time in seconds and the
* guide (in seconds) as arguments.
*/
function setFormatTime(customImplementation) {
implementation = customImplementation;
}
/**
* Resets formatTime to the default implementation.
*/
function resetFormatTime() {
implementation = defaultImplementation;
}
/**
* Delegates to either the default time formatting function or a custom
* function supplied via `setFormatTime`.
*
* Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
* guide (in seconds) will force a number of leading zeros to cover the
* length of the guide.
*
* @example formatTime(125, 600) === "02:05"
* @param {number} seconds
* Number of seconds to be turned into a string
*
* @param {number} guide
* Number (in seconds) to model the string after
*
* @return {string}
* Time formatted as H:MM:SS or M:SS
*/
function formatTime(seconds, guide = seconds) {
return implementation(seconds, guide);
}
var Time = /*#__PURE__*/Object.freeze({
__proto__: null,
createTimeRanges: createTimeRanges,
createTimeRange: createTimeRanges,
setFormatTime: setFormatTime,
resetFormatTime: resetFormatTime,
formatTime: formatTime
});
/**
* @file buffer.js
* @module buffer
*/
/** @import { TimeRange } from './time' */
/**
* Compute the percentage of the media that has been buffered.
*
* @param {TimeRange} buffered
* The current `TimeRanges` object representing buffered time ranges
*
* @param {number} duration
* Total duration of the media
*
* @return {number}
* Percent buffered of the total duration in decimal form.
*/
function bufferedPercent(buffered, duration) {
let bufferedDuration = 0;
let start;
let end;
if (!duration) {
return 0;
}
if (!buffered || !buffered.length) {
buffered = createTimeRanges(0, 0);
}
for (let i = 0; i < buffered.length; i++) {
start = buffered.start(i);
end = buffered.end(i);
// buffered end can be bigger than duration by a very small fraction
if (end > duration) {
end = duration;
}
bufferedDuration += end - start;
}
return bufferedDuration / duration;
}
/**
* @file media-error.js
*/
/**
* A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
*
* @param {number|string|Object|MediaError} value
* This can be of multiple types:
* - number: should be a standard error code
* - string: an error message (the code will be 0)
* - Object: arbitrary properties
* - `MediaError` (native): used to populate a video.js `MediaError` object
* - `MediaError` (video.js): will return itself if it's already a
* video.js `MediaError` object.
*
* @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
* @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
*
* @class MediaError
*/
function MediaError(value) {
// Allow redundant calls to this constructor to avoid having `instanceof`
// checks peppered around the code.
if (value instanceof MediaError) {
return value;
}
if (typeof value === 'number') {
this.code = value;
} else if (typeof value === 'string') {
// default code is zero, so this is a custom error
this.message = value;
} else if (isObject(value)) {
// We assign the `code` property manually because native `MediaError` objects
// do not expose it as an own/enumerable property of the object.
if (typeof value.code === 'number') {
this.code = value.code;
}
Object.assign(this, value);
}
if (!this.message) {
this.message = MediaError.defaultMessages[this.code] || '';
}
}
/**
* The error code that refers two one of the defined `MediaError` types
*
* @type {Number}
*/
MediaError.prototype.code = 0;
/**
* An optional message that to show with the error. Message is not part of the HTML5
* video spec but allows for more informative custom errors.
*
* @type {String}
*/
MediaError.prototype.message = '';
/**
* An optional status code that can be set by plugins to allow even more detail about
* the error. For example a plugin might provide a specific HTTP status code and an
* error message for that code. Then when the plugin gets that error this class will
* know how to display an error message for it. This allows a custom message to show
* up on the `Player` error overlay.
*
* @type {Array}
*/
MediaError.prototype.status = null;
/**
* An object containing an error type, as well as other information regarding the error.
*
* @typedef {{errorType: string, [key: string]: any}} ErrorMetadata
*/
/**
* An optional object to give more detail about the error. This can be used to give
* a higher level of specificity to an error versus the more generic MediaError codes.
* `metadata` expects an `errorType` string that should align with the values from videojs.Error.
*
* @type {ErrorMetadata}
*/
MediaError.prototype.metadata = null;
/**
* Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
* specification listed under {@link MediaError} for more information.
*
* @enum {array}
* @readonly
* @property {string} 0 - MEDIA_ERR_CUSTOM
* @property {string} 1 - MEDIA_ERR_ABORTED
* @property {string} 2 - MEDIA_ERR_NETWORK
* @property {string} 3 - MEDIA_ERR_DECODE
* @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
* @property {string} 5 - MEDIA_ERR_ENCRYPTED
*/
MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
/**
* The default `MediaError` messages based on the {@link MediaError.errorTypes}.
*
* @type {Array}
* @constant
*/
MediaError.defaultMessages = {
1: 'You aborted the media playback',
2: 'A network error caused the media download to fail part-way.',
3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
5: 'The media is encrypted and we do not have the keys to decrypt it.'
};
/**
* W3C error code for any custom error.
*
* @member MediaError#MEDIA_ERR_CUSTOM
* @constant {number}
* @default 0
*/
MediaError.MEDIA_ERR_CUSTOM = 0;
/**
* W3C error code for any custom error.
*
* @member MediaError.MEDIA_ERR_CUSTOM
* @constant {number}
* @default 0
*/
MediaError.prototype.MEDIA_ERR_CUSTOM = 0;
/**
* W3C error code for media error aborted.
*
* @member MediaError#MEDIA_ERR_ABORTED
* @constant {number}
* @default 1
*/
MediaError.MEDIA_ERR_ABORTED = 1;
/**
* W3C error code for media error aborted.
*
* @member MediaError.MEDIA_ERR_ABORTED
* @constant {number}
* @default 1
*/
MediaError.prototype.MEDIA_ERR_ABORTED = 1;
/**
* W3C error code for any network error.
*
* @member MediaError#MEDIA_ERR_NETWORK
* @constant {number}
* @default 2
*/
MediaError.MEDIA_ERR_NETWORK = 2;
/**
* W3C error code for any network error.
*
* @member MediaError.MEDIA_ERR_NETWORK
* @constant {number}
* @default 2
*/
MediaError.prototype.MEDIA_ERR_NETWORK = 2;
/**
* W3C error code for any decoding error.
*
* @member MediaError#MEDIA_ERR_DECODE
* @constant {number}
* @default 3
*/
MediaError.MEDIA_ERR_DECODE = 3;
/**
* W3C error code for any decoding error.
*
* @member MediaError.MEDIA_ERR_DECODE
* @constant {number}
* @default 3
*/
MediaError.prototype.MEDIA_ERR_DECODE = 3;
/**
* W3C error code for any time that a source is not supported.
*
* @member MediaError#MEDIA_ERR_SRC_NOT_SUPPORTED
* @constant {number}
* @default 4
*/
MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
/**
* W3C error code for any time that a source is not supported.
*
* @member MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
* @constant {number}
* @default 4
*/
MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
/**
* W3C error code for any time that a source is encrypted.
*
* @member MediaError#MEDIA_ERR_ENCRYPTED
* @constant {number}
* @default 5
*/
MediaError.MEDIA_ERR_ENCRYPTED = 5;
/**
* W3C error code for any time that a source is encrypted.
*
* @member MediaError.MEDIA_ERR_ENCRYPTED
* @constant {number}
* @default 5
*/
MediaError.prototype.MEDIA_ERR_ENCRYPTED = 5;
/**
* Returns whether an object is `Promise`-like (i.e. has a `then` method).
*
* @param {Object} value
* An object that may or may not be `Promise`-like.
*
* @return {boolean}
* Whether or not the object is `Promise`-like.
*/
function isPromise(value) {
return value !== undefined && value !== null && typeof value.then === 'function';
}
/**
* Silence a Promise-like object.
*
* This is useful for avoiding non-harmful, but potentially confusing "uncaught
* play promise" rejection error messages.
*
* @param {Object} value
* An object that may or may not be `Promise`-like.
*/
function silencePromise(value) {
if (isPromise(value)) {
value.then(null, e => {});
}
}
/**
* @file text-track-list-converter.js Utilities for capturing text track state and
* re-creating tracks based on a capture.
*
* @module text-track-list-converter
*/
/** @import Tech from '../tech/tech' */
/**
* Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
* represents the {@link TextTrack}'s state.
*
* @param {TextTrack} track
* The text track to query.
*
* @return {Object}
* A serializable javascript representation of the TextTrack.
* @private
*/
const trackToJson_ = function (track) {
const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
if (track[prop]) {
acc[prop] = track[prop];
}
return acc;
}, {
cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
return {
startTime: cue.startTime,
endTime: cue.endTime,
text: cue.text,
id: cue.id
};
})
});
return ret;
};
/**
* Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
* state of all {@link TextTrack}s currently configured. The return array is compatible with
* {@link text-track-list-converter:jsonToTextTracks}.
*
* @param {Tech} tech
* The tech object to query
*
* @return {Array}
* A serializable javascript representation of the {@link Tech}s
* {@link TextTrackList}.
*/
const textTracksToJson = function (tech) {
const trackEls = tech.$$('track');
const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
const json = trackToJson_(trackEl.track);
if (trackEl.src) {
json.src = trackEl.src;
}
return json;
});
return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
return trackObjs.indexOf(track) === -1;
}).map(trackToJson_));
};
/**
* Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
* object {@link TextTrack} representations.
*
* @param {Array} json
* An array of `TextTrack` representation objects, like those that would be
* produced by `textTracksToJson`.
*
* @param {Tech} tech
* The `Tech` to create the `TextTrack`s on.
*/
const jsonToTextTracks = function (json, tech) {
json.forEach(function (track) {
const addedTrack = tech.addRemoteTextTrack(track).track;
if (!track.src && track.cues) {
track.cues.forEach(cue => addedTrack.addCue(cue));
}
});
return tech.textTracks();
};
var textTrackConverter = {
textTracksToJson,
jsonToTextTracks,
trackToJson_
};
/**
* @file modal-dialog.js
*/
/** @import Player from './player' */
/** @import { ContentDescriptor } from './utils/dom' */
const MODAL_CLASS_NAME = 'vjs-modal-dialog';
/**
* The `ModalDialog` displays over the video and its controls, which blocks
* interaction with the player until it is closed.
*
* Modal dialogs include a "Close" button and will close when that button
* is activated - or when ESC is pressed anywhere.
*
* @extends Component
*/
class ModalDialog extends Component {
/**
* Creates an instance of this class.
*
* @param {Player} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param {ContentDescriptor} [options.content=undefined]
* Provide customized content for this modal.
*
* @param {string} [options.description]
* A text description for the modal, primarily for accessibility.
*
* @param {boolean} [options.fillAlways=false]
* Normally, modals are automatically filled only the first time
* they open. This tells the modal to refresh its content
* every time it opens.
*
* @param {string} [options.label]
* A text label for the modal, primarily for accessibility.
*
* @param {boolean} [options.pauseOnOpen=true]
* If `true`, playback will will be paused if playing when
* the modal opens, and resumed when it closes.
*
* @param {boolean} [options.temporary=true]
* If `true`, the modal can only be opened once; it will be
* disposed as soon as it's closed.
*
* @param {boolean} [options.uncloseable=false]
* If `true`, the user will not be able to close the modal
* through the UI in the normal ways. Programmatic closing is
* still possible.
*/
constructor(player, options) {
super(player, options);
this.handleKeyDown_ = e => this.handleKeyDown(e);
this.close_ = e => this.close(e);
this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
this.closeable(!this.options_.uncloseable);
this.content(this.options_.content);
// Make sure the contentEl is defined AFTER any children are initialized
// because we only want the contents of the modal in the contentEl
// (not the UI elements like the close button).
this.contentEl_ = createEl('div', {
className: `${MODAL_CLASS_NAME}-content`
}, {
role: 'document'
});
this.descEl_ = createEl('p', {
className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
id: this.el().getAttribute('aria-describedby')
});
textContent(this.descEl_, this.description());
this.el_.appendChild(this.descEl_);
this.el_.appendChild(this.contentEl_);
}
/**
* Create the `ModalDialog`'s DOM element
*
* @return {Element}
* The DOM element that gets created.
*/
createEl() {
return super.createEl('div', {
className: this.buildCSSClass(),
tabIndex: -1
}, {
'aria-describedby': `${this.id()}_description`,
'aria-hidden': 'true',
'aria-label': this.label(),
'role': 'dialog',
'aria-live': 'polite'
});
}
dispose() {
this.contentEl_ = null;
this.descEl_ = null;
this.previouslyActiveEl_ = null;
super.dispose();
}
/**
* Builds the default DOM `className`.
*
* @return {string}
* The DOM `className` for this object.
*/
buildCSSClass() {
return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
}
/**
* Returns the label string for this modal. Primarily used for accessibility.
*
* @return {string}
* the localized or raw label of this modal.
*/
label() {
return this.localize(this.options_.label || 'Modal Window');
}
/**
* Returns the description string for this modal. Primarily used for
* accessibility.
*
* @return {string}
* The localized or raw description of this modal.
*/
description() {
let desc = this.options_.description || this.localize('This is a modal window.');
// Append a universal closeability message if the modal is closeable.
if (this.closeable()) {
desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
}
return desc;
}
/**
* Opens the modal.
*
* @fires ModalDialog#beforemodalopen
* @fires ModalDialog#modalopen
*/
open() {
if (this.opened_) {
if (this.options_.fillAlways) {
this.fill();
}
return;
}
const player = this.player();
/**
* Fired just before a `ModalDialog` is opened.
*
* @event ModalDialog#beforemodalopen
* @type {Event}
*/
this.trigger('beforemodalopen');
this.opened_ = true;
// Fill content if the modal has never opened before and
// never been filled.
if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
this.fill();
}
// If the player was playing, pause it and take note of its previously
// playing state.
this.wasPlaying_ = !player.paused();
if (this.options_.pauseOnOpen && this.wasPlaying_) {
player.pause();
}
this.on('keydown', this.handleKeyDown_);
// Hide controls and note if they were enabled.
this.hadControls_ = player.controls();
player.controls(false);
this.show();
this.conditionalFocus_();
this.el().setAttribute('aria-hidden', 'false');
/**
* Fired just after a `ModalDialog` is opened.
*
* @event ModalDialog#modalopen
* @type {Event}
*/
this.trigger('modalopen');
this.hasBeenOpened_ = true;
}
/**
* If the `ModalDialog` is currently open or closed.
*
* @param {boolean} [value]
* If given, it will open (`true`) or close (`false`) the modal.
*
* @return {boolean}
* the current open state of the modaldialog
*/
opened(value) {
if (typeof value === 'boolean') {
this[value ? 'open' : 'close']();
}
return this.opened_;
}
/**
* Closes the modal, does nothing if the `ModalDialog` is
* not open.
*
* @fires ModalDialog#beforemodalclose
* @fires ModalDialog#modalclose
*/
close() {
if (!this.opened_) {
return;
}
const player = this.player();
/**
* Fired just before a `ModalDialog` is closed.
*
* @event ModalDialog#beforemodalclose
* @type {Event}
*/
this.trigger('beforemodalclose');
this.opened_ = false;
if (this.wasPlaying_ && this.options_.pauseOnOpen) {
player.play();
}
this.off('keydown', this.handleKeyDown_);
if (this.hadControls_) {
player.controls(true);
}
this.hide();
this.el().setAttribute('aria-hidden', 'true');
/**
* Fired just after a `ModalDialog` is closed.
*
* @event ModalDialog#modalclose
* @type {Event}
*
* @property {boolean} [bubbles=true]
*/
this.trigger({
type: 'modalclose',
bubbles: true
});
this.conditionalBlur_();
if (this.options_.temporary) {
this.dispose();
}
}
/**
* Check to see if the `ModalDialog` is closeable via the UI.
*
* @param {boolean} [value]
* If given as a boolean, it will set the `closeable` option.
*
* @return {boolean}
* Returns the final value of the closable option.
*/
closeable(value) {
if (typeof value === 'boolean') {
const closeable = this.closeable_ = !!value;
let close = this.getChild('closeButton');
// If this is being made closeable and has no close button, add one.
if (closeable && !close) {
// The close button should be a child of the modal - not its
// content element, so temporarily change the content element.
const temp = this.contentEl_;
this.contentEl_ = this.el_;
close = this.addChild('closeButton', {
controlText: 'Close Modal Dialog'
});
this.contentEl_ = temp;
this.on(close, 'close', this.close_);
}
// If this is being made uncloseable and has a close button, remove it.
if (!closeable && close) {
this.off(close, 'close', this.close_);
this.removeChild(close);
close.dispose();
}
}
return this.closeable_;
}
/**
* Fill the modal's content element with the modal's "content" option.
* The content element will be emptied before this change takes place.
*/
fill() {
this.fillWith(this.content());
}
/**
* Fill the modal's content element with arbitrary content.
* The content element will be emptied before this change takes place.
*
* @fires ModalDialog#beforemodalfill
* @fires ModalDialog#modalfill
*
* @param {ContentDescriptor} [content]
* The same rules apply to this as apply to the `content` option.
*/
fillWith(content) {
const contentEl = this.contentEl();
const parentEl = contentEl.parentNode;
const nextSiblingEl = contentEl.nextSibling;
/**
* Fired just before a `ModalDialog` is filled with content.
*
* @event ModalDialog#beforemodalfill
* @type {Event}
*/
this.trigger('beforemodalfill');
this.hasBeenFilled_ = true;
// Detach the content element from the DOM before performing
// manipulation to avoid modifying the live DOM multiple times.
parentEl.removeChild(contentEl);
this.empty();
insertContent(contentEl, content);
/**
* Fired just after a `ModalDialog` is filled with content.
*
* @event ModalDialog#modalfill
* @type {Event}
*/
this.trigger('modalfill');
// Re-inject the re-filled content element.
if (nextSiblingEl) {
parentEl.insertBefore(contentEl, nextSiblingEl);
} else {
parentEl.appendChild(contentEl);
}
// make sure that the close button is last in the dialog DOM
const closeButton = this.getChild('closeButton');
if (closeButton) {
parentEl.appendChild(closeButton.el_);
}
/**
* Fired after `ModalDialog` is re-filled with content & close button is appended.
*
* @event ModalDialog#aftermodalfill
* @type {Event}
*/
this.trigger('aftermodalfill');
}
/**
* Empties the content element. This happens anytime the modal is filled.
*
* @fires ModalDialog#beforemodalempty
* @fires ModalDialog#modalempty
*/
empty() {
/**
* Fired just before a `ModalDialog` is emptied.
*
* @event ModalDialog#beforemodalempty
* @type {Event}
*/
this.trigger('beforemodalempty');
emptyEl(this.contentEl());
/**
* Fired just after a `ModalDialog` is emptied.
*
* @event ModalDialog#modalempty
* @type {Event}
*/
this.trigger('modalempty');
}
/**
* Gets or sets the modal content, which gets normalized before being
* rendered into the DOM.
*
* This does not update the DOM or fill the modal, but it is called during
* that process.
*
* @param {ContentDescriptor} [value]
* If defined, sets the internal content value to be used on the
* next call(s) to `fill`. This value is normalized before being
* inserted. To "clear" the internal content value, pass `null`.
*
* @return {ContentDescriptor}
* The current content of the modal dialog
*/
content(value) {
if (typeof value !== 'undefined') {
this.content_ = value;
}
return this.content_;
}
/**
* conditionally focus the modal dialog if focus was previously on the player.
*
* @private
*/
conditionalFocus_() {
const activeEl = document.activeElement;
const playerEl = this.player_.el_;
this.previouslyActiveEl_ = null;
if (playerEl.contains(activeEl) || playerEl === activeEl) {
this.previouslyActiveEl_ = activeEl;
this.focus();
}
}
/**
* conditionally blur the element and refocus the last focused element
*
* @private
*/
conditionalBlur_() {
if (this.previouslyActiveEl_) {
this.previouslyActiveEl_.focus();
this.previouslyActiveEl_ = null;
}
}
/**
* Keydown handler. Attached when modal is focused.
*
* @listens keydown
*/
handleKeyDown(event) {
/**
* Fired a custom keyDown event that bubbles.
*
* @event ModalDialog#modalKeydown
* @type {Event}
*/
this.trigger({
type: 'modalKeydown',
originalEvent: event,
target: this,
bubbles: true
});
// Do not allow keydowns to reach out of the modal dialog.
event.stopPropagation();
if (event.key === 'Escape' && this.closeable()) {
event.preventDefault();
this.close();
return;
}
// exit early if it isn't a tab key
if (event.key !== 'Tab') {
return;
}
const focusableEls = this.focusableEls_();
const activeEl = this.el_.querySelector(':focus');
let focusIndex;
for (let i = 0; i < focusableEls.length; i++) {
if (activeEl === focusableEls[i]) {
focusIndex = i;
break;
}
}
if (document.activeElement === this.el_) {
focusIndex = 0;
}
if (event.shiftKey && focusIndex === 0) {
focusableEls[focusableEls.length - 1].focus();
event.preventDefault();
} else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
focusableEls[0].focus();
event.preventDefault();
}
}
/**
* get all focusable elements
*
* @private
*/
focusableEls_() {
const allChildren = this.el_.querySelectorAll('*');
return Array.prototype.filter.call(allChildren, child => {
return (child instanceof window.HTMLAnchorElement || child instanceof window.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window.HTMLInputElement || child instanceof window.HTMLSelectElement || child instanceof window.HTMLTextAreaElement || child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window.HTMLIFrameElement || child instanceof window.HTMLObjectElement || child instanceof window.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
});
}
}
/**
* Default options for `ModalDialog` default options.
*
* @type {Object}
* @private
*/
ModalDialog.prototype.options_ = {
pauseOnOpen: true,
temporary: true
};
Component.registerComponent('ModalDialog', ModalDialog);
/**
* @file track-list.js
*/
/** @import Track from './track' */
/**
* Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
* {@link VideoTrackList}
*
* @extends EventTarget
*/
class TrackList extends EventTarget {
/**
* Create an instance of this class
*
* @param { Track[] } tracks
* A list of tracks to initialize the list with.
*
* @abstract
*/
constructor(tracks = []) {
super();
this.tracks_ = [];
/**
* @memberof TrackList
* @member {number} length
* The current number of `Track`s in the this Trackist.
* @instance
*/
Object.defineProperty(this, 'length', {
get() {
return this.tracks_.length;
}
});
for (let i = 0; i < tracks.length; i++) {
this.addTrack(tracks[i]);
}
}
/**
* Add a {@link Track} to the `TrackList`
*
* @param {Track} track
* The audio, video, or text track to add to the list.
*
* @fires TrackList#addtrack
*/
addTrack(track) {
const index = this.tracks_.length;
if (!('' + index in this)) {
Object.defineProperty(this, index, {
get() {
return this.tracks_[index];
}
});
}
// Do not add duplicate tracks
if (this.tracks_.indexOf(track) === -1) {
this.tracks_.push(track);
/**
* Triggered when a track is added to a track list.
*
* @event TrackList#addtrack
* @type {Event}
* @property {Track} track
* A reference to track that was added.
*/
this.trigger({
track,
type: 'addtrack',
target: this
});
}
/**
* Triggered when a track label is changed.
*
* @event TrackList#addtrack
* @type {Event}
* @property {Track} track
* A reference to track that was added.
*/
track.labelchange_ = () => {
this.trigger({
track,
type: 'labelchange',
target: this
});
};
if (isEvented(track)) {
track.addEventListener('labelchange', track.labelchange_);
}
}
/**
* Remove a {@link Track} from the `TrackList`
*
* @param {Track} rtrack
* The audio, video, or text track to remove from the list.
*
* @fires TrackList#removetrack
*/
removeTrack(rtrack) {
let track;
for (let i = 0, l = this.length; i < l; i++) {
if (this[i] === rtrack) {
track = this[i];
if (track.off) {
track.off();
}
this.tracks_.splice(i, 1);
break;
}
}
if (!track) {
return;
}
/**
* Triggered when a track is removed from track list.
*
* @event TrackList#removetrack
* @type {Event}
* @property {Track} track
* A reference to track that was removed.
*/
this.trigger({
track,
type: 'removetrack',
target: this
});
}
/**
* Get a Track from the TrackList by a tracks id
*
* @param {string} id - the id of the track to get
* @method getTrackById
* @return {Track}
* @private
*/
getTrackById(id) {
let result = null;
for (let i = 0, l = this.length; i < l; i++) {
const track = this[i];
if (track.id === id) {
result = track;
break;
}
}
return result;
}
}
/**
* Triggered when a different track is selected/enabled.
*
* @event TrackList#change
* @type {Event}
*/
/**
* Events that can be called with on + eventName. See {@link EventHandler}.
*
* @property {Object} TrackList#allowedEvents_
* @protected
*/
TrackList.prototype.allowedEvents_ = {
change: 'change',
addtrack: 'addtrack',
removetrack: 'removetrack',
labelchange: 'labelchange'
};
// emulate attribute EventHandler support to allow for feature detection
for (const event in TrackList.prototype.allowedEvents_) {
TrackList.prototype['on' + event] = null;
}
/**
* @file audio-track-list.js
*/
/** @import AudioTrack from './audio-track' */
/**
* Anywhere we call this function we diverge from the spec
* as we only support one enabled audiotrack at a time
*
* @param {AudioTrackList} list
* list to work on
*
* @param {AudioTrack} track
* The track to skip
*
* @private
*/
const disableOthers$1 = function (list, track) {
for (let i = 0; i < list.length; i++) {
if (!Object.keys(list[i]).length || track.id === list[i].id) {
continue;
}
// another audio track is enabled, disable it
list[i].enabled = false;
}
};
/**
* The current list of {@link AudioTrack} for a media file.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
* @extends TrackList
*/
class AudioTrackList extends TrackList {
/**
* Create an instance of this class.
*
* @param {AudioTrack[]} [tracks=[]]
* A list of `AudioTrack` to instantiate the list with.
*/
constructor(tracks = []) {
// make sure only 1 track is enabled
// sorted from last index to first index
for (let i = tracks.length - 1; i >= 0; i--) {
if (tracks[i].enabled) {
disableOthers$1(tracks, tracks[i]);
break;
}
}
super(tracks);
this.changing_ = false;
}
/**
* Add an {@link AudioTrack} to the `AudioTrackList`.
*
* @param {AudioTrack} track
* The AudioTrack to add to the list
*
* @fires TrackList#addtrack
*/
addTrack(track) {
if (track.enabled) {
disableOthers$1(this, track);
}
super.addTrack(track);
// native tracks don't have this
if (!track.addEventListener) {
return;
}
track.enabledChange_ = () => {
// when we are disabling other tracks (since we don't support
// more than one track at a time) we will set changing_
// to true so that we don't trigger additional change events
if (this.changing_) {
return;
}
this.changing_ = true;
disableOthers$1(this, track);
this.changing_ = false;
this.trigger('change');
};
/**
* @listens AudioTrack#enabledchange
* @fires TrackList#change
*/
track.addEventListener('enabledchange', track.enabledChange_);
}
removeTrack(rtrack) {
super.removeTrack(rtrack);
if (rtrack.removeEventListener && rtrack.enabledChange_) {
rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
rtrack.enabledChange_ = null;
}
}
}
/**
* @file video-track-list.js
*/
/** @import VideoTrack from './video-track' */
/**
* Un-select all other {@link VideoTrack}s that are selected.
*
* @param {VideoTrackList} list
* list to work on
*
* @param {VideoTrack} track
* The track to skip
*
* @private
*/
const disableOthers = function (list, track) {
for (let i = 0; i < list.length; i++) {
if (!Object.keys(list[i]).length || track.id === list[i].id) {
continue;
}
// another video track is enabled, disable it
list[i].selected = false;
}
};
/**
* The current list of {@link VideoTrack} for a video.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
* @extends TrackList
*/
class VideoTrackList extends TrackList {
/**
* Create an instance of this class.
*
* @param {VideoTrack[]} [tracks=[]]
* A list of `VideoTrack` to instantiate the list with.
*/
constructor(tracks = []) {
// make sure only 1 track is enabled
// sorted from last index to first index
for (let i = tracks.length - 1; i >= 0; i--) {
if (tracks[i].selected) {
disableOthers(tracks, tracks[i]);
break;
}
}
super(tracks);
this.changing_ = false;
/**
* @member {number} VideoTrackList#selectedIndex
* The current index of the selected {@link VideoTrack`}.
*/
Object.defineProperty(this, 'selectedIndex', {
get() {
for (let i = 0; i < this.length; i++) {
if (this[i].selected) {
return i;
}
}
return -1;
},
set() {}
});
}
/**
* Add a {@link VideoTrack} to the `VideoTrackList`.
*
* @param {VideoTrack} track
* The VideoTrack to add to the list
*
* @fires TrackList#addtrack
*/
addTrack(track) {
if (track.selected) {
disableOthers(this, track);
}
super.addTrack(track);
// native tracks don't have this
if (!track.addEventListener) {
return;
}
track.selectedChange_ = () => {
if (this.changing_) {
return;
}
this.changing_ = true;
disableOthers(this, track);
this.changing_ = false;
this.trigger('change');
};
/**
* @listens VideoTrack#selectedchange
* @fires TrackList#change
*/
track.addEventListener('selectedchange', track.selectedChange_);
}
removeTrack(rtrack) {
super.removeTrack(rtrack);
if (rtrack.removeEventListener && rtrack.selectedChange_) {
rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
rtrack.selectedChange_ = null;
}
}
}
/**
* @file text-track-list.js
*/
/** @import TextTrack from './text-track' */
/**
* The current list of {@link TextTrack} for a media file.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
* @extends TrackList
*/
class TextTrackList extends TrackList {
/**
* Add a {@link TextTrack} to the `TextTrackList`
*
* @param {TextTrack} track
* The text track to add to the list.
*
* @fires TrackList#addtrack
*/
addTrack(track) {
super.addTrack(track);
if (!this.queueChange_) {
this.queueChange_ = () => this.queueTrigger('change');
}
if (!this.triggerSelectedlanguagechange) {
this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
}
/**
* @listens TextTrack#modechange
* @fires TrackList#change
*/
track.addEventListener('modechange', this.queueChange_);
const nonLanguageTextTrackKind = ['metadata', 'chapters'];
if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
}
}
removeTrack(rtrack) {
super.removeTrack(rtrack);
// manually remove the event handlers we added
if (rtrack.removeEventListener) {
if (this.queueChange_) {
rtrack.removeEventListener('modechange', this.queueChange_);
}
if (this.selectedlanguagechange_) {
rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
}
}
}
}
/**
* @file html-track-element-list.js
*/
/**
* The current list of {@link HtmlTrackElement}s.
*/
class HtmlTrackElementList {
/**
* Create an instance of this class.
*
* @param {HtmlTrackElement[]} [tracks=[]]
* A list of `HtmlTrackElement` to instantiate the list with.
*/
constructor(trackElements = []) {
this.trackElements_ = [];
/**
* @memberof HtmlTrackElementList
* @member {number} length
* The current number of `Track`s in the this Trackist.
* @instance
*/
Object.defineProperty(this, 'length', {
get() {
return this.trackElements_.length;
}
});
for (let i = 0, length = trackElements.length; i < length; i++) {
this.addTrackElement_(trackElements[i]);
}
}
/**
* Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
*
* @param {HtmlTrackElement} trackElement
* The track element to add to the list.
*
* @private
*/
addTrackElement_(trackElement) {
const index = this.trackElements_.length;
if (!('' + index in this)) {
Object.defineProperty(this, index, {
get() {
return this.trackElements_[index];
}
});
}
// Do not add duplicate elements
if (this.trackElements_.indexOf(trackElement) === -1) {
this.trackElements_.push(trackElement);
}
}
/**
* Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
* {@link TextTrack}.
*
* @param {TextTrack} track
* The track associated with a track element.
*
* @return {HtmlTrackElement|undefined}
* The track element that was found or undefined.
*
* @private
*/
getTrackElementByTrack_(track) {
let trackElement_;
for (let i = 0, length = this.trackElements_.length; i < length; i++) {
if (track === this.trackElements_[i].track) {
trackElement_ = this.trackElements_[i];
break;
}
}
return trackElement_;
}
/**
* Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
*
* @param {HtmlTrackElement} trackElement
* The track element to remove from the list.
*
* @private
*/
removeTrackElement_(trackElement) {
for (let i = 0, length = this.trackElements_.length; i < length; i++) {
if (trackElement === this.trackElements_[i]) {
if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
this.trackElements_[i].track.off();
}
if (typeof this.trackElements_[i].off === 'function') {
this.trackElements_[i].off();
}
this.trackElements_.splice(i, 1);
break;
}
}
}
}
/**
* @file text-track-cue-list.js
*/
/**
* @typedef {Object} TextTrackCueList~TextTrackCue
*
* @property {string} id
* The unique id for this text track cue
*
* @property {number} startTime
* The start time for this text track cue
*
* @property {number} endTime
* The end time for this text track cue
*
* @property {boolean} pauseOnExit
* Pause when the end time is reached if true.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
*/
/**
* A List of TextTrackCues.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
*/
class TextTrackCueList {
/**
* Create an instance of this class..
*
* @param {Array} cues
* A list of cues to be initialized with
*/
constructor(cues) {
TextTrackCueList.prototype.setCues_.call(this, cues);
/**
* @memberof TextTrackCueList
* @member {number} length
* The current number of `TextTrackCue`s in the TextTrackCueList.
* @instance
*/
Object.defineProperty(this, 'length', {
get() {
return this.length_;
}
});
}
/**
* A setter for cues in this list. Creates getters
* an an index for the cues.
*
* @param {Array} cues
* An array of cues to set
*
* @private
*/
setCues_(cues) {
const oldLength = this.length || 0;
let i = 0;
const l = cues.length;
this.cues_ = cues;
this.length_ = cues.length;
const defineProp = function (index) {
if (!('' + index in this)) {
Object.defineProperty(this, '' + index, {
get() {
return this.cues_[index];
}
});
}
};
if (oldLength < l) {
i = oldLength;
for (; i < l; i++) {
defineProp.call(this, i);
}
}
}
/**
* Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
*
* @param {string} id
* The id of the cue that should be searched for.
*
* @return {TextTrackCueList~TextTrackCue|null}
* A single cue or null if none was found.
*/
getCueById(id) {
let result = null;
for (let i = 0, l = this.length; i < l; i++) {
const cue = this[i];
if (cue.id === id) {
result = cue;
break;
}
}
return result;
}
}
/**
* @file track-kinds.js
*/
/**
* All possible `VideoTrackKind`s
*
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
* @typedef VideoTrack~Kind
* @enum
*/
const VideoTrackKind = {
alternative: 'alternative',
captions: 'captions',
main: 'main',
sign: 'sign',
subtitles: 'subtitles',
commentary: 'commentary'
};
/**
* All possible `AudioTrackKind`s
*
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
* @typedef AudioTrack~Kind
* @enum
*/
const AudioTrackKind = {
'alternative': 'alternative',
'descriptions': 'descriptions',
'main': 'main',
'main-desc': 'main-desc',
'translation': 'translation',
'commentary': 'commentary'
};
/**
* All possible `TextTrackKind`s
*
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
* @typedef TextTrack~Kind
* @enum
*/
const TextTrackKind = {
subtitles: 'subtitles',
captions: 'captions',
descriptions: 'descriptions',
chapters: 'chapters',
metadata: 'metadata'
};
/**
* All possible `TextTrackMode`s
*
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
* @typedef TextTrack~Mode
* @enum
*/
const TextTrackMode = {
disabled: 'disabled',
hidden: 'hidden',
showing: 'showing'
};
/**
* @file track.js
*/
/**
* A Track class that contains all of the common functionality for {@link AudioTrack},
* {@link VideoTrack}, and {@link TextTrack}.
*
* > Note: This class should not be used directly
*
* @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
* @extends EventTarget
* @abstract
*/
class Track extends EventTarget {
/**
* Create an instance of this class.
*
* @param {Object} [options={}]
* Object of option names and values
*
* @param {string} [options.kind='']
* A valid kind for the track type you are creating.
*
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
* A unique id for this AudioTrack.
*
* @param {string} [options.label='']
* The menu label for this track.
*
* @param {string} [options.language='']
* A valid two character language code.
*
* @abstract
*/
constructor(options = {}) {
super();
const trackProps = {
id: options.id || 'vjs_track_' + newGUID(),
kind: options.kind || '',
language: options.language || ''
};
let label = options.label || '';
/**
* @memberof Track
* @member {string} id
* The id of this track. Cannot be changed after creation.
* @instance
*
* @readonly
*/
/**
* @memberof Track
* @member {string} kind
* The kind of track that this is. Cannot be changed after creation.
* @instance
*
* @readonly
*/
/**
* @memberof Track
* @member {string} language
* The two letter language code for this track. Cannot be changed after
* creation.
* @instance
*
* @readonly
*/
for (const key in trackProps) {
Object.defineProperty(this, key, {
get() {
return trackProps[key];
},
set() {}
});
}
/**
* @memberof Track
* @member {string} label
* The label of this track. Cannot be changed after creation.
* @instance
*
* @fires Track#labelchange
*/
Object.defineProperty(this, 'label', {
get() {
return label;
},
set(newLabel) {
if (newLabel !== label) {
label = newLabel;
/**
* An event that fires when label changes on this track.
*
* > Note: This is not part of the spec!
*
* @event Track#labelchange
* @type {Event}
*/
this.trigger('labelchange');
}
}
});
}
}
/**
* @file url.js
* @module url
*/
/**
* Resolve and parse the elements of a URL.
*
* @function
* @param {string} url
* The url to parse
*
* @return {URL}
* An object of url details
*/
const parseUrl = function (url) {
return new URL(url, document.baseURI);
};
/**
* Get absolute version of relative URL.
*
* @function
* @param {string} url
* URL to make absolute
*
* @return {string}
* Absolute URL
*/
const getAbsoluteURL = function (url) {
return new URL(url, document.baseURI).href;
};
/**
* Returns the extension of the passed file name. It will return an empty string
* if passed an invalid path.
*
* @function
* @param {string} path
* The fileName path like '/path/to/file.mp4'
*
* @return {string}
* The extension in lower case or an empty string if no
* extension could be found.
*/
const getFileExtension = function (path) {
if (typeof path === 'string') {
const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
const pathParts = splitPathRe.exec(path);
if (pathParts) {
return pathParts.pop().toLowerCase();
}
}
return '';
};
/**
* Returns whether the url passed is a cross domain request or not.
*
* @function
* @param {string} url
* The url to check.
*
* @param {URL} [winLoc]
* the domain to check the url against, defaults to window.location
*
* @return {boolean}
* Whether it is a cross domain request or not.
*/
const isCrossOrigin = function (url, winLoc = window.location) {
return parseUrl(url).origin !== winLoc.origin;
};
var Url = /*#__PURE__*/Object.freeze({
__proto__: null,
parseUrl: parseUrl,
getAbsoluteURL: getAbsoluteURL,
getFileExtension: getFileExtension,
isCrossOrigin: isCrossOrigin
});
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function unwrapExports (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var win;
if (typeof window !== "undefined") {
win = window;
} else if (typeof commonjsGlobal !== "undefined") {
win = commonjsGlobal;
} else if (typeof self !== "undefined") {
win = self;
} else {
win = {};
}
var window_1 = win;
var _extends_1 = createCommonjsModule(function (module) {
function _extends() {
return (module.exports = _extends = Object.assign ? Object.assign.bind() : function (n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
}
return n;
}, module.exports.__esModule = true, module.exports["default"] = module.exports), _extends.apply(null, arguments);
}
module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(_extends_1);
var isFunction_1 = isFunction;
var toString = Object.prototype.toString;
function isFunction(fn) {
if (!fn) {
return false;
}
var string = toString.call(fn);
return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && (
// IE8 and below
fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt);
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (it) return (it = it.call(o)).next.bind(it);
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
var InterceptorsStorage = /*#__PURE__*/function () {
function InterceptorsStorage() {
this.typeToInterceptorsMap_ = new Map();
this.enabled_ = false;
}
var _proto = InterceptorsStorage.prototype;
_proto.getIsEnabled = function getIsEnabled() {
return this.enabled_;
};
_proto.enable = function enable() {
this.enabled_ = true;
};
_proto.disable = function disable() {
this.enabled_ = false;
};
_proto.reset = function reset() {
this.typeToInterceptorsMap_ = new Map();
this.enabled_ = false;
};
_proto.addInterceptor = function addInterceptor(type, interceptor) {
if (!this.typeToInterceptorsMap_.has(type)) {
this.typeToInterceptorsMap_.set(type, new Set());
}
var interceptorsSet = this.typeToInterceptorsMap_.get(type);
if (interceptorsSet.has(interceptor)) {
// already have this interceptor
return false;
}
interceptorsSet.add(interceptor);
return true;
};
_proto.removeInterceptor = function removeInterceptor(type, interceptor) {
var interceptorsSet = this.typeToInterceptorsMap_.get(type);
if (interceptorsSet && interceptorsSet.has(interceptor)) {
interceptorsSet.delete(interceptor);
return true;
}
return false;
};
_proto.clearInterceptorsByType = function clearInterceptorsByType(type) {
var interceptorsSet = this.typeToInterceptorsMap_.get(type);
if (!interceptorsSet) {
return false;
}
this.typeToInterceptorsMap_.delete(type);
this.typeToInterceptorsMap_.set(type, new Set());
return true;
};
_proto.clear = function clear() {
if (!this.typeToInterceptorsMap_.size) {
return false;
}
this.typeToInterceptorsMap_ = new Map();
return true;
};
_proto.getForType = function getForType(type) {
return this.typeToInterceptorsMap_.get(type) || new Set();
};
_proto.execute = function execute(type, payload) {
var interceptors = this.getForType(type);
for (var _iterator = _createForOfIteratorHelperLoose(interceptors), _step; !(_step = _iterator()).done;) {
var interceptor = _step.value;
try {
payload = interceptor(payload);
} catch (e) {//ignore
}
}
return payload;
};
return InterceptorsStorage;
}();
var interceptors = InterceptorsStorage;
var RetryManager = /*#__PURE__*/function () {
function RetryManager() {
this.maxAttempts_ = 1;
this.delayFactor_ = 0.1;
this.fuzzFactor_ = 0.1;
this.initialDelay_ = 1000;
this.enabled_ = false;
}
var _proto = RetryManager.prototype;
_proto.getIsEnabled = function getIsEnabled() {
return this.enabled_;
};
_proto.enable = function enable() {
this.enabled_ = true;
};
_proto.disable = function disable() {
this.enabled_ = false;
};
_proto.reset = function reset() {
this.maxAttempts_ = 1;
this.delayFactor_ = 0.1;
this.fuzzFactor_ = 0.1;
this.initialDelay_ = 1000;
this.enabled_ = false;
};
_proto.getMaxAttempts = function getMaxAttempts() {
return this.maxAttempts_;
};
_proto.setMaxAttempts = function setMaxAttempts(maxAttempts) {
this.maxAttempts_ = maxAttempts;
};
_proto.getDelayFactor = function getDelayFactor() {
return this.delayFactor_;
};
_proto.setDelayFactor = function setDelayFactor(delayFactor) {
this.delayFactor_ = delayFactor;
};
_proto.getFuzzFactor = function getFuzzFactor() {
return this.fuzzFactor_;
};
_proto.setFuzzFactor = function setFuzzFactor(fuzzFactor) {
this.fuzzFactor_ = fuzzFactor;
};
_proto.getInitialDelay = function getInitialDelay() {
return this.initialDelay_;
};
_proto.setInitialDelay = function setInitialDelay(initialDelay) {
this.initialDelay_ = initialDelay;
};
_proto.createRetry = function createRetry(_temp) {
var _ref = _temp === void 0 ? {} : _temp,
maxAttempts = _ref.maxAttempts,
delayFactor = _ref.delayFactor,
fuzzFactor = _ref.fuzzFactor,
initialDelay = _ref.initialDelay;
return new Retry({
maxAttempts: maxAttempts || this.maxAttempts_,
delayFactor: delayFactor || this.delayFactor_,
fuzzFactor: fuzzFactor || this.fuzzFactor_,
initialDelay: initialDelay || this.initialDelay_
});
};
return RetryManager;
}();
var Retry = /*#__PURE__*/function () {
function Retry(options) {
this.maxAttempts_ = options.maxAttempts;
this.delayFactor_ = options.delayFactor;
this.fuzzFactor_ = options.fuzzFactor;
this.currentDelay_ = options.initialDelay;
this.currentAttempt_ = 1;
}
var _proto2 = Retry.prototype;
_proto2.moveToNextAttempt = function moveToNextAttempt() {
this.currentAttempt_++;
var delayDelta = this.currentDelay_ * this.delayFactor_;
this.currentDelay_ = this.currentDelay_ + delayDelta;
};
_proto2.shouldRetry = function shouldRetry() {
return this.currentAttempt_ < this.maxAttempts_;
};
_proto2.getCurrentDelay = function getCurrentDelay() {
return this.currentDelay_;
};
_proto2.getCurrentMinPossibleDelay = function getCurrentMinPossibleDelay() {
return (1 - this.fuzzFactor_) * this.currentDelay_;
};
_proto2.getCurrentMaxPossibleDelay = function getCurrentMaxPossibleDelay() {
return (1 + this.fuzzFactor_) * this.currentDelay_;
}
/**
* For example fuzzFactor is 0.1
* This means ±10% deviation
* So if we have delay as 1000
* This function can generate any value from 900 to 1100
*/;
_proto2.getCurrentFuzzedDelay = function getCurrentFuzzedDelay() {
var lowValue = this.getCurrentMinPossibleDelay();
var highValue = this.getCurrentMaxPossibleDelay();
return lowValue + Math.random() * (highValue - lowValue);
};
return Retry;
}();
var retry = RetryManager;
var httpResponseHandler = function httpResponseHandler(callback, decodeResponseBody) {
if (decodeResponseBody === void 0) {
decodeResponseBody = false;
}
return function (err, response, responseBody) {
// if the XHR failed, return that error
if (err) {
callback(err);
return;
} // if the HTTP status code is 4xx or 5xx, the request also failed
if (response.statusCode >= 400 && response.statusCode <= 599) {
var cause = responseBody;
if (decodeResponseBody) {
if (window_1.TextDecoder) {
var charset = getCharset(response.headers && response.headers['content-type']);
try {
cause = new TextDecoder(charset).decode(responseBody);
} catch (e) {}
} else {
cause = String.fromCharCode.apply(null, new Uint8Array(responseBody));
}
}
callback({
cause: cause
});
return;
} // otherwise, request succeeded
callback(null, responseBody);
};
};
function getCharset(contentTypeHeader) {
if (contentTypeHeader === void 0) {
contentTypeHeader = '';
}
return contentTypeHeader.toLowerCase().split(';').reduce(function (charset, contentType) {
var _contentType$split = contentType.split('='),
type = _contentType$split[0],
value = _contentType$split[1];
if (type.trim() === 'charset') {
return value.trim();
}
return charset;
}, 'utf-8');
}
var httpHandler = httpResponseHandler;
createXHR.httpHandler = httpHandler;
createXHR.requestInterceptorsStorage = new interceptors();
createXHR.responseInterceptorsStorage = new interceptors();
createXHR.retryManager = new retry();
/**
* @license
* slighly modified parse-headers 2.0.2
* Copyright (c) 2014 David Björklund
* Available under the MIT license
*
*/
var parseHeaders = function parseHeaders(headers) {
var result = {};
if (!headers) {
return result;
}
headers.trim().split('\n').forEach(function (row) {
var index = row.indexOf(':');
var key = row.slice(0, index).trim().toLowerCase();
var value = row.slice(index + 1).trim();
if (typeof result[key] === 'undefined') {
result[key] = value;
} else if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
});
return result;
};
var lib = createXHR; // Allow use of default import syntax in TypeScript
var default_1 = createXHR;
createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop;
createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest;
forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) {
createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) {
options = initParams(uri, options, callback);
options.method = method.toUpperCase();
return _createXHR(options);
};
});
function forEachArray(array, iterator) {
for (var i = 0; i < array.length; i++) {
iterator(array[i]);
}
}
function isEmpty(obj) {
for (var i in obj) {
if (obj.hasOwnProperty(i)) return false;
}
return true;
}
function initParams(uri, options, callback) {
var params = uri;
if (isFunction_1(options)) {
callback = options;
if (typeof uri === "string") {
params = {
uri: uri
};
}
} else {
params = _extends_1({}, options, {
uri: uri
});
}
params.callback = callback;
return params;
}
function createXHR(uri, options, callback) {
options = initParams(uri, options, callback);
return _createXHR(options);
}
function _createXHR(options) {
if (typeof options.callback === "undefined") {
throw new Error("callback argument missing");
} // call all registered request interceptors for a given request type:
if (options.requestType && createXHR.requestInterceptorsStorage.getIsEnabled()) {
var requestInterceptorPayload = {
uri: options.uri || options.url,
headers: options.headers || {},
body: options.body,
metadata: options.metadata || {},
retry: options.retry,
timeout: options.timeout
};
var updatedPayload = createXHR.requestInterceptorsStorage.execute(options.requestType, requestInterceptorPayload);
options.uri = updatedPayload.uri;
options.headers = updatedPayload.headers;
options.body = updatedPayload.body;
options.metadata = updatedPayload.metadata;
options.retry = updatedPayload.retry;
options.timeout = updatedPayload.timeout;
}
var called = false;
var callback = function cbOnce(err, response, body) {
if (!called) {
called = true;
options.callback(err, response, body);
}
};
function readystatechange() {
// do not call load 2 times when response interceptors are enabled
// why do we even need this 2nd load?
if (xhr.readyState === 4 && !createXHR.responseInterceptorsStorage.getIsEnabled()) {
setTimeout(loadFunc, 0);
}
}
function getBody() {
// Chrome with requestType=blob throws errors arround when even testing access to responseText
var body = undefined;
if (xhr.response) {
body = xhr.response;
} else {
body = xhr.responseText || getXml(xhr);
}
if (isJson) {
try {
body = JSON.parse(body);
} catch (e) {}
}
return body;
}
function errorFunc(evt) {
clearTimeout(timeoutTimer);
clearTimeout(options.retryTimeout);
if (!(evt instanceof Error)) {
evt = new Error("" + (evt || "Unknown XMLHttpRequest Error"));
}
evt.statusCode = 0; // we would like to retry on error:
if (!aborted && createXHR.retryManager.getIsEnabled() && options.retry && options.retry.shouldRetry()) {
options.retryTimeout = setTimeout(function () {
options.retry.moveToNextAttempt(); // we want to re-use the same options and the same xhr object:
options.xhr = xhr;
_createXHR(options);
}, options.retry.getCurrentFuzzedDelay());
return;
} // call all registered response interceptors for a given request type:
if (options.requestType && createXHR.responseInterceptorsStorage.getIsEnabled()) {
var responseInterceptorPayload = {
headers: failureResponse.headers || {},
body: failureResponse.body,
responseUrl: xhr.responseURL,
responseType: xhr.responseType
};
var _updatedPayload = createXHR.responseInterceptorsStorage.execute(options.requestType, responseInterceptorPayload);
failureResponse.body = _updatedPayload.body;
failureResponse.headers = _updatedPayload.headers;
}
return callback(evt, failureResponse);
} // will load the data & process the response in a special response object
function loadFunc() {
if (aborted) return;
var status;
clearTimeout(timeoutTimer);
clearTimeout(options.retryTimeout);
if (options.useXDR && xhr.status === undefined) {
//IE8 CORS GET successful response doesn't have a status field, but body is fine
status = 200;
} else {
status = xhr.status === 1223 ? 204 : xhr.status;
}
var response = failureResponse;
var err = null;
if (status !== 0) {
response = {
body: getBody(),
statusCode: status,
method: method,
headers: {},
url: uri,
rawRequest: xhr
};
if (xhr.getAllResponseHeaders) {
//remember xhr can in fact be XDR for CORS in IE
response.headers = parseHeaders(xhr.getAllResponseHeaders());
}
} else {
err = new Error("Internal XMLHttpRequest Error");
} // call all registered response interceptors for a given request type:
if (options.requestType && createXHR.responseInterceptorsStorage.getIsEnabled()) {
var responseInterceptorPayload = {
headers: response.headers || {},
body: response.body,
responseUrl: xhr.responseURL,
responseType: xhr.responseType
};
var _updatedPayload2 = createXHR.responseInterceptorsStorage.execute(options.requestType, responseInterceptorPayload);
response.body = _updatedPayload2.body;
response.headers = _updatedPayload2.headers;
}
return callback(err, response, response.body);
}
var xhr = options.xhr || null;
if (!xhr) {
if (options.cors || options.useXDR) {
xhr = new createXHR.XDomainRequest();
} else {
xhr = new createXHR.XMLHttpRequest();
}
}
var key;
var aborted;
var uri = xhr.url = options.uri || options.url;
var method = xhr.method = options.method || "GET";
var body = options.body || options.data;
var headers = xhr.headers = options.headers || {};
var sync = !!options.sync;
var isJson = false;
var timeoutTimer;
var failureResponse = {
body: undefined,
headers: {},
statusCode: 0,
method: method,
url: uri,
rawRequest: xhr
};
if ("json" in options && options.json !== false) {
isJson = true;
headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
if (method !== "GET" && method !== "HEAD") {
headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
body = JSON.stringify(options.json === true ? body : options.json);
}
}
xhr.onreadystatechange = readystatechange;
xhr.onload = loadFunc;
xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function.
xhr.onprogress = function () {// IE must die
};
xhr.onabort = function () {
aborted = true;
clearTimeout(options.retryTimeout);
};
xhr.ontimeout = errorFunc;
xhr.open(method, uri, !sync, options.username, options.password); //has to be after open
if (!sync) {
xhr.withCredentials = !!options.withCredentials;
} // Cannot set timeout with sync request
// not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
// both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
if (!sync && options.timeout > 0) {
timeoutTimer = setTimeout(function () {
if (aborted) return;
aborted = true; //IE9 may still call readystatechange
xhr.abort("timeout");
var e = new Error("XMLHttpRequest timeout");
e.code = "ETIMEDOUT";
errorFunc(e);
}, options.timeout);
}
if (xhr.setRequestHeader) {
for (key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
} else if (options.headers && !isEmpty(options.headers)) {
throw new Error("Headers cannot be set on an XDomainRequest object");
}
if ("responseType" in options) {
xhr.responseType = options.responseType;
}
if ("beforeSend" in options && typeof options.beforeSend === "function") {
options.beforeSend(xhr);
} // Microsoft Edge browser sends "undefined" when send is called with undefined value.
// XMLHttpRequest spec says to pass null as body to indicate no body
// See https://github.com/naugtur/xhr/issues/100.
xhr.send(body || null);
return xhr;
}
function getXml(xhr) {
// xhr.responseXML will throw Exception "InvalidStateError" or "DOMException"
// See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML.
try {
if (xhr.responseType === "document") {
return xhr.responseXML;
}
var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
if (xhr.responseType === "" && !firefoxBugTakenEffect) {
return xhr.responseXML;
}
} catch (e) {}
return null;
}
function noop() {}
lib.default = default_1;
/**
* @file text-track.js
*/
/** @import Tech from '../tech/tech' */
/**
* Takes a webvtt file contents and parses it into cues
*
* @param {string} srcContent
* webVTT file contents
*
* @param {TextTrack} track
* TextTrack to add cues to. Cues come from the srcContent.
*
* @private
*/
const parseCues = function (srcContent, track) {
const parser = new window.WebVTT.Parser(window, window.vttjs, window.WebVTT.StringDecoder());
const errors = [];
parser.oncue = function (cue) {
track.addCue(cue);
};
parser.onparsingerror = function (error) {
errors.push(error);
};
parser.onflush = function () {
track.trigger({
type: 'loadeddata',
target: track
});
};
parser.parse(srcContent);
if (errors.length > 0) {
if (window.console && window.console.groupCollapsed) {
window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
}
errors.forEach(error => log.error(error));
if (window.console && window.console.groupEnd) {
window.console.groupEnd();
}
}
parser.flush();
};
/**
* Load a `TextTrack` from a specified url.
*
* @param {string} src
* Url to load track from.
*
* @param {TextTrack} track
* Track to add cues to. Comes from the content at the end of `url`.
*
* @private
*/
const loadTrack = function (src, track) {
const opts = {
uri: src
};
const crossOrigin = isCrossOrigin(src);
if (crossOrigin) {
opts.cors = crossOrigin;
}
const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
if (withCredentials) {
opts.withCredentials = withCredentials;
}
lib(opts, bind_(this, function (err, response, responseBody) {
if (err) {
return log.error(err, response);
}
track.loaded_ = true;
// Make sure that vttjs has loaded, otherwise, wait till it finished loading
// NOTE: this is only used for the alt/video.novtt.js build
if (typeof window.WebVTT !== 'function') {
if (track.tech_) {
// to prevent use before define eslint error, we define loadHandler
// as a let here
track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
if (event.type === 'vttjserror') {
log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
return;
}
return parseCues(responseBody, track);
});
}
} else {
parseCues(responseBody, track);
}
}));
};
/**
* A representation of a single `TextTrack`.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
* @extends Track
*/
class TextTrack extends Track {
/**
* Create an instance of this class.
*
* @param {Object} options={}
* Object of option names and values
*
* @param {Tech} options.tech
* A reference to the tech that owns this TextTrack.
*
* @param {TextTrack~Kind} [options.kind='subtitles']
* A valid text track kind.
*
* @param {TextTrack~Mode} [options.mode='disabled']
* A valid text track mode.
*
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
* A unique id for this TextTrack.
*
* @param {string} [options.label='']
* The menu label for this track.
*
* @param {string} [options.language='']
* A valid two character language code.
*
* @param {string} [options.srclang='']
* A valid two character language code. An alternative, but deprioritized
* version of `options.language`
*
* @param {string} [options.src]
* A url to TextTrack cues.
*
* @param {boolean} [options.default]
* If this track should default to on or off.
*/
constructor(options = {}) {
if (!options.tech) {
throw new Error('A tech was not provided.');
}
const settings = merge(options, {
kind: TextTrackKind[options.kind] || 'subtitles',
language: options.language || options.srclang || ''
});
let mode = TextTrackMode[settings.mode] || 'disabled';
const default_ = settings.default;
if (settings.kind === 'metadata' || settings.kind === 'chapters') {
mode = 'hidden';
}
super(settings);
this.tech_ = settings.tech;
this.cues_ = [];
this.activeCues_ = [];
this.preload_ = this.tech_.preloadTextTracks !== false;
const cues = new TextTrackCueList(this.cues_);
const activeCues = new TextTrackCueList(this.activeCues_);
let changed = false;
this.timeupdateHandler = bind_(this, function (event = {}) {
if (this.tech_.isDisposed()) {
return;
}
if (!this.tech_.isReady_) {
if (event.type !== 'timeupdate') {
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
}
return;
}
// Accessing this.activeCues for the side-effects of updating itself
// due to its nature as a getter function. Do not remove or cues will
// stop updating!
// Use the setter to prevent deletion from uglify (pure_getters rule)
this.activeCues = this.activeCues;
if (changed) {
this.trigger('cuechange');
changed = false;
}
if (event.type !== 'timeupdate') {
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
}
});
const disposeHandler = () => {
this.stopTracking();
};
this.tech_.one('dispose', disposeHandler);
if (mode !== 'disabled') {
this.startTracking();
}
Object.defineProperties(this, {
/**
* @memberof TextTrack
* @member {boolean} default
* If this track was set to be on or off by default. Cannot be changed after
* creation.
* @instance
*
* @readonly
*/
default: {
get() {
return default_;
},
set() {}
},
/**
* @memberof TextTrack
* @member {string} mode
* Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
* not be set if setting to an invalid mode.
* @instance
*
* @fires TextTrack#modechange
*/
mode: {
get() {
return mode;
},
set(newMode) {
if (!TextTrackMode[newMode]) {
return;
}
if (mode === newMode) {
return;
}
mode = newMode;
if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
// On-demand load.
loadTrack(this.src, this);
}
this.stopTracking();
if (mode !== 'disabled') {
this.startTracking();
}
/**
* An event that fires when mode changes on this track. This allows
* the TextTrackList that holds this track to act accordingly.
*
* > Note: This is not part of the spec!
*
* @event TextTrack#modechange
* @type {Event}
*/
this.trigger('modechange');
}
},
/**
* @memberof TextTrack
* @member {TextTrackCueList} cues
* The text track cue list for this TextTrack.
* @instance
*/
cues: {
get() {
if (!this.loaded_) {
return null;
}
return cues;
},
set() {}
},
/**
* @memberof TextTrack
* @member {TextTrackCueList} activeCues
* The list text track cues that are currently active for this TextTrack.
* @instance
*/
activeCues: {
get() {
if (!this.loaded_) {
return null;
}
// nothing to do
if (this.cues.length === 0) {
return activeCues;
}
const ct = this.tech_.currentTime();
const active = [];
for (let i = 0, l = this.cues.length; i < l; i++) {
const cue = this.cues[i];
if (cue.startTime <= ct && cue.endTime >= ct) {
active.push(cue);
}
}
changed = false;
if (active.length !== this.activeCues_.length) {
changed = true;
} else {
for (let i = 0; i < active.length; i++) {
if (this.activeCues_.indexOf(active[i]) === -1) {
changed = true;
}
}
}
this.activeCues_ = active;
activeCues.setCues_(this.activeCues_);
return activeCues;
},
// /!\ Keep this setter empty (see the timeupdate handler above)
set() {}
}
});
if (settings.src) {
this.src = settings.src;
if (!this.preload_) {
// Tracks will load on-demand.
// Act like we're loaded for other purposes.
this.loaded_ = true;
}
if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
loadTrack(this.src, this);
}
} else {
this.loaded_ = true;
}
}
startTracking() {
// More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
// Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
this.tech_.on('timeupdate', this.timeupdateHandler);
}
stopTracking() {
if (this.rvf_) {
this.tech_.cancelVideoFrameCallback(this.rvf_);
this.rvf_ = undefined;
}
this.tech_.off('timeupdate', this.timeupdateHandler);
}
/**
* Add a cue to the internal list of cues.
*
* @param {TextTrack~Cue} cue
* The cue to add to our internal list
*/
addCue(originalCue) {
let cue = originalCue;
// Testing if the cue is a VTTCue in a way that survives minification
if (!('getCueAsHTML' in cue)) {
cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
for (const prop in originalCue) {
if (!(prop in cue)) {
cue[prop] = originalCue[prop];
}
}
// make sure that `id` is copied over
cue.id = originalCue.id;
cue.originalCue_ = originalCue;
}
const tracks = this.tech_.textTracks();
for (let i = 0; i < tracks.length; i++) {
if (tracks[i] !== this) {
tracks[i].removeCue(cue);
}
}
this.cues_.push(cue);
this.cues.setCues_(this.cues_);
}
/**
* Remove a cue from our internal list
*
* @param {TextTrack~Cue} removeCue
* The cue to remove from our internal list
*/
removeCue(removeCue) {
let i = this.cues_.length;
while (i--) {
const cue = this.cues_[i];
if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
this.cues_.splice(i, 1);
this.cues.setCues_(this.cues_);
break;
}
}
}
}
/**
* cuechange - One or more cues in the track have become active or stopped being active.
*
* @protected
*/
TextTrack.prototype.allowedEvents_ = {
cuechange: 'cuechange'
};
/**
* A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
* only one `AudioTrack` in the list will be enabled at a time.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
* @extends Track
*/
class AudioTrack extends Track {
/**
* Create an instance of this class.
*
* @param {Object} [options={}]
* Object of option names and values
*
* @param {AudioTrack~Kind} [options.kind='']
* A valid audio track kind
*
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
* A unique id for this AudioTrack.
*
* @param {string} [options.label='']
* The menu label for this track.
*
* @param {string} [options.language='']
* A valid two character language code.
*
* @param {boolean} [options.enabled]
* If this track is the one that is currently playing. If this track is part of
* an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
*/
constructor(options = {}) {
const settings = merge(options, {
kind: AudioTrackKind[options.kind] || ''
});
super(settings);
let enabled = false;
/**
* @memberof AudioTrack
* @member {boolean} enabled
* If this `AudioTrack` is enabled or not. When setting this will
* fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
* @instance
*
* @fires VideoTrack#selectedchange
*/
Object.defineProperty(this, 'enabled', {
get() {
return enabled;
},
set(newEnabled) {
// an invalid or unchanged value
if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
return;
}
enabled = newEnabled;
/**
* An event that fires when enabled changes on this track. This allows
* the AudioTrackList that holds this track to act accordingly.
*
* > Note: This is not part of the spec! Native tracks will do
* this internally without an event.
*
* @event AudioTrack#enabledchange
* @type {Event}
*/
this.trigger('enabledchange');
}
});
// if the user sets this track to selected then
// set selected to that true value otherwise
// we keep it false
if (settings.enabled) {
this.enabled = settings.enabled;
}
this.loaded_ = true;
}
}
/**
* A representation of a single `VideoTrack`.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
* @extends Track
*/
class VideoTrack extends Track {
/**
* Create an instance of this class.
*
* @param {Object} [options={}]
* Object of option names and values
*
* @param {string} [options.kind='']
* A valid {@link VideoTrack~Kind}
*
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
* A unique id for this AudioTrack.
*
* @param {string} [options.label='']
* The menu label for this track.
*
* @param {string} [options.language='']
* A valid two character language code.
*
* @param {boolean} [options.selected]
* If this track is the one that is currently playing.
*/
constructor(options = {}) {
const settings = merge(options, {
kind: VideoTrackKind[options.kind] || ''
});
super(settings);
let selected = false;
/**
* @memberof VideoTrack
* @member {boolean} selected
* If this `VideoTrack` is selected or not. When setting this will
* fire {@link VideoTrack#selectedchange} if the state of selected changed.
* @instance
*
* @fires VideoTrack#selectedchange
*/
Object.defineProperty(this, 'selected', {
get() {
return selected;
},
set(newSelected) {
// an invalid or unchanged value
if (typeof newSelected !== 'boolean' || newSelected === selected) {
return;
}
selected = newSelected;
/**
* An event that fires when selected changes on this track. This allows
* the VideoTrackList that holds this track to act accordingly.
*
* > Note: This is not part of the spec! Native tracks will do
* this internally without an event.
*
* @event VideoTrack#selectedchange
* @type {Event}
*/
this.trigger('selectedchange');
}
});
// if the user sets this track to selected then
// set selected to that true value otherwise
// we keep it false
if (settings.selected) {
this.selected = settings.selected;
}
}
}
/**
* @file html-track-element.js
*/
/** @import Tech from '../tech/tech' */
/**
* A single track represented in the DOM.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
* @extends EventTarget
*/
class HTMLTrackElement extends EventTarget {
/**
* Create an instance of this class.
*
* @param {Object} options={}
* Object of option names and values
*
* @param {Tech} options.tech
* A reference to the tech that owns this HTMLTrackElement.
*
* @param {TextTrack~Kind} [options.kind='subtitles']
* A valid text track kind.
*
* @param {TextTrack~Mode} [options.mode='disabled']
* A valid text track mode.
*
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
* A unique id for this TextTrack.
*
* @param {string} [options.label='']
* The menu label for this track.
*
* @param {string} [options.language='']
* A valid two character language code.
*
* @param {string} [options.srclang='']
* A valid two character language code. An alternative, but deprioritized
* version of `options.language`
*
* @param {string} [options.src]
* A url to TextTrack cues.
*
* @param {boolean} [options.default]
* If this track should default to on or off.
*/
constructor(options = {}) {
super();
let readyState;
const track = new TextTrack(options);
this.kind = track.kind;
this.src = track.src;
this.srclang = track.language;
this.label = track.label;
this.default = track.default;
Object.defineProperties(this, {
/**
* @memberof HTMLTrackElement
* @member {HTMLTrackElement~ReadyState} readyState
* The current ready state of the track element.
* @instance
*/
readyState: {
get() {
return readyState;
}
},
/**
* @memberof HTMLTrackElement
* @member {TextTrack} track
* The underlying TextTrack object.
* @instance
*
*/
track: {
get() {
return track;
}
}
});
readyState = HTMLTrackElement.NONE;
/**
* @listens TextTrack#loadeddata
* @fires HTMLTrackElement#load
*/
track.addEventListener('loadeddata', () => {
readyState = HTMLTrackElement.LOADED;
this.trigger({
type: 'load',
target: this
});
});
}
}
/**
* @protected
*/
HTMLTrackElement.prototype.allowedEvents_ = {
load: 'load'
};
/**
* The text track not loaded state.
*
* @type {number}
* @static
*/
HTMLTrackElement.NONE = 0;
/**
* The text track loading state.
*
* @type {number}
* @static
*/
HTMLTrackElement.LOADING = 1;
/**
* The text track loaded state.
*
* @type {number}
* @static
*/
HTMLTrackElement.LOADED = 2;
/**
* The text track failed to load state.
*
* @type {number}
* @static
*/
HTMLTrackElement.ERROR = 3;
/*
* This file contains all track properties that are used in
* player.js, tech.js, html5.js and possibly other techs in the future.
*/
const NORMAL = {
audio: {
ListClass: AudioTrackList,
TrackClass: AudioTrack,
capitalName: 'Audio'
},
video: {
ListClass: VideoTrackList,
TrackClass: VideoTrack,
capitalName: 'Video'
},
text: {
ListClass: TextTrackList,
TrackClass: TextTrack,
capitalName: 'Text'
}
};
Object.keys(NORMAL).forEach(function (type) {
NORMAL[type].getterName = `${type}Tracks`;
NORMAL[type].privateName = `${type}Tracks_`;
});
const REMOTE = {
remoteText: {
ListClass: TextTrackList,
TrackClass: TextTrack,
capitalName: 'RemoteText',
getterName: 'remoteTextTracks',
privateName: 'remoteTextTracks_'
},
remoteTextEl: {
ListClass: HtmlTrackElementList,
TrackClass: HTMLTrackElement,
capitalName: 'RemoteTextTrackEls',
getterName: 'remoteTextTrackEls',
privateName: 'remoteTextTrackEls_'
}
};
const ALL = Object.assign({}, NORMAL, REMOTE);
REMOTE.names = Object.keys(REMOTE);
NORMAL.names = Object.keys(NORMAL);
ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
var vtt = {};
/**
* @file tech.js
*/
/** @import { TimeRange } from '../utils/time' */
/**
* An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
* that just contains the src url alone.
* * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
* `var SourceString = 'http://example.com/some-video.mp4';`
*
* @typedef {Object|string} SourceObject
*
* @property {string} src
* The url to the source
*
* @property {string} type
* The mime type of the source
*/
/**
* A function used by {@link Tech} to create a new {@link TextTrack}.
*
* @private
*
* @param {Tech} self
* An instance of the Tech class.
*
* @param {string} kind
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
*
* @param {string} [label]
* Label to identify the text track
*
* @param {string} [language]
* Two letter language abbreviation
*
* @param {Object} [options={}]
* An object with additional text track options
*
* @return {TextTrack}
* The text track that was created.
*/
function createTrackHelper(self, kind, label, language, options = {}) {
const tracks = self.textTracks();
options.kind = kind;
if (label) {
options.label = label;
}
if (language) {
options.language = language;
}
options.tech = self;
const track = new ALL.text.TrackClass(options);
tracks.addTrack(track);
return track;
}
/**
* This is the base class for media playback technology controllers, such as
* {@link HTML5}
*
* @extends Component
*/
class Tech extends Component {
/**
* Create an instance of this Tech.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param {Function} [ready]
* Callback function to call when the `HTML5` Tech is ready.
*/
constructor(options = {}, ready = function () {}) {
// we don't want the tech to report user activity automatically.
// This is done manually in addControlsListeners
options.reportTouchActivity = false;
super(null, options, ready);
this.onDurationChange_ = e => this.onDurationChange(e);
this.trackProgress_ = e => this.trackProgress(e);
this.trackCurrentTime_ = e => this.trackCurrentTime(e);
this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
this.queuedHanders_ = new Set();
// keep track of whether the current source has played at all to
// implement a very limited played()
this.hasStarted_ = false;
this.on('playing', function () {
this.hasStarted_ = true;
});
this.on('loadstart', function () {
this.hasStarted_ = false;
});
ALL.names.forEach(name => {
const props = ALL[name];
if (options && options[props.getterName]) {
this[props.privateName] = options[props.getterName];
}
});
// Manually track progress in cases where the browser/tech doesn't report it.
if (!this.featuresProgressEvents) {
this.manualProgressOn();
}
// Manually track timeupdates in cases where the browser/tech doesn't report it.
if (!this.featuresTimeupdateEvents) {
this.manualTimeUpdatesOn();
}
['Text', 'Audio', 'Video'].forEach(track => {
if (options[`native${track}Tracks`] === false) {
this[`featuresNative${track}Tracks`] = false;
}
});
if (options.nativeCaptions === false || options.nativeTextTracks === false) {
this.featuresNativeTextTracks = false;
} else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
this.featuresNativeTextTracks = true;
}
if (!this.featuresNativeTextTracks) {
this.emulateTextTracks();
}
this.preloadTextTracks = options.preloadTextTracks !== false;
this.autoRemoteTextTracks_ = new ALL.text.ListClass();
this.initTrackListeners();
// Turn on component tap events only if not using native controls
if (!options.nativeControlsForTouch) {
this.emitTapEvents();
}
if (this.constructor) {
this.name_ = this.constructor.name || 'Unknown Tech';
}
}
/**
* A special function to trigger source set in a way that will allow player
* to re-trigger if the player or tech are not ready yet.
*
* @fires Tech#sourceset
* @param {string} src The source string at the time of the source changing.
*/
triggerSourceset(src) {
if (!this.isReady_) {
// on initial ready we have to trigger source set
// 1ms after ready so that player can watch for it.
this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
}
/**
* Fired when the source is set on the tech causing the media element
* to reload.
*
* @see {@link Player#event:sourceset}
* @event Tech#sourceset
* @type {Event}
*/
this.trigger({
src,
type: 'sourceset'
});
}
/* Fallbacks for unsupported event types
================================================================================ */
/**
* Polyfill the `progress` event for browsers that don't support it natively.
*
* @see {@link Tech#trackProgress}
*/
manualProgressOn() {
this.on('durationchange', this.onDurationChange_);
this.manualProgress = true;
// Trigger progress watching when a source begins loading
this.one('ready', this.trackProgress_);
}
/**
* Turn off the polyfill for `progress` events that was created in
* {@link Tech#manualProgressOn}
*/
manualProgressOff() {
this.manualProgress = false;
this.stopTrackingProgress();
this.off('durationchange', this.onDurationChange_);
}
/**
* This is used to trigger a `progress` event when the buffered percent changes. It
* sets an interval function that will be called every 500 milliseconds to check if the
* buffer end percent has changed.
*
* > This function is called by {@link Tech#manualProgressOn}
*
* @param {Event} event
* The `ready` event that caused this to run.
*
* @listens Tech#ready
* @fires Tech#progress
*/
trackProgress(event) {
this.stopTrackingProgress();
this.progressInterval = this.setInterval(bind_(this, function () {
// Don't trigger unless buffered amount is greater than last time
const numBufferedPercent = this.bufferedPercent();
if (this.bufferedPercent_ !== numBufferedPercent) {
/**
* See {@link Player#progress}
*
* @event Tech#progress
* @type {Event}
*/
this.trigger('progress');
}
this.bufferedPercent_ = numBufferedPercent;
if (numBufferedPercent === 1) {
this.stopTrackingProgress();
}
}), 500);
}
/**
* Update our internal duration on a `durationchange` event by calling
* {@link Tech#duration}.
*
* @param {Event} event
* The `durationchange` event that caused this to run.
*
* @listens Tech#durationchange
*/
onDurationChange(event) {
this.duration_ = this.duration();
}
/**
* Get and create a `TimeRange` object for buffering.
*
* @return {TimeRange}
* The time range object that was created.
*/
buffered() {
return createTimeRanges(0, 0);
}
/**
* Get the percentage of the current video that is currently buffered.
*
* @return {number}
* A number from 0 to 1 that represents the decimal percentage of the
* video that is buffered.
*
*/
bufferedPercent() {
return bufferedPercent(this.buffered(), this.duration_);
}
/**
* Turn off the polyfill for `progress` events that was created in
* {@link Tech#manualProgressOn}
* Stop manually tracking progress events by clearing the interval that was set in
* {@link Tech#trackProgress}.
*/
stopTrackingProgress() {
this.clearInterval(this.progressInterval);
}
/**
* Polyfill the `timeupdate` event for browsers that don't support it.
*
* @see {@link Tech#trackCurrentTime}
*/
manualTimeUpdatesOn() {
this.manualTimeUpdates = true;
this.on('play', this.trackCurrentTime_);
this.on('pause', this.stopTrackingCurrentTime_);
}
/**
* Turn off the polyfill for `timeupdate` events that was created in
* {@link Tech#manualTimeUpdatesOn}
*/
manualTimeUpdatesOff() {
this.manualTimeUpdates = false;
this.stopTrackingCurrentTime();
this.off('play', this.trackCurrentTime_);
this.off('pause', this.stopTrackingCurrentTime_);
}
/**
* Sets up an interval function to track current time and trigger `timeupdate` every
* 250 milliseconds.
*
* @listens Tech#play
* @triggers Tech#timeupdate
*/
trackCurrentTime() {
if (this.currentTimeInterval) {
this.stopTrackingCurrentTime();
}
this.currentTimeInterval = this.setInterval(function () {
/**
* Triggered at an interval of 250ms to indicated that time is passing in the video.
*
* @event Tech#timeupdate
* @type {Event}
*/
this.trigger({
type: 'timeupdate',
target: this,
manuallyTriggered: true
});
// 42 = 24 fps // 250 is what Webkit uses // FF uses 15
}, 250);
}
/**
* Stop the interval function created in {@link Tech#trackCurrentTime} so that the
* `timeupdate` event is no longer triggered.
*
* @listens {Tech#pause}
*/
stopTrackingCurrentTime() {
this.clearInterval(this.currentTimeInterval);
// #1002 - if the video ends right before the next timeupdate would happen,
// the progress bar won't make it all the way to the end
this.trigger({
type: 'timeupdate',
target: this,
manuallyTriggered: true
});
}
/**
* Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
* {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
*
* @fires Component#dispose
*/
dispose() {
// clear out all tracks because we can't reuse them between techs
this.clearTracks(NORMAL.names);
// Turn off any manual progress or timeupdate tracking
if (this.manualProgress) {
this.manualProgressOff();
}
if (this.manualTimeUpdates) {
this.manualTimeUpdatesOff();
}
super.dispose();
}
/**
* Clear out a single `TrackList` or an array of `TrackLists` given their names.
*
* > Note: Techs without source handlers should call this between sources for `video`
* & `audio` tracks. You don't want to use them between tracks!
*
* @param {string[]|string} types
* TrackList names to clear, valid names are `video`, `audio`, and
* `text`.
*/
clearTracks(types) {
types = [].concat(types);
// clear out all tracks because we can't reuse them between techs
types.forEach(type => {
const list = this[`${type}Tracks`]() || [];
let i = list.length;
while (i--) {
const track = list[i];
if (type === 'text') {
this.removeRemoteTextTrack(track);
}
list.removeTrack(track);
}
});
}
/**
* Remove any TextTracks added via addRemoteTextTrack that are
* flagged for automatic garbage collection
*/
cleanupAutoTextTracks() {
const list = this.autoRemoteTextTracks_ || [];
let i = list.length;
while (i--) {
const track = list[i];
this.removeRemoteTextTrack(track);
}
}
/**
* Reset the tech, which will removes all sources and reset the internal readyState.
*
* @abstract
*/
reset() {}
/**
* Get the value of `crossOrigin` from the tech.
*
* @abstract
*
* @see {Html5#crossOrigin}
*/
crossOrigin() {}
/**
* Set the value of `crossOrigin` on the tech.
*
* @abstract
*
* @param {string} crossOrigin the crossOrigin value
* @see {Html5#setCrossOrigin}
*/
setCrossOrigin() {}
/**
* Get or set an error on the Tech.
*
* @param {MediaError} [err]
* Error to set on the Tech
*
* @return {MediaError|null}
* The current error object on the tech, or null if there isn't one.
*/
error(err) {
if (err !== undefined) {
this.error_ = new MediaError(err);
this.trigger('error');
}
return this.error_;
}
/**
* Returns the `TimeRange`s that have been played through for the current source.
*
* > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
* It only checks whether the source has played at all or not.
*
* @return {TimeRange}
* - A single time range if this video has played
* - An empty set of ranges if not.
*/
played() {
if (this.hasStarted_) {
return createTimeRanges(0, 0);
}
return createTimeRanges();
}
/**
* Start playback
*
* @abstract
*
* @see {Html5#play}
*/
play() {}
/**
* Set whether we are scrubbing or not
*
* @abstract
* @param {boolean} _isScrubbing
* - true for we are currently scrubbing
* - false for we are no longer scrubbing
*
* @see {Html5#setScrubbing}
*/
setScrubbing(_isScrubbing) {}
/**
* Get whether we are scrubbing or not
*
* @abstract
*
* @see {Html5#scrubbing}
*/
scrubbing() {}
/**
* Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
* previously called.
*
* @param {number} _seconds
* Set the current time of the media to this.
* @fires Tech#timeupdate
*/
setCurrentTime(_seconds) {
// improve the accuracy of manual timeupdates
if (this.manualTimeUpdates) {
/**
* A manual `timeupdate` event.
*
* @event Tech#timeupdate
* @type {Event}
*/
this.trigger({
type: 'timeupdate',
target: this,
manuallyTriggered: true
});
}
}
/**
* Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
* {@link TextTrackList} events.
*
* This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
*
* @fires Tech#audiotrackchange
* @fires Tech#videotrackchange
* @fires Tech#texttrackchange
*/
initTrackListeners() {
/**
* Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
*
* @event Tech#audiotrackchange
* @type {Event}
*/
/**
* Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
*
* @event Tech#videotrackchange
* @type {Event}
*/
/**
* Triggered when tracks are added or removed on the Tech {@link TextTrackList}
*
* @event Tech#texttrackchange
* @type {Event}
*/
NORMAL.names.forEach(name => {
const props = NORMAL[name];
const trackListChanges = () => {
this.trigger(`${name}trackchange`);
};
const tracks = this[props.getterName]();
tracks.addEventListener('removetrack', trackListChanges);
tracks.addEventListener('addtrack', trackListChanges);
this.on('dispose', () => {
tracks.removeEventListener('removetrack', trackListChanges);
tracks.removeEventListener('addtrack', trackListChanges);
});
});
}
/**
* Emulate TextTracks using vtt.js if necessary
*
* @fires Tech#vttjsloaded
* @fires Tech#vttjserror
*/
addWebVttScript_() {
if (window.WebVTT) {
return;
}
// Initially, Tech.el_ is a child of a dummy-div wait until the Component system
// signals that the Tech is ready at which point Tech.el_ is part of the DOM
// before inserting the WebVTT script
if (document.body.contains(this.el())) {
// load via require if available and vtt.js script location was not passed in
// as an option. novtt builds will turn the above require call into an empty object
// which will cause this if check to always fail.
if (!this.options_['vtt.js'] && isPlain(vtt) && Object.keys(vtt).length > 0) {
this.trigger('vttjsloaded');
return;
}
// load vtt.js via the script location option or the cdn of no location was
// passed in
const script = document.createElement('script');
script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
script.onload = () => {
/**
* Fired when vtt.js is loaded.
*
* @event Tech#vttjsloaded
* @type {Event}
*/
this.trigger('vttjsloaded');
};
script.onerror = () => {
/**
* Fired when vtt.js was not loaded due to an error
*
* @event Tech#vttjsloaded
* @type {Event}
*/
this.trigger('vttjserror');
};
this.on('dispose', () => {
script.onload = null;
script.onerror = null;
});
// but have not loaded yet and we set it to true before the inject so that
// we don't overwrite the injected window.WebVTT if it loads right away
window.WebVTT = true;
this.el().parentNode.appendChild(script);
} else {
this.ready(this.addWebVttScript_);
}
}
/**
* Emulate texttracks
*
*/
emulateTextTracks() {
const tracks = this.textTracks();
const remoteTracks = this.remoteTextTracks();
const handleAddTrack = e => tracks.addTrack(e.track);
const handleRemoveTrack = e => tracks.removeTrack(e.track);
remoteTracks.on('addtrack', handleAddTrack);
remoteTracks.on('removetrack', handleRemoveTrack);
this.addWebVttScript_();
const updateDisplay = () => this.trigger('texttrackchange');
const textTracksChanges = () => {
updateDisplay();
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
track.removeEventListener('cuechange', updateDisplay);
if (track.mode === 'showing') {
track.addEventListener('cuechange', updateDisplay);
}
}
};
textTracksChanges();
tracks.addEventListener('change', textTracksChanges);
tracks.addEventListener('addtrack', textTracksChanges);
tracks.addEventListener('removetrack', textTracksChanges);
this.on('dispose', function () {
remoteTracks.off('addtrack', handleAddTrack);
remoteTracks.off('removetrack', handleRemoveTrack);
tracks.removeEventListener('change', textTracksChanges);
tracks.removeEventListener('addtrack', textTracksChanges);
tracks.removeEventListener('removetrack', textTracksChanges);
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
track.removeEventListener('cuechange', updateDisplay);
}
});
}
/**
* Create and returns a remote {@link TextTrack} object.
*
* @param {string} kind
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
*
* @param {string} [label]
* Label to identify the text track
*
* @param {string} [language]
* Two letter language abbreviation
*
* @return {TextTrack}
* The TextTrack that gets created.
*/
addTextTrack(kind, label, language) {
if (!kind) {
throw new Error('TextTrack kind is required but was not provided');
}
return createTrackHelper(this, kind, label, language);
}
/**
* Create an emulated TextTrack for use by addRemoteTextTrack
*
* This is intended to be overridden by classes that inherit from
* Tech in order to create native or custom TextTracks.
*
* @param {Object} options
* The object should contain the options to initialize the TextTrack with.
*
* @param {string} [options.kind]
* `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
*
* @param {string} [options.label].
* Label to identify the text track
*
* @param {string} [options.language]
* Two letter language abbreviation.
*
* @return {HTMLTrackElement}
* The track element that gets created.
*/
createRemoteTextTrack(options) {
const track = merge(options, {
tech: this
});
return new REMOTE.remoteTextEl.TrackClass(track);
}
/**
* Creates a remote text track object and returns an html track element.
*
* > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
*
* @param {Object} options
* See {@link Tech#createRemoteTextTrack} for more detailed properties.
*
* @param {boolean} [manualCleanup=false]
* - When false: the TextTrack will be automatically removed from the video
* element whenever the source changes
* - When True: The TextTrack will have to be cleaned up manually
*
* @return {HTMLTrackElement}
* An Html Track Element.
*
*/
addRemoteTextTrack(options = {}, manualCleanup) {
const htmlTrackElement = this.createRemoteTextTrack(options);
if (typeof manualCleanup !== 'boolean') {
manualCleanup = false;
}
// store HTMLTrackElement and TextTrack to remote list
this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
this.remoteTextTracks().addTrack(htmlTrackElement.track);
if (manualCleanup === false) {
// create the TextTrackList if it doesn't exist
this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
}
return htmlTrackElement;
}
/**
* Remove a remote text track from the remote `TextTrackList`.
*
* @param {TextTrack} track
* `TextTrack` to remove from the `TextTrackList`
*/
removeRemoteTextTrack(track) {
const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
// remove HTMLTrackElement and TextTrack from remote list
this.remoteTextTrackEls().removeTrackElement_(trackElement);
this.remoteTextTracks().removeTrack(track);
this.autoRemoteTextTracks_.removeTrack(track);
}
/**
* Gets available media playback quality metrics as specified by the W3C's Media
* Playback Quality API.
*
* @see [Spec]{@link https://wicg.github.io/media-playback-quality}
*
* @return {Object}
* An object with supported media playback quality metrics
*
* @abstract
*/
getVideoPlaybackQuality() {
return {};
}
/**
* Attempt to create a floating video window always on top of other windows
* so that users may continue consuming media while they interact with other
* content sites, or applications on their device.
*
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
*
* @return {Promise|undefined}
* A promise with a Picture-in-Picture window if the browser supports
* Promises (or one was passed in as an option). It returns undefined
* otherwise.
*
* @abstract
*/
requestPictureInPicture() {
return Promise.reject();
}
/**
* A method to check for the value of the 'disablePictureInPicture'