video.js
Version:
An HTML5 video player that supports HLS and DASH with a common API and skin.
1,799 lines (1,673 loc) • 846 kB
JavaScript
/**
* @license
* Video.js 8.21.0 <http://videojs.com/>
* Copyright Brightcove, Inc. <https://www.brightcove.com/>
* Available under Apache License Version 2.0
* <https://github.com/videojs/video.js/blob/main/LICENSE>
*
* Includes vtt.js <https://github.com/mozilla/vtt.js>
* Available under Apache License Version 2.0
* <https://github.com/mozilla/vtt.js/blob/main/LICENSE>
*/
import window from 'global/window';
import document$1 from 'global/document';
import XHR from '@videojs/xhr';
import vtt from 'videojs-vtt.js';
var version = "8.21.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$1) {
browserApi = apiMap[i];
break;
}
}
// map the browser API names to the spec API names
if (browserApi) {
for (let i = 0; i < browserApi.length; i++) {
FullscreenApi[specApi[i]] = browserApi[i];
}
FullscreenApi.prefixed = browserApi[0] !== specApi[0];
}
/**
* @file create-logger.js
* @module create-logger
*/
// This is the private tracking variable for the logging history.
let history = [];
/**
* Log messages to the console and history based on the type of message
*
* @private
* @param {string} name
* The name of the console method to use.
*
* @param {Object} log
* The arguments to be passed to the matching console method.
*
* @param {string} [styles]
* styles for name
*/
const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
const lvl = log.levels[level];
const lvlRegExp = new RegExp(`^(${lvl})$`);
let resultName = name;
if (type !== 'log') {
// Add the type to the front of the message when it's not "log".
args.unshift(type.toUpperCase() + ':');
}
if (styles) {
resultName = `%c${name}`;
args.unshift(styles);
}
// Add console prefix after adding to history.
args.unshift(resultName + ':');
// Add a clone of the args at this point to history.
if (history) {
history.push([].concat(args));
// only store 1000 history entries
const splice = history.length - 1000;
history.splice(0, splice > 0 ? splice : 0);
}
// If there's no console then don't try to output messages, but they will
// still be stored in history.
if (!window.console) {
return;
}
// Was setting these once outside of this function, but containing them
// in the function makes it easier to test cases where console doesn't exist
// when the module is executed.
let fn = window.console[type];
if (!fn && type === 'debug') {
// Certain browsers don't have support for console.debug. For those, we
// should default to the closest comparable log.
fn = window.console.info || window.console.log;
}
// Bail out if there's no console or if this type is not allowed by the
// current logging level.
if (!fn || !lvl || !lvlRegExp.test(type)) {
return;
}
fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
};
function createLogger$1(name, delimiter = ':', styles = '') {
// This is the private tracking variable for logging level.
let level = 'info';
// the curried logByType bound to the specific log and history
let logByType;
/**
* Logs plain debug messages. Similar to `console.log`.
*
* Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
* of our JSDoc template, we cannot properly document this as both a function
* and a namespace, so its function signature is documented here.
*
* #### Arguments
* ##### *args
* *[]
*
* Any combination of values that could be passed to `console.log()`.
*
* #### Return Value
*
* `undefined`
*
* @namespace
* @param {...*} args
* One or more messages or objects that should be logged.
*/
function log(...args) {
logByType('log', level, args);
}
// This is the logByType helper that the logging methods below use
logByType = LogByTypeFactory(name, log, styles);
/**
* Create a new subLogger which chains the old name to the new name.
*
* For example, doing `mylogger = videojs.log.createLogger('player')` and then using that logger will log the following:
* ```js
* mylogger('foo');
* // > VIDEOJS: player: foo
* ```
*
* @param {string} subName
* The name to add call the new logger
* @param {string} [subDelimiter]
* Optional delimiter
* @param {string} [subStyles]
* Optional styles
* @return {Object}
*/
log.createLogger = (subName, subDelimiter, subStyles) => {
const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
const resultStyles = subStyles !== undefined ? subStyles : styles;
const resultName = `${name} ${resultDelimiter} ${subName}`;
return createLogger$1(resultName, resultDelimiter, resultStyles);
};
/**
* Create a new logger.
*
* @param {string} newName
* The name for the new logger
* @param {string} [newDelimiter]
* Optional delimiter
* @param {string} [newStyles]
* Optional styles
* @return {Object}
*/
log.createNewLogger = (newName, newDelimiter, newStyles) => {
return createLogger$1(newName, newDelimiter, newStyles);
};
/**
* Enumeration of available logging levels, where the keys are the level names
* and the values are `|`-separated strings containing logging methods allowed
* in that logging level. These strings are used to create a regular expression
* matching the function name being called.
*
* Levels provided by Video.js are:
*
* - `off`: Matches no calls. Any value that can be cast to `false` will have
* this effect. The most restrictive.
* - `all`: Matches only Video.js-provided functions (`debug`, `log`,
* `log.warn`, and `log.error`).
* - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
* - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
* - `warn`: Matches `log.warn` and `log.error` calls.
* - `error`: Matches only `log.error` calls.
*
* @type {Object}
*/
log.levels = {
all: 'debug|log|warn|error',
off: '',
debug: 'debug|log|warn|error',
info: 'log|warn|error',
warn: 'warn|error',
error: 'error',
DEFAULT: level
};
/**
* Get or set the current logging level.
*
* If a string matching a key from {@link module:log.levels} is provided, acts
* as a setter.
*
* @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
* Pass a valid level to set a new logging level.
*
* @return {string}
* The current logging level.
*/
log.level = lvl => {
if (typeof lvl === 'string') {
if (!log.levels.hasOwnProperty(lvl)) {
throw new Error(`"${lvl}" in not a valid log level`);
}
level = lvl;
}
return level;
};
/**
* Returns an array containing everything that has been logged to the history.
*
* This array is a shallow clone of the internal history record. However, its
* contents are _not_ cloned; so, mutating objects inside this array will
* mutate them in history.
*
* @return {Array}
*/
log.history = () => history ? [].concat(history) : [];
/**
* Allows you to filter the history by the given logger name
*
* @param {string} fname
* The name to filter by
*
* @return {Array}
* The filtered list to return
*/
log.history.filter = fname => {
return (history || []).filter(historyItem => {
// if the first item in each historyItem includes `fname`, then it's a match
return new RegExp(`.*${fname}.*`).test(historyItem[0]);
});
};
/**
* Clears the internal history tracking, but does not prevent further history
* tracking.
*/
log.history.clear = () => {
if (history) {
history.length = 0;
}
};
/**
* Disable history tracking if it is currently enabled.
*/
log.history.disable = () => {
if (history !== null) {
history.length = 0;
history = null;
}
};
/**
* Enable history tracking if it is currently disabled.
*/
log.history.enable = () => {
if (history === null) {
history = [];
}
};
/**
* Logs error messages. Similar to `console.error`.
*
* @param {...*} args
* One or more messages or objects that should be logged as an error
*/
log.error = (...args) => logByType('error', level, args);
/**
* Logs warning messages. Similar to `console.warn`.
*
* @param {...*} args
* One or more messages or objects that should be logged as a warning.
*/
log.warn = (...args) => logByType('warn', level, args);
/**
* Logs debug messages. Similar to `console.debug`, but may also act as a comparable
* log if `console.debug` is not available
*
* @param {...*} args
* One or more messages or objects that should be logged as debug.
*/
log.debug = (...args) => logByType('debug', level, args);
return log;
}
/**
* @file log.js
* @module log
*/
const log = createLogger$1('VIDEOJS');
const createLogger = log.createLogger;
/**
* @file obj.js
* @module obj
*/
/**
* @callback obj:EachCallback
*
* @param {*} value
* The current key for the object that is being iterated over.
*
* @param {string} key
* The current key-value for object that is being iterated over
*/
/**
* @callback obj:ReduceCallback
*
* @param {*} accum
* The value that is accumulating over the reduce loop.
*
* @param {*} value
* The current key for the object that is being iterated over.
*
* @param {string} key
* The current key-value for object that is being iterated over
*
* @return {*}
* The new accumulated value.
*/
const toString = 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.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<unknown>} - object values
*/
function values(source = {}) {
const result = [];
for (const key in source) {
if (source.hasOwnProperty(key)) {
const value = source[key];
result.push(value);
}
}
return result;
}
/**
* Object.defineProperty but "lazy", which means that the value is only set after
* it is retrieved the first time, rather than being set right away.
*
* @param {Object} obj the object to set the property on
* @param {string} key the key for the property to set
* @param {Function} getValue the function used to get the value when it is needed.
* @param {boolean} setter whether a setter should be allowed or not
*/
function defineLazyProperty(obj, key, getValue, setter = true) {
const set = value => Object.defineProperty(obj, key, {
value,
enumerable: true,
writable: true
});
const options = {
configurable: true,
enumerable: true,
get() {
const value = getValue();
set(value);
return value;
}
};
if (setter) {
options.set = set;
}
return Object.defineProperty(obj, key, options);
}
var Obj = /*#__PURE__*/Object.freeze({
__proto__: null,
each: each,
reduce: reduce,
isObject: isObject,
isPlain: isPlain,
merge: merge,
values: values,
defineLazyProperty: defineLazyProperty
});
/**
* @file browser.js
* @module browser
*/
/**
* Whether or not this device is an iPod.
*
* @static
* @type {Boolean}
*/
let IS_IPOD = false;
/**
* The detected iOS version - or `null`.
*
* @static
* @type {string|null}
*/
let IOS_VERSION = null;
/**
* Whether or not this is an Android device.
*
* @static
* @type {Boolean}
*/
let IS_ANDROID = false;
/**
* The detected Android version - or `null` if not Android or indeterminable.
*
* @static
* @type {number|string|null}
*/
let ANDROID_VERSION;
/**
* Whether or not this is Mozilla Firefox.
*
* @static
* @type {Boolean}
*/
let IS_FIREFOX = false;
/**
* Whether or not this is Microsoft Edge.
*
* @static
* @type {Boolean}
*/
let IS_EDGE = false;
/**
* Whether or not this is any Chromium Browser
*
* @static
* @type {Boolean}
*/
let IS_CHROMIUM = false;
/**
* Whether or not this is any Chromium browser that is not Edge.
*
* This will also be `true` for Chrome on iOS, which will have different support
* as it is actually Safari under the hood.
*
* Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
* IS_CHROMIUM should be used instead.
* "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
*
* @static
* @deprecated
* @type {Boolean}
*/
let IS_CHROME = false;
/**
* The detected Chromium version - or `null`.
*
* @static
* @type {number|null}
*/
let CHROMIUM_VERSION = null;
/**
* The detected Google Chrome version - or `null`.
* This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
* Deprecated, use CHROMIUM_VERSION instead.
*
* @static
* @deprecated
* @type {number|null}
*/
let CHROME_VERSION = null;
/**
* Whether or not this is a Chromecast receiver application.
*
* @static
* @type {Boolean}
*/
const IS_CHROMECAST_RECEIVER = Boolean(window.cast && window.cast.framework && window.cast.framework.CastReceiverContext);
/**
* The detected Internet Explorer version - or `null`.
*
* @static
* @deprecated
* @type {number|null}
*/
let IE_VERSION = null;
/**
* Whether or not this is desktop Safari.
*
* @static
* @type {Boolean}
*/
let IS_SAFARI = false;
/**
* Whether or not this is a Windows machine.
*
* @static
* @type {Boolean}
*/
let IS_WINDOWS = false;
/**
* Whether or not this device is an iPad.
*
* @static
* @type {Boolean}
*/
let IS_IPAD = false;
/**
* Whether or not this device is an iPhone.
*
* @static
* @type {Boolean}
*/
// The Facebook app's UIWebView identifies as both an iPhone and iPad, so
// to identify iPhones, we need to exclude iPads.
// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
let IS_IPHONE = false;
/**
* Whether or not this is a Tizen device.
*
* @static
* @type {Boolean}
*/
let IS_TIZEN = false;
/**
* Whether or not this is a WebOS device.
*
* @static
* @type {Boolean}
*/
let IS_WEBOS = false;
/**
* Whether or not this is a Smart TV (Tizen or WebOS) device.
*
* @static
* @type {Boolean}
*/
let IS_SMART_TV = false;
/**
* Whether or not this device is touch-enabled.
*
* @static
* @const
* @type {Boolean}
*/
const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch));
const UAD = window.navigator && window.navigator.userAgentData;
if (UAD && UAD.platform && UAD.brands) {
// If userAgentData is present, use it instead of userAgent to avoid warnings
// Currently only implemented on Chromium
// userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
IS_ANDROID = UAD.platform === 'Android';
IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
IS_CHROME = !IS_EDGE && IS_CHROMIUM;
CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
IS_WINDOWS = UAD.platform === 'Windows';
}
// If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
// or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
// the checks need to be made agiainst the regular userAgent string.
if (!IS_CHROMIUM) {
const USER_AGENT = window.navigator && window.navigator.userAgent || '';
IS_IPOD = /iPod/i.test(USER_AGENT);
IOS_VERSION = function () {
const match = USER_AGENT.match(/OS (\d+)_/i);
if (match && match[1]) {
return match[1];
}
return null;
}();
IS_ANDROID = /Android/i.test(USER_AGENT);
ANDROID_VERSION = function () {
// This matches Android Major.Minor.Patch versions
// ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
if (!match) {
return null;
}
const major = match[1] && parseFloat(match[1]);
const minor = match[2] && parseFloat(match[2]);
if (major && minor) {
return parseFloat(match[1] + '.' + match[2]);
} else if (major) {
return major;
}
return null;
}();
IS_FIREFOX = /Firefox/i.test(USER_AGENT);
IS_EDGE = /Edg/i.test(USER_AGENT);
IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
IS_CHROME = !IS_EDGE && IS_CHROMIUM;
CHROMIUM_VERSION = CHROME_VERSION = function () {
const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
if (match && match[2]) {
return parseFloat(match[2]);
}
return null;
}();
IE_VERSION = function () {
const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
let version = result && parseFloat(result[1]);
if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
// IE 11 has a different user agent string than other IE versions
version = 11.0;
}
return version;
}();
IS_TIZEN = /Tizen/i.test(USER_AGENT);
IS_WEBOS = /Web0S/i.test(USER_AGENT);
IS_SMART_TV = IS_TIZEN || IS_WEBOS;
IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV;
IS_WINDOWS = /Windows/i.test(USER_AGENT);
IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
}
/**
* Whether or not this is an iOS device.
*
* @static
* @const
* @type {Boolean}
*/
const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
/**
* Whether or not this is any flavor of Safari - including iOS.
*
* @static
* @const
* @type {Boolean}
*/
const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
var browser = /*#__PURE__*/Object.freeze({
__proto__: null,
get IS_IPOD () { return IS_IPOD; },
get IOS_VERSION () { return IOS_VERSION; },
get IS_ANDROID () { return IS_ANDROID; },
get ANDROID_VERSION () { return ANDROID_VERSION; },
get IS_FIREFOX () { return IS_FIREFOX; },
get IS_EDGE () { return IS_EDGE; },
get IS_CHROMIUM () { return IS_CHROMIUM; },
get IS_CHROME () { return IS_CHROME; },
get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
get CHROME_VERSION () { return CHROME_VERSION; },
IS_CHROMECAST_RECEIVER: IS_CHROMECAST_RECEIVER,
get IE_VERSION () { return IE_VERSION; },
get IS_SAFARI () { return IS_SAFARI; },
get IS_WINDOWS () { return IS_WINDOWS; },
get IS_IPAD () { return IS_IPAD; },
get IS_IPHONE () { return IS_IPHONE; },
get IS_TIZEN () { return IS_TIZEN; },
get IS_WEBOS () { return IS_WEBOS; },
get IS_SMART_TV () { return IS_SMART_TV; },
TOUCH_ENABLED: TOUCH_ENABLED,
IS_IOS: IS_IOS,
IS_ANY_SAFARI: IS_ANY_SAFARI
});
/**
* @file dom.js
* @module dom
*/
/**
* Detect if a value is a string with any non-whitespace characters.
*
* @private
* @param {string} str
* The string to check
*
* @return {boolean}
* Will be `true` if the string is non-blank, `false` otherwise.
*
*/
function isNonBlankString(str) {
// we use str.trim as it will trim any whitespace characters
// from the front or back of non-whitespace characters. aka
// Any string that contains non-whitespace characters will
// still contain them after `trim` but whitespace only strings
// will have a length of 0, failing this check.
return typeof str === 'string' && Boolean(str.trim());
}
/**
* Throws an error if the passed string has whitespace. This is used by
* class methods to be relatively consistent with the classList API.
*
* @private
* @param {string} str
* The string to check for whitespace.
*
* @throws {Error}
* Throws an error if there is whitespace in the string.
*/
function throwIfWhitespace(str) {
// str.indexOf instead of regex because str.indexOf is faster performance wise.
if (str.indexOf(' ') >= 0) {
throw new Error('class has illegal whitespace characters');
}
}
/**
* Whether the current DOM interface appears to be real (i.e. not simulated).
*
* @return {boolean}
* Will be `true` if the DOM appears to be real, `false` otherwise.
*/
function isReal() {
// Both document and window will never be undefined thanks to `global`.
return document$1 === 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$1[method](null);
}
if (isNonBlankString(context)) {
context = document$1.querySelector(context);
}
const ctx = isEl(context) ? context : document$1;
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$1.createElement(tagName);
Object.getOwnPropertyNames(properties).forEach(function (propName) {
const val = properties[propName];
// Handle textContent since it's not supported everywhere and we have a
// method for it.
if (propName === 'textContent') {
textContent(el, val);
} else if (el[propName] !== val || propName === 'tabIndex') {
el[propName] = val;
}
});
Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
el.setAttribute(attrName, attributes[attrName]);
});
if (content) {
appendContent(el, content);
}
return el;
}
/**
* Injects text into an element, replacing any existing contents entirely.
*
* @param {HTMLElement} el
* The element to add text content into
*
* @param {string} text
* The text content to add.
*
* @return {Element}
* The element with added text content.
*/
function textContent(el, text) {
if (typeof el.textContent === 'undefined') {
el.innerText = text;
} else {
el.textContent = text;
}
return el;
}
/**
* Insert an element as the first child node of another
*
* @param {Element} child
* Element to insert
*
* @param {Element} parent
* Element to insert child into
*/
function prependTo(child, parent) {
if (parent.firstChild) {
parent.insertBefore(child, parent.firstChild);
} else {
parent.appendChild(child);
}
}
/**
* Check if an element has a class name.
*
* @param {Element} element
* Element to check
*
* @param {string} classToCheck
* Class name to check for
*
* @return {boolean}
* Will be `true` if the element has a class, `false` otherwise.
*
* @throws {Error}
* Throws an error if `classToCheck` has white space.
*/
function hasClass(element, classToCheck) {
throwIfWhitespace(classToCheck);
return element.classList.contains(classToCheck);
}
/**
* Add a class name to an element.
*
* @param {Element} element
* Element to add class name to.
*
* @param {...string} classesToAdd
* One or more class name to add.
*
* @return {Element}
* The DOM element with the added class name.
*/
function addClass(element, ...classesToAdd) {
element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
return element;
}
/**
* Remove a class name from an element.
*
* @param {Element} element
* Element to remove a class name from.
*
* @param {...string} classesToRemove
* One or more class name to remove.
*
* @return {Element}
* The DOM element with class name removed.
*/
function removeClass(element, ...classesToRemove) {
// Protect in case the player gets disposed
if (!element) {
log.warn("removeClass was called with an element that doesn't exist");
return null;
}
element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
return element;
}
/**
* The callback definition for toggleClass.
*
* @callback PredicateCallback
* @param {Element} element
* The DOM element of the Component.
*
* @param {string} classToToggle
* The `className` that wants to be toggled
*
* @return {boolean|undefined}
* If `true` is returned, the `classToToggle` will be added to the
* `element`, but not removed. If `false`, the `classToToggle` will be removed from
* the `element`, but not added. If `undefined`, the callback will be ignored.
*
*/
/**
* Adds or removes a class name to/from an element depending on an optional
* condition or the presence/absence of the class name.
*
* @param {Element} element
* The element to toggle a class name on.
*
* @param {string} classToToggle
* The class that should be toggled.
*
* @param {boolean|PredicateCallback} [predicate]
* See the return value for {@link module:dom~PredicateCallback}
*
* @return {Element}
* The element with a class that has been toggled.
*/
function toggleClass(element, classToToggle, predicate) {
if (typeof predicate === 'function') {
predicate = predicate(element, classToToggle);
}
if (typeof predicate !== 'boolean') {
predicate = undefined;
}
classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
return element;
}
/**
* Apply attributes to an HTML element.
*
* @param {Element} el
* Element to add attributes to.
*
* @param {Object} [attributes]
* Attributes to be applied.
*/
function setAttributes(el, attributes) {
Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
const attrValue = attributes[attrName];
if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
el.removeAttribute(attrName);
} else {
el.setAttribute(attrName, attrValue === true ? '' : attrValue);
}
});
}
/**
* Get an element's attribute values, as defined on the HTML tag.
*
* Attributes are not the same as properties. They're defined on the tag
* or with setAttribute.
*
* @param {Element} tag
* Element from which to get tag attributes.
*
* @return {Object}
* All attributes of the element. Boolean attributes will be `true` or
* `false`, others will be strings.
*/
function getAttributes(tag) {
const obj = {};
// known boolean attributes
// we can check for matching boolean properties, but not all browsers
// and not all tags know about these attributes, so, we still want to check them manually
const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
if (tag && tag.attributes && tag.attributes.length > 0) {
const attrs = tag.attributes;
for (let i = attrs.length - 1; i >= 0; i--) {
const attrName = attrs[i].name;
/** @type {boolean|string} */
let attrVal = attrs[i].value;
// check for known booleans
// the matching element property will return a value for typeof
if (knownBooleans.includes(attrName)) {
// the value of an included boolean attribute is typically an empty
// string ('') which would equal false if we just check for a false value.
// we also don't want support bad code like autoplay='false'
attrVal = attrVal !== null ? true : false;
}
obj[attrName] = attrVal;
}
}
return obj;
}
/**
* Get the value of an element's attribute.
*
* @param {Element} el
* A DOM element.
*
* @param {string} attribute
* Attribute to get the value of.
*
* @return {string}
* The value of the attribute.
*/
function getAttribute(el, attribute) {
return el.getAttribute(attribute);
}
/**
* Set the value of an element's attribute.
*
* @param {Element} el
* A DOM element.
*
* @param {string} attribute
* Attribute to set.
*
* @param {string} value
* Value to set the attribute to.
*/
function setAttribute(el, attribute, value) {
el.setAttribute(attribute, value);
}
/**
* Remove an element's attribute.
*
* @param {Element} el
* A DOM element.
*
* @param {string} attribute
* Attribute to remove.
*/
function removeAttribute(el, attribute) {
el.removeAttribute(attribute);
}
/**
* Attempt to block the ability to select text.
*/
function blockTextSelection() {
document$1.body.focus();
document$1.onselectstart = function () {
return false;
};
}
/**
* Turn off text selection blocking.
*/
function unblockTextSelection() {
document$1.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$1[FullscreenApi.fullscreenElement]) {
left += el.offsetLeft;
top += el.offsetTop;
el = el.offsetParent;
}
return {
left,
top,
width,
height
};
}
/**
* Represents x and y coordinates for a DOM element or mouse pointer.
*
* @typedef {Object} module:dom~Coordinates
*
* @property {number} x
* x coordinate in pixels
*
* @property {number} y
* y coordinate in pixels
*/
/**
* Get the pointer position within an element.
*
* The base on the coordinates are the bottom left of the element.
*
* @param {Element} el
* Element on which to get the pointer position on.
*
* @param {Event} event
* Event object.
*
* @return {module:dom~Coordinates}
* A coordinates object corresponding to the mouse position.
*
*/
function getPointerPosition(el, event) {
const translated = {
x: 0,
y: 0
};
if (IS_IOS) {
let item = el;
while (item && item.nodeName.toLowerCase() !== 'html') {
const transform = computedStyle(item, 'transform');
if (/^matrix/.test(transform)) {
const values = transform.slice(7, -1).split(/,\s/).map(Number);
translated.x += values[4];
translated.y += values[5];
} else if (/^matrix3d/.test(transform)) {
const values = transform.slice(9, -1).split(/,\s/).map(Number);
translated.x += values[12];
translated.y += values[13];
}
if (item.assignedSlot && item.assignedSlot.parentElement && window.WebKitCSSMatrix) {
const transformValue = window.getComputedStyle(item.assignedSlot.parentElement).transform;
const matrix = new window.WebKitCSSMatrix(transformValue);
translated.x += matrix.m41;
translated.y += matrix.m42;
}
item = item.parentNode || item.host;
}
}
const position = {};
const boxTarget = findPosition(event.target);
const box = findPosition(el);
const boxW = box.width;
const boxH = box.height;
let offsetY = event.offsetY - (box.top - boxTarget.top);
let offsetX = event.offsetX - (box.left - boxTarget.left);
if (event.changedTouches) {
offsetX = event.changedTouches[0].pageX - box.left;
offsetY = event.changedTouches[0].pageY + box.top;
if (IS_IOS) {
offsetX -= translated.x;
offsetY -= translated.y;
}
}
position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
position.x = Math.max(0, Math.min(1, offsetX / boxW));
return position;
}
/**
* Determines, via duck typing, whether or not a value is a text node.
*
* @param {*} value
* Check if this value is a text node.
*
* @return {boolean}
* Will be `true` if the value is a text node, `false` otherwise.
*/
function isTextNode(value) {
return isObject(value) && value.nodeType === 3;
}
/**
* Empties the contents of an element.
*
* @param {Element} el
* The element to empty children from
*
* @return {Element}
* The element with no children
*/
function emptyEl(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
return el;
}
/**
* This is a mixed value that describes content to be injected into the DOM
* via some method. It can be of the following types:
*
* Type | Description
* -----------|-------------
* `string` | The value will be normalized into a text node.
* `Element` | The value will be accepted as-is.
* `Text` | A TextNode. The value will be accepted as-is.
* `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
* `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
*
* @typedef {string|Element|Text|Array|Function} ContentDescriptor
*/
/**
* Normalizes content for eventual insertion into the DOM.
*
* This allows a wide range of content definition methods, but helps protect
* from falling into the trap of simply writing to `innerHTML`, which could
* be an XSS concern.
*
* The content for an element can be passed in multiple types and
* combinations, whose behavior is as follows:
*
* @param {ContentDescriptor} content
* A content descriptor value.
*
* @return {Array}
* All of the content that was passed in, normalized to an array of
* elements or text nodes.
*/
function normalizeContent(content) {
// First, invoke content if it is a function. If it produces an array,
// that needs to happen before normalization.
if (typeof content === 'function') {
content = content();
}
// Next up, normalize to an array, so one or many items can be normalized,
// filtered, and returned.
return (Array.isArray(content) ? content : [content]).map(value => {
// First, invoke value if it is a function to produce a new value,
// which will be subsequently normalized to a Node of some kind.
if (typeof value === 'function') {
value = value();
}
if (isEl(value) || isTextNode(value)) {
return value;
}
if (typeof value === 'string' && /\S/.test(value)) {
return document$1.createTextNode(value);
}
}).filter(value => value);
}
/**
* Normalizes and appends content to an element.
*
* @param {Element} el
* Element to append normalized content to.
*
* @param {ContentDescriptor} content
* A content descriptor value.
*
* @return {Element}
* The element with appended normalized content.
*/
function appendContent(el, content) {
normalizeContent(content).forEach(node => el.appendChild(node));
return el;
}
/**
* Normalizes and inserts content into an element; this is identical to
* `appendContent()`, except it empties the element first.
*
* @param {Element} el
* Element to insert normalized content into.
*
* @param {ContentDescriptor} content
* A content descriptor value.
*
* @return {Element}
* The element with inserted normalized content.
*/
function insertContent(el, content) {
return appendContent(emptyEl(el), content);
}
/**
* Check if an event was a single left click.
*
* @param {MouseEvent} event
* Event object.
*
* @return {boolean}
* Will be `true` if a single left click, `false` otherwise.
*/
function isSingleLeftClick(event) {
// Note: if you create something draggable, be sure to
// call it on both `mousedown` and `mousemove` event,
// otherwise `mousedown` should be enough for a button
if (event.button === undefined && event.buttons === undefined) {
// Why do we need `buttons` ?
// Because, middle mouse sometimes have this:
// e.button === 0 and e.buttons === 4
// Furthermore, we want to prevent combination click, something like
// HOLD middlemouse then left click, that would be
// e.button === 0, e.buttons === 5
// just `button` is not gonna work
// Alright, then what this block does ?
// this is for chrome `simulate mobile devices`
// I want to support this as well
return true;
}
if (event.button === 0 && event.buttons === undefined) {
// Touch screen, sometimes on some specific device, `buttons`
// doesn't have anything (safari on ios, blackberry...)
return true;
}
// `mouseup` event on a single left click has
// `button` and `buttons` equal to 0
if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
return true;
}
// MacOS Sonoma trackpad when "tap to click enabled"
if (event.type === 'mousedown' && event.button === 0 && event.buttons === 0) {
return true;
}
if (event.button !== 0 || event.buttons !== 1) {
// This is the reason we have those if else block above
// if any special case we can catch and let it slide
// we do it above, when get to here, this definitely
// is-not-left-click
return false;
}
return true;
}
/**
* Finds a single DOM element matching `selector` within the optional
* `context` of another DOM element (defaulting to `document`).
*
* @param {string} selector
* A valid CSS selector, which will be passed to `querySelector`.
*
* @param {Element|String} [context=document]
* A DOM element within which to query. Can also be a selector
* string in which case the first matching element will be used
* as context. If missing (or no element matches selector), falls
* back to `document`.
*
* @return {Element|null}
* The element that was found or null.
*/
const $ = createQuerier('querySelector');
/**
* Finds a all DOM elements matching `selector` within the optional
* `context` of another DOM element (defaulting to `document`).
*
* @param {string} selector
* A valid CSS selector, which will be passed to `querySelectorAll`.
*
* @param {Element|String} [context=document]
* A DOM element within which to query. Can also be a selector
* string in which case the first matching element will be used
* as context. If missing (or no element matches selector), falls
* back to `document`.
*
* @return {NodeList}
* A element list of elements that were found. Will be empty if none
* were found.
*
*/
const $$ = createQuerier('querySelectorAll');
/**
* A safe getComputedStyle.
*
* This is needed because in Firefox, if the player is loaded in an iframe with
* `display:none`, then `getComputedStyle` returns `null`, so, we do a
* null-check to make sure that the player doesn't break in these cases.
*
* @param {Element} el
* The element you want the computed style of
*
* @param {string} prop
* The property name you want
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
*/
function computedStyle(el, prop) {
if (!el || !prop) {
return '';
}
if (typeof window.getComputedStyle === 'function') {
let computedStyleValue;
try {
computedStyleValue = window.getComputedStyle(el);
} catch (e) {
return '';
}
return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
}
return '';
}
/**
* Copy document style sheets to another window.
*
* @param {Window} win
* The window element you want to copy the document style sheets to.
*
*/
function copyStyleSheetsToWindow(win) {
[...document$1.styleSheets].forEach(styleSheet => {
try {
const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
const style = document$1.createElement('style');
style.textContent = cssRules;
win.document.head.appendChild(style);
} catch (e) {
const link = document$1.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,