/** * @license * video.js 7.4.1 * 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(require('global/document'), require('global/window')) : typeof define === 'function' && define.amd ? define(['global/document', 'global/window'], factory) : (global.videojs = factory(global.document,global.window)); }(this, (function (document,window$1) { document = document && document.hasownproperty('default') ? document['default'] : document; window$1 = window$1 && window$1.hasownproperty('default') ? window$1['default'] : window$1; var version = "7.4.1"; function _inheritsloose(subclass, superclass) { subclass.prototype = object.create(superclass.prototype); subclass.prototype.constructor = subclass; subclass.__proto__ = superclass; } function _setprototypeof(o, p) { _setprototypeof = object.setprototypeof || function _setprototypeof(o, p) { o.__proto__ = p; return o; }; return _setprototypeof(o, p); } function isnativereflectconstruct() { if (typeof reflect === "undefined" || !reflect.construct) return false; if (reflect.construct.sham) return false; if (typeof proxy === "function") return true; try { date.prototype.tostring.call(reflect.construct(date, [], function () {})); return true; } catch (e) { return false; } } function _construct(parent, args, class) { if (isnativereflectconstruct()) { _construct = reflect.construct; } else { _construct = function _construct(parent, args, class) { var a = [null]; a.push.apply(a, args); var constructor = function.bind.apply(parent, a); var instance = new constructor(); if (class) _setprototypeof(instance, class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _assertthisinitialized(self) { if (self === void 0) { throw new referenceerror("this hasn't been initialised - super() hasn't been called"); } return self; } function _taggedtemplateliteralloose(strings, raw) { if (!raw) { raw = strings.slice(0); } strings.raw = raw; return strings; } /** * @file create-logger.js * @module create-logger */ var history = []; /** * log messages to the console and history based on the type of message * * @private * @param {string} type * the name of the console method to use. * * @param {array} args * the arguments to be passed to the matching console method. */ var logbytypefactory = function logbytypefactory(name, log) { return function (type, level, args) { var lvl = log.levels[level]; var lvlregexp = new regexp("^(" + lvl + ")$"); if (type !== 'log') { // add the type to the front of the message when it's not "log". args.unshift(type.touppercase() + ':'); } // add console prefix after adding to history. args.unshift(name + ':'); // add a clone of the args at this point to history. if (history) { history.push([].concat(args)); } // 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. var 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(name) { // this is the private tracking variable for logging level. var level = 'info'; // the curried logbytype bound to the specific log and history var 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 * mixed[] * * any combination of values that could be passed to `console.log()`. * * #### return value * * `undefined` * * @namespace * @param {mixed[]} args * one or more messages or objects that should be logged. */ var log = function log() { for (var _len = arguments.length, args = new array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } logbytype('log', level, args); }; // this is the logbytype helper that the logging methods below use logbytype = logbytypefactory(name, log); /** * 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} name * the name to add call the new logger * @return {object} */ log.createlogger = function (subname) { return createlogger(name + ': ' + subname); }; /** * 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 {string} [lvl] * pass a valid level to set a new logging level. * * @return {string} * the current logging level. */ log.level = function (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 = function () { return 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 = function (fname) { return (history || []).filter(function (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 = function () { if (history) { history.length = 0; } }; /** * disable history tracking if it is currently enabled. */ log.history.disable = function () { if (history !== null) { history.length = 0; history = null; } }; /** * enable history tracking if it is currently disabled. */ log.history.enable = function () { if (history === null) { history = []; } }; /** * logs error messages. similar to `console.error`. * * @param {mixed[]} args * one or more messages or objects that should be logged as an error */ log.error = function () { for (var _len2 = arguments.length, args = new array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return logbytype('error', level, args); }; /** * logs warning messages. similar to `console.warn`. * * @param {mixed[]} args * one or more messages or objects that should be logged as a warning. */ log.warn = function () { for (var _len3 = arguments.length, args = new array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } return 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 {mixed[]} args * one or more messages or objects that should be logged as debug. */ log.debug = function () { for (var _len4 = arguments.length, args = new array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } return logbytype('debug', level, args); }; return log; } /** * @file log.js * @module log */ var log = createlogger('videojs'); var createlogger$1 = log.createlogger; function clean(s) { return s.replace(/\n\r?\s*/g, ''); } var tsml = function tsml(sa) { var s = '', i = 0; for (; i < arguments.length; i++) { s += clean(sa[i]) + (arguments[i + 1] || ''); } return s; }; /** * @file obj.js * @module obj */ /** * @callback obj:eachcallback * * @param {mixed} 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 {mixed} accum * the value that is accumulating over the reduce loop. * * @param {mixed} 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 {mixed} * the new accumulated value. */ var 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 */ var keys = function keys(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(function (key) { return 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 {mixed} [initial = 0] * starting value * * @return {mixed} * the final accumulated value. */ function reduce(object, fn, initial) { if (initial === void 0) { initial = 0; } return keys(object).reduce(function (accum, key) { return fn(accum, object[key], key); }, initial); } /** * object.assign-style object shallow merge/extend. * * @param {object} target * @param {object} ...sources * @return {object} */ function assign(target) { for (var _len = arguments.length, sources = new array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { sources[_key - 1] = arguments[_key]; } if (object.assign) { return object.assign.apply(object, [target].concat(sources)); } sources.foreach(function (source) { if (!source) { return; } each(source, function (value, key) { target[key] = value; }); }); return target; } /** * 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; } /** * @file computed-style.js * @module computed-style */ /** * 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. * * @function * @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') { var cs = window$1.getcomputedstyle(el); return cs ? cs[prop] : ''; } return ''; } function _templateobject() { var data = _taggedtemplateliteralloose(["setting attributes in the second argument of createel()\n has been deprecated. use the third argument instead.\n createel(type, properties, attributes). attempting to set ", " to ", "."]); _templateobject = function _templateobject() { return data; }; return data; } /** * 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) { return typeof str === 'string' && /\s/.test(str); } /** * 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) { if (/\s/.test(str)) { throw new error('class has illegal whitespace characters'); } } /** * produce a regular expression for matching a classname within an elements classname. * * @private * @param {string} classname * the classname to generate the regexp for. * * @return {regexp} * the regexp that will check for a specific `classname` in an elements * classname. */ function classregexp(classname) { return new regexp('(^|\\s)' + classname + '($|\\s)'); } /** * 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$1.document; } /** * determines, via duck typing, whether or not a value is a dom element. * * @param {mixed} 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[method](null); } if (isnonblankstring(context)) { context = document.queryselector(context); } var 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 {module:dom~contentdescriptor} content * a content descriptor object. * * @return {element} * the element that was created. */ function createel(tagname, properties, attributes, content) { if (tagname === void 0) { tagname = 'div'; } if (properties === void 0) { properties = {}; } if (attributes === void 0) { attributes = {}; } var el = document.createelement(tagname); object.getownpropertynames(properties).foreach(function (propname) { var val = properties[propname]; // see #2176 // we originally were accepting both properties and attributes in the // same object, but that doesn't work so well. if (propname.indexof('aria-') !== -1 || propname === 'role' || propname === 'type') { log.warn(tsml(_templateobject(), propname, val)); el.setattribute(propname, val); // handle textcontent since it's not supported everywhere and we have a // method for it. } else if (propname === 'textcontent') { textcontent(el, val); } else { 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 {element} 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); if (element.classlist) { return element.classlist.contains(classtocheck); } return classregexp(classtocheck).test(element.classname); } /** * add a class name to an element. * * @param {element} element * element to add class name to. * * @param {string} classtoadd * class name to add. * * @return {element} * the dom element with the added class name. */ function addclass(element, classtoadd) { if (element.classlist) { element.classlist.add(classtoadd); // don't need to `throwifwhitespace` here because `haselclass` will do it // in the case of classlist not being supported. } else if (!hasclass(element, classtoadd)) { element.classname = (element.classname + ' ' + classtoadd).trim(); } return element; } /** * remove a class name from an element. * * @param {element} element * element to remove a class name from. * * @param {string} classtoremove * class name to remove * * @return {element} * the dom element with class name removed. */ function removeclass(element, classtoremove) { if (element.classlist) { element.classlist.remove(classtoremove); } else { throwifwhitespace(classtoremove); element.classname = element.classname.split(/\s+/).filter(function (c) { return c !== classtoremove; }).join(' '); } 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) { // this cannot use `classlist` internally because ie11 does not support the // second parameter to the `classlist.toggle()` method! which is fine because // `classlist` will be used by the add/remove functions. var has = hasclass(element, classtotoggle); if (typeof predicate === 'function') { predicate = predicate(element, classtotoggle); } if (typeof predicate !== 'boolean') { predicate = !has; } // if the necessary class operation matches the current state of the // element, no action is required. if (predicate === has) { return; } if (predicate) { addclass(element, classtotoggle); } else { removeclass(element, classtotoggle); } 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) { var 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) { var 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 var knownbooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultmuted' + ','; if (tag && tag.attributes && tag.attributes.length > 0) { var attrs = tag.attributes; for (var i = attrs.length - 1; i >= 0; i--) { var attrname = attrs[i].name; var attrval = attrs[i].value; // check for known booleans // the matching element property will return a value for typeof if (typeof tag[attrname] === 'boolean' || knownbooleans.indexof(',' + attrname + ',') !== -1) { // 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) { var rect = el.getboundingclientrect(); var result = {}; ['bottom', 'height', 'left', 'right', 'top', 'width'].foreach(function (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) { var box; if (el.getboundingclientrect && el.parentnode) { box = el.getboundingclientrect(); } if (!box) { return { left: 0, top: 0 }; } var docel = document.documentelement; var body = document.body; var clientleft = docel.clientleft || body.clientleft || 0; var scrollleft = window$1.pagexoffset || body.scrollleft; var left = box.left + scrollleft - clientleft; var clienttop = docel.clienttop || body.clienttop || 0; var scrolltop = window$1.pageyoffset || body.scrolltop; var top = box.top + scrolltop - clienttop; // android sometimes returns slightly off decimal values, so need to round return { left: math.round(left), top: math.round(top) }; } /** * 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 {eventtarget~event} event * event object. * * @return {module:dom~coordinates} * a coordinates object corresponding to the mouse position. * */ function getpointerposition(el, event) { var position = {}; var box = findposition(el); var boxw = el.offsetwidth; var boxh = el.offsetheight; var boxy = box.top; var boxx = box.left; var pagey = event.pagey; var pagex = event.pagex; if (event.changedtouches) { pagex = event.changedtouches[0].pagex; pagey = event.changedtouches[0].pagey; } position.y = math.max(0, math.min(1, (boxy - pagey + boxh) / boxh)); position.x = math.max(0, math.min(1, (pagex - boxx) / boxw)); return position; } /** * determines, via duck typing, whether or not a value is a text node. * * @param {mixed} 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. * `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|textnode|array|function} module:dom~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 {module:dom~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(function (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(function (value) { return value; }); } /** * normalizes and appends content to an element. * * @param {element} el * element to append normalized content to. * * @param {module:dom~contentdescriptor} content * a content descriptor value. * * @return {element} * the element with appended normalized content. */ function appendcontent(el, content) { normalizecontent(content).foreach(function (node) { return 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 {module:dom~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 {eventtarget~event} 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; } 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. */ var $ = 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. * */ var $$ = createquerier('queryselectorall'); var dom = /*#__pure__*/object.freeze({ 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, $: $, $$: $$ }); /** * @file guid.js * @module guid */ /** * unique id for an element or function * @type {number} */ var _guid = 1; /** * 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 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 eldata = {}; /* * unique attribute name to store an element's guid in * * @type {string} * @constant * @private */ var elidattr = 'vdata' + new date().gettime(); /** * returns the cache object where data for an element is stored * * @param {element} el * element to store data for. * * @return {object} * the cache object for that el that was passed in. */ function getdata(el) { var id = el[elidattr]; if (!id) { id = el[elidattr] = newguid(); } if (!eldata[id]) { eldata[id] = {}; } return eldata[id]; } /** * returns whether or not an element has cached data * * @param {element} el * check if this element has cached data. * * @return {boolean} * - true if the dom element has cached data. * - false otherwise. */ function hasdata(el) { var id = el[elidattr]; if (!id) { return false; } return !!object.getownpropertynames(eldata[id]).length; } /** * delete data for the element from the cache and the guid attr from getelementbyid * * @param {element} el * remove cached data for this element. */ function removedata(el) { var id = el[elidattr]; if (!id) { return; } // remove all stored data delete eldata[id]; // remove the elidattr property from the dom node try { delete el[elidattr]; } catch (e) { if (el.removeattribute) { el.removeattribute(elidattr); } else { // ie doesn't appear to support removeattribute on the document element el[elidattr] = null; } } } /** * @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) { var data = getdata(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) { removedata(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} type * type of event to bind to. * * @param {eventtarget~eventlistener} 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) { 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) { var old = event || window$1.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 (var 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 if (key !== 'layerx' && key !== 'layery' && key !== 'keylocation' && key !== 'webkitmovementx' && key !== 'webkitmovementy') { // 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) { var doc = document.documentelement; var 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 */ } } // returns fixed-up instance return event; } /** * whether passive event listeners are supported */ var _supportspassive = false; (function () { try { var opts = object.defineproperty({}, 'passive', { get: function get() { _supportspassive = true; } }); window$1.addeventlistener('test', null, opts); window$1.removeeventlistener('test', null, opts); } catch (e) {// disregard } })(); /** * touch events chrome expects to be passive */ var 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 {eventtarget~eventlistener} fn * event listener. */ function on(elem, type, fn) { if (array.isarray(type)) { return _handlemultipleevents(on, elem, type, fn); } var data = getdata(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); var handlers = data.handlers[event.type]; if (handlers) { // copy handlers so if handlers are added/removed during the process it doesn't throw everything off. var handlerscopy = handlers.slice(0); for (var 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) { var 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 {eventtarget~eventlistener} [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 (!hasdata(elem)) { return; } var data = getdata(elem); // if no events exist, nothing to unbind if (!data.handlers) { return; } if (array.isarray(type)) { return _handlemultipleevents(off, elem, type, fn); } // utility function var removetype = function removetype(el, t) { data.handlers[t] = []; _cleanupevents(el, t); }; // are we removing all bound events? if (type === undefined) { for (var t in data.handlers) { if (object.prototype.hasownproperty.call(data.handlers || {}, t)) { removetype(elem, t); } } return; } var 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 (var 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. var elemdata = hasdata(elem) ? getdata(elem) : {}; var 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) { var targetdata = getdata(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); } var func = function func() { 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); } var events = /*#__pure__*/object.freeze({ fixevent: fixevent, on: on, off: off, trigger: trigger, one: one }); /** * @file setup.js - functions for setting up a player without * user interaction based on the data-setup `attribute` of the video tag. * * @module setup */ var _windowloaded = false; var videojs; /** * set up any tags that have a data-setup `attribute` when the player is started. */ var autosetup = function autosetup() { // protect against breakage in non-browser environments and check global autosetup option. if (!isreal() || videojs.options.autosetup === false) { return; } var vids = array.prototype.slice.call(document.getelementsbytagname('video')); var audios = array.prototype.slice.call(document.getelementsbytagname('audio')); var divs = array.prototype.slice.call(document.getelementsbytagname('video-js')); var mediaels = vids.concat(audios, divs); // check if any media elements exist if (mediaels && mediaels.length > 0) { for (var i = 0, e = mediaels.length; i < e; i++) { var 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) { var 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(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) { if (vjs) { videojs = vjs; } window$1.settimeout(autosetup, wait); } if (isreal() && document.readystate === 'complete') { _windowloaded = true; } else { /** * listen for the load event on window, and set _windowloaded to true. * * @listens load */ one(window$1, 'load', function () { _windowloaded = true; }); } /** * @file stylesheet.js * @module stylesheet */ /** * create a dom syle 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. */ var createstyleelement = function createstyleelement(classname) { var 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. */ var settextcontent = function settextcontent(el, content) { if (el.stylesheet) { el.stylesheet.csstext = content; } else { el.textcontent = content; } }; /** * @file fn.js * @module fn */ /** * bind (a.k.a proxy or context). a simple method for changing the context of * a function. * * it also stores a unique id on the function so it can be easily removed from * events. * * @function * @param {mixed} 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 */ var bind = function bind(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 var bound = function bound() { return fn.apply(context, arguments); }; // 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} */ var throttle = function throttle(fn, wait) { var last = date.now(); var throttled = function throttled() { var now = date.now(); if (now - last >= wait) { fn.apply(void 0, arguments); 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. */ var debounce = function debounce(func, wait, immediate, context) { if (context === void 0) { context = window$1; } var timeout; var cancel = function cancel() { context.cleartimeout(timeout); timeout = null; }; /* eslint-disable consistent-this */ var debounced = function debounced() { var self = this; var args = arguments; var _later = function later() { 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; }; /** * @file src/js/event-target.js */ /** * `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 */ var eventtarget = function eventtarget() {}; /** * a custom dom event. * * @typedef {object} eventtarget~event * @see [properties]{@link https://developer.mozilla.org/en-us/docs/web/api/customevent} */ /** * all event listeners should follow the following format. * * @callback eventtarget~eventlistener * @this {eventtarget} * * @param {eventtarget~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_ * @private */ eventtarget.prototype.allowedevents_ = {}; /** * 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 {eventtarget~eventlistener} fn * the function to call with `eventtarget`s */ eventtarget.prototype.on = function (type, fn) { // remove the addeventlistener alias before calling events.on // so we don't get into an infinite type loop var ael = this.addeventlistener; this.addeventlistener = function () {}; on(this, type, fn); this.addeventlistener = ael; }; /** * 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; /** * 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 {eventtarget~eventlistener} fn * the function to remove. */ eventtarget.prototype.off = function (type, fn) { off(this, type, fn); }; /** * 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; /** * 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 {eventtarget~eventlistener} fn * the function to be called once for each event name. */ eventtarget.prototype.one = function (type, fn) { // remove the addeventlistener alialing events.on // so we don't get into an infinite type loop var ael = this.addeventlistener; this.addeventlistener = function () {}; one(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. */ eventtarget.prototype.trigger = function (event) { var type = event.type || event; if (typeof event === 'string') { event = { type: type }; } event = fixevent(event); if (this.allowedevents_[type] && this['on' + type]) { this['on' + type](event); } trigger(this, event); }; /** * 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; var event_map; eventtarget.prototype.queuetrigger = function (event) { var _this = this; // only set up event_map if it'll be used if (!event_map) { event_map = new map(); } var type = event.type || event; var map = event_map.get(this); if (!map) { map = new map(); event_map.set(this, map); } var oldtimeout = map.get(type); map.delete(type); window$1.cleartimeout(oldtimeout); var timeout = window$1.settimeout(function () { // 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); }; /** * @file mixins/evented.js * @module evented */ /** * 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. */ var isevented = function isevented(object) { return object instanceof eventtarget || !!object.eventbusel_ && ['on', 'one', 'off', 'trigger'].every(function (k) { return typeof object[k] === 'function'; }); }; /** * adds a callback to run after the evented mixin applied. * * @param {object} object * an object to add * @param {function} callback * the callback to run. */ var addeventedcallback = function 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. */ var isvalideventtype = function isvalideventtype(type) { return (// 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. */ var validatetarget = function validatetarget(target) { if (!target.nodename && !isevented(target)) { throw new error('invalid target; 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. */ var validateeventtype = function validateeventtype(type) { if (!isvalideventtype(type)) { throw new error('invalid event type; 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. */ var validatelistener = function validatelistener(listener) { if (typeof listener !== 'function') { throw new error('invalid listener; 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()`. * * @return {object} * an object containing useful values for `on()` or `one()` calls. */ var normalizelistenargs = function normalizelistenargs(self, args) { // if the number of arguments is less than 3, the target is always the // evented object itself. var istargetingself = args.length < 3 || args[0] === self || args[0] === self.eventbusel_; var target; var type; var 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 = args[0]; listener = args[1]; } else { target = args[0]; type = args[1]; listener = args[2]; } validatetarget(target); validateeventtype(type); validatelistener(listener); listener = bind(self, listener); return { istargetingself: istargetingself, target: target, type: type, listener: 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. */ var listen = function listen(target, method, type, listener) { validatetarget(target); 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 */ var 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: function on$$1() { var _this = this; for (var _len = arguments.length, args = new array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var _normalizelistenargs = normalizelistenargs(this, args), istargetingself = _normalizelistenargs.istargetingself, target = _normalizelistenargs.target, type = _normalizelistenargs.type, listener = _normalizelistenargs.listener; listen(target, 'on', type, listener); // if this object is listening to another evented object. if (!istargetingself) { // if this object is disposed, remove the listener. var removelistenerondispose = function removelistenerondispose() { return _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. var removeremoverontargetdispose = function removeremoverontargetdispose() { return _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 only be called once 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: function one$$1() { var _this2 = this; for (var _len2 = arguments.length, args = new array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } var _normalizelistenargs2 = normalizelistenargs(this, args), istargetingself = _normalizelistenargs2.istargetingself, target = _normalizelistenargs2.target, type = _normalizelistenargs2.type, listener = _normalizelistenargs2.listener; // targeting this evented object. if (istargetingself) { listen(target, 'one', type, listener); // targeting another evented object. } else { var wrapper = function wrapper() { _this2.off(target, type, wrapper); for (var _len3 = arguments.length, largs = new array(_len3), _key3 = 0; _key3 < _len3; _key3++) { largs[_key3] = arguments[_key3]; } 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); } }, /** * 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: function off$$1(targetortype, typeorlistener, listener) { // targeting this evented object. if (!targetortype || isvalideventtype(targetortype)) { off(this.eventbusel_, targetortype, typeorlistener); // targeting another evented object. } else { var target = targetortype; var type = typeorlistener; // fail fast and in a meaningful way! validatetarget(target); validateeventtype(type); validatelistener(listener); // 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: function trigger$$1(event, hash) { 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) { if (options === void 0) { options = {}; } var _options = options, eventbuskey = _options.eventbuskey; // 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' }); } assign(target, eventedmixin); if (target.eventedcallbacks) { target.eventedcallbacks.foreach(function (callback) { callback(); }); } // when any evented object is disposed, it removes all its listeners. target.on('dispose', function () { target.off(); window$1.settimeout(function () { 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 */ var 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: function setstate(stateupdates) { var _this = this; // support providing the `stateupdates` state as a function. if (typeof stateupdates === 'function') { stateupdates = stateupdates(); } var changes; each(stateupdates, function (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: 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) { assign(target, statefulmixin); // this happens after the mixing-in because we need to replace the `state` // added in that step. target.state = 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 to-title-case.js * @module to-title-case */ /** * uppercase the first letter of a string. * * @param {string} string * string to be uppercased * * @return {string} * the string with an uppercased first letter */ function totitlecase(string) { if (typeof string !== 'string') { return string; } return string.charat(0).touppercase() + string.slice(1); } /** * 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 */ function titlecaseequals(str1, str2) { return totitlecase(str1) === totitlecase(str2); } /** * @file merge-options.js * @module merge-options */ /** * 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. * * @static * @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 mergeoptions() { var result = {}; for (var _len = arguments.length, sources = new array(_len), _key = 0; _key < _len; _key++) { sources[_key] = arguments[_key]; } sources.foreach(function (source) { if (!source) { return; } each(source, function (value, key) { if (!isplain(value)) { result[key] = value; return; } if (!isplain(result[key])) { result[key] = {}; } result[key] = mergeoptions(result[key], value); }); }); return result; } /** * 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} */ var component = /*#__pure__*/ function () { /** * a callback that is called when a component is ready. does not have any * paramters and any callback value will be ignored. * * @callback component~readycallback * @this component */ /** * creates an instance of this class. * * @param {player} player * the `player` that this class should be attached to. * * @param {object} [options] * the key/value store of player options. * * @param {object[]} [options.children] * an array of children objects to intialize 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 {component~readycallback} [ready] * function that gets called when the `component` is ready. */ function component(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; } // make a copy of prototype.options_ to protect against overriding defaults this.options_ = mergeoptions({}, this.options_); // updated options with supplied options options = this.options_ = mergeoptions(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 var 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 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 }); } stateful(this, this.constructor.defaultstate); this.children_ = []; this.childindex_ = {}; this.childnameindex_ = {}; // add any child components in options if (options.initchildren !== false) { this.initchildren(); } this.ready(ready); // don't want to trigger ready here or it will before init is actually // finished for all children that run this constructor if (options.reporttouchactivity !== false) { this.enabletouchactivity(); } } /** * dispose of the `component` and all child components. * * @fires component#dispose */ var _proto = component.prototype; _proto.dispose = function dispose() { /** * triggered when a `component` is disposed. * * @event component#dispose * @type {eventtarget~event} * * @property {boolean} [bubbles=false] * set to false so that the close event does not * bubble up */ this.trigger({ type: 'dispose', bubbles: false }); // dispose all children. if (this.children_) { for (var 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; if (this.el_) { // remove element from dom if (this.el_.parentnode) { this.el_.parentnode.removechild(this.el_); } removedata(this.el_); this.el_ = null; } // remove reference to the player after disposing of the element this.player_ = null; }; /** * return the {@link player} that the `component` has attached to. * * @return {player} * the player that this `component` has attached to. */ _proto.player = function 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:mergeoptions} * * @param {object} obj * the object that contains new options. * * @return {object} * a new object of `this.options_` and `obj` merged together. * * @deprecated since version 5 */ _proto.options = function options(obj) { log.warn('this.options() has been deprecated and will be moved to the constructor in 6.0'); if (!obj) { return this.options_; } this.options_ = mergeoptions(this.options_, obj); return this.options_; }; /** * get the `component`s dom element * * @return {element} * the dom element for this `component`. */ _proto.el = function 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. */ _proto.createel = function createel$$1(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. */ _proto.localize = function localize(string, tokens, defaultvalue) { if (defaultvalue === void 0) { defaultvalue = string; } var code = this.player_.language && this.player_.language(); var languages = this.player_.languages && this.player_.languages(); var language = languages && languages[code]; var primarycode = code && code.split('-')[0]; var primarylang = languages && languages[primarycode]; var 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) { var value = tokens[index - 1]; var ret = value; if (typeof value === 'undefined') { ret = match; } return ret; }); } return localizedstring; }; /** * 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`. */ _proto.contentel = function contentel() { return this.contentel_ || this.el_; }; /** * get this `component`s id * * @return {string} * the id of this `component` */ _proto.id = function 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`. */ _proto.name = function name() { return this.name_; }; /** * get an array of all child components * * @return {array} * the children */ _proto.children = function 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. */ _proto.getchildbyid = function 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. */ _proto.getchild = function getchild(name) { if (!name) { return; } name = totitlecase(name); return this.childnameindex_[name]; }; /** * 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. */ _proto.addchild = function addchild(child, options, index) { if (options === void 0) { options = {}; } if (index === void 0) { index = this.children_.length; } var component; var componentname; // if child is a string, create component with options if (typeof child === 'string') { componentname = totitlecase(child); var 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 var 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; } this.children_.splice(index, 0, component); 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; } // add the ui object's element to the container div (box) // having an element is not required if (typeof component.el === 'function' && component.el()) { var childnodes = this.contentel().children; var refnode = childnodes[index] || null; 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. */ _proto.removechild = function removechild(component) { if (typeof component === 'string') { component = this.getchild(component); } if (!component || !this.children_) { return; } var childfound = false; for (var i = this.children_.length - 1; i >= 0; i--) { if (this.children_[i] === component) { childfound = true; this.children_.splice(i, 1); break; } } if (!childfound) { return; } this.childindex_[component.id()] = null; this.childnameindex_[component.name()] = null; var compel = component.el(); if (compel && compel.parentnode === this.contentel()) { this.contentel().removechild(component.el()); } }; /** * add and initialize default child `component`s based upon options. */ _proto.initchildren = function initchildren() { var _this = this; var children = this.options_.children; if (children) { // `this` is `parent` var parentoptions = this.options_; var handleadd = function handleadd(child) { var name = child.name; var 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 var newchild = _this.addchild(name, opts); if (newchild) { _this[name] = newchild; } }; // allow for an array of children details to passed in the options var workingchildren; var 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(function (child) { var name; var opts; if (typeof child === 'string') { name = child; opts = children[name] || _this.options_[name] || {}; } else { name = child.name; opts = child; } return { name: name, opts: opts }; }).filter(function (child) { // we have to make sure that child.name isn't in the techorder since // techs are registerd as components but can't aren't compatible // see https://github.com/videojs/video.js/issues/2772 var c = component.getcomponent(child.opts.componentclass || totitlecase(child.name)); return c && !tech.istech(c); }).foreach(handleadd); } }; /** * builds the default dom class name. should be overriden by sub-components. * * @return {string} * the dom class name for this object. * * @abstract */ _proto.buildcssclass = function 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. * * @return {component} * returns itself; method can be chained. */ _proto.ready = function ready(fn, sync) { if (sync === void 0) { 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 */ _proto.triggerready = function triggerready() { this.isready_ = true; // ensure ready is triggered asynchronously this.settimeout(function () { var 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 {eventtarget~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) */ _proto.$ = function $$$1(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) */ _proto.$$ = function $$$$1(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` */ _proto.hasclass = function hasclass$$1(classtocheck) { return hasclass(this.el_, classtocheck); }; /** * add a css class name to the `component`s element. * * @param {string} classtoadd * css class name to add */ _proto.addclass = function addclass$$1(classtoadd) { addclass(this.el_, classtoadd); }; /** * remove a css class name from the `component`s element. * * @param {string} classtoremove * css class name to remove */ _proto.removeclass = function removeclass$$1(classtoremove) { removeclass(this.el_, classtoremove); }; /** * 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 */ _proto.toggleclass = function toggleclass$$1(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. */ _proto.show = function show() { this.removeclass('vjs-hidden'); }; /** * hide the `component`s element if it is currently showing by adding the * 'vjs-hidden` class name to it. */ _proto.hide = function 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 */ _proto.lockshowing = function 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 */ _proto.unlockshowing = function 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 attibute does not exist or has * no value. * * @see [dom api]{@link https://developer.mozilla.org/en-us/docs/web/api/element/getattribute} */ _proto.getattribute = function getattribute$$1(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} */ _proto.setattribute = function setattribute$$1(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} */ _proto.removeattribute = function removeattribute$$1(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|string} * the width when getting, zero if there is no width. can be a string * postpixed with '%' or 'px'. */ _proto.width = function 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|string} * the width when getting, zero if there is no width. can be a string * postpixed with '%' or 'px'. */ _proto.height = function 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. */ _proto.dimensions = function 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} * the dimension when getting or 0 if unset */ _proto.dimension = function 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 {eventtarget~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 var val = this.el_.style[widthorheight]; var 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. */ _proto.currentdimension = function currentdimension(widthorheight) { var computedwidthorheight = 0; if (widthorheight !== 'width' && widthorheight !== 'height') { throw new error('currentdimension only accepts width or height value'); } if (typeof window$1.getcomputedstyle === 'function') { var computedstyle = window$1.getcomputedstyle(this.el_); computedwidthorheight = computedstyle.getpropertyvalue(widthorheight) || computedstyle[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) { var 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. */ _proto.currentdimensions = function 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. */ _proto.currentwidth = function 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. */ _proto.currentheight = function currentheight() { return this.currentdimension('height'); }; /** * set the focus to this component */ _proto.focus = function focus() { this.el_.focus(); }; /** * remove the focus from this component */ _proto.blur = function blur() { this.el_.blur(); }; /** * 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. * * @private * @fires component#tap * @listens component#touchstart * @listens component#touchmove * @listens component#touchleave * @listens component#touchcancel * @listens component#touchend */ _proto.emittapevents = function emittapevents() { // track the start time so we can determine how long the touch lasted var touchstart = 0; var 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. var tapmovementthreshold = 10; // the maximum length a touch can be while still being considered a tap var touchtimethreshold = 200; var 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 = new date().gettime(); // 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 var xdiff = event.touches[0].pagex - firsttouch.pagex; var ydiff = event.touches[0].pagey - firsttouch.pagey; var touchdistance = math.sqrt(xdiff * xdiff + ydiff * ydiff); if (touchdistance > tapmovementthreshold) { couldbetap = false; } } }); var notap = function notap() { 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 var touchtime = new date().gettime() - 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 {eventtarget~event} */ 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 */ _proto.enabletouchactivity = function 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 var report = bind(this.player(), this.player().reportuseractivity); var 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); }); var touchend = function touchend(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} */ _proto.settimeout = function settimeout(fn, timeout) { var _this2 = this; // declare as variables so they are properly available in timeout function // eslint-disable-next-line var timeoutid, disposefn; fn = bind(this, fn); timeoutid = window$1.settimeout(function () { _this2.off('dispose', disposefn); fn(); }, timeout); disposefn = function disposefn() { return _this2.cleartimeout(timeoutid); }; disposefn.guid = "vjs-timeout-" + timeoutid; this.on('dispose', disposefn); 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} */ _proto.cleartimeout = function cleartimeout(timeoutid) { window$1.cleartimeout(timeoutid); var disposefn = function disposefn() {}; disposefn.guid = "vjs-timeout-" + timeoutid; this.off('dispose', disposefn); 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} */ _proto.setinterval = function setinterval(fn, interval) { var _this3 = this; fn = bind(this, fn); var intervalid = window$1.setinterval(fn, interval); var disposefn = function disposefn() { return _this3.clearinterval(intervalid); }; disposefn.guid = "vjs-interval-" + intervalid; this.on('dispose', disposefn); return intervalid; }; /** * clears an interval that gets created via `window.setinterval` or * {@link component#setinterval}. if you set an inteval 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} */ _proto.clearinterval = function clearinterval(intervalid) { window$1.clearinterval(intervalid); var disposefn = function disposefn() {}; disposefn.guid = "vjs-interval-" + intervalid; this.off('dispose', disposefn); 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} */ _proto.requestanimationframe = function requestanimationframe(fn) { var _this4 = this; // declare as variables so they are properly available in raf function // eslint-disable-next-line var id, disposefn; if (this.supportsraf_) { fn = bind(this, fn); id = window$1.requestanimationframe(function () { _this4.off('dispose', disposefn); fn(); }); disposefn = function disposefn() { return _this4.cancelanimationframe(id); }; disposefn.guid = "vjs-raf-" + id; this.on('dispose', disposefn); return id; } // fall back to using a timer. return this.settimeout(fn, 1000 / 60); }; /** * 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} */ _proto.cancelanimationframe = function cancelanimationframe(id) { if (this.supportsraf_) { window$1.cancelanimationframe(id); var disposefn = function disposefn() {}; disposefn.guid = "vjs-raf-" + id; this.off('dispose', disposefn); return id; } // fall back to using a timer. return this.cleartimeout(id); }; /** * 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. */ component.registercomponent = function registercomponent(name, componenttoregister) { if (typeof name !== 'string' || !name) { throw new error("illegal component name, \"" + name + "\"; must be a non-empty string."); } var tech = component.getcomponent('tech'); // we need to make sure this check is only done if tech has been registered. var istech = tech && tech.istech(componenttoregister); var iscomp = component === componenttoregister || component.prototype.isprototypeof(componenttoregister.prototype); if (istech || !iscomp) { var 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_ = {}; } var player = component.getcomponent('player'); if (name === 'player' && player && player.players) { var players = player.players; var 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(function (pname) { return players[pname]; }).every(boolean)) { throw new error('can not register player component after player has been created.'); } } component.components_[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 {component} * the `component` that got registered under the given name. * * @deprecated in `videojs` 6 this will not return `component`s that were not * registered using {@link component.registercomponent}. currently we * check the global `videojs` object for a `component` name and * return that if it exists. */ component.getcomponent = function getcomponent(name) { if (!name) { return; } name = totitlecase(name); if (component.components_ && component.components_[name]) { return component.components_[name]; } }; return component; }(); /** * whether or not this component supports `requestanimationframe`. * * this is exposed primarily for testing purposes. * * @private * @type {boolean} */ component.prototype.supportsraf_ = typeof window$1.requestanimationframe === 'function' && typeof window$1.cancelanimationframe === 'function'; component.registercomponent('component', component); /** * @file browser.js * @module browser */ var user_agent = window$1.navigator && window$1.navigator.useragent || ''; var webkitversionmap = /applewebkit\/([\d.]+)/i.exec(user_agent); var applewebkitversion = webkitversionmap ? parsefloat(webkitversionmap.pop()) : null; /** * whether or not this device is an ipad. * * @static * @const * @type {boolean} */ var is_ipad = /ipad/i.test(user_agent); /** * whether or not this device is an iphone. * * @static * @const * @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/ var is_iphone = /iphone/i.test(user_agent) && !is_ipad; /** * whether or not this device is an ipod. * * @static * @const * @type {boolean} */ var is_ipod = /ipod/i.test(user_agent); /** * whether or not this is an ios device. * * @static * @const * @type {boolean} */ var is_ios = is_iphone || is_ipad || is_ipod; /** * the detected ios version - or `null`. * * @static * @const * @type {string|null} */ var ios_version = function () { var match = user_agent.match(/os (\d+)_/i); if (match && match[1]) { return match[1]; } return null; }(); /** * whether or not this is an android device. * * @static * @const * @type {boolean} */ var is_android = /android/i.test(user_agent); /** * the detected android version - or `null`. * * @static * @const * @type {number|string|null} */ var 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 var match = user_agent.match(/android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); if (!match) { return null; } var major = match[1] && parsefloat(match[1]); var minor = match[2] && parsefloat(match[2]); if (major && minor) { return parsefloat(match[1] + '.' + match[2]); } else if (major) { return major; } return null; }(); /** * whether or not this is a native android browser. * * @static * @const * @type {boolean} */ var is_native_android = is_android && android_version < 5 && applewebkitversion < 537; /** * whether or not this is mozilla firefox. * * @static * @const * @type {boolean} */ var is_firefox = /firefox/i.test(user_agent); /** * whether or not this is microsoft edge. * * @static * @const * @type {boolean} */ var is_edge = /edge/i.test(user_agent); /** * whether or not this is google chrome. * * this will also be `true` for chrome on ios, which will have different support * as it is actually safari under the hood. * * @static * @const * @type {boolean} */ var is_chrome = !is_edge && (/chrome/i.test(user_agent) || /crios/i.test(user_agent)); /** * the detected google chrome version - or `null`. * * @static * @const * @type {number|null} */ var chrome_version = function () { var match = user_agent.match(/(chrome|crios)\/(\d+)/); if (match && match[2]) { return parsefloat(match[2]); } return null; }(); /** * the detected internet explorer version - or `null`. * * @static * @const * @type {number|null} */ var ie_version = function () { var result = /msie\s(\d+)\.\d/.exec(user_agent); var 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; }(); /** * whether or not this is desktop safari. * * @static * @const * @type {boolean} */ var is_safari = /safari/i.test(user_agent) && !is_chrome && !is_android && !is_edge; /** * whether or not this is any flavor of safari - including ios. * * @static * @const * @type {boolean} */ var is_any_safari = (is_safari || is_ios) && !is_chrome; /** * whether or not this device is touch-enabled. * * @static * @const * @type {boolean} */ var touch_enabled = isreal() && ('ontouchstart' in window$1 || window$1.navigator.maxtouchpoints || window$1.documenttouch && window$1.document instanceof window$1.documenttouch); var browser = /*#__pure__*/object.freeze({ is_ipad: is_ipad, is_iphone: is_iphone, is_ipod: is_ipod, is_ios: is_ios, ios_version: ios_version, is_android: is_android, android_version: android_version, is_native_android: is_native_android, is_firefox: is_firefox, is_edge: is_edge, is_chrome: is_chrome, chrome_version: chrome_version, ie_version: ie_version, is_safari: is_safari, is_any_safari: is_any_safari, touch_enabled: touch_enabled }); /** * @file time-ranges.js * @module time-ranges */ /** * 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. * * @typedef {object} timerange * * @property {number} length * the number of time ranges represented by this object. * * @property {module:time-ranges~timerangeindex} start * returns the time offset at which a specified time range begins. * * @property {module:time-ranges~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. */ function createtimerangesobj(ranges) { if (ranges === undefined || ranges.length === 0) { return { length: 0, start: function start() { throw new error('this timeranges object is empty'); }, end: function end() { throw new error('this timeranges object is empty'); } }; } return { length: ranges.length, start: getrange.bind(null, 'start', 0, ranges), end: getrange.bind(null, 'end', 1, ranges) }; } /** * 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. */ function createtimeranges(start, end) { if (array.isarray(start)) { return createtimerangesobj(start); } else if (start === undefined || end === undefined) { return createtimerangesobj(); } return createtimerangesobj([[start, end]]); } /** * @file buffer.js * @module buffer */ /** * compute the percentage of the media that has been buffered. * * @param {timerange} buffered * the current `timerange` 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) { var bufferedduration = 0; var start; var end; if (!duration) { return 0; } if (!buffered || !buffered.length) { buffered = createtimeranges(0, 0); } for (var 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 fullscreen-api.js * @module fullscreen-api * @private */ /** * 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} */ var fullscreenapi = {}; // browser api methods var apimap = [['requestfullscreen', 'exitfullscreen', 'fullscreenelement', 'fullscreenenabled', 'fullscreenchange', 'fullscreenerror'], // webkit ['webkitrequestfullscreen', 'webkitexitfullscreen', 'webkitfullscreenelement', 'webkitfullscreenenabled', 'webkitfullscreenchange', 'webkitfullscreenerror'], // old webkit (safari 5.1) ['webkitrequestfullscreen', 'webkitcancelfullscreen', 'webkitcurrentfullscreenelement', 'webkitcancelfullscreen', 'webkitfullscreenchange', 'webkitfullscreenerror'], // mozilla ['mozrequestfullscreen', 'mozcancelfullscreen', 'mozfullscreenelement', 'mozfullscreenenabled', 'mozfullscreenchange', 'mozfullscreenerror'], // microsoft ['msrequestfullscreen', 'msexitfullscreen', 'msfullscreenelement', 'msfullscreenenabled', 'msfullscreenchange', 'msfullscreenerror']]; var specapi = apimap[0]; var browserapi; // determine the supported set of functions for (var 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 (var _i = 0; _i < browserapi.length; _i++) { fullscreenapi[specapi[_i]] = browserapi[_i]; } } /** * @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; } 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; /** * 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.' }; // add types as properties on mediaerror // e.g. mediaerror.media_err_src_not_supported = 4; for (var errnum = 0; errnum < mediaerror.errortypes.length; errnum++) { mediaerror[mediaerror.errortypes[errnum]] = errnum; // values should be accessible on both the class and instance mediaerror.prototype[mediaerror.errortypes[errnum]] = errnum; } // jsdocs for instance/static members added above 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, function (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 */ var tracktojson_ = function tracktojson_(track) { var ret = ['kind', 'label', 'language', 'id', 'inbandmetadatatrackdispatchtype', 'mode', 'src'].reduce(function (acc, prop, i) { if (track[prop]) { acc[prop] = track[prop]; } return acc; }, { cues: track.cues && array.prototype.map.call(track.cues, function (cue) { return { starttime: cue.starttime, endtime: cue.endtime, text: cue.text, id: cue.id }; }) }); return ret; }; /** * examine a {@link tech} and return a json-compatible javascript array that represents the * state of all {@link texttrack}s currently configured. the return array is compatible with * {@link text-track-list-converter:jsontotexttracks}. * * @param {tech} tech * the tech object to query * * @return {array} * a serializable javascript representation of the {@link tech}s * {@link texttracklist}. */ var texttrackstojson = function texttrackstojson(tech) { var trackels = tech.$$('track'); var trackobjs = array.prototype.map.call(trackels, function (t) { return t.track; }); var tracks = array.prototype.map.call(trackels, function (trackel) { var 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. */ var jsontotexttracks = function jsontotexttracks(json, tech) { json.foreach(function (track) { var addedtrack = tech.addremotetexttrack(track).track; if (!track.src && track.cues) { track.cues.foreach(function (cue) { return addedtrack.addcue(cue); }); } }); return tech.texttracks(); }; var texttrackconverter = { texttrackstojson: texttrackstojson, jsontotexttracks: jsontotexttracks, tracktojson_: tracktojson_ }; var modal_class_name = 'vjs-modal-dialog'; var esc = 27; /** * 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 */ var modaldialog = /*#__pure__*/ function (_component) { _inheritsloose(modaldialog, _component); /** * create an instance of this class. * * @param {player} player * the `player` that this class should be attached to. * * @param {object} [options] * the key/value store of player options. * * @param {mixed} [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.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. */ function modaldialog(player, options) { var _this; _this = _component.call(this, player, options) || this; _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_); return _this; } /** * create the `modaldialog`'s dom element * * @return {element} * the dom element that gets created. */ var _proto = modaldialog.prototype; _proto.createel = function createel$$1() { return _component.prototype.createel.call(this, 'div', { classname: this.buildcssclass(), tabindex: -1 }, { 'aria-describedby': this.id() + "_description", 'aria-hidden': 'true', 'aria-label': this.label(), 'role': 'dialog' }); }; _proto.dispose = function dispose() { this.contentel_ = null; this.descel_ = null; this.previouslyactiveel_ = null; _component.prototype.dispose.call(this); }; /** * builds the default dom `classname`. * * @return {string} * the dom `classname` for this object. */ _proto.buildcssclass = function buildcssclass() { return modal_class_name + " vjs-hidden " + _component.prototype.buildcssclass.call(this); }; /** * handles `keydown` events on the document, looking for esc, which closes * the modal. * * @param {eventtarget~event} e * the keypress that triggered this event. * * @listens keydown */ _proto.handlekeypress = function handlekeypress(e) { if (e.which === esc && this.closeable()) { this.close(); } }; /** * returns the label string for this modal. primarily used for accessibility. * * @return {string} * the localized or raw label of this modal. */ _proto.label = function 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. */ _proto.description = function description() { var 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 */ _proto.open = function open() { if (!this.opened_) { var player = this.player(); /** * fired just before a `modaldialog` is opened. * * @event modaldialog#beforemodalopen * @type {eventtarget~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(); } if (this.closeable()) { this.on(this.el_.ownerdocument, 'keydown', bind(this, this.handlekeypress)); } // 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 {eventtarget~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 */ _proto.opened = function 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 */ _proto.close = function close() { if (!this.opened_) { return; } var player = this.player(); /** * fired just before a `modaldialog` is closed. * * @event modaldialog#beforemodalclose * @type {eventtarget~event} */ this.trigger('beforemodalclose'); this.opened_ = false; if (this.wasplaying_ && this.options_.pauseonopen) { player.play(); } if (this.closeable()) { this.off(this.el_.ownerdocument, 'keydown', bind(this, this.handlekeypress)); } 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 {eventtarget~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. */ _proto.closeable = function closeable(value) { if (typeof value === 'boolean') { var closeable = this.closeable_ = !!value; var 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. var 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. */ _proto.fill = function 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 {mixed} [content] * the same rules apply to this as apply to the `content` option. */ _proto.fillwith = function fillwith(content) { var contentel = this.contentel(); var parentel = contentel.parentnode; var nextsiblingel = contentel.nextsibling; /** * fired just before a `modaldialog` is filled with content. * * @event modaldialog#beforemodalfill * @type {eventtarget~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 {eventtarget~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 var 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 */ _proto.empty = function empty() { /** * fired just before a `modaldialog` is emptied. * * @event modaldialog#beforemodalempty * @type {eventtarget~event} */ this.trigger('beforemodalempty'); emptyel(this.contentel()); /** * fired just after a `modaldialog` is emptied. * * @event modaldialog#modalempty * @type {eventtarget~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 {mixed} [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 {mixed} * the current content of the modal dialog */ _proto.content = function 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 */ _proto.conditionalfocus_ = function conditionalfocus_() { var activeel = document.activeelement; var playerel = this.player_.el_; this.previouslyactiveel_ = null; if (playerel.contains(activeel) || playerel === activeel) { this.previouslyactiveel_ = activeel; this.focus(); this.on(document, 'keydown', this.handlekeydown); } }; /** * conditionally blur the element and refocus the last focused element * * @private */ _proto.conditionalblur_ = function conditionalblur_() { if (this.previouslyactiveel_) { this.previouslyactiveel_.focus(); this.previouslyactiveel_ = null; } this.off(document, 'keydown', this.handlekeydown); }; /** * keydown handler. attached when modal is focused. * * @listens keydown */ _proto.handlekeydown = function handlekeydown(event) { // exit early if it isn't a tab key if (event.which !== 9) { return; } var focusableels = this.focusableels_(); var activeel = this.el_.queryselector(':focus'); var focusindex; for (var 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 */ _proto.focusableels_ = function focusableels_() { var allchildren = this.el_.queryselectorall('*'); return array.prototype.filter.call(allchildren, function (child) { return (child instanceof window$1.htmlanchorelement || child instanceof window$1.htmlareaelement) && child.hasattribute('href') || (child instanceof window$1.htmlinputelement || child instanceof window$1.htmlselectelement || child instanceof window$1.htmltextareaelement || child instanceof window$1.htmlbuttonelement) && !child.hasattribute('disabled') || child instanceof window$1.htmliframeelement || child instanceof window$1.htmlobjectelement || child instanceof window$1.htmlembedelement || child.hasattribute('tabindex') && child.getattribute('tabindex') !== -1 || child.hasattribute('contenteditable'); }); }; return modaldialog; }(component); /** * default options for `modaldialog` default options. * * @type {object} * @private */ modaldialog.prototype.options_ = { pauseonopen: true, temporary: true }; component.registercomponent('modaldialog', modaldialog); /** * common functionaliy between {@link texttracklist}, {@link audiotracklist}, and * {@link videotracklist} * * @extends eventtarget */ var tracklist = /*#__pure__*/ function (_eventtarget) { _inheritsloose(tracklist, _eventtarget); /** * create an instance of this class * * @param {track[]} tracks * a list of tracks to initialize the list with. * * @abstract */ function tracklist(tracks) { var _this; if (tracks === void 0) { tracks = []; } _this = _eventtarget.call(this) || this; _this.tracks_ = []; /** * @memberof tracklist * @member {number} length * the current number of `track`s in the this trackist. * @instance */ object.defineproperty(_assertthisinitialized(_assertthisinitialized(_this)), 'length', { get: function get() { return this.tracks_.length; } }); for (var i = 0; i < tracks.length; i++) { _this.addtrack(tracks[i]); } return _this; } /** * add a {@link track} to the `tracklist` * * @param {track} track * the audio, video, or text track to add to the list. * * @fires tracklist#addtrack */ var _proto = tracklist.prototype; _proto.addtrack = function addtrack(track) { var index = this.tracks_.length; if (!('' + index in this)) { object.defineproperty(this, index, { get: function 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 {eventtarget~event} * @property {track} track * a reference to track that was added. */ this.trigger({ track: track, type: 'addtrack' }); } }; /** * remove a {@link track} from the `tracklist` * * @param {track} rtrack * the audio, video, or text track to remove from the list. * * @fires tracklist#removetrack */ _proto.removetrack = function removetrack(rtrack) { var track; for (var 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 {eventtarget~event} * @property {track} track * a reference to track that was removed. */ this.trigger({ track: track, type: 'removetrack' }); }; /** * get a track from the tracklist by a tracks id * * @param {string} id - the id of the track to get * @method gettrackbyid * @return {track} * @private */ _proto.gettrackbyid = function gettrackbyid(id) { var result = null; for (var i = 0, l = this.length; i < l; i++) { var track = this[i]; if (track.id === id) { result = track; break; } } return result; }; return tracklist; }(eventtarget); /** * triggered when a different track is selected/enabled. * * @event tracklist#change * @type {eventtarget~event} */ /** * events that can be called with on + eventname. see {@link eventhandler}. * * @property {object} tracklist#allowedevents_ * @private */ tracklist.prototype.allowedevents_ = { change: 'change', addtrack: 'addtrack', removetrack: 'removetrack' }; // emulate attribute eventhandler support to allow for feature detection for (var event in tracklist.prototype.allowedevents_) { tracklist.prototype['on' + event] = null; } /** * anywhere we call this function we diverge from the spec * as we only support one enabled audiotrack at a time * * @param {audiotracklist} list * list to work on * * @param {audiotrack} track * the track to skip * * @private */ var disableothers = function disableothers(list, track) { for (var 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 */ var audiotracklist = /*#__pure__*/ function (_tracklist) { _inheritsloose(audiotracklist, _tracklist); /** * create an instance of this class. * * @param {audiotrack[]} [tracks=[]] * a list of `audiotrack` to instantiate the list with. */ function audiotracklist(tracks) { var _this; if (tracks === void 0) { tracks = []; } // make sure only 1 track is enabled // sorted from last index to first index for (var i = tracks.length - 1; i >= 0; i--) { if (tracks[i].enabled) { disableothers(tracks, tracks[i]); break; } } _this = _tracklist.call(this, tracks) || this; _this.changing_ = false; return _this; } /** * add an {@link audiotrack} to the `audiotracklist`. * * @param {audiotrack} track * the audiotrack to add to the list * * @fires tracklist#addtrack */ var _proto = audiotracklist.prototype; _proto.addtrack = function addtrack(track) { var _this2 = this; if (track.enabled) { disableothers(this, track); } _tracklist.prototype.addtrack.call(this, track); // native tracks don't have this if (!track.addeventlistener) { return; } /** * @listens audiotrack#enabledchange * @fires tracklist#change */ track.addeventlistener('enabledchange', function () { // 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 (_this2.changing_) { return; } _this2.changing_ = true; disableothers(_this2, track); _this2.changing_ = false; _this2.trigger('change'); }); }; return audiotracklist; }(tracklist); /** * un-select all other {@link videotrack}s that are selected. * * @param {videotracklist} list * list to work on * * @param {videotrack} track * the track to skip * * @private */ var disableothers$1 = function disableothers(list, track) { for (var 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 */ var videotracklist = /*#__pure__*/ function (_tracklist) { _inheritsloose(videotracklist, _tracklist); /** * create an instance of this class. * * @param {videotrack[]} [tracks=[]] * a list of `videotrack` to instantiate the list with. */ function videotracklist(tracks) { var _this; if (tracks === void 0) { tracks = []; } // make sure only 1 track is enabled // sorted from last index to first index for (var i = tracks.length - 1; i >= 0; i--) { if (tracks[i].selected) { disableothers$1(tracks, tracks[i]); break; } } _this = _tracklist.call(this, tracks) || this; _this.changing_ = false; /** * @member {number} videotracklist#selectedindex * the current index of the selected {@link videotrack`}. */ object.defineproperty(_assertthisinitialized(_assertthisinitialized(_this)), 'selectedindex', { get: function get() { for (var _i = 0; _i < this.length; _i++) { if (this[_i].selected) { return _i; } } return -1; }, set: function set() {} }); return _this; } /** * add a {@link videotrack} to the `videotracklist`. * * @param {videotrack} track * the videotrack to add to the list * * @fires tracklist#addtrack */ var _proto = videotracklist.prototype; _proto.addtrack = function addtrack(track) { var _this2 = this; if (track.selected) { disableothers$1(this, track); } _tracklist.prototype.addtrack.call(this, track); // native tracks don't have this if (!track.addeventlistener) { return; } /** * @listens videotrack#selectedchange * @fires tracklist#change */ track.addeventlistener('selectedchange', function () { if (_this2.changing_) { return; } _this2.changing_ = true; disableothers$1(_this2, track); _this2.changing_ = false; _this2.trigger('change'); }); }; return videotracklist; }(tracklist); /** * 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 */ var texttracklist = /*#__pure__*/ function (_tracklist) { _inheritsloose(texttracklist, _tracklist); function texttracklist() { return _tracklist.apply(this, arguments) || this; } var _proto = texttracklist.prototype; /** * add a {@link texttrack} to the `texttracklist` * * @param {texttrack} track * the text track to add to the list. * * @fires tracklist#addtrack */ _proto.addtrack = function addtrack(track) { _tracklist.prototype.addtrack.call(this, track); /** * @listens texttrack#modechange * @fires tracklist#change */ track.addeventlistener('modechange', bind(this, function () { this.queuetrigger('change'); })); var nonlanguagetexttrackkind = ['metadata', 'chapters']; if (nonlanguagetexttrackkind.indexof(track.kind) === -1) { track.addeventlistener('modechange', bind(this, function () { this.trigger('selectedlanguagechange'); })); } }; return texttracklist; }(tracklist); /** * @file html-track-element-list.js */ /** * the current list of {@link htmltrackelement}s. */ var htmltrackelementlist = /*#__pure__*/ function () { /** * create an instance of this class. * * @param {htmltrackelement[]} [tracks=[]] * a list of `htmltrackelement` to instantiate the list with. */ function htmltrackelementlist(trackelements) { if (trackelements === void 0) { trackelements = []; } this.trackelements_ = []; /** * @memberof htmltrackelementlist * @member {number} length * the current number of `track`s in the this trackist. * @instance */ object.defineproperty(this, 'length', { get: function get() { return this.trackelements_.length; } }); for (var 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 */ var _proto = htmltrackelementlist.prototype; _proto.addtrackelement_ = function addtrackelement_(trackelement) { var index = this.trackelements_.length; if (!('' + index in this)) { object.defineproperty(this, index, { get: function 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 */ _proto.gettrackelementbytrack_ = function gettrackelementbytrack_(track) { var trackelement_; for (var 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 */ _proto.removetrackelement_ = function removetrackelement_(trackelement) { for (var i = 0, length = this.trackelements_.length; i < length; i++) { if (trackelement === this.trackelements_[i]) { this.trackelements_.splice(i, 1); break; } } }; return htmltrackelementlist; }(); /** * @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} */ var texttrackcuelist = /*#__pure__*/ function () { /** * create an instance of this class.. * * @param {array} cues * a list of cues to be initialized with */ function texttrackcuelist(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: function 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 */ var _proto = texttrackcuelist.prototype; _proto.setcues_ = function setcues_(cues) { var oldlength = this.length || 0; var i = 0; var l = cues.length; this.cues_ = cues; this.length_ = cues.length; var defineprop = function defineprop(index) { if (!('' + index in this)) { object.defineproperty(this, '' + index, { get: function 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. */ _proto.getcuebyid = function getcuebyid(id) { var result = null; for (var i = 0, l = this.length; i < l; i++) { var cue = this[i]; if (cue.id === id) { result = cue; break; } } return result; }; return texttrackcuelist; }(); /** * @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 */ var 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 */ var 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 */ var 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 */ var texttrackmode = { disabled: 'disabled', hidden: 'hidden', showing: 'showing' }; /** * 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 */ var track = /*#__pure__*/ function (_eventtarget) { _inheritsloose(track, _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 */ function track(options) { var _this; if (options === void 0) { options = {}; } _this = _eventtarget.call(this) || this; var trackprops = { id: options.id || 'vjs_track_' + newguid(), kind: options.kind || '', label: options.label || '', language: options.language || '' }; /** * @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} label * the label of this track. 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 */ var _loop = function _loop(key) { object.defineproperty(_assertthisinitialized(_assertthisinitialized(_this)), key, { get: function get() { return trackprops[key]; }, set: function set() {} }); }; for (var key in trackprops) { _loop(key); } return _this; } return track; }(eventtarget); /** * @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 */ var parseurl = function parseurl(url) { var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; // add the url to an anchor and let the browser parse the url var a = document.createelement('a'); a.href = url; // ie8 (and 9?) fix // ie8 doesn't parse the url correctly until the anchor is actually // added to the body, and an innerhtml is needed to trigger the parsing var addtobody = a.host === '' && a.protocol !== 'file:'; var div; if (addtobody) { div = document.createelement('div'); div.innerhtml = ""; a = div.firstchild; // prevent the div from affecting layout div.setattribute('style', 'display:none; position:absolute;'); document.body.appendchild(div); } // copy the specific url properties to a new object // this is also needed for ie8 because the anchor loses its // properties when it's removed from the dom var details = {}; for (var i = 0; i < props.length; i++) { details[props[i]] = a[props[i]]; } // ie9 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$1.location.protocol; } if (addtobody) { document.body.removechild(div); } return details; }; /** * get absolute version of relative url. used to tell flash the correct 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 */ var getabsoluteurl = function getabsoluteurl(url) { // check if absolute url if (!url.match(/^https?:\/\//)) { // convert to absolute url. flash hosted off-site needs an absolute url. var div = document.createelement('div'); div.innerhtml = "x"; url = div.firstchild.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' * * @returns {string} * the extension in lower case or an empty string if no * extension could be found. */ var getfileextension = function getfileextension(path) { if (typeof path === 'string') { var splitpathre = /^(\/?)([\s\s]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i; var 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. * * @return {boolean} * whether it is a cross domain request or not. */ var iscrossorigin = function iscrossorigin(url) { var winloc = window$1.location; var urlinfo = parseurl(url); // ie8 protocol relative urls will return ':' for protocol var 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 var crossorigin = srcprotocol + urlinfo.host !== winloc.protocol + winloc.host; return crossorigin; }; var url = /*#__pure__*/object.freeze({ parseurl: parseurl, getabsoluteurl: getabsoluteurl, getfileextension: getfileextension, iscrossorigin: iscrossorigin }); var isfunction_1 = isfunction; var tostring$1 = object.prototype.tostring; function isfunction(fn) { var string = tostring$1.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 commonjsglobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createcommonjsmodule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var trim_1 = createcommonjsmodule(function (module, exports) { exports = module.exports = trim; function trim(str) { return str.replace(/^\s*|\s*$/g, ''); } exports.left = function (str) { return str.replace(/^\s*/, ''); }; exports.right = function (str) { return str.replace(/\s*$/, ''); }; }); var trim_2 = trim_1.left; var trim_3 = trim_1.right; var fntostr = function.prototype.tostring; var constructorregex = /^\s*class\b/; var ises6classfn = function ises6classfunction(value) { try { var fnstr = fntostr.call(value); return constructorregex.test(fnstr); } catch (e) { return false; // not a function } }; var tryfunctionobject = function tryfunctiontostr(value) { try { if (ises6classfn(value)) { return false; } fntostr.call(value); return true; } catch (e) { return false; } }; var tostr = object.prototype.tostring; var fnclass = '[object function]'; var genclass = '[object generatorfunction]'; var hastostringtag = typeof symbol === 'function' && typeof symbol.tostringtag === 'symbol'; var iscallable = function iscallable(value) { if (!value) { return false; } if (typeof value !== 'function' && typeof value !== 'object') { return false; } if (typeof value === 'function' && !value.prototype) { return true; } if (hastostringtag) { return tryfunctionobject(value); } if (ises6classfn(value)) { return false; } var strclass = tostr.call(value); return strclass === fnclass || strclass === genclass; }; var tostr$1 = object.prototype.tostring; var hasownproperty = object.prototype.hasownproperty; var foreacharray = function foreacharray(array, iterator, receiver) { for (var i = 0, len = array.length; i < len; i++) { if (hasownproperty.call(array, i)) { if (receiver == null) { iterator(array[i], i, array); } else { iterator.call(receiver, array[i], i, array); } } } }; var foreachstring = function foreachstring(string, iterator, receiver) { for (var i = 0, len = string.length; i < len; i++) { // no such thing as a sparse string. if (receiver == null) { iterator(string.charat(i), i, string); } else { iterator.call(receiver, string.charat(i), i, string); } } }; var foreachobject = function foreachobject(object, iterator, receiver) { for (var k in object) { if (hasownproperty.call(object, k)) { if (receiver == null) { iterator(object[k], k, object); } else { iterator.call(receiver, object[k], k, object); } } } }; var foreach = function foreach(list, iterator, thisarg) { if (!iscallable(iterator)) { throw new typeerror('iterator must be a function'); } var receiver; if (arguments.length >= 3) { receiver = thisarg; } if (tostr$1.call(list) === '[object array]') { foreacharray(list, iterator, receiver); } else if (typeof list === 'string') { foreachstring(list, iterator, receiver); } else { foreachobject(list, iterator, receiver); } }; var foreach_1 = foreach; var isarray = function isarray(arg) { return object.prototype.tostring.call(arg) === '[object array]'; }; var parseheaders = function parseheaders(headers) { if (!headers) return {}; var result = {}; foreach_1(trim_1(headers).split('\n'), function (row) { var index = row.indexof(':'), key = trim_1(row.slice(0, index)).tolowercase(), value = trim_1(row.slice(index + 1)); if (typeof result[key] === 'undefined') { result[key] = value; } else if (isarray(result[key])) { result[key].push(value); } else { result[key] = [result[key], value]; } }); return result; }; var immutable = extend; var hasownproperty$1 = object.prototype.hasownproperty; function extend() { var target = {}; for (var i = 0; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (hasownproperty$1.call(source, key)) { target[key] = source[key]; } } } return target; } var xhr = createxhr; createxhr.xmlhttprequest = window$1.xmlhttprequest || noop; createxhr.xdomainrequest = "withcredentials" in new createxhr.xmlhttprequest() ? createxhr.xmlhttprequest : window$1.xdomainrequest; foreacharray$1(["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$1(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 = immutable(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) { if (xhr.responsetype === "document") { return xhr.responsexml; } var firefoxbugtakeneffect = xhr.responsexml && xhr.responsexml.documentelement.nodename === "parsererror"; if (xhr.responsetype === "" && !firefoxbugtakeneffect) { return xhr.responsexml; } return null; } function noop() {} /** * 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 */ var parsecues = function parsecues(srccontent, track) { var parser = new window$1.webvtt.parser(window$1, window$1.vttjs, window$1.webvtt.stringdecoder()); var 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$1.console && window$1.console.groupcollapsed) { window$1.console.groupcollapsed("text track parsing errors for " + track.src); } errors.foreach(function (error) { return log.error(error); }); if (window$1.console && window$1.console.groupend) { window$1.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 */ var loadtrack = function loadtrack(src, track) { var opts = { uri: src }; var crossorigin = iscrossorigin(src); if (crossorigin) { opts.cors = crossorigin; } xhr(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$1.webvtt !== 'function') { if (track.tech_) { var loadhandler = function loadhandler() { return parsecues(responsebody, track); }; track.tech_.on('vttjsloaded', loadhandler); track.tech_.on('vttjserror', function () { log.error("vttjs failed to load, stopping trying to process " + track.src); track.tech_.off('vttjsloaded', loadhandler); }); } } 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 */ var texttrack = /*#__pure__*/ function (_track) { _inheritsloose(texttrack, _track); /** * create an instance of this class. * * @param {object} options={} * object of option names and values * * @param {tech} options.tech * a reference to the tech that owns this texttrack. * * @param {texttrack~kind} [options.kind='subtitles'] * a valid text track kind. * * @param {texttrack~mode} [options.mode='disabled'] * a valid text track mode. * * @param {string} [options.id='vjs_track_' + guid.newguid()] * a unique id for this texttrack. * * @param {string} [options.label=''] * the menu label for this track. * * @param {string} [options.language=''] * a valid two character language code. * * @param {string} [options.srclang=''] * a valid two character language code. an alternative, but deprioritized * version of `options.language` * * @param {string} [options.src] * a url to texttrack cues. * * @param {boolean} [options.default] * if this track should default to on or off. */ function texttrack(options) { var _this; if (options === void 0) { options = {}; } if (!options.tech) { throw new error('a tech was not provided.'); } var settings = mergeoptions(options, { kind: texttrackkind[options.kind] || 'subtitles', language: options.language || options.srclang || '' }); var mode = texttrackmode[settings.mode] || 'disabled'; var default_ = settings.default; if (settings.kind === 'metadata' || settings.kind === 'chapters') { mode = 'hidden'; } _this = _track.call(this, settings) || this; _this.tech_ = settings.tech; _this.cues_ = []; _this.activecues_ = []; var cues = new texttrackcuelist(_this.cues_); var activecues = new texttrackcuelist(_this.activecues_); var changed = false; var timeupdatehandler = bind(_assertthisinitialized(_assertthisinitialized(_this)), function () { // accessing this.activecues for the side-effects of updating itself // due to it's 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 (mode !== 'disabled') { _this.tech_.ready(function () { _this.tech_.on('timeupdate', timeupdatehandler); }, true); } object.defineproperties(_assertthisinitialized(_assertthisinitialized(_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: function get() { return default_; }, set: function 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: function get() { return mode; }, set: function set(newmode) { var _this2 = this; if (!texttrackmode[newmode]) { return; } mode = newmode; if (mode !== 'disabled') { this.tech_.ready(function () { _this2.tech_.on('timeupdate', timeupdatehandler); }, true); } else { this.tech_.off('timeupdate', timeupdatehandler); } /** * 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 {eventtarget~event} */ this.trigger('modechange'); } }, /** * @memberof texttrack * @member {texttrackcuelist} cues * the text track cue list for this texttrack. * @instance */ cues: { get: function get() { if (!this.loaded_) { return null; } return cues; }, set: function set() {} }, /** * @memberof texttrack * @member {texttrackcuelist} activecues * the list text track cues that are currently active for this texttrack. * @instance */ activecues: { get: function get() { if (!this.loaded_) { return null; } // nothing to do if (this.cues.length === 0) { return activecues; } var ct = this.tech_.currenttime(); var active = []; for (var i = 0, l = this.cues.length; i < l; i++) { var cue = this.cues[i]; if (cue.starttime <= ct && cue.endtime >= ct) { active.push(cue); } else if (cue.starttime === cue.endtime && cue.starttime <= ct && cue.starttime + 0.5 >= ct) { active.push(cue); } } changed = false; if (active.length !== this.activecues_.length) { changed = true; } else { for (var _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: function set() {} } }); if (settings.src) { _this.src = settings.src; loadtrack(settings.src, _assertthisinitialized(_assertthisinitialized(_this))); } else { _this.loaded_ = true; } return _this; } /** * add a cue to the internal list of cues. * * @param {texttrack~cue} cue * the cue to add to our internal list */ var _proto = texttrack.prototype; _proto.addcue = function addcue(originalcue) { var cue = originalcue; if (window$1.vttjs && !(originalcue instanceof window$1.vttjs.vttcue)) { cue = new window$1.vttjs.vttcue(originalcue.starttime, originalcue.endtime, originalcue.text); for (var 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; } var tracks = this.tech_.texttracks(); for (var 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 */ _proto.removecue = function removecue(_removecue) { var i = this.cues_.length; while (i--) { var cue = this.cues_[i]; if (cue === _removecue || cue.originalcue_ && cue.originalcue_ === _removecue) { this.cues_.splice(i, 1); this.cues.setcues_(this.cues_); break; } } }; return texttrack; }(track); /** * cuechange - one or more cues in the track have become active or stopped being active. */ 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 */ var audiotrack = /*#__pure__*/ function (_track) { _inheritsloose(audiotrack, _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. */ function audiotrack(options) { var _this; if (options === void 0) { options = {}; } var settings = mergeoptions(options, { kind: audiotrackkind[options.kind] || '' }); _this = _track.call(this, settings) || this; var 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(_assertthisinitialized(_assertthisinitialized(_this)), 'enabled', { get: function get() { return enabled; }, set: function 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 {eventtarget~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; return _this; } return audiotrack; }(track); /** * a representation of a single `videotrack`. * * @see [spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} * @extends track */ var videotrack = /*#__pure__*/ function (_track) { _inheritsloose(videotrack, _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. */ function videotrack(options) { var _this; if (options === void 0) { options = {}; } var settings = mergeoptions(options, { kind: videotrackkind[options.kind] || '' }); _this = _track.call(this, settings) || this; var 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(_assertthisinitialized(_assertthisinitialized(_this)), 'selected', { get: function get() { return selected; }, set: function 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 {eventtarget~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; } return _this; } return videotrack; }(track); /** * @memberof htmltrackelement * @typedef {htmltrackelement~readystate} * @enum {number} */ var none = 0; var loading = 1; var loaded = 2; var error = 3; /** * a single track represented in the dom. * * @see [spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} * @extends eventtarget */ var htmltrackelement = /*#__pure__*/ function (_eventtarget) { _inheritsloose(htmltrackelement, _eventtarget); /** * create an instance of this class. * * @param {object} options={} * object of option names and values * * @param {tech} options.tech * a reference to the tech that owns this htmltrackelement. * * @param {texttrack~kind} [options.kind='subtitles'] * a valid text track kind. * * @param {texttrack~mode} [options.mode='disabled'] * a valid text track mode. * * @param {string} [options.id='vjs_track_' + guid.newguid()] * a unique id for this texttrack. * * @param {string} [options.label=''] * the menu label for this track. * * @param {string} [options.language=''] * a valid two character language code. * * @param {string} [options.srclang=''] * a valid two character language code. an alternative, but deprioritized * vesion 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. */ function htmltrackelement(options) { var _this; if (options === void 0) { options = {}; } _this = _eventtarget.call(this) || this; var readystate; var 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(_assertthisinitialized(_assertthisinitialized(_this)), { /** * @memberof htmltrackelement * @member {htmltrackelement~readystate} readystate * the current ready state of the track element. * @instance */ readystate: { get: function get() { return readystate; } }, /** * @memberof htmltrackelement * @member {texttrack} track * the underlying texttrack object. * @instance * */ track: { get: function get() { return track; } } }); readystate = none; /** * @listens texttrack#loadeddata * @fires htmltrackelement#load */ track.addeventlistener('loadeddata', function () { readystate = loaded; _this.trigger({ type: 'load', target: _assertthisinitialized(_assertthisinitialized(_this)) }); }); return _this; } return htmltrackelement; }(eventtarget); htmltrackelement.prototype.allowedevents_ = { load: 'load' }; htmltrackelement.none = none; htmltrackelement.loading = loading; htmltrackelement.loaded = loaded; htmltrackelement.error = error; /* * this file contains all track properties that are used in * player.js, tech.js, html5.js and possibly other techs in the future. */ var 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_"; }); var remote = { remotetext: { listclass: texttracklist, trackclass: texttrack, capitalname: 'remotetext', gettername: 'remotetexttracks', privatename: 'remotetexttracks_' }, remotetextel: { listclass: htmltrackelementlist, trackclass: htmltrackelement, capitalname: 'remotetexttrackels', gettername: 'remotetexttrackels', privatename: 'remotetexttrackels_' } }; var all = mergeoptions(normal, remote); remote.names = object.keys(remote); normal.names = object.keys(normal); all.names = [].concat(remote.names).concat(normal.names); /** * 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{2})(:\d{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 set(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 get(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 has(k) { return k in this.values; }, // accept a setting if its one of the given alternatives. alt: function alt(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 integer(k, v) { if (/^-?\d+$/.test(v)) { // integer this.set(k, parseint(v, 10)); } }, // accept a setting if its a valid percentage. percent: function percent(k, v) { var m; if (m = 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]; var v = kv[1]; 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", "middle", "end"]); } break; case "position": vals = v.split(","); settings.percent(k, vals[0]); if (vals.length === 2) { settings.alt("positionalign", vals[1], ["start", "middle", "end"]); } break; case "size": settings.percent(k, v); break; case "align": settings.alt(k, v, ["start", "middle", "end", "left", "right"]); break; } }, /:/, /\s/); // apply default values for any missing fields. cue.region = settings.get("region", null); cue.vertical = settings.get("vertical", ""); cue.line = settings.get("line", "auto"); cue.linealign = settings.get("linealign", "start"); cue.snaptolines = settings.get("snaptolines", true); cue.size = settings.get("size", 100); cue.align = settings.get("align", "middle"); cue.position = settings.get("position", { start: 0, left: 0, middle: 50, end: 100, right: 100 }, cue.align); cue.positionalign = settings.get("positionalign", { start: "start", left: "start", middle: "middle", 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); } var escape = { "&": "&", "<": "<", ">": ">", "‎": "\u200e", "‏": "\u200f", " ": "\xa0" }; var tag_name = { c: "span", i: "i", b: "b", u: "u", ruby: "ruby", rt: "rt", v: "span", lang: "span" }; 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]); } // unescape a string 's'. function unescape1(e) { return escape[e]; } function unescape(s) { while (m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/)) { s = s.replace(m[0], unescape1); } 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); element.localname = 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]) { node.classname = m[2].substr(1).replace('.', ' '); } // 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" which is "center" in css. 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": textpos = cue.position; break; case "middle": textpos = cue.position - cue.size / 2; break; case "end": 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 "middle": 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 decode(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 reportorthrowerror(e) { if (e instanceof parsingerror) { this.onparsingerror && this.onparsingerror(e); } else { throw e; } }, parse: function parse(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, ""); 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; 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 flush() { 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, "middle": 1, "end": 1, "left": 1, "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 = 50; var _positionalign = "middle"; var _size = 50; var _align = "middle"; object.defineproperties(this, { "id": { enumerable: true, get: function get() { return _id; }, set: function set(value) { _id = "" + value; } }, "pauseonexit": { enumerable: true, get: function get() { return _pauseonexit; }, set: function set(value) { _pauseonexit = !!value; } }, "starttime": { enumerable: true, get: function get() { return _starttime; }, set: function set(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 get() { return _endtime; }, set: function set(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 get() { return _text; }, set: function set(value) { _text = "" + value; this.hasbeenreset = true; } }, "region": { enumerable: true, get: function get() { return _region; }, set: function set(value) { _region = value; this.hasbeenreset = true; } }, "vertical": { enumerable: true, get: function get() { return _vertical; }, set: function set(value) { var setting = finddirectionsetting(value); // have to check for false because the setting an be an empty string. if (setting === false) { throw new syntaxerror("an invalid or illegal string was specified."); } _vertical = setting; this.hasbeenreset = true; } }, "snaptolines": { enumerable: true, get: function get() { return _snaptolines; }, set: function set(value) { _snaptolines = !!value; this.hasbeenreset = true; } }, "line": { enumerable: true, get: function get() { return _line; }, set: function set(value) { if (typeof value !== "number" && value !== autokeyword) { throw new syntaxerror("an invalid number or illegal string was specified."); } _line = value; this.hasbeenreset = true; } }, "linealign": { enumerable: true, get: function get() { return _linealign; }, set: function set(value) { var setting = findalignsetting(value); if (!setting) { throw new syntaxerror("an invalid or illegal string was specified."); } _linealign = setting; this.hasbeenreset = true; } }, "position": { enumerable: true, get: function get() { return _position; }, set: function set(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 get() { return _positionalign; }, set: function set(value) { var setting = findalignsetting(value); if (!setting) { throw new syntaxerror("an invalid or illegal string was specified."); } _positionalign = setting; this.hasbeenreset = true; } }, "size": { enumerable: true, get: function get() { return _size; }, set: function set(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 get() { return _align; }, set: function set(value) { var setting = findalignsetting(value); if (!setting) { throw new syntaxerror("an invalid or illegal string was specified."); } _align = setting; this.hasbeenreset = true; } } }); /** * other spec defined properties */ // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state this.displaystate = undefined; } /** * vttcue methods */ vttcue.prototype.getcueashtml = function () { // assume webvtt.convertcuetodomtree is on the global. return webvtt.convertcuetodomtree(window, this.text); }; var vttcue = vttcue; /** * 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 scrollsetting = { "": true, "up": true }; function findscrollsetting(value) { if (typeof value !== "string") { return false; } var scroll = scrollsetting[value.tolowercase()]; return scroll ? value.tolowercase() : false; } function isvalidpercentvalue(value) { return typeof value === "number" && value >= 0 && value <= 100; } // vttregion shim http://dev.w3.org/html5/webvtt/#vttregion-interface function vttregion() { var _width = 100; var _lines = 3; var _regionanchorx = 0; var _regionanchory = 100; var _viewportanchorx = 0; var _viewportanchory = 100; var _scroll = ""; object.defineproperties(this, { "width": { enumerable: true, get: function get() { return _width; }, set: function set(value) { if (!isvalidpercentvalue(value)) { throw new error("width must be between 0 and 100."); } _width = value; } }, "lines": { enumerable: true, get: function get() { return _lines; }, set: function set(value) { if (typeof value !== "number") { throw new typeerror("lines must be set to a number."); } _lines = value; } }, "regionanchory": { enumerable: true, get: function get() { return _regionanchory; }, set: function set(value) { if (!isvalidpercentvalue(value)) { throw new error("regionanchorx must be between 0 and 100."); } _regionanchory = value; } }, "regionanchorx": { enumerable: true, get: function get() { return _regionanchorx; }, set: function set(value) { if (!isvalidpercentvalue(value)) { throw new error("regionanchory must be between 0 and 100."); } _regionanchorx = value; } }, "viewportanchory": { enumerable: true, get: function get() { return _viewportanchory; }, set: function set(value) { if (!isvalidpercentvalue(value)) { throw new error("viewportanchory must be between 0 and 100."); } _viewportanchory = value; } }, "viewportanchorx": { enumerable: true, get: function get() { return _viewportanchorx; }, set: function set(value) { if (!isvalidpercentvalue(value)) { throw new error("viewportanchorx must be between 0 and 100."); } _viewportanchorx = value; } }, "scroll": { enumerable: true, get: function get() { return _scroll; }, set: function set(value) { var setting = findscrollsetting(value); // have to check for false as an empty string is a legal value. if (setting === false) { throw new syntaxerror("an invalid or illegal string was specified."); } _scroll = setting; } } }); } var vttregion = vttregion; var browserindex = createcommonjsmodule(function (module) { /** * 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. */ // default exports for node. export the extended versions of vttcue and // vttregion in node since we likely want the capability to convert back and // forth between json. if we don't then it's not that big of a deal since we're // off browser. var vttjs = module.exports = { webvtt: vtt, vttcue: vttcue, vttregion: vttregion }; window$1.vttjs = vttjs; window$1.webvtt = vttjs.webvtt; var cueshim = vttjs.vttcue; var regionshim = vttjs.vttregion; var nativevttcue = window$1.vttcue; var nativevttregion = window$1.vttregion; vttjs.shim = function () { window$1.vttcue = cueshim; window$1.vttregion = regionshim; }; vttjs.restore = function () { window$1.vttcue = nativevttcue; window$1.vttregion = nativevttregion; }; if (!window$1.vttcue) { vttjs.shim(); } }); var browserindex_1 = browserindex.webvtt; var browserindex_2 = browserindex.vttcue; var browserindex_3 = browserindex.vttregion; /** * an object containing a structure like: `{src: 'url', type: 'mimetype'}` or string * that just contains the src url alone. * * `var sourceobject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` * `var sourcestring = 'http://example.com/some-video.mp4';` * * @typedef {object|string} tech~sourceobject * * @property {string} src * the url to the source * * @property {string} type * the mime type of the source */ /** * a function used by {@link tech} to create a new {@link texttrack}. * * @private * * @param {tech} self * an instance of the tech class. * * @param {string} kind * `texttrack` kind (subtitles, captions, descriptions, chapters, or metadata) * * @param {string} [label] * label to identify the text track * * @param {string} [language] * two letter language abbreviation * * @param {object} [options={}] * an object with additional text track options * * @return {texttrack} * the text track that was created. */ function createtrackhelper(self, kind, label, language, options) { if (options === void 0) { options = {}; } var tracks = self.texttracks(); options.kind = kind; if (label) { options.label = label; } if (language) { options.language = language; } options.tech = self; var track = new all.text.trackclass(options); tracks.addtrack(track); return track; } /** * this is the base class for media playback technology controllers, such as * {@link flash} and {@link html5} * * @extends component */ var tech = /*#__pure__*/ function (_component) { _inheritsloose(tech, _component); /** * create an instance of this tech. * * @param {object} [options] * the key/value store of player options. * * @param {component~readycallback} ready * callback function to call when the `html5` tech is ready. */ function tech(options, ready) { var _this; if (options === void 0) { options = {}; } if (ready === void 0) { ready = function ready() {}; } // we don't want the tech to report user activity automatically. // this is done manually in addcontrolslisteners options.reporttouchactivity = false; _this = _component.call(this, null, options, ready) || this; // keep track of whether the current source has played at all to // implement a very limited played() _this.hasstarted_ = false; _this.on('playing', function () { this.hasstarted_ = true; }); _this.on('loadstart', function () { this.hasstarted_ = false; }); all.names.foreach(function (name) { var props = all[name]; if (options && options[props.gettername]) { _this[props.privatename] = options[props.gettername]; } }); // manually track progress in cases where the browser/flash player doesn't report it. if (!_this.featuresprogressevents) { _this.manualprogresson(); } // manually track timeupdates in cases where the browser/flash player doesn't report it. if (!_this.featurestimeupdateevents) { _this.manualtimeupdateson(); } ['text', 'audio', 'video'].foreach(function (track) { if (options["native" + track + "tracks"] === false) { _this["featuresnative" + track + "tracks"] = false; } }); if (options.nativecaptions === false || options.nativetexttracks === false) { _this.featuresnativetexttracks = false; } else if (options.nativecaptions === true || options.nativetexttracks === true) { _this.featuresnativetexttracks = true; } if (!_this.featuresnativetexttracks) { _this.emulatetexttracks(); } _this.autoremotetexttracks_ = new all.text.listclass(); _this.inittracklisteners(); // turn on component tap events only if not using native controls if (!options.nativecontrolsfortouch) { _this.emittapevents(); } if (_this.constructor) { _this.name_ = _this.constructor.name || 'unknown tech'; } return _this; } /** * a special function to trigger source set in a way that will allow player * to re-trigger if the player or tech are not ready yet. * * @fires tech#sourceset * @param {string} src the source string at the time of the source changing. */ var _proto = tech.prototype; _proto.triggersourceset = function triggersourceset(src) { var _this2 = this; if (!this.isready_) { // on initial ready we have to trigger source set // 1ms after ready so that player can watch for it. this.one('ready', function () { return _this2.settimeout(function () { return _this2.triggersourceset(src); }, 1); }); } /** * fired when the source is set on the tech causing the media element * to reload. * * @see {@link player#event:sourceset} * @event tech#sourceset * @type {eventtarget~event} */ this.trigger({ src: src, type: 'sourceset' }); }; /* fallbacks for unsupported event types ================================================================================ */ /** * polyfill the `progress` event for browsers that don't support it natively. * * @see {@link tech#trackprogress} */ _proto.manualprogresson = function manualprogresson() { this.on('durationchange', this.ondurationchange); this.manualprogress = true; // trigger progress watching when a source begins loading this.one('ready', this.trackprogress); }; /** * turn off the polyfill for `progress` events that was created in * {@link tech#manualprogresson} */ _proto.manualprogressoff = function manualprogressoff() { this.manualprogress = false; this.stoptrackingprogress(); this.off('durationchange', this.ondurationchange); }; /** * this is used to trigger a `progress` event when the buffered percent changes. it * sets an interval function that will be called every 500 milliseconds to check if the * buffer end percent has changed. * * > this function is called by {@link tech#manualprogresson} * * @param {eventtarget~event} event * the `ready` event that caused this to run. * * @listens tech#ready * @fires tech#progress */ _proto.trackprogress = function trackprogress(event) { this.stoptrackingprogress(); this.progressinterval = this.setinterval(bind(this, function () { // don't trigger unless buffered amount is greater than last time var numbufferedpercent = this.bufferedpercent(); if (this.bufferedpercent_ !== numbufferedpercent) { /** * see {@link player#progress} * * @event tech#progress * @type {eventtarget~event} */ this.trigger('progress'); } this.bufferedpercent_ = numbufferedpercent; if (numbufferedpercent === 1) { this.stoptrackingprogress(); } }), 500); }; /** * update our internal duration on a `durationchange` event by calling * {@link tech#duration}. * * @param {eventtarget~event} event * the `durationchange` event that caused this to run. * * @listens tech#durationchange */ _proto.ondurationchange = function ondurationchange(event) { this.duration_ = this.duration(); }; /** * get and create a `timerange` object for buffering. * * @return {timerange} * the time range object that was created. */ _proto.buffered = function buffered() { return createtimeranges(0, 0); }; /** * get the percentage of the current video that is currently buffered. * * @return {number} * a number from 0 to 1 that represents the decimal percentage of the * video that is buffered. * */ _proto.bufferedpercent = function bufferedpercent$$1() { return bufferedpercent(this.buffered(), this.duration_); }; /** * turn off the polyfill for `progress` events that was created in * {@link tech#manualprogresson} * stop manually tracking progress events by clearing the interval that was set in * {@link tech#trackprogress}. */ _proto.stoptrackingprogress = function stoptrackingprogress() { this.clearinterval(this.progressinterval); }; /** * polyfill the `timeupdate` event for browsers that don't support it. * * @see {@link tech#trackcurrenttime} */ _proto.manualtimeupdateson = function manualtimeupdateson() { this.manualtimeupdates = true; this.on('play', this.trackcurrenttime); this.on('pause', this.stoptrackingcurrenttime); }; /** * turn off the polyfill for `timeupdate` events that was created in * {@link tech#manualtimeupdateson} */ _proto.manualtimeupdatesoff = function manualtimeupdatesoff() { this.manualtimeupdates = false; this.stoptrackingcurrenttime(); this.off('play', this.trackcurrenttime); this.off('pause', this.stoptrackingcurrenttime); }; /** * sets up an interval function to track current time and trigger `timeupdate` every * 250 milliseconds. * * @listens tech#play * @triggers tech#timeupdate */ _proto.trackcurrenttime = function trackcurrenttime() { if (this.currenttimeinterval) { this.stoptrackingcurrenttime(); } this.currenttimeinterval = this.setinterval(function () { /** * triggered at an interval of 250ms to indicated that time is passing in the video. * * @event tech#timeupdate * @type {eventtarget~event} */ this.trigger({ type: 'timeupdate', target: this, manuallytriggered: true }); // 42 = 24 fps // 250 is what webkit uses // ff uses 15 }, 250); }; /** * stop the interval function created in {@link tech#trackcurrenttime} so that the * `timeupdate` event is no longer triggered. * * @listens {tech#pause} */ _proto.stoptrackingcurrenttime = function stoptrackingcurrenttime() { this.clearinterval(this.currenttimeinterval); // #1002 - if the video ends right before the next timeupdate would happen, // the progress bar won't make it all the way to the end this.trigger({ type: 'timeupdate', target: this, manuallytriggered: true }); }; /** * turn off all event polyfills, clear the `tech`s {@link audiotracklist}, * {@link videotracklist}, and {@link texttracklist}, and dispose of this tech. * * @fires component#dispose */ _proto.dispose = function dispose() { // clear out all tracks because we can't reuse them between techs this.cleartracks(normal.names); // turn off any manual progress or timeupdate tracking if (this.manualprogress) { this.manualprogressoff(); } if (this.manualtimeupdates) { this.manualtimeupdatesoff(); } _component.prototype.dispose.call(this); }; /** * clear out a single `tracklist` or an array of `tracklists` given their names. * * > note: techs without source handlers should call this between sources for `video` * & `audio` tracks. you don't want to use them between tracks! * * @param {string[]|string} types * tracklist names to clear, valid names are `video`, `audio`, and * `text`. */ _proto.cleartracks = function cleartracks(types) { var _this3 = this; types = [].concat(types); // clear out all tracks because we can't reuse them between techs types.foreach(function (type) { var list = _this3[type + "tracks"]() || []; var i = list.length; while (i--) { var track = list[i]; if (type === 'text') { _this3.removeremotetexttrack(track); } list.removetrack(track); } }); }; /** * remove any texttracks added via addremotetexttrack that are * flagged for automatic garbage collection */ _proto.cleanupautotexttracks = function cleanupautotexttracks() { var list = this.autoremotetexttracks_ || []; var i = list.length; while (i--) { var track = list[i]; this.removeremotetexttrack(track); } }; /** * reset the tech, which will removes all sources and reset the internal readystate. * * @abstract */ _proto.reset = function reset() {}; /** * get or set an error on the tech. * * @param {mediaerror} [err] * error to set on the tech * * @return {mediaerror|null} * the current error object on the tech, or null if there isn't one. */ _proto.error = function error(err) { if (err !== undefined) { this.error_ = new mediaerror(err); this.trigger('error'); } return this.error_; }; /** * returns the `timerange`s that have been played through for the current source. * * > note: this implementation is incomplete. it does not track the played `timerange`. * it only checks whether the source has played at all or not. * * @return {timerange} * - a single time range if this video has played * - an empty set of ranges if not. */ _proto.played = function played() { if (this.hasstarted_) { return createtimeranges(0, 0); } return createtimeranges(); }; /** * causes a manual time update to occur if {@link tech#manualtimeupdateson} was * previously called. * * @fires tech#timeupdate */ _proto.setcurrenttime = function setcurrenttime() { // improve the accuracy of manual timeupdates if (this.manualtimeupdates) { /** * a manual `timeupdate` event. * * @event tech#timeupdate * @type {eventtarget~event} */ this.trigger({ type: 'timeupdate', target: this, manuallytriggered: true }); } }; /** * turn on listeners for {@link videotracklist}, {@link {audiotracklist}, and * {@link texttracklist} events. * * this adds {@link eventtarget~eventlisteners} for `addtrack`, and `removetrack`. * * @fires tech#audiotrackchange * @fires tech#videotrackchange * @fires tech#texttrackchange */ _proto.inittracklisteners = function inittracklisteners() { var _this4 = this; /** * triggered when tracks are added or removed on the tech {@link audiotracklist} * * @event tech#audiotrackchange * @type {eventtarget~event} */ /** * triggered when tracks are added or removed on the tech {@link videotracklist} * * @event tech#videotrackchange * @type {eventtarget~event} */ /** * triggered when tracks are added or removed on the tech {@link texttracklist} * * @event tech#texttrackchange * @type {eventtarget~event} */ normal.names.foreach(function (name) { var props = normal[name]; var tracklistchanges = function tracklistchanges() { _this4.trigger(name + "trackchange"); }; var tracks = _this4[props.gettername](); tracks.addeventlistener('removetrack', tracklistchanges); tracks.addeventlistener('addtrack', tracklistchanges); _this4.on('dispose', function () { tracks.removeeventlistener('removetrack', tracklistchanges); tracks.removeeventlistener('addtrack', tracklistchanges); }); }); }; /** * emulate texttracks using vtt.js if necessary * * @fires tech#vttjsloaded * @fires tech#vttjserror */ _proto.addwebvttscript_ = function addwebvttscript_() { var _this5 = this; if (window$1.webvtt) { return; } // initially, tech.el_ is a child of a dummy-div wait until the component system // signals that the tech is ready at which point tech.el_ is part of the dom // before inserting the webvtt script if (document.body.contains(this.el())) { // load via require if available and vtt.js script location was not passed in // as an option. novtt builds will turn the above require call into an empty object // which will cause this if check to always fail. if (!this.options_['vtt.js'] && isplain(browserindex) && object.keys(browserindex).length > 0) { this.trigger('vttjsloaded'); return; } // load vtt.js via the script location option or the cdn of no location was // passed in var script = document.createelement('script'); script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; script.onload = function () { /** * fired when vtt.js is loaded. * * @event tech#vttjsloaded * @type {eventtarget~event} */ _this5.trigger('vttjsloaded'); }; script.onerror = function () { /** * fired when vtt.js was not loaded due to an error * * @event tech#vttjsloaded * @type {eventtarget~event} */ _this5.trigger('vttjserror'); }; this.on('dispose', function () { script.onload = null; script.onerror = null; }); // but have not loaded yet and we set it to true before the inject so that // we don't overwrite the injected window.webvtt if it loads right away window$1.webvtt = true; this.el().parentnode.appendchild(script); } else { this.ready(this.addwebvttscript_); } }; /** * emulate texttracks * */ _proto.emulatetexttracks = function emulatetexttracks() { var _this6 = this; var tracks = this.texttracks(); var remotetracks = this.remotetexttracks(); var handleaddtrack = function handleaddtrack(e) { return tracks.addtrack(e.track); }; var handleremovetrack = function handleremovetrack(e) { return tracks.removetrack(e.track); }; remotetracks.on('addtrack', handleaddtrack); remotetracks.on('removetrack', handleremovetrack); this.addwebvttscript_(); var updatedisplay = function updatedisplay() { return _this6.trigger('texttrackchange'); }; var texttrackschanges = function texttrackschanges() { updatedisplay(); for (var i = 0; i < tracks.length; i++) { var track = tracks[i]; track.removeeventlistener('cuechange', updatedisplay); if (track.mode === 'showing') { track.addeventlistener('cuechange', updatedisplay); } } }; texttrackschanges(); tracks.addeventlistener('change', texttrackschanges); tracks.addeventlistener('addtrack', texttrackschanges); tracks.addeventlistener('removetrack', texttrackschanges); this.on('dispose', function () { remotetracks.off('addtrack', handleaddtrack); remotetracks.off('removetrack', handleremovetrack); tracks.removeeventlistener('change', texttrackschanges); tracks.removeeventlistener('addtrack', texttrackschanges); tracks.removeeventlistener('removetrack', texttrackschanges); for (var i = 0; i < tracks.length; i++) { var track = tracks[i]; track.removeeventlistener('cuechange', updatedisplay); } }); }; /** * create and returns a remote {@link texttrack} object. * * @param {string} kind * `texttrack` kind (subtitles, captions, descriptions, chapters, or metadata) * * @param {string} [label] * label to identify the text track * * @param {string} [language] * two letter language abbreviation * * @return {texttrack} * the texttrack that gets created. */ _proto.addtexttrack = function addtexttrack(kind, label, language) { if (!kind) { throw new error('texttrack kind is required but was not provided'); } return createtrackhelper(this, kind, label, language); }; /** * create an emulated texttrack for use by addremotetexttrack * * this is intended to be overridden by classes that inherit from * tech in order to create native or custom texttracks. * * @param {object} options * the object should contain the options to initialize the texttrack with. * * @param {string} [options.kind] * `texttrack` kind (subtitles, captions, descriptions, chapters, or metadata). * * @param {string} [options.label]. * label to identify the text track * * @param {string} [options.language] * two letter language abbreviation. * * @return {htmltrackelement} * the track element that gets created. */ _proto.createremotetexttrack = function createremotetexttrack(options) { var track = mergeoptions(options, { tech: this }); return new remote.remotetextel.trackclass(track); }; /** * creates a remote text track object and returns an html track element. * * > note: this can be an emulated {@link htmltrackelement} or a native one. * * @param {object} options * see {@link tech#createremotetexttrack} for more detailed properties. * * @param {boolean} [manualcleanup=true] * - when false: the texttrack will be automatically removed from the video * element whenever the source changes * - when true: the texttrack will have to be cleaned up manually * * @return {htmltrackelement} * an html track element. * * @deprecated the default functionality for this function will be equivalent * to "manualcleanup=false" in the future. the manualcleanup parameter will * also be removed. */ _proto.addremotetexttrack = function addremotetexttrack(options, manualcleanup) { var _this7 = this; if (options === void 0) { options = {}; } var htmltrackelement = this.createremotetexttrack(options); if (manualcleanup !== true && manualcleanup !== false) { // deprecation warning log.warn('calling addremotetexttrack without explicitly setting the "manualcleanup" parameter to `true` is deprecated and default to `false` in future version of video.js'); manualcleanup = true; } // store htmltrackelement and texttrack to remote list this.remotetexttrackels().addtrackelement_(htmltrackelement); this.remotetexttracks().addtrack(htmltrackelement.track); if (manualcleanup !== true) { // create the texttracklist if it doesn't exist this.ready(function () { return _this7.autoremotetexttracks_.addtrack(htmltrackelement.track); }); } return htmltrackelement; }; /** * remove a remote text track from the remote `texttracklist`. * * @param {texttrack} track * `texttrack` to remove from the `texttracklist` */ _proto.removeremotetexttrack = function removeremotetexttrack(track) { var trackelement = this.remotetexttrackels().gettrackelementbytrack_(track); // remove htmltrackelement and texttrack from remote list this.remotetexttrackels().removetrackelement_(trackelement); this.remotetexttracks().removetrack(track); this.autoremotetexttracks_.removetrack(track); }; /** * gets available media playback quality metrics as specified by the w3c's media * playback quality api. * * @see [spec]{@link https://wicg.github.io/media-playback-quality} * * @return {object} * an object with supported media playback quality metrics * * @abstract */ _proto.getvideoplaybackquality = function getvideoplaybackquality() { return {}; }; /** * a method to set a poster from a `tech`. * * @abstract */ _proto.setposter = function setposter() {}; /** * a method to check for the presence of the 'playsinline'