/**
* @license
* Video.js 8.12.0
* 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.12.0";
/**
* 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.
*/
const log = function (...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 `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;
/**
* 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; },
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 module:dom~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`. If `false`, the `classToToggle` will be removed from
* the `element`. 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|module:dom~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];
}
item = item.parentNode;
}
}
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;
}
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 a whitelist of event props
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 (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
// 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 {
[target, type, listener] = args;
}
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
});
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 keycode = createCommonjsModule(function (module, exports) {
// Source: http://jsfiddle.net/vWx8V/
// http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes
/**
* Conenience method returns corresponding value for given keyName or keyCode.
*
* @param {Mixed} keyCode {Number} or keyName {String}
* @return {Mixed}
* @api public
*/
function keyCode(searchInput) {
// Keyboard Events
if (searchInput && 'object' === typeof searchInput) {
var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
if (hasKeyCode) searchInput = hasKeyCode;
}
// Numbers
if ('number' === typeof searchInput) return names[searchInput];
// Everything else (cast to string)
var search = String(searchInput);
// check codes
var foundNamedKey = codes[search.toLowerCase()];
if (foundNamedKey) return foundNamedKey;
// check aliases
var foundNamedKey = aliases[search.toLowerCase()];
if (foundNamedKey) return foundNamedKey;
// weird character?
if (search.length === 1) return search.charCodeAt(0);
return undefined;
}
/**
* Compares a keyboard event with a given keyCode or keyName.
*
* @param {Event} event Keyboard event that should be tested
* @param {Mixed} keyCode {Number} or keyName {String}
* @return {Boolean}
* @api public
*/
keyCode.isEventKey = function isEventKey(event, nameOrCode) {
if (event && 'object' === typeof event) {
var keyCode = event.which || event.keyCode || event.charCode;
if (keyCode === null || keyCode === undefined) {
return false;
}
if (typeof nameOrCode === 'string') {
// check codes
var foundNamedKey = codes[nameOrCode.toLowerCase()];
if (foundNamedKey) {
return foundNamedKey === keyCode;
}
// check aliases
var foundNamedKey = aliases[nameOrCode.toLowerCase()];
if (foundNamedKey) {
return foundNamedKey === keyCode;
}
} else if (typeof nameOrCode === 'number') {
return nameOrCode === keyCode;
}
return false;
}
};
exports = module.exports = keyCode;
/**
* Get by name
*
* exports.code['enter'] // => 13
*/
var codes = exports.code = exports.codes = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'pause/break': 19,
'caps lock': 20,
'esc': 27,
'space': 32,
'page up': 33,
'page down': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'insert': 45,
'delete': 46,
'command': 91,
'left command': 91,
'right command': 93,
'numpad *': 106,
'numpad +': 107,
'numpad -': 109,
'numpad .': 110,
'numpad /': 111,
'num lock': 144,
'scroll lock': 145,
'my computer': 182,
'my calculator': 183,
';': 186,
'=': 187,
',': 188,
'-': 189,
'.': 190,
'/': 191,
'`': 192,
'[': 219,
'\\': 220,
']': 221,
"'": 222
};
// Helper aliases
var aliases = exports.aliases = {
'windows': 91,
'⇧': 16,
'⌥': 18,
'⌃': 17,
'⌘': 91,
'ctl': 17,
'control': 17,
'option': 18,
'pause': 19,
'break': 19,
'caps': 20,
'return': 13,
'escape': 27,
'spc': 32,
'spacebar': 32,
'pgup': 33,
'pgdn': 34,
'ins': 45,
'del': 46,
'cmd': 91
};
/*!
* Programatically add the following
*/
// lower case chars
for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32;
// numbers
for (var i = 48; i < 58; i++) codes[i - 48] = i;
// function keys
for (i = 1; i < 13; i++) codes['f' + i] = i + 111;
// numpad keys
for (i = 0; i < 10; i++) codes['numpad ' + i] = i + 96;
/**
* Get by code
*
* exports.name[13] // => 'Enter'
*/
var names = exports.names = exports.title = {}; // title for backward compat
// Create reverse mapping
for (i in codes) names[codes[i]] = i;
// Add aliases
for (var alias in aliases) {
codes[alias] = aliases[alias];
}
});
keycode.code;
keycode.codes;
keycode.aliases;
keycode.names;
keycode.title;
/**
* Player Component - Base class for all UI objects
*
* @file component.js
*/
/**
* 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 {
/**
* A callback that is called when a component is ready. Does not have any
* parameters and any callback value will be ignored.
*
* @callback ReadyCallback
* @this Component
*/
/**
* Creates an instance of this class.
*
* @param { import('./player').default } 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
*/
on(type, fn) {}
/**
* 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.
*/
off(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) {}
/**
* 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) {}
/**
* 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
*/
trigger(event, hash) {}
/**
* 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 { import('./player').default }
* 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.
*
* @return {Component}
* Returns itself; method can be chained.
*/
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 based on (@link Component#hasClass}
*
* @param {boolean|Dom~predicate} [predicate]
* An {@link Dom~predicate} function or a boolean
*/
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');
}
/**
* 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.
if (!keycode.isEventKey(event, 'Tab')) {
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)) {
return;
}
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;
});
}
/**
* 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
*/
/**
* Compute the percentage of the media that has been buffered.
*
* @param { import('./time').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;
var tuple = SafeParseTuple;
function SafeParseTuple(obj, reviver) {
var json;
var error = null;
try {
json = JSON.parse(obj, reviver);
} catch (err) {
error = err;
}
return [error, json];
}
/**
* 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
*/
/**
* 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 { import('../tech/tech').default } 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
*/
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 {
/**
* Create an instance of this class.
*
* @param { import('./player').default } player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param { import('./utils/dom').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}
*/
this.trigger('modalclose');
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 { import('./utils/dom').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_);
}
}
/**
* 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 { import('./utils/dom').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 { import('./utils/dom').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) {
// Do not allow keydowns to reach out of the modal dialog.
event.stopPropagation();
if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
event.preventDefault();
this.close();
return;
}
// exit early if it isn't a tab key
if (!keycode.isEventKey(event, '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
*/
/**
* Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
* {@link VideoTrackList}
*
* @extends EventTarget
*/
class TrackList extends EventTarget {
/**
* Create an instance of this class
*
* @param { import('./track').default[] } 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 { import('./track').default } 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 { import('./track').default } 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 { import('./track').default }
* @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
*/
/**
* 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 { import('./audio-track').default } 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 { import('./audio-track').default[] } [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 { import('./audio-track').default } 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
*/
/**
* Un-select all other {@link VideoTrack}s that are selected.
*
* @param {VideoTrackList} list
* list to work on
*
* @param { import('./video-track').default } 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 { import('./video-track').default } 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
*/
/**
* 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 { import('./text-track').default } 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
*/
/**
* @typedef {Object} url:URLObject
*
* @property {string} protocol
* The protocol of the url that was parsed.
*
* @property {string} hostname
* The hostname of the url that was parsed.
*
* @property {string} port
* The port of the url that was parsed.
*
* @property {string} pathname
* The pathname of the url that was parsed.
*
* @property {string} search
* The search query of the url that was parsed.
*
* @property {string} hash
* The hash of the url that was parsed.
*
* @property {string} host
* The host of the url that was parsed.
*/
/**
* Resolve and parse the elements of a URL.
*
* @function
* @param {String} url
* The url to parse
*
* @return {url:URLObject}
* An object of url details
*/
const parseUrl = function (url) {
// This entire method can be replace with URL once we are able to drop IE11
const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
// add the url to an anchor and let the browser parse the URL
const a = document.createElement('a');
a.href = url;
// Copy the specific URL properties to a new object
// This is also needed for IE because the anchor loses its
// properties when it's removed from the dom
const details = {};
for (let i = 0; i < props.length; i++) {
details[props[i]] = a[props[i]];
}
// IE adds the port to the host property unlike everyone else. If
// a port identifier is added for standard ports, strip it.
if (details.protocol === 'http:') {
details.host = details.host.replace(/:80$/, '');
}
if (details.protocol === 'https:') {
details.host = details.host.replace(/:443$/, '');
}
if (!details.protocol) {
details.protocol = window.location.protocol;
}
/* istanbul ignore if */
if (!details.host) {
details.host = window.location.host;
}
return details;
};
/**
* Get absolute version of relative URL.
*
* @function
* @param {string} url
* URL to make absolute
*
* @return {string}
* Absolute URL
*
* @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
*/
const getAbsoluteURL = function (url) {
// Check if absolute URL
if (!url.match(/^https?:\/\//)) {
// Add the url to an anchor and let the browser parse it to convert to an absolute url
const a = document.createElement('a');
a.href = url;
url = a.href;
}
return url;
};
/**
* 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 {Object} [winLoc]
* the domain to check the url against, defaults to window.location
*
* @param {string} [winLoc.protocol]
* The window location protocol defaults to window.location.protocol
*
* @param {string} [winLoc.host]
* The window location host defaults to window.location.host
*
* @return {boolean}
* Whether it is a cross domain request or not.
*/
const isCrossOrigin = function (url, winLoc = window.location) {
const urlInfo = parseUrl(url);
// IE8 protocol relative urls will return ':' for protocol
const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
// Check if url is for another domain/origin
// IE8 doesn't know location.origin, so we won't rely on it here
const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
return crossOrigin;
};
var Url = /*#__PURE__*/Object.freeze({
__proto__: null,
parseUrl: parseUrl,
getAbsoluteURL: getAbsoluteURL,
getFileExtension: getFileExtension,
isCrossOrigin: isCrossOrigin
});
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() {
module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
}, module.exports.__esModule = true, module.exports["default"] = module.exports;
return _extends.apply(this, 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);
}
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;
/**
* @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");
}
var called = false;
var callback = function cbOnce(err, response, body) {
if (!called) {
called = true;
options.callback(err, response, body);
}
};
function readystatechange() {
if (xhr.readyState === 4) {
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);
if (!(evt instanceof Error)) {
evt = new Error("" + (evt || "Unknown XMLHttpRequest Error"));
}
evt.statusCode = 0;
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);
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");
}
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;
};
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
*/
/**
* 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 { import('../tech/tech').default } 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
*/
/**
* 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 { import('../tech/tech').default } 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 minDoc = {};
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {};
var doccy;
if (typeof document !== 'undefined') {
doccy = document;
} else {
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
if (!doccy) {
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
}
}
var document_1 = doccy;
/**
* Copyright 2013 vtt.js Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
var _objCreate = Object.create || function () {
function F() {}
return function (o) {
if (arguments.length !== 1) {
throw new Error('Object.create shim only accepts one parameter.');
}
F.prototype = o;
return new F();
};
}();
// Creates a new ParserError object from an errorData object. The errorData
// object should have default code and message properties. The default message
// property can be overriden by passing in a message parameter.
// See ParsingError.Errors below for acceptable errors.
function ParsingError(errorData, message) {
this.name = "ParsingError";
this.code = errorData.code;
this.message = message || errorData.message;
}
ParsingError.prototype = _objCreate(Error.prototype);
ParsingError.prototype.constructor = ParsingError;
// ParsingError metadata for acceptable ParsingErrors.
ParsingError.Errors = {
BadSignature: {
code: 0,
message: "Malformed WebVTT signature."
},
BadTimeStamp: {
code: 1,
message: "Malformed time stamp."
}
};
// Try to parse input as a time stamp.
function parseTimeStamp(input) {
function computeSeconds(h, m, s, f) {
return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
}
var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/);
if (!m) {
return null;
}
if (m[3]) {
// Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]);
} else if (m[1] > 59) {
// Timestamp takes the form of [hours]:[minutes].[milliseconds]
// First position is hours as it's over 59.
return computeSeconds(m[1], m[2], 0, m[4]);
} else {
// Timestamp takes the form of [minutes]:[seconds].[milliseconds]
return computeSeconds(0, m[1], m[2], m[4]);
}
}
// A settings object holds key/value pairs and will ignore anything but the first
// assignment to a specific key.
function Settings() {
this.values = _objCreate(null);
}
Settings.prototype = {
// Only accept the first assignment to any key.
set: function (k, v) {
if (!this.get(k) && v !== "") {
this.values[k] = v;
}
},
// Return the value for a key, or a default value.
// If 'defaultKey' is passed then 'dflt' is assumed to be an object with
// a number of possible default values as properties where 'defaultKey' is
// the key of the property that will be chosen; otherwise it's assumed to be
// a single value.
get: function (k, dflt, defaultKey) {
if (defaultKey) {
return this.has(k) ? this.values[k] : dflt[defaultKey];
}
return this.has(k) ? this.values[k] : dflt;
},
// Check whether we have a value for a key.
has: function (k) {
return k in this.values;
},
// Accept a setting if its one of the given alternatives.
alt: function (k, v, a) {
for (var n = 0; n < a.length; ++n) {
if (v === a[n]) {
this.set(k, v);
break;
}
}
},
// Accept a setting if its a valid (signed) integer.
integer: function (k, v) {
if (/^-?\d+$/.test(v)) {
// integer
this.set(k, parseInt(v, 10));
}
},
// Accept a setting if its a valid percentage.
percent: function (k, v) {
if (v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) {
v = parseFloat(v);
if (v >= 0 && v <= 100) {
this.set(k, v);
return true;
}
}
return false;
}
};
// Helper function to parse input into groups separated by 'groupDelim', and
// interprete each group as a key/value pair separated by 'keyValueDelim'.
function parseOptions(input, callback, keyValueDelim, groupDelim) {
var groups = groupDelim ? input.split(groupDelim) : [input];
for (var i in groups) {
if (typeof groups[i] !== "string") {
continue;
}
var kv = groups[i].split(keyValueDelim);
if (kv.length !== 2) {
continue;
}
var k = kv[0].trim();
var v = kv[1].trim();
callback(k, v);
}
}
function parseCue(input, cue, regionList) {
// Remember the original input if we need to throw an error.
var oInput = input;
// 4.1 WebVTT timestamp
function consumeTimeStamp() {
var ts = parseTimeStamp(input);
if (ts === null) {
throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput);
}
// Remove time stamp from input.
input = input.replace(/^[^\sa-zA-Z-]+/, "");
return ts;
}
// 4.4.2 WebVTT cue settings
function consumeCueSettings(input, cue) {
var settings = new Settings();
parseOptions(input, function (k, v) {
switch (k) {
case "region":
// Find the last region we parsed with the same region id.
for (var i = regionList.length - 1; i >= 0; i--) {
if (regionList[i].id === v) {
settings.set(k, regionList[i].region);
break;
}
}
break;
case "vertical":
settings.alt(k, v, ["rl", "lr"]);
break;
case "line":
var vals = v.split(","),
vals0 = vals[0];
settings.integer(k, vals0);
settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
settings.alt(k, vals0, ["auto"]);
if (vals.length === 2) {
settings.alt("lineAlign", vals[1], ["start", "center", "end"]);
}
break;
case "position":
vals = v.split(",");
settings.percent(k, vals[0]);
if (vals.length === 2) {
settings.alt("positionAlign", vals[1], ["start", "center", "end"]);
}
break;
case "size":
settings.percent(k, v);
break;
case "align":
settings.alt(k, v, ["start", "center", "end", "left", "right"]);
break;
}
}, /:/, /\s/);
// Apply default values for any missing fields.
cue.region = settings.get("region", null);
cue.vertical = settings.get("vertical", "");
try {
cue.line = settings.get("line", "auto");
} catch (e) {}
cue.lineAlign = settings.get("lineAlign", "start");
cue.snapToLines = settings.get("snapToLines", true);
cue.size = settings.get("size", 100);
// Safari still uses the old middle value and won't accept center
try {
cue.align = settings.get("align", "center");
} catch (e) {
cue.align = settings.get("align", "middle");
}
try {
cue.position = settings.get("position", "auto");
} catch (e) {
cue.position = settings.get("position", {
start: 0,
left: 0,
center: 50,
middle: 50,
end: 100,
right: 100
}, cue.align);
}
cue.positionAlign = settings.get("positionAlign", {
start: "start",
left: "start",
center: "center",
middle: "center",
end: "end",
right: "end"
}, cue.align);
}
function skipWhitespace() {
input = input.replace(/^\s+/, "");
}
// 4.1 WebVTT cue timings.
skipWhitespace();
cue.startTime = consumeTimeStamp(); // (1) collect cue start time
skipWhitespace();
if (input.substr(0, 3) !== "-->") {
// (3) next characters must match "-->"
throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput);
}
input = input.substr(3);
skipWhitespace();
cue.endTime = consumeTimeStamp(); // (5) collect cue end time
// 4.1 WebVTT cue settings list.
skipWhitespace();
consumeCueSettings(input, cue);
}
// When evaluating this file as part of a Webpack bundle for server
// side rendering, `document` is an empty object.
var TEXTAREA_ELEMENT = document_1.createElement && document_1.createElement("textarea");
var TAG_NAME = {
c: "span",
i: "i",
b: "b",
u: "u",
ruby: "ruby",
rt: "rt",
v: "span",
lang: "span"
};
// 5.1 default text color
// 5.2 default text background color is equivalent to text color with bg_ prefix
var DEFAULT_COLOR_CLASS = {
white: 'rgba(255,255,255,1)',
lime: 'rgba(0,255,0,1)',
cyan: 'rgba(0,255,255,1)',
red: 'rgba(255,0,0,1)',
yellow: 'rgba(255,255,0,1)',
magenta: 'rgba(255,0,255,1)',
blue: 'rgba(0,0,255,1)',
black: 'rgba(0,0,0,1)'
};
var TAG_ANNOTATION = {
v: "title",
lang: "lang"
};
var NEEDS_PARENT = {
rt: "ruby"
};
// Parse content into a document fragment.
function parseContent(window, input) {
function nextToken() {
// Check for end-of-string.
if (!input) {
return null;
}
// Consume 'n' characters from the input.
function consume(result) {
input = input.substr(result.length);
return result;
}
var m = input.match(/^([^<]*)(<[^>]*>?)?/);
// If there is some text before the next tag, return it, otherwise return
// the tag.
return consume(m[1] ? m[1] : m[2]);
}
function unescape(s) {
TEXTAREA_ELEMENT.innerHTML = s;
s = TEXTAREA_ELEMENT.textContent;
TEXTAREA_ELEMENT.textContent = "";
return s;
}
function shouldAdd(current, element) {
return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName;
}
// Create an element for this tag.
function createElement(type, annotation) {
var tagName = TAG_NAME[type];
if (!tagName) {
return null;
}
var element = window.document.createElement(tagName);
var name = TAG_ANNOTATION[type];
if (name && annotation) {
element[name] = annotation.trim();
}
return element;
}
var rootDiv = window.document.createElement("div"),
current = rootDiv,
t,
tagStack = [];
while ((t = nextToken()) !== null) {
if (t[0] === '<') {
if (t[1] === "/") {
// If the closing tag matches, move back up to the parent node.
if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
tagStack.pop();
current = current.parentNode;
}
// Otherwise just ignore the end tag.
continue;
}
var ts = parseTimeStamp(t.substr(1, t.length - 2));
var node;
if (ts) {
// Timestamps are lead nodes as well.
node = window.document.createProcessingInstruction("timestamp", ts);
current.appendChild(node);
continue;
}
var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
// If we can't parse the tag, skip to the next tag.
if (!m) {
continue;
}
// Try to construct an element, and ignore the tag if we couldn't.
node = createElement(m[1], m[3]);
if (!node) {
continue;
}
// Determine if the tag should be added based on the context of where it
// is placed in the cuetext.
if (!shouldAdd(current, node)) {
continue;
}
// Set the class list (as a list of classes, separated by space).
if (m[2]) {
var classes = m[2].split('.');
classes.forEach(function (cl) {
var bgColor = /^bg_/.test(cl);
// slice out `bg_` if it's a background color
var colorName = bgColor ? cl.slice(3) : cl;
if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) {
var propName = bgColor ? 'background-color' : 'color';
var propValue = DEFAULT_COLOR_CLASS[colorName];
node.style[propName] = propValue;
}
});
node.className = classes.join(' ');
}
// Append the node to the current node, and enter the scope of the new
// node.
tagStack.push(m[1]);
current.appendChild(node);
current = node;
continue;
}
// Text nodes are leaf nodes.
current.appendChild(window.document.createTextNode(unescape(t)));
}
return rootDiv;
}
// This is a list of all the Unicode characters that have a strong
// right-to-left category. What this means is that these characters are
// written right-to-left for sure. It was generated by pulling all the strong
// right-to-left characters out of the Unicode data table. That table can
// found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6], [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d], [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6], [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5], [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815], [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858], [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f], [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c], [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1], [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc], [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808], [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855], [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f], [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13], [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58], [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72], [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f], [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32], [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42], [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f], [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59], [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62], [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77], [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b], [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]];
function isStrongRTLChar(charCode) {
for (var i = 0; i < strongRTLRanges.length; i++) {
var currentRange = strongRTLRanges[i];
if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
return true;
}
}
return false;
}
function determineBidi(cueDiv) {
var nodeStack = [],
text = "",
charCode;
if (!cueDiv || !cueDiv.childNodes) {
return "ltr";
}
function pushNodes(nodeStack, node) {
for (var i = node.childNodes.length - 1; i >= 0; i--) {
nodeStack.push(node.childNodes[i]);
}
}
function nextTextNode(nodeStack) {
if (!nodeStack || !nodeStack.length) {
return null;
}
var node = nodeStack.pop(),
text = node.textContent || node.innerText;
if (text) {
// TODO: This should match all unicode type B characters (paragraph
// separator characters). See issue #115.
var m = text.match(/^.*(\n|\r)/);
if (m) {
nodeStack.length = 0;
return m[0];
}
return text;
}
if (node.tagName === "ruby") {
return nextTextNode(nodeStack);
}
if (node.childNodes) {
pushNodes(nodeStack, node);
return nextTextNode(nodeStack);
}
}
pushNodes(nodeStack, cueDiv);
while (text = nextTextNode(nodeStack)) {
for (var i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (isStrongRTLChar(charCode)) {
return "rtl";
}
}
}
return "ltr";
}
function computeLinePos(cue) {
if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) {
return cue.line;
}
if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) {
return -1;
}
var track = cue.track,
trackList = track.textTrackList,
count = 0;
for (var i = 0; i < trackList.length && trackList[i] !== track; i++) {
if (trackList[i].mode === "showing") {
count++;
}
}
return ++count * -1;
}
function StyleBox() {}
// Apply styles to a div. If there is no div passed then it defaults to the
// div on 'this'.
StyleBox.prototype.applyStyles = function (styles, div) {
div = div || this.div;
for (var prop in styles) {
if (styles.hasOwnProperty(prop)) {
div.style[prop] = styles[prop];
}
}
};
StyleBox.prototype.formatStyle = function (val, unit) {
return val === 0 ? 0 : val + unit;
};
// Constructs the computed display state of the cue (a div). Places the div
// into the overlay which should be a block level element (usually a div).
function CueStyleBox(window, cue, styleOptions) {
StyleBox.call(this);
this.cue = cue;
// Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
// have inline positioning and will function as the cue background box.
this.cueDiv = parseContent(window, cue.text);
var styles = {
color: "rgba(255, 255, 255, 1)",
backgroundColor: "rgba(0, 0, 0, 0.8)",
position: "relative",
left: 0,
right: 0,
top: 0,
bottom: 0,
display: "inline",
writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
unicodeBidi: "plaintext"
};
this.applyStyles(styles, this.cueDiv);
// Create an absolutely positioned div that will be used to position the cue
// div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
// mirrors of them except middle instead of center on Safari.
this.div = window.document.createElement("div");
styles = {
direction: determineBidi(this.cueDiv),
writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
unicodeBidi: "plaintext",
textAlign: cue.align === "middle" ? "center" : cue.align,
font: styleOptions.font,
whiteSpace: "pre-line",
position: "absolute"
};
this.applyStyles(styles);
this.div.appendChild(this.cueDiv);
// Calculate the distance from the reference edge of the viewport to the text
// position of the cue box. The reference edge will be resolved later when
// the box orientation styles are applied.
var textPos = 0;
switch (cue.positionAlign) {
case "start":
case "line-left":
textPos = cue.position;
break;
case "center":
textPos = cue.position - cue.size / 2;
break;
case "end":
case "line-right":
textPos = cue.position - cue.size;
break;
}
// Horizontal box orientation; textPos is the distance from the left edge of the
// area to the left edge of the box and cue.size is the distance extending to
// the right from there.
if (cue.vertical === "") {
this.applyStyles({
left: this.formatStyle(textPos, "%"),
width: this.formatStyle(cue.size, "%")
});
// Vertical box orientation; textPos is the distance from the top edge of the
// area to the top edge of the box and cue.size is the height extending
// downwards from there.
} else {
this.applyStyles({
top: this.formatStyle(textPos, "%"),
height: this.formatStyle(cue.size, "%")
});
}
this.move = function (box) {
this.applyStyles({
top: this.formatStyle(box.top, "px"),
bottom: this.formatStyle(box.bottom, "px"),
left: this.formatStyle(box.left, "px"),
right: this.formatStyle(box.right, "px"),
height: this.formatStyle(box.height, "px"),
width: this.formatStyle(box.width, "px")
});
};
}
CueStyleBox.prototype = _objCreate(StyleBox.prototype);
CueStyleBox.prototype.constructor = CueStyleBox;
// Represents the co-ordinates of an Element in a way that we can easily
// compute things with such as if it overlaps or intersects with another Element.
// Can initialize it with either a StyleBox or another BoxPosition.
function BoxPosition(obj) {
// Either a BoxPosition was passed in and we need to copy it, or a StyleBox
// was passed in and we need to copy the results of 'getBoundingClientRect'
// as the object returned is readonly. All co-ordinate values are in reference
// to the viewport origin (top left).
var lh, height, width, top;
if (obj.div) {
height = obj.div.offsetHeight;
width = obj.div.offsetWidth;
top = obj.div.offsetTop;
var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects();
obj = obj.div.getBoundingClientRect();
// In certain cases the outter div will be slightly larger then the sum of
// the inner div's lines. This could be due to bold text, etc, on some platforms.
// In this case we should get the average line height and use that. This will
// result in the desired behaviour.
lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0;
}
this.left = obj.left;
this.right = obj.right;
this.top = obj.top || top;
this.height = obj.height || height;
this.bottom = obj.bottom || top + (obj.height || height);
this.width = obj.width || width;
this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
}
// Move the box along a particular axis. Optionally pass in an amount to move
// the box. If no amount is passed then the default is the line height of the
// box.
BoxPosition.prototype.move = function (axis, toMove) {
toMove = toMove !== undefined ? toMove : this.lineHeight;
switch (axis) {
case "+x":
this.left += toMove;
this.right += toMove;
break;
case "-x":
this.left -= toMove;
this.right -= toMove;
break;
case "+y":
this.top += toMove;
this.bottom += toMove;
break;
case "-y":
this.top -= toMove;
this.bottom -= toMove;
break;
}
};
// Check if this box overlaps another box, b2.
BoxPosition.prototype.overlaps = function (b2) {
return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top;
};
// Check if this box overlaps any other boxes in boxes.
BoxPosition.prototype.overlapsAny = function (boxes) {
for (var i = 0; i < boxes.length; i++) {
if (this.overlaps(boxes[i])) {
return true;
}
}
return false;
};
// Check if this box is within another box.
BoxPosition.prototype.within = function (container) {
return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right;
};
// Check if this box is entirely within the container or it is overlapping
// on the edge opposite of the axis direction passed. For example, if "+x" is
// passed and the box is overlapping on the left edge of the container, then
// return true.
BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) {
switch (axis) {
case "+x":
return this.left < container.left;
case "-x":
return this.right > container.right;
case "+y":
return this.top < container.top;
case "-y":
return this.bottom > container.bottom;
}
};
// Find the percentage of the area that this box is overlapping with another
// box.
BoxPosition.prototype.intersectPercentage = function (b2) {
var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
intersectArea = x * y;
return intersectArea / (this.height * this.width);
};
// Convert the positions from this box to CSS compatible positions using
// the reference container's positions. This has to be done because this
// box's positions are in reference to the viewport origin, whereas, CSS
// values are in referecne to their respective edges.
BoxPosition.prototype.toCSSCompatValues = function (reference) {
return {
top: this.top - reference.top,
bottom: reference.bottom - this.bottom,
left: this.left - reference.left,
right: reference.right - this.right,
height: this.height,
width: this.width
};
};
// Get an object that represents the box's position without anything extra.
// Can pass a StyleBox, HTMLElement, or another BoxPositon.
BoxPosition.getSimpleBoxPosition = function (obj) {
var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0;
var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0;
var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0;
obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj;
var ret = {
left: obj.left,
right: obj.right,
top: obj.top || top,
height: obj.height || height,
bottom: obj.bottom || top + (obj.height || height),
width: obj.width || width
};
return ret;
};
// Move a StyleBox to its specified, or next best, position. The containerBox
// is the box that contains the StyleBox, such as a div. boxPositions are
// a list of other boxes that the styleBox can't overlap with.
function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
// Find the best position for a cue box, b, on the video. The axis parameter
// is a list of axis, the order of which, it will move the box along. For example:
// Passing ["+x", "-x"] will move the box first along the x axis in the positive
// direction. If it doesn't find a good position for it there it will then move
// it along the x axis in the negative direction.
function findBestPosition(b, axis) {
var bestPosition,
specifiedPosition = new BoxPosition(b),
percentage = 1; // Highest possible so the first thing we get is better.
for (var i = 0; i < axis.length; i++) {
while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) {
b.move(axis[i]);
}
// We found a spot where we aren't overlapping anything. This is our
// best position.
if (b.within(containerBox)) {
return b;
}
var p = b.intersectPercentage(containerBox);
// If we're outside the container box less then we were on our last try
// then remember this position as the best position.
if (percentage > p) {
bestPosition = new BoxPosition(b);
percentage = p;
}
// Reset the box position to the specified position.
b = new BoxPosition(specifiedPosition);
}
return bestPosition || specifiedPosition;
}
var boxPosition = new BoxPosition(styleBox),
cue = styleBox.cue,
linePos = computeLinePos(cue),
axis = [];
// If we have a line number to align the cue to.
if (cue.snapToLines) {
var size;
switch (cue.vertical) {
case "":
axis = ["+y", "-y"];
size = "height";
break;
case "rl":
axis = ["+x", "-x"];
size = "width";
break;
case "lr":
axis = ["-x", "+x"];
size = "width";
break;
}
var step = boxPosition.lineHeight,
position = step * Math.round(linePos),
maxPosition = containerBox[size] + step,
initialAxis = axis[0];
// If the specified intial position is greater then the max position then
// clamp the box to the amount of steps it would take for the box to
// reach the max position.
if (Math.abs(position) > maxPosition) {
position = position < 0 ? -1 : 1;
position *= Math.ceil(maxPosition / step) * step;
}
// If computed line position returns negative then line numbers are
// relative to the bottom of the video instead of the top. Therefore, we
// need to increase our initial position by the length or width of the
// video, depending on the writing direction, and reverse our axis directions.
if (linePos < 0) {
position += cue.vertical === "" ? containerBox.height : containerBox.width;
axis = axis.reverse();
}
// Move the box to the specified position. This may not be its best
// position.
boxPosition.move(initialAxis, position);
} else {
// If we have a percentage line value for the cue.
var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100;
switch (cue.lineAlign) {
case "center":
linePos -= calculatedPercentage / 2;
break;
case "end":
linePos -= calculatedPercentage;
break;
}
// Apply initial line position to the cue box.
switch (cue.vertical) {
case "":
styleBox.applyStyles({
top: styleBox.formatStyle(linePos, "%")
});
break;
case "rl":
styleBox.applyStyles({
left: styleBox.formatStyle(linePos, "%")
});
break;
case "lr":
styleBox.applyStyles({
right: styleBox.formatStyle(linePos, "%")
});
break;
}
axis = ["+y", "-x", "+x", "-y"];
// Get the box position again after we've applied the specified positioning
// to it.
boxPosition = new BoxPosition(styleBox);
}
var bestPosition = findBestPosition(boxPosition, axis);
styleBox.move(bestPosition.toCSSCompatValues(containerBox));
}
function WebVTT$1() {
// Nothing
}
// Helper to allow strings to be decoded instead of the default binary utf8 data.
WebVTT$1.StringDecoder = function () {
return {
decode: function (data) {
if (!data) {
return "";
}
if (typeof data !== "string") {
throw new Error("Error - expected string data.");
}
return decodeURIComponent(encodeURIComponent(data));
}
};
};
WebVTT$1.convertCueToDOMTree = function (window, cuetext) {
if (!window || !cuetext) {
return null;
}
return parseContent(window, cuetext);
};
var FONT_SIZE_PERCENT = 0.05;
var FONT_STYLE = "sans-serif";
var CUE_BACKGROUND_PADDING = "1.5%";
// Runs the processing model over the cues and regions passed to it.
// @param overlay A block level element (usually a div) that the computed cues
// and regions will be placed into.
WebVTT$1.processCues = function (window, cues, overlay) {
if (!window || !cues || !overlay) {
return null;
}
// Remove all previous children.
while (overlay.firstChild) {
overlay.removeChild(overlay.firstChild);
}
var paddedOverlay = window.document.createElement("div");
paddedOverlay.style.position = "absolute";
paddedOverlay.style.left = "0";
paddedOverlay.style.right = "0";
paddedOverlay.style.top = "0";
paddedOverlay.style.bottom = "0";
paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
overlay.appendChild(paddedOverlay);
// Determine if we need to compute the display states of the cues. This could
// be the case if a cue's state has been changed since the last computation or
// if it has not been computed yet.
function shouldCompute(cues) {
for (var i = 0; i < cues.length; i++) {
if (cues[i].hasBeenReset || !cues[i].displayState) {
return true;
}
}
return false;
}
// We don't need to recompute the cues' display states. Just reuse them.
if (!shouldCompute(cues)) {
for (var i = 0; i < cues.length; i++) {
paddedOverlay.appendChild(cues[i].displayState);
}
return;
}
var boxPositions = [],
containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
var styleOptions = {
font: fontSize + "px " + FONT_STYLE
};
(function () {
var styleBox, cue;
for (var i = 0; i < cues.length; i++) {
cue = cues[i];
// Compute the intial position and styles of the cue div.
styleBox = new CueStyleBox(window, cue, styleOptions);
paddedOverlay.appendChild(styleBox.div);
// Move the cue div to it's correct line position.
moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
// Remember the computed div so that we don't have to recompute it later
// if we don't have too.
cue.displayState = styleBox.div;
boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
}
})();
};
WebVTT$1.Parser = function (window, vttjs, decoder) {
if (!decoder) {
decoder = vttjs;
vttjs = {};
}
if (!vttjs) {
vttjs = {};
}
this.window = window;
this.vttjs = vttjs;
this.state = "INITIAL";
this.buffer = "";
this.decoder = decoder || new TextDecoder("utf8");
this.regionList = [];
};
WebVTT$1.Parser.prototype = {
// If the error is a ParsingError then report it to the consumer if
// possible. If it's not a ParsingError then throw it like normal.
reportOrThrowError: function (e) {
if (e instanceof ParsingError) {
this.onparsingerror && this.onparsingerror(e);
} else {
throw e;
}
},
parse: function (data) {
var self = this;
// If there is no data then we won't decode it, but will just try to parse
// whatever is in buffer already. This may occur in circumstances, for
// example when flush() is called.
if (data) {
// Try to decode the data that we received.
self.buffer += self.decoder.decode(data, {
stream: true
});
}
function collectNextLine() {
var buffer = self.buffer;
var pos = 0;
while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
++pos;
}
var line = buffer.substr(0, pos);
// Advance the buffer early in case we fail below.
if (buffer[pos] === '\r') {
++pos;
}
if (buffer[pos] === '\n') {
++pos;
}
self.buffer = buffer.substr(pos);
return line;
}
// 3.4 WebVTT region and WebVTT region settings syntax
function parseRegion(input) {
var settings = new Settings();
parseOptions(input, function (k, v) {
switch (k) {
case "id":
settings.set(k, v);
break;
case "width":
settings.percent(k, v);
break;
case "lines":
settings.integer(k, v);
break;
case "regionanchor":
case "viewportanchor":
var xy = v.split(',');
if (xy.length !== 2) {
break;
}
// We have to make sure both x and y parse, so use a temporary
// settings object here.
var anchor = new Settings();
anchor.percent("x", xy[0]);
anchor.percent("y", xy[1]);
if (!anchor.has("x") || !anchor.has("y")) {
break;
}
settings.set(k + "X", anchor.get("x"));
settings.set(k + "Y", anchor.get("y"));
break;
case "scroll":
settings.alt(k, v, ["up"]);
break;
}
}, /=/, /\s/);
// Create the region, using default values for any values that were not
// specified.
if (settings.has("id")) {
var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)();
region.width = settings.get("width", 100);
region.lines = settings.get("lines", 3);
region.regionAnchorX = settings.get("regionanchorX", 0);
region.regionAnchorY = settings.get("regionanchorY", 100);
region.viewportAnchorX = settings.get("viewportanchorX", 0);
region.viewportAnchorY = settings.get("viewportanchorY", 100);
region.scroll = settings.get("scroll", "");
// Register the region.
self.onregion && self.onregion(region);
// Remember the VTTRegion for later in case we parse any VTTCues that
// reference it.
self.regionList.push({
id: settings.get("id"),
region: region
});
}
}
// draft-pantos-http-live-streaming-20
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
// 3.5 WebVTT
function parseTimestampMap(input) {
var settings = new Settings();
parseOptions(input, function (k, v) {
switch (k) {
case "MPEGT":
settings.integer(k + 'S', v);
break;
case "LOCA":
settings.set(k + 'L', parseTimeStamp(v));
break;
}
}, /[^\d]:/, /,/);
self.ontimestampmap && self.ontimestampmap({
"MPEGTS": settings.get("MPEGTS"),
"LOCAL": settings.get("LOCAL")
});
}
// 3.2 WebVTT metadata header syntax
function parseHeader(input) {
if (input.match(/X-TIMESTAMP-MAP/)) {
// This line contains HLS X-TIMESTAMP-MAP metadata
parseOptions(input, function (k, v) {
switch (k) {
case "X-TIMESTAMP-MAP":
parseTimestampMap(v);
break;
}
}, /=/);
} else {
parseOptions(input, function (k, v) {
switch (k) {
case "Region":
// 3.3 WebVTT region metadata header syntax
parseRegion(v);
break;
}
}, /:/);
}
}
// 5.1 WebVTT file parsing.
try {
var line;
if (self.state === "INITIAL") {
// We can't start parsing until we have the first line.
if (!/\r\n|\n/.test(self.buffer)) {
return this;
}
line = collectNextLine();
var m = line.match(/^WEBVTT([ \t].*)?$/);
if (!m || !m[0]) {
throw new ParsingError(ParsingError.Errors.BadSignature);
}
self.state = "HEADER";
}
var alreadyCollectedLine = false;
while (self.buffer) {
// We can't parse a line until we have the full line.
if (!/\r\n|\n/.test(self.buffer)) {
return this;
}
if (!alreadyCollectedLine) {
line = collectNextLine();
} else {
alreadyCollectedLine = false;
}
switch (self.state) {
case "HEADER":
// 13-18 - Allow a header (metadata) under the WEBVTT line.
if (/:/.test(line)) {
parseHeader(line);
} else if (!line) {
// An empty line terminates the header and starts the body (cues).
self.state = "ID";
}
continue;
case "NOTE":
// Ignore NOTE blocks.
if (!line) {
self.state = "ID";
}
continue;
case "ID":
// Check for the start of NOTE blocks.
if (/^NOTE($|[ \t])/.test(line)) {
self.state = "NOTE";
break;
}
// 19-29 - Allow any number of line terminators, then initialize new cue values.
if (!line) {
continue;
}
self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, "");
// Safari still uses the old middle value and won't accept center
try {
self.cue.align = "center";
} catch (e) {
self.cue.align = "middle";
}
self.state = "CUE";
// 30-39 - Check if self line contains an optional identifier or timing data.
if (line.indexOf("-->") === -1) {
self.cue.id = line;
continue;
}
// Process line as start of a cue.
/*falls through*/
case "CUE":
// 40 - Collect cue timings and settings.
try {
parseCue(line, self.cue, self.regionList);
} catch (e) {
self.reportOrThrowError(e);
// In case of an error ignore rest of the cue.
self.cue = null;
self.state = "BADCUE";
continue;
}
self.state = "CUETEXT";
continue;
case "CUETEXT":
var hasSubstring = line.indexOf("-->") !== -1;
// 34 - If we have an empty line then report the cue.
// 35 - If we have the special substring '-->' then report the cue,
// but do not collect the line as we need to process the current
// one as a new cue.
if (!line || hasSubstring && (alreadyCollectedLine = true)) {
// We are done parsing self cue.
self.oncue && self.oncue(self.cue);
self.cue = null;
self.state = "ID";
continue;
}
if (self.cue.text) {
self.cue.text += "\n";
}
self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n');
continue;
case "BADCUE":
// BADCUE
// 54-62 - Collect and discard the remaining cue.
if (!line) {
self.state = "ID";
}
continue;
}
}
} catch (e) {
self.reportOrThrowError(e);
// If we are currently parsing a cue, report what we have.
if (self.state === "CUETEXT" && self.cue && self.oncue) {
self.oncue(self.cue);
}
self.cue = null;
// Enter BADWEBVTT state if header was not parsed correctly otherwise
// another exception occurred so enter BADCUE state.
self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
}
return this;
},
flush: function () {
var self = this;
try {
// Finish decoding the stream.
self.buffer += self.decoder.decode();
// Synthesize the end of the current cue or region.
if (self.cue || self.state === "HEADER") {
self.buffer += "\n\n";
self.parse();
}
// If we've flushed, parsed, and we're still on the INITIAL state then
// that means we don't have enough of the stream to parse the first
// line.
if (self.state === "INITIAL") {
throw new ParsingError(ParsingError.Errors.BadSignature);
}
} catch (e) {
self.reportOrThrowError(e);
}
self.onflush && self.onflush();
return this;
}
};
var vtt = WebVTT$1;
/**
* Copyright 2013 vtt.js Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var autoKeyword = "auto";
var directionSetting = {
"": 1,
"lr": 1,
"rl": 1
};
var alignSetting = {
"start": 1,
"center": 1,
"end": 1,
"left": 1,
"right": 1,
"auto": 1,
"line-left": 1,
"line-right": 1
};
function findDirectionSetting(value) {
if (typeof value !== "string") {
return false;
}
var dir = directionSetting[value.toLowerCase()];
return dir ? value.toLowerCase() : false;
}
function findAlignSetting(value) {
if (typeof value !== "string") {
return false;
}
var align = alignSetting[value.toLowerCase()];
return align ? value.toLowerCase() : false;
}
function VTTCue(startTime, endTime, text) {
/**
* Shim implementation specific properties. These properties are not in
* the spec.
*/
// Lets us know when the VTTCue's data has changed in such a way that we need
// to recompute its display state. This lets us compute its display state
// lazily.
this.hasBeenReset = false;
/**
* VTTCue and TextTrackCue properties
* http://dev.w3.org/html5/webvtt/#vttcue-interface
*/
var _id = "";
var _pauseOnExit = false;
var _startTime = startTime;
var _endTime = endTime;
var _text = text;
var _region = null;
var _vertical = "";
var _snapToLines = true;
var _line = "auto";
var _lineAlign = "start";
var _position = "auto";
var _positionAlign = "auto";
var _size = 100;
var _align = "center";
Object.defineProperties(this, {
"id": {
enumerable: true,
get: function () {
return _id;
},
set: function (value) {
_id = "" + value;
}
},
"pauseOnExit": {
enumerable: true,
get: function () {
return _pauseOnExit;
},
set: function (value) {
_pauseOnExit = !!value;
}
},
"startTime": {
enumerable: true,
get: function () {
return _startTime;
},
set: function (value) {
if (typeof value !== "number") {
throw new TypeError("Start time must be set to a number.");
}
_startTime = value;
this.hasBeenReset = true;
}
},
"endTime": {
enumerable: true,
get: function () {
return _endTime;
},
set: function (value) {
if (typeof value !== "number") {
throw new TypeError("End time must be set to a number.");
}
_endTime = value;
this.hasBeenReset = true;
}
},
"text": {
enumerable: true,
get: function () {
return _text;
},
set: function (value) {
_text = "" + value;
this.hasBeenReset = true;
}
},
"region": {
enumerable: true,
get: function () {
return _region;
},
set: function (value) {
_region = value;
this.hasBeenReset = true;
}
},
"vertical": {
enumerable: true,
get: function () {
return _vertical;
},
set: function (value) {
var setting = findDirectionSetting(value);
// Have to check for false because the setting an be an empty string.
if (setting === false) {
throw new SyntaxError("Vertical: an invalid or illegal direction string was specified.");
}
_vertical = setting;
this.hasBeenReset = true;
}
},
"snapToLines": {
enumerable: true,
get: function () {
return _snapToLines;
},
set: function (value) {
_snapToLines = !!value;
this.hasBeenReset = true;
}
},
"line": {
enumerable: true,
get: function () {
return _line;
},
set: function (value) {
if (typeof value !== "number" && value !== autoKeyword) {
throw new SyntaxError("Line: an invalid number or illegal string was specified.");
}
_line = value;
this.hasBeenReset = true;
}
},
"lineAlign": {
enumerable: true,
get: function () {
return _lineAlign;
},
set: function (value) {
var setting = findAlignSetting(value);
if (!setting) {
console.warn("lineAlign: an invalid or illegal string was specified.");
} else {
_lineAlign = setting;
this.hasBeenReset = true;
}
}
},
"position": {
enumerable: true,
get: function () {
return _position;
},
set: function (value) {
if (value < 0 || value > 100) {
throw new Error("Position must be between 0 and 100.");
}
_position = value;
this.hasBeenReset = true;
}
},
"positionAlign": {
enumerable: true,
get: function () {
return _positionAlign;
},
set: function (value) {
var setting = findAlignSetting(value);
if (!setting) {
console.warn("positionAlign: an invalid or illegal string was specified.");
} else {
_positionAlign = setting;
this.hasBeenReset = true;
}
}
},
"size": {
enumerable: true,
get: function () {
return _size;
},
set: function (value) {
if (value < 0 || value > 100) {
throw new Error("Size must be between 0 and 100.");
}
_size = value;
this.hasBeenReset = true;
}
},
"align": {
enumerable: true,
get: function () {
return _align;
},
set: function (value) {
var setting = findAlignSetting(value);
if (!setting) {
throw new SyntaxError("align: an invalid or illegal alignment string was specified.");
}
_align = setting;
this.hasBeenReset = true;
}
}
});
/**
* Other