UNPKG

video.js

Version:

An HTML5 video player that supports HLS and DASH with a common API and skin.

1,774 lines (1,650 loc) 1.85 MB
/** * @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$1 from 'global/window'; import document$1 from 'global/document'; import XHR from '@videojs/xhr'; import vtt from 'videojs-vtt.js'; import _extends from '@babel/runtime/helpers/extends'; import _resolveUrl from '@videojs/vhs-utils/es/resolve-url.js'; import { Parser } from 'm3u8-parser'; import { DEFAULT_VIDEO_CODEC, DEFAULT_AUDIO_CODEC, parseCodecs, muxerSupportsCodec, browserSupportsCodec, translateLegacyCodec, codecsFromDefault, isAudioCodec, getMimeForCodec } from '@videojs/vhs-utils/es/codecs.js'; import { simpleTypeFromSourceType } from '@videojs/vhs-utils/es/media-types.js'; import { isArrayBufferView, concatTypedArrays, stringToBytes, toUint8 } from '@videojs/vhs-utils/es/byte-helpers'; import { generateSidxKey, parseUTCTiming, parse, addSidxSegmentsToPlaylist } from 'mpd-parser'; import parseSidx from 'mux.js/lib/tools/parse-sidx'; import { getId3Offset } from '@videojs/vhs-utils/es/id3-helpers'; import { detectContainerForBytes, isLikelyFmp4MediaSegment } from '@videojs/vhs-utils/es/containers'; import { ONE_SECOND_IN_TS } from 'mux.js/lib/utils/clock'; var version$6 = "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$1.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$1.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$1.console.info || window$1.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$1.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$1 = createLogger$1('VIDEOJS'); const createLogger = log$1.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$1(...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$1(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$1, 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$1.cast && window$1.cast.framework && window$1.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$1 || window$1.navigator.maxTouchPoints || window$1.DocumentTouch && window$1.document instanceof window$1.DocumentTouch)); const UAD = window$1.navigator && window$1.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$1.navigator && window$1.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$1.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$1.parent !== window$1.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$1.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$1.WebKitCSSMatrix) { const transformValue = window$1.getComputedStyle(item.assignedSlot.parentElement).transform; const matrix = new window$1.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$1.getComputedStyle === 'function') { let computedStyleValue; try { computedStyleValue = window$1.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